Issue #150194: add @ProjectServiceProvider and @LookupMerger.Registration as alternatives to @LookupProvider.Registration. These are potentially easier to use and more efficient than the older annotation. However, they do not apply to cases where the choice of lookup items must be determined dynamically. diff --git a/openide.util/src/org/openide/util/lookup/ExcludingLookup.java b/openide.util/src/org/openide/util/lookup/ExcludingLookup.java --- a/openide.util/src/org/openide/util/lookup/ExcludingLookup.java +++ b/openide.util/src/org/openide/util/lookup/ExcludingLookup.java @@ -47,6 +47,7 @@ import java.util.*; import org.openide.util.LookupEvent; +import org.openide.util.Parameters; /** Allows exclusion of certain instances from lookup. @@ -67,6 +68,9 @@ ExcludingLookup(Lookup delegate, Class[] classes) { this.delegate = delegate; + for (Class c : classes) { + Parameters.notNull("classes[x]", c); + } if (classes.length == 1) { this.classes = classes[0]; } else { diff --git a/projectapi/src/org/netbeans/modules/projectapi/LazyLookupProviders.java b/projectapi/src/org/netbeans/modules/projectapi/LazyLookupProviders.java new file mode 100644 --- /dev/null +++ b/projectapi/src/org/netbeans/modules/projectapi/LazyLookupProviders.java @@ -0,0 +1,137 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2008 Sun Microsystems, Inc. All rights reserved. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2008 Sun Microsystems, Inc. + */ + +package org.netbeans.modules.projectapi; + +import java.lang.reflect.Constructor; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import org.netbeans.api.project.Project; +import org.netbeans.spi.project.LookupMerger; +import org.netbeans.spi.project.LookupProvider; +import org.netbeans.spi.project.ProjectServiceProvider; +import org.openide.util.Exceptions; +import org.openide.util.Lookup; +import org.openide.util.Lookup.Template; +import org.openide.util.lookup.Lookups; +import org.openide.util.lookup.ProxyLookup; + +/** + * Factory methods for lazy {@link LookupProvider} registration. + */ +public class LazyLookupProviders { + + private LazyLookupProviders() {} + + /** + * @see ProjectServiceProvider + */ + public static LookupProvider forProjectServiceProvider(Map attrs) throws ClassNotFoundException { + final String serviceName = (String) attrs.get("service"); + final String implName = (String) attrs.get("class"); + return new LookupProvider() { + public Lookup createAdditionalLookup(final Lookup lkp) { + return new ProxyLookup() { + Class service; + protected @Override void beforeLookup(Template template) { + if (service == null && template.getType().getName().equals(serviceName)) { + service = template.getType(); + try { + Class impl = Thread.currentThread().getContextClassLoader().loadClass(implName); + CONSTRUCTOR: for (Constructor c : impl.getConstructors()) { + Class[] params = c.getParameterTypes(); + if (params.length > 2) { + continue; + } + List values = new ArrayList(); + for (Class param : params) { + if (param == Lookup.class) { + values.add(lkp); + } else if (param == Project.class) { + values.add(lkp.lookup(Project.class)); + } else { + continue CONSTRUCTOR; + } + } + Object instance = c.newInstance(values.toArray()); + service.cast(instance); + setLookups(Lookups.singleton(instance)); + } + } catch (Exception x) { + Exceptions.printStackTrace(x); + } + } + } + }; + } + }; + } + + /** + * @see org.netbeans.spi.project.LookupMerger.Registration + */ + public static MetaLookupMerger forLookupMerger(Map attrs) throws ClassNotFoundException { + final String serviceName = (String) attrs.get("service"); + final String implName = (String) attrs.get("class"); + return new MetaLookupMerger() { + private LookupMerger delegate; + public boolean canNowMerge(Class service) { + if (delegate == null && service.getName().equals(serviceName)) { + try { + Class impl = Thread.currentThread().getContextClassLoader().loadClass(implName); + LookupMerger m = (LookupMerger) impl.newInstance(); + if (service != m.getMergeableClass()) { + throw new ClassCastException(service + " vs. " + m.getMergeableClass()); + } + delegate = m; + return true; + } catch (Exception x) { + Exceptions.printStackTrace(x); + } + } + return false; + } + public LookupMerger merger() { + return delegate; + } + }; + } + +} diff --git a/projectapi/src/org/netbeans/modules/projectapi/LookupProviderAnnotationProcessor.java b/projectapi/src/org/netbeans/modules/projectapi/LookupProviderAnnotationProcessor.java --- a/projectapi/src/org/netbeans/modules/projectapi/LookupProviderAnnotationProcessor.java +++ b/projectapi/src/org/netbeans/modules/projectapi/LookupProviderAnnotationProcessor.java @@ -39,6 +39,7 @@ package org.netbeans.modules.projectapi; +import java.util.List; import java.util.Set; import javax.annotation.processing.Processor; import javax.annotation.processing.RoundEnvironment; @@ -46,10 +47,21 @@ import javax.annotation.processing.SupportedSourceVersion; import javax.lang.model.SourceVersion; import javax.lang.model.element.Element; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.MirroredTypeException; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.ElementFilter; +import org.netbeans.api.project.Project; +import org.netbeans.spi.project.LookupMerger; import org.netbeans.spi.project.LookupProvider; +import org.netbeans.spi.project.ProjectServiceProvider; import org.openide.filesystems.annotations.LayerGeneratingProcessor; import org.openide.filesystems.annotations.LayerGenerationException; +import org.openide.util.Lookup; import org.openide.util.lookup.ServiceProvider; /** @@ -58,7 +70,11 @@ */ @ServiceProvider(service=Processor.class) @SupportedSourceVersion(SourceVersion.RELEASE_6) -@SupportedAnnotationTypes("org.netbeans.spi.project.LookupProvider.Registration") +@SupportedAnnotationTypes({ + "org.netbeans.spi.project.LookupProvider.Registration", + "org.netbeans.spi.project.ProjectServiceProvider", + "org.netbeans.spi.project.LookupMerger.Registration" +}) public class LookupProviderAnnotationProcessor extends LayerGeneratingProcessor { @Override @@ -72,7 +88,91 @@ layer(e).instanceFile("Projects/" + type + "/Lookup", null, LookupProvider.class).write(); } } + for (Element e : roundEnv.getElementsAnnotatedWith(ProjectServiceProvider.class)) { + ProjectServiceProvider psp = e.getAnnotation(ProjectServiceProvider.class); + TypeElement clazz = (TypeElement) e; + TypeMirror service; + try { + psp.service(); + assert false; + continue; + } catch (MirroredTypeException x) { + service = x.getTypeMirror(); + } + if (!processingEnv.getTypeUtils().isAssignable(clazz.asType(), service)) { + throw new LayerGenerationException("Not assignable to " + service, e); + } + int constructorCount = 0; + CONSTRUCTOR: for (ExecutableElement constructor : ElementFilter.constructorsIn(clazz.getEnclosedElements())) { + if (!constructor.getModifiers().contains(Modifier.PUBLIC)) { + continue; + } + List params = constructor.getParameters(); + if (params.size() > 2) { + continue; + } + for (VariableElement param : params) { + if (!param.asType().equals(processingEnv.getElementUtils().getTypeElement(Project.class.getCanonicalName()).asType()) && + !param.asType().equals(processingEnv.getElementUtils().getTypeElement(Lookup.class.getCanonicalName()).asType())) { + continue CONSTRUCTOR; + } + } + constructorCount++; + } + if (constructorCount != 1) { + throw new LayerGenerationException("Must have exactly one public constructor optionally taking Project and/or Lookup", e); + } + String binName = processingEnv.getElementUtils().getBinaryName(clazz).toString(); + String serviceBinName = processingEnv.getElementUtils().getBinaryName((TypeElement) processingEnv.getTypeUtils().asElement(service)).toString(); + if (serviceBinName.equals(LookupMerger.class.getName())) { + throw new LayerGenerationException("@ProjectServiceProvider should not be used on LookupMerger; use @LookupMerger.Registration instead", e); + } + for (String type : psp.projectType()) { + layer(e).file("Projects/" + type + "/Lookup/" + binName.replace('.', '-') + ".instance"). + methodvalue("instanceCreate", LazyLookupProviders.class.getName(), "forProjectServiceProvider"). + stringvalue("class", binName). + stringvalue("service", serviceBinName). + write(); + } + } + for (Element e : roundEnv.getElementsAnnotatedWith(LookupMerger.Registration.class)) { + LookupMerger.Registration lmr = e.getAnnotation(LookupMerger.Registration.class); + TypeElement clazz = (TypeElement) e; + String binName = processingEnv.getElementUtils().getBinaryName(clazz).toString(); + DeclaredType service = findLookupMergerType((DeclaredType) clazz.asType()); + if (service == null) { + throw new LayerGenerationException("Not assignable to LookupMerger for some T", e); + } + String serviceBinName = processingEnv.getElementUtils().getBinaryName((TypeElement) service.asElement()).toString(); + for (String type : lmr.projectType()) { + layer(e).file("Projects/" + type + "/Lookup/" + binName.replace('.', '-') + ".instance"). + methodvalue("instanceCreate", LazyLookupProviders.class.getName(), "forLookupMerger"). + // XXX if supporting also factory methods, could use instanceAttribute here so that attr value is actually a LookupMerger + stringvalue("class", binName). + stringvalue("service", serviceBinName). + write(); + } + } return true; } + private DeclaredType findLookupMergerType(DeclaredType t) { + String rawName = processingEnv.getTypeUtils().erasure(t).toString(); + if (rawName.equals(LookupMerger.class.getName())) { + List args = t.getTypeArguments(); + if (args.size() == 1) { + return (DeclaredType) args.get(0); + } else { + return null; + } + } + for (TypeMirror supe : processingEnv.getTypeUtils().directSupertypes(t)) { + DeclaredType result = findLookupMergerType((DeclaredType) supe); + if (result != null) { + return result; + } + } + return null; + } + } diff --git a/projectapi/src/org/netbeans/modules/projectapi/MetaLookupMerger.java b/projectapi/src/org/netbeans/modules/projectapi/MetaLookupMerger.java new file mode 100644 --- /dev/null +++ b/projectapi/src/org/netbeans/modules/projectapi/MetaLookupMerger.java @@ -0,0 +1,54 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2009 Sun Microsystems, Inc. All rights reserved. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2009 Sun Microsystems, Inc. + */ + +package org.netbeans.modules.projectapi; + +import org.netbeans.spi.project.LookupMerger; +import org.openide.util.Lookup; + +/** + * @see LazyLookupProviders#forLookupMerger + */ +public interface MetaLookupMerger { + + boolean canNowMerge(Class service); + + LookupMerger/*|null*/ merger(); + +} diff --git a/projectapi/src/org/netbeans/spi/project/LookupMerger.java b/projectapi/src/org/netbeans/spi/project/LookupMerger.java --- a/projectapi/src/org/netbeans/spi/project/LookupMerger.java +++ b/projectapi/src/org/netbeans/spi/project/LookupMerger.java @@ -41,6 +41,10 @@ package org.netbeans.spi.project; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import org.openide.util.Lookup; /** @@ -72,4 +76,20 @@ */ T merge(Lookup lookup); + /** + * Registers a lookup merger for some project types. + * The annotated class must be assignable to {@link LookupMerger} with a type parameter. + * @since XXX + */ + @Retention(RetentionPolicy.SOURCE) + @Target(ElementType.TYPE) // XXX support static factory methods too + @interface Registration { + + /** + * Token(s) denoting one or more project types, e.g. {@code "org-netbeans-modules-java-j2seproject"} + */ + String[] projectType(); + + } + } diff --git a/projectapi/src/org/netbeans/spi/project/LookupProvider.java b/projectapi/src/org/netbeans/spi/project/LookupProvider.java --- a/projectapi/src/org/netbeans/spi/project/LookupProvider.java +++ b/projectapi/src/org/netbeans/spi/project/LookupProvider.java @@ -66,7 +66,10 @@ Lookup createAdditionalLookup(Lookup baseContext); /** - * annotation to register LookupProvider instances. + * Annotation to register {@link LookupProvider} instances. + *

