diff --git a/jdtls.ext/com.microsoft.jdtls.ext.core/plugin.xml b/jdtls.ext/com.microsoft.jdtls.ext.core/plugin.xml index 5148ec64..85fc9375 100644 --- a/jdtls.ext/com.microsoft.jdtls.ext.core/plugin.xml +++ b/jdtls.ext/com.microsoft.jdtls.ext.core/plugin.xml @@ -7,6 +7,8 @@ + + arguments, IProgress return PackageCommand.getChildren(arguments, monitor); case "java.resolvePath": return PackageCommand.resolvePath(arguments, monitor); + case "java.project.getMainMethod": + return ProjectCommand.getMainMethod(arguments, monitor); + case "java.project.generateJar": + return ProjectCommand.exportJar(arguments, monitor); default: break; } diff --git a/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/PackageCommand.java b/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/PackageCommand.java index 6b763833..27ba95fa 100644 --- a/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/PackageCommand.java +++ b/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/PackageCommand.java @@ -526,7 +526,7 @@ private static Object[] findJarDirectoryChildren(JarEntryDirectory directory, St return null; } - private static IJavaProject getJavaProject(String projectUri) { + public static IJavaProject getJavaProject(String projectUri) { IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot(); IContainer[] containers = root.findContainersForLocationURI(JDTUtils.toURI(projectUri)); diff --git a/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/ProjectCommand.java b/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/ProjectCommand.java index b5cf9427..3f0e82bc 100644 --- a/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/ProjectCommand.java +++ b/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/ProjectCommand.java @@ -11,17 +11,45 @@ package com.microsoft.jdtls.ext.core; +import java.io.File; +import java.io.FileOutputStream; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashSet; import java.util.List; import java.util.Objects; +import java.util.Set; +import java.util.jar.Attributes; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; +import java.util.zip.ZipFile; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IWorkspaceRoot; import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.core.runtime.Path; +import org.eclipse.core.resources.IFile; +import org.eclipse.jdt.core.search.IJavaSearchScope; +import org.eclipse.jdt.core.search.SearchEngine; +import org.eclipse.jdt.core.search.SearchPattern; +import org.eclipse.jdt.core.IJavaElement; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IMethod; +import org.eclipse.jdt.core.IModuleDescription; +import org.eclipse.jdt.core.IPackageFragmentRoot; import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jdt.core.search.IJavaSearchConstants; +import org.eclipse.jdt.core.search.SearchRequestor; +import org.eclipse.jdt.core.search.SearchMatch; +import org.eclipse.jdt.core.search.SearchParticipant; +import org.eclipse.jdt.launching.JavaRuntime; import org.eclipse.jdt.ls.core.internal.JavaLanguageServerPlugin; import org.eclipse.jdt.ls.core.internal.ProjectUtils; import org.eclipse.jdt.ls.core.internal.ResourceUtils; @@ -30,9 +58,31 @@ import com.microsoft.jdtls.ext.core.model.NodeKind; import com.microsoft.jdtls.ext.core.model.PackageNode; +import org.eclipse.lsp4j.jsonrpc.json.adapters.CollectionTypeAdapter; +import org.eclipse.lsp4j.jsonrpc.json.adapters.EnumTypeAdapter; + +import static org.eclipse.jdt.internal.jarpackager.JarPackageUtil.writeFile; +import static org.eclipse.jdt.internal.jarpackager.JarPackageUtil.writeArchive; public final class ProjectCommand { + public static class MainClassInfo { + + public String name; + + public String path; + + public MainClassInfo(String name, String path) { + this.name = name; + this.path = path; + } + } + + private static final Gson gson = new GsonBuilder() + .registerTypeAdapterFactory(new CollectionTypeAdapter.Factory()) + .registerTypeAdapterFactory(new EnumTypeAdapter.Factory()) + .create(); + public static List listProjects(List arguments, IProgressMonitor monitor) { String workspaceUri = (String) arguments.get(0); IPath workspacePath = ResourceUtils.canonicalFilePathFromURI(workspaceUri); @@ -79,4 +129,116 @@ private static String getWorkspaceInvisibleProjectName(IPath workspacePath) { String fileName = workspacePath.toFile().getName(); return fileName + "_" + Integer.toHexString(workspacePath.toPortableString().hashCode()); } + + public static boolean exportJar(List arguments, IProgressMonitor monitor) { + if (arguments.size() < 3) { + return false; + } + String mainMethod = gson.fromJson(gson.toJson(arguments.get(0)), String.class); + String[] classpaths = gson.fromJson(gson.toJson(arguments.get(1)), String[].class); + String destination = gson.fromJson(gson.toJson(arguments.get(2)), String.class); + Manifest manifest = new Manifest(); + manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); + if (mainMethod.length() > 0) { + manifest.getMainAttributes().put(Attributes.Name.MAIN_CLASS,mainMethod); + } + try (JarOutputStream target = new JarOutputStream(new FileOutputStream(destination), manifest)) { + Set fDirectories = new HashSet<>(); + for (String classpath : classpaths) { + if (classpath != null) { + if(classpath.endsWith(".jar")) { + ZipFile zip = new ZipFile(classpath); + writeArchive(zip, true, true, target, fDirectories, monitor); + } + else { + File folder = new File(classpath); + writeFileRecursively(folder, target, fDirectories, folder.getAbsolutePath().length() + 1); + } + } + } + } catch (Exception e) { + return false; + } + return true; + } + + private static void writeFileRecursively(File folder, JarOutputStream fJarOutputStream, Set fDirectories, int len) { + File[] files = folder.listFiles(); + for (File file : files) { + if (file.isDirectory()) { + writeFileRecursively(file, fJarOutputStream, fDirectories, len); + } else if (file.isFile()) { + try { + writeFile(file, new Path(file.getAbsolutePath().substring(len)), true, true, fJarOutputStream, fDirectories); + } + catch (Exception e) { + // do nothing + } + } + } + } + + public static List getMainMethod(List arguments, IProgressMonitor monitor) throws Exception { + List projectList = listProjects(arguments, monitor); + final List res = new ArrayList<>(); + List searchRoots = new ArrayList<>(); + if (projectList.size() == 0) { + return res; + } + for (PackageNode project : projectList) { + IJavaProject javaProject = PackageCommand.getJavaProject(project.getUri()); + for (IPackageFragmentRoot packageFragmentRoot : javaProject.getAllPackageFragmentRoots()) { + if (!packageFragmentRoot.isExternal()) { + searchRoots.add(packageFragmentRoot); + } + } + } + IJavaSearchScope scope = SearchEngine.createJavaSearchScope(searchRoots.toArray(IJavaElement[]::new)); + SearchPattern pattern = SearchPattern.createPattern("main(String[]) void", IJavaSearchConstants.METHOD, + IJavaSearchConstants.DECLARATIONS, SearchPattern.R_EXACT_MATCH | SearchPattern.R_CASE_SENSITIVE); + SearchRequestor requestor = new SearchRequestor() { + @Override + public void acceptSearchMatch(SearchMatch match) { + Object element = match.getElement(); + if (!(element instanceof IMethod)) { + return; + } + IMethod method = (IMethod) element; + try { + if (!method.isMainMethod() || method.getResource() == null || method.getJavaProject() == null) { + return; + } + String mainClass = method.getDeclaringType().getFullyQualifiedName(); + String filePath = ""; + if (match.getResource() instanceof IFile) { + filePath = match.getResource().getLocation().toOSString(); + } + res.add(new MainClassInfo(mainClass, filePath)); + } catch (JavaModelException e) { + // ignore + } + } + }; + SearchEngine searchEngine = new SearchEngine(); + try { + searchEngine.search(pattern, new SearchParticipant[] {SearchEngine.getDefaultSearchParticipant()}, + scope, requestor, new NullProgressMonitor()); + } catch (CoreException e) { + // ignore + } + return res; + } + + public static String getModuleName(IJavaProject project) { + if (project == null || !JavaRuntime.isModularProject(project)) { + return null; + } + try { + IModuleDescription module = project.getModuleDescription(); + return module == null ? null : module.getElementName(); + } catch (CoreException e) { + return null; + } + } + } diff --git a/jdtls.ext/com.microsoft.jdtls.ext.core/src/org/eclipse/jdt/internal/jarpackager/JarPackageUtil.java b/jdtls.ext/com.microsoft.jdtls.ext.core/src/org/eclipse/jdt/internal/jarpackager/JarPackageUtil.java new file mode 100644 index 00000000..a3ed3c5d --- /dev/null +++ b/jdtls.ext/com.microsoft.jdtls.ext.core/src/org/eclipse/jdt/internal/jarpackager/JarPackageUtil.java @@ -0,0 +1,357 @@ +/******************************************************************************* + * Copyright (c) 2020 IBM Corporation and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * IBM Corporation - initial API and implementation + * Microsoft Corporation - based this file on JarWriter3, JarWriter4, UnpackFatJarBuilder, JarPackagerUtil and JarBuilder + *******************************************************************************/ +package org.eclipse.jdt.internal.jarpackager; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.Set; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; +import java.util.zip.CRC32; +import java.util.zip.ZipEntry; +import java.util.zip.ZipException; +import java.util.zip.ZipFile; + +import com.microsoft.jdtls.ext.core.JdtlsExtActivator; + +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.MultiStatus; +import org.eclipse.core.runtime.OperationCanceledException; +import org.eclipse.core.runtime.Status; + +public class JarPackageUtil { + + private static final int INTERNAL_ERROR= 10001; + + /** + * Write the given entry describing the given content to the current archive. + * Extracted from org.eclipse.jdt.ui.jarpackager.JarWriter3 + * + * @param entry the entry to write + * @param content the content to write + * @param fJarOutputStream the destination JarOutputStream + * + * @throws IOException If an I/O error occurred + * + * @since 1.14 + * + */ + private static void addEntry(JarEntry entry, InputStream content, JarOutputStream fJarOutputStream) throws IOException { + byte[] readBuffer = new byte[4096]; + try { + fJarOutputStream.putNextEntry(entry); + int count; + while ((count = content.read(readBuffer, 0, readBuffer.length)) != -1) { + fJarOutputStream.write(readBuffer, 0, count); + } + } finally { + if (content != null) { + content.close(); + } + /* + * Commented out because some JREs throw an NPE if a stream + * is closed twice. This works because + * a) putNextEntry closes the previous entry + * b) closing the stream closes the last entry + */ + } + } + + /** + * Write the contents of the given zipfile to the JarOutputStream. + * Extracted from org.eclipse.jdt.internal.ui.jarpackagerfat.UnpackFatJarBuilder + * + * @param zipFile the zipfile to extract + * @param areDirectoryEntriesIncluded the directory entries are included + * @param isCompressed the jar is compressed + * @param fJarOutputStream the destination JarOutputStream + * @param fDirectories the temporary set saves existing directories + * @param progressMonitor the progressMonitor + * + * @return the MultiStatus saving the warnings during the process + * + * @since 1.14 + * + */ + public static MultiStatus writeArchive(ZipFile zipFile, boolean areDirectoryEntriesIncluded, + boolean isCompressed, JarOutputStream fJarOutputStream, + Set fDirectories, IProgressMonitor progressMonitor) { + MultiStatus fStatus = new MultiStatus(JdtlsExtActivator.PLUGIN_ID, IStatus.OK, ""); //$NON-NLS-1$ + Enumeration jarEntriesEnum = zipFile.entries(); + File zipFile1 = new File(zipFile.getName()); + try { + String zipFileCanonical = zipFile1.getCanonicalPath(); + while (jarEntriesEnum.hasMoreElements()) { + ZipEntry zipEntry = jarEntriesEnum.nextElement(); + if (!zipEntry.isDirectory()) { + String entryName = zipEntry.getName(); + File zipEntryFile = new File(zipFile1, entryName); + String zipEntryCanonical = zipEntryFile.getCanonicalPath(); + if (zipEntryCanonical.startsWith(zipFileCanonical + File.separator)) { + addFile(entryName, zipEntry, zipFile, areDirectoryEntriesIncluded, isCompressed, fJarOutputStream, fDirectories, fStatus); + } else { + addWarning("Invalid path" + entryName, null, fStatus); //$NON-NLS-1$ + } + } + progressMonitor.worked(1); + if (progressMonitor.isCanceled()) { + throw new OperationCanceledException(); + } + } + } catch (IOException e) { + addWarning("ZipFile error" + zipFile.getName(), null, fStatus); //$NON-NLS-1$ + e.printStackTrace(); + } + return fStatus; + } + + /** + * Write the entry to the destinationPath of the given JarOutputStream. + * Extracted from org.eclipse.jdt.internal.ui.jarpackagerfat.UnpackFatJarBuilder + * + * @param destinationPath the destinationPath in the jar file + * @param jarEntry the jar entry to write + * @param zipFile the zipfile to extract + * @param areDirectoryEntriesIncluded the directory entries are included + * @param isCompressed the jar is compressed + * @param fJarOutputStream the destination JarOutputStream + * @param fDirectories the temporary set saves existing directories + * @param fStatus the MultiStatus saving the warnings during the process + * + * @since 1.14 + * + */ + private static void addFile(String destinationPath, ZipEntry jarEntry, ZipFile zipFile, + boolean areDirectoryEntriesIncluded, boolean isCompressed, + JarOutputStream fJarOutputStream, Set fDirectories, MultiStatus fStatus) { + // Handle META-INF/MANIFEST.MF + if (destinationPath.equalsIgnoreCase("META-INF/MANIFEST.MF") //$NON-NLS-1$ + || (destinationPath.startsWith("META-INF/") && destinationPath.endsWith(".SF"))) { //$NON-NLS-1$//$NON-NLS-2$ + return; + } + try { + addZipEntry(jarEntry, zipFile, destinationPath, areDirectoryEntriesIncluded, isCompressed, fJarOutputStream, fDirectories); + } catch (IOException ex) { + if (ex instanceof ZipException && ex.getMessage() != null && ex.getMessage().startsWith("duplicate entry:")) {//$NON-NLS-1$ + // ignore duplicates in META-INF (*.SF, *.RSA) + if (!destinationPath.startsWith("META-INF/")) { //$NON-NLS-1$ + addWarning(ex.getMessage(), ex, fStatus); + } + } + } + } + + /** + * Write the entry to the destinationPath of the given JarOutputStream. + * Extracted from org.eclipse.jdt.internal.ui.jarpackagerfat.JarWriter4 + * + * @param zipEntry the jar entry to write + * @param zipFile the zipfile to extract + * @param path the destinationPath in the jar file + * @param areDirectoryEntriesIncluded the directory entries are included + * @param isCompressed the jar is compressed + * @param fJarOutputStream the destination JarOutputStream + * @param fDirectories the temporary set saves existing directories + * + * @throws IOException If an I/O error occurred + * + * @since 1.14 + * + */ + private static void addZipEntry(ZipEntry zipEntry, ZipFile zipFile, String path, + boolean areDirectoryEntriesIncluded, boolean isCompressed, + JarOutputStream fJarOutputStream, Set fDirectories) throws IOException { + if (areDirectoryEntriesIncluded) { + addDirectories(path, fJarOutputStream, fDirectories); + } + JarEntry newEntry = new JarEntry(path.replace(File.separatorChar, '/')); + if (isCompressed) { + newEntry.setMethod(ZipEntry.DEFLATED); + // Entry is filled automatically. + } else { + newEntry.setMethod(ZipEntry.STORED); + newEntry.setSize(zipEntry.getSize()); + newEntry.setCrc(zipEntry.getCrc()); + } + long lastModified = System.currentTimeMillis(); + // Set modification time + newEntry.setTime(lastModified); + addEntry(newEntry, zipFile.getInputStream(zipEntry), fJarOutputStream); + } + + /** + * Creates the directory entries for the given path and writes it to the current archive. + * Extracted from org.eclipse.jdt.ui.jarpackager.JarWriter3 + * + * @param destPath the path to add + * @param fJarOutputStream the destination JarOutputStream + * @param fDirectories the temporary set saves existing directories + * + * @throws IOException if an I/O error has occurred + * + * @since 1.14 + */ + private static void addDirectories(String destPath, JarOutputStream fJarOutputStream, Set fDirectories) throws IOException { + String path = destPath.replace(File.separatorChar, '/'); + int lastSlash = path.lastIndexOf('/'); + List directories = new ArrayList<>(2); + while (lastSlash != -1) { + path= path.substring(0, lastSlash + 1); + if (!fDirectories.add(path)) { + break; + } + JarEntry newEntry = new JarEntry(path); + newEntry.setMethod(ZipEntry.STORED); + newEntry.setSize(0); + newEntry.setCrc(0); + newEntry.setTime(System.currentTimeMillis()); + directories.add(newEntry); + lastSlash= path.lastIndexOf('/', lastSlash - 1); + } + for (int i = directories.size() - 1; i >= 0; --i) { + fJarOutputStream.putNextEntry(directories.get(i)); + } + } + + /** + * Write the single file to the JarOutputStream. + * Extracted from org.eclipse.jdt.internal.ui.jarpackagerfat.JarWriter4 + * + * @param file the file to write + * @param destinationPath the destinationPath in the jar file + * @param areDirectoryEntriesIncluded the directory entries are included + * @param isCompressed the jar is compressed + * @param fJarOutputStream the destination JarOutputStream + * @param fDirectories the temporary set saves existing directories + * + * @throws CoreException if an error has occurred + * + * @since 1.14 + * + */ + public static void writeFile(File file, IPath destinationPath, boolean areDirectoryEntriesIncluded, + boolean isCompressed, JarOutputStream fJarOutputStream, Set fDirectories) throws CoreException { + try { + addFile(file, destinationPath, areDirectoryEntriesIncluded, isCompressed, fJarOutputStream, fDirectories); + } catch (IOException ex) { + throw new CoreException(new Status(IStatus.ERROR, JdtlsExtActivator.PLUGIN_ID, INTERNAL_ERROR, ex.getLocalizedMessage(), ex)); + } + } + + /** + * Add the single file to the JarOutputStream. + * Extracted from org.eclipse.jdt.internal.ui.jarpackagerfat.JarWriter4 + * + * @param file the file to write + * @param path the destinationPath in the jar file + * @param areDirectoryEntriesIncluded the directory entries are included + * @param isCompressed the jar is compressed + * @param fJarOutputStream the destination JarOutputStream + * @param fDirectories the temporary set saves existing directories + * + * @throws IOException if an I/O error has occurred + * + * @since 1.14 + * + */ + private static void addFile(File file, IPath path, boolean areDirectoryEntriesIncluded, + boolean isCompressed, JarOutputStream fJarOutputStream, Set fDirectories) throws IOException { + if (areDirectoryEntriesIncluded) { + addDirectories(path, fJarOutputStream, fDirectories); + } + JarEntry newEntry = new JarEntry(path.toString().replace(File.separatorChar, '/')); + if (isCompressed) { + newEntry.setMethod(ZipEntry.DEFLATED); + // Entry is filled automatically. + } else { + newEntry.setMethod(ZipEntry.STORED); + calculateCrcAndSize(newEntry, new FileInputStream(file), new byte[4096]); + } + newEntry.setTime(file.lastModified()); + addEntry(newEntry, new FileInputStream(file), fJarOutputStream); + } + + /** + * Creates the directory entries for the given path and writes it to the current archive. + * Extracted from org.eclipse.jdt.ui.jarpackager.JarWriter3 + * + * @param destinationPath the path to add + * @param fJarOutputStream the destination JarOutputStream + * @param fDirectories the temporary set saves existing directories + * + * @throws IOException if an I/O error has occurred + * + * @since 1.14 + */ + private static void addDirectories(IPath destinationPath, JarOutputStream fJarOutputStream, Set fDirectories) throws IOException { + addDirectories(destinationPath.toString(), fJarOutputStream, fDirectories); + } + + /** + * Calculates the crc and size of the resource and updates the entry. + * Extracted from org.eclipse.jdt.internal.ui.jarpackager.JarPackagerUtil + * + * @param entry the jar entry to update + * @param stream the input stream + * @param buffer a shared buffer to store temporary data + * + * @throws IOException if an input/output error occurs + * + * @since 1.14 + */ + private static void calculateCrcAndSize(final ZipEntry entry, final InputStream stream, final byte[] buffer) throws IOException { + int size = 0; + final CRC32 crc = new CRC32(); + int count; + try { + while ((count = stream.read(buffer, 0, buffer.length)) != -1) { + crc.update(buffer, 0, count); + size += count; + } + } finally { + if (stream != null) { + try { + stream.close(); + } catch (IOException exception) { + // Do nothing + } + } + } + entry.setSize(size); + entry.setCrc(crc.getValue()); + } + + /** + * add a warning message into the MultiStatus. + * + * @param message the message to add + * @param error the reason of the message + * @param fStatus the MultiStatus to write + * + * @since 1.14 + */ + private final static void addWarning(String message, Throwable error, MultiStatus fStatus) { + fStatus.add(new Status(IStatus.WARNING, JdtlsExtActivator.PLUGIN_ID, INTERNAL_ERROR, message, error)); + } + +} diff --git a/jdtls.ext/pom.xml b/jdtls.ext/pom.xml index 73d68ee8..d8b15320 100644 --- a/jdtls.ext/pom.xml +++ b/jdtls.ext/pom.xml @@ -81,9 +81,9 @@ - 201906 + 202006 p2 - http://download.eclipse.org/releases/2019-06/ + http://download.eclipse.org/releases/2020-06/ oss.sonatype.org diff --git a/jdtls.ext/target.target b/jdtls.ext/target.target index fd2606f8..db205200 100644 --- a/jdtls.ext/target.target +++ b/jdtls.ext/target.target @@ -1,10 +1,16 @@ - + - - + + + + + + + + @@ -12,11 +18,10 @@ - - - - - + + + + @@ -24,16 +29,13 @@ - + - - - - - - - + + + + diff --git a/package-lock.json b/package-lock.json index 98af8c03..7dbcc3ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -320,7 +320,7 @@ }, "ansi-colors": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", + "resolved": "http://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", "dev": true, "requires": { @@ -860,7 +860,7 @@ }, "util": { "version": "0.10.3", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "resolved": "http://registry.npmjs.org/util/-/util-0.10.3.tgz", "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", "dev": true, "requires": { @@ -1710,7 +1710,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "requires": { @@ -2686,7 +2686,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "requires": { @@ -4177,7 +4177,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "requires": { @@ -6531,7 +6531,7 @@ }, "os-locale": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "resolved": "http://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", "dev": true, "requires": { @@ -6974,7 +6974,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "requires": { @@ -7321,7 +7321,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "requires": { @@ -9330,7 +9330,7 @@ }, "wrap-ansi": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "resolved": "http://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", "dev": true, "requires": { diff --git a/package.json b/package.json index 1815342e..66d0ec87 100644 --- a/package.json +++ b/package.json @@ -105,6 +105,12 @@ "title": "%contributes.commands.java.view.package.revealFileInOS%", "category": "Java" }, + { + "command": "java.view.package.exportJar", + "title": "%contributes.commands.java.view.package.exportJar%", + "category": "Java", + "icon": "$(arrow-down)" + }, { "command": "java.view.package.copyFilePath", "title": "%contributes.commands.java.view.package.copyFilePath%", @@ -177,6 +183,10 @@ "command": "java.view.package.revealFileInOS", "when": "never" }, + { + "command": "java.view.package.exportJar", + "when": "java:serverMode != LightWeight" + }, { "command": "java.view.package.copyFilePath", "when": "never" @@ -203,6 +213,11 @@ } ], "view/title": [ + { + "command": "java.view.package.exportJar", + "when": "view == javaProjectExplorer && java:serverMode!= LightWeight && workspaceFolderCount != 0", + "group": "navigation@50" + }, { "command": "java.view.package.refresh", "when": "view == javaProjectExplorer && java:serverMode!= LightWeight", @@ -269,6 +284,11 @@ "command": "java.project.maven.addDependency", "when": "view == javaProjectExplorer && mavenEnabled && viewItem =~ /container\/maven-dependencies/", "group": "inline@0" + }, + { + "command": "java.view.package.exportJar", + "when": "view == javaProjectExplorer && viewItem =~ /java:workspace.*?\\+uri/ && java:serverMode!= LightWeight", + "group": "inline" } ] }, diff --git a/package.nls.json b/package.nls.json index 9aeab13b..7c68803a 100644 --- a/package.nls.json +++ b/package.nls.json @@ -11,6 +11,7 @@ "contributes.commands.java.view.package.linkWithFolderExplorer":"Synchronize dependency viewer selection with folder explorer", "contributes.commands.java.view.package.unlinkWithFolderExplorer":"Desynchronize dependency viewer selection with folder explorer", "contributes.commands.java.view.package.revealFileInOS": "Reveal in Explorer", + "contributes.commands.java.view.package.exportJar": "Export Jar...", "contributes.commands.java.view.package.copyFilePath": "Copy Path", "contributes.commands.java.view.package.copyRelativeFilePath": "Copy Relative Path", "configuration.java.dependency.title": "Java Dependency Configuration", diff --git a/package.nls.zh.json b/package.nls.zh.json index 423aca44..37454098 100644 --- a/package.nls.zh.json +++ b/package.nls.zh.json @@ -11,6 +11,7 @@ "contributes.commands.java.view.package.linkWithFolderExplorer":"开启 Java 依赖项资源管理器与当前浏览文件的关联", "contributes.commands.java.view.package.unlinkWithFolderExplorer":"关闭 Java 依赖项资源管理器与当前浏览文件的关联", "contributes.commands.java.view.package.revealFileInOS": "打开所在的文件夹", + "contributes.commands.java.view.package.exportJar": "导出到 Jar 文件...", "contributes.commands.java.view.package.copyFilePath": "复制路径", "contributes.commands.java.view.package.copyRelativeFilePath": "复制相对路径", "configuration.java.dependency.title": "Java 依赖管理配置", diff --git a/src/build.ts b/src/build.ts new file mode 100644 index 00000000..11bcd6b9 --- /dev/null +++ b/src/build.ts @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +import { basename } from "path"; +import { commands, DiagnosticSeverity, languages, Uri, window } from "vscode"; +import { instrumentOperation, sendInfo, sendOperationError, setErrorCode } from "vscode-extension-telemetry-wrapper"; +import { Commands, executeJavaExtensionCommand } from "./commands"; +import { Jdtls } from "./java/jdtls"; +import { UserError } from "./utility"; + +export async function buildWorkspace(): Promise { + const buildResult = await instrumentOperation("build", async (operationId: string) => { + let error; + try { + await executeJavaExtensionCommand(Commands.JAVA_BUILD_WORKSPACE, false); + } catch (err) { + error = err; + } + + return { + error, + operationId, + }; + })(); + + if (buildResult.error) { + return handleBuildFailure(buildResult.operationId, buildResult.error); + } + return true; +} + +async function handleBuildFailure(operationId: string, err: any): Promise { + + const error: Error = new UserError({ + message: "Build failed", + }); + setErrorCode(error, Number(err)); + sendOperationError(operationId, "build", error); + if (err === Jdtls.CompileWorkspaceStatus.Witherror || err === Jdtls.CompileWorkspaceStatus.Failed) { + if (checkErrorsReportedByJavaExtension()) { + commands.executeCommand("workbench.actions.view.problems"); + } + + const ans = await window.showErrorMessage("Build failed, do you want to continue?", + "Proceed", "Fix...", "Cancel"); + sendInfo(operationId, { + operationName: "build", + choiceForBuildError: ans || "esc", + }); + if (ans === "Proceed") { + return true; + } else if (ans === "Fix...") { + showFixSuggestions(operationId); + } + return false; + } + return false; +} + +function checkErrorsReportedByJavaExtension(): boolean { + const problems = languages.getDiagnostics() || []; + for (const problem of problems) { + const fileName = basename(problem[0].fsPath || ""); + if (fileName.endsWith(".java") || fileName === "pom.xml" || fileName.endsWith(".gradle")) { + if (problem[1].filter((diagnostic) => diagnostic.severity === DiagnosticSeverity.Error).length) { + return true; + } + } + } + return false; +} + +async function showFixSuggestions(operationId: string) { + let buildFiles = []; + try { + buildFiles = await Jdtls.resolveBuildFiles(); + } catch (error) { + // do nothing + } + + const pickitems = []; + pickitems.push({ + label: "Clean workspace cache", + detail: "Clean the stale workspace and reload the window", + }); + if (buildFiles.length) { + pickitems.push({ + label: "Update project configuration", + detail: "Force the language server to update the project configuration/classpath", + }); + } + pickitems.push({ + label: "Open log file", + detail: "Open log file to view more details for the build errors", + }); + + const ans = await window.showQuickPick(pickitems, { + placeHolder: "Please fix the errors in PROBLEMS first, then try the fix suggestions below.", + }); + sendInfo(operationId, { + operationName: "build", + choiceForBuildFix: ans ? ans.label : "esc", + }); + if (!ans) { + return; + } + + if (ans.label === "Clean workspace cache") { + commands.executeCommand("java.clean.workspace"); + } else if (ans.label === "Update project configuration") { + for (const buildFile of buildFiles) { + await commands.executeCommand("java.projectConfiguration.update", Uri.parse(buildFile)); + } + } else if (ans.label === "Open log file") { + commands.executeCommand("java.open.serverLog"); + } +} diff --git a/src/commands.ts b/src/commands.ts index e68846c5..65eba097 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -1,6 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. - +import { commands } from "vscode"; /** * Commonly used commands */ @@ -30,6 +30,8 @@ export namespace Commands { export const VIEW_PACKAGE_COPY_RELATIVE_FILE_PATH = "java.view.package.copyRelativeFilePath"; + export const VIEW_PACKAGE_EXPORT_JAR = "java.view.package.exportJar"; + export const JAVA_PROJECT_CREATE = "java.project.create"; export const JAVA_PROJECT_ADD_LIBRARIES = "java.project.addLibraries"; @@ -57,5 +59,22 @@ export namespace Commands { export const JAVA_RESOLVEPATH = "java.resolvePath"; + export const JAVA_PROJECT_GETMAINMETHOD = "java.project.getMainMethod"; + + export const JAVA_PROJECT_GENERATEJAR = "java.project.generateJar"; + export const VSCODE_OPEN_FOLDER = "vscode.openFolder"; + + export const JAVA_BUILD_WORKSPACE = "java.workspace.compile"; + + export const JAVA_RESOLVE_BUILD_FILES = "vscode.java.resolveBuildFiles"; +} + +export function executeJavaLanguageServerCommand(...rest) { + return executeJavaExtensionCommand(Commands.EXECUTE_WORKSPACE_COMMAND, ...rest); +} + +export async function executeJavaExtensionCommand(commandName: string, ...rest) { + // TODO: need to handle error and trace telemetry + return commands.executeCommand(commandName, ...rest); } diff --git a/src/exportJarFileCommand.ts b/src/exportJarFileCommand.ts new file mode 100644 index 00000000..846e717a --- /dev/null +++ b/src/exportJarFileCommand.ts @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +import { EOL, platform } from "os"; +import { commands, Uri, window } from "vscode"; +import { sendOperationError } from "vscode-extension-telemetry-wrapper"; +import { buildWorkspace } from "./build"; +import { GenerateJarExecutor } from "./exportJarSteps/GenerateJarExecutor"; +import { IExportJarStepExecutor } from "./exportJarSteps/IExportJarStepExecutor"; +import { ResolveMainMethodExecutor } from "./exportJarSteps/ResolveMainMethodExecutor"; +import { ResolveWorkspaceExecutor } from "./exportJarSteps/ResolveWorkspaceExecutor"; +import { isStandardServerReady } from "./extension"; +import { INodeData } from "./java/nodeData"; + +export interface IStepMetadata { + entry?: INodeData; + workspaceUri?: Uri; + isPickedWorkspace: boolean; + projectList?: INodeData[]; + selectedMainMethod?: string; + outputPath?: string; + elements: string[]; +} + +export enum ExportJarStep { + ResolveWorkspace = "RESOLVEWORKSPACE", + ResolveMainMethod = "RESOLVEMAINMETHOD", + GenerateJar = "GENERATEJAR", + Finish = "FINISH", +} + +let isExportingJar: boolean = false; +const stepMap: Map = new Map([ + [ExportJarStep.ResolveWorkspace, new ResolveWorkspaceExecutor()], + [ExportJarStep.ResolveMainMethod, new ResolveMainMethodExecutor()], + [ExportJarStep.GenerateJar, new GenerateJarExecutor()], +]); + +export async function createJarFile(node?: INodeData) { + if (!isStandardServerReady() || isExportingJar) { + return; + } + isExportingJar = true; + return new Promise(async (resolve, reject) => { + if (await buildWorkspace() === false) { + return reject(); + } + let step: ExportJarStep = ExportJarStep.ResolveWorkspace; + const stepMetadata: IStepMetadata = { + entry: node, + isPickedWorkspace: false, + elements: [], + }; + while (step !== ExportJarStep.Finish) { + try { + step = await stepMap.get(step).execute(stepMetadata); + } catch (err) { + return err ? reject(`${err}`) : reject(); + } + } + return resolve(stepMetadata.outputPath); + }).then((message) => { + successMessage(message); + isExportingJar = false; + }, (err) => { + failMessage(err); + isExportingJar = false; + }); +} + +function failMessage(message: string) { + sendOperationError("", "Export Jar", new Error(message)); + window.showErrorMessage(message, "Done"); +} + +function successMessage(outputFileName: string) { + let openInExplorer: string; + if (platform() === "win32") { + openInExplorer = "Reveal in File Explorer"; + } else if (platform() === "darwin") { + openInExplorer = "Reveal in Finder"; + } else { + openInExplorer = "Open Containing Folder"; + } + window.showInformationMessage("Successfully exported jar to" + EOL + outputFileName, + openInExplorer, "Done").then((messageResult) => { + if (messageResult === openInExplorer) { + commands.executeCommand("revealFileInOS", Uri.file(outputFileName)); + } + }); +} diff --git a/src/exportJarSteps/GenerateJarExecutor.ts b/src/exportJarSteps/GenerateJarExecutor.ts new file mode 100644 index 00000000..c75357bf --- /dev/null +++ b/src/exportJarSteps/GenerateJarExecutor.ts @@ -0,0 +1,155 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +import { pathExists } from "fs-extra"; +import { basename, extname, join } from "path"; +import { Disposable, Extension, extensions, ProgressLocation, QuickInputButtons, QuickPick, window } from "vscode"; +import { ExportJarStep, IStepMetadata } from "../exportJarFileCommand"; +import { Jdtls } from "../java/jdtls"; +import { IExportJarStepExecutor } from "./IExportJarStepExecutor"; +import { createPickBox, IJarQuickPickItem } from "./utility"; + +export class GenerateJarExecutor implements IExportJarStepExecutor { + + public async execute(stepMetadata: IStepMetadata): Promise { + if (await this.generateJar(stepMetadata)) { + return ExportJarStep.Finish; + } + return ExportJarStep.ResolveMainMethod; + } + + private async generateJar(stepMetadata: IStepMetadata): Promise { + if (!(await this.generateElements(stepMetadata))) { + return false; + } + return window.withProgress({ + location: ProgressLocation.Window, + title: "Exporting Jar : Generating jar...", + cancellable: true, + }, (progress, token) => { + return new Promise(async (resolve, reject) => { + token.onCancellationRequested(() => { + return reject(); + }); + const destPath = join(stepMetadata.workspaceUri.fsPath, basename(stepMetadata.workspaceUri.fsPath) + ".jar"); + const exportResult = await Jdtls.exportJar(basename(stepMetadata.selectedMainMethod), stepMetadata.elements, destPath); + if (exportResult === true) { + stepMetadata.outputPath = destPath; + return resolve(true); + } else { + return reject(new Error("Export jar failed.")); + } + }); + }); + } + + private async generateElements(stepMetadata: IStepMetadata): Promise { + const extension: Extension | undefined = extensions.getExtension("redhat.java"); + const extensionApi: any = await extension?.activate(); + const dependencyItems: IJarQuickPickItem[] = await window.withProgress({ + location: ProgressLocation.Window, + title: "Exporting Jar : Resolving classpaths...", + cancellable: true, + }, (progress, token) => { + return new Promise(async (resolve, reject) => { + token.onCancellationRequested(() => { + return reject(); + }); + const pickItems: IJarQuickPickItem[] = []; + const uriSet: Set = new Set(); + for (const rootNode of stepMetadata.projectList) { + const classPaths: ClasspathResult = await extensionApi.getClasspaths(rootNode.uri, { scope: "runtime" }); + pickItems.push(...await this.parseDependencyItems(classPaths.classpaths, uriSet, stepMetadata.workspaceUri.fsPath, true), + ...await this.parseDependencyItems(classPaths.modulepaths, uriSet, stepMetadata.workspaceUri.fsPath, true)); + const classPathsTest: ClasspathResult = await extensionApi.getClasspaths(rootNode.uri, { scope: "test" }); + pickItems.push(...await this.parseDependencyItems(classPathsTest.classpaths, uriSet, stepMetadata.workspaceUri.fsPath, false), + ...await this.parseDependencyItems(classPathsTest.modulepaths, uriSet, stepMetadata.workspaceUri.fsPath, false)); + } + return resolve(pickItems); + }); + }); + if (dependencyItems.length === 0) { + throw new Error("No classpath found. Please make sure your project is valid."); + } else if (dependencyItems.length === 1) { + stepMetadata.elements.push(dependencyItems[0].uri); + return true; + } + dependencyItems.sort((node1, node2) => { + if (node1.description !== node2.description) { + return node1.description.localeCompare(node2.description); + } + if (node1.type !== node2.type) { + return node2.type.localeCompare(node1.type); + } + return node1.label.localeCompare(node2.label); + }); + const pickedDependencyItems: IJarQuickPickItem[] = []; + for (const item of dependencyItems) { + if (item.picked) { + pickedDependencyItems.push(item); + } + } + const disposables: Disposable[] = []; + let result: boolean = false; + try { + result = await new Promise(async (resolve, reject) => { + const pickBox = createPickBox("Export Jar : Determine elements", "Select the elements", dependencyItems, true, true); + pickBox.selectedItems = pickedDependencyItems; + disposables.push( + pickBox.onDidTriggerButton((item) => { + if (item === QuickInputButtons.Back) { + return resolve(false); + } + }), + pickBox.onDidAccept(() => { + for (const item of pickBox.selectedItems) { + stepMetadata.elements.push(item.uri); + } + return resolve(true); + }), + pickBox.onDidHide(() => { + return reject(); + }), + ); + disposables.push(pickBox); + pickBox.show(); + }); + } finally { + for (const d of disposables) { + d.dispose(); + } + } + return result; + } + + private async parseDependencyItems(paths: string[], uriSet: Set, projectPath: string, isRuntime: boolean): Promise { + const dependencyItems: IJarQuickPickItem[] = []; + for (const classpath of paths) { + if (await pathExists(classpath) === false) { + continue; + } + const extName = extname(classpath); + const baseName = (extName === ".jar") ? basename(classpath) : classpath.substring(projectPath.length + 1); + const descriptionValue = (isRuntime) ? "Runtime" : "Test"; + const typeValue = (extName === ".jar") ? "external" : "internal"; + if (!uriSet.has(classpath)) { + uriSet.add(classpath); + dependencyItems.push({ + label: baseName, + description: descriptionValue, + uri: classpath, + type: typeValue, + picked: isRuntime, + }); + } + } + return dependencyItems; + } + +} + +class ClasspathResult { + public projectRoot: string; + public classpaths: string[]; + public modulepaths: string[]; +} diff --git a/src/exportJarSteps/IExportJarStepExecutor.ts b/src/exportJarSteps/IExportJarStepExecutor.ts new file mode 100644 index 00000000..fee4d3d4 --- /dev/null +++ b/src/exportJarSteps/IExportJarStepExecutor.ts @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +import { ExportJarStep, IStepMetadata } from "../exportJarFileCommand"; + +export interface IExportJarStepExecutor { + execute(stepMetadata?: IStepMetadata): Promise; +} diff --git a/src/exportJarSteps/ResolveMainMethodExecutor.ts b/src/exportJarSteps/ResolveMainMethodExecutor.ts new file mode 100644 index 00000000..8cd6d8cd --- /dev/null +++ b/src/exportJarSteps/ResolveMainMethodExecutor.ts @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +import { Disposable, ProgressLocation, QuickInputButtons, QuickPick, window } from "vscode"; +import { ExportJarStep, IStepMetadata } from "../exportJarFileCommand"; +import { Jdtls } from "../java/jdtls"; +import { IExportJarStepExecutor } from "./IExportJarStepExecutor"; +import { createPickBox, IJarQuickPickItem } from "./utility"; + +export class ResolveMainMethodExecutor implements IExportJarStepExecutor { + + private static getName(data: MainMethodInfo) { + return data.name.substring(data.name.lastIndexOf(".") + 1); + } + + public async execute(stepMetadata: IStepMetadata): Promise { + if (await this.resolveMainMethod(stepMetadata)) { + return ExportJarStep.GenerateJar; + } + return ExportJarStep.ResolveWorkspace; + } + + private async resolveMainMethod(stepMetadata: IStepMetadata): Promise { + const mainMethods: MainMethodInfo[] = await window.withProgress({ + location: ProgressLocation.Window, + title: "Exporting Jar : Resolving main classes...", + cancellable: true, + }, (progress, token) => { + return new Promise(async (resolve, reject) => { + token.onCancellationRequested(() => { + return reject(); + }); + resolve(await Jdtls.getMainMethod(stepMetadata.workspaceUri.toString())); + }); + }); + if (mainMethods === undefined || mainMethods.length === 0) { + stepMetadata.selectedMainMethod = ""; + return true; + } + const pickItems: IJarQuickPickItem[] = []; + for (const mainMethod of mainMethods) { + pickItems.push({ + label: ResolveMainMethodExecutor.getName(mainMethod), + description: mainMethod.name, + }); + } + const noMainClassItem: IJarQuickPickItem = { + label: "", + }; + pickItems.push(noMainClassItem); + const disposables: Disposable[] = []; + let result: boolean = false; + try { + result = await new Promise(async (resolve, reject) => { + const pickBox = createPickBox("Export Jar : Determine main class", "Select the main class", + pickItems, stepMetadata.isPickedWorkspace); + disposables.push( + pickBox.onDidTriggerButton((item) => { + if (item === QuickInputButtons.Back) { + return resolve(false); + } + }), + pickBox.onDidAccept(() => { + stepMetadata.selectedMainMethod = pickBox.selectedItems[0].description; + return resolve(true); + }), + pickBox.onDidHide(() => { + return reject(); + }), + ); + disposables.push(pickBox); + pickBox.show(); + }); + } finally { + for (const d of disposables) { + d.dispose(); + } + } + return result; + } +} + +export class MainMethodInfo { + public name: string; + public path: string; +} diff --git a/src/exportJarSteps/ResolveWorkspaceExecutor.ts b/src/exportJarSteps/ResolveWorkspaceExecutor.ts new file mode 100644 index 00000000..7be4f968 --- /dev/null +++ b/src/exportJarSteps/ResolveWorkspaceExecutor.ts @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +import { Disposable, QuickPick, Uri, workspace } from "vscode"; +import { ExportJarStep, IStepMetadata } from "../exportJarFileCommand"; +import { Jdtls } from "../java/jdtls"; +import { INodeData } from "../java/nodeData"; +import { WorkspaceNode } from "../views/workspaceNode"; +import { IExportJarStepExecutor } from "./IExportJarStepExecutor"; +import { createPickBox, IJarQuickPickItem } from "./utility"; + +export class ResolveWorkspaceExecutor implements IExportJarStepExecutor { + + public async execute(stepMetadata: IStepMetadata): Promise { + await this.resolveWorkspaceFolder(stepMetadata, stepMetadata.entry); + stepMetadata.projectList = await Jdtls.getProjects(stepMetadata.workspaceUri.toString()); + if (stepMetadata.projectList === undefined) { + throw new Error("No project found. Please make sure your project folder is opened."); + } + return ExportJarStep.ResolveMainMethod; + } + + private async resolveWorkspaceFolder(stepMetadata: IStepMetadata, node?: INodeData): Promise { + if (node instanceof WorkspaceNode) { + stepMetadata.workspaceUri = Uri.parse(node.uri); + return true; + } + const folders = workspace.workspaceFolders; + // Guarded by workspaceFolderCount != 0 in package.json + if (folders.length === 1) { + stepMetadata.workspaceUri = Uri.parse(folders[0].uri.toString()); + return true; + } + const pickItems: IJarQuickPickItem[] = []; + for (const folder of folders) { + pickItems.push({ + label: folder.name, + description: folder.uri.fsPath, + uri: folder.uri.toString(), + }); + } + stepMetadata.isPickedWorkspace = true; + const disposables: Disposable[] = []; + let result: boolean = false; + try { + result = await new Promise((resolve, reject) => { + const pickBox = createPickBox("Export Jar : Determine project", "Select the project", pickItems, false); + disposables.push( + pickBox.onDidAccept(() => { + stepMetadata.workspaceUri = Uri.parse(pickBox.selectedItems[0].uri); + return resolve(true); + }), + pickBox.onDidHide(() => { + return reject(); + }), + ); + disposables.push(pickBox); + pickBox.show(); + }); + } finally { + for (const d of disposables) { + d.dispose(); + } + } + return result; + } +} diff --git a/src/exportJarSteps/utility.ts b/src/exportJarSteps/utility.ts new file mode 100644 index 00000000..0cc4d627 --- /dev/null +++ b/src/exportJarSteps/utility.ts @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +import { QuickInputButtons, QuickPick, QuickPickItem, window } from "vscode"; +import { IExportJarStepExecutor } from "./IExportJarStepExecutor"; + +export interface IJarQuickPickItem extends QuickPickItem { + uri?: string; + type?: string; +} + +export function createPickBox(title: string, placeholder: string, items: IJarQuickPickItem[], + backBtnEnabled: boolean, canSelectMany: boolean = false): QuickPick { + const pickBox = window.createQuickPick(); + pickBox.title = title; + pickBox.placeholder = placeholder; + pickBox.canSelectMany = canSelectMany; + pickBox.items = items; + pickBox.ignoreFocusOut = true; + pickBox.buttons = backBtnEnabled ? [(QuickInputButtons.Back)] : []; + return pickBox; +} diff --git a/src/java/jdtls.ts b/src/java/jdtls.ts index a1b60955..cd29bfcc 100644 --- a/src/java/jdtls.ts +++ b/src/java/jdtls.ts @@ -2,15 +2,16 @@ // Licensed under the MIT license. import { commands } from "vscode"; -import { Commands } from "../commands"; +import { Commands, executeJavaLanguageServerCommand } from "../commands"; +import { MainMethodInfo } from "../exportJarSteps/ResolveMainMethodExecutor"; import { INodeData } from "./nodeData"; export namespace Jdtls { - export function getProjects(params): Thenable { + export function getProjects(params: string): Thenable { return commands.executeCommand(Commands.EXECUTE_WORKSPACE_COMMAND, Commands.JAVA_PROJECT_LIST, params); } - export function refreshLibraries(params): Thenable { + export function refreshLibraries(params: string): Thenable { return commands.executeCommand(Commands.EXECUTE_WORKSPACE_COMMAND, Commands.JAVA_PROJECT_REFRESH_LIB_SERVER, params); } @@ -18,7 +19,26 @@ export namespace Jdtls { return commands.executeCommand(Commands.EXECUTE_WORKSPACE_COMMAND, Commands.JAVA_GETPACKAGEDATA, params); } - export function resolvePath(params): Thenable { + export function resolvePath(params: string): Thenable { return commands.executeCommand(Commands.EXECUTE_WORKSPACE_COMMAND, Commands.JAVA_RESOLVEPATH, params); } + + export function getMainMethod(params: string): Thenable { + return commands.executeCommand(Commands.EXECUTE_WORKSPACE_COMMAND, Commands.JAVA_PROJECT_GETMAINMETHOD, params); + } + + export function exportJar(mainMethod: string, elements: string[], destination: string): Thenable { + return commands.executeCommand(Commands.EXECUTE_WORKSPACE_COMMAND, Commands.JAVA_PROJECT_GENERATEJAR, mainMethod, elements, destination); + } + + export enum CompileWorkspaceStatus { + Failed = 0, + Succeed = 1, + Witherror = 2, + Cancelled = 3, + } + + export function resolveBuildFiles(): Promise { + return >executeJavaLanguageServerCommand(Commands.JAVA_RESOLVE_BUILD_FILES); + } } diff --git a/src/utility.ts b/src/utility.ts index 60973a31..8775e8ac 100644 --- a/src/utility.ts +++ b/src/utility.ts @@ -2,6 +2,7 @@ // Licensed under the MIT license. import { window, workspace, WorkspaceFolder } from "vscode"; +import { setUserError } from "vscode-extension-telemetry-wrapper"; export class Utility { @@ -13,9 +14,42 @@ export class Utility { return workspace.workspaceFolders[0]; } if (window.activeTextEditor) { - const activeWorkspaceFolder: WorkspaceFolder | undefined = workspace.getWorkspaceFolder(window.activeTextEditor.document.uri); + const activeWorkspaceFolder: WorkspaceFolder | undefined = + workspace.getWorkspaceFolder(window.activeTextEditor.document.uri); return activeWorkspaceFolder; } return undefined; } + +} + +export class UserError extends Error { + public context: ITroubleshootingMessage; + + constructor(context: ITroubleshootingMessage) { + super(context.message); + this.context = context; + setUserError(this); + } +} + +interface IProperties { + [key: string]: string; +} + +interface ILoggingMessage { + message: string; + type?: Type; + details?: IProperties; +} + +interface ITroubleshootingMessage extends ILoggingMessage { + anchor?: string; +} + +export enum Type { + EXCEPTION = "exception", + USAGEDATA = "usageData", + USAGEERROR = "usageError", + ACTIVATEEXTENSION = "activateExtension", // TODO: Activation belongs to usage data, remove this category. } diff --git a/src/views/dependencyDataProvider.ts b/src/views/dependencyDataProvider.ts index 449200cd..2948eb00 100644 --- a/src/views/dependencyDataProvider.ts +++ b/src/views/dependencyDataProvider.ts @@ -8,6 +8,7 @@ import { } from "vscode"; import { instrumentOperation, instrumentOperationAsVsCodeCommand } from "vscode-extension-telemetry-wrapper"; import { Commands } from "../commands"; +import { createJarFile } from "../exportJarFileCommand"; import { isStandardServerReady, isSwitchingServer } from "../extension"; import { Jdtls } from "../java/jdtls"; import { INodeData, NodeKind } from "../java/nodeData"; @@ -30,7 +31,8 @@ export class DependencyDataProvider implements TreeDataProvider { constructor(public readonly context: ExtensionContext) { context.subscriptions.push(commands.registerCommand(Commands.VIEW_PACKAGE_REFRESH, (debounce?: boolean) => this.refreshWithLog(debounce))); - context.subscriptions.push(instrumentOperationAsVsCodeCommand(Commands.VIEW_PACKAGE_REVEAL_FILE_OS, (node: INodeData) => + context.subscriptions.push(instrumentOperationAsVsCodeCommand(Commands.VIEW_PACKAGE_EXPORT_JAR, (node: INodeData) => createJarFile(node))); + context.subscriptions.push(instrumentOperationAsVsCodeCommand(Commands.VIEW_PACKAGE_REVEAL_FILE_OS, (node?: INodeData) => commands.executeCommand("revealFileInOS", Uri.parse(node.uri)))); context.subscriptions.push(instrumentOperationAsVsCodeCommand(Commands.VIEW_PACKAGE_COPY_FILE_PATH, (node: INodeData) => commands.executeCommand("copyFilePath", Uri.parse(node.uri)))); diff --git a/src/views/projectNode.ts b/src/views/projectNode.ts index 4511d133..3c309ea6 100644 --- a/src/views/projectNode.ts +++ b/src/views/projectNode.ts @@ -69,4 +69,7 @@ export class ProjectNode extends DataNode { protected get iconPath(): ThemeIcon { return new ThemeIcon("project"); } + protected get contextValue(): string { + return `project/${this.name}`; + } } diff --git a/src/views/workspaceNode.ts b/src/views/workspaceNode.ts index 78fd1085..a8826ce2 100644 --- a/src/views/workspaceNode.ts +++ b/src/views/workspaceNode.ts @@ -30,4 +30,7 @@ export class WorkspaceNode extends DataNode { protected get iconPath(): ThemeIcon { return new ThemeIcon("root-folder"); } + protected get contextValue(): string { + return `workspace/${this.name}`; + } }