org.openide.filesystems.annotations.LayerBuilder Maven / Gradle / Ivy
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.openide.filesystems.annotations;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Annotation;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
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.ProcessingEnvironment;
import javax.lang.model.element.Element;
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.element.VariableElement;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;
import javax.tools.Diagnostic.Kind;
import javax.tools.FileObject;
import javax.tools.JavaFileManager.Location;
import javax.tools.StandardLocation;
import org.openide.util.NbBundle.Messages;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
/**
* Convenience class for generating fragments of an XML layer.
* @see LayerGeneratingProcessor#layer
* @since org.openide.filesystems 7.15
*/
public final class LayerBuilder {
private final Document doc;
private final Element originatingElement;
private final ProcessingEnvironment processingEnv;
private final List unwrittenFiles = new LinkedList();
LayerBuilder(Document document, Element/*or null*/ originatingElement, ProcessingEnvironment/* or null*/ processingEnv) {
this.doc = document;
this.originatingElement = originatingElement;
this.processingEnv = processingEnv;
}
/**
* 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) {
if (!path.matches("[^/]+(/[^/]+)*")) {
throw new IllegalArgumentException(path);
}
File f = new File(path, false);
unwrittenFiles.add(f);
return f;
}
/**
* Adds a folder to the layer.
* You need to {@link File#write} it in order to finalize the effect.
* Normally just using {@link #file} suffices, since parent folders are
* created as needed, but you may use this method if you wish to create a folder
* (possibly with some attributes) without necessarily creating any children.
* @param path the full path to the desired folder in resource format, e.g. {@code "Menu/File"}
* @return a file builder
* @since org.openide.filesystems 7.26
*/
public File folder(String path) {
File f = new File(path, true);
unwrittenFiles.add(f);
return f;
}
void close() {
for (File f : unwrittenFiles) {
if (f.getPath().startsWith("dummy/")) {
// ActionProcessor calls instanceFile purely to check for LayerGenerationException.
// Better would be to factor out the type-checking into its own set of utility methods.
continue;
}
processingEnv.getMessager().printMessage(Kind.WARNING, "layer file " + f.getPath() + " was never written");
}
unwrittenFiles.clear();
}
/**
* Generates an instance file whose {@code InstanceCookie} would load the associated 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 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 File#write} to finalize)
* @throws IllegalArgumentException if the builder is not associated with exactly one
* {@linkplain TypeElement class} or {@linkplain ExecutableElement method}
* @throws LayerGenerationException if the associated element would not be loadable as an instance of the specified type
*/
public File instanceFile(String path, String name, Class> type) throws IllegalArgumentException, LayerGenerationException {
return instanceFile(path, name, type, null, null);
}
/**
* Generates an instance file whose {@code InstanceCookie} would load the associated 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 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 annotation as in {@link LayerGenerationException#LayerGenerationException(String,Element,ProcessingEnvironment,Annotation,String)}
* @param annotationMethod as in {@link LayerGenerationException#LayerGenerationException(String,Element,ProcessingEnvironment,Annotation,String)}
* @return an instance file (call {@link File#write} to finalize)
* @throws IllegalArgumentException if the builder is not associated with exactly one
* {@linkplain TypeElement class} or {@linkplain ExecutableElement method}
* @throws LayerGenerationException if the associated element would not be loadable as an instance of the specified type
* @since 7.50
*/
public File instanceFile(String path, String name, Class> type, Annotation annotation, String annotationMethod) throws IllegalArgumentException, LayerGenerationException {
String[] clazzOrMethod = instantiableClassOrMethod(type, annotation, annotationMethod);
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;
}
/**
* Generates an instance file that is not initialized with an instance.
* Useful for {@link LayerGeneratingProcessor}s which define layer fragments
* which indirectly instantiate Java objects from the annotated code via a generic factory method.
* Invoke the factory using {@link File#methodvalue} on {@code instanceCreate}
* and configure it with a {@link File#instanceAttribute} appropriate to the factory.
*
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 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
* @return an instance file (call {@link File#write} to finalize)
* @throws IllegalArgumentException if the builder is not associated with exactly one
* {@linkplain TypeElement class} or {@linkplain ExecutableElement method}
* @throws LayerGenerationException if the associated element would not be loadable as an instance
* @since org.openide.filesystems 7.27
*/
public File instanceFile(String path, String name) throws IllegalArgumentException, LayerGenerationException {
return instanceFile(path, name, null, null);
}
/**
* Generates an instance file that is not initialized with an instance.
* Useful for {@link LayerGeneratingProcessor}s which define layer fragments
* which indirectly instantiate Java objects from the annotated code via a generic factory method.
* Invoke the factory using {@link File#methodvalue} on {@code instanceCreate}
* and configure it with a {@link File#instanceAttribute} appropriate to the factory.
*
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 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 annotation as in {@link LayerGenerationException#LayerGenerationException(String,Element,ProcessingEnvironment,Annotation,String)}
* @param annotationMethod as in {@link LayerGenerationException#LayerGenerationException(String,Element,ProcessingEnvironment,Annotation,String)}
* @return an instance file (call {@link File#write} to finalize)
* @throws IllegalArgumentException if the builder is not associated with exactly one
* {@linkplain TypeElement class} or {@linkplain ExecutableElement method}
* @throws LayerGenerationException if the associated element would not be loadable as an instance
* @since org.openide.filesystems 7.50
*/
public File instanceFile(String path, String name, Annotation annotation, String annotationMethod) throws IllegalArgumentException, LayerGenerationException {
String[] clazzOrMethod = instantiableClassOrMethod(null, annotation, annotationMethod);
String clazz = clazzOrMethod[0];
String method = clazzOrMethod[1];
String basename;
if (name == null) {
basename = clazz.replace('.', '-');
if (method != null) {
basename += "-" + method;
}
} else {
basename = name;
}
return file(path + "/" + basename + ".instance");
}
private String[] instantiableClassOrMethod(Class> type, Annotation annotation, String annotationMethod) throws IllegalArgumentException, LayerGenerationException {
if (originatingElement == null) {
throw new IllegalArgumentException("Only applicable to builders with exactly one associated element");
}
TypeMirror typeMirror = type != null ?
processingEnv.getTypeUtils().getDeclaredType(
processingEnv.getElementUtils().getTypeElement(type.getName().replace('$', '.'))) :
null;
switch (originatingElement.getKind()) {
case CLASS: {
String clazz = processingEnv.getElementUtils().getBinaryName((TypeElement) originatingElement).toString();
if (originatingElement.getModifiers().contains(Modifier.ABSTRACT)) {
throw new LayerGenerationException(clazz + " must not be abstract", originatingElement, processingEnv, annotation, annotationMethod);
}
{
boolean hasDefaultCtor = false;
for (ExecutableElement constructor : ElementFilter.constructorsIn(originatingElement.getEnclosedElements())) {
if (constructor.getParameters().isEmpty()) {
hasDefaultCtor = true;
break;
}
}
if (!hasDefaultCtor) {
throw new LayerGenerationException(clazz + " must have a no-argument constructor", originatingElement, processingEnv, annotation, annotationMethod);
}
}
if (typeMirror != null && !processingEnv.getTypeUtils().isAssignable(originatingElement.asType(), typeMirror)) {
throw new LayerGenerationException(clazz + " is not assignable to " + typeMirror, originatingElement, processingEnv, annotation, annotationMethod);
}
if (!originatingElement.getModifiers().contains(Modifier.PUBLIC)) {
throw new LayerGenerationException(clazz + " is not public", originatingElement, processingEnv, annotation, annotationMethod);
}
if (((TypeElement) originatingElement).getNestingKind().isNested() && !originatingElement.getModifiers().contains(Modifier.STATIC)) {
throw new LayerGenerationException(clazz + " is nested but not static", originatingElement, processingEnv, annotation, annotationMethod);
}
return new String[] {clazz, null};
}
case METHOD: {
String clazz = processingEnv.getElementUtils().getBinaryName((TypeElement) originatingElement.getEnclosingElement()).toString();
String method = originatingElement.getSimpleName().toString();
if (!originatingElement.getModifiers().contains(Modifier.STATIC)) {
throw new LayerGenerationException(clazz + "." + method + " must be static", originatingElement, processingEnv, annotation, annotationMethod);
}
List extends VariableElement> params = ((ExecutableElement) originatingElement).getParameters();
TypeMirror utilMapType = processingEnv.getTypeUtils().getDeclaredType(
processingEnv.getElementUtils().getTypeElement("java.util.Map"));
boolean mapParam = (params.size() == 1 && processingEnv.getTypeUtils().isAssignable(
params.get(0).asType(), utilMapType));
if (!params.isEmpty() && !mapParam) {
throw new LayerGenerationException(clazz + "." + method + " must not take arguments", originatingElement, processingEnv, annotation, annotationMethod);
}
if (typeMirror != null && !processingEnv.getTypeUtils().isAssignable(((ExecutableElement) originatingElement).getReturnType(), typeMirror)) {
throw new LayerGenerationException(clazz + "." + method + " is not assignable to " + typeMirror, originatingElement, processingEnv, annotation, annotationMethod);
}
return new String[] {clazz, method};
}
default:
throw new LayerGenerationException("Annotated element is not loadable as an instance", originatingElement, processingEnv, annotation, annotationMethod);
}
}
/**
* 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);
}
/**
* Validates a resource named in an annotation.
*
Note that resources found in the binary classpath (if permitted)
* cannot actually be located when running inside javac on JDK 6 (see #196933 for discussion), in which case
* no exception is thrown but the return value may not permit {@link FileObject#openInputStream}.
* {@code AnnotationProcessorTestUtils.searchClasspathBroken} should be used in unit tests affected by this bug.
*
Also remember that the binary compilation classpath for an Ant-based NetBeans module does
* not include non-public packages.
* (As of the 7.1 harness it does include non-classfile resources from public packages of module dependencies.)
* The processorpath does contain all of these but it is not consulted.
* The classpath for a Maven-based module does contain all resources from dependencies.
* @param resource an absolute resource path with no leading slash (perhaps the output of {@link #absolutizeResource})
* @param originatingElement the annotated element; used both for error reporting, and (optionally) for its package
* @param annotation as in {@link LayerGenerationException#LayerGenerationException(String,Element,ProcessingEnvironment,Annotation,String)}
* @param annotationMethod as in {@link LayerGenerationException#LayerGenerationException(String,Element,ProcessingEnvironment,Annotation,String)}
* @param searchClasspath true to search in the binary classpath and not just source path (see caveat about JDK 6)
* @return the content of the resource, for further validation
* @throws LayerGenerationException if no such resource can be found
* @since 7.51
*/
public FileObject validateResource(String resource, Element originatingElement, Annotation annotation, String annotationMethod, boolean searchClasspath) throws LayerGenerationException {
if (resource.startsWith("/")) {
throw new LayerGenerationException("do not use leading slashes on resource paths", originatingElement, processingEnv, annotation, annotationMethod);
}
if (searchClasspath) {
for (Location loc : new Location[] {StandardLocation.SOURCE_PATH, /* #181355 */StandardLocation.CLASS_OUTPUT, StandardLocation.CLASS_PATH, StandardLocation.PLATFORM_CLASS_PATH}) {
try {
FileObject f = processingEnv.getFiler().getResource(loc, "", resource);
if (loc.isOutputLocation()) {
f.openInputStream().close();
}
return f;
} catch (IOException ex) {
continue;
}
}
throw new LayerGenerationException("Cannot find resource " + resource, originatingElement, processingEnv, annotation, annotationMethod);
} else {
try {
try {
FileObject f = processingEnv.getFiler().getResource(StandardLocation.SOURCE_PATH, "", resource);
f.openInputStream().close();
return f;
} catch (FileNotFoundException x) {
try {
FileObject f = processingEnv.getFiler().getResource(StandardLocation.CLASS_OUTPUT, "", resource);
f.openInputStream().close();
return f;
} catch (IOException x2) {
throw x;
}
}
} catch (IOException x) {
throw new LayerGenerationException("Cannot find resource " + resource, originatingElement, processingEnv, annotation, annotationMethod);
}
}
}
/**
* Allows a processor to accept relative resource paths.
* For example, to produce the output value {@code net/nowhere/lib/icon.png}
* given an element in the package {@code net.nowhere.app}, the following inputs are permitted:
*
* - {@code ../lib/icon.png}
*
- {@code /net/nowhere/lib/icon.png}
*
* @param originatingElement the annotated element, used for its package
* @param resource a possibly relative resource path
* @return an absolute resource path (with no leading slash)
* @throws LayerGenerationException in case the resource path is malformed
* @since 7.51
*/
public static String absolutizeResource(Element originatingElement, String resource) throws LayerGenerationException {
if (resource.startsWith("/")) {
return resource.substring(1);
} else {
try {
return new URI(null, findPackage(originatingElement).replace('.', '/') + "/", null).resolve(new URI(null, resource, null)).getPath();
} catch (URISyntaxException x) {
throw new LayerGenerationException(x.toString(), originatingElement);
}
}
}
private static String findPackage(Element e) {
switch (e.getKind()) {
case PACKAGE:
return ((PackageElement) e).getQualifiedName().toString();
default:
return findPackage(e.getEnclosingElement());
}
}
/**
* Builder for creating a single file entry.
*/
public final class File {
private final String path;
private final boolean folder;
private final Map attrs = new LinkedHashMap();
private String contents;
private String url;
File(String path, boolean folder) {
this.path = path;
this.folder = folder;
}
/**
* 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 || folder) {
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 || folder) {
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, e.g. {@code "/my/module/resource.html"}
* or {@code "nbresloc:/my/module/resource.html"}; relative values permitted
* but not likely useful as base URL would be e.g. {@code "jar:...!/META-INF/"}
* @return this builder
* @throws LayerGenerationException in case an opaque URI is passed as {@code value}
*/
public File urlvalue(String attr, URI value) throws LayerGenerationException {
if (value.isOpaque()) {
throw new LayerGenerationException("Cannot use an opaque URI: " + value, originatingElement);
}
attrs.put(attr, new String[] {"urlvalue", value.toString()});
return this;
}
/**
* Adds a URL-valued attribute.
* @param attr the attribute name
* @param value the attribute value, e.g. {@code "/my/module/resource.html"}
* or {@code "nbresloc:/my/module/resource.html"}; relative values permitted
* but not likely useful as base URL would be e.g. {@code "jar:...!/META-INF/"}
* @return this builder
* @throws LayerGenerationException in case {@code value} cannot be parsed as a URI or is opaque
*/
public File urlvalue(String attr, String value) throws LayerGenerationException {
try {
return urlvalue(attr, URI.create(value));
} catch (IllegalArgumentException x) {
throw new LayerGenerationException(x.getLocalizedMessage(), originatingElement);
}
}
/**
* Adds an attribute loaded from a Java method.
* @param attr the attribute name
* @param clazz the fully-qualified binary 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 binary 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 the associated 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 type a type to which the instance ought to be assignable, or null to skip this check
* @return this builder
* @throws IllegalArgumentException if the associated element is not a {@linkplain TypeElement class} or {@linkplain ExecutableElement method}
* @throws LayerGenerationException if the associated element would not be loadable as an instance of the specified type
*/
public File instanceAttribute(String attr, Class> type) throws IllegalArgumentException, LayerGenerationException {
return instanceAttribute(attr, type, null, null);
}
/**
* Adds an attribute to load the associated 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 type a type to which the instance ought to be assignable, or null to skip this check
* @param annotation as in {@link LayerGenerationException#LayerGenerationException(String,Element,ProcessingEnvironment,Annotation,String)}
* @param annotationMethod as in {@link LayerGenerationException#LayerGenerationException(String,Element,ProcessingEnvironment,Annotation,String)}
* @return this builder
* @throws IllegalArgumentException if the associated element is not a {@linkplain TypeElement class} or {@linkplain ExecutableElement method}
* @throws LayerGenerationException if the associated element would not be loadable as an instance of the specified type
* @since 7.50
*/
public File instanceAttribute(String attr, Class> type, Annotation annotation, String annotationMethod) throws IllegalArgumentException, LayerGenerationException {
String[] clazzOrMethod = instantiableClassOrMethod(type, annotation, annotationMethod);
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 as the element associated with this builder (if exactly one)
* @return this builder
* @throws LayerGenerationException if a bundle key is requested but it cannot be found in sources
*/
public File bundlevalue(String attr, String label) throws LayerGenerationException {
return bundlevalue(attr, label, null, null);
}
/**
* 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 as the element associated with this builder (if exactly one)
* @param annotation as in {@link LayerGenerationException#LayerGenerationException(String,Element,ProcessingEnvironment,Annotation,String)}
* @param annotationMethod as in {@link LayerGenerationException#LayerGenerationException(String,Element,ProcessingEnvironment,Annotation,String)}
* @return this builder
* @throws LayerGenerationException if a bundle key is requested but it cannot be found in sources
* @since 7.50
*/
public File bundlevalue(String attr, String label, Annotation annotation, String annotationMethod) throws LayerGenerationException {
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) {
Element referenceElement = originatingElement;
while (referenceElement != null && referenceElement.getKind() != ElementKind.PACKAGE) {
referenceElement = referenceElement.getEnclosingElement();
}
if (referenceElement == null) {
throw new LayerGenerationException("No reference element to determine package in '" + label + "'", originatingElement);
}
bundle = ((PackageElement) referenceElement).getQualifiedName() + ".Bundle";
}
verifyBundleKey(bundle, key, m.group(1) == null, annotation, annotationMethod);
bundlevalue(attr, bundle, key);
} else {
stringvalue(attr, label);
}
return this;
}
private void verifyBundleKey(String bundle, String key, boolean samePackage, Annotation annotation, String annotationMethod) throws LayerGenerationException {
if (processingEnv == null) {
return;
}
if (samePackage) {
for (Element e = originatingElement; e != null; e = e.getEnclosingElement()) {
Messages m = e.getAnnotation(Messages.class);
if (m != null) {
for (String kv : m.value()) {
if (kv.startsWith(key + "=")) {
return;
}
}
}
}
}
try {
InputStream is = validateResource(bundle.replace('.', '/') + ".properties", originatingElement, null, null, false).openInputStream();
try {
Properties p = new Properties();
p.load(is);
if (p.getProperty(key) == null) {
throw new LayerGenerationException("No key '" + key + "' found in " + bundle, originatingElement, processingEnv, annotation, annotationMethod);
}
} finally {
is.close();
}
} catch (IOException x) {
throw new LayerGenerationException("Could not open " + bundle + ": " + x, originatingElement, processingEnv, annotation, annotationMethod);
}
}
/**
* 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 or folder to the layer.
* Any intervening parent folders are created automatically.
* If the file already exists, the old copy is replaced (not true in case of a folder).
* @return the originating layer builder, in case you want to add another file
*/
public LayerBuilder write() {
unwrittenFiles.remove(this);
org.w3c.dom.Element e = doc.getDocumentElement();
String[] pieces = path.split("/");
for (String piece : Arrays.asList(pieces).subList(0, pieces.length - 1)) {
org.w3c.dom.Element kid = find(e, piece, "file|folder");
if (kid != null) {
if (!kid.getNodeName().equals("folder")) {
throw new IllegalArgumentException(path);
}
e = kid;
} else {
e = (org.w3c.dom.Element) e.appendChild(doc.createElement("folder"));
e.setAttribute("name", piece);
}
}
String piece = pieces[pieces.length - 1];
org.w3c.dom.Element file = find(e, piece, "file|folder");
if (file == null) {
file = (org.w3c.dom.Element) e.appendChild(doc.createElement(folder ? "folder" : "file"));
file.setAttribute("name", piece);
}
if (originatingElement != null) {
// Embed comment in generated-layer.xml for easy navigation back to the annotation.
String name;
switch (originatingElement.getKind()) {
case CONSTRUCTOR:
case ENUM_CONSTANT:
case FIELD:
case INSTANCE_INIT:
case METHOD:
case STATIC_INIT:
name = originatingElement.getEnclosingElement() + "." + originatingElement;
break;
default:
name = originatingElement.toString();
}
boolean addComment = true;
NodeList oldComments = file.getChildNodes();
for (int i = 0; i < oldComments.getLength(); i++) {
Node node = oldComments.item(i);
if (node.getNodeType() == Node.COMMENT_NODE && node.getNodeValue().equals(name)) {
addComment = false;
break;
}
}
if (addComment) {
file.appendChild(doc.createComment(name));
}
}
for (Map.Entry entry : attrs.entrySet()) {
org.w3c.dom.Element former = find(file, entry.getKey(), "attr");
if (former != null) {
file.removeChild(former);
}
org.w3c.dom.Element attr = (org.w3c.dom.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) {
NodeList oldContents = file.getChildNodes();
for (int i = 0; i < oldContents.getLength();) {
Node node = oldContents.item(i);
if (node.getNodeType() == Node.CDATA_SECTION_NODE) {
file.removeChild(node);
} else {
i++;
}
}
file.appendChild(doc.createCDATASection(contents));
}
return LayerBuilder.this;
}
private org.w3c.dom.Element find(org.w3c.dom.Element parent, String name, String kindRx) {
NodeList nl = parent.getChildNodes();
for (int i = 0; i < nl.getLength(); i++) {
Node item = nl.item(i);
if (item.getNodeType() != Node.ELEMENT_NODE) {
continue;
}
org.w3c.dom.Element e = (org.w3c.dom.Element) item;
if (e.getAttribute("name").equals(name) && e.getNodeName().matches(kindRx)) {
return e;
}
}
return null;
}
}
}