diff --git a/apisupport.project/src/org/netbeans/modules/apisupport/project/layers/LayerUtils.java b/apisupport.project/src/org/netbeans/modules/apisupport/project/layers/LayerUtils.java --- a/apisupport.project/src/org/netbeans/modules/apisupport/project/layers/LayerUtils.java +++ b/apisupport.project/src/org/netbeans/modules/apisupport/project/layers/LayerUtils.java @@ -620,6 +620,7 @@ if (roLayer != null) { readOnlyLayers.add(roLayer); } + // XXX could also look for ${sister}/build/classes/META-INF/generated-layer.xml } Set jars = getPlatformJarsForSuiteComponentProject(p, suite); readOnlyLayers.addAll(Arrays.asList(getPlatformLayers(jars))); @@ -650,6 +651,7 @@ continue; } otherLayerURLs.add(layerXml.getURL()); + // XXX as above, could add generated-layer.xml } XMLFileSystem xfs = new XMLFileSystem(); try { @@ -780,6 +782,24 @@ throw (IOException) new IOException(e.toString()).initCause(e); } } + { // #149136: load generated layers too + // XXX might be faster to use ManifestManager's original opening of JAR to see if it really had such a layer + URL generatedLayer = new URL("jar:" + jar.toURI() + "!/META-INF/generated-layer.xml"); + boolean ok = true; + try { + generatedLayer.openConnection().connect(); + } catch (IOException x) { + // ignore, probably just means resource does not exist + ok = false; + } + if (ok) { + try { + layers.add(new XMLFileSystem(generatedLayer)); + } catch (SAXException x) { + throw (IOException) new IOException(x.toString()).initCause(x); + } + } + } } return layers.toArray(new FileSystem[layers.size()]); } diff --git a/core.startup/src/org/netbeans/core/startup/NbInstaller.java b/core.startup/src/org/netbeans/core/startup/NbInstaller.java --- a/core.startup/src/org/netbeans/core/startup/NbInstaller.java +++ b/core.startup/src/org/netbeans/core/startup/NbInstaller.java @@ -45,11 +45,13 @@ import java.io.DataOutputStream; import java.io.File; import java.io.IOException; +import java.lang.reflect.Method; import java.net.URL; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Collections; import java.util.Date; +import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; @@ -542,9 +544,22 @@ modules = new ArrayList(modules); Collections.reverse(modules); Map> urls = new HashMap>(5); - urls.put(ModuleLayeredFileSystem.getUserModuleLayer(), new ArrayList(1000)); - urls.put(ModuleLayeredFileSystem.getInstallationModuleLayer(), new ArrayList(1000)); + ModuleLayeredFileSystem userModuleLayer = ModuleLayeredFileSystem.getUserModuleLayer(); + ModuleLayeredFileSystem installationModuleLayer = ModuleLayeredFileSystem.getInstallationModuleLayer(); + urls.put(userModuleLayer, new ArrayList(1000)); + urls.put(installationModuleLayer, new ArrayList(1000)); for (Module m: modules) { + // #19458: only put reloadables into the "session layer" + // (where they will not have their layers cached). All others + // should go into "installation layer" (so that they can mask + // layers according to cross-dependencies). + ModuleLayeredFileSystem host = m.isReloadable() ? userModuleLayer : installationModuleLayer; + List theseurls = urls.get(host); + if (theseurls == null) { + theseurls = new ArrayList(1000); + urls.put(host, theseurls); + } + ClassLoader cl = m.getClassLoader(); String s = layers.get(m); if (s != null) { Util.err.fine("loadLayer: " + s + " load=" + load); @@ -557,22 +572,6 @@ } else { base = s.substring(0, idx); ext = s.substring(idx); - } - ClassLoader cl = m.getClassLoader(); - ModuleLayeredFileSystem host; - // #19458: only put reloadables into the "session layer" - // (where they will not have their layers cached). All others - // should go into "installation layer" (so that they can mask - // layers according to cross-dependencies). - if (m.isReloadable()) { - host = ModuleLayeredFileSystem.getUserModuleLayer(); - } else { - host = ModuleLayeredFileSystem.getInstallationModuleLayer(); - } - List theseurls = urls.get(host); - if (theseurls == null) { - theseurls = new ArrayList(1000); - urls.put(host, theseurls); } boolean foundSomething = false; for (String suffix : NbCollections.iterable(NbBundle.getLocalizingSuffixes())) { @@ -589,6 +588,18 @@ continue; } } + try { // #149136 + // Cannot use getResources because we do not wish to delegate to parents. + // In fact both URLClassLoader and ProxyClassLoader override this method to be public. + Method findResources = ClassLoader.class.getDeclaredMethod("findResources", String.class); // NOI18N + findResources.setAccessible(true); + Enumeration e = (Enumeration) findResources.invoke(cl, "META-INF/generated-layer.xml"); // NOI18N + while (e.hasMoreElements()) { + theseurls.add((URL) e.nextElement()); + } + } catch (Exception x) { + Exceptions.printStackTrace(x); + } } // Now actually do it. for (Map.Entry> entry: urls.entrySet()) { @@ -601,8 +612,8 @@ } else { // #106737: we might have the wrong host, since it switches when reloadable flag is toggled. // To be safe, remove from both. - ModuleLayeredFileSystem.getUserModuleLayer().removeURLs(theseurls); - ModuleLayeredFileSystem.getInstallationModuleLayer().removeURLs(theseurls); + userModuleLayer.removeURLs(theseurls); + installationModuleLayer.removeURLs(theseurls); } } catch (Exception e) { Util.err.log(Level.WARNING, null, e); diff --git a/core.startup/src/org/netbeans/core/startup/layers/ModuleLayeredFileSystem.java b/core.startup/src/org/netbeans/core/startup/layers/ModuleLayeredFileSystem.java --- a/core.startup/src/org/netbeans/core/startup/layers/ModuleLayeredFileSystem.java +++ b/core.startup/src/org/netbeans/core/startup/layers/ModuleLayeredFileSystem.java @@ -193,6 +193,9 @@ is.close(); } } + for (URL generatedLayer : NbCollections.iterable(loader.getResources("META-INF/generated-layer.xml"))) { // NOI18N + layerUrls.add(generatedLayer); + } XMLFileSystem xmlfs = new XMLFileSystem(); xmlfs.setXmlUrls(layerUrls.toArray(new URL[layerUrls.size()])); l.add(xmlfs); diff --git a/o.n.bootstrap/src/org/netbeans/JarClassLoader.java b/o.n.bootstrap/src/org/netbeans/JarClassLoader.java --- a/o.n.bootstrap/src/org/netbeans/JarClassLoader.java +++ b/o.n.bootstrap/src/org/netbeans/JarClassLoader.java @@ -222,7 +222,7 @@ } // look up the jars and return a resource based on a content of jars @Override - protected URL findResource(String name) { + public URL findResource(String name) { for( int i=0; i simpleFindResources(String name) { + public Enumeration findResources(String name) { Vector v = new Vector(3); // look up the jars and return a resource based on a content of jars diff --git a/o.n.bootstrap/src/org/netbeans/MainImpl.java b/o.n.bootstrap/src/org/netbeans/MainImpl.java --- a/o.n.bootstrap/src/org/netbeans/MainImpl.java +++ b/o.n.bootstrap/src/org/netbeans/MainImpl.java @@ -250,7 +250,7 @@ if (cp.isEmpty ()) { value = searchBuildNumber(this.getResources("META-INF/MANIFEST.MF")); } else { - value = searchBuildNumber(this.simpleFindResources("META-INF/MANIFEST.MF")); + value = searchBuildNumber(this.findResources("META-INF/MANIFEST.MF")); } } catch (IOException ex) { ex.printStackTrace(); diff --git a/o.n.bootstrap/src/org/netbeans/ProxyClassLoader.java b/o.n.bootstrap/src/org/netbeans/ProxyClassLoader.java --- a/o.n.bootstrap/src/org/netbeans/ProxyClassLoader.java +++ b/o.n.bootstrap/src/org/netbeans/ProxyClassLoader.java @@ -362,8 +362,8 @@ * if the resource could not be found. */ @Override - protected URL findResource(String name) { - return null; + public URL findResource(String name) { + return super.findResource(name); } /** @@ -377,7 +377,7 @@ * @throws IOException if I/O errors occur */ @Override - protected final synchronized Enumeration findResources(String name) throws IOException { + public final synchronized Enumeration getResources(String name) throws IOException { name = stripInitialSlash(name); final int slashIdx = name.lastIndexOf('/'); if (slashIdx == -1) { @@ -398,16 +398,20 @@ if (del != null) { // unclaimed resource, go directly to SCL for (ProxyClassLoader pcl : parents) { // all our accessible parents if (del.contains(pcl) && shouldDelegateResource(path, pcl)) { // that cover given package - sub.add(pcl.simpleFindResources(name)); + sub.add(pcl.findResources(name)); } } - if (del.contains(this)) sub.add(simpleFindResources(name)); + if (del.contains(this)) { + sub.add(findResources(name)); + } } } else { // Don't bother optimizing this call by domains. for (ProxyClassLoader pcl : parents) { - if (shouldDelegateResource(path, pcl)) sub.add(pcl.simpleFindResources(name)); + if (shouldDelegateResource(path, pcl)) { + sub.add(pcl.findResources(name)); + } } - sub.add(simpleFindResources(name)); + sub.add(findResources(name)); } // Should not be duplicates, assuming the parent loaders are properly distinct // from one another and do not overlap in JAR usage, which they ought not. @@ -416,7 +420,8 @@ return Enumerations.concat(Collections.enumeration(sub)); } - protected Enumeration simpleFindResources(String name) throws IOException { + @Override + public Enumeration findResources(String name) throws IOException { return super.findResources(name); } diff --git a/o.n.bootstrap/test/unit/src/org/netbeans/ProxyClassLoaderTest.java b/o.n.bootstrap/test/unit/src/org/netbeans/ProxyClassLoaderTest.java --- a/o.n.bootstrap/test/unit/src/org/netbeans/ProxyClassLoaderTest.java +++ b/o.n.bootstrap/test/unit/src/org/netbeans/ProxyClassLoaderTest.java @@ -42,8 +42,14 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Arrays; import java.util.Collections; +import java.util.Enumeration; import junit.framework.TestCase; +import org.openide.util.Enumerations; +import org.openide.util.Exceptions; public class ProxyClassLoaderTest extends TestCase { @@ -52,6 +58,41 @@ } public void testAmbiguousDelegation() throws Exception { + class CL extends ProxyClassLoader { + final Class[] owned; + final String name; + CL(ClassLoader[] parents, String name, Class... owned) { + super(parents, false); + addCoveredPackages(Collections.singleton("org.netbeans")); + this.name = name; + this.owned = owned; + } + protected @Override Class doLoadClass(String pkg, String name) { + for (Class c : owned) { + if (name.equals(c.getName())) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + InputStream is = CL.class.getClassLoader().getResourceAsStream(name.replace('.', '/') + ".class"); + byte[] buf = new byte[4096]; + int read; + try { + while ((read = is.read(buf)) != -1) { + baos.write(buf, 0, read); + } + } catch (IOException x) { + assert false : x; + } + return defineClass(name, baos.toByteArray(), 0, baos.size()); + } + } + return null; + } + protected @Override boolean shouldDelegateResource(String pkg, ClassLoader parent) { + return parent != null || !pkg.equals("org/netbeans/"); + } + public @Override String toString() { + return name; + } + } ClassLoader l1 = new CL(new ClassLoader[0], "l1", A.class); ClassLoader l2 = new CL(new ClassLoader[0], "l2", A.class); ClassLoader l3 = new CL(new ClassLoader[] {l1}, "l3", B.class); @@ -76,42 +117,6 @@ assertEquals(l1, l5.loadClass(C.class.getName()).getMethod("a").invoke(null).getClass().getClassLoader()); } - static class CL extends ProxyClassLoader { - final Class[] owned; - final String name; - CL(ClassLoader[] parents, String name, Class... owned) { - super(parents, false); - addCoveredPackages(Collections.singleton("org.netbeans")); - this.name = name; - this.owned = owned; - } - protected @Override Class doLoadClass(String pkg, String name) { - for (Class c : owned) { - if (name.equals(c.getName())) { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - InputStream is = CL.class.getClassLoader().getResourceAsStream(name.replace('.', '/') + ".class"); - byte[] buf = new byte[4096]; - int read; - try { - while ((read = is.read(buf)) != -1) { - baos.write(buf, 0, read); - } - } catch (IOException x) { - assert false : x; - } - return defineClass(name, baos.toByteArray(), 0, baos.size()); - } - } - return null; - } - protected @Override boolean shouldDelegateResource(String pkg, ClassLoader parent) { - return parent != null || !pkg.equals("org/netbeans/"); - } - public @Override String toString() { - return name; - } - } - public static class A {} public static class B { public static A a() { @@ -124,4 +129,60 @@ } } + public void testResourceDelegation() throws Exception { // #32576 + class CL extends ProxyClassLoader { + final URL base1, base2; + final String[] owned; + CL(ClassLoader[] parents, URL base1, URL base2, String... owned) { + super(parents, false); + this.base1 = base1; + this.base2 = base2; + this.owned = owned; + addCoveredPackages(Collections.singleton("p")); + } + @Override public URL findResource(String name) { + if (Arrays.asList(owned).contains(name)) { + try { + return new URL(base1, name); + } catch (MalformedURLException ex) { + Exceptions.printStackTrace(ex); + } + } + return null; + } + @Override public synchronized Enumeration findResources(String name) throws IOException { + if (Arrays.asList(owned).contains(name)) { + return Enumerations.array(new URL(base1, name), new URL(base2, name)); + } + return super.findResources(name); + } + } + URL b = new URL("http://nowhere.net/"); + ProxyClassLoader cl1 = new CL(new ClassLoader[0], new URL(b, "1a/"), new URL(b, "1b/"), "p/1"); + ProxyClassLoader cl2 = new CL(new ClassLoader[] {cl1}, new URL(b, "2a/"), new URL(b, "2b/"), "p/2"); + ProxyClassLoader cl3 = new CL(new ClassLoader[] {cl1}, new URL(b, "3a/"), new URL(b, "3b/"), "p/1", "p/3"); + ProxyClassLoader cl4 = new CL(new ClassLoader[] {cl1, cl2, cl3}, new URL(b, "4a/"), new URL(b, "4b/")); + assertEquals(new URL(b, "1a/p/1"), cl1.getResource("p/1")); + assertEquals(null, cl1.getResource("p/1x")); + assertEquals(Arrays.asList(new URL(b, "1a/p/1"), new URL(b, "1b/p/1")), Collections.list(cl1.getResources("p/1"))); + assertEquals(new URL(b, "1a/p/1"), cl2.getResource("p/1")); + assertEquals(null, cl2.findResource("p/1")); + assertEquals(new URL(b, "2a/p/2"), cl2.getResource("p/2")); + assertEquals(new URL(b, "2a/p/2"), cl2.findResource("p/2")); + assertEquals(Arrays.asList(new URL(b, "2a/p/2"), new URL(b, "2b/p/2")), Collections.list(cl2.getResources("p/2"))); + assertEquals(null, cl2.findResource("p/1")); + assertEquals(new URL(b, "1a/p/1"), cl3.getResource("p/1")); + assertEquals(new URL(b, "3a/p/1"), cl3.findResource("p/1")); + assertEquals(Arrays.asList(new URL(b, "1a/p/1"), new URL(b, "1b/p/1"), new URL(b, "3a/p/1"), new URL(b, "3b/p/1")), + Collections.list(cl3.getResources("p/1"))); + assertEquals(Arrays.asList(new URL(b, "3a/p/1"), new URL(b, "3b/p/1")), Collections.list(cl3.findResources("p/1"))); + assertEquals(new URL(b, "1a/p/1"), cl4.getResource("p/1")); + assertEquals(new URL(b, "2a/p/2"), cl4.getResource("p/2")); + assertEquals(new URL(b, "3a/p/3"), cl4.getResource("p/3")); + assertEquals(Arrays.asList(new URL(b, "1a/p/1"), new URL(b, "1b/p/1"), new URL(b, "3a/p/1"), new URL(b, "3b/p/1")), + Collections.list(cl4.getResources("p/1"))); + assertEquals(Arrays.asList(new URL(b, "2a/p/2"), new URL(b, "2b/p/2")), Collections.list(cl4.getResources("p/2"))); + assertEquals(Arrays.asList(new URL(b, "3a/p/3"), new URL(b, "3b/p/3")), Collections.list(cl4.getResources("p/3"))); + } + } diff --git a/openide.filesystems/nbproject/project.properties b/openide.filesystems/nbproject/project.properties --- a/openide.filesystems/nbproject/project.properties +++ b/openide.filesystems/nbproject/project.properties @@ -40,6 +40,7 @@ javac.compilerargs=-Xlint -Xlint:-serial javac.source=1.5 module.jar.dir=core +cp.extra=${nb_all}/libs.javacapi/external/javac-api-nb-7.0-b07.jar javadoc.main.page=org/openide/filesystems/doc-files/api.html javadoc.arch=${basedir}/arch.xml javadoc.apichanges=${basedir}/apichanges.xml diff --git a/openide.filesystems/nbproject/project.xml b/openide.filesystems/nbproject/project.xml --- a/openide.filesystems/nbproject/project.xml +++ b/openide.filesystems/nbproject/project.xml @@ -75,6 +75,7 @@ org.openide.filesystems + org.openide.filesystems.annotations diff --git a/openide.filesystems/src/org/openide/filesystems/ExternalUtil.java b/openide.filesystems/src/org/openide/filesystems/ExternalUtil.java --- a/openide.filesystems/src/org/openide/filesystems/ExternalUtil.java +++ b/openide.filesystems/src/org/openide/filesystems/ExternalUtil.java @@ -213,6 +213,9 @@ is.close(); } } + for (URL generatedLayer : NbCollections.iterable(l.getResources("META-INF/generated-layer.xml"))) { // NOI18N + layerUrls.add(generatedLayer); + } layers.setXmlUrls(layerUrls.toArray(new URL[layerUrls.size()])); LOG.log(Level.FINE, "Loading classpath layers: {0}", layerUrls); } catch (Exception x) { diff --git a/openide.filesystems/src/org/openide/filesystems/annotations/LayerBuilder.java b/openide.filesystems/src/org/openide/filesystems/annotations/LayerBuilder.java new file mode 100644 --- /dev/null +++ b/openide.filesystems/src/org/openide/filesystems/annotations/LayerBuilder.java @@ -0,0 +1,342 @@ +/* + * 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.openide.filesystems.annotations; + +import java.net.URL; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.Map; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; + +/** + * Convenience class for generating fragments of an XML layer. + * @see LayerGeneratingProcessor#layer + */ +public final class LayerBuilder { + + private final Document doc; + + /** + * Creates a new builder. + * @param document a DOM representation of an XML layer which will be modified + */ + public LayerBuilder(Document document) { + this.doc = document; + } + + /** + * Adds a file to the layer. + * You need to {@link File#write} it in order to finalize the effect. + * @param path the full path to the desired file in resource format, e.g. {@code "Menu/File/exit.instance"} + * @return a file builder + */ + public File file(String path) { + return new File(path); + } + + /** + * Builder for creating a single file entry. + */ + public final class File { + + private final String path; + private final Map attrs = new LinkedHashMap(); + private String contents; + private String url; + + File(String path) { + this.path = path; + } + + /** + * Gets the path this file is to be created under. + * @return the configured path, as in {@link #file} + */ + public String getPath() { + return path; + } + + /** + * Configures the file to have inline text contents. + * @param contents text to use as the body of the file + * @return this builder + */ + public File contents(String contents) { + if (this.contents != null || url != null || contents == null) { + throw new IllegalArgumentException(); + } + this.contents = contents; + return this; + } + + /** + * Configures the file to have external contents. + * @param url a URL to the body of the file, e.g. {@code "nbresloc:/org/my/module/resources/definition.xml"} + * or more commonly an absolute resource path such as {@code "/org/my/module/resources/definition.xml"} + * @return this builder + */ + public File url(String url) { + if (contents != null || this.url != null || url == null) { + throw new IllegalArgumentException(); + } + this.url = url; + return this; + } + + /** + * Adds a string-valued attribute. + * @param attr the attribute name + * @param value the attribute value + * @return this builder + */ + public File stringvalue(String attr, String value) { + attrs.put(attr, new String[] {"stringvalue", value}); + return this; + } + + /** + * Adds a byte-valued attribute. + * @param attr the attribute name + * @param value the attribute value + * @return this builder + */ + public File bytevalue(String attr, byte value) { + attrs.put(attr, new String[] {"bytevalue", Byte.toString(value)}); + return this; + } + + /** + * Adds a short-valued attribute. + * @param attr the attribute name + * @param value the attribute value + * @return this builder + */ + public File shortvalue(String attr, short value) { + attrs.put(attr, new String[] {"shortvalue", Short.toString(value)}); + return this; + } + + /** + * Adds an int-valued attribute. + * @param attr the attribute name + * @param value the attribute value + * @return this builder + */ + public File intvalue(String attr, int value) { + attrs.put(attr, new String[] {"intvalue", Integer.toString(value)}); + return this; + } + + /** + * Adds a long-valued attribute. + * @param attr the attribute name + * @param value the attribute value + * @return this builder + */ + public File longvalue(String attr, long value) { + attrs.put(attr, new String[] {"longvalue", Long.toString(value)}); + return this; + } + + /** + * Adds a float-valued attribute. + * @param attr the attribute name + * @param value the attribute value + * @return this builder + */ + public File floatvalue(String attr, float value) { + attrs.put(attr, new String[] {"floatvalue", Float.toString(value)}); + return this; + } + + /** + * Adds a double-valued attribute. + * @param attr the attribute name + * @param value the attribute value + * @return this builder + */ + public File doublevalue(String attr, double value) { + attrs.put(attr, new String[] {"doublevalue", Double.toString(value)}); + return this; + } + + /** + * Adds a boolean-valued attribute. + * @param attr the attribute name + * @param value the attribute value + * @return this builder + */ + public File boolvalue(String attr, boolean value) { + attrs.put(attr, new String[] {"boolvalue", Boolean.toString(value)}); + return this; + } + + /** + * Adds a character-valued attribute. + * @param attr the attribute name + * @param value the attribute value + * @return this builder + */ + public File charvalue(String attr, char value) { + attrs.put(attr, new String[] {"charvalue", Character.toString(value)}); + return this; + } + + /** + * Adds a URL-valued attribute. + * @param attr the attribute name + * @param value the attribute value + * @return this builder + */ + public File urlvalue(String attr, URL value) { + attrs.put(attr, new String[] {"urlvalue", value.toString()}); + return this; + } + + /** + * Adds an attribute loaded from a Java method. + * @param attr the attribute name + * @param clazz the fully-qualified name of the factory class + * @param method the name of a static method + * @return this builder + */ + public File methodvalue(String attr, String clazz, String method) { + attrs.put(attr, new String[] {"methodvalue", clazz + "." + method}); + return this; + } + + /** + * Adds an attribute loaded from a Java constructor. + * @param attr the attribute name + * @param clazz the fully-qualified name of a class with a no-argument constructor + * @return this builder + */ + public File newvalue(String attr, String clazz) { + attrs.put(attr, new String[] {"newvalue", clazz}); + return this; + } + + /** + * Adds an attribute loaded from a resource bundle. + * @param attr the attribute name + * @param bundle the full name of the bundle, e.g. {@code "org.my.module.Bundle"} + * @param key the key to look up inside the bundle + * @return this builder + */ + public File bundlevalue(String attr, String bundle, String key) { + attrs.put(attr, new String[] {"bundlevalue", bundle + "#" + key}); + return this; + } + + // XXX do we want/need serialvalue? passed as String, or byte[], or Object? + + /** + * Sets a position attribute. + * This is a convenience method so you can define in your annotation: + * int position() default Integer.MAX_VALUE; + * and later call: + * fileBuilder.position(annotation.position()) + * @param position a numeric position for this file, or {@link Integer#MAX_VALUE} to not define any position + * @return this builder + */ + public File position(int position) { + if (position != Integer.MAX_VALUE) { + intvalue("position", position); + } + return this; + } + + /** + * Writes the file to the layer. + * Any intervening parent folders are created automatically. + * If the file already exists, the old copy is replaced. + * @return the originating layer builder, in case you want to add another file + */ + public LayerBuilder write() { + Element e = doc.getDocumentElement(); + String[] pieces = path.split("/"); + for (String piece : Arrays.asList(pieces).subList(0, pieces.length - 1)) { + Element kid = find(e, piece); + if (kid != null) { + if (!kid.getNodeName().equals("folder")) { + throw new IllegalArgumentException(path); + } + e = kid; + } else { + e = (Element) e.appendChild(doc.createElement("folder")); + e.setAttribute("name", piece); + } + } + String piece = pieces[pieces.length - 1]; + Element file = find(e,piece); + if (file != null) { + e.removeChild(file); + } + file = (Element) e.appendChild(doc.createElement("file")); + file.setAttribute("name", piece); + for (Map.Entry entry : attrs.entrySet()) { + Element attr = (Element) file.appendChild(doc.createElement("attr")); + attr.setAttribute("name", entry.getKey()); + attr.setAttribute(entry.getValue()[0], entry.getValue()[1]); + } + if (url != null) { + file.setAttribute("url", url); + } else if (contents != null) { + file.appendChild(doc.createCDATASection(contents)); + } + return LayerBuilder.this; + } + + private Element find(Element parent, String name) { + NodeList nl = parent.getElementsByTagName("*"); + for (int i = 0; i < nl.getLength(); i++) { + Element e = (Element) nl.item(i); + if (e.getAttribute("name").equals(name)) { + return e; + } + } + return null; + } + + } + +} diff --git a/openide.filesystems/src/org/openide/filesystems/annotations/LayerGeneratingProcessor.java b/openide.filesystems/src/org/openide/filesystems/annotations/LayerGeneratingProcessor.java new file mode 100644 --- /dev/null +++ b/openide.filesystems/src/org/openide/filesystems/annotations/LayerGeneratingProcessor.java @@ -0,0 +1,287 @@ +/* + * 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.openide.filesystems.annotations; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.WeakHashMap; +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.ProcessingEnvironment; +import javax.annotation.processing.RoundEnvironment; +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.type.TypeMirror; +import javax.lang.model.util.ElementFilter; +import javax.tools.Diagnostic.Kind; +import javax.tools.FileObject; +import javax.tools.StandardLocation; +import org.openide.filesystems.XMLFileSystem; +import org.openide.xml.XMLUtil; +import org.w3c.dom.Document; +import org.w3c.dom.NodeList; +import org.xml.sax.EntityResolver; +import org.xml.sax.ErrorHandler; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; + +/** + * Convenience base class for an annotation processor which creates XML layer entries. + * @see XMLFileSystem + */ +public abstract class LayerGeneratingProcessor extends AbstractProcessor { + + private static final String GENERATED_LAYER = "META-INF/generated-layer.xml"; + private static final String PUBLIC_DTD_ID = "-//NetBeans//DTD Filesystem 1.2//EN"; + private static final String NETWORK_DTD_URL = "http://www.netbeans.org/dtds/filesystem-1_2.dtd"; + private static final String LOCAL_DTD_RESOURCE = "/org/openide/filesystems/filesystem1_2.dtd"; + + private static final ErrorHandler ERROR_HANDLER = new ErrorHandler() { + public void warning(SAXParseException exception) throws SAXException {throw exception;} + public void error(SAXParseException exception) throws SAXException {throw exception;} + public void fatalError(SAXParseException exception) throws SAXException {throw exception;} + }; + + private static final EntityResolver ENTITY_RESOLVER = new EntityResolver() { + public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException { + if (PUBLIC_DTD_ID.equals(publicId)) { + return new InputSource(LayerGeneratingProcessor.class.getResource(LOCAL_DTD_RESOURCE).toString()); + } else { + return null; + } + } + }; + + private static final Map generatedLayerByProcessor = new WeakHashMap(); + private static final Map> originatingElementsByProcessor = new WeakHashMap>(); + + /** For access by subclasses. */ + protected LayerGeneratingProcessor() {} + + @Override + public final boolean process(Set annotations, RoundEnvironment roundEnv) { + boolean ret = doProcess(annotations, roundEnv); + if (roundEnv.processingOver()) { + Document doc = generatedLayerByProcessor.remove(processingEnv); + if (doc != null) { + Element[] originatingElementsA = new Element[0]; + List originatingElementsL = originatingElementsByProcessor.remove(processingEnv); + if (originatingElementsL != null) { + originatingElementsA = originatingElementsL.toArray(originatingElementsA); + } + try { + // Write to memory and reparse to make sure it is valid according to DTD before writing to disk. + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + XMLUtil.write(doc, baos, "UTF-8"); + byte[] data = baos.toByteArray(); + XMLUtil.parse(new InputSource(new ByteArrayInputStream(data)), true, true, ERROR_HANDLER, ENTITY_RESOLVER); + FileObject layer = processingEnv.getFiler().createResource(StandardLocation.CLASS_OUTPUT, "", GENERATED_LAYER, originatingElementsA); + OutputStream os = layer.openOutputStream(); + try { + os.write(data); + } finally { + os.close(); + } + { + SortedSet files = new TreeSet(); + NodeList nl = doc.getElementsByTagName("file"); + for (int i = 0; i < nl.getLength(); i++) { + org.w3c.dom.Element e = (org.w3c.dom.Element) nl.item(i); + String name = e.getAttribute("name"); + while ((e = (org.w3c.dom.Element) e.getParentNode()).getTagName().equals("folder")) { + name = e.getAttribute("name") + "/" + name; + } + files.add(name); + } + processingEnv.getMessager().printMessage(Kind.NOTE, "generated layer entries: " + files); + } + } catch (IOException x) { + processingEnv.getMessager().printMessage(Kind.ERROR, "Failed to write generated-layer.xml: " + x.toString()); + } catch (SAXException x) { + processingEnv.getMessager().printMessage(Kind.ERROR, "Refused to write invalid generated-layer.xml: " + x.toString()); + } + } + } + return ret; + } + + /** + * The regular body of {@link #process}. + * In the last round, one of the layer-generating processors will write out generated-layer.xml. + *

