Java tutorial
/* * Copyright 2015-2017 UnboundID Corp. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License (GPLv2 only) * or the terms of the GNU Lesser General Public License (LGPLv2.1 only) * as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see <http://www.gnu.org/licenses>. */ package com.unboundid.scim2.common.utils; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.unboundid.scim2.common.Path; import com.unboundid.scim2.common.exceptions.BadRequestException; import com.unboundid.scim2.common.exceptions.ScimException; import com.unboundid.scim2.common.filters.AndFilter; import com.unboundid.scim2.common.filters.ComplexValueFilter; import com.unboundid.scim2.common.filters.ContainsFilter; import com.unboundid.scim2.common.filters.EndsWithFilter; import com.unboundid.scim2.common.filters.EqualFilter; import com.unboundid.scim2.common.filters.Filter; import com.unboundid.scim2.common.filters.FilterVisitor; import com.unboundid.scim2.common.filters.GreaterThanFilter; import com.unboundid.scim2.common.filters.GreaterThanOrEqualFilter; import com.unboundid.scim2.common.filters.LessThanFilter; import com.unboundid.scim2.common.filters.LessThanOrEqualFilter; import com.unboundid.scim2.common.filters.NotEqualFilter; import com.unboundid.scim2.common.filters.NotFilter; import com.unboundid.scim2.common.filters.OrFilter; import com.unboundid.scim2.common.filters.PresentFilter; import com.unboundid.scim2.common.filters.StartsWithFilter; import com.unboundid.scim2.common.types.AttributeDefinition; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; /** * A filter visitor that will evaluate a filter on a JsonNode and return * whether the JsonNode matches the filter. */ public class FilterEvaluator implements FilterVisitor<Boolean, JsonNode> { private static final FilterEvaluator SINGLETON = new FilterEvaluator(); private static final Path VALUE_PATH = Path.root().attribute("value"); /** * Evaluate the provided filter against the provided JsonNode. * * @param filter The filter to evaluate. * @param jsonNode The JsonNode to evaluate the filter against. * @return {@code true} if the JsonNode matches the filter or {@code false} * otherwise. * @throws ScimException If the filter is not valid for matching. */ public static boolean evaluate(final Filter filter, final JsonNode jsonNode) throws ScimException { return filter.visit(SINGLETON, jsonNode); } /** * {@inheritDoc} */ public Boolean visit(final EqualFilter filter, final JsonNode object) throws ScimException { Iterable<JsonNode> nodes = getCandidateNodes(filter.getAttributePath(), object); if (filter.getComparisonValue().isNull() && isEmpty(nodes)) { // draft-ietf-scim-core-schema section 2.4 states "Unassigned // attributes, the null value, or empty array (in the case of // a multi-valued attribute) SHALL be considered to be // equivalent in "state". return true; } for (JsonNode node : nodes) { if (JsonUtils.compareTo(node, filter.getComparisonValue(), getAttributeDefinition(filter.getAttributePath())) == 0) { return true; } } return false; } /** * {@inheritDoc} */ public Boolean visit(final NotEqualFilter filter, final JsonNode object) throws ScimException { Iterable<JsonNode> nodes = getCandidateNodes(filter.getAttributePath(), object); if (filter.getComparisonValue().isNull() && isEmpty(nodes)) { // draft-ietf-scim-core-schema section 2.4 states "Unassigned // attributes, the null value, or empty array (in the case of // a multi-valued attribute) SHALL be considered to be // equivalent in "state". return false; } for (JsonNode node : nodes) { if (JsonUtils.compareTo(node, filter.getComparisonValue(), getAttributeDefinition(filter.getAttributePath())) == 0) { return false; } } return true; } /** * {@inheritDoc} */ public Boolean visit(final ContainsFilter filter, final JsonNode object) throws ScimException { return substringMatch(filter, object); } /** * {@inheritDoc} */ public Boolean visit(final StartsWithFilter filter, final JsonNode object) throws ScimException { return substringMatch(filter, object); } /** * {@inheritDoc} */ public Boolean visit(final EndsWithFilter filter, final JsonNode object) throws ScimException { return substringMatch(filter, object); } /** * {@inheritDoc} */ public Boolean visit(final PresentFilter filter, final JsonNode object) throws ScimException { Iterable<JsonNode> nodes = getCandidateNodes(filter.getAttributePath(), object); for (JsonNode node : nodes) { // draft-ietf-scim-core-schema section 2.4 states "Unassigned // attributes, the null value, or empty array (in the case of // a multi-valued attribute) SHALL be considered to be // equivalent in "state". if (!isEmpty(node)) { return true; } } return false; } /** * {@inheritDoc} */ public Boolean visit(final GreaterThanFilter filter, final JsonNode object) throws ScimException { Iterable<JsonNode> nodes = getCandidateNodes(filter.getAttributePath(), object); for (JsonNode node : nodes) { if (node.isBoolean() || node.isBinary()) { throw BadRequestException.invalidFilter( "Greater than filter may not compare boolean or binary " + "attribute values"); } if (JsonUtils.compareTo(node, filter.getComparisonValue(), getAttributeDefinition(filter.getAttributePath())) > 0) { return true; } } return false; } /** * {@inheritDoc} */ public Boolean visit(final GreaterThanOrEqualFilter filter, final JsonNode object) throws ScimException { Iterable<JsonNode> nodes = getCandidateNodes(filter.getAttributePath(), object); for (JsonNode node : nodes) { if (node.isBoolean() || node.isBinary()) { throw BadRequestException.invalidFilter( "Greater than or equal " + "filter may not compare boolean or binary attribute values"); } if (JsonUtils.compareTo(node, filter.getComparisonValue(), getAttributeDefinition(filter.getAttributePath())) >= 0) { return true; } } return false; } /** * {@inheritDoc} */ public Boolean visit(final LessThanFilter filter, final JsonNode object) throws ScimException { Iterable<JsonNode> nodes = getCandidateNodes(filter.getAttributePath(), object); for (JsonNode node : nodes) { if (node.isBoolean() || node.isBinary()) { throw BadRequestException.invalidFilter( "Less than or equal " + "filter may not compare boolean or binary attribute values"); } if (JsonUtils.compareTo(node, filter.getComparisonValue(), getAttributeDefinition(filter.getAttributePath())) < 0) { return true; } } return false; } /** * {@inheritDoc} */ public Boolean visit(final LessThanOrEqualFilter filter, final JsonNode object) throws ScimException { Iterable<JsonNode> nodes = getCandidateNodes(filter.getAttributePath(), object); for (JsonNode node : nodes) { if (node.isBoolean() || node.isBinary()) { throw BadRequestException.invalidFilter( "Less than or equal " + "filter may not compare boolean or binary attribute values"); } if (JsonUtils.compareTo(node, filter.getComparisonValue(), getAttributeDefinition(filter.getAttributePath())) <= 0) { return true; } } return false; } /** * {@inheritDoc} */ public Boolean visit(final AndFilter filter, final JsonNode object) throws ScimException { for (Filter combinedFilter : filter.getCombinedFilters()) { if (!combinedFilter.visit(this, object)) { return false; } } return true; } /** * {@inheritDoc} */ public Boolean visit(final OrFilter filter, final JsonNode object) throws ScimException { for (Filter combinedFilter : filter.getCombinedFilters()) { if (combinedFilter.visit(this, object)) { return true; } } return false; } /** * {@inheritDoc} */ public Boolean visit(final NotFilter filter, final JsonNode object) throws ScimException { return !filter.getInvertedFilter().visit(this, object); } /** * {@inheritDoc} */ public Boolean visit(final ComplexValueFilter filter, final JsonNode object) throws ScimException { Iterable<JsonNode> nodes = getCandidateNodes(filter.getAttributePath(), object); for (JsonNode node : nodes) { if (node.isArray()) { // filter each element of the array individually for (JsonNode value : node) { if (filter.getValueFilter().visit(this, value)) { return true; } } } else if (filter.getValueFilter().visit(this, node)) { return true; } } return false; } /** * Retrieve the attribute definition for the attribute specified by the path * to determine case sensitivity during string matching. * * @param path The path to the attribute whose definition to retrieve. * @return the attribute definition or {@code null} if not available, in which * case case insensitive string value matching will be performed. */ protected AttributeDefinition getAttributeDefinition(final Path path) { return null; } /** * Retrieves the JsonNodes to compare against. * * @param path The path to the value. * @param jsonNode The JsonNode containing the value. * @return The JsonNodes to compare against. * @throws ScimException If an exception occurs during the operation. */ private Iterable<JsonNode> getCandidateNodes(final Path path, final JsonNode jsonNode) throws ScimException { if (jsonNode.isArray()) { return jsonNode; } if (jsonNode.isObject()) { List<JsonNode> nodes = JsonUtils.findMatchingPaths(path, (ObjectNode) jsonNode); ArrayList<JsonNode> flattenedNodes = new ArrayList<JsonNode>(nodes.size()); for (JsonNode node : nodes) { if (node.isArray()) { for (JsonNode child : node) { flattenedNodes.add(child); } } else { flattenedNodes.add(node); } } return flattenedNodes; } if (jsonNode.isValueNode() && path.equals(VALUE_PATH)) { // Special case for the "value" path to reference the value itself. // Used for referencing the value nodes of an array when the filter is // attr[value eq "value1"] and the multi-valued attribute is // "attr": ["value1", "value2", "value3"]. return Collections.singletonList(jsonNode); } return Collections.emptyList(); } /** * Return true if the node is either {@code null} or an empty array. * @param node node to examine * @return boolean */ private boolean isEmpty(final JsonNode node) { if (node.isArray()) { Iterator<JsonNode> iterator = node.elements(); while (iterator.hasNext()) { if (!isEmpty(iterator.next())) { return false; } } return true; } else { return node.isNull(); } } /** * Return true if the specified node list contains nothing * but empty arrays and/or {@code null} nodes. * @param nodes list of nodes as returned from JsonUtils.findMatchingPaths * @return true if the list contains only empty array(s) */ private boolean isEmpty(final Iterable<JsonNode> nodes) { for (JsonNode node : nodes) { if (!isEmpty(node)) { return false; } } return true; } /** * Evaluate a substring match filter. * * @param filter The filter to operate on. * @param object The object to evaluate. * @return The return value from the operation. * @throws ScimException If an exception occurs during the operation. */ private boolean substringMatch(final Filter filter, final JsonNode object) throws ScimException { Iterable<JsonNode> nodes = getCandidateNodes(filter.getAttributePath(), object); for (JsonNode node : nodes) { if (node.isTextual() && filter.getComparisonValue().isTextual()) { AttributeDefinition attributeDefinition = getAttributeDefinition(filter.getAttributePath()); String nodeValue = node.textValue(); String comparisonValue = filter.getComparisonValue().textValue(); if (attributeDefinition == null || !attributeDefinition.isCaseExact()) { nodeValue = StaticUtils.toLowerCase(nodeValue); comparisonValue = StaticUtils.toLowerCase(comparisonValue); } switch (filter.getFilterType()) { case CONTAINS: if (nodeValue.contains(comparisonValue)) { return true; } break; case STARTS_WITH: if (nodeValue.startsWith(comparisonValue)) { return true; } break; case ENDS_WITH: if (nodeValue.endsWith(comparisonValue)) { return true; } break; } } else if (node.equals(filter.getComparisonValue())) { return true; } } return false; } }