blob: 67c8f48cf529e72ffdf61fd9159a1303479dac6a [file] [log] [blame]
/**
* 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.netbeans.modules.apisupport.hints;
import com.sun.source.tree.AnnotationTree;
import com.sun.source.tree.AssignmentTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.ModifiersTree;
import com.sun.source.tree.NewArrayTree;
import com.sun.source.tree.Tree;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.RunnableFuture;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.swing.text.Document;
import org.netbeans.api.annotations.common.CheckForNull;
import org.netbeans.api.annotations.common.CheckReturnValue;
import org.netbeans.api.annotations.common.NullAllowed;
import org.netbeans.api.java.classpath.ClassPath;
import org.netbeans.api.java.source.JavaSource;
import org.netbeans.api.java.source.Task;
import org.netbeans.api.java.source.TreeMaker;
import org.netbeans.api.java.source.WorkingCopy;
import org.netbeans.modules.apisupport.project.api.LayerHandle;
import org.netbeans.spi.editor.hints.ChangeInfo;
import org.netbeans.spi.editor.hints.ErrorDescription;
import org.netbeans.spi.editor.hints.ErrorDescriptionFactory;
import org.netbeans.spi.editor.hints.Fix;
import org.netbeans.spi.editor.hints.Severity;
import org.openide.DialogDisplayer;
import org.openide.NotifyDescriptor;
import org.openide.NotifyDescriptor.Message;
import org.openide.cookies.SaveCookie;
import org.openide.filesystems.FileObject;
import org.openide.loaders.DataObject;
import static org.netbeans.modules.apisupport.hints.Bundle.*;
import org.openide.util.NbBundle.Messages;
/**
* One category of hint.
* Register implementation into global lookup.
*/
public interface Hinter {
/**
* Check for hints.
* @param ctx context of a single layer entry
* @throws Exception in case of problem
*/
void process(Context ctx) throws Exception;
/**
* Context supplied to a {@link Hinter}.
*/
class Context {
private static final Logger LOG = Logger.getLogger(Hinter.class.getName());
private final Document doc;
private final LayerHandle layer;
private final FileObject file;
private final RunnableFuture<Map<String,Integer>> lines;
private final List<? super ErrorDescription> errors;
Context(Document doc, LayerHandle layer, FileObject file, RunnableFuture<Map<String,Integer>> lines, List<? super ErrorDescription> errors) {
this.doc = doc;
this.layer = layer;
this.file = file;
this.lines = lines;
this.errors = errors;
}
/**
* Gets the layer entry you may offer hints for.
* File attribute names like {@code literal:instanceCreate} may return values like {@code new:pkg.Clazz} or {@code method:pkg.Clazz.factory}.
* @return a file (or folder) in the project's layer
*/
public FileObject file() {
return file;
}
/**
* @return standard description to pass to {@link #addHint}
*/
@Messages("Hinter.description=Use of layer entry where annotation is available")
public String standardAnnotationDescription() {
return Hinter_description();
}
/**
* @return standard fix description to pass to {@link #addHint}
*/
@Messages("Hinter.fix.description=Convert registration to Java annotation")
public String standardAnnotationFixDescription() {
return Hinter_fix_description();
}
/**
* Add a hint.
* @param severity whether to treat as a warning, etc.
* @param description description of hint
* @param fixes any fixes to offer
* @see #addStandardAnnotationHint
*/
public void addHint(Severity severity, String description, Fix... fixes) {
Integer line = null;
try {
lines.run();
line = lines.get().get(file.getPath());
} catch (Exception x) {
LOG.log(Level.INFO, null, x);
}
if (line != null) {
errors.add(ErrorDescriptionFactory.createErrorDescription(severity, description, Arrays.asList(fixes), doc, line));
} else {
LOG.log(Level.WARNING, "no line found for {0}", file);
}
}
/**
* Add an annotation-oriented warning hint following the standard pattern.
* @param fix what to do for a fix (see e.g. {@link #findAndModifyDeclaration}); no change info
* @see #addHint
* @see #standardAnnotationDescription
* @see #standardAnnotationFixDescription
*/
public void addStandardAnnotationHint(final Callable<Void> fix) {
addHint(Severity.WARNING, standardAnnotationDescription(), new Fix() {
public @Override String getText() {
return standardAnnotationFixDescription();
}
public @Override ChangeInfo implement() throws Exception {
fix.call();
return null;
}
});
}
/**
* Checks whether a given API class can be accessed from the current classpath.
* This can be used to control whether or not a given hint or fix is enabled.
* @param api binary name of an API class
* @return true if it is visible
*/
public boolean canAccess(String api) {
ClassPath cp = ClassPath.getClassPath(layer.getLayerFile(), ClassPath.COMPILE);
if (cp == null) {
return false;
}
return cp.findResource(api.replace('.', '/') + ".class") != null;
}
/**
* Locate the declaration of an object declared as a newvalue or methodvalue attribute.
* @param instanceAttribute the result of {@link FileObject#getAttribute} on a {@code literal:*} key
* @return the source file containing the corresponding declaration, or null if not found
*/
private @CheckForNull FileObject findDeclaringSource(@NullAllowed Object instanceAttribute) {
if (!(instanceAttribute instanceof String)) {
return null;
}
// XXX this will not find classes in a sister module; maybe look in ClassPath.EXECUTE, then use SFBQ to find it?
ClassPath src = ClassPath.getClassPath(layer.getLayerFile(), ClassPath.SOURCE); // should work even for Maven src/main/resources/.../layer.xml
if (src == null) {
return null;
}
String attr = (String) instanceAttribute;
if (attr.startsWith("new:")) {
return src.findResource(attr.substring(4).replaceFirst("[$][^.]+$", "").replace('.', '/') + ".java");
} else if (attr.startsWith("method:")) {
return src.findResource(attr.substring(7, attr.lastIndexOf('.')).replaceFirst("[$][^.]+$", "").replace('.', '/') + ".java");
} else {
return null;
}
}
/**
* Locate the declaration of an object declared as a newvalue or methodvalue attribute.
* @param wc context of a modification task
* @param instanceAttribute the result of {@link FileObject#getAttribute} on a {@code literal:*} key
* @return the corresponding declaration, or null if not found
*/
private @CheckForNull Element findDeclaration(WorkingCopy wc, @NullAllowed Object instanceAttribute) {
if (!(instanceAttribute instanceof String)) {
return null;
}
String attr = (String) instanceAttribute;
if (attr.startsWith("new:")) {
return wc.getElements().getTypeElement(attr.substring(4).replace('$', '.'));
} else if (attr.startsWith("method:")) {
int dot = attr.lastIndexOf('.');
TypeElement type = wc.getElements().getTypeElement(attr.substring(7, dot).replace('$', '.'));
if (type != null) {
String meth = attr.substring(dot + 1);
for (Element check : type.getEnclosedElements()) {
if (check.getKind() == ElementKind.METHOD && check.getSimpleName().contentEquals(meth) && ((ExecutableElement) check).getParameters().isEmpty()) {
return check;
}
}
}
}
return null;
}
/**
* Saves the layer after some modifications to {@link #file()}.
* @throws IOException if the layer could not be saved
*/
public void saveLayer() throws IOException {
layer.save();
}
/**
* Task to be used from {@link #findAndModifyDeclaration}.
*/
interface ModifyDeclarationTask {
/**
* Modify the original declaration.
* @param wc Java source information
* @param declaration the {@link TypeElement} or {@link ExecutableElement} that the instance attribute corresponds to
* @param modifiers modifiers of the declaration that you might wish to add annotations to
* @throws Exception in case of problem
*/
void run(WorkingCopy wc, Element declaration, ModifiersTree modifiers) throws Exception;
}
/**
* Tries to find the Java declaration of an instance attribute, and if successful, runs a task to modify it.
* @param instanceAttribute the result of {@link FileObject#getAttribute} on a {@code literal:*} key (or {@link #instanceAttribute})
* @param task a task to run (may modify Java sources and layer objects; all will be saved for you)
* @throws IOException in case of problem (will instead show a message and return early if the type could not be found)
*/
@Messages({
"# {0} - layer attribute", "Hinter.missing_instance_class=Could not find Java source corresponding to {0}.",
"Hinter.do_not_edit_layer=Do not edit layer.xml until the hint has had a chance to run."
})
public void findAndModifyDeclaration(@NullAllowed final Object instanceAttribute, final ModifyDeclarationTask task) throws IOException {
FileObject java = findDeclaringSource(instanceAttribute);
if (java == null) {
DialogDisplayer.getDefault().notify(new Message(Hinter_missing_instance_class(instanceAttribute), NotifyDescriptor.WARNING_MESSAGE));
return;
}
JavaSource js = JavaSource.forFileObject(java);
if (js == null) {
throw new IOException("No source info for " + java);
}
js.runModificationTask(new Task<WorkingCopy>() {
public @Override void run(WorkingCopy wc) throws Exception {
wc.toPhase(JavaSource.Phase.RESOLVED);
if (DataObject.find(layer.getLayerFile()).isModified()) { // #207077
DialogDisplayer.getDefault().notify(new Message(Hinter_do_not_edit_layer(), NotifyDescriptor.WARNING_MESSAGE));
return;
}
Element decl = findDeclaration(wc, instanceAttribute);
if (decl == null) {
DialogDisplayer.getDefault().notify(new Message(Hinter_missing_instance_class(instanceAttribute), NotifyDescriptor.WARNING_MESSAGE));
return;
}
ModifiersTree mods;
if (decl.getKind() == ElementKind.CLASS) {
mods = wc.getTrees().getTree((TypeElement) decl).getModifiers();
} else {
mods = wc.getTrees().getTree((ExecutableElement) decl).getModifiers();
}
task.run(wc, decl, mods);
saveLayer();
}
}).commit();
SaveCookie sc = DataObject.find(java).getLookup().lookup(SaveCookie.class);
if (sc != null) {
sc.save();
}
}
/**
* Tries to find the instance associated with a file.
* If it is a folder, or not a {@code *.instance} file, null is returned.
* If it has an {@code instanceCreate} attribute, that is used, else
* {@code instanceClass}, and finally the class implied by the filename.
* @param file any file
* @return a methodvalue or newvalue attribute, or null
* @see #findAndModifyDeclaration
*/
public @CheckForNull Object instanceAttribute(FileObject file) {
if (!file.isData() || !file.hasExt("instance")) {
return null; // not supporting *.settings etc. for now
}
Object instanceCreate = file.getAttribute("literal:instanceCreate");
if (instanceCreate != null) {
return instanceCreate;
}
Object clazz = file.getAttribute("instanceClass");
if (clazz != null) {
return "new:" + clazz;
}
return "new:" + file.getName().replace('-', '.');
}
/**
* Converts a (possibly) localized string attribute in a layer into a value suitable for {@link org.openide.filesystems.annotations.LayerBuilder.File#bundlevalue(String,String)}.
* @param attribute the result of {@link FileObject#getAttribute} on a {@code literal:*} key
* @param declaration the declaring element (used to calculate package)
* @return a string referring to the same (possibly) localized value (may be null)
*/
public @CheckForNull String bundlevalue(@NullAllowed Object attribute, Element declaration) {
if (attribute instanceof String) {
String val = (String) attribute;
if (val.startsWith("bundle:")) {
PackageElement pkg = findPackage(declaration);
if (pkg != null) {
String expected = "bundle:" + pkg.getQualifiedName() + ".Bundle#";
if (val.startsWith(expected)) {
return val.substring(expected.length() - 1); // keep '#'
}
}
return val.substring(7);
}
return val;
} else {
return null;
}
}
private PackageElement findPackage(Element e) {
if (e.getKind() == ElementKind.PACKAGE) {
return ((PackageElement) e);
}
Element parent = e.getEnclosingElement();
if (parent == null) {
return null;
}
return findPackage(parent);
}
/**
* Convenience method to add an annotation to an element.
* @param wc Java source context
* @param modifiers the element's modifiers to append to
* @param type canonical name of the annotation type
* @param pluralType if not null, canonical name of a plural variant of {@code type}
* @param parameters simple parameters of String or primitive type or String[] (null values are skipped)
* @return the expanded modifiers tree
*/
// XXX ought to also accept CompilationUnitTree
public @CheckReturnValue ModifiersTree addAnnotation(WorkingCopy wc, ModifiersTree modifiers, String type, @NullAllowed String pluralType, Map<String,Object> parameters) {
TreeMaker make = wc.getTreeMaker();
TypeElement ann = wc.getElements().getTypeElement(type);
if (ann == null) {
// XXX does this deserve a localized message? generally hint should have been disabled already if missing
throw new IllegalArgumentException("Could not find " + type + " in classpath");
}
List<ExpressionTree> arguments = new ArrayList<ExpressionTree>();
for (Map.Entry<String,Object> entry : parameters.entrySet()) {
Object value = entry.getValue();
ExpressionTree valueTree;
if (value instanceof Object[]) {
Object[] array = (Object[]) value;
if (array.length == 1) {
valueTree = make.Literal(array[0]);
} else {
List<ExpressionTree> elements = new ArrayList<ExpressionTree>();
for (Object o : array) {
elements.add(make.Literal(o));
}
valueTree = make.NewArray(null, Collections.<ExpressionTree>emptyList(), elements);
}
} else if (value != null) {
valueTree = make.Literal(value);
} else {
continue;
}
arguments.add(make.Assignment(make.Identifier(entry.getKey()), valueTree));
}
AnnotationTree toAdd = make.Annotation(make.QualIdent(ann), arguments);
if (pluralType != null) {
List<? extends AnnotationTree> existingAnns = modifiers.getAnnotations();
for (int i = 0; i < existingAnns.size(); i++) {
AnnotationTree existingAnn = existingAnns.get(i);
// XXX see UseNbBundleMessages; is there a better way to do this?
String existingType = existingAnn.getAnnotationType().toString();
// XXX this will not work if type is a nested class
if (existingType.equals(type) || existingType.equals(type.replaceFirst(".+[.]", ""))) {
return make.insertModifiersAnnotation(make.removeModifiersAnnotation(modifiers, i), i,
make.Annotation(make.QualIdent(pluralType),
Collections.singletonList(make.Assignment(make.Identifier("value"),
make.NewArray(null, Collections.<ExpressionTree>emptyList(), Arrays.asList(existingAnn, toAdd))))));
} else if (existingType.equals(pluralType) || existingType.equals(pluralType.replaceFirst(".+[.]", ""))) {
List<? extends ExpressionTree> args = existingAnn.getArguments();
if (args.size() != 1) {
throw new IllegalArgumentException("expecting just one arg for @" + pluralType);
}
AssignmentTree assign = (AssignmentTree) args.get(0);
if (!assign.getVariable().toString().equals("value")) {
throw new IllegalArgumentException("expected value=... for @" + pluralType);
}
ExpressionTree arg = assign.getExpression();
NewArrayTree arr;
if (arg.getKind() == Tree.Kind.STRING_LITERAL) {
arr = make.NewArray(null, Collections.<ExpressionTree>emptyList(), Collections.singletonList(arg));
} else if (arg.getKind() == Tree.Kind.NEW_ARRAY) {
arr = (NewArrayTree) arg;
} else {
throw new IllegalArgumentException("unknown arg kind " + arg.getKind() + ": " + arg);
}
return make.insertModifiersAnnotation(make.removeModifiersAnnotation(modifiers, i), i,
make.Annotation(existingAnn.getAnnotationType(),
Collections.singletonList(make.Assignment(assign.getVariable(), make.addNewArrayInitializer(arr, toAdd)))));
}
}
}
return make.addModifiersAnnotation(modifiers, toAdd);
}
/**
* Deletes an obsolete layer entry.
* Also deletes empty parent directories.
* @param entry a file to delete
* @throws IOException in case of problem
*/
public void delete(FileObject entry) throws IOException {
entry.delete();
FileObject parent = entry.getParent();
if (parent.getChildren().length == 0 && !parent.getAttributes().hasMoreElements()) {
if (parent.isRoot()) {
// XXX maybe delete the whole layer file! (and its reference in manifest.mf)
} else {
delete(parent);
}
}
}
}
}