| /* |
| * 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) |
| && !Profile.JAKARTA_EE_10_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>(); |
| } |