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,174 @@ +/* + * 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 { + Class service = Thread.currentThread().getContextClassLoader().loadClass((String) attrs.get("service")); + String implName = (String) attrs.get("class"); + return new LazyLookupProvider(service, implName, false); + } + + /** + * @see org.netbeans.spi.project.LookupMerger.Registration + */ + public static LookupProvider forLookupMerger(Map attrs) throws ClassNotFoundException { + Class service = Thread.currentThread().getContextClassLoader().loadClass((String) attrs.get("service")); + String implName = (String) attrs.get("class"); + return new LazyLookupProvider(service, implName, true); + } + + private static class LazyLookupProvider implements LookupProvider { + private final Class service; + private final String implName; + private final boolean merger; + LazyLookupProvider(Class service, String implName, boolean merger) { + this.service = service; + this.implName = implName; + this.merger = merger; + } + public Lookup createAdditionalLookup(final Lookup lkp) { + if (merger) { + // must be a separate method so that it can capture the type parameter + return proxyMergerLookup(service, implName, lkp); + } else { + return new ProxyLookup() { + boolean inited = false; + protected @Override void beforeLookup(Template template) { + System.err.println("XXX template.type=" + template.getType()); + if (!inited && template.getType() == service) { + inited = true; + 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); + System.err.println("XXX adding " + instance); + setLookups(Lookups.singleton(instance)); + } + } catch (Exception x) { + Exceptions.printStackTrace(x); + } + } + } + }; + } + } + } + + private static Lookup proxyMergerLookup(final Class service, final String implName, final Lookup lkp) { + return new ProxyLookup() { + boolean mergerInited = false; + boolean serviceInited = false; + protected @Override void beforeLookup(Template template) { + System.err.println("XXX (merger) template.type=" + template.getType()); + if (template.getType() == service) { + System.err.println("XXX service inited"); + serviceInited = true; + } + if (!mergerInited && template.getType() == LookupMerger.class) { + mergerInited = true; + System.err.println("XXX adding proxy merger"); + setLookups(Lookups.fixed(new LookupMerger() { + public Class getMergeableClass() { + return service; + } + public T merge(Lookup lookup) { + if (!serviceInited) { + System.err.println("XXX service not inited yet, not merging anything"); + return null; + } + System.err.println("XXX merging " + service); + try { + Class impl = Thread.currentThread().getContextClassLoader().loadClass(implName); + LookupMerger m = (LookupMerger) impl.newInstance(); + if (m.getMergeableClass() != service) { + throw new ClassCastException(); + } + return service.cast(m.merge(lkp)); + } catch (Exception x) { + Exceptions.printStackTrace(x); + return null; + } + } + })); + } + } + }; + } + +} 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,88 @@ 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(); + 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/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/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 @@ -195,7 +195,10 @@ continue; } filteredClasses.add(c); - mergedInstances.add(lm.merge(lkp)); + Object instance = lm.merge(lkp); + if (instance != null) { + mergedInstances.add(instance); + } Lookup.Result result = lkp.lookupResult(c); 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,14 @@ package org.netbeans.spi.project.support; import java.beans.PropertyChangeListener; +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 +63,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 +291,64 @@ public void removePropertyChangeListener(PropertyChangeListener listener) { } } + + public void testLazyProviders() throws Exception { + assertLoadedClasses(); + Lookup all = LookupProviderSupport.createCompositeLookup(Lookups.fixed("hello"), "Projects/x/Lookup"); + assertLoadedClasses(); + assertEquals("hello", all.lookup(String.class)); + assertLoadedClasses(); + Collection svcs2 = all.lookupAll(Service2.class); + assertLoadedClasses("ServiceImpl2"); + assertEquals(1, svcs2.size()); + assertEquals(ServiceImpl2.class, svcs2.iterator().next().getClass()); + Collection svcs1 = all.lookupAll(Service1.class); + assertLoadedClasses("Merger", "ServiceImpl1", "ServiceImpl1a", "ServiceImpl2"); + assertEquals(svcs1.toString(), 3, svcs1.size()); + assertTrue(svcs1.toString(), svcs1.toString().contains("ServiceImpl1@")); + assertTrue(svcs1.toString(), svcs1.toString().contains("ServiceImpl1a@")); + 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 ServiceImpl1 implements Service1 { + static {loadedClasses.add(ServiceImpl1.class);} + public ServiceImpl1() {} + } + @ProjectServiceProvider(projectType="x", service=Service1.class) + public static class ServiceImpl1a implements Service1 { + static {loadedClasses.add(ServiceImpl1a.class);} + public ServiceImpl1a() {} + } + @ProjectServiceProvider(projectType="x", service=Service2.class) + public static class ServiceImpl2 implements Service2 { + static {loadedClasses.add(ServiceImpl2.class);} + public ServiceImpl2(Lookup base) { + assertNotNull(base.lookup(String.class)); + } + } + @LookupMerger.Registration(projectType="x") + public static class Merger implements LookupMerger { + static {loadedClasses.add(Merger.class);} + public Class getMergeableClass() { + return Service1.class; + } + public Service1 merge(final Lookup lkp) { + return new Service1() { + public @Override String toString() { + return "Merge" + lkp.lookupAll(Service1.class); + } + }; + } + } }