--- a/openide.util/src/org/openide/util/lookup/ExcludingLookup.java +++ a/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 { --- a/projectapi/src/org/netbeans/modules/projectapi/LazyLookupProviders.java +++ a/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; + } + }; + } + +} --- a/projectapi/src/org/netbeans/modules/projectapi/LookupProviderAnnotationProcessor.java +++ a/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; + } + } --- a/projectapi/src/org/netbeans/modules/projectapi/MetaLookupMerger.java +++ a/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(); + +} --- a/projectapi/src/org/netbeans/spi/project/LookupMerger.java +++ a/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(); + + } + } --- a/projectapi/src/org/netbeans/spi/project/LookupProvider.java +++ a/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 { --- a/projectapi/src/org/netbeans/spi/project/ProjectServiceProvider.java +++ a/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(); + +} --- a/projectapi/src/org/netbeans/spi/project/support/LookupProviderSupport.java +++ a/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, --- a/projectapi/test/unit/src/org/netbeans/spi/project/support/LookupProviderSupportTest.java +++ a/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; + } + } }