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();
+        }
+    }
+}


Reply via email to