blob: 4b4649c0ab972457ec4524cd874be7929c573e2c [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.websocket.editor;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.ElementFilter;
import javax.swing.text.Position;
import org.netbeans.api.j2ee.core.Profile;
import org.netbeans.api.java.lexer.JavaTokenId;
import org.netbeans.api.java.source.CancellableTask;
import org.netbeans.api.java.source.CompilationInfo;
import org.netbeans.api.java.source.ElementHandle;
import org.netbeans.api.java.source.JavaSource;
import org.netbeans.api.java.source.ModificationResult;
import org.netbeans.api.java.source.ModificationResult.Difference;
import org.netbeans.api.java.source.Task;
import org.netbeans.api.java.source.TreeMaker;
import org.netbeans.api.java.source.TreeUtilities;
import org.netbeans.api.java.source.WorkingCopy;
import org.netbeans.api.lexer.Token;
import org.netbeans.api.lexer.TokenSequence;
import org.netbeans.api.project.FileOwnerQuery;
import org.netbeans.api.project.Project;
import org.netbeans.modules.web.api.webmodule.WebModule;
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.HintsController;
import org.netbeans.spi.editor.hints.Severity;
import org.openide.filesystems.FileObject;
import org.openide.util.NbBundle;
import com.sun.source.tree.AnnotationTree;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.ModifiersTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.TypeParameterTree;
import com.sun.source.tree.VariableTree;
import com.sun.source.util.SourcePositions;
/**
* @author ads
*
*/
public class WebSocketMethodsTask implements CancellableTask<CompilationInfo> {
private static final String ON_ERROR_ANNOTATION = "javax.websocket.OnError"; // NOI18N
@Override
public void run(CompilationInfo compilationInfo) throws Exception {
FileObject fileObject = compilationInfo.getFileObject();
if (!isApplicable(fileObject)) {
return;
}
WebSocketTask task = new WebSocketTask(compilationInfo);
runTask.set(task);
task.run();
runTask.compareAndSet(task, null);
HintsController.setErrors(fileObject, "WebSocket Methods Scanner", // NOI18N
task.getDescriptions());
}
/*
* (non-Javadoc)
*
* @see org.netbeans.api.java.source.CancellableTask#cancel()
*/
@Override
public void cancel() {
WebSocketTask scanTask = runTask.getAndSet(null);
if (scanTask != null) {
scanTask.stop();
}
}
private boolean isApplicable(FileObject fileObject) {
Project project = FileOwnerQuery.getOwner(fileObject);
if (project == null) {
return false;
}
WebModule webModule = WebModule.getWebModule(project
.getProjectDirectory());
if (webModule == null) {
return false;
}
Profile profile = webModule.getJ2eeProfile();
if (!Profile.JAVA_EE_7_WEB.equals(profile)
&& !Profile.JAVA_EE_7_FULL.equals(profile)
&& !Profile.JAVA_EE_8_WEB.equals(profile)
&& !Profile.JAVA_EE_8_FULL.equals(profile)
&& !Profile.JAKARTA_EE_8_FULL.equals(profile)
&& !Profile.JAKARTA_EE_9_FULL.equals(profile)
&& !Profile.JAKARTA_EE_9_1_FULL.equals(profile)) {
return false;
}
return true;
}
private boolean hasAnnotation(Element element, String... annotationFqns) {
List<? extends AnnotationMirror> annotations = element
.getAnnotationMirrors();
for (AnnotationMirror annotation : annotations) {
Element annotationElement = annotation.getAnnotationType()
.asElement();
if (annotationElement instanceof TypeElement) {
String fqn = ((TypeElement) annotationElement)
.getQualifiedName().toString();
for (String annotationFqn : annotationFqns) {
if (fqn.equals(annotationFqn)) {
return true;
}
}
}
}
return false;
}
private Map<String, String> suggestWebSocketMethod() {
Map<String, String> result = new HashMap<String, String>();
result.put("javax.websocket.OnMessage", "onMessage"); // NOI18N
result.put("javax.websocket.OnOpen", "onOpen"); // NOI18N
result.put("javax.websocket.OnClose", "onClose"); // NOI18N
result.put(ON_ERROR_ANNOTATION, "onError"); // NOI18N
return result;
}
private Collection<String> getAnnotationsFqn(Element element) {
List<? extends AnnotationMirror> annotations = element
.getAnnotationMirrors();
Collection<String> result = new HashSet<String>();
for (AnnotationMirror annotation : annotations) {
Element annotationElement = annotation.getAnnotationType()
.asElement();
if (annotationElement instanceof TypeElement) {
String fqn = ((TypeElement) annotationElement)
.getQualifiedName().toString();
result.add(fqn);
}
}
return result;
}
private List<Integer> getElementPosition(CompilationInfo info, Tree tree) {
SourcePositions srcPos = info.getTrees().getSourcePositions();
int startOffset = (int) srcPos.getStartPosition(
info.getCompilationUnit(), tree);
int endOffset = (int) srcPos.getEndPosition(info.getCompilationUnit(),
tree);
Tree startTree = null;
if (TreeUtilities.CLASS_TREE_KINDS.contains(tree.getKind())) {
startTree = ((ClassTree) tree).getModifiers();
} else if (tree.getKind() == Tree.Kind.METHOD) {
startTree = ((MethodTree) tree).getReturnType();
} else if (tree.getKind() == Tree.Kind.VARIABLE) {
startTree = ((VariableTree) tree).getType();
}
if (startTree != null) {
int searchStart = (int) srcPos.getEndPosition(
info.getCompilationUnit(), startTree);
TokenSequence<?> tokenSequence = info.getTreeUtilities().tokensFor(
tree);
if (tokenSequence != null) {
boolean eob = false;
tokenSequence.move(searchStart);
do {
eob = !tokenSequence.moveNext();
} while (!eob
&& tokenSequence.token().id() != JavaTokenId.IDENTIFIER);
if (!eob) {
Token<?> identifier = tokenSequence.token();
startOffset = identifier.offset(info.getTokenHierarchy());
endOffset = startOffset + identifier.length();
}
}
}
List<Integer> result = new ArrayList<Integer>(2);
result.add(startOffset);
result.add(endOffset);
return result;
}
private class WebSocketTask {
private WebSocketTask(CompilationInfo info) {
myInfo = info;
descriptions = new LinkedList<ErrorDescription>();
}
void run() {
List<? extends TypeElement> classes = myInfo.getTopLevelElements();
for (TypeElement clazz : classes) {
if (stop) {
return;
}
List<ExecutableElement> methods = ElementFilter.methodsIn(clazz
.getEnclosedElements());
if (!isEndpoint(clazz)) {
continue;
}
Map<String, String> webSocketMethod = suggestWebSocketMethod();
Set<String> wsAnnotations = webSocketMethod.keySet();
Set<String> existedMethods = new HashSet<String>();
for (ExecutableElement method : methods) {
if (stop) {
return;
}
wsAnnotations.removeAll(getAnnotationsFqn(method));
existedMethods.add(method.getSimpleName().toString());
}
List<Fix> fixes = new LinkedList<Fix>();
for (Entry<String, String> entry : webSocketMethod.entrySet()) {
Fix fix = new AddMethod(myInfo.getFileObject(),
ElementHandle.create(clazz), entry.getKey(),
entry.getValue(), existedMethods);
fixes.add(fix);
}
if (!fixes.isEmpty()) {
ClassTree classTree = myInfo.getTrees().getTree(clazz);
List<Integer> positions = getElementPosition(myInfo,
classTree);
ErrorDescription description = ErrorDescriptionFactory
.createErrorDescription(Severity.HINT, NbBundle
.getMessage(WebSocketMethodsTask.class,
"TXT_AddWebSocketMethods"), // NOI18N
fixes, myInfo.getFileObject(), positions
.get(0), positions.get(1));
getDescriptions().add(description);
}
}
}
private boolean isEndpoint(TypeElement clazz) {
return hasAnnotation(clazz, "javax.websocket.server.ServerEndpoint"); // NOI18N
}
Collection<ErrorDescription> getDescriptions() {
return descriptions;
}
void stop() {
stop = true;
}
private final Collection<ErrorDescription> descriptions;
private volatile boolean stop;
private final CompilationInfo myInfo;
}
private class AddMethod implements Fix {
AddMethod(FileObject fileObject,
ElementHandle<TypeElement> endpointClass, String annotation,
String methodName, Set<String> existedMethodNames) {
myFileObject = fileObject;
myHandle = endpointClass;
myAnnotation = annotation;
myMethodName = methodName;
myExistedMethods = existedMethodNames;
}
@Override
public ChangeInfo implement() throws Exception {
JavaSource javaSource = JavaSource.forFileObject(myFileObject);
if (javaSource == null) {
return null;
}
ModificationResult modificationTask = javaSource
.runModificationTask(new Task<WorkingCopy>() {
@Override
public void run(WorkingCopy copy) throws Exception {
copy.toPhase(JavaSource.Phase.ELEMENTS_RESOLVED);
TypeElement clazz = myHandle.resolve(copy);
TreeMaker maker = copy.getTreeMaker();
ClassTree classTree = copy.getTrees()
.getTree(clazz);
AnnotationTree annotation = maker.Annotation(
maker.QualIdent(myAnnotation),
Collections.<ExpressionTree> emptyList());
ModifiersTree modifiers = maker.Modifiers(EnumSet
.of(Modifier.PUBLIC), Collections
.<AnnotationTree> singletonList(annotation));
VariableTree par1 = null;
if (ON_ERROR_ANNOTATION.equals(myAnnotation)) {
ModifiersTree parMods = maker.Modifiers(
Collections.<Modifier>emptySet(),
Collections.<AnnotationTree>emptyList());
par1 = maker.Variable(parMods, "t", // NOI18N
maker.QualIdent("java.lang.Throwable"), null); // NOI18N
}
MethodTree method = maker.Method(
modifiers,
getMethodName(),
maker.Type("void"), // NOI18N
Collections.<TypeParameterTree> emptyList(),
/*
* XXX : optional session parameter could be
* provided in the method signature :
* javax.websocket.Session
*/
par1 != null
? Collections.<VariableTree>singletonList(par1)
: Collections.<VariableTree>emptyList(),
Collections.<ExpressionTree> emptyList(),
"{}", null);
ClassTree newTree = maker.addClassMember(classTree,
method);
copy.rewrite(classTree, newTree);
}
});
List<? extends Difference> differences = modificationTask
.getDifferences(myFileObject);
ChangeInfo changeInfo = new ChangeInfo();
for (Difference difference : differences) {
Position start = difference.getStartPosition();
Position end = difference.getEndPosition();
changeInfo.add(myFileObject, start, end);
}
modificationTask.commit();
return changeInfo;
}
@Override
public String getText() {
return NbBundle.getMessage(WebSocketMethodsTask.class,
"TXT_AddWebScoketMethod", getDisplayName()); // NOI18N
}
private String getMethodName() {
return getMethodName(myMethodName, 0);
}
private String getMethodName(String name, int i) {
String suggestName = name;
if (i == 0) {
suggestName = name;
} else {
suggestName = name + i;
}
if (myExistedMethods.contains(suggestName)) {
return getMethodName(name, i + 1);
}
return suggestName;
}
private String getDisplayName() {
int index = myAnnotation.lastIndexOf('.');
return "@" + myAnnotation.substring(index + 1); // NOI18N
}
private final FileObject myFileObject;
private final ElementHandle<TypeElement> myHandle;
private final String myAnnotation;
private final String myMethodName;
private final Set<String> myExistedMethods;
}
private final AtomicReference<WebSocketTask> runTask = new AtomicReference<WebSocketTask>();
}