/******************************************************************************* * Copyright (c) 2006-2013, Cloudsmith Inc. * The code, documentation and other materials contained herein have been * licensed under the Eclipse Public License - v 1.0 by the copyright holder * listed above, as the Initial Contributor under such license. The text or * such license is available at www.eclipse.org. ******************************************************************************/ package org.eclipse.buckminster.osgi.filter.impl; import java.util.AbstractMap; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Dictionary; import java.util.Enumeration; import java.util.HashMap; import java.util.Iterator; import java.util.Locale; import java.util.Map; import java.util.Set; import org.eclipse.buckminster.osgi.filter.Filter; import org.osgi.framework.ServiceReference; abstract class FilterImpl implements Filter, Comparable<FilterImpl> { /** * A Map that performs case insensitive String lookups on the Map that it * contains. */ private static class CaseInsensitiveMap<V> extends AbstractMap<String, V> { private final Map<String, String> lowerCaseMap; private final Map<String, ? extends V> map; CaseInsensitiveMap(Map<String, ? extends V> map) { int top = map.size(); Map<String, String> lcMap = null; for (String key : map.keySet()) { String lowKey = key.toLowerCase(Locale.ENGLISH); if (key != lowKey) { if (lcMap == null) lcMap = new HashMap<String, String>(top); if (lcMap.put(lowKey, key) != null || map.containsKey(lowKey)) throw new IllegalArgumentException("case variants of key: " + lowKey); //$NON-NLS-1$ } } this.lowerCaseMap = lcMap; this.map = map; } @SuppressWarnings("unchecked") @Override public Set<Map.Entry<String, V>> entrySet() { return ((Map<String, V>) map).entrySet(); } @Override public V get(Object key) { String stringKey = ((String) key).toLowerCase(Locale.ENGLISH); if (lowerCaseMap != null) { String realKey = lowerCaseMap.get(stringKey); if (realKey != null) stringKey = realKey; } return map.get(stringKey); } } /** * This Map is used for key lookup from {@link Dictionary} instances that do * not implement the {@link Map} interface (most implementations do since * they extend the {@link HashMap}). The implementation only supports the * get operation using a String key as no other operations are used by the * Filter implementation. */ @SuppressWarnings("serial") private static class DictionaryMap extends HashMap<String, Object> { private final boolean caseSensitive; DictionaryMap(Dictionary<String, ? extends Object> dictionary, boolean caseSensitive) { super(dictionary.size()); Enumeration<String> keys = dictionary.keys(); while (keys.hasMoreElements()) { String key = keys.nextElement(); Object value = dictionary.get(key); if (!caseSensitive) { String lowKey = key.toLowerCase(Locale.ENGLISH); if (containsKey(lowKey)) throw new IllegalArgumentException("case variants of key: " + lowKey); //$NON-NLS-1$ key = lowKey; } put(key, value); } this.caseSensitive = caseSensitive; } @Override public Set<Map.Entry<String, Object>> entrySet() { throw new UnsupportedOperationException(); } @Override public Object get(Object key) { return caseSensitive ? super.get(key) : super.get(((String) key).toLowerCase(Locale.ENGLISH)); } } /** * This Map is used for key lookup from a ServiceReference during filter * evaluation. The Map implementation only supports the get operation using * a String key as no other operations are used by the Filter * implementation. */ private static class ServiceReferenceMap extends AbstractMap<String, Object> { private final ServiceReference<?> reference; ServiceReferenceMap(ServiceReference<?> reference) { this.reference = reference; } @Override public Set<Map.Entry<String, Object>> entrySet() { throw new UnsupportedOperationException(); } @Override public Object get(Object key) { return reference.getProperty((String) key); } } static final int LESS = 4; static final int PRESENT = 5; static final int SUBSTRING = 6; static final int AND = 7; static final int OR = 8; static final int NOT = 9; /** * Map a string for an APPROX (~=) comparison. * * This implementation removes white spaces. This is the minimum * implementation allowed by the OSGi spec. * * @param input * Input string. * @return String ready for APPROX comparison. */ static String approxString(String input) { boolean changed = false; char[] output = input.toCharArray(); int cursor = 0; for (int i = 0, length = output.length; i < length; i++) { char c = output[i]; if (Character.isWhitespace(c)) { changed = true; continue; } output[cursor] = c; cursor++; } return changed ? new String(output, 0, cursor) : input; } /** * Encode the value string such that '(', '*', ')' and '\' are escaped. * * @param value * unencoded value string. * @return encoded value string. */ static String encodeValue(String value) { int inlen = value.length(); char[] output = null; int cursor = 0; for (int i = 0; i < inlen; i++) { char c = value.charAt(i); switch (c) { case '(': case '*': case ')': case '\\': if (output == null) { output = new char[inlen << 2]; if (i > 0) { value.getChars(0, i, output, 0); cursor = i; } } output[cursor++] = '\\'; break; } if (output != null) output[cursor++] = c; } return output == null ? value : new String(output, 0, cursor); } private final int op; static final int EQUAL = 1; static final int APPROX = 2; static final int GREATER = 3; /** filter attribute or null if operation AND, OR or NOT */ private final String attr; FilterImpl(int operation, String attr) { this.op = operation; this.attr = attr; } @Override public void addConsultedAttributes(Map<String, String[]> propertyChoices) { String stringValue = getValueAsString(); // Add the attribute value as a valid choice for the attribute // unless it's already present. // synchronized (propertyChoices) { String[] choices = propertyChoices.get(getAttr()); if (choices == null) { propertyChoices.put(getAttr(), stringValue == null ? new String[0] : new String[] { stringValue }); return; } if (stringValue == null) // Since choices already exists for this attribute and this // apparently is a null check return; int top = choices.length; int idx = top; while (--idx >= 0) if (stringValue.equals(choices[idx])) return; String[] newChoices = new String[top + 1]; System.arraycopy(choices, 0, newChoices, 0, top); newChoices[top] = stringValue; propertyChoices.put(getAttr(), newChoices); } } @Override public Filter addFilterWithAnd(Filter subFilter) { return subFilter == null ? this : addFilter((FilterImpl) subFilter, AND); } @Override public Filter addFilterWithOr(Filter subFilter) { return subFilter == null ? this : addFilter((FilterImpl) subFilter, OR); } @Override public boolean equals(Object obj) { return obj == this || (obj instanceof FilterImpl && compareTo((FilterImpl) obj) == 0) || (obj instanceof Filter && toString().equals(obj.toString())); } public int getOp() { return op; } @Override public int hashCode() { return 11 * toString().hashCode(); } @Override @SuppressWarnings("rawtypes") public boolean match(Dictionary properties) { return match(properties, false); } @Override public boolean match(ServiceReference<?> reference) { return match0(reference == null ? Collections.<String, Object> emptyMap() : new ServiceReferenceMap(reference)); } @Override @SuppressWarnings("rawtypes") public boolean matchCase(Dictionary dictionary) { return match(dictionary, true); } @Override public boolean matchCase(Map<String, ? extends Object> properties) { return match0(properties == null ? Collections.<String, Object> emptyMap() : properties); } @Override public boolean matches(Map<String, ? extends Object> properties) { return match0(properties == null ? Collections.<String, Object> emptyMap() : new CaseInsensitiveMap<Object>(properties)); } @Override public Filter stripFilter(Filter subFilter) { return equals(subFilter) ? null : this; } @Override public String toString() { StringBuilder bld = new StringBuilder(); toString(bld); return bld.toString(); } FilterImpl addFilter(FilterImpl subFilter, int operator) { int cmp = compareTo(subFilter); if (cmp == 0) return this; ArrayList<FilterImpl> filters = new ArrayList<FilterImpl>(2); filters.add(this); filters.add(subFilter); return Parser.normalize(filters, operator); } final boolean compare(Object value1) { if (value1 == null) return false; if (value1 instanceof Collection<?>) { for (Iterator<?> iterator = ((Collection<?>) value1).iterator(); iterator.hasNext();) if (compare(iterator.next())) return true; return false; } if (value1 instanceof Object[]) { Object[] array = (Object[]) value1; for (int i = 0, size = array.length; i < size; i++) if (compare(array[i])) return true; return false; } return internalCompare(value1); } String getAttr() { return attr; } FilterImpl[] getFilterImpls() { return new FilterImpl[] { this }; } String getValueAsString() { return null; } boolean internalCompare(Object value) { return false; } int internalCompareTo(FilterImpl o) { int cmp = op > o.op ? 1 : (op < o.op ? -1 : 0); if (cmp == 0) { if (attr == null) { if (o.attr != null) cmp = -1; } else { cmp = o.attr == null ? 1 : attr.compareTo(o.attr); } } return cmp; } abstract boolean match0(Map<String, ? extends Object> properties); abstract void toString(StringBuilder bld); @SuppressWarnings({ "rawtypes", "unchecked" }) private boolean match(Dictionary dictionary, boolean caseSensitive) { Map props = null; if (dictionary instanceof Map) { props = (Map) dictionary; if (!caseSensitive) props = new CaseInsensitiveMap(props); } else if (dictionary == null) props = Collections.emptyMap(); else props = new DictionaryMap(dictionary, caseSensitive); return match0(props); } }