From 076c78aa32f39fe76d74dca79b550f3049e2baa5 Mon Sep 17 00:00:00 2001
From: Matthew Swift <matthew.swift@forgerock.com>
Date: Fri, 25 Nov 2016 18:00:18 +0000
Subject: [PATCH] OPENDJ-3189: Implement EL expression support in cn=config
---
opendj-server-legacy/tests/unit-tests-testng/resource/el-password.pin | 1
opendj-server-legacy/pom.xml | 14 +
opendj-server-legacy/src/main/java/org/opends/server/config/Functions.java | 80 ++++++++
opendj-server-legacy/tests/unit-tests-testng/resource/el-config.properties | 19 +
opendj-server-legacy/src/messages/org/opends/messages/config.properties | 10
opendj-server-legacy/src/main/java/org/opends/server/config/ConfigurationHandler.java | 99 ++++++++-
opendj-server-legacy/src/main/java/org/opends/server/config/Expression.java | 189 ++++++++++++++++++
opendj-server-legacy/src/test/java/org/opends/server/config/ExpressionTest.java | 105 ++++++++++
opendj-server-legacy/src/test/java/org/opends/server/config/FunctionsTest.java | 51 +++++
9 files changed, 553 insertions(+), 15 deletions(-)
diff --git a/opendj-server-legacy/pom.xml b/opendj-server-legacy/pom.xml
index 940f2f8..187b618 100644
--- a/opendj-server-legacy/pom.xml
+++ b/opendj-server-legacy/pom.xml
@@ -67,6 +67,8 @@
</opendj.osgi.import.additional>
<product.archive.name>${product.name.lowercase}-${project.version}</product.archive.name>
+
+ <juel.version>2.2.7</juel.version>
</properties>
<dependencies>
@@ -237,6 +239,18 @@
<artifactId>jcip-annotations</artifactId>
<version>1.0-1</version>
</dependency>
+
+ <dependency>
+ <groupId>de.odysseus.juel</groupId>
+ <artifactId>juel-impl</artifactId>
+ <version>${juel.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>de.odysseus.juel</groupId>
+ <artifactId>juel-api</artifactId>
+ <version>${juel.version}</version>
+ </dependency>
</dependencies>
<build>
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/config/ConfigurationHandler.java b/opendj-server-legacy/src/main/java/org/opends/server/config/ConfigurationHandler.java
index 6517cc8..42fea94 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/config/ConfigurationHandler.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/config/ConfigurationHandler.java
@@ -15,6 +15,7 @@
*/
package org.opends.server.config;
+import static org.forgerock.opendj.ldap.Entries.unmodifiableEntry;
import static org.opends.messages.ConfigMessages.*;
import static org.opends.server.config.ConfigConstants.*;
import static org.opends.server.extensions.ExtensionsConstants.*;
@@ -48,6 +49,7 @@
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.LocalizableMessageBuilder;
+import org.forgerock.i18n.LocalizableMessageDescriptor.Arg4;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.forgerock.opendj.adapter.server3x.Converters;
import org.forgerock.opendj.config.ConfigurationFramework;
@@ -57,6 +59,7 @@
import org.forgerock.opendj.config.server.spi.ConfigChangeListener;
import org.forgerock.opendj.config.server.spi.ConfigDeleteListener;
import org.forgerock.opendj.config.server.spi.ConfigurationRepository;
+import org.forgerock.opendj.ldap.Attribute;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.CancelRequestListener;
import org.forgerock.opendj.ldap.CancelledResultException;
@@ -66,6 +69,8 @@
import org.forgerock.opendj.ldap.Filter;
import org.forgerock.opendj.ldap.LdapException;
import org.forgerock.opendj.ldap.LdapResultHandler;
+import org.forgerock.opendj.ldap.LinkedAttribute;
+import org.forgerock.opendj.ldap.LinkedHashMapEntry;
import org.forgerock.opendj.ldap.MemoryBackend;
import org.forgerock.opendj.ldap.RequestContext;
import org.forgerock.opendj.ldap.ResultCode;
@@ -341,12 +346,8 @@
@Override
public Entry getEntry(final DN dn) throws ConfigException
{
- Entry entry = backend.get(dn);
- if (entry != null)
- {
- entry = Entries.unmodifiableEntry(entry);
- }
- return entry;
+ final Entry entry = backend.get(dn);
+ return entry == null ? null : unmodifiableEntry(evaluateEntryIfPossible(entry));
}
/**
@@ -451,12 +452,15 @@
final DN parentDN = retrieveParentDNForAdd(entryDN);
+ // If the entry contains any expressions then these must be evaluated before passing to listeners.
+ final Entry evaluatedEntry = evaluateEntry(entry, ERR_CONFIG_FILE_ADD_REJECTED_DUE_TO_EVALUATION_FAILURE);
+
// Iterate through add listeners to make sure the new entry is acceptable.
final List<ConfigAddListener> addListeners = getAddListeners(parentDN);
final LocalizableMessageBuilder unacceptableReason = new LocalizableMessageBuilder();
for (final ConfigAddListener listener : addListeners)
{
- if (!listener.configAddIsAcceptable(entry, unacceptableReason))
+ if (!listener.configAddIsAcceptable(evaluatedEntry, unacceptableReason))
{
throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, ERR_CONFIG_FILE_ADD_REJECTED_BY_LISTENER.get(
entryDN, parentDN, unacceptableReason));
@@ -479,7 +483,7 @@
final ConfigChangeResult ccr = new ConfigChangeResult();
for (final ConfigAddListener listener : addListeners)
{
- final ConfigChangeResult result = listener.applyConfigurationAdd(entry);
+ final ConfigChangeResult result = listener.applyConfigurationAdd(evaluatedEntry);
ccr.aggregate(result);
handleConfigChangeResult(result, entry.getName(), listener.getClass().getName(), "applyConfigurationAdd");
}
@@ -532,12 +536,16 @@
final List<ConfigDeleteListener> deleteListeners = getDeleteListeners(parentDN);
final LocalizableMessageBuilder unacceptableReason = new LocalizableMessageBuilder();
final Entry entry = backend.get(dn);
+
+ // If the entry contains any expressions then these must be evaluated before passing to listeners.
+ final Entry evaluatedEntry = evaluateEntry(entry, ERR_CONFIG_FILE_DELETE_REJECTED_DUE_TO_EVALUATION_FAILURE);
+
for (final ConfigDeleteListener listener : deleteListeners)
{
- if (!listener.configDeleteIsAcceptable(entry, unacceptableReason))
+ if (!listener.configDeleteIsAcceptable(evaluatedEntry, unacceptableReason))
{
throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM,
- ERR_CONFIG_FILE_DELETE_REJECTED_BY_LISTENER.get(entry, parentDN, unacceptableReason));
+ ERR_CONFIG_FILE_DELETE_REJECTED_BY_LISTENER.get(dn, parentDN, unacceptableReason));
}
}
@@ -558,7 +566,7 @@
final ConfigChangeResult ccr = new ConfigChangeResult();
for (final ConfigDeleteListener listener : deleteListeners)
{
- final ConfigChangeResult result = listener.applyConfigurationDelete(entry);
+ final ConfigChangeResult result = listener.applyConfigurationDelete(evaluatedEntry);
ccr.aggregate(result);
handleConfigChangeResult(result, dn, listener.getClass().getName(), "applyConfigurationDelete");
}
@@ -600,12 +608,15 @@
ERR_CONFIG_FILE_MODIFY_STRUCTURAL_CHANGE_NOT_ALLOWED.get(oldEntry.getName()));
}
+ // If the entry contains any expressions then these must be evaluated before passing to listeners.
+ final Entry evaluatedNewEntry = evaluateEntry(newEntry, ERR_CONFIG_FILE_MODIFY_REJECTED_DUE_TO_EVALUATION_FAILURE);
+
// Iterate through change listeners to make sure the change is acceptable.
final List<ConfigChangeListener> changeListeners = getChangeListeners(newEntryDN);
final LocalizableMessageBuilder unacceptableReason = new LocalizableMessageBuilder();
for (ConfigChangeListener listeners : changeListeners)
{
- if (!listeners.configChangeIsAcceptable(newEntry, unacceptableReason))
+ if (!listeners.configChangeIsAcceptable(evaluatedNewEntry, unacceptableReason))
{
throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM,
ERR_CONFIG_FILE_MODIFY_REJECTED_BY_CHANGE_LISTENER.get(newEntryDN, unacceptableReason));
@@ -634,7 +645,7 @@
// some listeners may have de-registered themselves due to previous changes, ignore them
continue;
}
- final ConfigChangeResult result = listener.applyConfigurationChange(newEntry);
+ final ConfigChangeResult result = listener.applyConfigurationChange(evaluatedNewEntry);
ccr.aggregate(result);
handleConfigChangeResult(result, newEntryDN, listener.getClass().getName(), "applyConfigurationChange");
}
@@ -1077,7 +1088,7 @@
@Override
public boolean handleEntry(SearchResultEntry entry)
{
- org.opends.server.types.Entry serverEntry = Converters.to(entry);
+ org.opends.server.types.Entry serverEntry = Converters.to(evaluateEntryIfPossible(entry));
serverEntry.processVirtualAttributes();
return !filterMatchesEntry(serverEntry) || searchOperation.returnEntry(serverEntry, null);
}
@@ -1783,4 +1794,64 @@
logger.debug(INFO_CONFIG_CHANGE_RESULT_MESSAGES, className, methodName, entryDN, messages);
}
}
+
+ private static Entry evaluateEntryIfPossible(final Entry entry)
+ {
+ try
+ {
+ return evaluateEntry(entry, ERR_CONFIG_FILE_READ_FAILED_DUE_TO_EVALUATION_FAILURE);
+ }
+ catch (final DirectoryException e)
+ {
+ // The entry contained an invalid expression. Fall-back to returning the original entry.
+ logger.traceException(e);
+ return entry;
+ }
+ }
+
+ private static Entry evaluateEntry(final Entry entry, final Arg4<Object, Object, Object, Object> errMsg)
+ throws DirectoryException
+ {
+ final Entry evaluatedEntry = new LinkedHashMapEntry(entry.getName());
+ for (final Attribute attribute : entry.getAllAttributes())
+ {
+ evaluatedEntry.addAttribute(evaluateAttribute(entry.getName(), attribute, errMsg));
+ }
+ return evaluatedEntry;
+ }
+
+ private static Attribute evaluateAttribute(final DN dn, final Attribute attribute,
+ final Arg4<Object, Object, Object, Object> errMsg)
+ throws DirectoryException
+ {
+ // Skip any attributes which are not config related.
+ if (!attribute.getAttributeDescriptionAsString().startsWith("ds-cfg-"))
+ {
+ return attribute;
+ }
+ final Attribute evaluatedAttribute = new LinkedAttribute(attribute.getAttributeDescription());
+ for (final ByteString value : attribute)
+ {
+ ByteString evaluatedValue = value;
+ for (int i = 0; i < value.length(); i++)
+ {
+ if (value.byteAt(i) == '$')
+ {
+ // Potential expression.
+ try
+ {
+ evaluatedValue = ByteString.valueOfUtf8(Expression.eval(value.toString(), String.class));
+ }
+ catch (final Exception e)
+ {
+ throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM,
+ errMsg.get(dn, attribute.getAttributeDescription(), value, e.getMessage()));
+ }
+ break;
+ }
+ }
+ evaluatedAttribute.add(evaluatedValue);
+ }
+ return evaluatedAttribute;
+ }
}
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/config/Expression.java b/opendj-server-legacy/src/main/java/org/opends/server/config/Expression.java
new file mode 100644
index 0000000..a112b35
--- /dev/null
+++ b/opendj-server-legacy/src/main/java/org/opends/server/config/Expression.java
@@ -0,0 +1,189 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2016 ForgeRock AS.
+ */
+package org.opends.server.config;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.el.ELContext;
+import javax.el.ELException;
+import javax.el.ExpressionFactory;
+import javax.el.ValueExpression;
+
+import org.forgerock.util.Reject;
+
+import de.odysseus.el.ExpressionFactoryImpl;
+import de.odysseus.el.util.RootPropertyResolver;
+import de.odysseus.el.util.SimpleContext;
+import de.odysseus.el.util.SimpleResolver;
+
+/**
+ * A Unified Expression Language read-only expression. Creating an expression is the equivalent to
+ * compiling it. Once created, an expression can be evaluated with an optional set of bindings. Expressions are
+ * thread safe.
+ *
+ * @param <T>
+ * expected result type
+ */
+final class Expression<T> {
+ /** Context used when compiling expressions and evaluating expressions that don't have bindings. */
+ private static final ELContext EL_CONTEXT = getELContext0(null);
+ /** Factory for compiling expressions. */
+ private static final ExpressionFactory FACTORY = new ExpressionFactoryImpl();
+
+ /**
+ * Compiles the provided expression and evaluates it without any bindings.
+ *
+ * @param <T>
+ * Expected result type
+ * @param expression
+ * The expression to compile.
+ * @param expectedType
+ * The expected result type of the expression.
+ * @return The result of the expression evaluation.
+ * @throws IllegalArgumentException
+ * If the expression could not be compiled, evaluated, or the returned value did not have the expected
+ * type.
+ */
+ public static <T> T eval(final String expression, final Class<T> expectedType) {
+ return compile(expression, expectedType).eval();
+ }
+
+ /**
+ * Compiles the provided expression and evaluates it using the provided bindings.
+ *
+ * @param <T>
+ * Expected result type
+ * @param expression
+ * The expression to compile.
+ * @param expectedType
+ * The expected result type of the expression.
+ * @param bindings
+ * The bindings, which may be empty or {@code null}.
+ * @return The result of the expression evaluation.
+ * @throws IllegalArgumentException
+ * If the expression could not be compiled, evaluated, or the returned value did not have the expected
+ * type.
+ */
+ public static <T> T eval(final String expression, final Class<T> expectedType, final Map<String, Object> bindings) {
+ return compile(expression, expectedType).eval(bindings);
+ }
+
+ /** The expected type of this expression. */
+ private final Class<T> expectedType;
+ /** The compiled EL expression. */
+ private final ValueExpression valueExpression;
+
+ /**
+ * Compiles the provided expression string.
+ *
+ * @param <T>
+ * Expected result type
+ * @param expression
+ * The expression to compile.
+ * @param expectedType
+ * The expected result type of the expression.
+ * @return The compiled expression.
+ * @throws IllegalArgumentException
+ * If the expression was not syntactically correct or contained unrecognized functions.
+ */
+ public static <T> Expression<T> compile(final String expression, final Class<T> expectedType) {
+ Reject.ifNull(expression, "expression must not be null");
+ Reject.ifNull(expectedType, "expectedType must not be null");
+ try {
+ final ValueExpression valueExpression = FACTORY.createValueExpression(EL_CONTEXT, expression, expectedType);
+ return new Expression<>(expectedType, valueExpression);
+ } catch (final ELException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ private Expression(final Class<T> expectedType, final ValueExpression valueExpression) {
+ this.expectedType = expectedType;
+ this.valueExpression = valueExpression;
+ }
+
+ /**
+ * Evaluates this expression without any bindings.
+ *
+ * @return The result of the expression evaluation.
+ * @throws IllegalArgumentException
+ * If the expression could not be evaluated or the returned value did not have the expected type.
+ */
+ public T eval() {
+ return eval(Collections.<String, Object>emptyMap());
+ }
+
+ /**
+ * Evaluates this expression using the provided bindings.
+ *
+ * @param bindings
+ * The bindings, which may be empty or {@code null}.
+ * @return The result of the expression evaluation.
+ * @throws IllegalArgumentException
+ * If the expression could not be evaluated or the returned value did not have the expected type.
+ */
+ public T eval(final Map<String, Object> bindings) {
+ try {
+ return expectedType.cast(valueExpression.getValue(getELContext(bindings)));
+ } catch (final ELException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return valueExpression.getExpressionString();
+ }
+
+ private static ELContext getELContext(final Map<String, Object> bindings) {
+ if (bindings == null || bindings.isEmpty()) {
+ return EL_CONTEXT;
+ }
+ return getELContext0(bindings);
+ }
+
+ private static ELContext getELContext0(final Map<String, Object> bindings) {
+ final SimpleResolver resolver = new SimpleResolver(false);
+ final RootPropertyResolver rootPropertyResolver = resolver.getRootPropertyResolver();
+ rootPropertyResolver.setProperty("env", Collections.unmodifiableMap(System.getenv()));
+ rootPropertyResolver.setProperty("system", Collections.unmodifiableMap(System.getProperties()));
+ if (bindings != null) {
+ for (final Map.Entry<String, Object> binding : bindings.entrySet()) {
+ rootPropertyResolver.setProperty(binding.getKey(), binding.getValue());
+ }
+ }
+ final SimpleContext context = new SimpleContext(resolver);
+ for (final Map.Entry<String, Method> function : getPublicStaticMethods(Functions.class).entrySet()) {
+ context.setFunction("", function.getKey(), function.getValue());
+ }
+ return context;
+ }
+
+ private static Map<String, Method> getPublicStaticMethods(final Class<?> target) {
+ final Map<String, Method> methods = new HashMap<>();
+ for (final Method method : target.getMethods()) {
+ if (Modifier.isStatic(method.getModifiers())) {
+ method.setAccessible(true);
+ methods.put(method.getName(), method);
+ }
+ }
+ return methods;
+ }
+}
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/config/Functions.java b/opendj-server-legacy/src/main/java/org/opends/server/config/Functions.java
new file mode 100644
index 0000000..a46a302
--- /dev/null
+++ b/opendj-server-legacy/src/main/java/org/opends/server/config/Functions.java
@@ -0,0 +1,80 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2016 ForgeRock AS.
+ */
+package org.opends.server.config;
+
+import static java.nio.charset.Charset.defaultCharset;
+
+import java.io.BufferedReader;
+import java.io.FileInputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.net.URI;
+import java.util.Properties;
+import java.util.regex.Pattern;
+
+import org.forgerock.util.encode.Base64;
+
+/** Functions which can be invoked from within expressions. */
+final class Functions {
+ // URL scheme: alpha *( alpha | digit | "+" | "-" | "." )
+ private static final Pattern URL_SCHEME_PATTERN = Pattern.compile("^[A-Za-z][A-Za-z0-9+-.]*:.*");
+
+ public static String trim(String value) {
+ return value != null ? value.trim() : null;
+ }
+
+ public static String encodeBase64(final String value) {
+ return value != null ? Base64.encode(value.getBytes()) : null;
+ }
+
+ public static String decodeBase64(final String value) {
+ return value != null ? new String(Base64.decode(value)) : null;
+ }
+
+ public static String read(final String url) throws Exception {
+ try (final BufferedReader reader = new BufferedReader(open(url))) {
+ final StringBuilder builder = new StringBuilder();
+ boolean isFirst = true;
+ for (String line = reader.readLine(); line != null; line = reader.readLine()) {
+ if (!isFirst) {
+ builder.append(System.lineSeparator());
+ }
+ builder.append(line);
+ isFirst = false;
+ }
+ return builder.toString();
+ }
+ }
+
+ public static Properties readProperties(final String url) throws Exception {
+ try (final Reader reader = open(url)) {
+ final Properties properties = new Properties();
+ properties.load(reader);
+ return properties;
+ }
+ }
+
+ private static InputStreamReader open(final String url) throws Exception {
+ // Check if the URL is actually just a relative path name without a scheme. If it is then URL parsing will
+ // fail, so parse it as a file and open it directly, otherwise assume we have a valid URL with a scheme.
+ if (URL_SCHEME_PATTERN.matcher(url).matches()) {
+ return new InputStreamReader(new URI(url).toURL().openStream(), defaultCharset());
+ }
+ return new InputStreamReader(new FileInputStream(url), defaultCharset());
+ }
+
+ private Functions() { /* Utility class. */ }
+}
diff --git a/opendj-server-legacy/src/messages/org/opends/messages/config.properties b/opendj-server-legacy/src/messages/org/opends/messages/config.properties
index 6630fa4..359087b 100644
--- a/opendj-server-legacy/src/messages/org/opends/messages/config.properties
+++ b/opendj-server-legacy/src/messages/org/opends/messages/config.properties
@@ -863,4 +863,12 @@
trying to update schema with its content: %s
WARN_CONFIG_SCHEMA_FILE_HAS_SCHEMA_WARNING_WITH_OVERWRITE_762=The config schema file '%s' generated \
warning when trying to update schema with its content, despite allowing to overwrite definitions: %s
-ERR_CONFIG_BACKEND_BASE_IS_EMPTY_763=Unable to configure the backend '%s' because one of its base DNs is the empty DN
\ No newline at end of file
+ERR_CONFIG_BACKEND_BASE_IS_EMPTY_763=Unable to configure the backend '%s' because one of its base DNs is the empty DN
+ERR_CONFIG_FILE_ADD_REJECTED_DUE_TO_EVALUATION_FAILURE_764=Entry '%s' cannot be added because attribute '%s' \
+ contained an expression '%s' that could not be evaluated: %s
+ERR_CONFIG_FILE_DELETE_REJECTED_DUE_TO_EVALUATION_FAILURE_765=Entry '%s' cannot be deleted because attribute '%s' \
+ contained an expression '%s' that could not be evaluated: %s
+ERR_CONFIG_FILE_MODIFY_REJECTED_DUE_TO_EVALUATION_FAILURE_766=Entry '%s' cannot be modified because attribute '%s' \
+ contained an expression '%s' that could not be evaluated: %s
+ERR_CONFIG_FILE_READ_FAILED_DUE_TO_EVALUATION_FAILURE_767=Entry '%s' cannot be read because attribute '%s' \
+ contained an expression '%s' that could not be evaluated: %s
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/config/ExpressionTest.java b/opendj-server-legacy/src/test/java/org/opends/server/config/ExpressionTest.java
new file mode 100644
index 0000000..042d2c3
--- /dev/null
+++ b/opendj-server-legacy/src/test/java/org/opends/server/config/ExpressionTest.java
@@ -0,0 +1,105 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2016 ForgeRock AS.
+ */
+package org.opends.server.config;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.Collections;
+import java.util.Map;
+
+import org.opends.server.DirectoryServerTestCase;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+@SuppressWarnings("javadoc")
+@Test(groups = { "precommit" }, sequential = true)
+public class ExpressionTest extends DirectoryServerTestCase {
+ @DataProvider
+ public static Object[][] expressions() {
+ return new Object[][] { { "true", String.class, "true" },
+ { "true", Boolean.class, true },
+ { "false", Boolean.class, false },
+ { "123", Integer.class, 123 },
+ { "123", Long.class, 123L }, };
+ }
+
+ @Test(dataProvider = "expressions")
+ public void expressionEvaluationShouldReturnExpectedValues(final String expression, final Class<?> type,
+ final Object expected) {
+ assertThat(Expression.eval(expression, type)).isEqualTo(expected);
+ }
+
+ @Test
+ public void expressionsCanAccessEnvironment() {
+ assertThat(Expression.eval("${env['PATH']}", String.class)).isNotNull();
+ }
+
+ @Test
+ public void expressionsCanAccessSystemProperties() {
+ assertThat(Expression.eval("${system['user.home']}", String.class)).isNotNull();
+ }
+
+ @Test
+ public void expressionsCanAccessBeanBinding() {
+ final Map<String, Object> bindings = bindings("server", new Server("myhost", 1389));
+ assertThat(Expression.eval("${server.hostName}", String.class, bindings)).isEqualTo("myhost");
+ assertThat(Expression.eval("${server.hostName.length()}", Integer.class, bindings)).isEqualTo(6);
+ assertThat(Expression.eval("${server.port}", Integer.class, bindings)).isEqualTo(1389);
+ }
+
+ @Test
+ public void expressionsCanAccessMapBinding() {
+ final Map<String, Object> bindings = bindings("map", Collections.singletonMap("name", "World"));
+ assertThat(Expression.eval("Hello ${map.name}", String.class, bindings)).isEqualTo("Hello World");
+ }
+
+ @Test(expectedExceptions = IllegalArgumentException.class)
+ public void expressionsShouldThrowIllegalArgumentExceptionForBadType() {
+ Expression.eval("${env['PATH']}", Integer.class);
+ }
+
+ @Test(expectedExceptions = IllegalArgumentException.class)
+ public void expressionsShouldThrowIllegalArgumentExceptionForMissingProperty() {
+ Expression.eval("${missing}", String.class);
+ }
+
+ @Test(expectedExceptions = IllegalArgumentException.class)
+ public void expressionsShouldThrowIllegalArgumentExceptionForMissingFunction() {
+ Expression.eval("${missingFunction()}", String.class);
+ }
+
+ private Map<String, Object> bindings(final String key, final Object value) {
+ return Collections.singletonMap(key, value);
+ }
+
+ private final class Server {
+ private String hostName;
+ private int port;
+
+ private Server(final String hostName, final int port) {
+ this.hostName = hostName;
+ this.port = port;
+ }
+
+ public String getHostName() {
+ return hostName;
+ }
+
+ public int getPort() {
+ return port;
+ }
+ }
+}
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/config/FunctionsTest.java b/opendj-server-legacy/src/test/java/org/opends/server/config/FunctionsTest.java
new file mode 100644
index 0000000..5bb0090
--- /dev/null
+++ b/opendj-server-legacy/src/test/java/org/opends/server/config/FunctionsTest.java
@@ -0,0 +1,51 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2016 ForgeRock AS.
+ */
+package org.opends.server.config;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.opends.server.TestCaseUtils.getTestResource;
+
+import java.io.File;
+
+import org.opends.server.DirectoryServerTestCase;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+@SuppressWarnings("javadoc")
+@Test(groups = { "precommit" }, sequential = true)
+public class FunctionsTest extends DirectoryServerTestCase {
+ private static final File CONFIG_PROPERTIES = getTestResource("el-config.properties");
+ private static final File PASSWORD_PIN = getTestResource("el-password.pin");
+
+ @DataProvider
+ public static Object[][] expressions() {
+ return new Object[][] {
+ { "${trim(' text ')}", String.class, "text" },
+ { "${read('" + PASSWORD_PIN + "')}", String.class, "changeit" },
+ { "${read('file:" + PASSWORD_PIN + "')}", String.class, "changeit" },
+ { "${readProperties('" + CONFIG_PROPERTIES + "').hostName}", String.class, "myhost" },
+ { "${readProperties('" + CONFIG_PROPERTIES + "').port}", Integer.class, 1389 },
+ { "${readProperties('file:" + CONFIG_PROPERTIES + "').port}", Integer.class, 1389 },
+ { "${readProperties('" + CONFIG_PROPERTIES.toURI() + "').port}", Integer.class, 1389 },
+ };
+ }
+
+ @Test(dataProvider = "expressions")
+ public void functionsShouldReturnExpectedValues(final String expression, final Class<?> type,
+ final Object expected) {
+ assertThat(Expression.eval(expression, type)).isEqualTo(expected);
+ }
+}
diff --git a/opendj-server-legacy/tests/unit-tests-testng/resource/el-config.properties b/opendj-server-legacy/tests/unit-tests-testng/resource/el-config.properties
new file mode 100644
index 0000000..8243dc5
--- /dev/null
+++ b/opendj-server-legacy/tests/unit-tests-testng/resource/el-config.properties
@@ -0,0 +1,19 @@
+#
+# The contents of this file are subject to the terms of the Common Development and
+# Distribution License (the License). You may not use this file except in compliance with the
+# License.
+#
+# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+# specific language governing permission and limitations under the License.
+#
+# When distributing Covered Software, include this CDDL Header Notice in each file and include
+# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+# Header, with the fields enclosed by brackets [] replaced by your own identifying
+# information: "Portions copyright [year] [name of copyright owner]".
+#
+# Copyright 2016 ForgeRock AS.
+#
+hostName=myhost
+port=1389
+bindDN=cn=directory manager
+bindPassword=changeit
diff --git a/opendj-server-legacy/tests/unit-tests-testng/resource/el-password.pin b/opendj-server-legacy/tests/unit-tests-testng/resource/el-password.pin
new file mode 100644
index 0000000..1d40192
--- /dev/null
+++ b/opendj-server-legacy/tests/unit-tests-testng/resource/el-password.pin
@@ -0,0 +1 @@
+changeit
--
Gitblit v1.10.0