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