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,546 @@
+/*
+ * 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.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Properties;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import javax.annotation.processing.Filer;
+import javax.annotation.processing.ProcessingEnvironment;
+import javax.lang.model.element.ElementKind;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.Modifier;
+import javax.lang.model.element.PackageElement;
+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.StandardLocation;
+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
+ * @since XXX #150447
+ */
+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);
+ }
+
+ /**
+ * Generates an instance file whose {@code InstanceCookie} would load a given class or method.
+ * Useful for {@link LayerGeneratingProcessor}s 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 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
+ * @param processingEnv a processor environment used for {@link ProcessingEnvironment#getElementUtils} and {@link ProcessingEnvironment#getTypeUtils}
+ * @return an instance file (call {@link 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})
+ */
+ public File instanceFile(javax.lang.model.element.Element annotationTarget, String path, String name, Class type,
+ ProcessingEnvironment processingEnv) throws IllegalArgumentException {
+ String[] clazzOrMethod = instantiableClassOrMethod(annotationTarget, type, processingEnv);
+ String clazz = clazzOrMethod[0];
+ String method = clazzOrMethod[1];
+ String basename;
+ if (name == null) {
+ basename = clazz.replace('.', '-');
+ if (method != null) {
+ basename += "-" + method;
+ }
+ } else {
+ basename = name;
+ }
+ LayerBuilder.File f = 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;
+ }
+
+ private static String[] instantiableClassOrMethod(javax.lang.model.element.Element annotationTarget, Class type,
+ ProcessingEnvironment processingEnv) throws IllegalArgumentException {
+ TypeMirror typeMirror = type != null ? processingEnv.getElementUtils().getTypeElement(type.getName().replace('$', '.')).asType() : null;
+ switch (annotationTarget.getKind()) {
+ case CLASS: {
+ String clazz = processingEnv.getElementUtils().getBinaryName((TypeElement) annotationTarget).toString();
+ 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);
+ }
+ return new String[] {clazz, null};
+ }
+ case METHOD: {
+ String clazz = processingEnv.getElementUtils().getBinaryName((TypeElement) annotationTarget.getEnclosingElement()).toString();
+ String 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);
+ }
+ return new String[] {clazz, method};
+ }
+ default:
+ throw new IllegalArgumentException("Annotated element is not loadable as an instance: " + annotationTarget);
+ }
+ }
+
+ /**
+ * Convenience method to create a shadow file (like a symbolic link).
+ *
While you can pick a specific shadow 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 target the complete path to the original file (use {@link File#getPath} if you just made it)
+ * @param folder the folder path in which to create the shadow, e.g. {@code "Menu/File"}
+ * @param name the basename of the shadow file sans extension, e.g. {@code "my-Action"}, or null to pick a default
+ * @return a shadow file (call {@link File#write} to finalize)
+ */
+ public File shadowFile(String target, String folder, String name) {
+ if (name == null) {
+ name = target.replaceFirst("^.+/", "").replaceFirst("\\.[^./]+$", "");
+ }
+ return file(folder + "/" + name + ".shadow").stringvalue("originalFile", target);
+ }
+
+ /**
+ * 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 to load a given class or method.
+ * Useful for {@link LayerGeneratingProcessor}s which define layer fragments which instantiate Java objects from the annotated code.
+ * @param attr the attribute name
+ * @param annotationTarget an annotated {@linkplain TypeElement class} or {@linkplain ExecutableElement method}
+ * @param type a type to which the instance ought to be assignable, or null to skip this check
+ * @param processingEnv a processor environment used for {@link ProcessingEnvironment#getElementUtils} and {@link ProcessingEnvironment#getTypeUtils}
+ * @return this builder
+ * @throws IllegalArgumentException if the annotationTarget is not of a suitable sort
+ * (detail message can be reported as a {@link Kind#ERROR})
+ */
+ public File instanceAttribute(String attr, javax.lang.model.element.Element annotationTarget, Class type,
+ ProcessingEnvironment processingEnv) throws IllegalArgumentException {
+ String[] clazzOrMethod = instantiableClassOrMethod(annotationTarget, type, processingEnv);
+ if (clazzOrMethod[1] == null) {
+ newvalue(attr, clazzOrMethod[0]);
+ } else {
+ methodvalue(attr, clazzOrMethod[0], clazzOrMethod[1]);
+ }
+ 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;
+ }
+
+ /**
+ * Adds an attribute for a possibly localized string.
+ * @param attr the attribute name
+ * @param label either a general string to store as is, or a resource bundle reference
+ * such as {@code "my.module.Bundle#some_key"},
+ * or just {@code "#some_key"} to load from a {@code "Bundle"} in the same package
+ * @param referenceElement if not null, a source element to determine the package
+ * @param filer if not null, a way to look up the source bundle to verify that it exists and has the specified key
+ * @return this builder
+ * @throws IllegalArgumentException if a bundle key is requested but it cannot be found in sources
+ * (detail message can be reported as a {@link Kind#ERROR})
+ */
+ public File bundlevalue(String attr, String label, javax.lang.model.element.Element referenceElement, Filer filer) throws IllegalArgumentException {
+ String javaIdentifier = "(?:\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)";
+ Matcher m = Pattern.compile("((?:" + javaIdentifier + "\\.)+[^\\s.#]+)?#(\\S+)").matcher(label);
+ if (m.matches()) {
+ String bundle = m.group(1);
+ String key = m.group(2);
+ if (bundle == null) {
+ while (referenceElement != null && referenceElement.getKind() != ElementKind.PACKAGE) {
+ referenceElement = referenceElement.getEnclosingElement();
+ }
+ if (referenceElement == null) {
+ throw new IllegalArgumentException("No reference element to determine package in '" + label + "'");
+ }
+ bundle = ((PackageElement) referenceElement).getQualifiedName() + ".Bundle";
+ }
+ if (filer != null) {
+ try {
+ InputStream is = filer.getResource(StandardLocation.SOURCE_PATH, "", bundle.replace('.', '/') + ".properties").openInputStream();
+ try {
+ Properties p = new Properties();
+ p.load(is);
+ if (p.getProperty(key) == null) {
+ throw new IllegalArgumentException("No key '" + key + "' found in " + bundle);
+ }
+ } finally {
+ is.close();
+ }
+ } catch (IOException x) {
+ throw new IllegalArgumentException("Could not open " + bundle + ": " + x);
+ }
+ }
+ bundlevalue(attr, bundle, key);
+ } else {
+ stringvalue(attr, label);
+ }
+ return this;
+ }
+
+ /**
+ * Adds an attribute which deserializes a Java value.
+ * @param attr the attribute name
+ * @param data the serial data as created by {@link ObjectOutputStream}
+ * @return this builder
+ */
+ public File serialvalue(String attr, byte[] data) {
+ StringBuilder buf = new StringBuilder(data.length * 2);
+ for (byte b : data) {
+ if (b >= 0 && b < 16) {
+ buf.append('0');
+ }
+ buf.append(Integer.toHexString(b < 0 ? b + 256 : b));
+ }
+ attrs.put(attr, new String[] {"serialvalue", buf.toString().toUpperCase(Locale.ENGLISH)});
+ return this;
+ }
+
+ /**
+ * 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,210 @@
+/*
+ * 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.Filer;
+import javax.annotation.processing.ProcessingEnvironment;
+import javax.annotation.processing.RoundEnvironment;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.TypeElement;
+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
+ * @since XXX #150447
+ */
+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 extends TypeElement> annotations, RoundEnvironment roundEnv) {
+ boolean ret = doProcess(annotations, roundEnv);
+ if (roundEnv.processingOver() && !roundEnv.errorRaised()) {
+ 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);
+ }
+ for (String file : files) {
+ processingEnv.getMessager().printMessage(Kind.NOTE, "generated layer entry: " + file);
+ }
+ }
+ } 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 extends TypeElement> 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;
+ }
+
+}
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,120 @@
+/*
+ * 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());
+ }
+
+ public void testShadows() throws Exception {
+ LayerBuilder.File orig = b.file("Actions/System/some-Action.instance");
+ orig.write();
+ b.shadowFile(orig.getPath(), "Menu/File", null).write();
+ b.shadowFile(orig.getPath(), "Shortcuts", "C-F6").write();
+ assertEquals("" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "" +
+ "", dump());
+ }
+
+ public void testSerialValue() throws Exception {
+ b.file("x").serialvalue("a", new byte[] {0, 10, 100, (byte) 200}).write();
+ assertEquals("", dump());
+ }
+
+}