mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

Matthew Swift
25.41.2016 076c78aa32f39fe76d74dca79b550f3049e2baa5
OPENDJ-3189: Implement EL expression support in cn=config

It is now possible to use EL expression inside cn=config. Examples:

1. set the LDAP port to the value of the OPENDJ_PORT env variable:

ds-cfg-listen-port: ${env['OPENDJ_PORT']}

2. set the LDAP port to the value of the opendj.port system property:

ds-cfg-listen-port: ${system['opendj.port']}

3. set the LDAP port to the value of the opendj.port property in a
property file:

ds-cfg-listen-port: ${readProperties(config.properties)['port']}

4. set the JKS key manager's PIN file:

ds-cfg-key-store-pin: ${read('config/keystore.pin')}

Other functions are provided in the Functions class.

KNOWN ISSUES:

Expressions are evaluated by the server's config framework before
publishing them to external components (e.g. config listeners, LDAP
clients). This allows client applications to function properly because
they receive the "effective" value rather than the unevaluated
expression, which is likely to be invalid according to the server's
schema (e.g. ${env['OPENDJ_PORT']} is not a valid integer).

Tools like dsconfig can read and update the configuration. However,
depending on the schema, it is often not possible to configure
expressions using dsconfig. Instead, users must first configure their
server and then manually edit config.ldif in order to add the
expressions where needed.

Another side-effect of exposing "effective" (evaluated) values over LDAP
is that secrets may be accidentally exposed to a wider audience than
anticipated. For example, the key manager pin can now be read from a
file using two approaches:

ds-cfg-key-store-pin: ${read('config/keystore.pin')}
ds-cfg-key-store-pin-file: config/keystore.pin

Reading the associated config entry over LDAP will return the evaluated
content in the first case, but not the second.
6 files added
3 files modified
568 ■■■■■ changed files
opendj-server-legacy/pom.xml 14 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/config/ConfigurationHandler.java 99 ●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/config/Expression.java 189 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/config/Functions.java 80 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/messages/org/opends/messages/config.properties 10 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/config/ExpressionTest.java 105 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/config/FunctionsTest.java 51 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/tests/unit-tests-testng/resource/el-config.properties 19 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/tests/unit-tests-testng/resource/el-password.pin 1 ●●●● patch | view | raw | blame | history
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>
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;
  }
}
opendj-server-legacy/src/main/java/org/opends/server/config/Expression.java
New file
@@ -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;
    }
}
opendj-server-legacy/src/main/java/org/opends/server/config/Functions.java
New file
@@ -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. */ }
}
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
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
opendj-server-legacy/src/test/java/org/opends/server/config/ExpressionTest.java
New file
@@ -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;
        }
    }
}
opendj-server-legacy/src/test/java/org/opends/server/config/FunctionsTest.java
New file
@@ -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);
    }
}
opendj-server-legacy/tests/unit-tests-testng/resource/el-config.properties
New file
@@ -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
opendj-server-legacy/tests/unit-tests-testng/resource/el-password.pin
New file
@@ -0,0 +1 @@
changeit