blob: 68a48021daba83c054f3bdd24f517c2567722542 [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.java.lsp.server.protocol;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSyntaxException;
import com.sun.source.util.TreePath;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import org.eclipse.lsp4j.CodeAction;
import org.eclipse.lsp4j.CodeActionKind;
import org.eclipse.lsp4j.CodeActionParams;
import org.eclipse.lsp4j.ShowDocumentParams;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.netbeans.api.java.classpath.ClassPath;
import org.netbeans.api.java.project.JavaProjectConstants;
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.JavaSource;
import org.netbeans.api.java.source.TreeUtilities;
import org.netbeans.api.project.FileOwnerQuery;
import org.netbeans.api.project.Project;
import org.netbeans.api.project.ProjectUtils;
import org.netbeans.api.project.SourceGroup;
import org.netbeans.modules.gsf.testrunner.api.TestCreatorProvider;
import org.netbeans.modules.gsf.testrunner.plugin.CommonTestUtilProvider;
import org.netbeans.modules.gsf.testrunner.plugin.GuiUtilsProvider;
import org.netbeans.modules.java.lsp.server.URITranslator;
import org.netbeans.modules.java.lsp.server.Utils;
import org.netbeans.modules.java.lsp.server.input.InputBoxStep;
import org.netbeans.modules.java.lsp.server.input.InputService;
import org.netbeans.modules.java.lsp.server.input.QuickPickItem;
import org.netbeans.modules.java.lsp.server.input.QuickPickStep;
import org.netbeans.modules.java.lsp.server.input.ShowMutliStepInputParams;
import org.netbeans.modules.parsing.api.ResultIterator;
import org.openide.filesystems.FileChangeAdapter;
import org.openide.filesystems.FileChangeListener;
import org.openide.filesystems.FileEvent;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.util.BaseUtilities;
import org.openide.util.Lookup;
import org.openide.util.NbBundle;
import org.openide.util.RequestProcessor;
import org.openide.util.lookup.ServiceProvider;
/**
*
* @author Dusan Balek
*/
@ServiceProvider(service = CodeActionsProvider.class, position = 9)
public final class TestClassGenerator extends CodeActionsProvider {
private static final String GENERATE_TEST_CLASS_COMMAND = "nbls.java.generate.testClass";
private static final String FRAMEWORKS = "frameworks";
private static final String CLASS_NAME = "className";
private final Gson gson = new Gson();
@Override
@NbBundle.Messages({
"DN_GenerateTestClass=Generate Tests..."
})
public List<CodeAction> getCodeActions(NbCodeLanguageClient client, ResultIterator resultIterator, CodeActionParams params) throws Exception {
List<String> only = params.getContext().getOnly();
if (only == null || !only.contains(CodeActionKind.Source)) {
return Collections.emptyList();
}
CompilationController info = resultIterator.getParserResult() != null ? CompilationController.get(resultIterator.getParserResult()) : null;
if (info == null) {
return Collections.emptyList();
}
info.toPhase(JavaSource.Phase.RESOLVED);
int offset = getOffset(info, params.getRange().getStart());
TreePath tp = info.getTreeUtilities().pathFor(offset);
if (!TreeUtilities.CLASS_TREE_KINDS.contains(tp.getLeaf().getKind())) {
return Collections.emptyList();
}
ClassPath cp = info.getClasspathInfo().getClassPath(ClasspathInfo.PathKind.SOURCE);
FileObject fileObject = info.getFileObject();
if (!fileObject.isValid()) {
return Collections.emptyList();
}
FileObject root = cp.findOwnerRoot(fileObject);
if (root == null) {
return Collections.emptyList();
}
Map<Object, List<String>> validCombinations = getValidCombinations(info);
if (validCombinations == null || validCombinations.isEmpty()) {
return Collections.emptyList();
}
List<CodeAction> result = new ArrayList<>();
for (Map.Entry<Object, List<String>> entrySet : validCombinations.entrySet()) {
Object location = entrySet.getKey();
List<QuickPickItem> testingFrameworks = entrySet.getValue().stream().map(framework -> new QuickPickItem(framework)).collect(Collectors.toList());
result.add((createCodeAction(client, Bundle.DN_GenerateTestClass(), CodeActionKind.Source, null, GENERATE_TEST_CLASS_COMMAND, Utils.toUri(fileObject), getTargetFolderUri(location), testingFrameworks)));
}
return result;
}
@Override
public Set<String> getCommands() {
return Collections.singleton(GENERATE_TEST_CLASS_COMMAND);
}
@Override
@NbBundle.Messages({
"DN_SelectFramework=Select a test framework to use",
"DN_ProvideClassName=Please type the target test class name"
})
public CompletableFuture<Object> processCommand(NbCodeLanguageClient client, String command, List<Object> arguments) {
CompletableFuture<Object> future = new CompletableFuture<>();
try {
if (arguments.size() > 2) {
String uri = ((JsonPrimitive) arguments.get(0)).getAsString();
FileObject fileObject = Utils.fromUri(uri);
if (fileObject == null) {
throw new IllegalArgumentException(String.format("Cannot resolve source file from uri: %s", uri));
}
String targetUri = ((JsonPrimitive) arguments.get(1)).getAsString();
FileObject targetFolder = getTargetFolder(targetUri);
if (targetFolder == null) {
throw new IllegalArgumentException(String.format("Cannot resolve target folder from uri: %s", targetUri));
}
List<QuickPickItem> testingFrameworks = Arrays.asList(gson.fromJson((JsonArray)arguments.get(2), QuickPickItem[].class));
InputService.Registry inputServiceRegistry = Lookup.getDefault().lookup(InputService.Registry.class);
if (inputServiceRegistry != null) {
int totalSteps = testingFrameworks.size() > 1 ? 2 : 1;
String inputId = inputServiceRegistry.registerInput(params -> {
CompletableFuture<Either<QuickPickStep, InputBoxStep>> f = new CompletableFuture<>();
if (params.getStep() < totalSteps) {
Either<List<QuickPickItem>,String> frameworkData = params.getData().get(FRAMEWORKS);
if (frameworkData != null) {
List<QuickPickItem> selectedFrameworks = frameworkData.getLeft();
for (QuickPickItem testingFramework : testingFrameworks) {
testingFramework.setPicked(selectedFrameworks.contains(testingFramework));
}
}
f.complete(Either.forLeft(new QuickPickStep(totalSteps, FRAMEWORKS, Bundle.DN_SelectFramework(), testingFrameworks)));
} else if (params.getStep() == totalSteps) {
Either<List<QuickPickItem>,String> frameworkData = params.getData().get(FRAMEWORKS);
QuickPickItem selectedFramework = (frameworkData != null ? frameworkData.getLeft() : testingFrameworks).get(0);
f.complete(Either.forRight(new InputBoxStep(totalSteps, CLASS_NAME, Bundle.DN_ProvideClassName(), getPreffiledName(fileObject, selectedFramework.getLabel()))));
} else {
f.complete(null);
}
return f;
});
client.showMultiStepInput(new ShowMutliStepInputParams(inputId, Bundle.DN_GenerateDelegateMethod())).thenAccept(result -> {
Either<List<QuickPickItem>, String> frameworkData = result.get(FRAMEWORKS);
QuickPickItem selectedFramework = (frameworkData != null ? frameworkData.getLeft() : testingFrameworks).get(0);
Either<List<QuickPickItem>, String> classNameData = result.get(CLASS_NAME);
String className = classNameData != null ? classNameData.getRight() : null;
future.complete(selectedFramework != null && className != null ? generate(client, fileObject, targetFolder, className, selectedFramework.getLabel()) : null);
});
}
} else {
throw new IllegalArgumentException(String.format("Illegal number of arguments received for command: %s", command));
}
} catch (JsonSyntaxException | IllegalArgumentException | MalformedURLException ex) {
future.completeExceptionally(ex);
}
return future;
}
private boolean generate(NbCodeLanguageClient client, FileObject fileObject, FileObject targetFolder, String className, String testingFramework) {
Collection<? extends Lookup.Item<TestCreatorProvider>> providers = Lookup.getDefault().lookupResult(TestCreatorProvider.class).allItems();
for (final Lookup.Item<TestCreatorProvider> provider : providers) {
if (provider.getDisplayName().equals(testingFramework)) {
final TestCreatorProvider.Context context = new TestCreatorProvider.Context(new FileObject[]{fileObject});
context.setSingleClass(true);
context.setTargetFolder(targetFolder);
context.setTestClassName(className);
AtomicReference<FileChangeListener> fcl = new AtomicReference<>();
fcl.set(new FileChangeAdapter() {
@Override
public void fileDataCreated(FileEvent fe) {
RequestProcessor.getDefault().post(() -> {
client.showDocument(new ShowDocumentParams(Utils.toUri(fe.getFile())));
}, 1000);
FileChangeListener l = fcl.getAndSet(null);
if (l != null) {
targetFolder.removeRecursiveListener(l);
}
}
});
targetFolder.addRecursiveListener(fcl.get());
try {
provider.getInstance().createTests(context);
} finally {
RequestProcessor.getDefault().post(() -> {
FileChangeListener l = fcl.getAndSet(null);
if (l != null) {
targetFolder.removeRecursiveListener(l);
}
}, 10000);
}
return true;
}
}
return false;
}
private static Map<Object, List<String>> getValidCombinations(CompilationInfo info) {
List<String> testingFrameworks = getTestingFrameworks(info.getFileObject());
if (testingFrameworks.isEmpty()) {
return null;
}
Map<Object, List<String>> validCombinations = new HashMap<>();
for (Object location : getLocations(info.getFileObject())) {
String targetFolderPath = getTargetFolderPath(location);
List<String> framework2Add = new ArrayList<>();
for (String framework : testingFrameworks) {
String preffiledName = getPreffiledName(info.getFileObject(), framework);
preffiledName = preffiledName.replace('.', File.separatorChar).concat(".java");
String path = targetFolderPath.concat(File.separator).concat(preffiledName);
File f = new File(path);
FileObject fo = FileUtil.toFileObject(f);
if (fo == null) {
framework2Add.add(framework);
}
}
if (!framework2Add.isEmpty()) {
validCombinations.put(location, framework2Add);
}
}
return validCombinations;
}
private static List<String> getTestingFrameworks(FileObject fileObject) {
List<String> testingFrameworks = new ArrayList<>();
Collection<? extends Lookup.Item<TestCreatorProvider>> testCreatorProviders = Lookup.getDefault().lookupResult(TestCreatorProvider.class).allItems();
for (Lookup.Item<TestCreatorProvider> provider : testCreatorProviders) {
if (provider.getInstance().enable(new FileObject[]{fileObject})) {
testingFrameworks.add(provider.getDisplayName());
}
}
return testingFrameworks;
}
private static Object[] getLocations(FileObject activeFO) {
Object[] locations = null;
Collection<? extends CommonTestUtilProvider> testUtilProviders = Lookup.getDefault().lookupAll(CommonTestUtilProvider.class);
for (CommonTestUtilProvider provider : testUtilProviders) {
locations = provider.getTestTargets(activeFO);
break;
}
if (locations != null && locations.length == 0) {
SourceGroup sourceGroupOwner = findSourceGroupOwner(activeFO);
if (sourceGroupOwner != null) {
locations = UnitTestForSourceQuery.findUnitTests(sourceGroupOwner.getRootFolder());
}
}
return locations != null ? locations : new Object[0];
}
private static String getPreffiledName(FileObject fileObj, String selectedFramework) {
ClassPath cp = ClassPath.getClassPath(fileObj, ClassPath.SOURCE);
String className = cp.getResourceName(fileObj, '.', false);
return className + getTestingFrameworkSuffix(selectedFramework) + "Test";
}
private static String getTestingFrameworkSuffix(String selectedFramework) {
if (selectedFramework == null) {
return "";
}
String testngFramework = "";
Collection<? extends GuiUtilsProvider> providers = Lookup.getDefault().lookupAll(GuiUtilsProvider.class);
for (GuiUtilsProvider provider : providers) {
testngFramework = provider.getTestngFramework();
break;
}
return selectedFramework.equals(testngFramework) ? "NG" : "";
}
private static String getTargetFolderUri(Object selectedLocation) throws URISyntaxException {
if (selectedLocation == null) {
return null;
}
if (selectedLocation instanceof SourceGroup) {
return Utils.toUri(((SourceGroup) selectedLocation).getRootFolder());
}
if (selectedLocation instanceof URL) {
return URITranslator.getDefault().uriToLSP(((URL) selectedLocation).toURI().toString());
}
assert selectedLocation instanceof FileObject;
return Utils.toUri((FileObject) selectedLocation);
}
private static FileObject getTargetFolder(String uri) throws MalformedURLException {
FileObject targetFolder = Utils.fromUri(uri);
if (targetFolder == null) {
File file = BaseUtilities.toFile(URI.create(uri));
if (file != null && !file.exists()) {
file.mkdirs();
}
targetFolder = Utils.fromUri(uri);
}
return targetFolder != null && targetFolder.isFolder() ? targetFolder : null;
}
private static String getTargetFolderPath(Object selectedLocation) {
if (selectedLocation == null) {
return null;
}
if (selectedLocation instanceof SourceGroup) {
return ((SourceGroup) selectedLocation).getRootFolder().getPath();
}
if (selectedLocation instanceof URL) {
return ((URL) selectedLocation).getPath();
}
assert selectedLocation instanceof FileObject;
return ((FileObject) selectedLocation).getPath();
}
private static SourceGroup findSourceGroupOwner(FileObject file) {
final Project project = FileOwnerQuery.getOwner(file);
if (project != null) {
final SourceGroup[] sourceGroups = ProjectUtils.getSources(project).getSourceGroups(JavaProjectConstants.SOURCES_TYPE_JAVA);
for (int i = 0; i < sourceGroups.length; i++) {
SourceGroup srcGroup = sourceGroups[i];
FileObject root = srcGroup.getRootFolder();
if (((file==root)||(FileUtil.isParentOf(root,file))) && srcGroup.contains(file)) {
return srcGroup;
}
}
}
return null;
}
}