If you wish to unconditionally register one or more objects, + * it will be more efficient and may be easier to use + * {@link ProjectServiceProvider} (and/or {@link LookupMerger.Registration}). * @since org.netbeans.modules.projectapi 1.21 */ public @interface Registration { diff --git a/projectapi/src/org/netbeans/spi/project/ProjectServiceProvider.java b/projectapi/src/org/netbeans/spi/project/ProjectServiceProvider.java new file mode 100644 --- /dev/null +++ b/projectapi/src/org/netbeans/spi/project/ProjectServiceProvider.java @@ -0,0 +1,69 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2008 Sun Microsystems, Inc. All rights reserved. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2008 Sun Microsystems, Inc. + */ + +package org.netbeans.spi.project; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.netbeans.api.project.Project; +import org.openide.util.Lookup; + +/** + * Like {@link LookupProvider} but registers a single object into a project's lookup. + * The annotated class must have one public constructor, which may take {@link Project} and/or {@link Lookup} parameters. + * @since XXX + */ +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.TYPE) // XXX support static factory methods too +public @interface ProjectServiceProvider { + + /** + * Service class to be registered. + * The annotated class must be assignable to the service class. + */ + Class service(); + + /** + * Token(s) denoting one or more project types, e.g. {@code "org-netbeans-modules-java-j2seproject"} + */ + String[] projectType(); + +} diff --git a/projectapi/src/org/netbeans/spi/project/support/LookupProviderSupport.java b/projectapi/src/org/netbeans/spi/project/support/LookupProviderSupport.java --- a/projectapi/src/org/netbeans/spi/project/support/LookupProviderSupport.java +++ b/projectapi/src/org/netbeans/spi/project/support/LookupProviderSupport.java @@ -54,6 +54,7 @@ import javax.swing.event.ChangeListener; import org.netbeans.api.project.SourceGroup; import org.netbeans.api.project.Sources; +import org.netbeans.modules.projectapi.MetaLookupMerger; import org.netbeans.spi.project.LookupMerger; import org.netbeans.spi.project.LookupProvider; import org.openide.ErrorManager; @@ -108,6 +109,7 @@ private List currentLookups; private Lookup.Result mergers; + private final Lookup.Result metaMergers; private Reference listenerRef; //#68623: the proxy lookup fires changes only if someone listens on a particular template: private final List> results = new ArrayList>(); @@ -121,17 +123,20 @@ assert base != null; baseLookup = base; providerResult = providerLookup.lookup(new Lookup.Template(LookupProvider.class)); + metaMergers = providerLookup.lookupResult(MetaLookupMerger.class); assert isAllJustLookupProviders(providerLookup) : "Layer content at " + path + " contains other than LookupProvider instances! See messages.log file for more details."; //NOI18N - doDelegate(providerResult.allInstances()); + doDelegate(); providerListener = new LookupListener() { public void resultChanged(LookupEvent ev) { // XXX this may need to be run asynchronously; deadlock-prone - doDelegate(providerResult.allInstances()); + doDelegate(); } }; providerResult.addLookupListener( WeakListeners.create(LookupListener.class, providerListener, providerResult)); + metaMergers.addLookupListener( + WeakListeners.create(LookupListener.class, providerListener, metaMergers)); } //just for assertion evaluation. @@ -139,7 +144,7 @@ Lookup.Result res = lkp.lookupResult(Object.class); Set> set = res.allClasses(); for (Class clzz : set) { - if (!LookupProvider.class.isAssignableFrom(clzz)) { + if (!LookupProvider.class.isAssignableFrom(clzz) && !MetaLookupMerger.class.isAssignableFrom(clzz)) { Logger.getLogger(LookupProviderSupport.class.getName()).warning("" + clzz.getName() + " is not instance of LookupProvider."); //NOI18N return false; } @@ -149,16 +154,25 @@ public void resultChanged(LookupEvent ev) { - doDelegate(providerResult.allInstances()); + doDelegate(); + } + + protected @Override void beforeLookup(Lookup.Template template) { + for (MetaLookupMerger metaMerger : metaMergers.allInstances()) { + if (metaMerger.canNowMerge(template.getType())) { + doDelegate(); + } + } } - private synchronized void doDelegate(Collection providers) { + private synchronized void doDelegate() { //unregister listeners from the old results: for (Lookup.Result r : results) { r.removeLookupListener(this); } + Collection providers = providerResult.allInstances(); List newLookups = new ArrayList(); for (LookupProvider elem : providers) { if (old.contains(elem)) { @@ -186,7 +200,14 @@ l = WeakListeners.create(LookupListener.class, this, mergers); listenerRef = new WeakReference(l); mergers.addLookupListener(l); - for (LookupMerger lm : mergers.allInstances()) { + Collection allMergers = new ArrayList(mergers.allInstances()); + for (MetaLookupMerger metaMerger : metaMergers.allInstances()) { + LookupMerger merger = metaMerger.merger(); + if (merger != null) { + allMergers.add(merger); + } + } + for (LookupMerger lm : allMergers) { Class c = lm.getMergeableClass(); if (filteredClasses.contains(c)) { ErrorManager.getDefault().log(ErrorManager.WARNING, diff --git a/projectapi/test/unit/src/org/netbeans/spi/project/support/LookupProviderSupportTest.java b/projectapi/test/unit/src/org/netbeans/spi/project/support/LookupProviderSupportTest.java --- a/projectapi/test/unit/src/org/netbeans/spi/project/support/LookupProviderSupportTest.java +++ b/projectapi/test/unit/src/org/netbeans/spi/project/support/LookupProviderSupportTest.java @@ -42,9 +42,16 @@ package org.netbeans.spi.project.support; import java.beans.PropertyChangeListener; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; import javax.swing.Icon; import javax.swing.JButton; import javax.swing.JCheckBox; @@ -58,6 +65,7 @@ import org.netbeans.junit.NbTestCase; import org.netbeans.spi.project.LookupMerger; import org.netbeans.spi.project.LookupProvider; +import org.netbeans.spi.project.ProjectServiceProvider; import org.openide.filesystems.FileObject; import org.openide.util.Lookup; import org.openide.util.lookup.AbstractLookup; @@ -285,5 +293,84 @@ public void removePropertyChangeListener(PropertyChangeListener listener) { } } + + public void testLazyProviders() throws Exception { + // Cannot simply use static initializers to tell when classes are loaded; + // these will not be run in case a service is loaded but not yet initialized. + ClassLoader l = new URLClassLoader(new URL[] { + LookupProviderSupportTest.class.getProtectionDomain().getCodeSource().getLocation()}, + LookupProviderSupportTest.class.getClassLoader()) { + protected @Override synchronized Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + if (name.startsWith(LookupProviderSupportTest.class.getName() + "$")) { + Class c = findLoadedClass(name); + if (c == null) { + // do not delegate to parent, i.e. be sure we have loaded it + c = findClass(name); + if (resolve) { + resolveClass(c); + } + loadedClasses.add(c); + } + return c; + } else { + return super.loadClass(name, resolve); + } + } + }; + Thread.currentThread().setContextClassLoader(l); + assertLoadedClasses(); + Lookup all = LookupProviderSupport.createCompositeLookup(Lookups.fixed("hello"), "Projects/x/Lookup"); + assertLoadedClasses(); + assertEquals("hello", all.lookup(String.class)); + assertLoadedClasses(); + Collection svcs2 = all.lookupAll(l.loadClass(Service2.class.getName())); + assertEquals(1, svcs2.size()); + assertEquals(ServiceImpl2.class.getName(), svcs2.iterator().next().getClass().getName()); + assertLoadedClasses("Service2", "ServiceImpl2"); + Collection svcs1 = all.lookupAll(l.loadClass(Service1.class.getName())); + assertLoadedClasses("MergedServiceImpl1", "Merger", "Service1", "Service2", "ServiceImpl1a", "ServiceImpl1b", "ServiceImpl2"); + assertEquals(svcs1.toString(), 1, svcs1.size()); + assertTrue(svcs1.toString(), svcs1.toString().contains("ServiceImpl1a@")); + assertTrue(svcs1.toString(), svcs1.toString().contains("ServiceImpl1b@")); + assertTrue(svcs1.toString(), svcs1.toString().contains("Merge[")); + } + private static final Set> loadedClasses = new HashSet>(); + private static void assertLoadedClasses(String... names) { + SortedSet actual = new TreeSet(); + for (Class clazz : loadedClasses) { + actual.add(clazz.getName().replaceFirst("^\\Q" + LookupProviderSupportTest.class.getName() + "$\\E", "")); + } + assertEquals(Arrays.toString(names), actual.toString()); + } + public interface Service1 {} + public interface Service2 {} + @ProjectServiceProvider(projectType="x", service=Service1.class) + public static class ServiceImpl1a implements Service1 {} + @ProjectServiceProvider(projectType="x", service=Service1.class) + public static class ServiceImpl1b implements Service1 {} + @ProjectServiceProvider(projectType="x", service=Service2.class) + public static class ServiceImpl2 implements Service2 { + public ServiceImpl2(Lookup base) { + assertNotNull(base.lookup(String.class)); + } + } + @LookupMerger.Registration(projectType="x") + public static class Merger implements LookupMerger { + public Class getMergeableClass() { + return Service1.class; + } + public Service1 merge(final Lookup lkp) { + return new MergedServiceImpl1(lkp.lookupAll(Service1.class)); + } + } + private static class MergedServiceImpl1 implements Service1 { + private final Collection delegates; + MergedServiceImpl1(Collection delegates) { + this.delegates = delegates; + } + public @Override String toString() { + return "Merge" + delegates; + } + } }