Author: pauls Date: Mon Jul 31 20:51:16 2017 New Revision: 1803574 URL: http://svn.apache.org/viewvc?rev=1803574&view=rev Log: Add first pass on Capability matching.
Added: sling/whiteboard/cziegeler/feature-support/src/main/java/org/apache/sling/feature/support/impl/ sling/whiteboard/cziegeler/feature-support/src/main/java/org/apache/sling/feature/support/impl/CapabilityMatcher.java sling/whiteboard/cziegeler/feature-support/src/main/java/org/apache/sling/feature/support/impl/SimpleFilter.java sling/whiteboard/cziegeler/feature-support/src/main/java/org/apache/sling/feature/support/impl/StringComparator.java sling/whiteboard/cziegeler/feature-support/src/main/java/org/apache/sling/feature/support/impl/VersionRange.java Added: sling/whiteboard/cziegeler/feature-support/src/main/java/org/apache/sling/feature/support/impl/CapabilityMatcher.java URL: http://svn.apache.org/viewvc/sling/whiteboard/cziegeler/feature-support/src/main/java/org/apache/sling/feature/support/impl/CapabilityMatcher.java?rev=1803574&view=auto ============================================================================== --- sling/whiteboard/cziegeler/feature-support/src/main/java/org/apache/sling/feature/support/impl/CapabilityMatcher.java (added) +++ sling/whiteboard/cziegeler/feature-support/src/main/java/org/apache/sling/feature/support/impl/CapabilityMatcher.java Mon Jul 31 20:51:16 2017 @@ -0,0 +1,621 @@ +package org.apache.sling.feature.support.impl; + +import java.lang.reflect.Array; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.sling.feature.Capability; +import org.apache.sling.feature.Requirement; +import org.osgi.framework.Constants; +import org.osgi.framework.Version; + +import static org.osgi.framework.Constants.RESOLUTION_DIRECTIVE; +import static org.osgi.framework.Constants.RESOLUTION_OPTIONAL; + +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +public class CapabilityMatcher +{ + public static Set<Capability> match(Set<Capability> caps, final SimpleFilter sf) + { + Set<Capability> matches = Collections.newSetFromMap(new ConcurrentHashMap<Capability, Boolean>()); + + if (sf.getOperation() == SimpleFilter.MATCH_ALL) + { + matches.addAll(caps); + } + else if (sf.getOperation() == SimpleFilter.AND) + { + // Evaluate each subfilter against the remaining capabilities. + // For AND we calculate the intersection of each subfilter. + // We can short-circuit the AND operation if there are no + // remaining capabilities. + final List<SimpleFilter> sfs = (List<SimpleFilter>) sf.getValue(); + for (int i = 0; (caps.size() > 0) && (i < sfs.size()); i++) + { + matches = match(caps, sfs.get(i)); + caps = matches; + } + } + else if (sf.getOperation() == SimpleFilter.OR) + { + // Evaluate each subfilter against the remaining capabilities. + // For OR we calculate the union of each subfilter. + List<SimpleFilter> sfs = (List<SimpleFilter>) sf.getValue(); + for (int i = 0; i < sfs.size(); i++) + { + matches.addAll(match(caps, sfs.get(i))); + } + } + else if (sf.getOperation() == SimpleFilter.NOT) + { + // Evaluate each subfilter against the remaining capabilities. + // For OR we calculate the union of each subfilter. + matches.addAll(caps); + List<SimpleFilter> sfs = (List<SimpleFilter>) sf.getValue(); + for (int i = 0; i < sfs.size(); i++) + { + matches.removeAll(match(caps, sfs.get(i))); + } + } + else + { + for (Iterator<Capability> it = caps.iterator(); it.hasNext(); ) + { + Capability cap = it.next(); + Object lhs = cap.getAttributes().get(sf.getName()); + if (lhs != null) + { + if (compare(lhs, sf.getValue(), sf.getOperation())) + { + matches.add(cap); + } + } + } + } + + return matches; + } + + public static boolean matches(Capability cap, SimpleFilter sf) + { + return matchesInternal(cap, sf) && matchMandatory(cap, sf); + } + + private static boolean matchesInternal(Capability cap, SimpleFilter sf) + { + boolean matched = true; + + if (sf.getOperation() == SimpleFilter.MATCH_ALL) + { + matched = true; + } + else if (sf.getOperation() == SimpleFilter.AND) + { + // Evaluate each subfilter against the remaining capabilities. + // For AND we calculate the intersection of each subfilter. + // We can short-circuit the AND operation if there are no + // remaining capabilities. + List<SimpleFilter> sfs = (List<SimpleFilter>) sf.getValue(); + for (int i = 0; matched && (i < sfs.size()); i++) + { + matched = matchesInternal(cap, sfs.get(i)); + } + } + else if (sf.getOperation() == SimpleFilter.OR) + { + // Evaluate each subfilter against the remaining capabilities. + // For OR we calculate the union of each subfilter. + matched = false; + List<SimpleFilter> sfs = (List<SimpleFilter>) sf.getValue(); + for (int i = 0; !matched && (i < sfs.size()); i++) + { + matched = matchesInternal(cap, sfs.get(i)); + } + } + else if (sf.getOperation() == SimpleFilter.NOT) + { + // Evaluate each subfilter against the remaining capabilities. + // For OR we calculate the union of each subfilter. + List<SimpleFilter> sfs = (List<SimpleFilter>) sf.getValue(); + for (int i = 0; i < sfs.size(); i++) + { + matched = !(matchesInternal(cap, sfs.get(i))); + } + } + else + { + matched = false; + Object lhs = cap.getAttributes().get(sf.getName()); + if (lhs != null) + { + matched = compare(lhs, sf.getValue(), sf.getOperation()); + } + } + + return matched; + } + + private static Set<Capability> matchMandatory( + Set<Capability> caps, SimpleFilter sf) + { + for (Iterator<Capability> it = caps.iterator(); it.hasNext(); ) + { + Capability cap = it.next(); + if (!matchMandatory(cap, sf)) + { + it.remove(); + } + } + return caps; + } + + private static boolean matchMandatory(Capability cap, SimpleFilter sf) + { + Map<String, Object> attrs = cap.getAttributes(); + for (Entry<String, Object> entry : attrs.entrySet()) + { + if (isAttributeMandatory(cap, entry.getKey()) + && !matchMandatoryAttribute(entry.getKey(), sf)) + { + return false; + } + } + return true; + } + + private static boolean matchMandatoryAttribute(String attrName, SimpleFilter sf) + { + if ((sf.getName() != null) && sf.getName().equals(attrName)) + { + return true; + } + else if (sf.getOperation() == SimpleFilter.AND) + { + List list = (List) sf.getValue(); + for (int i = 0; i < list.size(); i++) + { + SimpleFilter sf2 = (SimpleFilter) list.get(i); + if ((sf2.getName() != null) + && sf2.getName().equals(attrName)) + { + return true; + } + } + } + return false; + } + + private static final Class<?>[] STRING_CLASS = new Class[] { String.class }; + private static final String VALUE_OF_METHOD_NAME = "valueOf"; + + private static boolean compare(Object lhs, Object rhsUnknown, int op) + { + if (lhs == null) + { + return false; + } + + // If this is a PRESENT operation, then just return true immediately + // since we wouldn't be here if the attribute wasn't present. + if (op == SimpleFilter.PRESENT) + { + return true; + } + + //Need a special case here when lhs is a Version and rhs is a VersionRange + //Version is comparable so we need to check this first + if(lhs instanceof Version && op == SimpleFilter.EQ) + { + Object rhs = null; + try + { + rhs = coerceType(lhs, (String) rhsUnknown); + } + catch (Exception ex) + { + //Do nothing will check later if rhs is null + } + + if(rhs != null && rhs instanceof VersionRange) + { + return ((VersionRange)rhs).isInRange((Version)lhs); + } + } + + // If the type is comparable, then we can just return the + // result immediately. + if (lhs instanceof Comparable) + { + // Spec says SUBSTRING is false for all types other than string. + if ((op == SimpleFilter.SUBSTRING) && !(lhs instanceof String)) + { + return false; + } + + Object rhs; + if (op == SimpleFilter.SUBSTRING) + { + rhs = rhsUnknown; + } + else + { + try + { + rhs = coerceType(lhs, (String) rhsUnknown); + } + catch (Exception ex) + { + return false; + } + } + + switch (op) + { + case SimpleFilter.EQ : + try + { + return (((Comparable) lhs).compareTo(rhs) == 0); + } + catch (Exception ex) + { + return false; + } + case SimpleFilter.GTE : + try + { + return (((Comparable) lhs).compareTo(rhs) >= 0); + } + catch (Exception ex) + { + return false; + } + case SimpleFilter.LTE : + try + { + return (((Comparable) lhs).compareTo(rhs) <= 0); + } + catch (Exception ex) + { + return false; + } + case SimpleFilter.APPROX : + return compareApproximate(lhs, rhs); + case SimpleFilter.SUBSTRING : + return SimpleFilter.compareSubstring((List<String>) rhs, (String) lhs); + default: + throw new RuntimeException( + "Unknown comparison operator: " + op); + } + } + // Booleans do not implement comparable, so special case them. + else if (lhs instanceof Boolean) + { + Object rhs; + try + { + rhs = coerceType(lhs, (String) rhsUnknown); + } + catch (Exception ex) + { + return false; + } + + switch (op) + { + case SimpleFilter.EQ : + case SimpleFilter.GTE : + case SimpleFilter.LTE : + case SimpleFilter.APPROX : + return (lhs.equals(rhs)); + default: + throw new RuntimeException( + "Unknown comparison operator: " + op); + } + } + + // If the LHS is not a comparable or boolean, check if it is an + // array. If so, convert it to a list so we can treat it as a + // collection. + if (lhs.getClass().isArray()) + { + lhs = convertArrayToList(lhs); + } + + // If LHS is a collection, then call compare() on each element + // of the collection until a match is found. + if (lhs instanceof Collection) + { + for (Iterator iter = ((Collection) lhs).iterator(); iter.hasNext(); ) + { + if (compare(iter.next(), rhsUnknown, op)) + { + return true; + } + } + + return false; + } + + // Spec says SUBSTRING is false for all types other than string. + if ((op == SimpleFilter.SUBSTRING) && !(lhs instanceof String)) + { + return false; + } + + // Since we cannot identify the LHS type, then we can only perform + // equality comparison. + try + { + return lhs.equals(coerceType(lhs, (String) rhsUnknown)); + } + catch (Exception ex) + { + return false; + } + } + + private static boolean compareApproximate(Object lhs, Object rhs) + { + if (rhs instanceof String) + { + return removeWhitespace((String) lhs) + .equalsIgnoreCase(removeWhitespace((String) rhs)); + } + else if (rhs instanceof Character) + { + return Character.toLowerCase(((Character) lhs)) + == Character.toLowerCase(((Character) rhs)); + } + return lhs.equals(rhs); + } + + private static String removeWhitespace(String s) + { + StringBuffer sb = new StringBuffer(s.length()); + for (int i = 0; i < s.length(); i++) + { + if (!Character.isWhitespace(s.charAt(i))) + { + sb.append(s.charAt(i)); + } + } + return sb.toString(); + } + + private static Object coerceType(Object lhs, String rhsString) throws Exception + { + // If the LHS expects a string, then we can just return + // the RHS since it is a string. + if (lhs.getClass() == rhsString.getClass()) + { + return rhsString; + } + + // Try to convert the RHS type to the LHS type by using + // the string constructor of the LHS class, if it has one. + Object rhs = null; + try + { + // The Character class is a special case, since its constructor + // does not take a string, so handle it separately. + if (lhs instanceof Character) + { + rhs = new Character(rhsString.charAt(0)); + } + else if(lhs instanceof Version && rhsString.indexOf(',') >= 0) + { + rhs = VersionRange.parse(rhsString); + } + else + { + // Spec says we should trim number types. + if ((lhs instanceof Number) || (lhs instanceof Boolean)) + { + rhsString = rhsString.trim(); + } + + try + { + // Try to find a suitable static valueOf method + Method valueOfMethod = lhs.getClass().getDeclaredMethod(VALUE_OF_METHOD_NAME, STRING_CLASS); + if (valueOfMethod.getReturnType().isAssignableFrom(lhs.getClass()) + && ((valueOfMethod.getModifiers() & Modifier.STATIC) > 0)) + { + valueOfMethod.setAccessible(true); + rhs = valueOfMethod.invoke(null, new Object[] { rhsString }); + } + } + catch (Exception ex) + { + // Static valueOf fails, try the next conversion mechanism + } + + if (rhs == null) + { + Constructor ctor = lhs.getClass().getConstructor(STRING_CLASS); + ctor.setAccessible(true); + rhs = ctor.newInstance(new Object[] { rhsString }); + } + } + } + catch (Exception ex) + { + throw new Exception( + "Could not instantiate class " + + lhs.getClass().getName() + + " from string constructor with argument '" + + rhsString + "' because " + ex); + } + + return rhs; + } + + /** + * This is an ugly utility method to convert an array of primitives + * to an array of primitive wrapper objects. This method simplifies + * processing LDAP filters since the special case of primitive arrays + * can be ignored. + * @param array An array of primitive types. + * @return An corresponding array using pritive wrapper objects. + **/ + private static List convertArrayToList(Object array) + { + int len = Array.getLength(array); + List list = new ArrayList(len); + for (int i = 0; i < len; i++) + { + list.add(Array.get(array, i)); + } + return list; + } + + + public static boolean matches(Capability capability, Requirement requirement) { + if (requirement.getNamespace().equals(capability.getNamespace())) { + String filter = (String) requirement.getAttributes().get(Constants.FILTER_DIRECTIVE); + if (filter != null) { + return matches(capability, SimpleFilter.parse(filter)); + } + return true; + } + return false; + } + + public static boolean isOptional(Requirement requirement) { + return RESOLUTION_OPTIONAL.equals(requirement. getDirectives().get(RESOLUTION_DIRECTIVE)); + } + + public static boolean isAttributeMandatory(Capability capability, String name) + { + String value = (String) capability.getDirectives().get(Constants.MANDATORY_DIRECTIVE); + if (value != null) + { + return parseDelimitedString(value, ",").contains(name); + } + return false; + } + + public static List<String> parseDelimitedString(String value, String delim) + { + return CapabilityMatcher.parseDelimitedString(value, delim, true); + } + + /** + * Parses delimited string and returns an array containing the tokens. This + * parser obeys quotes, so the delimiter character will be ignored if it is + * inside of a quote. This method assumes that the quote character is not + * included in the set of delimiter characters. + * @param value the delimited string to parse. + * @param delim the characters delimiting the tokens. + * @return a list of string or an empty list if there are none. + **/ + public static List<String> parseDelimitedString(String value, String delim, boolean trim) + { + if (value == null) + { + value = ""; + } + + List<String> list = new ArrayList<String>(); + + int CHAR = 1; + int DELIMITER = 2; + int STARTQUOTE = 4; + int ENDQUOTE = 8; + + StringBuffer sb = new StringBuffer(); + + int expecting = (CHAR | DELIMITER | STARTQUOTE); + + boolean isEscaped = false; + for (int i = 0; i < value.length(); i++) + { + char c = value.charAt(i); + + boolean isDelimiter = (delim.indexOf(c) >= 0); + + if (!isEscaped && (c == '\\')) + { + isEscaped = true; + continue; + } + + if (isEscaped) + { + sb.append(c); + } + else if (isDelimiter && ((expecting & DELIMITER) > 0)) + { + if (trim) + { + list.add(sb.toString().trim()); + } + else + { + list.add(sb.toString()); + } + sb.delete(0, sb.length()); + expecting = (CHAR | DELIMITER | STARTQUOTE); + } + else if ((c == '"') && ((expecting & STARTQUOTE) > 0)) + { + sb.append(c); + expecting = CHAR | ENDQUOTE; + } + else if ((c == '"') && ((expecting & ENDQUOTE) > 0)) + { + sb.append(c); + expecting = (CHAR | STARTQUOTE | DELIMITER); + } + else if ((expecting & CHAR) > 0) + { + sb.append(c); + } + else + { + throw new IllegalArgumentException("Invalid delimited string: " + value); + } + + isEscaped = false; + } + + if (sb.length() > 0) + { + if (trim) + { + list.add(sb.toString().trim()); + } + else + { + list.add(sb.toString()); + } + } + + return list; + } +} Added: sling/whiteboard/cziegeler/feature-support/src/main/java/org/apache/sling/feature/support/impl/SimpleFilter.java URL: http://svn.apache.org/viewvc/sling/whiteboard/cziegeler/feature-support/src/main/java/org/apache/sling/feature/support/impl/SimpleFilter.java?rev=1803574&view=auto ============================================================================== --- sling/whiteboard/cziegeler/feature-support/src/main/java/org/apache/sling/feature/support/impl/SimpleFilter.java (added) +++ sling/whiteboard/cziegeler/feature-support/src/main/java/org/apache/sling/feature/support/impl/SimpleFilter.java Mon Jul 31 20:51:16 2017 @@ -0,0 +1,647 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sling.feature.support.impl; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +public class SimpleFilter +{ + public static final int MATCH_ALL = 0; + public static final int AND = 1; + public static final int OR = 2; + public static final int NOT = 3; + public static final int EQ = 4; + public static final int LTE = 5; + public static final int GTE = 6; + public static final int SUBSTRING = 7; + public static final int PRESENT = 8; + public static final int APPROX = 9; + + private final String m_name; + private final Object m_value; + private final int m_op; + + public SimpleFilter(String attr, Object value, int op) + { + m_name = attr; + m_value = value; + m_op = op; + } + + public String getName() + { + return m_name; + } + + public Object getValue() + { + return m_value; + } + + public int getOperation() + { + return m_op; + } + + public String toString() + { + String s = null; + switch (m_op) + { + case AND: + s = "(&" + toString((List) m_value) + ")"; + break; + case OR: + s = "(|" + toString((List) m_value) + ")"; + break; + case NOT: + s = "(!" + toString((List) m_value) + ")"; + break; + case EQ: + s = "(" + m_name + "=" + toEncodedString(m_value) + ")"; + break; + case LTE: + s = "(" + m_name + "<=" + toEncodedString(m_value) + ")"; + break; + case GTE: + s = "(" + m_name + ">=" + toEncodedString(m_value) + ")"; + break; + case SUBSTRING: + s = "(" + m_name + "=" + unparseSubstring((List<String>) m_value) + ")"; + break; + case PRESENT: + s = "(" + m_name + "=*)"; + break; + case APPROX: + s = "(" + m_name + "~=" + toEncodedString(m_value) + ")"; + break; + case MATCH_ALL: + s = "(*)"; + break; + } + return s; + } + + private static String toString(List list) + { + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < list.size(); i++) + { + sb.append(list.get(i).toString()); + } + return sb.toString(); + } + + private static String toDecodedString(String s, int startIdx, int endIdx) + { + StringBuffer sb = new StringBuffer(endIdx - startIdx); + boolean escaped = false; + for (int i = 0; i < (endIdx - startIdx); i++) + { + char c = s.charAt(startIdx + i); + if (!escaped && (c == '\\')) + { + escaped = true; + } + else + { + escaped = false; + sb.append(c); + } + } + + return sb.toString(); + } + + private static String toEncodedString(Object o) + { + if (o instanceof String) + { + String s = (String) o; + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < s.length(); i++) + { + char c = s.charAt(i); + if ((c == '\\') || (c == '(') || (c == ')') || (c == '*')) + { + sb.append('\\'); + } + sb.append(c); + } + + o = sb.toString(); + } + + return o.toString(); + } + + public static SimpleFilter parse(String filter) + { + int idx = skipWhitespace(filter, 0); + + if ((filter == null) || (filter.length() == 0) || (idx >= filter.length())) + { + throw new IllegalArgumentException("Null or empty filter."); + } + else if (filter.charAt(idx) != '(') + { + throw new IllegalArgumentException("Missing opening parenthesis: " + filter); + } + + SimpleFilter sf = null; + List stack = new ArrayList(); + boolean isEscaped = false; + while (idx < filter.length()) + { + if (sf != null) + { + throw new IllegalArgumentException( + "Only one top-level operation allowed: " + filter); + } + + if (!isEscaped && (filter.charAt(idx) == '(')) + { + // Skip paren and following whitespace. + idx = skipWhitespace(filter, idx + 1); + + if (filter.charAt(idx) == '&') + { + int peek = skipWhitespace(filter, idx + 1); + if (filter.charAt(peek) == '(') + { + idx = peek - 1; + stack.add(0, new SimpleFilter(null, new ArrayList(), SimpleFilter.AND)); + } + else + { + stack.add(0, new Integer(idx)); + } + } + else if (filter.charAt(idx) == '|') + { + int peek = skipWhitespace(filter, idx + 1); + if (filter.charAt(peek) == '(') + { + idx = peek - 1; + stack.add(0, new SimpleFilter(null, new ArrayList(), SimpleFilter.OR)); + } + else + { + stack.add(0, new Integer(idx)); + } + } + else if (filter.charAt(idx) == '!') + { + int peek = skipWhitespace(filter, idx + 1); + if (filter.charAt(peek) == '(') + { + idx = peek - 1; + stack.add(0, new SimpleFilter(null, new ArrayList(), SimpleFilter.NOT)); + } + else + { + stack.add(0, new Integer(idx)); + } + } + else + { + stack.add(0, new Integer(idx)); + } + } + else if (!isEscaped && (filter.charAt(idx) == ')')) + { + Object top = stack.remove(0); + if (top instanceof SimpleFilter) + { + if (!stack.isEmpty() && (stack.get(0) instanceof SimpleFilter)) + { + ((List) ((SimpleFilter) stack.get(0)).m_value).add(top); + } + else + { + sf = (SimpleFilter) top; + } + } + else if (!stack.isEmpty() && (stack.get(0) instanceof SimpleFilter)) + { + ((List) ((SimpleFilter) stack.get(0)).m_value).add( + SimpleFilter.subfilter(filter, ((Integer) top).intValue(), idx)); + } + else + { + sf = SimpleFilter.subfilter(filter, ((Integer) top).intValue(), idx); + } + } + else if (!isEscaped && (filter.charAt(idx) == '\\')) + { + isEscaped = true; + } + else + { + isEscaped = false; + } + + idx = skipWhitespace(filter, idx + 1); + } + + if (sf == null) + { + throw new IllegalArgumentException("Missing closing parenthesis: " + filter); + } + + return sf; + } + + private static SimpleFilter subfilter(String filter, int startIdx, int endIdx) + { + final String opChars = "=<>~"; + + // Determine the ending index of the attribute name. + int attrEndIdx = startIdx; + for (int i = 0; i < (endIdx - startIdx); i++) + { + char c = filter.charAt(startIdx + i); + if (opChars.indexOf(c) >= 0) + { + break; + } + else if (!Character.isWhitespace(c)) + { + attrEndIdx = startIdx + i + 1; + } + } + if (attrEndIdx == startIdx) + { + throw new IllegalArgumentException( + "Missing attribute name: " + filter.substring(startIdx, endIdx)); + } + String attr = filter.substring(startIdx, attrEndIdx); + + // Skip the attribute name and any following whitespace. + startIdx = skipWhitespace(filter, attrEndIdx); + + // Determine the operator type. + int op = -1; + switch (filter.charAt(startIdx)) + { + case '=': + op = EQ; + startIdx++; + break; + case '<': + if (filter.charAt(startIdx + 1) != '=') + { + throw new IllegalArgumentException( + "Unknown operator: " + filter.substring(startIdx, endIdx)); + } + op = LTE; + startIdx += 2; + break; + case '>': + if (filter.charAt(startIdx + 1) != '=') + { + throw new IllegalArgumentException( + "Unknown operator: " + filter.substring(startIdx, endIdx)); + } + op = GTE; + startIdx += 2; + break; + case '~': + if (filter.charAt(startIdx + 1) != '=') + { + throw new IllegalArgumentException( + "Unknown operator: " + filter.substring(startIdx, endIdx)); + } + op = APPROX; + startIdx += 2; + break; + default: + throw new IllegalArgumentException( + "Unknown operator: " + filter.substring(startIdx, endIdx)); + } + + // Parse value. + Object value = toDecodedString(filter, startIdx, endIdx); + + // Check if the equality comparison is actually a substring + // or present operation. + if (op == EQ) + { + String valueStr = filter.substring(startIdx, endIdx); + List<String> values = parseSubstring(valueStr); + if ((values.size() == 2) + && (values.get(0).length() == 0) + && (values.get(1).length() == 0)) + { + op = PRESENT; + } + else if (values.size() > 1) + { + op = SUBSTRING; + value = values; + } + } + + return new SimpleFilter(attr, value, op); + } + + public static List<String> parseSubstring(String value) + { + List<String> pieces = new ArrayList(); + StringBuffer ss = new StringBuffer(); + // int kind = SIMPLE; // assume until proven otherwise + boolean wasStar = false; // indicates last piece was a star + boolean leftstar = false; // track if the initial piece is a star + boolean rightstar = false; // track if the final piece is a star + + int idx = 0; + + // We assume (sub)strings can contain leading and trailing blanks + boolean escaped = false; + loop: for (;;) + { + if (idx >= value.length()) + { + if (wasStar) + { + // insert last piece as "" to handle trailing star + rightstar = true; + } + else + { + pieces.add(ss.toString()); + // accumulate the last piece + // note that in the case of + // (cn=); this might be + // the string "" (!=null) + } + ss.setLength(0); + break loop; + } + + // Read the next character and account for escapes. + char c = value.charAt(idx++); + if (!escaped && (c == '*')) + { + // If we have successive '*' characters, then we can + // effectively collapse them by ignoring succeeding ones. + if (!wasStar) + { + if (ss.length() > 0) + { + pieces.add(ss.toString()); // accumulate the pieces + // between '*' occurrences + } + ss.setLength(0); + // if this is a leading star, then track it + if (pieces.isEmpty()) + { + leftstar = true; + } + wasStar = true; + } + } + else if (!escaped && (c == '\\')) + { + escaped = true; + } + else + { + escaped = false; + wasStar = false; + ss.append(c); + } + } + if (leftstar || rightstar || pieces.size() > 1) + { + // insert leading and/or trailing "" to anchor ends + if (rightstar) + { + pieces.add(""); + } + if (leftstar) + { + pieces.add(0, ""); + } + } + return pieces; + } + + public static String unparseSubstring(List<String> pieces) + { + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < pieces.size(); i++) + { + if (i > 0) + { + sb.append("*"); + } + sb.append(toEncodedString(pieces.get(i))); + } + return sb.toString(); + } + + public static boolean compareSubstring(List<String> pieces, String s) + { + // Walk the pieces to match the string + // There are implicit stars between each piece, + // and the first and last pieces might be "" to anchor the match. + // assert (pieces.length > 1) + // minimal case is <string>*<string> + + boolean result = true; + int len = pieces.size(); + + // Special case, if there is only one piece, then + // we must perform an equality test. + if (len == 1) + { + return s.equals(pieces.get(0)); + } + + // Otherwise, check whether the pieces match + // the specified string. + + int index = 0; + + loop: for (int i = 0; i < len; i++) + { + String piece = pieces.get(i); + + // If this is the first piece, then make sure the + // string starts with it. + if (i == 0) + { + if (!s.startsWith(piece)) + { + result = false; + break loop; + } + } + + // If this is the last piece, then make sure the + // string ends with it. + if (i == (len - 1)) + { + if (s.endsWith(piece) && (s.length() >= (index + piece.length()))) + { + result = true; + } + else + { + result = false; + } + break loop; + } + + // If this is neither the first or last piece, then + // make sure the string contains it. + if ((i > 0) && (i < (len - 1))) + { + index = s.indexOf(piece, index); + if (index < 0) + { + result = false; + break loop; + } + } + + // Move string index beyond the matching piece. + index += piece.length(); + } + + return result; + } + + private static int skipWhitespace(String s, int startIdx) + { + int len = s.length(); + while ((startIdx < len) && Character.isWhitespace(s.charAt(startIdx))) + { + startIdx++; + } + return startIdx; + } + + /** + * Converts a attribute map to a filter. The filter is created by iterating + * over the map's entry set. If ordering of attributes is important (e.g., + * for hitting attribute indices), then the map's entry set should iterate + * in the desired order. Equality testing is assumed for all attribute types + * other than version ranges, which are handled appropriated. If the attribute + * map is empty, then a filter that matches anything is returned. + * @param attrs Map of attributes to convert to a filter. + * @return A filter corresponding to the attributes. + */ + public static SimpleFilter convert(Map<String, Object> attrs) + { + // Rather than building a filter string to be parsed into a SimpleFilter, + // we will just create the parsed SimpleFilter directly. + + List<SimpleFilter> filters = new ArrayList<SimpleFilter>(); + + for (Entry<String, Object> entry : attrs.entrySet()) + { + if (entry.getValue() instanceof VersionRange) + { + VersionRange vr = (VersionRange) entry.getValue(); + if (vr.isFloorInclusive()) + { + filters.add( + new SimpleFilter( + entry.getKey(), + vr.getFloor().toString(), + SimpleFilter.GTE)); + } + else + { + SimpleFilter not = + new SimpleFilter(null, new ArrayList(), SimpleFilter.NOT); + ((List) not.getValue()).add( + new SimpleFilter( + entry.getKey(), + vr.getFloor().toString(), + SimpleFilter.LTE)); + filters.add(not); + } + + if (vr.getCeiling() != null) + { + if (vr.isCeilingInclusive()) + { + filters.add( + new SimpleFilter( + entry.getKey(), + vr.getCeiling().toString(), + SimpleFilter.LTE)); + } + else + { + SimpleFilter not = + new SimpleFilter(null, new ArrayList(), SimpleFilter.NOT); + ((List) not.getValue()).add( + new SimpleFilter( + entry.getKey(), + vr.getCeiling().toString(), + SimpleFilter.GTE)); + filters.add(not); + } + } + } + else + { + List<String> values = SimpleFilter.parseSubstring(entry.getValue().toString()); + if (values.size() > 1) + { + filters.add( + new SimpleFilter( + entry.getKey(), + values, + SimpleFilter.SUBSTRING)); + } + else + { + filters.add( + new SimpleFilter( + entry.getKey(), + values.get(0), + SimpleFilter.EQ)); + } + } + } + + SimpleFilter sf = null; + + if (filters.size() == 1) + { + sf = filters.get(0); + } + else if (attrs.size() > 1) + { + sf = new SimpleFilter(null, filters, SimpleFilter.AND); + } + else if (filters.isEmpty()) + { + sf = new SimpleFilter(null, null, SimpleFilter.MATCH_ALL); + } + + return sf; + } +} Added: sling/whiteboard/cziegeler/feature-support/src/main/java/org/apache/sling/feature/support/impl/StringComparator.java URL: http://svn.apache.org/viewvc/sling/whiteboard/cziegeler/feature-support/src/main/java/org/apache/sling/feature/support/impl/StringComparator.java?rev=1803574&view=auto ============================================================================== --- sling/whiteboard/cziegeler/feature-support/src/main/java/org/apache/sling/feature/support/impl/StringComparator.java (added) +++ sling/whiteboard/cziegeler/feature-support/src/main/java/org/apache/sling/feature/support/impl/StringComparator.java Mon Jul 31 20:51:16 2017 @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sling.feature.support.impl; + +import java.util.Comparator; + +public class StringComparator implements Comparator<String> +{ + + public static final StringComparator COMPARATOR = new StringComparator(); + + public int compare(String s1, String s2) + { + int n1 = s1.length(); + int n2 = s2.length(); + int min = n1 < n2 ? n1 : n2; + for ( int i = 0; i < min; i++ ) + { + char c1 = s1.charAt( i ); + char c2 = s2.charAt( i ); + if ( c1 != c2 ) + { + // Fast check for simple ascii codes + if ( c1 <= 128 && c2 <= 128 ) + { + c1 = toLowerCaseFast(c1); + c2 = toLowerCaseFast(c2); + if ( c1 != c2 ) + { + return c1 - c2; + } + } + else + { + c1 = Character.toUpperCase( c1 ); + c2 = Character.toUpperCase( c2 ); + if ( c1 != c2 ) + { + c1 = Character.toLowerCase( c1 ); + c2 = Character.toLowerCase( c2 ); + if ( c1 != c2 ) + { + // No overflow because of numeric promotion + return c1 - c2; + } + } + } + } + } + return n1 - n2; + } + + private static char toLowerCaseFast( char ch ) + { + return ( ch >= 'A' && ch <= 'Z' ) ? ( char ) ( ch + 'a' - 'A' ) : ch; + } +} Added: sling/whiteboard/cziegeler/feature-support/src/main/java/org/apache/sling/feature/support/impl/VersionRange.java URL: http://svn.apache.org/viewvc/sling/whiteboard/cziegeler/feature-support/src/main/java/org/apache/sling/feature/support/impl/VersionRange.java?rev=1803574&view=auto ============================================================================== --- sling/whiteboard/cziegeler/feature-support/src/main/java/org/apache/sling/feature/support/impl/VersionRange.java (added) +++ sling/whiteboard/cziegeler/feature-support/src/main/java/org/apache/sling/feature/support/impl/VersionRange.java Mon Jul 31 20:51:16 2017 @@ -0,0 +1,157 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sling.feature.support.impl; + +import org.osgi.framework.Version; + +public class VersionRange +{ + private final Version m_floor; + private final boolean m_isFloorInclusive; + private final Version m_ceiling; + private final boolean m_isCeilingInclusive; + public static final VersionRange infiniteRange + = new VersionRange(Version.emptyVersion, true, null, true); + + public VersionRange( + Version low, boolean isLowInclusive, + Version high, boolean isHighInclusive) + { + m_floor = low; + m_isFloorInclusive = isLowInclusive; + m_ceiling = high; + m_isCeilingInclusive = isHighInclusive; + } + + public Version getFloor() + { + return m_floor; + } + + public boolean isFloorInclusive() + { + return m_isFloorInclusive; + } + + public Version getCeiling() + { + return m_ceiling; + } + + public boolean isCeilingInclusive() + { + return m_isCeilingInclusive; + } + + public boolean isInRange(Version version) + { + // We might not have an upper end to the range. + if (m_ceiling == null) + { + return (version.compareTo(m_floor) >= 0); + } + else if (isFloorInclusive() && isCeilingInclusive()) + { + return (version.compareTo(m_floor) >= 0) && (version.compareTo(m_ceiling) <= 0); + } + else if (isCeilingInclusive()) + { + return (version.compareTo(m_floor) > 0) && (version.compareTo(m_ceiling) <= 0); + } + else if (isFloorInclusive()) + { + return (version.compareTo(m_floor) >= 0) && (version.compareTo(m_ceiling) < 0); + } + return (version.compareTo(m_floor) > 0) && (version.compareTo(m_ceiling) < 0); + } + + public static VersionRange parse(String range) + { + // Check if the version is an interval. + if (range.indexOf(',') >= 0) + { + String s = range.substring(1, range.length() - 1); + String vlo = s.substring(0, s.indexOf(',')).trim(); + String vhi = s.substring(s.indexOf(',') + 1, s.length()).trim(); + return new VersionRange ( + new Version(vlo), (range.charAt(0) == '['), + new Version(vhi), (range.charAt(range.length() - 1) == ']')); + } + else + { + return new VersionRange(new Version(range), true, null, false); + } + } + + public boolean equals(Object obj) + { + if (obj == null) + { + return false; + } + if (getClass() != obj.getClass()) + { + return false; + } + final VersionRange other = (VersionRange) obj; + if (m_floor != other.m_floor && (m_floor == null || !m_floor.equals(other.m_floor))) + { + return false; + } + if (m_isFloorInclusive != other.m_isFloorInclusive) + { + return false; + } + if (m_ceiling != other.m_ceiling && (m_ceiling == null || !m_ceiling.equals(other.m_ceiling))) + { + return false; + } + if (m_isCeilingInclusive != other.m_isCeilingInclusive) + { + return false; + } + return true; + } + + public int hashCode() + { + int hash = 5; + hash = 97 * hash + (m_floor != null ? m_floor.hashCode() : 0); + hash = 97 * hash + (m_isFloorInclusive ? 1 : 0); + hash = 97 * hash + (m_ceiling != null ? m_ceiling.hashCode() : 0); + hash = 97 * hash + (m_isCeilingInclusive ? 1 : 0); + return hash; + } + + public String toString() + { + if (m_ceiling != null) + { + StringBuffer sb = new StringBuffer(); + sb.append(m_isFloorInclusive ? '[' : '('); + sb.append(m_floor.toString()); + sb.append(','); + sb.append(m_ceiling.toString()); + sb.append(m_isCeilingInclusive ? ']' : ')'); + return sb.toString(); + } + else + { + return m_floor.toString(); + } + } +}