blob: a10b30eef171f96748a689fdca7887b1df8235e6 [file] [log] [blame]
/*
* 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.felix.cm.impl;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Dictionary;
import java.util.Iterator;
import java.util.List;
import org.osgi.framework.InvalidSyntaxException;
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;
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
toString(sb);
return sb.toString();
}
private void toString(StringBuilder sb)
{
switch (m_op)
{
case AND:
sb.append("(&");
toString(sb, (List) m_value);
sb.append(")");
break;
case OR:
sb.append("(|");
toString(sb, (List) m_value);
sb.append(")");
break;
case NOT:
sb.append("(!");
toString(sb, (List) m_value);
sb.append(")");
break;
case EQ:
sb.append("(")
.append(m_name)
.append("=");
toEncodedString(sb, m_value);
sb.append(")");
break;
case LTE:
sb.append("(")
.append(m_name)
.append("<=");
toEncodedString(sb, m_value);
sb.append(")");
break;
case GTE:
sb.append("(")
.append(m_name)
.append(">=");
toEncodedString(sb, m_value);
sb.append(")");
break;
case SUBSTRING:
sb.append("(").append(m_name).append("=");
unparseSubstring(sb, (List) m_value);
sb.append(")");
break;
case PRESENT:
sb.append("(").append(m_name).append("=*)");
break;
case APPROX:
sb.append("(").append(m_name).append("~=");
toEncodedString(sb, m_value);
sb.append(")");
break;
case MATCH_ALL:
sb.append("(*)");
break;
}
}
private static void toString(StringBuilder sb, List list)
{
for (Object o : list)
{
SimpleFilter sf = (SimpleFilter) o;
sf.toString(sb);
}
}
private static String toDecodedString(String s, int startIdx, int endIdx)
{
StringBuilder sb = new StringBuilder(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 void toEncodedString(StringBuilder sb, Object o)
{
if (o instanceof String)
{
String s = (String) o;
for (int i = 0; i < s.length(); i++)
{
char c = s.charAt(i);
if ((c == '\\') || (c == '(') || (c == ')') || (c == '*'))
{
sb.append('\\');
}
sb.append(c);
}
}
else
{
sb.append(o);
}
}
public static SimpleFilter parse(final String filter) throws InvalidSyntaxException
{
int idx = skipWhitespace(filter, 0);
if ((filter == null) || (filter.length() == 0) || (idx >= filter.length()))
{
throw new InvalidSyntaxException("Null or empty filter.", filter);
}
else if (filter.charAt(idx) != '(')
{
throw new InvalidSyntaxException("Missing opening parenthesis", filter);
}
SimpleFilter sf = null;
List<Object> stack = new ArrayList<Object>();
boolean isEscaped = false;
while (idx < filter.length())
{
if (sf != null)
{
throw new InvalidSyntaxException(
"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, 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, 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, idx);
}
}
else
{
stack.add(0, 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, idx));
}
else
{
sf = SimpleFilter.subfilter(filter, (Integer) top, idx);
}
}
else if (!isEscaped && (filter.charAt(idx) == '\\'))
{
isEscaped = true;
}
else
{
isEscaped = false;
}
idx = skipWhitespace(filter, idx + 1);
}
if (sf == null)
{
throw new InvalidSyntaxException("Missing closing parenthesis", filter);
}
return sf;
}
private static SimpleFilter subfilter(String filter, int startIdx, int endIdx) throws InvalidSyntaxException
{
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 InvalidSyntaxException(
"Missing attribute name: " + filter.substring(startIdx, endIdx), filter);
}
String attr = filter.substring(startIdx, attrEndIdx);
// Skip the attribute name and any following whitespace.
startIdx = skipWhitespace(filter, attrEndIdx);
// Determine the operator type.
int op;
switch (filter.charAt(startIdx))
{
case '=':
op = EQ;
startIdx++;
break;
case '<':
if (filter.charAt(startIdx + 1) != '=')
{
throw new InvalidSyntaxException(
"Unknown operator: " + filter.substring(startIdx, endIdx), filter);
}
op = LTE;
startIdx += 2;
break;
case '>':
if (filter.charAt(startIdx + 1) != '=')
{
throw new InvalidSyntaxException(
"Unknown operator: " + filter.substring(startIdx, endIdx), filter);
}
op = GTE;
startIdx += 2;
break;
case '~':
if (filter.charAt(startIdx + 1) != '=')
{
throw new InvalidSyntaxException(
"Unknown operator: " + filter.substring(startIdx, endIdx), filter);
}
op = APPROX;
startIdx += 2;
break;
default:
throw new InvalidSyntaxException(
"Unknown operator: " + filter.substring(startIdx, endIdx), filter);
}
// 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<String>();
StringBuilder ss = new StringBuilder();
// 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 void unparseSubstring(StringBuilder sb, List<String> pieces)
{
for (int i = 0; i < pieces.size(); i++)
{
if (i > 0)
{
sb.append("*");
}
toEncodedString(sb, pieces.get(i));
}
}
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;
}
public boolean matches(Dictionary dict)
{
boolean matched = true;
if (getOperation() == SimpleFilter.MATCH_ALL)
{
matched = true;
}
else if (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>) getValue();
for (int i = 0; matched && (i < sfs.size()); i++)
{
matched = sfs.get(i).matches(dict);
}
}
else if (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>) getValue();
for (int i = 0; !matched && (i < sfs.size()); i++)
{
matched = sfs.get(i).matches(dict);
}
}
else if (getOperation() == SimpleFilter.NOT)
{
// Evaluate each subfilter against the remaining capabilities.
// For OR we calculate the union of each subfilter.
List<SimpleFilter> sfs = (List<SimpleFilter>) getValue();
for (int i = 0; i < sfs.size(); i++)
{
matched = !sfs.get(i).matches(dict);
}
}
else
{
matched = false;
Object lhs = dict.get( getName() );
if (lhs != null)
{
matched = compare(lhs, getValue(), getOperation());
}
}
return matched;
}
private static final Class<?>[] STRING_CLASS = new Class[] { String.class };
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;
}
// 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)
{
StringBuilder sb = new StringBuilder(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 = rhsString.charAt(0);
}
else
{
// Spec says we should trim number types.
if ((lhs instanceof Number) || (lhs instanceof Boolean))
{
rhsString = rhsString.trim();
}
Constructor ctor = lhs.getClass().getConstructor(STRING_CLASS);
ctor.setAccessible(true);
rhs = ctor.newInstance(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<Object> list = new ArrayList<Object>(len);
for (int i = 0; i < len; i++)
{
list.add(Array.get(array, i));
}
return list;
}
}