Do not attempt to read or write the layer file directly; just use {@link #layer}. + * You may however wish to create other resource files yourself: see {@link LayerBuilder.File#url} for syntax. + * @param annotations as in {@link #process} + * @param roundEnv as in {@link #process} + * @return as in {@link #process} + */ + protected abstract boolean doProcess(Set annotations, RoundEnvironment roundEnv); + + /** + * Access the generated XML layer document. + * May already have content from a previous compilation run which should be overwritten. + * May also have content from other layer-generated processors which should be appended to. + * Simply make changes to the document and they will be written to disk at the end of the job. + *

Use {@link LayerBuilder} to easily add file entries without working with the DOM directly. + * @param originatingElements as in {@link Filer#createResource}, optional + * @return the DOM document corresponding to the XML layer being created + */ + protected final Document layer(Element... originatingElements) { + List originatingElementsL = originatingElementsByProcessor.get(processingEnv); + if (originatingElementsL == null) { + originatingElementsL = new ArrayList(); + originatingElementsByProcessor.put(processingEnv, originatingElementsL); + } + originatingElementsL.addAll(Arrays.asList(originatingElements)); + Document doc = generatedLayerByProcessor.get(processingEnv); + if (doc == null) { + try { + FileObject layer = processingEnv.getFiler().getResource(StandardLocation.CLASS_OUTPUT, "", GENERATED_LAYER); + InputStream is = layer.openInputStream(); + try { + doc = XMLUtil.parse(new InputSource(is), true, true, ERROR_HANDLER, ENTITY_RESOLVER); + } finally { + is.close(); + } + } catch (FileNotFoundException fnfe) { + // Fine, not yet created. + } catch (IOException x) { + processingEnv.getMessager().printMessage(Kind.ERROR, "Failed to read generated-layer.xml: " + x.toString()); + } catch (SAXException x) { + processingEnv.getMessager().printMessage(Kind.ERROR, "Failed to parse generated-layer.xml: " + x.toString()); + } + if (doc == null) { + doc = XMLUtil.createDocument("filesystem", null, PUBLIC_DTD_ID, NETWORK_DTD_URL); + } + generatedLayerByProcessor.put(processingEnv, doc); + } + return doc; + } + + /** + * Generate an instance file whose {@code InstanceCookie} would load a given class or method. + * Useful for processors which define layer fragments which instantiate Java objects from the annotated code. + *

While you can pick a specific instance file name, if possible you should pass null for {@code name} + * as using the generated name will help avoid accidental name collisions between annotations. + * @param builder a builder to add a file to + * @param annotationTarget an annotated {@linkplain TypeElement class} or {@linkplain ExecutableElement method} + * @param path path to folder of instance file, e.g. {@code "Menu/File"} + * @param name instance file basename, e.g. {@code "my-menu-Item"}, or null to pick a name according to the element + * @param type a type to which the instance ought to be assignable, or null to skip this check + * @return an instance file (call {@link LayerBuilder.File#write} to finalize) + * @throws IllegalArgumentException if the annotationTarget is not of a suitable sort + * (detail message can be reported as a {@link Kind#ERROR}) + */ + protected final LayerBuilder.File instanceFile(LayerBuilder builder, Element annotationTarget, + String path, String name, Class type) throws IllegalArgumentException { + String clazz, method; + TypeMirror typeMirror = type != null ? processingEnv.getElementUtils().getTypeElement(type.getName()).asType() : null; + switch (annotationTarget.getKind()) { + case CLASS: { + clazz = processingEnv.getElementUtils().getBinaryName((TypeElement) annotationTarget).toString(); + method = null; + if (annotationTarget.getModifiers().contains(Modifier.ABSTRACT)) { + throw new IllegalArgumentException(clazz + " must not be abstract"); + } + { + boolean hasDefaultCtor = false; + for (ExecutableElement constructor : ElementFilter.constructorsIn(annotationTarget.getEnclosedElements())) { + if (constructor.getParameters().isEmpty()) { + hasDefaultCtor = true; + break; + } + } + if (!hasDefaultCtor) { + throw new IllegalArgumentException(clazz + " must have a no-argument constructor"); + } + } + if (typeMirror != null && !processingEnv.getTypeUtils().isAssignable(annotationTarget.asType(), typeMirror)) { + throw new IllegalArgumentException(clazz + " is not assignable to " + typeMirror); + } + break; + } + case METHOD: { + clazz = processingEnv.getElementUtils().getBinaryName((TypeElement) annotationTarget.getEnclosingElement()).toString(); + method = annotationTarget.getSimpleName().toString(); + if (!annotationTarget.getModifiers().contains(Modifier.STATIC)) { + throw new IllegalArgumentException(clazz + "." + method + " must be static"); + } + if (!((ExecutableElement) annotationTarget).getParameters().isEmpty()) { + throw new IllegalArgumentException(clazz + "." + method + " must not take arguments"); + } + if (typeMirror != null && !processingEnv.getTypeUtils().isAssignable(((ExecutableElement) annotationTarget).getReturnType(), typeMirror)) { + throw new IllegalArgumentException(clazz + "." + method + " is not assignable to " + typeMirror); + } + break; + } + default: + throw new IllegalArgumentException("Annotated element is not loadable as an instance: " + annotationTarget); + } + String basename; + if (name == null) { + basename = clazz.replace('.', '-'); + if (method != null) { + basename += "-" + method; + } + } else { + basename = name; + } + LayerBuilder.File f = builder.file(path + "/" + basename + ".instance"); + if (method != null) { + f.methodvalue("instanceCreate", clazz, method); + } else if (name != null) { + f.stringvalue("instanceClass", clazz); + } // else name alone suffices + return f; + } + +} diff --git a/openide.filesystems/test/unit/src/org/openide/filesystems/annotations/LayerBuilderTest.java b/openide.filesystems/test/unit/src/org/openide/filesystems/annotations/LayerBuilderTest.java new file mode 100644 --- /dev/null +++ b/openide.filesystems/test/unit/src/org/openide/filesystems/annotations/LayerBuilderTest.java @@ -0,0 +1,101 @@ +/* + * 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.openide.filesystems.annotations; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import org.netbeans.junit.NbTestCase; +import org.openide.xml.XMLUtil; +import org.w3c.dom.Document; + +public class LayerBuilderTest extends NbTestCase { + + public LayerBuilderTest(String n) { + super(n); + } + + private Document doc; + private LayerBuilder b; + + @Override + protected void setUp() throws Exception { + super.setUp(); + doc = XMLUtil.createDocument("filesystem", null, null, null); + b = new LayerBuilder(doc); + assertEquals("", dump()); + } + + private String dump() throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + XMLUtil.write(doc, baos, "UTF-8"); + return baos.toString("UTF-8"). + replace('"', '\''). + replaceFirst("^<\\?xml version='1\\.0' encoding='UTF-8'\\?>\r?\n", ""). + replaceAll("\r?\n *", ""); + } + + public void testBasicFiles() throws Exception { + b.file("Menu/File/x.instance").stringvalue("instanceClass", "some.X").write(). + file("Menu/Edit/y.instance").stringvalue("instanceClass", "some.Y").write(); + assertEquals("" + + "" + + "" + + "", dump()); + } + + public void testContent() throws Exception { + b.file("a.txt").contents("some text here...").write(). + file("b.xml").url("/resources/b.xml").write(); + assertEquals("" + + "", dump()); + } + + public void testOverwriting() throws Exception { + b.file("Menu/File/x.instance").stringvalue("instanceClass", "some.X").write(); + assertEquals("" + + "" + + "", dump()); + b.file("Menu/File/x.instance").write(); + assertEquals("" + + "" + + "", dump()); + } + +}