/* * 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]". * * Portions copyright 2015 ForgeRock AS. */ package org.forgerock.openidm.sync.impl; import javax.script.ScriptException; import org.forgerock.json.JsonPointer; import org.forgerock.json.JsonValue; import org.forgerock.json.JsonValueException; import org.forgerock.json.resource.QueryFilters; import org.forgerock.openidm.sync.impl.Scripts.Script; import org.forgerock.util.query.QueryFilter; import org.forgerock.util.query.QueryFilterVisitor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; /** * Represents a condition on which a property mapping may be applied, or a policy may be enforced. */ class Condition { /** * Logger */ private final static Logger LOGGER = LoggerFactory.getLogger(Condition.class); /** * The types of conditions. */ private enum Type { /** * A condition evaluated by a script. */ SCRIPTED, /** * A condition evaluated by a matching "queryFilter". */ QUERY_FILTER, /** * A condition which always passes. This is used if a null configuration is passed in. */ TRUE } /** * This condition's type */ private Type type; /** * The query filter if configured */ private QueryFilter<JsonPointer> queryFilter; /** * The condition script if configured */ private Script script; /** * The constructor. * * @param config the condition configuration */ public Condition(JsonValue config) { if (config.isNull()) { init(Type.TRUE, null, null); } else if (config.isString()) { init(Type.QUERY_FILTER, QueryFilters.parse(config.asString()), null); } else { init(Type.SCRIPTED, null, Scripts.newInstance(config)); } } /** * Initializes the condition fields. * * @param type the conditions type. * @param queryFilter the query filter. * @param script the condition script. */ private void init(Type type, QueryFilter<JsonPointer> queryFilter, Script script) { this.type = type; this.queryFilter = queryFilter; this.script = script; } /** * Evaluates the condition. Returns true if the condition is met, false otherwise. * * @param params parameters to use during evaluation. * @return true if the condition is met, false otherwise. * @throws SynchronizationException if errors are encountered. */ public boolean evaluate(JsonValue params) throws SynchronizationException { switch (type) { case TRUE: return true; case QUERY_FILTER: return queryFilter == null ? false : queryFilter.accept(JSONVALUE_FILTER_VISITOR, params); case SCRIPTED: Map<String, Object> scope = new HashMap<String, Object>(); try { if (params.isMap()) { scope.putAll(params.asMap()); } Object o = script.exec(scope); if (o == null || !(o instanceof Boolean) || Boolean.FALSE.equals(o)) { return false; // property mapping is not applicable; do not apply } return true; } catch (JsonValueException jve) { LOGGER.warn("Unexpected JSON value exception while evaluating condition", jve); throw new SynchronizationException(jve); } catch (ScriptException se) { LOGGER.warn("Script encountered exception while evaluating condition", se); throw new SynchronizationException(se); } default: return false; } } /** * This is a relatively generic implementation for testing JsonValue objects though it * only returns Boolean for the test result. This may be extracted to a more common * location for broader use. */ private static final QueryFilterVisitor<Boolean, JsonValue, JsonPointer> JSONVALUE_FILTER_VISITOR = new QueryFilterVisitor<Boolean, JsonValue, JsonPointer>() { @Override public Boolean visitAndFilter(final JsonValue p, final List<QueryFilter<JsonPointer>> subFilters) { for (final QueryFilter<JsonPointer> subFilter : subFilters) { if (!subFilter.accept(this, p)) { return Boolean.FALSE; } } return Boolean.TRUE; } @Override public Boolean visitBooleanLiteralFilter(final JsonValue p, final boolean value) { return value; } @Override public Boolean visitContainsFilter(final JsonValue p, final JsonPointer field, final Object valueAssertion) { for (final Object value : getValues(p, field)) { if (isCompatible(valueAssertion, value)) { if (valueAssertion instanceof String) { final String s1 = ((String) valueAssertion).toLowerCase(Locale.ENGLISH); final String s2 = ((String) value).toLowerCase(Locale.ENGLISH); if (s2.contains(s1)) { return Boolean.TRUE; } } else { // Use equality matching for numbers and booleans. if (compareValues(valueAssertion, value) == 0) { return Boolean.TRUE; } } } } return Boolean.FALSE; } @Override public Boolean visitEqualsFilter(final JsonValue p, final JsonPointer field, final Object valueAssertion) { for (final Object value : getValues(p, field)) { if (isCompatible(value, valueAssertion) && compareValues(value, valueAssertion) == 0) { return Boolean.TRUE; } } return Boolean.FALSE; } @Override public Boolean visitExtendedMatchFilter(final JsonValue p, final JsonPointer field, final String matchingRuleId, final Object valueAssertion) { // Extended filters are not supported return Boolean.FALSE; } @Override public Boolean visitGreaterThanFilter(final JsonValue p, final JsonPointer field, final Object valueAssertion) { for (final Object value : getValues(p, field)) { if (isCompatible(value, valueAssertion) && compareValues(value, valueAssertion) > 0) { return Boolean.TRUE; } } return Boolean.FALSE; } @Override public Boolean visitGreaterThanOrEqualToFilter(final JsonValue p, final JsonPointer field, final Object valueAssertion) { for (final Object value : getValues(p, field)) { if (isCompatible(value, valueAssertion) && compareValues(value, valueAssertion) >= 0) { return Boolean.TRUE; } } return Boolean.FALSE; } @Override public Boolean visitLessThanFilter(final JsonValue p, final JsonPointer field, final Object valueAssertion) { for (final Object value : getValues(p, field)) { if (isCompatible(value, valueAssertion) && compareValues(value, valueAssertion) < 0) { return Boolean.TRUE; } } return Boolean.FALSE; } @Override public Boolean visitLessThanOrEqualToFilter(final JsonValue p, final JsonPointer field, final Object valueAssertion) { for (final Object value : getValues(p, field)) { if (isCompatible(value, valueAssertion) && compareValues(value, valueAssertion) <= 0) { return Boolean.TRUE; } } return Boolean.FALSE; } @Override public Boolean visitNotFilter(final JsonValue p, final QueryFilter<JsonPointer> subFilter) { return !subFilter.accept(this, p); } @Override public Boolean visitOrFilter(final JsonValue p, final List<QueryFilter<JsonPointer>> subFilters) { for (final QueryFilter<JsonPointer> subFilter : subFilters) { if (subFilter.accept(this, p)) { return Boolean.TRUE; } } return Boolean.FALSE; } @Override public Boolean visitPresentFilter(final JsonValue p, final JsonPointer field) { final JsonValue value = p.get(field); return value != null && !value.isNull(); } @Override public Boolean visitStartsWithFilter(final JsonValue p, final JsonPointer field, final Object valueAssertion) { for (final Object value : getValues(p, field)) { if (isCompatible(valueAssertion, value)) { if (valueAssertion instanceof String) { final String s1 = ((String) valueAssertion).toLowerCase(Locale.ENGLISH); final String s2 = ((String) value).toLowerCase(Locale.ENGLISH); if (s2.startsWith(s1)) { return Boolean.TRUE; } } else { // Use equality matching for numbers and booleans. if (compareValues(valueAssertion, value) == 0) { return Boolean.TRUE; } } } } return Boolean.FALSE; } private List<Object> getValues(final JsonValue resource, final JsonPointer field) { final JsonValue value = resource.get(field); if (value == null) { return Collections.emptyList(); } else if (value.isList()) { return value.asList(); } else { return Collections.singletonList(value.getObject()); } } }; private static int compareValues(final Object v1, final Object v2) { if (v1 instanceof String && v2 instanceof String) { final String s1 = (String) v1; final String s2 = (String) v2; return s1.compareToIgnoreCase(s2); } else if (v1 instanceof Number && v2 instanceof Number) { final Double n1 = ((Number) v1).doubleValue(); final Double n2 = ((Number) v2).doubleValue(); return n1.compareTo(n2); } else if (v1 instanceof Boolean && v2 instanceof Boolean) { final Boolean b1 = (Boolean) v1; final Boolean b2 = (Boolean) v2; return b1.compareTo(b2); } else { // Different types: we need to ensure predictable ordering, // so use class name as secondary key. return v1.getClass().getName().compareTo(v2.getClass().getName()); } } private static boolean isCompatible(final Object v1, final Object v2) { return (v1 instanceof String && v2 instanceof String) || (v1 instanceof Number && v2 instanceof Number) || (v1 instanceof Boolean && v2 instanceof Boolean); } }