diff --git a/oak-core-spi/src/main/java/org/apache/jackrabbit/oak/osgi/OsgiUtil.java b/oak-core-spi/src/main/java/org/apache/jackrabbit/oak/osgi/OsgiUtil.java index 4d60b85..6b435e4 100644 --- a/oak-core-spi/src/main/java/org/apache/jackrabbit/oak/osgi/OsgiUtil.java +++ b/oak-core-spi/src/main/java/org/apache/jackrabbit/oak/osgi/OsgiUtil.java @@ -18,8 +18,14 @@ package org.apache.jackrabbit.oak.osgi; import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.Filter; +import org.osgi.framework.FrameworkUtil; +import org.osgi.framework.InvalidSyntaxException; import org.osgi.service.component.ComponentContext; +import java.util.Map; + import static com.google.common.base.Preconditions.checkNotNull; /** @@ -153,4 +159,46 @@ public class OsgiUtil { return string; } + /** + * Create a {@link Filter} using the passed Class as an objectClass and the map + * as the filter attributes. + * @param clazz the target objectClass + * @param attributes target attributes (null value for the absence) + * @return OSGi filter representing the input + */ + public static Filter getFilter(Class clazz, Map attributes) { + StringBuilder filterBuilder = new StringBuilder("(&"); + appendLdapFilterAttribute(filterBuilder, Constants.OBJECTCLASS, clazz.getName()); + for (Map.Entry e : attributes.entrySet()) { + appendLdapFilterAttribute(filterBuilder, e.getKey(), e.getValue()); + } + filterBuilder.append(')'); + try { + return FrameworkUtil.createFilter(filterBuilder.toString()); + } catch(InvalidSyntaxException e) { + throw new IllegalArgumentException(e); + } + } + + static StringBuilder appendLdapFilterAttribute(StringBuilder filterBuilder, String key, String value) { + if (value == null) { + filterBuilder.append("(!(").append(key).append("=*))"); + } else { + filterBuilder.append("(").append(key).append("="); + appendEscapedLdapValue(filterBuilder, value); + filterBuilder.append(")"); + } + return filterBuilder; + } + + static StringBuilder appendEscapedLdapValue(StringBuilder filterBuilder, String value) { + for (int i = 0; i < value.length(); i++) { + char c = value.charAt(i); + if ((c == '\\') || (c == '(') || (c == ')') || (c == '*')) { + filterBuilder.append('\\'); + } + filterBuilder.append(c); + } + return filterBuilder; + } } diff --git a/oak-core-spi/src/main/java/org/apache/jackrabbit/oak/osgi/OsgiWhiteboard.java b/oak-core-spi/src/main/java/org/apache/jackrabbit/oak/osgi/OsgiWhiteboard.java index 0397d39..b507be7 100644 --- a/oak-core-spi/src/main/java/org/apache/jackrabbit/oak/osgi/OsgiWhiteboard.java +++ b/oak-core-spi/src/main/java/org/apache/jackrabbit/oak/osgi/OsgiWhiteboard.java @@ -22,7 +22,9 @@ import static com.google.common.collect.Lists.newArrayList; import static com.google.common.collect.Maps.newHashMap; import static com.google.common.collect.Maps.newTreeMap; import static java.util.Collections.emptyList; +import static java.util.Collections.emptyMap; import static java.util.Collections.singletonList; +import static org.apache.jackrabbit.oak.osgi.OsgiUtil.getFilter; import java.util.Collections; import java.util.Dictionary; @@ -33,11 +35,13 @@ import java.util.SortedMap; import java.util.concurrent.atomic.AtomicReference; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import org.apache.jackrabbit.oak.spi.whiteboard.Registration; import org.apache.jackrabbit.oak.spi.whiteboard.Tracker; import org.apache.jackrabbit.oak.spi.whiteboard.Whiteboard; import org.osgi.framework.BundleContext; +import org.osgi.framework.Filter; import org.osgi.framework.ServiceReference; import org.osgi.framework.ServiceRegistration; import org.osgi.util.tracker.ServiceTracker; @@ -101,6 +105,15 @@ public class OsgiWhiteboard implements Whiteboard { */ @Override public Tracker track(final Class type) { + return track(type, emptyMap()); + } + + @Override + public Tracker track(Class type, Map filterProperties) { + return track(type, getFilter(type, filterProperties)); + } + + private Tracker track(Class type, @Nullable Filter filter) { checkNotNull(type); final AtomicReference> list = new AtomicReference>(Collections.emptyList()); @@ -143,8 +156,8 @@ public class OsgiWhiteboard implements Whiteboard { context.ungetService(reference); } }; - final ServiceTracker tracker = - new ServiceTracker(context, type.getName(), customizer); + + final ServiceTracker tracker = new ServiceTracker(context, filter, customizer); tracker.open(); return new Tracker() { @Override diff --git a/oak-core-spi/src/main/java/org/apache/jackrabbit/oak/spi/whiteboard/DefaultWhiteboard.java b/oak-core-spi/src/main/java/org/apache/jackrabbit/oak/spi/whiteboard/DefaultWhiteboard.java index d5a4ad1..baab939 100644 --- a/oak-core-spi/src/main/java/org/apache/jackrabbit/oak/spi/whiteboard/DefaultWhiteboard.java +++ b/oak-core-spi/src/main/java/org/apache/jackrabbit/oak/spi/whiteboard/DefaultWhiteboard.java @@ -16,9 +16,10 @@ */ package org.apache.jackrabbit.oak.spi.whiteboard; +import javax.annotation.Nonnull; + import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.collect.Lists.newArrayList; import static com.google.common.collect.Maps.newHashMap; import static com.google.common.collect.Sets.newIdentityHashSet; import static java.util.Collections.emptyList; @@ -26,13 +27,14 @@ import static java.util.Collections.emptyList; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; public class DefaultWhiteboard implements Whiteboard { - private final Map, Set> registry = newHashMap(); + private final Map, Set> registry = newHashMap(); - private synchronized void registered(Class type, T service) { - Set services = registry.get(type); + private synchronized void registered(Class type, Service service) { + Set services = registry.get(type); if (services == null) { services = newIdentityHashSet(); registry.put(type, services); @@ -40,8 +42,8 @@ public class DefaultWhiteboard implements Whiteboard { services.add(service); } - private synchronized void unregistered(Class type, T service) { - Set services = registry.get(type); + private synchronized void unregistered(Class type, Service service) { + Set services = registry.get(type); if (services != null) { services.remove(service); } @@ -49,9 +51,26 @@ public class DefaultWhiteboard implements Whiteboard { @SuppressWarnings("unchecked") private synchronized List lookup(Class type) { - Set services = registry.get(type); + Set services = registry.get(type); if (services != null) { - return (List) newArrayList(services); + return (List) services + .stream() + .map(Service::getService) + .collect(Collectors.toList()); + } else { + return emptyList(); + } + } + + @SuppressWarnings("unchecked") + private synchronized List lookup(Class type, Map filterProperties) { + Set services = registry.get(type); + if (services != null) { + return (List) services + .stream() + .filter(s -> s.matches(filterProperties)) + .map(Service::getService) + .collect(Collectors.toList()); } else { return emptyList(); } @@ -65,11 +84,14 @@ public class DefaultWhiteboard implements Whiteboard { checkNotNull(type); checkNotNull(service); checkArgument(type.isInstance(service)); - registered(type, service); + + Service s = new Service(service, properties); + + registered(type, s); return new Registration() { @Override public void unregister() { - unregistered(type, service); + unregistered(type, s); } }; } @@ -88,4 +110,53 @@ public class DefaultWhiteboard implements Whiteboard { }; } + @Override + public Tracker track(Class type, Map filterProperties) { + + checkNotNull(type); + return new Tracker() { + @Override + public List getServices() { + return lookup(type, filterProperties); + } + @Override + public void stop() { + } + }; + } + + private static class Service { + + private final Object service; + + private final Map properties; + + private Service(@Nonnull Object service, Map properties) { + checkNotNull(service); + this.service = service; + this.properties = properties; + } + + private Object getService() { + return service; + } + + private boolean matches(Map properties) { + return properties.entrySet().stream() + .allMatch(this::propertyMatches); + } + + private boolean propertyMatches(Map.Entry filterEntry) { + String key = filterEntry.getKey(); + String expectedValue = filterEntry.getValue(); + if (properties == null || !properties.containsKey(key)) { + return expectedValue == null; + } + Object value = properties.get(key); + if (value == null) { + return expectedValue == null; + } + return value.toString().equals(expectedValue); + } + } } diff --git a/oak-core-spi/src/main/java/org/apache/jackrabbit/oak/spi/whiteboard/Whiteboard.java b/oak-core-spi/src/main/java/org/apache/jackrabbit/oak/spi/whiteboard/Whiteboard.java index 44c96c5..1506489 100644 --- a/oak-core-spi/src/main/java/org/apache/jackrabbit/oak/spi/whiteboard/Whiteboard.java +++ b/oak-core-spi/src/main/java/org/apache/jackrabbit/oak/spi/whiteboard/Whiteboard.java @@ -39,4 +39,14 @@ public interface Whiteboard { */ Tracker track(Class type); + /** + * Starts tracking services of the given type, with given attributes. + * + * @param type type of the services to track + * @param filterProperties only services with these properties will be tracked. + * Null keys are not permitted. + * @return service tracker + */ + Tracker track(Class type, Map filterProperties); + } diff --git a/oak-core-spi/src/test/java/org/apache/jackrabbit/oak/osgi/OsgiUtilTest.java b/oak-core-spi/src/test/java/org/apache/jackrabbit/oak/osgi/OsgiUtilTest.java index 7546cc2..22d6bd8 100644 --- a/oak-core-spi/src/test/java/org/apache/jackrabbit/oak/osgi/OsgiUtilTest.java +++ b/oak-core-spi/src/test/java/org/apache/jackrabbit/oak/osgi/OsgiUtilTest.java @@ -17,12 +17,20 @@ package org.apache.jackrabbit.oak.osgi; +import org.apache.jackrabbit.oak.spi.whiteboard.WhiteboardUtils; import org.junit.Test; import org.osgi.framework.BundleContext; +import org.osgi.framework.FrameworkUtil; +import org.osgi.framework.InvalidSyntaxException; import org.osgi.service.component.ComponentContext; import java.util.Dictionary; +import java.util.Map; +import static com.google.common.collect.Maps.newLinkedHashMap; +import static org.apache.jackrabbit.oak.osgi.OsgiUtil.appendEscapedLdapValue; +import static org.apache.jackrabbit.oak.osgi.OsgiUtil.appendLdapFilterAttribute; +import static org.apache.jackrabbit.oak.osgi.OsgiUtil.getFilter; import static org.apache.jackrabbit.oak.osgi.OsgiUtil.lookup; import static org.apache.jackrabbit.oak.osgi.OsgiUtil.lookupConfigurationThenFramework; import static org.apache.jackrabbit.oak.osgi.OsgiUtil.lookupFrameworkThenConfiguration; @@ -204,4 +212,27 @@ public class OsgiUtilTest { assertEquals("fvalue", lookupFrameworkThenConfiguration(componentContext, "cname", "fname")); } + @Test + public void filterBuilding() throws InvalidSyntaxException { + StringBuilder b = new StringBuilder(); + + assertEquals("foo\\\\bar\\(foo\\)bar\\*foo", appendEscapedLdapValue(b, "foo\\bar(foo)bar*foo").toString()); + b.setLength(0); + + assertEquals("(foo=bar)", appendLdapFilterAttribute(b, "foo", "bar").toString()); + b.setLength(0); + + assertEquals("(foo=\\(bar\\))", appendLdapFilterAttribute(b, "foo", "(bar)").toString()); + b.setLength(0); + + assertEquals("(!(foo=*))", appendLdapFilterAttribute(b, "foo", null).toString()); + b.setLength(0); + + Map m = newLinkedHashMap(); + m.put("foo", "bar"); + m.put("empty", null); + m.put("escaped", "*xyz)"); + assertEquals(FrameworkUtil.createFilter("(&(objectClass=java.lang.String)(foo=bar)(!(empty=*))(escaped=\\*xyz\\)))"), getFilter(String.class, m)); + b.setLength(0); + } } diff --git a/oak-core-spi/src/test/java/org/apache/jackrabbit/oak/spi/whiteboard/DefaultWhiteboardTest.java b/oak-core-spi/src/test/java/org/apache/jackrabbit/oak/spi/whiteboard/DefaultWhiteboardTest.java new file mode 100644 index 0000000..675bbd0 --- /dev/null +++ b/oak-core-spi/src/test/java/org/apache/jackrabbit/oak/spi/whiteboard/DefaultWhiteboardTest.java @@ -0,0 +1,148 @@ +/* + * 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.jackrabbit.oak.spi.whiteboard; + +import com.google.common.collect.ImmutableMap; +import org.junit.Before; +import org.junit.Test; + +import java.util.Map; +import java.util.Set; + +import static com.google.common.collect.ImmutableSet.of; +import static java.util.Collections.singletonMap; +import static java.util.stream.Collectors.toSet; +import static org.junit.Assert.assertEquals; + +public class DefaultWhiteboardTest { + + private Whiteboard whiteboard; + + @Before + public void createWhiteboard() { + whiteboard = new DefaultWhiteboard(); + } + + @Test + public void filteredTracker() { + whiteboard.register(Service1.class, new Service1("s1"), ImmutableMap.of()); + whiteboard.register(Service2.class, new Service2("s2"), ImmutableMap.of("role", "myrole")); + whiteboard.register(Service3.class, new Service3("s3_1"), ImmutableMap.of()); + whiteboard.register(Service3.class, new Service3("s3_2"), ImmutableMap.of("role", "myrole")); + whiteboard.register(Service3.class, new Service3("s3_3"), ImmutableMap.of("role", "myotherrole", "id", 1024)); + + assertEquals(of("s1"), track(Service1.class)); + assertEquals(of("s1"), track(Service1.class, singletonMap("role", null))); + assertEquals(of(), track(Service1.class, ImmutableMap.of("role", "myrole"))); + + assertEquals(of("s2"), track(Service2.class)); + assertEquals(of(), track(Service2.class, singletonMap("role", null))); + assertEquals(of("s2"), track(Service2.class, ImmutableMap.of("role", "myrole"))); + + assertEquals(of("s3_1", "s3_2", "s3_3"), track(Service3.class)); + assertEquals(of("s3_1"), track(Service3.class, singletonMap("role", null))); + assertEquals(of("s3_2"), track(Service3.class, ImmutableMap.of("role", "myrole"))); + assertEquals(of("s3_3"), track(Service3.class, ImmutableMap.of("role", "myotherrole"))); + assertEquals(of("s3_3"), track(Service3.class, ImmutableMap.of("role", "myotherrole", "id", "1024"))); + assertEquals(of("s3_3"), track(Service3.class, ImmutableMap.of("id", "1024"))); + assertEquals(of(), track(Service3.class, ImmutableMap.of("id", "2048"))); + } + + @Test + public void sameServiceRegisteredAgain() { + Service1 s1 = new Service1("s1"); + + whiteboard.register(Service1.class, s1, ImmutableMap.of()); + whiteboard.register(Service1.class, s1, ImmutableMap.of()); + whiteboard.register(Service1.class, s1, ImmutableMap.of()); + + assertEquals(of("s1"), track(Service1.class)); + } + + @Test + public void unregister() { + Registration r1 = whiteboard.register(Service1.class, new Service1("s1"), ImmutableMap.of()); + Registration r2 = whiteboard.register(Service2.class, new Service2("s2"), ImmutableMap.of("role", "myrole")); + Registration r3_1 = whiteboard.register(Service3.class, new Service3("s3_1"), ImmutableMap.of()); + Registration r3_2 = whiteboard.register(Service3.class, new Service3("s3_2"), ImmutableMap.of("role", "myrole")); + Registration r3_3 = whiteboard.register(Service3.class, new Service3("s3_3"), ImmutableMap.of("role", "myotherrole", "id", 1024)); + + assertEquals(of("s1"), track(Service1.class)); + r1.unregister(); + assertEquals(of(), track(Service1.class)); + + assertEquals(of("s2"), track(Service2.class)); + r2.unregister(); + assertEquals(of(), track(Service2.class)); + + assertEquals(of("s3_1", "s3_2", "s3_3"), track(Service3.class)); + r3_1.unregister(); + assertEquals(of("s3_2", "s3_3"), track(Service3.class)); + r3_2.unregister(); + assertEquals(of("s3_3"), track(Service3.class)); + r3_3.unregister(); + assertEquals(of(), track(Service3.class)); + } + + private Set track(Class clazz) { + return track(clazz, null); + } + + private Set track(Class clazz, Map properties) { + final Tracker tracker; + if (properties == null) { + tracker = whiteboard.track(clazz); + } else { + tracker = whiteboard.track(clazz, properties); + } + try { + return tracker.getServices().stream().map(Service::getId).collect(toSet()); + } finally { + tracker.stop(); + } + } + + public abstract static class Service { + private final String id; + + private Service(String id) { + this.id = id; + } + + public String getId() { + return id; + } + } + + private final static class Service1 extends Service { + private Service1(String id) { + super(id); + } + } + + private final static class Service2 extends Service { + private Service2(String id) { + super(id); + } + } + + private final static class Service3 extends Service { + private Service3(String id) { + super(id); + } + } +}