org.netbeans.modules.java.JavaNode 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.netbeans.modules.java;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.CompilationUnitTree;
import org.netbeans.api.java.source.support.ErrorAwareTreeScanner;
import java.awt.Image;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.net.URI;
import java.net.URL;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.lang.model.element.Modifier;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import org.netbeans.api.annotations.common.CheckForNull;
import org.netbeans.api.annotations.common.NonNull;
import org.netbeans.api.annotations.common.NullAllowed;
import org.netbeans.api.java.classpath.ClassPath;
import org.netbeans.api.java.source.CompilationController;
import org.netbeans.api.java.source.JavaSource;
import org.netbeans.api.java.source.ModificationResult;
import org.netbeans.api.java.source.Task;
import org.netbeans.api.project.FileOwnerQuery;
import org.netbeans.api.project.Project;
import org.netbeans.api.queries.FileBuiltQuery;
import org.netbeans.api.queries.FileBuiltQuery.Status;
import org.netbeans.modules.classfile.Access;
import org.netbeans.modules.classfile.ClassFile;
import org.netbeans.modules.classfile.InvalidClassFormatException;
import org.netbeans.modules.java.source.usages.ExecutableFilesIndex;
import org.netbeans.modules.parsing.api.ParserManager;
import org.netbeans.modules.parsing.api.ResultIterator;
import org.netbeans.modules.parsing.api.Source;
import org.netbeans.modules.parsing.api.UserTask;
import org.netbeans.modules.parsing.spi.ParseException;
import org.netbeans.modules.parsing.spi.Parser;
import org.netbeans.spi.java.loaders.RenameHandler;
import org.openide.filesystems.*;
import org.openide.loaders.DataNode;
import org.openide.loaders.DataObject;
import org.openide.nodes.Children;
import org.openide.nodes.Node;
import org.openide.nodes.PropertySupport;
import org.openide.nodes.Sheet;
import org.openide.util.Exceptions;
import org.openide.util.ImageUtilities;
import static org.openide.util.ImageUtilities.assignToolTipToImage;
import static org.openide.util.ImageUtilities.loadImage;
import org.openide.util.Lookup;
import static org.openide.util.NbBundle.getMessage;
import org.openide.util.NbPreferences;
import org.openide.util.RequestProcessor;
import org.openide.util.Utilities;
import org.openide.util.WeakListeners;
/**
* The node representation of Java source files.
*/
public final class JavaNode extends DataNode implements ChangeListener {
/** generated Serialized Version UID */
private static final long serialVersionUID = -7396485743899766258L;
private static final String JAVA_ICON_BASE = "org/netbeans/modules/java/resources/class.png"; // NOI18N
private static final String CLASS_ICON_BASE = "org/netbeans/modules/java/resources/clazz.gif"; // NOI18N
private static final String ABSTRACT_CLASS_ICON_BASE = "org/netbeans/modules/java/resources/abstract_class_file.png"; //NOI18N
private static final String INTERFACE_ICON_BASE = "org/netbeans/modules/java/resources/interface_file.png"; //NOI18N
private static final String ENUM_ICON_BASE = "org/netbeans/modules/java/resources/enum_file.png"; //NOI18N
private static final String ANNOTATION_ICON_BASE = "org/netbeans/modules/java/resources/annotation_file.png"; //NOI18N
private static final String EXECUTABLE_BADGE_URL = "org/netbeans/modules/java/resources/executable-badge.png"; //NOI18N
private static final String NEEDS_COMPILE_BADGE_URL = "org/netbeans/modules/java/resources/needs-compile.png"; //NOI18N
private static final String FILE_ARGUMENTS = "single_file_run_arguments"; //NOI18N
private static final String FILE_VM_OPTIONS = "single_file_vm_options"; //NOI18N
private static final Map IMAGE_CACHE = new ConcurrentHashMap<>();
private static final boolean ALWAYS_PREFFER_COMPUTED_ICON = Boolean.getBoolean("JavaNode.prefferComputedIcon"); //NOI18N
private static final Logger LOG = Logger.getLogger(JavaNode.class.getName());
private Status status;
private final boolean isJavaSource;
private final AtomicReference isCompiled;
private final AtomicReference isExecutable;
private final AtomicReference computedIcon;
private final AtomicReference computedIconListener;
private ChangeListener executableListener;
/** Create a node for the Java data object using the default children.
* @param jdo the data object to represent
*/
public JavaNode (final DataObject jdo, boolean isJavaSource) {
super (jdo, Children.LEAF);
this.isJavaSource = isJavaSource;
this.isCompiled = new AtomicReference<>();
this.isExecutable = new AtomicReference<>();
this.computedIcon = new AtomicReference<>();
this.computedIconListener = new AtomicReference<>();
this.setIconBaseWithExtension(isJavaSource ? JAVA_ICON_BASE : CLASS_ICON_BASE);
Logger.getLogger("TIMER").log(Level.FINE, "JavaNode", new Object[] {jdo.getPrimaryFile(), this});
if (!jdo.isTemplate()) {
WORKER.post(IconTask.create(this));
if (isJavaSource) {
WORKER.post(new BuildStatusTask(this));
WORKER.post(new ExecutableTask(this));
jdo.addPropertyChangeListener(new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent evt) {
if (DataObject.PROP_PRIMARY_FILE.equals(evt.getPropertyName())) {
Logger.getLogger("TIMER").log(Level.FINE, "JavaNode", new Object[]{jdo.getPrimaryFile(), this});
WORKER.post(new Runnable() {
public void run() {
computedIconListener.set(null);
synchronized (JavaNode.this) {
status = null;
executableListener = null;
WORKER.post(new BuildStatusTask(JavaNode.this));
WORKER.post(new ExecutableTask(JavaNode.this));
}
}
});
}
}
});
}
}
}
@Override
public void setName(String name) {
RenameHandler handler = getRenameHandler();
if (handler == null) {
super.setName(name);
} else {
try {
handler.handleRename(JavaNode.this, name);
} catch (IllegalArgumentException ioe) {
super.setName(name);
}
}
}
private static synchronized RenameHandler getRenameHandler() {
Collection extends RenameHandler> handlers = (Lookup.getDefault().lookupAll(RenameHandler.class)) ;
if (handlers.size()==0)
return null;
if (handlers.size()>1)
LOG.warning("Multiple instances of RenameHandler found in Lookup; only using first one: " + handlers); //NOI18N
return handlers.iterator().next();
}
private PropertySet[] propertySets;
@Override
public PropertySet[] getPropertySets() {
getSheet(); //force initialization
synchronized (this) {
return Arrays.copyOf(propertySets, propertySets.length);
}
}
/** Create the property sheet.
* @return the sheet
*/
@Override
protected final Sheet createSheet () {
Sheet sheet = super.createSheet();
//if there is any rename handler installed
//push under our own property
if (getRenameHandler() != null)
sheet.get(Sheet.PROPERTIES).put(createNameProperty());
// Add classpath-related properties.
Sheet.Set ps = new Sheet.Set();
ps.setName("classpaths"); // NOI18N
ps.setDisplayName(getMessage(JavaNode.class, "LBL_JavaNode_sheet_classpaths"));
ps.setShortDescription(getMessage(JavaNode.class, "HINT_JavaNode_sheet_classpaths"));
ps.put(new Node.Property[] {
new ClasspathProperty(ClassPath.COMPILE,
getMessage(JavaNode.class, "PROP_JavaNode_compile_classpath"),
getMessage(JavaNode.class, "HINT_JavaNode_compile_classpath")),
new ClasspathProperty(ClassPath.EXECUTE,
getMessage(JavaNode.class, "PROP_JavaNode_execute_classpath"),
getMessage(JavaNode.class, "HINT_JavaNode_execute_classpath")),
new ClasspathProperty(ClassPath.BOOT,
getMessage(JavaNode.class, "PROP_JavaNode_boot_classpath"),
getMessage(JavaNode.class, "HINT_JavaNode_boot_classpath")),
});
sheet.put(ps);
Project parentProject = FileOwnerQuery.getOwner(super.getDataObject().getPrimaryFile());
DataObject dObj = super.getDataObject();
// If any of the parent folders is a project, user won't have the option to specify these attributes to the java files.
if (parentProject == null) {
Node.Property arguments = new org.openide.nodes.PropertySupport.ReadWrite (
"runFileArguments", // NOI18N
String.class,
"Arguments",
"Arguments passed to the main method while running the file."
) {
public String getValue () {
Object arguments = dObj.getPrimaryFile().getAttribute(FILE_ARGUMENTS);
return arguments != null ? (String) arguments : "";
}
public void setValue (String o) {
try {
dObj.getPrimaryFile().setAttribute(FILE_ARGUMENTS, o);
} catch (IOException ex) {
LOG.log(
Level.WARNING,
"Java File does not exist : {0}", //NOI18N
dObj.getPrimaryFile().getName());
}
}
};
Node.Property vmOptions = new org.openide.nodes.PropertySupport.ReadWrite (
"runFileVMOptions", // NOI18N
String.class,
"VM Options",
"VM Options to be considered while running the file."
) {
public String getValue () {
Object vmOptions = dObj.getPrimaryFile().getAttribute(FILE_VM_OPTIONS);
return vmOptions != null ? (String) vmOptions : "";
}
public void setValue(String o) {
try {
dObj.getPrimaryFile().setAttribute(FILE_VM_OPTIONS, o);
Source s = Source.create(dObj.getPrimaryFile());
ModificationResult result = ModificationResult.runModificationTask(Collections.singleton(s), new UserTask() {
@Override
public void run(ResultIterator resultIterator) {
}
});
result.commit();
} catch (IOException | ParseException ex) {
LOG.log(
Level.WARNING,
"Java File does not exist : {0}", //NOI18N
dObj.getPrimaryFile().getName());
}
}
};
Sheet.Set ss = new Sheet.Set();
ss.setName("runFileArguments"); // NOI18N
ss.setDisplayName(getMessage(JavaNode.class, "LBL_JavaNode_without_project_run"));
ss.setShortDescription("Run the file's source code.");
ss.put (arguments);
ss.put (vmOptions);
sheet.put(ss);
}
@SuppressWarnings("LocalVariableHidesMemberVariable")
PropertySet[] propertySets = sheet.toArray();
synchronized (this) {
this.propertySets = propertySets;
}
return sheet;
}
private Node.Property createNameProperty () {
Node.Property p = new PropertySupport.ReadWrite (
DataObject.PROP_NAME,
String.class,
getMessage (DataObject.class, "PROP_name"),
getMessage (DataObject.class, "HINT_name")
) {
public String getValue () {
return JavaNode.this.getName();
}
@Override
public Object getValue(String key) {
if ("suppressCustomEditor".equals (key)) { //NOI18N
return Boolean.TRUE;
} else {
return super.getValue (key);
}
}
public void setValue(String val) throws IllegalAccessException,
IllegalArgumentException, InvocationTargetException {
if (!canWrite())
throw new IllegalAccessException();
JavaNode.this.setName(val);
}
@Override
public boolean canWrite() {
return JavaNode.this.canRename();
}
};
return p;
}
/**
* Displays one kind of classpath for this Java source.
* Tries to use the normal format (directory or JAR names), falling back to URLs if necessary.
*/
private final class ClasspathProperty extends PropertySupport.ReadOnly {
private final String id;
public ClasspathProperty(String id, String displayName, String shortDescription) {
super(id, /*XXX NbClassPath would be preferable, but needs org.openide.execution*/String.class, displayName, shortDescription);
this.id = id;
// XXX the following does not always work... why?
setValue("oneline", false); // NOI18N
}
public String getValue() {
ClassPath cp = ClassPath.getClassPath(getDataObject().getPrimaryFile(), id);
if (cp != null) {
StringBuffer sb = new StringBuffer();
for (ClassPath.Entry entry : cp.entries()) {
URL u = entry.getURL();
String item = u.toExternalForm(); // fallback
if (u.getProtocol().equals("file")) { // NOI18N
item = Utilities.toFile(URI.create(item)).getAbsolutePath();
} else if (u.getProtocol().equals("jar") && item.endsWith("!/")) { // NOI18N
URL embedded = FileUtil.getArchiveFile(u);
assert embedded != null : u;
if (embedded.getProtocol().equals("file")) { // NOI18N
item = Utilities.toFile(URI.create(embedded.toExternalForm())).getAbsolutePath();
}
}
if (sb.length() > 0) {
sb.append(File.pathSeparatorChar);
}
sb.append(item);
}
return sb.toString();
} else {
return getMessage(JavaNode.class, "LBL_JavaNode_classpath_unknown");
}
}
}
public void stateChanged(ChangeEvent e) {
WORKER.post(new BuildStatusTask(this));
}
public Image getIcon(int type) {
Image i = prefferImage(
computedIcon.get(),
super.getIcon(type),
type);
return enhanceIcon(i);
}
public Image getOpenedIcon(int type) {
Image i = super.getOpenedIcon(type);
return enhanceIcon(i);
}
private Image prefferImage(Image computed, Image parent, int type) {
if (computed == null) {
return parent;
}
if (!ALWAYS_PREFFER_COMPUTED_ICON) {
final Object attrValue = parent.getProperty("url", null); //NOI18N
if (attrValue instanceof URL) {
final String url = attrValue.toString();
if (!(isJavaSource ? url.endsWith(JAVA_ICON_BASE) : url.endsWith(CLASS_ICON_BASE))) {
return parent;
}
}
}
try {
final FileObject fo = getDataObject().getPrimaryFile ();
computed = FileUIUtils.getImageDecorator(fo.getFileSystem ()).annotateIcon (
computed,
type,
Collections.singleton(fo));
} catch (FileStateInvalidException e) {
// no fs, do nothing
}
return computed;
}
private Image enhanceIcon(Image i) {
Image needsCompile = isCompiled.get();
if (needsCompile != null) {
i = ImageUtilities.mergeImages(i, needsCompile, 16, 0);
}
Image executable = isExecutable.get();
if (executable != null) {
i = ImageUtilities.mergeImages(i, executable, 10, 6);
}
return i;
}
private static final RequestProcessor WORKER = new RequestProcessor("Java Node Badge Processor", 1, false, false);
private static Image getImage(
@NonNull final String resourceId,
@NullAllowed final String annotationTemplate) {
Image result = IMAGE_CACHE.get(resourceId);
if (result == null) {
result = loadImage(resourceId, true);
if (annotationTemplate != null) {
URL resourceURL = JavaNode.class.getClassLoader().getResource(resourceId);
final String annotation = MessageFormat.format(
annotationTemplate,
resourceURL);
result = assignToolTipToImage(result, annotation);
}
IMAGE_CACHE.put(resourceId, result);
}
return result;
}
private abstract static class IconTask implements Runnable {
protected final JavaNode node;
IconTask(@NonNull final JavaNode node) {
this.node = node;
}
@CheckForNull
abstract String computeIcon(@NonNull final FileObject file);
@Override
public final void run() {
String res = null;
final FileObject file = node.getDataObject().getPrimaryFile();
if (file != null && file.isValid()) {
if (node.computedIconListener.get() == null) {
final FileChangeListener l = new FCL(node);
if (node.computedIconListener.compareAndSet(null, l)) {
file.addFileChangeListener(FileUtil.weakFileChangeListener(l, file));
}
}
res = computeIcon(file);
}
if (res == null) {
res = node.isJavaSource ?
JAVA_ICON_BASE :
CLASS_ICON_BASE;
}
node.computedIcon.set(getImage(res, null));
node.fireIconChange();
node.fireOpenedIconChange();
}
private static final class FCL extends FileChangeAdapter {
private final JavaNode node;
FCL(@NonNull final JavaNode node) {
this.node = node;
}
@Override
public void fileChanged(FileEvent fe) {
WORKER.post(IconTask.create(node));
}
}
private static final class SourceIcon extends IconTask {
private SourceIcon(@NonNull final JavaNode node) {
super(node);
}
@Override
String computeIcon(@NonNull final FileObject file) {
final String[] res = new String[1];
final JavaSource src = JavaSource.forFileObject(file);
if (src != null) {
try {
src.runUserActionTask(new Task() {
@Override
public void run(CompilationController cc) throws Exception {
cc.toPhase(JavaSource.Phase.PARSED);
final CompilationUnitTree cu = cc.getCompilationUnit();
final Collection topTypes = cu.accept(
new ClasssFinder(),
new ArrayList());
for (ClassTree ct : topTypes) {
switch(ct.getKind()) {
case CLASS:
res[0] = ct.getModifiers().getFlags().contains(Modifier.ABSTRACT) ?
ABSTRACT_CLASS_ICON_BASE :
JAVA_ICON_BASE;
break;
case INTERFACE:
res[0] = INTERFACE_ICON_BASE;
break;
case ENUM:
res[0] = ENUM_ICON_BASE;
break;
case ANNOTATION_TYPE:
res[0] = ANNOTATION_ICON_BASE;
break;
}
if (file.getName().contentEquals(ct.getSimpleName())) {
break;
}
}
}
}, true);
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
}
}
return res[0];
}
private static final class ClasssFinder extends ErrorAwareTreeScanner, Collection> {
@Override
public Collection visitCompilationUnit(CompilationUnitTree node, Collection p) {
super.visitCompilationUnit(node, p);
return p;
}
@Override
public Collection visitClass(ClassTree node, Collection p) {
p.add(node);
return p;
}
}
}
private static final class ClassIcon extends IconTask {
private ClassIcon(@NonNull final JavaNode node) {
super(node);
}
@Override
@CheckForNull
String computeIcon(@NonNull final FileObject file) {
String res = CLASS_ICON_BASE;
try {
try (InputStream in = file.getInputStream()) {
final ClassFile cf = new ClassFile(in, false);
if (cf.isEnum()) {
res = ENUM_ICON_BASE;
} else if (cf.isAnnotation()) {
res = ANNOTATION_ICON_BASE;
} else if ((cf.getAccess() & Access.INTERFACE) == Access.INTERFACE) {
res = INTERFACE_ICON_BASE;
} else {
res = (cf.getAccess() & Access.ABSTRACT) == Access.ABSTRACT ?
ABSTRACT_CLASS_ICON_BASE :
CLASS_ICON_BASE;
}
}
} catch (FileNotFoundException e) {
// may happen in the file is just being cleaned up; should not log.
} catch (InvalidClassFormatException e) {
// broken class file just log
LOG.log(
Level.INFO,
"Invalid classfile: {0}", //NOI18N
FileUtil.getFileDisplayName(file));
} catch (IOException e) {
Exceptions.printStackTrace(e);
}
return res;
}
}
static IconTask create(@NonNull final JavaNode node) {
return node.isJavaSource ?
new SourceIcon(node) :
new ClassIcon(node);
}
}
private static class BuildStatusTask implements Runnable {
private JavaNode node;
public BuildStatusTask(JavaNode node) {
this.node = node;
}
public void run() {
Status _status = null;
synchronized (node) {
_status = node.status;
}
if (_status == null) {
FileObject jf = node.getDataObject().getPrimaryFile();
_status = FileBuiltQuery.getStatus(jf);
synchronized (node) {
if (_status != null && node.status == null) {
node.status = _status;
node.status.addChangeListener(WeakListeners.change(node, node.status));
}
}
}
boolean isPackageInfo = "package-info.java".equals(node.getDataObject().getPrimaryFile().getNameExt());
boolean newIsCompiled = _status != null && !isPackageInfo ? _status.isBuilt() : true;
boolean oldIsCompiled = node.isCompiled.getAndSet(
newIsCompiled ?
null :
getImage(
NEEDS_COMPILE_BADGE_URL,
" " + getMessage(JavaNode.class, "TP_NeedsCompileBadge") //NOI18N
)) == null;
if (newIsCompiled != oldIsCompiled) {
node.fireIconChange();
node.fireOpenedIconChange();
}
}
}
private static class ExecutableTask implements Runnable {
private final JavaNode node;
public ExecutableTask(JavaNode node) {
this.node = node;
}
public void run() {
ChangeListener _executableListener;
synchronized (node) {
_executableListener = node.executableListener;
}
FileObject file = node.getDataObject().getPrimaryFile();
if (_executableListener == null) {
_executableListener = new ChangeListener() {
public void stateChanged(ChangeEvent e) {
WORKER.post(new ExecutableTask(node));
}
};
ExecutableFilesIndex.DEFAULT.addChangeListener(file.toURL(), _executableListener);
synchronized (node) {
if (node.executableListener == null) {
node.executableListener = _executableListener;
}
}
}
ClassPath cp = ClassPath.getClassPath(file, ClassPath.SOURCE);
FileObject root = cp != null ? cp.findOwnerRoot(file) : null;
if (root != null) {
boolean newIsExecutable = ExecutableFilesIndex.DEFAULT.isMainClass(root.toURL(), file.toURL());
boolean oldIsExecutable = node.isExecutable.getAndSet(
newIsExecutable ?
getImage(
EXECUTABLE_BADGE_URL,
" " + getMessage(JavaNode.class, "TP_ExecutableBadge")):
null) != null;
if (newIsExecutable != oldIsExecutable) {
node.fireIconChange();
node.fireOpenedIconChange();
}
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy