diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/build.properties b/eclipse-language-servers/org.springframework.tooling.boot.ls/build.properties index 14cd24533..1c52b90e3 100644 --- a/eclipse-language-servers/org.springframework.tooling.boot.ls/build.properties +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/build.properties @@ -6,4 +6,5 @@ bin.includes = META-INF/,\ servers/,\ syntaxes/,\ icons/,\ - about.html + about.html,\ + jars/ diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/plugin.xml b/eclipse-language-servers/org.springframework.tooling.boot.ls/plugin.xml index 4166d63e2..7a7bca761 100644 --- a/eclipse-language-servers/org.springframework.tooling.boot.ls/plugin.xml +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/plugin.xml @@ -355,5 +355,14 @@ class="org.springframework.tooling.boot.ls.prefs.RemoteAppsFromPrefsDataContributor"> + + + + + + diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/pom.xml b/eclipse-language-servers/org.springframework.tooling.boot.ls/pom.xml index 010b7f622..d282de961 100644 --- a/eclipse-language-servers/org.springframework.tooling.boot.ls/pom.xml +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/pom.xml @@ -43,6 +43,33 @@ ${project.build.directory}/../servers/spring-boot-language-server + + xml-extension + prepare-package + + copy + + + + + org.springframework.ide.vscode + xml-ls-extension + ${project.version} + true + + + org.springframework.ide.vscode + commons-lsp-extensions + ${project.version} + true + + + true + true + true + ${project.build.directory}/../jars + + @@ -71,6 +98,9 @@ servers + + jars + diff --git a/eclipse-language-servers/org.springframework.tooling.ls.eclipse.commons/plugin.xml b/eclipse-language-servers/org.springframework.tooling.ls.eclipse.commons/plugin.xml index 4f03d4551..0694f9d69 100644 --- a/eclipse-language-servers/org.springframework.tooling.ls.eclipse.commons/plugin.xml +++ b/eclipse-language-servers/org.springframework.tooling.ls.eclipse.commons/plugin.xml @@ -91,6 +91,10 @@ class="org.springframework.tooling.ls.eclipse.commons.commands.OpenResourceInEditor" commandId="org.springframework.tooling.ls.eclipse.commons.commands.OpenResourceInEditor"> + + diff --git a/eclipse-language-servers/org.springframework.tooling.ls.eclipse.commons/src/org/springframework/tooling/ls/eclipse/commons/STS4LanguageClientImpl.java b/eclipse-language-servers/org.springframework.tooling.ls.eclipse.commons/src/org/springframework/tooling/ls/eclipse/commons/STS4LanguageClientImpl.java index c7bcda1f9..5d3b0a6cd 100644 --- a/eclipse-language-servers/org.springframework.tooling.ls.eclipse.commons/src/org/springframework/tooling/ls/eclipse/commons/STS4LanguageClientImpl.java +++ b/eclipse-language-servers/org.springframework.tooling.ls.eclipse.commons/src/org/springframework/tooling/ls/eclipse/commons/STS4LanguageClientImpl.java @@ -93,7 +93,7 @@ import com.google.common.collect.ImmutableMap; @SuppressWarnings("restriction") public class STS4LanguageClientImpl extends LanguageClientImpl implements STS4LanguageClient { - private static ReusableClasspathListenerHandler classpathService = new ReusableClasspathListenerHandler ( + public static final ReusableClasspathListenerHandler CLASSPATH_SERVICE = new ReusableClasspathListenerHandler ( Logger.forEclipsePlugin(LanguageServerCommonsActivator::getInstance), new LSP4ECommandExecutor(), () -> new ProjectSorter() @@ -371,7 +371,7 @@ public class STS4LanguageClientImpl extends LanguageClientImpl implements STS4La public STS4LanguageClientImpl() { - classpathService.addNotificationsSentCallback(projectNames -> { + CLASSPATH_SERVICE.addNotificationsSentCallback(projectNames -> { List projects = projectNames.stream().map(projectName -> ResourcesPlugin.getWorkspace().getRoot().getProject(projectName)).filter(Objects::nonNull).collect(Collectors.toList()); for (IWorkbenchWindow ww : PlatformUI.getWorkbench().getWorkbenchWindows()) { for (IWorkbenchPage page : ww.getPages()) { @@ -397,12 +397,12 @@ public class STS4LanguageClientImpl extends LanguageClientImpl implements STS4La @Override public CompletableFuture addClasspathListener(ClasspathListenerParams params) { - return CompletableFuture.completedFuture(classpathService.addClasspathListener(params.getCallbackCommandId(), params.isBatched())); + return CompletableFuture.completedFuture(CLASSPATH_SERVICE.addClasspathListener(params.getCallbackCommandId(), params.isBatched())); } @Override public CompletableFuture removeClasspathListener(ClasspathListenerParams params) { - return CompletableFuture.completedFuture(classpathService.removeClasspathListener(params.getCallbackCommandId())); + return CompletableFuture.completedFuture(CLASSPATH_SERVICE.removeClasspathListener(params.getCallbackCommandId())); } @Override diff --git a/eclipse-language-servers/org.springframework.tooling.ls.eclipse.commons/src/org/springframework/tooling/ls/eclipse/commons/commands/JavaWorkspaceCommandHanlder.java b/eclipse-language-servers/org.springframework.tooling.ls.eclipse.commons/src/org/springframework/tooling/ls/eclipse/commons/commands/JavaWorkspaceCommandHanlder.java new file mode 100644 index 000000000..ad22b81bb --- /dev/null +++ b/eclipse-language-servers/org.springframework.tooling.ls.eclipse.commons/src/org/springframework/tooling/ls/eclipse/commons/commands/JavaWorkspaceCommandHanlder.java @@ -0,0 +1,38 @@ +package org.springframework.tooling.ls.eclipse.commons.commands; + +import java.util.List; + +import org.eclipse.core.commands.ExecutionEvent; +import org.eclipse.core.commands.ExecutionException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.lsp4e.command.LSPCommandHandler; +import org.eclipse.lsp4j.Command; +import org.springframework.tooling.ls.eclipse.commons.STS4LanguageClientImpl; + +public class JavaWorkspaceCommandHanlder extends LSPCommandHandler { + + public static final String JAVA_WORKSPACE_COMMAND = "java.execute.workspaceCommand"; + + public static final String ADD_CLASSPATH_LISTENER_COMMAND = "sts.java.addClasspathListener"; + public static final String REMOVE_CLASSPATH_LISTENER_COMMAND = "sts.java.removeClasspathListener"; + + @Override + public Object execute(ExecutionEvent event, Command command, IPath path) throws ExecutionException { + if (JAVA_WORKSPACE_COMMAND.equals(command.getCommand())) { + String cmdId = (String) command.getArguments().get(0); + List arguments = command.getArguments().subList(1, command.getArguments().size()); + + switch (cmdId) { + case ADD_CLASSPATH_LISTENER_COMMAND: + String callbackCommandIdForAdd = (String) arguments.get(0); + Boolean batched = (Boolean) arguments.get(1); + return STS4LanguageClientImpl.CLASSPATH_SERVICE.addClasspathListener(callbackCommandIdForAdd, batched); + case REMOVE_CLASSPATH_LISTENER_COMMAND: + String callbackCommandIdForRemove = (String) arguments.get(0); + return STS4LanguageClientImpl.CLASSPATH_SERVICE.removeClasspathListener(callbackCommandIdForRemove); + } + } + return null; + } + +} diff --git a/eclipse-language-servers/pom.xml b/eclipse-language-servers/pom.xml index cc0b7ded5..bb920478d 100644 --- a/eclipse-language-servers/pom.xml +++ b/eclipse-language-servers/pom.xml @@ -165,6 +165,11 @@ p2 https://download.eclipse.org/tm4e/snapshots/ + + wwd + p2 + https://download.eclipse.org/wildwebdeveloper/releases/latest/ + @@ -210,6 +215,11 @@ p2 https://download.eclipse.org/tm4e/snapshots/ + + wwd + p2 + https://download.eclipse.org/wildwebdeveloper/releases/latest/ + @@ -260,6 +270,11 @@ p2 https://download.eclipse.org/tm4e/snapshots/ + + wwd + p2 + https://download.eclipse.org/wildwebdeveloper/releases/latest/ + diff --git a/headless-services/jdt-ls-extension/org.springframework.tooling.jdt.ls.commons.test/pom.xml b/headless-services/jdt-ls-extension/org.springframework.tooling.jdt.ls.commons.test/pom.xml index e386fb98a..747d67b54 100644 --- a/headless-services/jdt-ls-extension/org.springframework.tooling.jdt.ls.commons.test/pom.xml +++ b/headless-services/jdt-ls-extension/org.springframework.tooling.jdt.ls.commons.test/pom.xml @@ -40,6 +40,23 @@ target-platform-configuration ${tycho-version} + + + win32 + win32 + x86_64 + + + linux + gtk + x86_64 + + + macosx + cocoa + x86_64 + + diff --git a/headless-services/pom.xml b/headless-services/pom.xml index f2bbf7d63..10cdde31e 100644 --- a/headless-services/pom.xml +++ b/headless-services/pom.xml @@ -16,6 +16,7 @@ spring-boot-language-server bosh-language-server jdt-ls-extension + xml-ls-extension diff --git a/headless-services/spring-boot-language-server/build.sh b/headless-services/spring-boot-language-server/build.sh index 920834b31..8ce95dd2b 100755 --- a/headless-services/spring-boot-language-server/build.sh +++ b/headless-services/spring-boot-language-server/build.sh @@ -18,3 +18,10 @@ else -am \ clean install fi +cd ../xml-ls-extension + ../mvnw \ + -DtrimStackTrace=false \ + -f ../pom.xml \ + -pl xml-ls-extension \ + -am \ + clean install diff --git a/headless-services/xml-ls-extension/pom.xml b/headless-services/xml-ls-extension/pom.xml new file mode 100644 index 000000000..a62495aff --- /dev/null +++ b/headless-services/xml-ls-extension/pom.xml @@ -0,0 +1,97 @@ + + 4.0.0 + xml-ls-extension + xml-ls-extension + Lemminx XML Language Server Extension for contributing project classpath specific NSURI resolution + + + org.springframework.ide.vscode + commons-parent + 1.23.0-SNAPSHOT + ../commons/pom.xml + + + + ${project.version} + 0.14.0-SNAPSHOT + + + + + lemminx-snapshots + https://repo.eclipse.org/content/repositories/lemminx-snapshots/ + + false + + + true + + + + + + + org.eclipse.lsp4j + org.eclipse.lsp4j + ${lsp4j-version} + + + org.eclipse.lemminx + org.eclipse.lemminx + ${lsp4xml.version} + + + org.eclipse.lsp4j + org.eclipse.lsp4j + + + org.eclipse.lsp4j + org.eclipse.lsp4j.jsonrpc + + + + + org.springframework.ide.vscode + commons-lsp-extensions + ${project.version} + + + junit + junit + test + + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-dependencies + package + + copy-dependencies + + + true + ${project.build.directory}/dependencies + true + true + + + + + + + org.apache.felix + maven-bundle-plugin + 6.0.3 + + + + \ No newline at end of file diff --git a/headless-services/xml-ls-extension/src/main/java/org/springframework/ide/vscode/xml/IJavaProjectProvider.java b/headless-services/xml-ls-extension/src/main/java/org/springframework/ide/vscode/xml/IJavaProjectProvider.java new file mode 100644 index 000000000..17f3378f9 --- /dev/null +++ b/headless-services/xml-ls-extension/src/main/java/org/springframework/ide/vscode/xml/IJavaProjectProvider.java @@ -0,0 +1,48 @@ +package org.springframework.ide.vscode.xml; + +import java.util.Collection; +import java.util.function.Consumer; + +import org.springframework.ide.vscode.commons.protocol.java.Classpath; + +public interface IJavaProjectProvider { + + interface IJavaProjectData { + + String getName(); + + String getUri(); + + Classpath getClasspath(); + + } + + Collection all(); + + IJavaProjectData get(String name); + + void addListener(Consumer listener); + + void removeListener(Consumer listener); + + default IJavaProjectData findProject(String fileUri) { + IJavaProjectData bestMatch = null; + for (IJavaProjectData p : all()) { + if (fileUri.startsWith(p.getUri())) { + if (bestMatch == null) { + bestMatch = p; + } else { + if (bestMatch.getUri().length() < p.getUri().length()) { + bestMatch = p; + } + } + } + } + return bestMatch; + } + + default boolean exists(String name) { + return get(name) != null; + } + +} diff --git a/headless-services/xml-ls-extension/src/main/java/org/springframework/ide/vscode/xml/JavaProjectCache.java b/headless-services/xml-ls-extension/src/main/java/org/springframework/ide/vscode/xml/JavaProjectCache.java new file mode 100644 index 000000000..ca5cf430c --- /dev/null +++ b/headless-services/xml-ls-extension/src/main/java/org/springframework/ide/vscode/xml/JavaProjectCache.java @@ -0,0 +1,239 @@ +package org.springframework.ide.vscode.xml; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.Consumer; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.eclipse.lemminx.services.IXMLDocumentProvider; +import org.eclipse.lemminx.services.IXMLValidationService; +import org.eclipse.lemminx.services.extensions.commands.IXMLCommandService; +import org.eclipse.lsp4j.ExecuteCommandParams; +import org.springframework.ide.vscode.commons.protocol.java.Classpath; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; + +class JavaProjectCache implements IJavaProjectProvider { + + private static Logger LOGGER = Logger.getLogger(JavaProjectCache.class.getName()); + + private class JavaProjectData implements IJavaProjectData { + + private String name; + private String uri; + private Classpath classpath; + + public JavaProjectData(String name, String uri, Classpath classpath) { + super(); + this.name = name; + this.uri = uri; + this.classpath = classpath; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getUri() { + return uri; + } + + @Override + public Classpath getClasspath() { + return classpath; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + getEnclosingInstance().hashCode(); + result = prime * result + ((classpath == null) ? 0 : classpath.hashCode()); + result = prime * result + ((name == null) ? 0 : name.hashCode()); + result = prime * result + ((uri == null) ? 0 : uri.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + JavaProjectData other = (JavaProjectData) obj; + if (!getEnclosingInstance().equals(other.getEnclosingInstance())) + return false; + if (classpath == null) { + if (other.classpath != null) + return false; + } else if (!classpath.equals(other.classpath)) + return false; + if (name == null) { + if (other.name != null) + return false; + } else if (!name.equals(other.name)) + return false; + if (uri == null) { + if (other.uri != null) + return false; + } else if (!uri.equals(other.uri)) + return false; + return true; + } + + private JavaProjectCache getEnclosingInstance() { + return JavaProjectCache.this; + } + + } + + private static final String JAVA_EXECUTE_WORKSPACE_COMMAND = "java.execute.workspaceCommand"; + + private static final Gson gson = new Gson(); + + private static final long CLASSPATH_TIMEOUT = 30_000; + + private final Map projectsCache = new ConcurrentHashMap<>(); + private final String callbackCommandId; + + private List> listeners = new ArrayList<>(); + + private final IXMLCommandService commandService; + + private final IXMLDocumentProvider documentProvider; + + private final IXMLValidationService validationService; + + public JavaProjectCache(IXMLCommandService commandService, IXMLDocumentProvider documentProvider, IXMLValidationService validationService) { + this.callbackCommandId = UUID.randomUUID().toString(); + this.commandService = commandService; + this.documentProvider = documentProvider; + this.validationService = validationService; + } + + void start() { + // Register handler for the classpath change callback command + commandService.registerCommand(callbackCommandId, (params, cancelChecker) -> handleClasspathChanged(params)); + + // Register classpath listener by executing the command below + final ExecuteCommandParams execCmdParams = new ExecuteCommandParams(JAVA_EXECUTE_WORKSPACE_COMMAND, Arrays.asList("sts.java.addClasspathListener", callbackCommandId, true)); + + // Keep trying to register classpath listener until success or timeout + new Thread() { + @Override + public void run() { + Object result = null; + long startTime = System.currentTimeMillis(); + while (result == null && System.currentTimeMillis() - startTime < CLASSPATH_TIMEOUT) { + CompletableFuture clientCommand = commandService.executeClientCommand(execCmdParams); + try { + result = clientCommand.get(1000, TimeUnit.MILLISECONDS); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + clientCommand.cancel(true); + try { + Thread.sleep(1000); + } catch (InterruptedException e1) { + // ignore + } + } + } + } + }.start(); + + } + + private String handleClasspathChanged(ExecuteCommandParams callbackParams) { + List args = callbackParams.getArguments(); + //Args are deserialized as com.google.gson.JsonElements. + List projectUris = new ArrayList<>(); + if (((JsonElement) args.get(0)).isJsonArray()) { + // If events are batched... then they will arrive as a array of arrays. + for (Object arg : args) { + JsonArray event = (JsonArray) arg; + String projectUri = event.get(0).getAsString(); + String name = event.get(1).getAsString(); + boolean deleted = event.get(2).getAsBoolean(); + Classpath classpath = gson.fromJson((JsonElement)event.get(3), Classpath.class); + + projectUris.add(projectUri); + updateProject(name, projectUri, classpath, deleted); + } + } else { + //Still support non-batched events for backwards compatibility with clients + // that don't provide batched event support (e.g. IDEA client may only adopt this + // later, or not adopt it at all). + String projectUri = ((JsonElement) args.get(0)).getAsString(); + String name = ((JsonElement) args.get(1)).getAsString(); + boolean deleted = ((JsonElement)args.get(2)).getAsBoolean(); + Classpath classpath = gson.fromJson((JsonElement)args.get(3), Classpath.class); + + projectUris.add(projectUri); + updateProject(name, projectUri, classpath, deleted); + } + + documentProvider.getAllDocuments().stream() + .filter(dm -> projectUris.stream().filter(uri -> dm.getTextDocument().getUri().startsWith(uri)).findFirst().isPresent()) + .forEach(dm -> validationService.validate(dm)); + + return "done"; + } + + void stop() { + ExecuteCommandParams execCmdParams = new ExecuteCommandParams(JAVA_EXECUTE_WORKSPACE_COMMAND, Arrays.asList("sts.java.removeClasspathListener", callbackCommandId)); + commandService.executeClientCommand(execCmdParams); + commandService.unregisterCommand(callbackCommandId); + } + + private void updateProject(String name, String projectUri, Classpath classpath, boolean deleted) { + JavaProjectData project = new JavaProjectData(name, projectUri, classpath); + if (deleted) { + this.projectsCache.remove(project.getName()); + } else { + this.projectsCache.put(name, project); + } + for (Consumer l : listeners) { + try { + l.accept(project); + } catch (Exception e) { + LOGGER.log(Level.SEVERE, e, null); + } + } + } + + @Override + public Collection all() { + return projectsCache.values(); + } + + @Override + public IJavaProjectData get(String name) { + return projectsCache.get(name); + } + + @Override + public void addListener(Consumer listener) { + listeners.add(listener); + } + + @Override + public void removeListener(Consumer listener) { + listeners.remove(listener); + } + +} diff --git a/headless-services/xml-ls-extension/src/main/java/org/springframework/ide/vscode/xml/SpringXmlPlugin.java b/headless-services/xml-ls-extension/src/main/java/org/springframework/ide/vscode/xml/SpringXmlPlugin.java new file mode 100644 index 000000000..f30ba28f8 --- /dev/null +++ b/headless-services/xml-ls-extension/src/main/java/org/springframework/ide/vscode/xml/SpringXmlPlugin.java @@ -0,0 +1,33 @@ +package org.springframework.ide.vscode.xml; + +import java.net.URL; + +import org.eclipse.lemminx.services.extensions.IXMLExtension; +import org.eclipse.lemminx.services.extensions.XMLExtensionsRegistry; +import org.eclipse.lsp4j.InitializeParams; +import org.springframework.ide.vscode.xml.namespaces.ProjectAwareUrlStreamHandlerFactory; +import org.springframework.ide.vscode.xml.namespaces.ProjectClasspathUriResolverExtension; +import org.springframework.ide.vscode.xml.namespaces.classpath.ProjectResourceLoaderCache; + +public class SpringXmlPlugin implements IXMLExtension { + + private JavaProjectCache javaProjectCache; + + @Override + public void start(InitializeParams params, XMLExtensionsRegistry registry) { + javaProjectCache = new JavaProjectCache(registry.getCommandService(), registry.getDocumentProvider(), registry.getValidationService()); + javaProjectCache.start(); + + ProjectResourceLoaderCache loaderCache = new ProjectResourceLoaderCache(javaProjectCache); + + URL.setURLStreamHandlerFactory(new ProjectAwareUrlStreamHandlerFactory(javaProjectCache, loaderCache)); + + registry.getResolverExtensionManager().registerResolver(new ProjectClasspathUriResolverExtension(javaProjectCache, loaderCache)); + } + + @Override + public void stop(XMLExtensionsRegistry registry) { + javaProjectCache.stop(); + } + +} diff --git a/headless-services/xml-ls-extension/src/main/java/org/springframework/ide/vscode/xml/namespaces/ProjectAwareUrlStreamHandlerFactory.java b/headless-services/xml-ls-extension/src/main/java/org/springframework/ide/vscode/xml/namespaces/ProjectAwareUrlStreamHandlerFactory.java new file mode 100644 index 000000000..3d102d0cd --- /dev/null +++ b/headless-services/xml-ls-extension/src/main/java/org/springframework/ide/vscode/xml/namespaces/ProjectAwareUrlStreamHandlerFactory.java @@ -0,0 +1,70 @@ +package org.springframework.ide.vscode.xml.namespaces; + +import java.io.IOException; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandler; +import java.net.URLStreamHandlerFactory; + +import org.springframework.ide.vscode.xml.IJavaProjectProvider; +import org.springframework.ide.vscode.xml.IJavaProjectProvider.IJavaProjectData; +import org.springframework.ide.vscode.xml.namespaces.classpath.ProjectResourceLoaderCache; +import org.springframework.ide.vscode.xml.namespaces.classpath.ResourceLoader; + +public class ProjectAwareUrlStreamHandlerFactory implements URLStreamHandlerFactory { + + public static final String PROJECT_AWARE_PROTOCOL = "project-aware"; + public static final String PROJECT_AWARE_PROTOCOL_HEADER = PROJECT_AWARE_PROTOCOL + "://"; + + private IJavaProjectProvider javaProjectProvider; + private ProjectResourceLoaderCache loaderCache; + + public ProjectAwareUrlStreamHandlerFactory(IJavaProjectProvider javaProjectProvider, ProjectResourceLoaderCache loaderCache) { + this.javaProjectProvider = javaProjectProvider; + this.loaderCache = loaderCache; + } + + @Override + public URLStreamHandler createURLStreamHandler(String protocol) { + if (PROJECT_AWARE_PROTOCOL.equals(protocol)) { + return new URLStreamHandler() { + + @Override + protected URLConnection openConnection(URL u) throws IOException { + String systemId = u.toString(); + String nameAndLocation = systemId.substring(PROJECT_AWARE_PROTOCOL_HEADER.length()); + String projectName = nameAndLocation.substring(0, nameAndLocation.indexOf('/')); + IJavaProjectData project = javaProjectProvider.get(projectName); + String resourceId = nameAndLocation.substring(nameAndLocation.indexOf('/') + 1); + ResourceLoader cl = loaderCache.getResourceLoader(project, null); + URL resource = cl.getResource(resourceId); + if (resource != null) { + return resource.openConnection(); + } + return null; + } + }; + } + return null; + } + + /** + * Creates a string representation of a project-aware protocol URL from + * project name and a resource name + * + * @param projectName + * spring project name + * @param resourceName + * class loader resource name + * @return URL string + */ + public static String createProjectAwareUrl(String projectName, String resourceName) { + StringBuilder sb = new StringBuilder(); + sb.append(PROJECT_AWARE_PROTOCOL_HEADER); + sb.append(projectName); + sb.append('/'); + sb.append(resourceName); + return sb.toString(); + } + +} diff --git a/headless-services/xml-ls-extension/src/main/java/org/springframework/ide/vscode/xml/namespaces/ProjectClasspathUriResolver.java b/headless-services/xml-ls-extension/src/main/java/org/springframework/ide/vscode/xml/namespaces/ProjectClasspathUriResolver.java new file mode 100644 index 000000000..758bfad7a --- /dev/null +++ b/headless-services/xml-ls-extension/src/main/java/org/springframework/ide/vscode/xml/namespaces/ProjectClasspathUriResolver.java @@ -0,0 +1,236 @@ +package org.springframework.ide.vscode.xml.namespaces; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.springframework.ide.vscode.xml.IJavaProjectProvider.IJavaProjectData; +import org.springframework.ide.vscode.xml.namespaces.classpath.ProjectResourceLoaderCache; +import org.springframework.ide.vscode.xml.namespaces.classpath.PropertiesLoaderUtils; +import org.springframework.ide.vscode.xml.namespaces.classpath.ResourceLoader; +import org.springframework.ide.vscode.xml.namespaces.model.NamespaceDefinition; +import org.springframework.ide.vscode.xml.namespaces.util.TargetNamespaceScanner; + +/** + * resolves URIs on the project classpath using the protocol established by + * spring.schemas files. + * + * @author Martin Lippert + * @since 2.7.0 + */ +public class ProjectClasspathUriResolver { + + private static Logger LOGGER = Logger.getLogger(ProjectClasspathUriResolver.class.getName()); + + private static final String DEFAULT_SCHEMA_MAPPINGS_LOCATION = "META-INF/spring.schemas"; + + + private final IJavaProjectData project; + private boolean disableCaching; + + private Map typePublic; + private Map typeUri; + private Map schemaMappings; + private ProjectResourceLoaderCache loaderCache; + + public ProjectClasspathUriResolver(ProjectResourceLoaderCache loaderCache, IJavaProjectData project, boolean disableCaching) { + this.loaderCache = loaderCache; + this.project = project; + this.disableCaching = disableCaching; + if (!disableCaching) { + init(); + } + } + + /** + * Resolves the given systemId on the classpath configured by + * the file's project. + */ + public String resolveOnClasspath(String publicId, String systemId) { + if (disableCaching) { + return resolveOnClasspathAndSourceFolders(publicId, systemId); + } + return resolveOnClasspathOnly(publicId, systemId); + } + + private String resolveOnClasspathAndSourceFolders(String publicId, + String systemId) { + ResourceLoader classLoader = loaderCache.getResourceLoader(project, null); + Map mappings = getSchemaMappings(classLoader); + + if (mappings != null && systemId != null) { + if (mappings.containsKey(systemId)) { + String xsdPath = mappings.get(systemId); + return resolveXsdPathOnClasspath(xsdPath, classLoader); + } + else if (mappings.containsKey(systemId.replace("https://", "http://"))) { + String xsdPath = mappings.get(systemId.replace("https://", "http://")); + return resolveXsdPathOnClasspath(xsdPath, classLoader); + } + else if (mappings.containsKey(systemId.replace("http://", "https://"))) { + String xsdPath = mappings.get(systemId.replace("http://", "https://")); + return resolveXsdPathOnClasspath(xsdPath, classLoader); + } + } + + return null; + } + + private String resolveOnClasspathOnly(String publicId, String systemId) { + String resolved = null; + + if (systemId != null) { + resolved = typeUri.get(systemId); + } + + if (resolved == null && systemId != null && systemId.startsWith("https://")) { + resolved = typeUri.get(systemId.replace("https://", "http://")); + } + + if (resolved == null && systemId != null && systemId.startsWith("http://")) { + resolved = typeUri.get(systemId.replace("http://", "https://")); + } + + if (resolved == null && publicId != null) { + if (!(systemId != null && systemId.endsWith(".xsd"))) { + resolved = typePublic.get(publicId); + } + } + return resolved; + } + + private void init() { + this.typePublic = new ConcurrentHashMap(); + this.typeUri = new ConcurrentHashMap(); + + Map namespaceDefinitionRegistry = new HashMap(); + ResourceLoader classLoader = loaderCache.getResourceLoader(project, null); + + schemaMappings = getSchemaMappings(classLoader); + if (schemaMappings != null) { + for (String key : schemaMappings.keySet()) { + String path = schemaMappings.get(key); + + // add the resolved path to the list of uris + String resolvedPath = resolveXsdPathOnClasspath(path, + classLoader); + if (resolvedPath != null) { + typeUri.put(key, resolvedPath); + + // collect base information to later extract the default uri + String namespaceUri = getTargetNamespace(resolvedPath); + + if (namespaceDefinitionRegistry.containsKey(namespaceUri)) { + namespaceDefinitionRegistry.get(namespaceUri) + .addSchemaLocation(key); + namespaceDefinitionRegistry.get(namespaceUri).addUri( + path); + } else { + NamespaceDefinition namespaceDefinition = new NamespaceDefinition( + null); + namespaceDefinition.addSchemaLocation(key); + namespaceDefinition.setNamespaceUri(namespaceUri); + namespaceDefinition.addUri(path); + namespaceDefinitionRegistry.put(namespaceUri, + namespaceDefinition); + } + } + } + + // Add catalog entry to namespace uri + for (NamespaceDefinition definition : namespaceDefinitionRegistry + .values()) { + String namespaceKey = definition.getNamespaceUri(); + String defaultUri = definition.getDefaultUri(); + + String resolvedPath = resolveXsdPathOnClasspath(defaultUri, + classLoader); + if (resolvedPath != null) { + typePublic.put(namespaceKey, resolvedPath); + } + } + + } + } + + /** + * Returns the target namespace URI of the XSD identified by the given + * resolvedPath. + */ + private String getTargetNamespace(String resolvedPath) { + if (resolvedPath == null) { + return null; + } + + try { + URL url = new URI( + ProjectAwareUrlStreamHandlerFactory.createProjectAwareUrl( + project.getName(), resolvedPath)).toURL(); + return TargetNamespaceScanner.getTargetNamespace(url); + } catch (URISyntaxException | IOException e) { + LOGGER.log(Level.WARNING, e, null); + } + return null; + } + + /** + * Loads all schema mappings from all spring.schemas files on + * the project classpath. + * + * @param classLoader + * The classloader that is used to load the properties + */ + private Map getSchemaMappings(ResourceLoader classLoader) { + Map handlerMappings = new ConcurrentHashMap(); +// try { + Properties mappings = PropertiesLoaderUtils + .loadAllProperties( + DEFAULT_SCHEMA_MAPPINGS_LOCATION, + classLoader); + mergePropertiesIntoMap(mappings, handlerMappings); +// } catch (IOException ex) { +// // We can ignore this as we simply don't find the xsd file then. +// } + return handlerMappings; + } + + @SuppressWarnings("unchecked") + private static void mergePropertiesIntoMap(/*@Nullable*/ Properties props, Map map) { + if (props != null) { + for (Enumeration en = props.propertyNames(); en.hasMoreElements();) { + String key = (String) en.nextElement(); + Object value = props.get(key); + if (value == null) { + // Allow for defaults fallback or potentially overridden accessor... + value = props.getProperty(key); + } + map.put((K) key, (V) value); + } + } + } + + + private String resolveXsdPathOnClasspath(String xsdPath, ResourceLoader classLoader) { + URL url = classLoader.getResource(xsdPath); + + // fallback, if schema location starts with / and therefore fails to be + // found by classloader + if (url == null && xsdPath.startsWith("/")) { + xsdPath = xsdPath.substring(1); + url = classLoader.getResource(xsdPath); + } + + return url == null ? null : xsdPath; + } + + + +} diff --git a/headless-services/xml-ls-extension/src/main/java/org/springframework/ide/vscode/xml/namespaces/ProjectClasspathUriResolverExtension.java b/headless-services/xml-ls-extension/src/main/java/org/springframework/ide/vscode/xml/namespaces/ProjectClasspathUriResolverExtension.java new file mode 100644 index 000000000..c325c56f0 --- /dev/null +++ b/headless-services/xml-ls-extension/src/main/java/org/springframework/ide/vscode/xml/namespaces/ProjectClasspathUriResolverExtension.java @@ -0,0 +1,222 @@ +package org.springframework.ide.vscode.xml.namespaces; + +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.FutureTask; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; + +import org.eclipse.lemminx.uriresolver.URIResolverExtension; +import org.springframework.ide.vscode.commons.protocol.java.Classpath; +import org.springframework.ide.vscode.commons.protocol.java.Classpath.CPE; +import org.springframework.ide.vscode.xml.IJavaProjectProvider; +import org.springframework.ide.vscode.xml.IJavaProjectProvider.IJavaProjectData; +import org.springframework.ide.vscode.xml.namespaces.classpath.ProjectResourceLoaderCache; +import org.springframework.ide.vscode.xml.namespaces.util.DocumentAccessor; +import org.springframework.ide.vscode.xml.namespaces.util.DocumentAccessor.SchemaLocations; +import org.w3c.dom.Document; + +public class ProjectClasspathUriResolverExtension implements URIResolverExtension { + + private IJavaProjectProvider javaProjectProvider; + + private ConcurrentMap> projectResolvers = + new ConcurrentHashMap>(); + + private ProjectResourceLoaderCache loaderCache; + + final private Consumer projectListener = project -> { + projectResolvers.remove(project); + + List projectSources = project.getClasspath().getEntries().stream() + .filter(cpe -> Classpath.isProjectSource(cpe)) + .map(cpe -> cpe.getPath()) + .collect(Collectors.toList()); + + for (IJavaProjectData javaProject : projectResolvers.keySet()) { + if (javaProject != null) { + for (CPE cpe : javaProject.getClasspath().getEntries()) { + if (Classpath.isSource(cpe) && !cpe.isOwn() && !cpe.isSystem() && !cpe.isTest()) { + if (projectSources.contains(cpe.getPath())) { + projectResolvers.remove(javaProject); + break; + } + } + } + } + } + }; + + public ProjectClasspathUriResolverExtension(IJavaProjectProvider javaProjectProvider, ProjectResourceLoaderCache loaderCache) { + this.javaProjectProvider = javaProjectProvider; + this.loaderCache = loaderCache; + + javaProjectProvider.addListener(projectListener); + } + + @Override + public String resolve(String file, String publicId, String systemId) { + System.out.println("BaseLocation=" + file + " publicId=" + publicId + " systemId=" + systemId); + + // systemId is already resolved; so don't touch + if (systemId != null && systemId.startsWith("jar:")) { + return null; + } + + // identify the correct project + IJavaProjectData project = null; + + if (file != null) { + if (file.startsWith(ProjectAwareUrlStreamHandlerFactory.PROJECT_AWARE_PROTOCOL_HEADER)) { + String nameAndLocation = file + .substring(ProjectAwareUrlStreamHandlerFactory.PROJECT_AWARE_PROTOCOL_HEADER + .length()); + String projectName = nameAndLocation.substring(0, nameAndLocation.indexOf('/')); + project = javaProjectProvider.get(projectName); + } else { + project = getBestMatchingProject(file); + } + } + + if (project == null) { + return null; + } + + if (systemId == null && file != null) { + systemId = findSystemIdFromFile(file, publicId); + } + + if (systemId == null && publicId == null) { + return null; + } + + ProjectClasspathUriResolver resolver = getProjectResolver(file, project); + if (resolver != null) { + String resolved = resolver.resolveOnClasspath(publicId, systemId); + if (resolved != null) { + resolved = ProjectAwareUrlStreamHandlerFactory.createProjectAwareUrl(project.getName(), resolved); + } + return resolved; + } + + + return null; + } + + private boolean isCachingDisabled() { + // TODO Auto-generated method stub + return false; + } + + private ProjectClasspathUriResolver getProjectResolver(String fileUrl, final IJavaProjectData project) { + +// if (!XmlNamespaceUtils.useNamespacesFromClasspath(project)) { +// return null; +// } + + if (fileUrl != null && !fileUrl.endsWith(".xml") && !fileUrl.endsWith(".xsd")) { + return null; + } + + // Special case for 'pom.xml'. We can skip it entirely because it is not used to define spring beans. + // Also... m2e apparantly causes this to be called directly from the UI thread causing major hangs / annoyance. + // See: https://github.com/spring-projects/sts4/issues/318 + if (fileUrl != null && fileUrl.endsWith("pom.xml")) { + return null; + } + + while (true) { + Future future = projectResolvers.get(project); + if (future == null) { + + Callable createResolver = new Callable() { + public ProjectClasspathUriResolver call() throws InterruptedException { + ProjectClasspathUriResolver resolver = new ProjectClasspathUriResolver(loaderCache, project, isCachingDisabled()); + return resolver; + } + + }; + + FutureTask futureTask = new FutureTask(createResolver); + future = projectResolvers.putIfAbsent(project, futureTask); + if (future == null) { + future = futureTask; + futureTask.run(); + } + } + + try { + return future.get(); + } + catch (CancellationException e) { + projectResolvers.remove(project, future); + return null; + } + catch (ExecutionException e) { + return null; + } catch (InterruptedException e) { + return null; + } + } + + } + + private IJavaProjectData getBestMatchingProject(String file) { + try { + String fileUri = new URL(file).toURI().toString(); + return javaProjectProvider.findProject(fileUri); + } catch (MalformedURLException | URISyntaxException e) { + throw new IllegalStateException(e); + } + } + + private String findSystemIdFromFile(String file, String publicId) { + InputStream contents = null; + try { + contents = new URL(file).openStream(); + DocumentBuilderFactory builderFactory = DocumentBuilderFactory + .newInstance(); + builderFactory.setValidating(false); + builderFactory.setNamespaceAware(true); + + builderFactory.setFeature("http://xml.org/sax/features/validation", false); + builderFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-dtd-grammar", false); + builderFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); + + DocumentBuilder builder = builderFactory.newDocumentBuilder(); + Document doc = builder.parse(contents); + + DocumentAccessor accessor = new DocumentAccessor(); + accessor.pushDocument(doc); + SchemaLocations locations = accessor.getCurrentSchemaLocations(); + + String location = locations.getSchemaLocation(publicId); + return location; + } catch (Exception e) { + // do nothing, systemId cannot be identified + } finally { + if (contents != null) { + try { + contents.close(); + } catch (IOException e) { + // do nothing, systemId cannot be identified + } + } + } + return null; + } + +} diff --git a/headless-services/xml-ls-extension/src/main/java/org/springframework/ide/vscode/xml/namespaces/classpath/FilteringURLResourceLoader.java b/headless-services/xml-ls-extension/src/main/java/org/springframework/ide/vscode/xml/namespaces/classpath/FilteringURLResourceLoader.java new file mode 100644 index 000000000..c7fc76fb4 --- /dev/null +++ b/headless-services/xml-ls-extension/src/main/java/org/springframework/ide/vscode/xml/namespaces/classpath/FilteringURLResourceLoader.java @@ -0,0 +1,264 @@ +package org.springframework.ide.vscode.xml.namespaces.classpath; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.file.FileVisitResult; +import java.nio.file.FileVisitor; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.atomic.AtomicLong; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import com.google.common.base.Predicate; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ImmutableSetMultimap; + +public class FilteringURLResourceLoader extends ResourceLoader { + + private static Logger LOGGER = Logger.getLogger(FilteringURLResourceLoader.class.getName()); + + +// private static Map jarScanned = Collections.synchronizedMap(new HashMap()); + +// private static final boolean DEBUG = false; + + private static Set _fetchedResources = Collections.synchronizedSet(new HashSet<>()); + private static ImmutableSet fetchedResources = ImmutableSet.copyOf(_fetchedResources); + + private URL[] urls; + private ResourceLoader parent; + + private Set _indexValidFor = ImmutableSet.of(); + private ImmutableSetMultimap resourcesIndex = null; + + + public FilteringURLResourceLoader(URL[] directories, ResourceLoader parent) { + this.urls = directories; + this.parent = parent == null ? ResourceLoader.NULL : parent; + } + + private static AtomicLong indexBuilt = new AtomicLong(); + private static AtomicLong indexReused = new AtomicLong(); + + + +// @Override +// public URL getResource(String resourceName) { +// try { +// if (!shouldFilter(resourceName)) { +// URL fromParent = parent.getResource(resourceName); +// if (fromParent!=null) { +// return fromParent; +// } +// Collection resources = getResourcesCollection(resourceName); +// if (!resources.isEmpty()) { +// return new URL(resources.iterator().next()); +// } +// } +// } catch (Exception e) { +// SpringXmlNamespacesPlugin.log(e); +// } +// return null; +// } +// + /** + * Get's collection of resources from this resource loader, but excluding resources + * from the parent. + */ + private Collection getResourcesCollection(String resourceName) { + if (!shouldFilter(resourceName)) { +// long start = System.currentTimeMillis(); + try { + ensureIndexed(resourceName); + Collection r = resourcesIndex.get(resourceName); + return r != null ? r : ImmutableList.of(); + } catch (Exception e) { + LOGGER.log(Level.SEVERE, e, null); +// } finally { +// long duration = System.currentTimeMillis() - start; +// long total = timeUsed.addAndGet(duration); +// long requestCount = request.incrementAndGet(); +// System.out.println("Time spent finding resources:"); +// System.out.println(" requests = " + requestCount); +// System.out.println(" avg = " + total / requestCount); +// System.out.println(" total = " + total); +// System.out.println(" index built/reused = " + indexBuilt.get() +" / "+indexReused.get()); + +// System.out.println("Jar scan counts:"); +// for (Entry e : jarScanned.entrySet()) { +// System.out.println(" "+e.getValue().get() +": "+e.getKey()); +// } + } + } + return ImmutableSet.of(); + } + + private synchronized void ensureIndexed(String resourceName) { + if (resourcesIndex!=null && isIndexValidFor(resourceName)) { + indexReused.incrementAndGet(); + } else { + indexBuilt.incrementAndGet(); + synchronized (_fetchedResources) { + if (_fetchedResources.add(resourceName)) { + fetchedResources = ImmutableSet.copyOf(_fetchedResources); +// save(_fetchedResources); + } + } + resourcesIndex = buildIndex(name -> isInterestingByDefault(name) || fetchedResources.contains(name)); + _indexValidFor = fetchedResources; + } + } + + private boolean isIndexValidFor(String resourceName) { + return isInterestingByDefault(resourceName) || _indexValidFor.contains(resourceName); + } + + /** + * If you can predict, based on looking at a resourceName that it is likely to + * be 'interesting' for future lookups, then this method can be overridden so it returns true + * for those 'interesting' resources. This will allow the resource-loader to pre-cache + * the interesting values from the get-go, and thereby avoid rebuilding the + * index multiple times. + *

+ * The default implementation provided here is optimised specifically for resolving + * spring .xsd schemas. + */ + protected boolean isInterestingByDefault(String resourceName) { + return resourceName.startsWith("META-INF/spring") || resourceName.endsWith(".xsd"); + } + + private ImmutableSetMultimap buildIndex(Predicate interestingResourceNames) { + ImmutableSetMultimap.Builder resources = ImmutableSetMultimap.builder(); + //find in our urls + for (URL url : urls) { + try { + if (isZip(url)) { + fetchResourceFromZip(interestingResourceNames, url, resources); + } else { + url.getProtocol().equals("file"); + File file = Paths.get(url.toURI()).toFile(); + if (file.isDirectory()) { + fetchResourceFromDirectory(interestingResourceNames, file, resources); + } else { + fetchResourceFromZip(interestingResourceNames, url, resources); + } + } + } catch (Exception e) { + LOGGER.log(Level.SEVERE, e, null); + } + } + return resources.build(); + } + + private void fetchResourceFromDirectory(Predicate interesttingResourceNames, + File file, + ImmutableSetMultimap.Builder resources + ) { + try { + Path rootDir = file.toPath(); + FileVisitor visitor = new FileVisitor() { + + final FileVisitResult fvr = FileVisitResult.CONTINUE; + + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { + return fvr; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + if (attrs.isRegularFile()) { + String name = rootDir.relativize(file).toString(); + if (interesttingResourceNames.test(name)) { + resources.put(name, file.toUri().toString()); + } + } + return fvr; + } + + @Override + public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException { + return fvr; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { + return fvr; + } + }; + Files.walkFileTree(rootDir, visitor); + } catch (IOException e) { + LOGGER.log(Level.SEVERE, e, null); + } + } + + private void fetchResourceFromZip(Predicate interestingResourceNames, URL url, ImmutableSetMultimap.Builder requestor) { +// AtomicLong counter = jarScanned.computeIfAbsent(url.toString(), s -> new AtomicLong()); +// counter.incrementAndGet(); + try { + try (InputStream input = url.openStream()) { + ZipInputStream zip = new ZipInputStream(input); + ZipEntry ze = zip.getNextEntry(); + while (ze!=null) { + String resourceName = ze.getName(); + if (interestingResourceNames.test(resourceName)) { + //Example url: jar:file:/home/kdvolder/.m2/repository/org/springframework/boot/spring-boot/2.1.4.RELEASE/spring-boot-2.1.4.RELEASE.jar!/META-INF/spring.factories +// System.out.println("FOUND "+resourceName+" in "+url); + requestor.put(resourceName, "jar:"+url+"!/"+ze); +// } else { +// System.out.println("mismatch: "+ze.getName()); + } + ze = zip.getNextEntry(); + } + } + } catch (Exception e) { + LOGGER.log(Level.SEVERE, e, null); + } + } + + private boolean isZip(URL url) { + String path = url.getPath(); + return path.endsWith(".jar") || path.endsWith(".zip"); + } + + @Override + public Stream getResources(String resourceName) { + if (!shouldFilter(resourceName)) { + Stream localResources = + getResourcesCollection(resourceName).stream() + .map(resource -> { + try { + return new URL(resource); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + }); + return Stream.concat(parent.getResources(resourceName), localResources); + } + return Stream.of(); + } + + public static boolean shouldFilter(String name) { + if ("commons-logging.properties".equals(name)) return true; + if (name != null && name.startsWith("META-INF/services/")) { + return (name.indexOf('/', 18) == -1 + && !name.startsWith("org.springframework", 18)); + } + return false; + } + +} diff --git a/headless-services/xml-ls-extension/src/main/java/org/springframework/ide/vscode/xml/namespaces/classpath/ProjectResourceLoaderCache.java b/headless-services/xml-ls-extension/src/main/java/org/springframework/ide/vscode/xml/namespaces/classpath/ProjectResourceLoaderCache.java new file mode 100644 index 000000000..7157b2a66 --- /dev/null +++ b/headless-services/xml-ls-extension/src/main/java/org/springframework/ide/vscode/xml/namespaces/classpath/ProjectResourceLoaderCache.java @@ -0,0 +1,420 @@ +package org.springframework.ide.vscode.xml.namespaces.classpath; + +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.springframework.ide.vscode.commons.protocol.java.Classpath; +import org.springframework.ide.vscode.commons.protocol.java.Classpath.CPE; +import org.springframework.ide.vscode.xml.IJavaProjectProvider; +import org.springframework.ide.vscode.xml.IJavaProjectProvider.IJavaProjectData; + +/** + * Internal cache of classpath urls and corresponding resourceloaders. + * @author Christian Dupuis + * @author Martin Lippert + * @since 2.2.5 + */ +public class ProjectResourceLoaderCache { + + private static Logger LOGGER = Logger.getLogger(ProjectResourceLoaderCache.class.getName()); + + private static final int CACHE_SIZE = 200; + + private final List resourceLoaderCache = new ArrayList(CACHE_SIZE); + +// private IResourceChangeListener resourceChangeListener = null; + + private IJavaProjectProvider javaProjectProvider; + + public ProjectResourceLoaderCache(IJavaProjectProvider javaProjectProvider) { + this.javaProjectProvider = javaProjectProvider; + } + + private ResourceLoader addResourceLoaderToCache(IJavaProjectData project, List urls, ResourceLoader parentResourceLoader) { + synchronized (resourceLoaderCache) { + int nEntries = resourceLoaderCache.size(); + if (nEntries >= CACHE_SIZE) { + // find obsolete entries or remove entry that was least recently accessed + ResourceLoaderCacheEntry oldest = null; + List obsoleteResourceLoaders = new ArrayList(CACHE_SIZE); + for (int i = 0; i < nEntries; i++) { + ResourceLoaderCacheEntry entry = (ResourceLoaderCacheEntry) resourceLoaderCache.get(i); + IJavaProjectData curr = entry.getProject(); + if (javaProjectProvider.exists(curr.getName())) { + obsoleteResourceLoaders.add(entry); + } + else { + if (oldest == null || entry.getLastAccess() < oldest.getLastAccess()) { + oldest = entry; + } + } + } + if (!obsoleteResourceLoaders.isEmpty()) { + for (int i = 0; i < obsoleteResourceLoaders.size(); i++) { + removeResourceLoaderEntryFromCache((ResourceLoaderCacheEntry) obsoleteResourceLoaders.get(i)); + } + } + else if (oldest != null) { + removeResourceLoaderEntryFromCache(oldest); + } + } + ResourceLoaderCacheEntry newEntry = new ResourceLoaderCacheEntry(project, urls, parentResourceLoader); + resourceLoaderCache.add(newEntry); + return newEntry.getResourceLoader(); + } + } + + /** + * Add {@link URL}s to the given set of paths. + */ + private void addClassPathUrls(IJavaProjectData project, List paths, Set resolvedProjects) { + + // add project to local cache to prevent adding its classpaths multiple times + if (resolvedProjects.contains(project)) { + return; + } else { + resolvedProjects.add(project); + } + + // configured classpath + Classpath classpath = project.getClasspath(); + + // add class path entries + for (CPE cpe : classpath.getEntries()) { + try { + if (Classpath.isBinary(cpe)) { + addFile(paths, new File(cpe.getPath())); + } else if (Classpath.isSource(cpe)) { + addFile(paths, new File(cpe.getPath())); + addFile(paths, new File(cpe.getOutputFolder())); + } + } catch (MalformedURLException e) { + LOGGER.log(Level.SEVERE, e, null); + } + } + } + + private static void addFile(List paths, File file) throws MalformedURLException { + if (file.exists()) { + if (file.isDirectory()) { + paths.add(new URL(file.toURI().toString() + File.separator)); + } + else { + paths.add(file.toURI().toURL()); + } + } else { + LOGGER.log(Level.WARNING, "Classpath entry '" + file + "' does not exist!"); + } + } + + private ResourceLoader findResourceLoaderInCache(IJavaProjectData project, ResourceLoader parentResourceLoader) { + synchronized (resourceLoaderCache) { + for (int i = resourceLoaderCache.size() - 1; i >= 0; i--) { + ResourceLoaderCacheEntry entry = (ResourceLoaderCacheEntry) resourceLoaderCache.get(i); + IJavaProjectData curr = entry.getProject(); + if (javaProjectProvider.exists(curr.getName())) { + removeResourceLoaderEntryFromCache(entry); + } + else { + if (entry.matches(project, parentResourceLoader)) { + entry.markAsAccessed(); + return entry.getResourceLoader(); + } + } + } + } + return null; + } + + /** + * Iterates all class path entries of the given project and all depending projects. + *

+ * Note: if useParentResourceLoader is true, the Spring, AspectJ, Commons Logging and ASM bundles are + * automatically added to the paths. + * @param project the {@link IProject} + * @param useParentResourceLoader use the OSGi resourceloader as parent + * @return a set of {@link URL}s that can be used to construct a {@link URLResourceLoader} + */ + public List getClassPathUrls(IJavaProjectData project, ResourceLoader parentResourceLoader) { + + // needs to be linked to preserve ordering + List paths = new ArrayList(); + Set resolvedProjects = new HashSet(); + addClassPathUrls(project, paths, resolvedProjects); + + return paths; + } + +// /** +// * Registers internal listeners that listen to changes relevant to clear out stale cache entries. +// */ +// private void registerListenersIfRequired() { +// if (resourceChangeListener == null) { +// resourceChangeListener = new SourceAndOutputLocationResourceChangeListener(); +// ResourcesPlugin.getWorkspace().addResourceChangeListener(resourceChangeListener); +// } +// } + + /** + * Removes the given {@link ResourceLoaderCacheEntry} from the internal cache. + * @param entry the entry to remove + */ + private void removeResourceLoaderEntryFromCache(ResourceLoaderCacheEntry entry) { + synchronized (resourceLoaderCache) { + entry.dispose(); + resourceLoaderCache.remove(entry); + } + } + + /** + * Returns a {@link ResourceLoader} for the given project. + */ + public ResourceLoader getResourceLoader(IJavaProjectData project, ResourceLoader parentResourceLoader) { + synchronized (ProjectResourceLoaderCache.class) { + // Setup the root class loader to be used when no explicit parent class loader is given + if (parentResourceLoader==null) { + parentResourceLoader = ResourceLoader.NULL; + } + if (project == null) { + return parentResourceLoader; + } + +// registerListenersIfRequired(); + } + + ResourceLoader classLoader = findResourceLoaderInCache(project, parentResourceLoader); + if (classLoader == null) { + List urls = getClassPathUrls(project, parentResourceLoader); + classLoader = addResourceLoaderToCache(project, urls, parentResourceLoader); + } + return classLoader; + } + + /** + * Removes any cached {@link ResourceLoaderCacheEntry} for the given {@link IProject}. + * @param project the project to remove {@link ResourceLoaderCacheEntry} for + */ + protected void removeResourceLoaderEntryFromCache(IJavaProjectData project) { + synchronized (resourceLoaderCache) { + for (ResourceLoaderCacheEntry entry : new ArrayList(resourceLoaderCache)) { + if (project.equals(entry.getProject())) { + entry.dispose(); + resourceLoaderCache.remove(entry); + } + } + } + } + + /** + * Internal cache entry + */ + class ResourceLoaderCacheEntry /*implements IElementChangedListener*/ { + + private URL[] directories; + + private ResourceLoader jarResourceLoader; + + private long lastAccess; + + private ResourceLoader parentResourceLoader; + + private IJavaProjectData project; + + private URL[] urls; + + public ResourceLoaderCacheEntry(IJavaProjectData project, List urls, ResourceLoader parentResourceLoader) { + this.project = project; + this.urls = urls.toArray(new URL[urls.size()]); + this.parentResourceLoader = parentResourceLoader; + markAsAccessed(); + } + + public void dispose() { + this.urls = null; + this.jarResourceLoader = null; + } + +// public void elementChanged(ElementChangedEvent event) { +// IJavaProject javaProject = JdtUtils.getJavaProject(project); +// if (javaProject != null) { +// for (IJavaElementDelta delta : event.getDelta().getAffectedChildren()) { +// if ((delta.getFlags() & IJavaElementDelta.F_RESOLVED_CLASSPATH_CHANGED) != 0 +// || (delta.getFlags() & IJavaElementDelta.F_CLASSPATH_CHANGED) != 0) { +// if (javaProject.equals(delta.getElement()) || javaProject.isOnClasspath(delta.getElement())) { +// removeResourceLoaderEntryFromCache(this); +// } +// } +// } +// } +// } + + public ResourceLoader getResourceLoader() { + ResourceLoader parent = getJarResourceLoader(); + return new FilteringURLResourceLoader(directories, parent); + } + + public long getLastAccess() { + return lastAccess; + } + + public IJavaProjectData getProject() { + return this.project; + } + + public void markAsAccessed() { + lastAccess = System.currentTimeMillis(); + } + + public boolean matches(IJavaProjectData project, ResourceLoader parentResourceLoader) { + return this.project.equals(project) + && ((parentResourceLoader == null && this.parentResourceLoader == null) || (parentResourceLoader != null && parentResourceLoader + .equals(this.parentResourceLoader))); + } + + private synchronized ResourceLoader getJarResourceLoader() { + if (jarResourceLoader == null) { + Set jars = new LinkedHashSet(); + List dirs = new ArrayList(); + for (URL url : urls) { + if (shouldLoadFromParent(url)) { + jars.add(url); + } + else { + dirs.add(url); + } + } + if (parentResourceLoader == null) { + parentResourceLoader = ResourceLoader.NULL; + } + jarResourceLoader = new FilteringURLResourceLoader((URL[]) jars.toArray(new URL[jars.size()]), + parentResourceLoader); + directories = dirs.toArray(new URL[dirs.size()]); + } + return jarResourceLoader; + } + + private boolean shouldLoadFromParent(URL url) { + String path = url.getPath(); + if (path.endsWith(".jar") || path.endsWith(".zip")) { + return true; + } + else if (path.contains("/org.eclipse.osgi/bundles/")) { + return true; + } + return false; + } + } + +// /** +// * {@link IResourceChangeListener} to clear the cache whenever new source or output folders are being added. +// * @since 2.5.2 +// */ +// static class SourceAndOutputLocationResourceChangeListener implements IResourceChangeListener { +// +// private static final int VISITOR_FLAGS = IResourceDelta.ADDED | IResourceDelta.CHANGED; +// +// /** +// * {@inheritDoc} +// */ +// public void resourceChanged(IResourceChangeEvent event) { +// if (event.getSource() instanceof IWorkspace) { +// int eventType = event.getType(); +// switch (eventType) { +// case IResourceChangeEvent.POST_CHANGE: +// IResourceDelta delta = event.getDelta(); +// if (delta != null) { +// try { +// delta.accept(getVisitor(), VISITOR_FLAGS); +// } +// catch (CoreException e) { +// SpringXmlNamespacesPlugin.log("Error while traversing resource change delta", e); +// } +// } +// break; +// } +// } +// else if (event.getSource() instanceof IProject) { +// int eventType = event.getType(); +// switch (eventType) { +// case IResourceChangeEvent.POST_CHANGE: +// IResourceDelta delta = event.getDelta(); +// if (delta != null) { +// try { +// delta.accept(getVisitor(), VISITOR_FLAGS); +// } +// catch (CoreException e) { +// SpringXmlNamespacesPlugin.log("Error while traversing resource change delta", e); +// } +// } +// break; +// } +// } +// +// } +// +// protected IResourceDeltaVisitor getVisitor() { +// return new SourceAndOutputLocationResourceVisitor(); +// } +// +// /** +// * Internal resource delta visitor. +// */ +// protected class SourceAndOutputLocationResourceVisitor implements IResourceDeltaVisitor { +// +// public final boolean visit(IResourceDelta delta) throws CoreException { +// IResource resource = delta.getResource(); +// switch (delta.getKind()) { +// case IResourceDelta.ADDED: +// return resourceAdded(resource); +// } +// return true; +// } +// +// protected boolean resourceAdded(IResource resource) { +// if (resource instanceof IFolder && JdtUtils.isJavaProject(resource)) { +// try { +// IJavaProject javaProject = JdtUtils.getJavaProject(resource); +// // Safe guard once again +// if (javaProject == null) { +// return false; +// } +// +// // Check the default output location +// if (javaProject.getOutputLocation() != null +// && javaProject.getOutputLocation().equals(resource.getFullPath())) { +// removeResourceLoaderEntryFromCache(resource.getProject()); +// return false; +// } +// +// // Check any source and output folder location +// for (IClasspathEntry entry : javaProject.getRawClasspath()) { +// if (resource.getFullPath() != null && resource.getFullPath().equals(entry.getPath())) { +// removeResourceLoaderEntryFromCache(resource.getProject()); +// return false; +// } +// else if (resource.getFullPath() != null +// && resource.getFullPath().equals(entry.getOutputLocation())) { +// removeResourceLoaderEntryFromCache(resource.getProject()); +// return false; +// } +// } +// } +// catch (JavaModelException e) { +// SpringXmlNamespacesPlugin.log("Error traversing resource change delta", e); +// } +// } +// return true; +// } +// } +// +// } + +} diff --git a/headless-services/xml-ls-extension/src/main/java/org/springframework/ide/vscode/xml/namespaces/classpath/PropertiesLoaderUtils.java b/headless-services/xml-ls-extension/src/main/java/org/springframework/ide/vscode/xml/namespaces/classpath/PropertiesLoaderUtils.java new file mode 100644 index 000000000..ca98d6320 --- /dev/null +++ b/headless-services/xml-ls-extension/src/main/java/org/springframework/ide/vscode/xml/namespaces/classpath/PropertiesLoaderUtils.java @@ -0,0 +1,96 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed 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 + * + * https://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.springframework.ide.vscode.xml.namespaces.classpath; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URLConnection; +import java.util.Properties; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Convenient utility methods for loading of {@code java.util.Properties}, + * performing standard handling of input streams. + * + *

For more configurable properties loading, including the option of a + * customized encoding, consider using the PropertiesLoaderSupport class. + * + * @author Juergen Hoeller + * @author Rob Harrop + * @since 2.0 + * @see PropertiesLoaderSupport + */ +public abstract class PropertiesLoaderUtils { + + private static Logger LOGGER = Logger.getLogger(PropertiesLoaderUtils.class.getName()); + + private static final String XML_FILE_EXTENSION = ".xml"; + + /** + * Load all properties from the specified class path resource + * (in ISO-8859-1 encoding), using the given class loader. + *

Merges properties if more than one resource of the same name + * found in the class path. + * @param resourceName the name of the class path resource + * @param classLoader the ClassLoader to use for loading + * (or {@code null} to use the default class loader) + * @return the populated Properties instance + * @throws IOException if loading failed + */ + public static Properties loadAllProperties(String resourceName, ResourceLoader classLoader) { + if (resourceName == null) { + throw new IllegalArgumentException("Resource name must not be null"); + } + ResourceLoader classLoaderToUse = classLoader; + if (classLoaderToUse == null) { + classLoaderToUse = ResourceLoader.NULL; + } + ResourceLoader loader = classLoaderToUse; + Properties props = new Properties(); + loader.getResources(resourceName).parallel().forEach(url -> { + try { + URLConnection con = url.openConnection(); + useCachesIfNecessary(con); + InputStream is = con.getInputStream(); + try { + if (resourceName.endsWith(XML_FILE_EXTENSION)) { + props.loadFromXML(is); + } else { + props.load(is); + } + } finally { + is.close(); + } + } catch (Exception e) { + LOGGER.log(Level.SEVERE, e, null); + } + }); + return props; + } + + /** + * Set the {@link URLConnection#setUseCaches "useCaches"} flag on the + * given connection, preferring {@code false} but leaving the + * flag at {@code true} for JNLP based resources. + * @param con the URLConnection to set the flag on + */ + private static void useCachesIfNecessary(URLConnection con) { + con.setUseCaches(con.getClass().getSimpleName().startsWith("JNLP")); + } + +} diff --git a/headless-services/xml-ls-extension/src/main/java/org/springframework/ide/vscode/xml/namespaces/classpath/ResourceLoader.java b/headless-services/xml-ls-extension/src/main/java/org/springframework/ide/vscode/xml/namespaces/classpath/ResourceLoader.java new file mode 100644 index 000000000..f91e7aa36 --- /dev/null +++ b/headless-services/xml-ls-extension/src/main/java/org/springframework/ide/vscode/xml/namespaces/classpath/ResourceLoader.java @@ -0,0 +1,42 @@ +package org.springframework.ide.vscode.xml.namespaces.classpath; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.stream.Stream; + +/** + * Subset of {@link ClassLoader} interface. Mimicks behavior of + * classloaders but only can be used to load 'resources' as data. + * + * @author Kris De Volder + */ +public abstract class ResourceLoader { + + public static final ResourceLoader NULL = new ResourceLoader() { + @Override + public Stream getResources(String resourceName) { + return Stream.of(); + } + + @Override + public String toString() { + return "ResourceLoader.NULL"; + } + }; + + public final URL getResource(String resourceName) { + return getResources(resourceName).findAny().orElse(null); + } + protected abstract Stream getResources(String resourceName); + + public final InputStream getResourceAsStream(String name) { + URL url = getResource(name); + try { + return url != null ? url.openStream() : null; + } catch (IOException e) { + return null; + } + + } +} diff --git a/headless-services/xml-ls-extension/src/main/java/org/springframework/ide/vscode/xml/namespaces/model/NamespaceDefinition.java b/headless-services/xml-ls-extension/src/main/java/org/springframework/ide/vscode/xml/namespaces/model/NamespaceDefinition.java new file mode 100644 index 000000000..5420afe4c --- /dev/null +++ b/headless-services/xml-ls-extension/src/main/java/org/springframework/ide/vscode/xml/namespaces/model/NamespaceDefinition.java @@ -0,0 +1,149 @@ +package org.springframework.ide.vscode.xml.namespaces.model; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class NamespaceDefinition { + + private Pattern versionPattern = Pattern.compile(".*-([0-9,.]*)\\.xsd"); + + private String name; + + private String namespaceUri; + + private String prefix; + + private Set schemaLocations = new CopyOnWriteArraySet(); + + private Set uris = new CopyOnWriteArraySet(); + + private Properties uriMapping = new Properties(); + + private String defaultSchemaLocation = null; + + public NamespaceDefinition(Properties uriMapping) { + this.uriMapping = uriMapping; + } + + public void addSchemaLocation(String schemaLocation) { + schemaLocations.add(schemaLocation); + } + + public void addUri(String uri) { + uris.add(uri); + } + + /** + * {@inheritDoc} + */ + public synchronized String getDefaultSchemaLocation() { + if (defaultSchemaLocation == null) { + // Per convention the version-less XSD is the default + for (String schemaLocation : schemaLocations) { + if (!versionPattern.matcher(schemaLocation).matches()) { + defaultSchemaLocation = schemaLocation; + } + } + if (defaultSchemaLocation == null && schemaLocations.size() > 0) { + List locations = new ArrayList(schemaLocations); + Collections.sort(locations); + defaultSchemaLocation = locations.get(0); + } + } + return defaultSchemaLocation; + } + + /** + * {@inheritDoc} + */ + public String getDefaultUri() { + String defaultUri = null; + NamespaceVersion version = NamespaceVersion.MINIMUM_VERSION; + for (String uri : uris) { + NamespaceVersion tempVersion = NamespaceVersion.MINIMUM_VERSION; + Matcher matcher = versionPattern.matcher(uri); + if (matcher.matches()) { + tempVersion = new NamespaceVersion(matcher.group(1)); + } + if (tempVersion.compareTo(version) >= 0) { + version = tempVersion; + defaultUri = uri; + } + } + return defaultUri; + } + + /** + * {@inheritDoc} + */ + public String getName() { + return name; + } + + /** + * {@inheritDoc} + */ + public String getNamespaceUri() { + return namespaceUri; + } + + /** + * {@inheritDoc} + */ + public String getPrefix() { + if (prefix != null) { + return prefix; + } + int ix = namespaceUri.lastIndexOf('/'); + if (ix > 0) { + return namespaceUri.substring(ix + 1); + } + return null; + } + + /** + * {@inheritDoc} + */ + public Set getSchemaLocations() { + return schemaLocations; + } + + /** + * {@inheritDoc} + */ + public void setName(String name) { + this.name = name; + } + + /** + * {@inheritDoc} + */ + public void setNamespaceUri(String namespaceUri) { + this.namespaceUri = namespaceUri; + } + + /** + * {@inheritDoc} + */ + public void setPrefix(String prefix) { + this.prefix = prefix; + } + + /** + * {@inheritDoc} + */ + public Properties getUriMapping() { + return this.uriMapping; + } + + @Override + public String toString() { + return this.namespaceUri; + } +} \ No newline at end of file diff --git a/headless-services/xml-ls-extension/src/main/java/org/springframework/ide/vscode/xml/namespaces/model/NamespaceVersion.java b/headless-services/xml-ls-extension/src/main/java/org/springframework/ide/vscode/xml/namespaces/model/NamespaceVersion.java new file mode 100644 index 000000000..44b31bf4d --- /dev/null +++ b/headless-services/xml-ls-extension/src/main/java/org/springframework/ide/vscode/xml/namespaces/model/NamespaceVersion.java @@ -0,0 +1,83 @@ +package org.springframework.ide.vscode.xml.namespaces.model; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class NamespaceVersion implements Comparable { + + private static final String MINIMUM_VERSION_STRING = "0"; + + private static final Pattern versionPattern = Pattern.compile("([0-9]\\d*)(?:\\.(\\d+))?(?:\\.(\\d+))?(?:-([a-zA-Z0-9]+))?"); + + public static final NamespaceVersion MINIMUM_VERSION = new NamespaceVersion(MINIMUM_VERSION_STRING); + + private final int major; + private final int minor; + private final int patch; + private final String qualifier; + + public NamespaceVersion(String v) { + Matcher matcher = versionPattern.matcher(v); + + if (matcher.matches()) { + qualifier = matcher.groupCount() > 3 ? (matcher.group(4) == null ? "" : matcher.group(4)) : ""; + patch = matcher.groupCount() > 2 ? (matcher.group(3) == null ? 0 : Integer.valueOf(matcher.group(3))) : 0; + minor = matcher.groupCount() > 1 ? (matcher.group(2) == null ? 0 : Integer.valueOf(matcher.group(2))) : 0; + major = matcher.groupCount() > 0 ? (matcher.group(1) == null ? 0 : Integer.valueOf(matcher.group(1))) : 0; + } else { + major = 0; + minor = 0; + patch = 0; + qualifier = ""; + } + } + + public int compareTo(NamespaceVersion v2) { + if (major == v2.major) { + if (minor == v2.minor) { + if (patch == v2.patch) { + return qualifier.compareTo(v2.qualifier); + } else { + return patch - v2.patch; + } + } else { + return minor - v2.minor; + } + } else { + return major - v2.major; + } + } + + public int getMajor() { + return major; + } + + public int getMinor() { + return minor; + } + + public int getPatch() { + return patch; + } + + public String getQualifier() { + return qualifier; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(major); + sb.append('.'); + sb.append(minor); + sb.append('.'); + sb.append(patch); + if (!qualifier.isEmpty()) { + sb.append('-'); + sb.append(qualifier); + } + return sb.toString(); + } + + +} \ No newline at end of file diff --git a/headless-services/xml-ls-extension/src/main/java/org/springframework/ide/vscode/xml/namespaces/util/DocumentAccessor.java b/headless-services/xml-ls-extension/src/main/java/org/springframework/ide/vscode/xml/namespaces/util/DocumentAccessor.java new file mode 100644 index 000000000..db051aa8c --- /dev/null +++ b/headless-services/xml-ls-extension/src/main/java/org/springframework/ide/vscode/xml/namespaces/util/DocumentAccessor.java @@ -0,0 +1,119 @@ +package org.springframework.ide.vscode.xml.namespaces.util; + +import java.util.HashMap; +import java.util.Map; +import java.util.Stack; + +import org.w3c.dom.Document; +import org.w3c.dom.Node; + +/** + * Utility that manages an internal stack of {@link Document}. + * @author Christian Dupuis + * @since 2.2.7 + */ +public class DocumentAccessor { + + private Stack documents = new Stack(); + + private Stack elements = new Stack(); + + private Node lastElement = null; + + private Document lastDocument = null; + + private Map schemaLocations = new HashMap(); + + /** Push a new document onto the internal stack structure */ + public void pushDocument(Document doc) { + lastDocument = doc; + documents.push(doc); + } + + /** Push a new element onto the internal stack structure */ + public void pushElement(Node element) { + lastElement = element; + elements.push(element); + } + + /** Returns the current document; meaning the first document in the stack */ + public Document getCurrentDocument() { + if (!documents.isEmpty()) { + return documents.peek(); + } + return null; + } + + /** Retuns the last procssed document */ + public Document getLastDocument() { + return lastDocument; + } + + /** Returns the current element; meaning the first element in the stack */ + public Node getCurrentElement() { + if (!elements.isEmpty()) { + return elements.peek(); + } + return null; + } + + /** Returns the last processed element */ + public Node getLastElement() { + return lastElement; + } + + /** Returns the current values of the schemaLocation attribute */ + public synchronized SchemaLocations getCurrentSchemaLocations() { + Document doc = getCurrentDocument(); + if (!this.schemaLocations.containsKey(doc)) { + if (doc != null && doc.getDocumentElement() != null) { + SchemaLocations schemaLocations = new SchemaLocations(); + schemaLocations.initSchemaLocations(doc.getDocumentElement().getAttributeNS( + "http://www.w3.org/2001/XMLSchema-instance", "schemaLocation")); + this.schemaLocations.put(doc, schemaLocations); + } + } + return this.schemaLocations.get(doc); + } + + /** Removes the first document from the stack */ + public Document popDocument() { + if (!documents.isEmpty()) { + return documents.pop(); + } + return null; + } + + /** Removes the first element from the stack */ + public Node popElement() { + if (!elements.isEmpty()) { + return elements.pop(); + } + return null; + } + + /** + * Internal class that parses the value of the schemaLocation attribute and offers accessors to the + * mapping. + */ + public static class SchemaLocations { + + private Map mapping = new HashMap(); + + public void initSchemaLocations(String schemaLocations) { + if (schemaLocations != null && !schemaLocations.isEmpty()) { + String[] tokens = schemaLocations.split("\\s*\\r?\\n\\s*"); + if (tokens.length % 2 == 0) { + for (int i = 0; i < tokens.length; i = i + 2) { + mapping.put(tokens[i], tokens[i + 1]); + } + } + } + } + + public String getSchemaLocation(String namespaceUri) { + return mapping.get(namespaceUri); + } + } + +} diff --git a/headless-services/xml-ls-extension/src/main/java/org/springframework/ide/vscode/xml/namespaces/util/TargetNamespaceScanner.java b/headless-services/xml-ls-extension/src/main/java/org/springframework/ide/vscode/xml/namespaces/util/TargetNamespaceScanner.java new file mode 100644 index 000000000..a4ab0c902 --- /dev/null +++ b/headless-services/xml-ls-extension/src/main/java/org/springframework/ide/vscode/xml/namespaces/util/TargetNamespaceScanner.java @@ -0,0 +1,59 @@ +package org.springframework.ide.vscode.xml.namespaces.util; + +import java.io.IOException; +import java.net.URL; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import org.w3c.dom.Document; +import org.xml.sax.SAXException; + +/** + * Scanner to quickly identify the namespace that is declared inside an XSD. + * @author Martin Lippert + * @since 2.8.0 + */ +public class TargetNamespaceScanner { + + private static Logger LOGGER = Logger.getLogger(TargetNamespaceScanner.class.getName()); + + /** + * Returns the target namespace URI of the XSD identified by the given + * url. + */ + public static String getTargetNamespace(URL url) { + if (url == null) { + return null; + } + + ClassLoader ccl = Thread.currentThread().getContextClassLoader(); + try { + Thread.currentThread().setContextClassLoader(TargetNamespaceScanner.class.getClassLoader()); + + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setValidating(false); + + factory.setFeature("http://xml.org/sax/features/validation", false); + factory.setFeature("http://apache.org/xml/features/validation/dynamic", false); + factory.setFeature("http://apache.org/xml/features/validation/schema", false); + factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); + + DocumentBuilder docBuilder = factory.newDocumentBuilder(); + + Document doc = docBuilder.parse(url.openStream()); + + return doc.getDocumentElement().getAttribute("targetNamespace"); + } catch (SAXException|IOException|ParserConfigurationException e) { + LOGGER.log(Level.WARNING, e, null); + } + finally { + Thread.currentThread().setContextClassLoader(ccl); + } + return null; + } + +} diff --git a/headless-services/xml-ls-extension/src/main/resources/META-INF/services/org.eclipse.lemminx.services.extensions.IXMLExtension b/headless-services/xml-ls-extension/src/main/resources/META-INF/services/org.eclipse.lemminx.services.extensions.IXMLExtension new file mode 100644 index 000000000..705b05413 --- /dev/null +++ b/headless-services/xml-ls-extension/src/main/resources/META-INF/services/org.eclipse.lemminx.services.extensions.IXMLExtension @@ -0,0 +1 @@ +org.springframework.ide.vscode.xml.SpringXmlPlugin \ No newline at end of file diff --git a/headless-services/xml-ls-extension/src/test/java/org/springframework/ide/vscode/xml/test/NamespaceVersionTest.java b/headless-services/xml-ls-extension/src/test/java/org/springframework/ide/vscode/xml/test/NamespaceVersionTest.java new file mode 100644 index 000000000..cd2ae9f5d --- /dev/null +++ b/headless-services/xml-ls-extension/src/test/java/org/springframework/ide/vscode/xml/test/NamespaceVersionTest.java @@ -0,0 +1,35 @@ +package org.springframework.ide.vscode.xml.test; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; +import org.springframework.ide.vscode.xml.namespaces.model.NamespaceVersion; + +public class NamespaceVersionTest { + + @Test + public void allVersion() throws Exception { + assertEquals("11.2.345-alpha", new NamespaceVersion("11.2.345-alpha").toString()); + } + + @Test + public void majorAndMinorVersion() throws Exception { + assertEquals("11.262.0", new NamespaceVersion("11.262").toString()); + } + + @Test + public void majorVersion() throws Exception { + assertEquals("11.0.0", new NamespaceVersion("11").toString()); + } + + @Test + public void majorAndMinorAndPatchVersion() throws Exception { + assertEquals("11.0.78", new NamespaceVersion("11.0.78").toString()); + } + + @Test + public void zeroVersion() throws Exception { + assertEquals("0.0.0", new NamespaceVersion("0").toString()); + } + +} diff --git a/vscode-extensions/vscode-spring-boot/package.json b/vscode-extensions/vscode-spring-boot/package.json index d44b66872..e85868828 100644 --- a/vscode-extensions/vscode-spring-boot/package.json +++ b/vscode-extensions/vscode-spring-boot/package.json @@ -38,6 +38,10 @@ "./jars/jdt-ls-commons.jar", "./jars/jdt-ls-extension.jar" ], + "xml.javaExtensions": [ + "./jars/commons-lsp-extensions.jar", + "./jars/xml-ls-extension.jar" + ], "languages": [ { "id": "spring-boot-properties-yaml", @@ -521,7 +525,7 @@ "vscode-languageclient": "6.0.0" }, "devDependencies": { - "vsce": "^1.74.0", + "vsce": "^1.81.1", "typescript": "3.8.3", "@types/node": "^12.7.9", "@types/vscode": "^1.41.0" diff --git a/vscode-extensions/vscode-spring-boot/scripts/preinstall.sh b/vscode-extensions/vscode-spring-boot/scripts/preinstall.sh index 916959cc0..e1a954078 100755 --- a/vscode-extensions/vscode-spring-boot/scripts/preinstall.sh +++ b/vscode-extensions/vscode-spring-boot/scripts/preinstall.sh @@ -31,6 +31,7 @@ cd ${workdir}/language-server server_jar_file=$(find ${workdir}/../../headless-services/spring-boot-language-server/target -name '*-exec.jar'); jar -xvf ${server_jar_file} +# JDT LS Extension cd ${workdir}/../../headless-services/jdt-ls-extension find . -name "*-sources.jar" -delete cp org.springframework.tooling.jdt.ls.extension/target/*.jar ${workdir}/jars/jdt-ls-extension.jar @@ -39,3 +40,10 @@ cp org.springframework.tooling.jdt.ls.commons/target/*.jar ${workdir}/jars/jdt-l # Copy Reactor dependency bundles cp org.springframework.tooling.jdt.ls.commons/target/dependencies/io.projectreactor.reactor-core.jar ${workdir}/jars/ cp org.springframework.tooling.jdt.ls.commons/target/dependencies/org.reactivestreams.reactive-streams.jar ${workdir}/jars/ + +# XML LS Extension +cd ${workdir}/../../headless-services/xml-ls-extension +find . -name "*-sources.jar" -delete +cp target/*.jar ${workdir}/jars/xml-ls-extension.jar +cp target/dependencies/commons-lsp-extensions.jar ${workdir}/jars/ +