Micronaut: Source action to generate tests for endpoints added.
diff --git a/enterprise/micronaut/src/org/netbeans/modules/micronaut/db/MicronautDataEndpointGenerator.java b/enterprise/micronaut/src/org/netbeans/modules/micronaut/db/MicronautDataEndpointGenerator.java
index 6180f82..184fe69 100644
--- a/enterprise/micronaut/src/org/netbeans/modules/micronaut/db/MicronautDataEndpointGenerator.java
+++ b/enterprise/micronaut/src/org/netbeans/modules/micronaut/db/MicronautDataEndpointGenerator.java
@@ -59,16 +59,12 @@
import org.netbeans.api.java.source.CompilationInfo;
import org.netbeans.api.java.source.GeneratorUtilities;
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.java.source.TreeUtilities;
import org.netbeans.api.java.source.WorkingCopy;
import org.netbeans.api.lsp.CodeAction;
import org.netbeans.api.lsp.Command;
import org.netbeans.api.lsp.Range;
-import org.netbeans.api.lsp.TextDocumentEdit;
-import org.netbeans.api.lsp.TextEdit;
-import org.netbeans.api.lsp.WorkspaceEdit;
import org.netbeans.modules.parsing.api.ResultIterator;
import org.netbeans.modules.parsing.spi.ParseException;
import org.netbeans.spi.editor.codegen.CodeGenerator;
@@ -83,7 +79,6 @@
import org.openide.util.Lookup;
import org.openide.util.NbBundle;
import org.openide.util.RequestProcessor;
-import org.openide.util.Union2;
import org.openide.util.lookup.ServiceProviders;
import org.openide.util.lookup.ServiceProvider;
@@ -212,7 +207,7 @@
} else {
List<String> repositoryNames = Arrays.asList(gson.fromJson(data.get(REPOSITORIES), String[].class));
String controllerId = data.getAsJsonPrimitive(CONTROLLER_ID).getAsString();
- future.complete(modify2Edit(js, getTask(offset, repositoryNames, selected, controllerId)));
+ future.complete(Utils.modify2Edit(js, getTask(offset, repositoryNames, selected, controllerId)));
}
}
} catch (IOException ex) {
@@ -281,23 +276,6 @@
};
}
- private static WorkspaceEdit modify2Edit(JavaSource js, Task<WorkingCopy> task) throws IOException {
- FileObject[] file = new FileObject[1];
- ModificationResult changes = js.runModificationTask(wc -> {
- task.run(wc);
- file[0] = wc.getFileObject();
- });
- List<? extends ModificationResult.Difference> diffs = changes.getDifferences(file[0]);
- if (diffs != null) {
- List<TextEdit> edits = new ArrayList<>();
- for (ModificationResult.Difference diff : diffs) {
- edits.add(new TextEdit(diff.getStartPosition().getOffset(), diff.getEndPosition().getOffset(), diff.getNewText()));
- }
- return new WorkspaceEdit(Collections.singletonList(Union2.createFirst(new TextDocumentEdit(file[0].toURI().toString(), edits))));
- }
- return null;
- }
-
@NbBundle.Messages({
"LBL_GenerateButton=Generate...",
"LBL_CancelButton=Cancel",
diff --git a/enterprise/micronaut/src/org/netbeans/modules/micronaut/db/MicronautEndpointTestGenerator.java b/enterprise/micronaut/src/org/netbeans/modules/micronaut/db/MicronautEndpointTestGenerator.java
new file mode 100644
index 0000000..0c61b9d
--- /dev/null
+++ b/enterprise/micronaut/src/org/netbeans/modules/micronaut/db/MicronautEndpointTestGenerator.java
@@ -0,0 +1,585 @@
+/*
+ * 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.micronaut.db;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.sun.source.tree.AnnotationTree;
+import com.sun.source.tree.ClassTree;
+import com.sun.source.tree.MethodTree;
+import com.sun.source.tree.Tree;
+import com.sun.source.tree.VariableTree;
+import com.sun.source.util.TreePath;
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.Type;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import javax.lang.model.element.AnnotationMirror;
+import javax.lang.model.element.AnnotationValue;
+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.element.VariableElement;
+import javax.lang.model.type.DeclaredType;
+import javax.lang.model.type.TypeKind;
+import static javax.lang.model.type.TypeKind.BOOLEAN;
+import static javax.lang.model.type.TypeKind.BYTE;
+import static javax.lang.model.type.TypeKind.CHAR;
+import static javax.lang.model.type.TypeKind.DOUBLE;
+import static javax.lang.model.type.TypeKind.FLOAT;
+import static javax.lang.model.type.TypeKind.INT;
+import static javax.lang.model.type.TypeKind.LONG;
+import static javax.lang.model.type.TypeKind.SHORT;
+import javax.lang.model.type.TypeMirror;
+import javax.lang.model.type.TypeVariable;
+import javax.lang.model.util.ElementFilter;
+import javax.swing.text.Document;
+import org.netbeans.api.java.classpath.ClassPath;
+import org.netbeans.api.java.queries.UnitTestForSourceQuery;
+import org.netbeans.api.java.source.ClasspathInfo;
+import org.netbeans.api.java.source.CompilationController;
+import org.netbeans.api.java.source.CompilationInfo;
+import org.netbeans.api.java.source.GeneratorUtilities;
+import org.netbeans.api.java.source.JavaSource;
+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.lsp.CodeAction;
+import org.netbeans.api.lsp.Command;
+import org.netbeans.api.lsp.Range;
+import org.netbeans.api.project.FileOwnerQuery;
+import org.netbeans.api.project.Project;
+import org.netbeans.api.project.ProjectUtils;
+import org.netbeans.modules.j2ee.core.api.support.java.GenerationUtils;
+import org.netbeans.modules.micronaut.symbol.MicronautSymbolFinder;
+import static org.netbeans.modules.micronaut.symbol.MicronautSymbolFinder.getEndpointMethod;
+import org.netbeans.modules.parsing.api.ResultIterator;
+import org.netbeans.modules.parsing.spi.ParseException;
+import org.netbeans.spi.lsp.CodeActionProvider;
+import org.netbeans.spi.lsp.CommandProvider;
+import org.openide.DialogDescriptor;
+import org.openide.DialogDisplayer;
+import org.openide.NotifyDescriptor;
+import org.openide.filesystems.FileObject;
+import org.openide.filesystems.URLMapper;
+import org.openide.util.BaseUtilities;
+import org.openide.util.Exceptions;
+import org.openide.util.Lookup;
+import org.openide.util.NbBundle;
+import org.openide.util.RequestProcessor;
+import org.openide.util.lookup.ServiceProviders;
+import org.openide.util.lookup.ServiceProvider;
+
+/**
+ *
+ * @author Dusan Balek
+ */
+@ServiceProviders({
+ @ServiceProvider(service = CodeActionProvider.class),
+ @ServiceProvider(service = CommandProvider.class)
+})
+public class MicronautEndpointTestGenerator implements CodeActionProvider, CommandProvider {
+
+ private static final String SOURCE = "source";
+ private static final String GENERATE_MICRONAUT_ENDPOINT_TEST = "nbls.micronaut.generate.endpoint.test";
+ private static final String CONTROLLER_ANNOTATION_NAME = "io.micronaut.http.annotation.Controller";
+ private static final String MICRONAUT_TEST_ANNOTATION_NAME = "io.micronaut.test.extensions.junit5.annotation.MicronautTest";
+ private static final String MICRONAUT_HTTP_CLIENT_ANNOTATION_NAME = "io.micronaut.http.client.annotation.Client";
+ private static final String MICRONAUT_SERDEABLE_ANNOTATION_NAME = "io.micronaut.serde.annotation.Serdeable";
+ private static final String MICRONAUT_BODY_ANNOTATION_NAME = "io.micronaut.http.annotation.Body";
+ private static final String JAKARTA_INJECT_NAME = "jakarta.inject.Inject";
+ private static final String JUPITER_ASSERTIONS_NAME = "org.junit.jupiter.api.Assertions";
+ private static final String JUPITER_TEST_ANNOTATION_NAME = "org.junit.jupiter.api.Test";
+ private static final String MICRONAUT_HTTP_CLIENT_NAME = "io.micronaut.http.client.HttpClient";
+ private static final String MICRONAUT_HTTP_REQUEST_NAME = "io.micronaut.http.HttpRequest";
+ private static final String MICRONAUT_HTTP_RESPONSE_NAME = "io.micronaut.http.HttpResponse";
+ private static final String MICRONAUT_HTTP_STATUS_NAME = "io.micronaut.http.HttpStatus";
+ private static final String MICRONAUT_ARGUMENT_NAME = "io.micronaut.core.type.Argument";
+ private static final String LIST_TYPE_NAME = "java.util.List";
+ private static final String MAP_TYPE_NAME = "java.util.Map";
+ private static final String OPTIONAL_TYPE_NAME = "java.util.Optional";
+ private static final String ORIGIN = "origin";
+ private static final String FILE_NAME = "fileName";
+ private static final String LOCATIONS = "locations";
+ private static final String PUT = "PUT";
+ private static final String POST = "POST";
+ private static final String DELETE = "DELETE";
+ private static final String EMPTY = "";
+
+ private final Gson gson = new GsonBuilder().registerTypeAdapter(NotifyDescriptor.QuickPick.Item.class, (JsonDeserializer<NotifyDescriptor.QuickPick.Item>) (JsonElement json, Type type, JsonDeserializationContext jdc) -> {
+ String label = json.getAsJsonObject().get("label").getAsString();
+ String description = json.getAsJsonObject().get("description").getAsString();
+ return new NotifyDescriptor.QuickPick.Item(label, description);
+ }).create();
+
+ @Override
+ @NbBundle.Messages({
+ "DN_GenerateEndpointTest=Generate Micronaut Endpoint Tests...",
+ "DN_SelectTargetLocation=Select target location"
+ })
+ public List<CodeAction> getCodeActions(Document doc, Range range, Lookup context) {
+ try {
+ List<String> only = context.lookup(List.class);
+ if (only == null || !only.contains(SOURCE)) {
+ return Collections.emptyList();
+ }
+ ResultIterator resultIterator = context.lookup(ResultIterator.class);
+ CompilationController cc = resultIterator != null && resultIterator.getParserResult() != null ? CompilationController.get(resultIterator.getParserResult()) : null;
+ if (cc == null) {
+ return Collections.emptyList();
+ }
+ ClassPath cp = cc.getClasspathInfo().getClassPath(ClasspathInfo.PathKind.SOURCE);
+ FileObject fileObject = cc.getFileObject();
+ if (!fileObject.isValid()) {
+ return Collections.emptyList();
+ }
+ FileObject root = cp.findOwnerRoot(fileObject);
+ if (root == null) {
+ return Collections.emptyList();
+ }
+ cc.toPhase(JavaSource.Phase.ELEMENTS_RESOLVED);
+ int offset = range.getStartOffset();
+ TreePath path = cc.getTreeUtilities().pathFor(offset);
+ path = cc.getTreeUtilities().getPathElementOfKind(TreeUtilities.CLASS_TREE_KINDS, path);
+ if (path == null) {
+ return Collections.emptyList();
+ }
+ TypeElement te = (TypeElement) cc.getTrees().getElement(path);
+ if (te == null || !te.getKind().isClass()) {
+ return Collections.emptyList();
+ }
+ AnnotationMirror controllerAnn = Utils.getAnnotation(te.getAnnotationMirrors(), CONTROLLER_ANNOTATION_NAME);
+ if (controllerAnn == null) {
+ return Collections.emptyList();
+ }
+ URL[] locations = getMicronautTestLocations(root);
+ if (locations == null || locations.length == 0) {
+ return Collections.emptyList();
+ }
+ String name = cp.getResourceName(fileObject, '/', false) + "MicronautTest.java";
+ long cnt = Arrays.stream(locations).filter(location -> {
+ FileObject fo = URLMapper.findFileObject(location);
+ return fo != null && fo.getFileObject(name) != null;
+ }).count();
+ if (cnt == 0) {
+ Map<String, Object> data = new HashMap<>();
+ data.put(ORIGIN, te.getQualifiedName().toString());
+ data.put(FILE_NAME, name);
+ data.put(LOCATIONS, Arrays.stream(locations).map(location -> new NotifyDescriptor.QuickPick.Item(location.toString(), EMPTY)).collect(Collectors.toList()));
+ return Collections.singletonList(new CodeAction(Bundle.DN_GenerateEndpointTest(), SOURCE, new Command(Bundle.DN_GenerateEndpointTest(), "nbls.generate.code", Arrays.asList(GENERATE_MICRONAUT_ENDPOINT_TEST, data)), null));
+ }
+ } catch (IOException | ParseException ex) {
+ Exceptions.printStackTrace(ex);
+ }
+ return Collections.emptyList();
+ }
+
+ @Override
+ public Set<String> getCommands() {
+ return Collections.singleton(GENERATE_MICRONAUT_ENDPOINT_TEST);
+ }
+
+ @Override
+ public CompletableFuture<Object> runCommand(String command, List<Object> arguments) {
+ if (arguments.isEmpty()) {
+ return CompletableFuture.completedFuture(null);
+ }
+ JsonObject data = (JsonObject) arguments.get(0);
+ CompletableFuture<Object> future = new CompletableFuture<>();
+ RequestProcessor.getDefault().post(() -> {
+ String origin = data.getAsJsonPrimitive(ORIGIN).getAsString();
+ String name = data.getAsJsonPrimitive(FILE_NAME).getAsString();
+ List<NotifyDescriptor.QuickPick.Item> items = Arrays.asList(gson.fromJson(data.get(LOCATIONS), NotifyDescriptor.QuickPick.Item[].class));
+ if (items.size() == 1) {
+ future.complete(generate(items.get(0).getLabel(), name, origin));
+ } else {
+ NotifyDescriptor.QuickPick pick = new NotifyDescriptor.QuickPick(Bundle.DN_GenerateEndpointTest(), Bundle.DN_SelectTargetLocation(), items, false);
+ if (DialogDescriptor.OK_OPTION != DialogDisplayer.getDefault().notify(pick)) {
+ future.complete(null);
+ } else {
+ future.complete(null);
+ List<NotifyDescriptor.QuickPick.Item> selected = pick.getItems().stream().filter(item -> item.isSelected()).collect(Collectors.toList());
+ if (selected.isEmpty()) {
+ future.complete(null);
+ } else {
+ future.complete(generate(selected.get(0).getLabel(), name, origin));
+ }
+ }
+ }
+ });
+ return future;
+ }
+
+ private URL[] getMicronautTestLocations(FileObject root) {
+ Project p = FileOwnerQuery.getOwner(root);
+ if (p != null) {
+ Project parent = FileOwnerQuery.getOwner(p.getProjectDirectory().getParent());
+ if (parent != null) {
+ Set<Project> containedProjects = ProjectUtils.getContainedProjects(parent, false);
+ if (containedProjects.contains(p)) {
+ List<URL> urls = new ArrayList<>();
+ for (Project cp : containedProjects) {
+ if (cp != p && ProjectUtils.getDependencyProjects(cp, false).contains(p)) {
+ urls.addAll(Arrays.asList(UnitTestForSourceQuery.findUnitTests(cp.getProjectDirectory())));
+ }
+ }
+ if (!urls.isEmpty()) {
+ return urls.toArray(new URL[0]);
+ }
+ }
+ }
+ }
+ return UnitTestForSourceQuery.findUnitTests(root);
+ }
+
+ @NbBundle.Messages({
+ "MSG_MicronautEndpointTestClass=Micronaut endpoint test class {0}\n"
+ })
+ private static Object generate(String location, String fqn, String origin) {
+ try {
+ java.net.URI locationURI = java.net.URI.create(location);
+ int idx = fqn.lastIndexOf('/');
+ String folderName = idx < 0 ? EMPTY : fqn.substring(0, idx);
+ locationURI = locationURI.resolve(folderName);
+ File file = BaseUtilities.toFile(locationURI);
+ if (file != null && !file.exists()) {
+ file.mkdirs();
+ }
+ FileObject folder = URLMapper.findFileObject(locationURI.toURL());
+ if (folder == null) {
+ return null;
+ }
+ String fileName = idx < 0 ? fqn : fqn.substring(idx + 1);
+ FileObject fo = GenerationUtils.createClass(folder, fileName.substring(0, fileName.length() - 5), Bundle.MSG_MicronautEndpointTestClass(fileName));
+ if (fo != null) {
+ JavaSource js = JavaSource.forFileObject(fo);
+ if (js != null) {
+ return Utils.modify2Edit(js, copy -> {
+ copy.toPhase(JavaSource.Phase.RESOLVED);
+ Tree origTree = copy.getCompilationUnit().getTypeDecls().get(0);
+ if (origTree.getKind() == Tree.Kind.CLASS) {
+ GenerationUtils gu = GenerationUtils.newInstance(copy);
+ ClassTree cls = gu.addAnnotation((ClassTree) origTree, gu.createAnnotation(MICRONAUT_TEST_ANNOTATION_NAME));
+ List<Tree> members = new ArrayList<>();
+ members.add(createClientField(copy));
+ TypeElement te = copy.getElements().getTypeElement(origin);
+ if (te != null) {
+ AnnotationMirror controllerAnn = Utils.getAnnotation(te.getAnnotationMirrors(), CONTROLLER_ANNOTATION_NAME);
+ String path = "";
+ if (controllerAnn != null) {
+ for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry : controllerAnn.getElementValues().entrySet()) {
+ if ("value".contentEquals(entry.getKey().getSimpleName())) {
+ path = (String) entry.getValue().getValue();
+ }
+ }
+ }
+ Set<Element> toImport = new HashSet<>();
+ for (ExecutableElement ee : ElementFilter.methodsIn(te.getEnclosedElements())) {
+ MicronautSymbolFinder.MthIterator it = new MicronautSymbolFinder.MthIterator(ee, copy.getElements(), copy.getTypes());
+ while(it.hasNext()) {
+ ExecutableElement mth = it.next();
+ for (AnnotationMirror ann : mth.getAnnotationMirrors()) {
+ String method = getEndpointMethod((TypeElement) ann.getAnnotationType().asElement());
+ if (method != null) {
+ List<String> ids = new ArrayList<>();
+ Map<? extends ExecutableElement, ? extends AnnotationValue> values = ann.getElementValues();
+ if (values.isEmpty()) {
+ ids.add("/");
+ } else {
+ for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry : values.entrySet()) {
+ if ("value".contentEquals(entry.getKey().getSimpleName()) || "uri".contentEquals(entry.getKey().getSimpleName())) {
+ ids.add((String) entry.getValue().getValue());
+ } else if ("uris".contentEquals(entry.getKey().getSimpleName())) {
+ for (AnnotationValue av : (List<AnnotationValue>) entry.getValue().getValue()) {
+ ids.add((String) av.getValue());
+ }
+ }
+ }
+ }
+ if (!ids.isEmpty()) {
+ members.add(createTestMethodFor(copy, ee, method, path, ids, toImport));
+ }
+ }
+ }
+ }
+ }
+ copy.rewrite(copy.getCompilationUnit(), GeneratorUtilities.get(copy).addImports(copy.getCompilationUnit(), toImport));
+ }
+ copy.rewrite(origTree, GeneratorUtilities.get(copy).insertClassMembers(cls, members));
+ }
+ });
+ }
+ }
+ } catch (IOException | IllegalArgumentException ex) {
+ Exceptions.printStackTrace(ex);
+ }
+ return null;
+ }
+
+ private static VariableTree createClientField(WorkingCopy copy) {
+ GenerationUtils gu = GenerationUtils.newInstance(copy);
+ TreeMaker tm = copy.getTreeMaker();
+ List<? extends AnnotationTree> anns = List.of(gu.createAnnotation(JAKARTA_INJECT_NAME),
+ gu.createAnnotation(MICRONAUT_HTTP_CLIENT_ANNOTATION_NAME, List.of(tm.Literal("/")))
+ );
+ return tm.Variable(tm.Modifiers(Set.of(Modifier.PRIVATE), anns), "client", tm.Type(MICRONAUT_HTTP_CLIENT_NAME), null);
+ }
+
+ private static MethodTree createTestMethodFor(WorkingCopy copy, ExecutableElement ee, String method, String path, List<String> ids, Set<Element> toImport) {
+ TreeMaker tm = copy.getTreeMaker();
+ toImport.add(copy.getElements().getTypeElement(MICRONAUT_HTTP_REQUEST_NAME));
+ Set<String> assertionMethods = new HashSet<>();
+ String[] info = getInfo(copy, ee, toImport);
+ Map<String, VariableElement> bodyParams = getBodyParams(ee);
+ List<? extends AnnotationTree> anns = List.of(GenerationUtils.newInstance(copy).createAnnotation(JUPITER_TEST_ANNOTATION_NAME));
+ Map<String, String> usedPathNames = new LinkedHashMap<>();
+ Set<String> usedBodyNames = new HashSet<>();
+ StringBuilder body = new StringBuilder("{");
+ for (int i = 0; i < ids.size(); i++) {
+ String id = ids.get(i);
+ Map<String, TypeMirror> pathParams = new LinkedHashMap<>();
+ String processedPath = processPathParams(ee, path + id, pathParams);
+ for (Map.Entry<String, TypeMirror> entry : pathParams.entrySet()) {
+ String name = entry.getKey();
+ TypeMirror value = entry.getValue();
+ if (usedPathNames.put(name, name) == null) {
+ if (value != null) {
+ if (value.getKind() == TypeKind.DECLARED) {
+ toImport.add(((DeclaredType) value).asElement());
+ }
+ body.append(typeName(copy, entry.getValue())).append(' ');
+ } else {
+ body.append("Object ");
+ }
+ }
+ body.append(name).append(" = ").append(defaultValue(value)).append(";\n");
+ }
+ StringBuilder sb = new StringBuilder();
+ if (bodyParams != null) {
+ for (Map.Entry<String, VariableElement> entry : bodyParams.entrySet()) {
+ String name = entry.getKey();
+ VariableElement value = entry.getValue();
+ String simpleName = value.getSimpleName().toString();
+ if (EMPTY.equals(name)) {
+ sb.append(',').append(simpleName);
+ } else {
+ if (sb.isEmpty()) {
+ sb.append(",Map.of(");
+ } else {
+ sb.append(',');
+ }
+ sb.append('"').append(name).append("\",").append(simpleName);
+ }
+ if (usedBodyNames.add(simpleName)) {
+ body.append(typeName(copy, value.asType())).append(' ');
+ }
+ body.append(simpleName).append(" = ").append(defaultValue(value.asType())).append(";\n");
+ }
+ if (sb.isEmpty()) {
+ sb.append(",null");
+ } else {
+ toImport.add(copy.getElements().getTypeElement(MAP_TYPE_NAME));
+ }
+ }
+ if (i == 0) {
+ body.append(info[1]).append(' ');
+ }
+ body.append(info[2]).append("=client.toBlocking().").append(info[0]).append('(')
+ .append("HttpRequest.").append(method).append('(').append(processedPath).append(sb).append(')').append(info[3]).append(");");
+ if (info[1].startsWith("List<") || info[1].startsWith("Optional<")) {
+ body.append("assertFalse(").append(info[2]).append(".isEmpty());\n");
+ assertionMethods.add("assertFalse");
+ } else if (info[1].startsWith("HttpResponse<")) {
+ body.append("assertEquals(HttpStatus.");
+ switch (method) {
+ case POST:
+ body.append("CREATED");
+ break;
+ case PUT:
+ case DELETE:
+ body.append("NO_CONTENT");
+ break;
+ default:
+ body.append("OK");
+ }
+ body.append(',').append(info[2]).append(".getStatus());\n");
+ assertionMethods.add("assertEquals");
+ toImport.add(copy.getElements().getTypeElement(MICRONAUT_HTTP_STATUS_NAME));
+ } else {
+ body.append("assertNotNull(").append(info[2]).append(");\n");
+ assertionMethods.add("assertNotNull");
+ }
+ }
+ body.append("\n// TODO review the generated test code and remove the default call to fail.\n");
+ body.append("fail(\"The test case is a prototype.\");");
+ assertionMethods.add("fail");
+ TypeElement ae = copy.getElements().getTypeElement(JUPITER_ASSERTIONS_NAME);
+ for (ExecutableElement mth : ElementFilter.methodsIn(ae.getEnclosedElements())) {
+ if (assertionMethods.remove(mth.getSimpleName().toString())) {
+ toImport.add(mth);
+ }
+ }
+ body.append('}');
+ String joined = usedPathNames.keySet().stream().map(name -> varName(name, true)).collect(Collectors.joining("And"));
+ String methodName = joined.isEmpty() ? ee.getSimpleName().toString() : ee.getSimpleName().toString() + "By" + joined;
+ return tm.Method(tm.Modifiers(Set.of(Modifier.PUBLIC), anns), methodName, tm.Type(copy.getTypes().getNoType(TypeKind.VOID)), List.of(), List.of(), List.of(), body.toString(), null);
+ }
+
+ private static Map<String, VariableElement> getBodyParams(ExecutableElement ee) {
+ Map<String, VariableElement> bodyParams = new LinkedHashMap<>();
+ for (VariableElement ve : ee.getParameters()) {
+ AnnotationMirror am = Utils.getAnnotation(ve.getAnnotationMirrors(), MICRONAUT_BODY_ANNOTATION_NAME);
+ if (am != null) {
+ String value = EMPTY;
+ for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry : am.getElementValues().entrySet()) {
+ if ("value".contentEquals(entry.getKey().getSimpleName())) {
+ value = (String) entry.getValue().getValue();
+ }
+ }
+ if (value.isEmpty()) {
+ return Collections.singletonMap(EMPTY, ve);
+ } else {
+ bodyParams.put(value, ve);
+ }
+ }
+ }
+ return bodyParams.isEmpty() ? null : bodyParams;
+ }
+
+ private static String processPathParams(ExecutableElement ee, String path, Map<String, TypeMirror> pathParams) {
+ Matcher matcher = Pattern.compile("\\{(.*)}").matcher(path);
+ StringBuilder sb = new StringBuilder("\"");
+ int idx = 0;
+ Stream<? extends VariableElement> parameters = ee.getParameters().stream();
+ while (matcher.find(idx)) {
+ String name = matcher.group(1);
+ if (!pathParams.containsKey(name)) {
+ Optional<? extends VariableElement> param = parameters.filter(p -> name.contentEquals(p.getSimpleName())).findFirst();
+ pathParams.put(name, param.isPresent() ? param.get().asType() : null);
+ }
+ sb.append(path.substring(idx, matcher.start())).append("\"+").append(name);
+ idx = matcher.end();
+ if (idx < path.length()) {
+ sb.append("+\"");
+ }
+ }
+ if (idx < path.length()) {
+ sb.append(path.substring(idx)).append('"');
+ }
+ return sb.toString();
+ }
+
+ private static String[] getInfo(CompilationInfo info, ExecutableElement ee, Set<Element> toImport) {
+ TypeMirror type = ee.getReturnType();
+ if (type.getKind() == TypeKind.DECLARED) {
+ DeclaredType declType = (DeclaredType) type;
+ TypeElement te = (TypeElement) declType.asElement();
+ switch (te.getQualifiedName().toString()) {
+ case LIST_TYPE_NAME:
+ TypeMirror typeArg = declType.getTypeArguments().get(0);
+ if (typeArg.getKind() == TypeKind.DECLARED) {
+ TypeElement tae = (TypeElement) ((DeclaredType) typeArg).asElement();
+ if (Utils.getAnnotation(tae.getAnnotationMirrors(), MICRONAUT_SERDEABLE_ANNOTATION_NAME) != null) {
+ toImport.add(info.getElements().getTypeElement(LIST_TYPE_NAME));
+ toImport.add(info.getElements().getTypeElement(MICRONAUT_ARGUMENT_NAME));
+ toImport.add(tae);
+ String typeName = typeName(info, type);
+ String typeArgName = typeName(info, typeArg);
+ return new String[] {"retrieve", typeName, "list", ",Argument.listOf(" + typeArgName + ".class)"};
+ }
+ }
+ break;
+ case OPTIONAL_TYPE_NAME:
+ typeArg = declType.getTypeArguments().get(0);
+ if (typeArg.getKind() == TypeKind.DECLARED) {
+ TypeElement tae = (TypeElement) ((DeclaredType) typeArg).asElement();
+ if (Utils.getAnnotation(tae.getAnnotationMirrors(), MICRONAUT_SERDEABLE_ANNOTATION_NAME) != null) {
+ toImport.add(info.getElements().getTypeElement(OPTIONAL_TYPE_NAME));
+ toImport.add(info.getElements().getTypeElement(MICRONAUT_ARGUMENT_NAME));
+ toImport.add(tae);
+ String typeName = typeName(info, type);
+ String typeArgName = typeName(info, typeArg);
+ return new String[] {"retrieve", typeName, varName(typeArgName, false), ",Argument.optionalOf(" + typeArgName + ".class)"};
+ }
+ }
+ break;
+ default:
+ if (Utils.getAnnotation(te.getAnnotationMirrors(), MICRONAUT_SERDEABLE_ANNOTATION_NAME) != null) {
+ toImport.add(te);
+ String typeName = typeName(info, type);
+ return new String[] {"retrieve", typeName, varName(typeName, false), "," + typeName + ".class"};
+ }
+ }
+ }
+ toImport.add(info.getElements().getTypeElement(MICRONAUT_HTTP_RESPONSE_NAME));
+ return new String[] {"exchange", "HttpResponse<?>", "response", EMPTY};
+ }
+
+ private static String typeName(CompilationInfo info, TypeMirror tm) {
+ if (tm.getKind() == TypeKind.TYPEVAR) {
+ tm = ((TypeVariable) tm).getUpperBound();
+ }
+ return Utils.getTypeName(info, tm, false, false).toString();
+ }
+
+ private static String varName(String varTypeName, boolean upperCase) {
+ StringBuilder sb = new StringBuilder(varTypeName);
+ char firstChar = sb.charAt(0);
+ sb.setCharAt(0, upperCase ? Character.toUpperCase(firstChar) : Character.toLowerCase(firstChar));
+ return sb.toString();
+ }
+
+ private static String defaultValue(TypeMirror tm) {
+ if (tm != null) {
+ switch(tm.getKind()) {
+ case BOOLEAN:
+ return "false";
+ case BYTE:
+ case CHAR:
+ case DOUBLE:
+ case FLOAT:
+ case INT:
+ case LONG:
+ case SHORT:
+ return "0";
+ }
+ }
+ return "null";
+ }
+}
diff --git a/enterprise/micronaut/src/org/netbeans/modules/micronaut/db/Utils.java b/enterprise/micronaut/src/org/netbeans/modules/micronaut/db/Utils.java
index bead572..84fdae4 100644
--- a/enterprise/micronaut/src/org/netbeans/modules/micronaut/db/Utils.java
+++ b/enterprise/micronaut/src/org/netbeans/modules/micronaut/db/Utils.java
@@ -24,6 +24,7 @@
import com.sun.source.tree.ModifiersTree;
import com.sun.source.tree.TypeParameterTree;
import com.sun.source.tree.VariableTree;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
@@ -60,11 +61,19 @@
import org.netbeans.api.java.lexer.JavaTokenId;
import org.netbeans.api.java.source.CompilationInfo;
import org.netbeans.api.java.source.GeneratorUtilities;
+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.java.source.TreeMaker;
import org.netbeans.api.java.source.TypeUtilities;
import org.netbeans.api.java.source.WorkingCopy;
+import org.netbeans.api.lsp.TextDocumentEdit;
+import org.netbeans.api.lsp.TextEdit;
+import org.netbeans.api.lsp.WorkspaceEdit;
import org.netbeans.api.project.SourceGroup;
import org.netbeans.modules.j2ee.core.api.support.java.GenerationUtils;
+import org.openide.filesystems.FileObject;
+import org.openide.util.Union2;
import org.openide.util.WeakListeners;
/**
@@ -349,6 +358,23 @@
return info.getTypeUtilities().getTypeName(type, options.toArray(new TypeUtilities.TypeNameOptions[0]));
}
+ public static WorkspaceEdit modify2Edit(JavaSource js, Task<WorkingCopy> task) throws IOException {
+ FileObject[] file = new FileObject[1];
+ ModificationResult changes = js.runModificationTask(wc -> {
+ task.run(wc);
+ file[0] = wc.getFileObject();
+ });
+ List<? extends ModificationResult.Difference> diffs = changes.getDifferences(file[0]);
+ if (diffs != null) {
+ List<TextEdit> edits = new ArrayList<>();
+ for (ModificationResult.Difference diff : diffs) {
+ edits.add(new TextEdit(diff.getStartPosition().getOffset(), diff.getEndPosition().getOffset(), diff.getNewText()));
+ }
+ return new WorkspaceEdit(Collections.singletonList(Union2.createFirst(new TextDocumentEdit(file[0].toURI().toString(), edits))));
+ }
+ return null;
+ }
+
private static ExecutableElement getEndpointMethodFor(CompilationInfo info, List<ExecutableElement> methods, DeclaredType repositoryType, ExecutableElement delegateMethod, String id) {
String delegateMethodName = delegateMethod.getSimpleName().toString();
String annotationName = getControllerDataEndpointAnnotationTypeName(delegateMethodName);
diff --git a/enterprise/micronaut/src/org/netbeans/modules/micronaut/symbol/MicronautSymbolFinder.java b/enterprise/micronaut/src/org/netbeans/modules/micronaut/symbol/MicronautSymbolFinder.java
index d32f5d1..9dca8ab 100644
--- a/enterprise/micronaut/src/org/netbeans/modules/micronaut/symbol/MicronautSymbolFinder.java
+++ b/enterprise/micronaut/src/org/netbeans/modules/micronaut/symbol/MicronautSymbolFinder.java
@@ -256,7 +256,7 @@
return null;
}
- private static String getEndpointMethod(TypeElement te) {
+ public static String getEndpointMethod(TypeElement te) {
for (AnnotationMirror ann : te.getAnnotationMirrors()) {
Element el = ann.getAnnotationType().asElement();
if ("io.micronaut.http.annotation.HttpMethodMapping".contentEquals(((TypeElement) el).getQualifiedName())) {
@@ -345,7 +345,7 @@
}
}
- private static class MthIterator implements Iterator<ExecutableElement> {
+ public static class MthIterator implements Iterator<ExecutableElement> {
private final ExecutableElement ee;
private final Elements elements;
@@ -353,7 +353,7 @@
private boolean createIt = false;
private Iterator<ExecutableElement> it = null;
- private MthIterator(Element e, Elements elements, Types types) {
+ public MthIterator(Element e, Elements elements, Types types) {
this.ee = e != null && e.getKind() == ElementKind.METHOD ? (ExecutableElement) e : null;
this.elements = elements;
this.types = types;
diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/TestClassGenerator.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/TestClassGenerator.java
index 68a4802..9f76eb4 100644
--- a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/TestClassGenerator.java
+++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/TestClassGenerator.java
@@ -104,7 +104,7 @@
if (info == null) {
return Collections.emptyList();
}
- info.toPhase(JavaSource.Phase.RESOLVED);
+ info.toPhase(JavaSource.Phase.PARSED);
int offset = getOffset(info, params.getRange().getStart());
TreePath tp = info.getTreeUtilities().pathFor(offset);
if (!TreeUtilities.CLASS_TREE_KINDS.contains(tp.getLeaf().getKind())) {
diff --git a/java/java.lsp.server/vscode/src/extension.ts b/java/java.lsp.server/vscode/src/extension.ts
index 1e78564..fa61aef 100644
--- a/java/java.lsp.server/vscode/src/extension.ts
+++ b/java/java.lsp.server/vscode/src/extension.ts
@@ -612,6 +612,10 @@
if (edit) {
const wsEdit = await (await client).protocol2CodeConverter.asWorkspaceEdit(edit as ls.WorkspaceEdit);
await workspace.applyEdit(wsEdit);
+ for (const entry of wsEdit.entries()) {
+ const file = vscode.Uri.parse(entry[0].fsPath);
+ await vscode.window.showTextDocument(file, { preview: false });
+ }
await commands.executeCommand('workbench.action.focusActiveEditorGroup');
}
}));