/*
* 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.dm.impl.index.multiproperty;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.CopyOnWriteArrayList;
import org.apache.felix.dm.FilterIndex;
import org.apache.felix.dm.tracker.ServiceTracker;
import org.apache.felix.dm.tracker.ServiceTrackerCustomizer;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceEvent;
import org.osgi.framework.ServiceListener;
import org.osgi.framework.ServiceReference;
/**
* @author <a href="mailto:dev@felix.apache.org">Felix Project Team</a>
*/
@SuppressWarnings("rawtypes")
public class MultiPropertyFilterIndex implements FilterIndex, ServiceTrackerCustomizer {
private final Object m_lock = new Object();
private ServiceTracker m_tracker;
private BundleContext m_context;
private Map<String, Property> m_configProperties = new LinkedHashMap<>();
private List<String> m_negatePropertyKeys = new ArrayList<>();
private final Map<MultiPropertyKey, List<ServiceReference>> m_keyToServiceReferencesMap = new HashMap<>();
private final Map<MultiPropertyKey, List<ServiceListener>> m_keyToListenersMap = new HashMap<>();
private final Map<ServiceListener, String> m_listenerToFilterMap = new HashMap<>();
public MultiPropertyFilterIndex(String configString) {
parseConfig(configString);
}
public boolean isApplicable(String clazz, String filterString) {
Filter filter = createFilter(clazz, filterString);
if (!filter.isValid()) {
return false;
}
// compare property keys to the ones in the configuration
Set<String> filterPropertyKeys = filter.getPropertyKeys();
if (m_configProperties.size() != filterPropertyKeys.size()) {
return false;
}
Iterator<String> filterPropertyKeysIterator = filterPropertyKeys.iterator();
while (filterPropertyKeysIterator.hasNext()) {
String filterPropertyKey = filterPropertyKeysIterator.next();
if (!m_configProperties.containsKey(filterPropertyKey)) {
return false;
} else if ((m_configProperties.get(filterPropertyKey)).isNegate() != filter.getProperty(filterPropertyKey).isNegate()) {
// negation should be equal
return false;
} else if (!filter.getProperty(filterPropertyKey).isNegate() && filter.getProperty(filterPropertyKey).getValue().equals("*")) {
// no wildcards without negation allowed
return false;
}
}
// our properties match so we're applicable
return true;
}
public boolean isApplicable(ServiceReference ref) {
String[] propertyKeys = ref.getPropertyKeys();
TreeSet<String> referenceProperties = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
for (int i = 0; i < propertyKeys.length; i++) {
referenceProperties.add(propertyKeys[i]);
}
Iterator<String> iterator = m_configProperties.keySet().iterator();
while (iterator.hasNext()) {
String item = iterator.next();
Property configProperty = m_configProperties.get(item);
if (!configProperty.isNegate() && !(referenceProperties.contains(item))) {
return false;
} else if (configProperty.isNegate() && referenceProperties.contains(item)) {
return false;
}
}
return true;
}
private void parseConfig(String configString) {
String[] propertyConfigs = configString.split(",");
for (int i = 0; i < propertyConfigs.length; i++) {
String propertyConfig = propertyConfigs[i];
Property property = new Property();
String key;
String value = null;
if (propertyConfig.startsWith("!")) {
property.setNegate(true);
key = propertyConfig.substring(1);
} else if (propertyConfig.startsWith("#")) {
property.setPermute(false);
key = propertyConfig.substring(1);
} else {
key = propertyConfig;
}
if (key.endsWith("*")) {
key = key.substring(0, key.indexOf("*"));
value = "*";
}
property.setKey(key.toLowerCase());
property.addValue(value, property.isNegate());
m_configProperties.put(key.toLowerCase(), property);
if (property.isNegate()) {
m_negatePropertyKeys.add(key);
}
}
}
protected Collection<Property> getProperties() {
return m_configProperties.values();
}
protected MultiPropertyKey createKeyFromFilter(String clazz, String filterString) {
int filterSize = m_configProperties.size();
return createFilter(clazz, filterString).createKey(filterSize);
}
//KEYS OF A FILTER
private Filter createFilter(String clazz, String filterString) {
String filterStringWithObjectClass = filterString;
if (clazz != null && !clazz.isEmpty()) {
if (filterString != null) {
if (!filterStringWithObjectClass.startsWith("(&(objectClass=")) {
filterStringWithObjectClass = "(&(objectClass=" + clazz + ")" + filterString + ")";
}
} else {
filterStringWithObjectClass = "(objectClass=" + clazz + ")";
}
}
Filter filter = Filter.parse(filterStringWithObjectClass);
return filter;
}
public List<MultiPropertyKey> createKeys(ServiceReference reference) {
List<MultiPropertyKey> results = new ArrayList<>();
String[] keys = reference.getPropertyKeys();
Arrays.sort(keys, String.CASE_INSENSITIVE_ORDER);
MultiPropertyKey multiPropertyKey = new MultiPropertyKey(0);
List<MultiPropertyKey> permutations = new ArrayList<>();
List<List<MultiPropertyKey>> sets = new ArrayList<>();
for (int i = 0; i < keys.length; i++) {
String key = null;
boolean hasUpperCase = !keys[i].equals(keys[i].toLowerCase());
if(hasUpperCase) {
key = keys[i].toLowerCase();
} else {
key = keys[i];
}
if (m_configProperties.containsKey(key)) {
Object valueObject = reference.getProperty(key);
if (valueObject instanceof String[]) {
String[] values = (String[]) valueObject;
if (m_configProperties.get(key).isPermute()) {
sets.add(getPermutations(key, values));
} else {
List<MultiPropertyKey> singleValues = new ArrayList<>();
for (int v = 0; v < values.length; v++) {
MultiPropertyKey single = new MultiPropertyKey(0);
single.append(toKey(key, values[v]));
singleValues.add(single);
}
sets.add(singleValues);
}
} else {
multiPropertyKey.append(toKey(key, (String) valueObject.toString()));
}
}
}
if (permutations != null && !permutations.isEmpty()) {
for (MultiPropertyKey permutation : permutations) {
permutation.append(multiPropertyKey);
results.add(permutation);
}
} else {
if (!sets.isEmpty()) {
List<List<MultiPropertyKey>> carthesianProductMultiProperty = carthesianProductMultiProperty(0, sets);
for (List<MultiPropertyKey> keyList : carthesianProductMultiProperty) {
MultiPropertyKey merged = new MultiPropertyKey(0);
merged.append(multiPropertyKey);
for (MultiPropertyKey single : keyList) {
merged.append(single);
}
results.add(merged);
}
} else {
results.add(multiPropertyKey);
}
}
return results;
}
private List<List<MultiPropertyKey>> carthesianProductMultiProperty(int index, List<List<MultiPropertyKey>> sets) {
List<List<MultiPropertyKey>> result = new ArrayList<>();
if (index == sets.size()) {
result.add(new ArrayList<MultiPropertyKey>());
} else {
List<MultiPropertyKey> set = sets.get(index);
for (int i = 0; i < set.size(); i++) {
MultiPropertyKey object = set.get(i);
List<List<MultiPropertyKey>> pSets = carthesianProductMultiProperty(index + 1, sets);
for (int j = 0; j < pSets.size(); j++) {
List<MultiPropertyKey> pSet = pSets.get(j);
pSet.add(object);
result.add(pSet);
}
}
}
return result;
}
List<MultiPropertyKey> getPermutations(String key, String[] values) {
List<MultiPropertyKey> results = new ArrayList<>();
Arrays.sort(values, String.CASE_INSENSITIVE_ORDER);
for (int v = 0; v < values.length; v++) {
String processValue = values[v];
List<String> items = new ArrayList<>();
items.add(processValue);
// per value get combinations
List<String> subItems = new ArrayList<>(items);
for (int w = v; w < values.length; w++) {
// make a copy of the current list
subItems = new ArrayList<>(subItems);
if (w != v) {
String value = values[w];
subItems.add(value);
}
results.add(toKey(key, subItems));
}
}
return results;
}
protected MultiPropertyKey toKey(String key, List<String> values) {
MultiPropertyKey kvc = new MultiPropertyKey(values.size());
for (int i = 0; i < values.size(); i++) {
kvc.add(key, values.get(i));
}
return kvc;
}
protected MultiPropertyKey toKey(String key, Object value) {
MultiPropertyKey kvc = new MultiPropertyKey(1);
kvc.add(key, value.toString());
return kvc;
}
public Object addingService(ServiceReference reference) {
BundleContext context;
synchronized (m_lock) {
context = m_context;
}
if (context != null) {
return context.getService(reference);
} else {
throw new IllegalStateException("No valid bundle context.");
}
}
public void addedService(ServiceReference reference, Object service) {
if (isApplicable(reference) && shouldBeIndexed(reference)) {
handleServiceAdd(reference);
}
}
public void modifiedService(ServiceReference reference, Object service) {
if (isApplicable(reference)) {
handleServicePropertiesChange(reference);
}
}
public void removedService(ServiceReference reference, Object service) {
if (isApplicable(reference) && shouldBeIndexed(reference)) {
handleServiceRemove(reference);
}
}
protected void handleServiceAdd(ServiceReference reference) {
List<MultiPropertyKey> keys = createKeys(reference);
synchronized (m_keyToServiceReferencesMap) {
for (int i = 0; i < keys.size(); i++) {
List<ServiceReference> references = m_keyToServiceReferencesMap.get(keys.get(i));
if (references == null) {
references = new ArrayList<>(1);
m_keyToServiceReferencesMap.put(keys.get(i), references);
}
references.add(reference);
}
}
}
protected void handleServicePropertiesChange(ServiceReference reference) {
synchronized (m_keyToServiceReferencesMap) {
// TODO this is a quite expensive linear scan over the existing collection
// because we first need to remove any existing references and they can be
// all over the place :)
Iterator<List<ServiceReference>> iterator = m_keyToServiceReferencesMap.values().iterator();
while (iterator.hasNext()) {
List<ServiceReference> list = iterator.next();
if (list != null) {
Iterator<ServiceReference> i2 = list.iterator();
while (i2.hasNext()) {
ServiceReference ref = i2.next();
if (ref.equals(reference)) {
i2.remove();
}
}
}
}
// only re-add the reference when it is still applicable for this filter index
if (shouldBeIndexed(reference)) {
List<MultiPropertyKey> keys = createKeys(reference);
for (int i = 0; i < keys.size(); i++) {
List<ServiceReference> references = m_keyToServiceReferencesMap.get(keys.get(i));
if (references == null) {
references = new ArrayList<>(keys.size());
m_keyToServiceReferencesMap.put(keys.get(i), references);
}
references.add(reference);
}
}
}
}
protected void handleServiceRemove(ServiceReference reference) {
List<MultiPropertyKey> keys = createKeys(reference);
synchronized (m_keyToServiceReferencesMap) {
for (int i = 0; i < keys.size(); i++) {
List<ServiceReference> references = m_keyToServiceReferencesMap.get(keys.get(i));
if (references != null) {
references.remove(reference);
if (references.isEmpty()) {
m_keyToServiceReferencesMap.remove(keys.get(i));
} else {
((ArrayList) reference).trimToSize();
}
}
}
}
}
protected boolean shouldBeIndexed(ServiceReference reference) {
// is already applicable, so we should only check whether there's a negate field in the filter which has a value in the reference
Iterator<String> negatePropertyKeyIterator = m_negatePropertyKeys.iterator();
while (negatePropertyKeyIterator.hasNext()) {
String negatePropertyKey = negatePropertyKeyIterator.next();
if (reference.getProperty(negatePropertyKey) != null) {
return false;
}
}
return true;
}
public void open(BundleContext context) {
synchronized (m_lock) {
if (m_context != null) {
throw new IllegalStateException("Filter already open.");
}
try {
m_tracker = new ServiceTracker(context, context.createFilter("(" + Constants.OBJECTCLASS + "=*)"), this);
} catch (InvalidSyntaxException e) {
throw new Error();
}
m_context = context;
}
m_tracker.open(true, true);
}
public void close() {
ServiceTracker tracker;
synchronized (m_lock) {
if (m_context == null) {
throw new IllegalStateException("Filter already closed.");
}
tracker = m_tracker;
m_tracker = null;
m_context = null;
}
tracker.close();
}
public List<ServiceReference> getAllServiceReferences(String clazz, String filter) {
List<ServiceReference> result = new ArrayList<>();
MultiPropertyKey key = createKeyFromFilter(clazz, filter);
synchronized (m_keyToServiceReferencesMap) {
List<ServiceReference> references = m_keyToServiceReferencesMap.get(key);
if (references != null) {
result.addAll(references);
}
}
return result;
}
public void serviceChanged(ServiceEvent event) {
if (isApplicable(event.getServiceReference())) {
List<MultiPropertyKey> keys = createKeys(event.getServiceReference());
List<ServiceListener> list = new ArrayList<ServiceListener>();
synchronized (m_keyToListenersMap) {
for (int i = 0; i < keys.size(); i++) {
//TODO fix
MultiPropertyKey key = keys.get(i);
List<ServiceListener> listeners = m_keyToListenersMap.get(key);
if (listeners != null) {
list.addAll(listeners);
}
}
}
if (list != null) {
Iterator<ServiceListener> iterator = list.iterator();
while (iterator.hasNext()) {
ServiceListener listener = iterator.next();
listener.serviceChanged(event);
}
}
}
}
public void addServiceListener(ServiceListener listener, String filter) {
MultiPropertyKey key = createKeyFromFilter(null, filter);
synchronized (m_keyToListenersMap) {
List<ServiceListener> listeners = m_keyToListenersMap.get(key);
if (listeners == null) {
listeners = new CopyOnWriteArrayList<ServiceListener>();
m_keyToListenersMap.put(key, listeners);
}
listeners.add(listener);
m_listenerToFilterMap.put(listener, filter);
}
}
public void removeServiceListener(ServiceListener listener) {
synchronized (m_keyToListenersMap) {
String filter = m_listenerToFilterMap.remove(listener);
if (filter != null) {
// the listener does exist
MultiPropertyKey key = createKeyFromFilter(null, filter);
boolean result = filter != null;
if (result) {
List<ServiceListener> listeners = m_keyToListenersMap.get(key);
if (listeners != null) {
listeners.remove(listener);
if (listeners.isEmpty()) {
m_keyToListenersMap.remove(key);
}
}
// TODO actually, if listeners == null that would be strange....
}
}
}
}
protected Collection<ServiceListener> getServiceListeners() {
return m_listenerToFilterMap.keySet();
}
public String toString() {
StringBuffer sb = new StringBuffer();
sb.append(" dMultiPropertyExactFilter[");
sb.append("K2L: " + m_keyToListenersMap.size());
sb.append(", K2SR: " + m_keyToServiceReferencesMap.size());
sb.append(", L2F: " + m_listenerToFilterMap.size());
sb.append("]");
return sb.toString();
}
@Override
public void swappedService(ServiceReference reference, Object service, ServiceReference newReference, Object newService) {
addedService(newReference, newService);
removedService(reference, service);
}
}