XML LS extension for namespace resolution

This commit is contained in:
BoykoAlex
2020-10-06 08:42:18 -04:00
parent e0bba737d3
commit 787aa0fc0b
29 changed files with 2353 additions and 6 deletions

View File

@@ -6,4 +6,5 @@ bin.includes = META-INF/,\
servers/,\
syntaxes/,\
icons/,\
about.html
about.html,\
jars/

View File

@@ -355,5 +355,14 @@
class="org.springframework.tooling.boot.ls.prefs.RemoteAppsFromPrefsDataContributor">
</injection>
</extension>
<extension
point="org.eclipse.wildwebdeveloper.xml.lemminxExtension">
<jar
path="jars/commons-lsp-extensions.jar">
</jar>
<jar
path="jars/xml-ls-extension.jar">
</jar>
</extension>
</plugin>

View File

@@ -43,6 +43,33 @@
<outputDirectory>${project.build.directory}/../servers/spring-boot-language-server</outputDirectory>
</configuration>
</execution>
<execution>
<id>xml-extension</id>
<phase>prepare-package</phase>
<goals>
<goal>copy</goal>
</goals>
<configuration>
<artifactItems>
<artifactItem>
<groupId>org.springframework.ide.vscode</groupId>
<artifactId>xml-ls-extension</artifactId>
<version>${project.version}</version>
<overWrite>true</overWrite>
</artifactItem>
<artifactItem>
<groupId>org.springframework.ide.vscode</groupId>
<artifactId>commons-lsp-extensions</artifactId>
<version>${project.version}</version>
<overWrite>true</overWrite>
</artifactItem>
</artifactItems>
<stripVersion>true</stripVersion>
<overWriteReleases>true</overWriteReleases>
<overWriteSnapshots>true</overWriteSnapshots>
<outputDirectory>${project.build.directory}/../jars</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
@@ -71,6 +98,9 @@
<fileset>
<directory>servers</directory>
</fileset>
<fileset>
<directory>jars</directory>
</fileset>
</filesets>
</configuration>
</plugin>

View File

@@ -91,6 +91,10 @@
class="org.springframework.tooling.ls.eclipse.commons.commands.OpenResourceInEditor"
commandId="org.springframework.tooling.ls.eclipse.commons.commands.OpenResourceInEditor">
</handler>
<handler
class="org.springframework.tooling.ls.eclipse.commons.commands.JavaWorkspaceCommandHanlder"
commandId="java.execute.workspaceCommand">
</handler>
</extension>
<extension
point="org.eclipse.ui.commands">

View File

@@ -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<IProject> 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<Object> 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<Object> removeClasspathListener(ClasspathListenerParams params) {
return CompletableFuture.completedFuture(classpathService.removeClasspathListener(params.getCallbackCommandId()));
return CompletableFuture.completedFuture(CLASSPATH_SERVICE.removeClasspathListener(params.getCallbackCommandId()));
}
@Override

View File

@@ -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<Object> 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;
}
}

View File

@@ -165,6 +165,11 @@
<layout>p2</layout>
<url>https://download.eclipse.org/tm4e/snapshots/</url>
</repository>
<repository>
<id>wwd</id>
<layout>p2</layout>
<url>https://download.eclipse.org/wildwebdeveloper/releases/latest/</url>
</repository>
</repositories>
</profile>
@@ -210,6 +215,11 @@
<layout>p2</layout>
<url>https://download.eclipse.org/tm4e/snapshots/</url>
</repository>
<repository>
<id>wwd</id>
<layout>p2</layout>
<url>https://download.eclipse.org/wildwebdeveloper/releases/latest/</url>
</repository>
</repositories>
</profile>
@@ -260,6 +270,11 @@
<layout>p2</layout>
<url>https://download.eclipse.org/tm4e/snapshots/</url>
</repository>
<repository>
<id>wwd</id>
<layout>p2</layout>
<url>https://download.eclipse.org/wildwebdeveloper/releases/latest/</url>
</repository>
</repositories>
</profile>

View File

@@ -40,6 +40,23 @@
<artifactId>target-platform-configuration</artifactId>
<version>${tycho-version}</version>
<configuration>
<environments>
<environment>
<os>win32</os>
<ws>win32</ws>
<arch>x86_64</arch>
</environment>
<environment>
<os>linux</os>
<ws>gtk</ws>
<arch>x86_64</arch>
</environment>
<environment>
<os>macosx</os>
<ws>cocoa</ws>
<arch>x86_64</arch>
</environment>
</environments>
<dependency-resolution>
<extraRequirements>
<requirement>

View File

@@ -16,6 +16,7 @@
<module>spring-boot-language-server</module>
<module>bosh-language-server</module>
<module>jdt-ls-extension</module>
<module>xml-ls-extension</module>
</modules>
</project>

View File

@@ -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

View File

@@ -0,0 +1,97 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>xml-ls-extension</artifactId>
<name>xml-ls-extension</name>
<description>Lemminx XML Language Server Extension for contributing project classpath specific NSURI resolution</description>
<parent>
<groupId>org.springframework.ide.vscode</groupId>
<artifactId>commons-parent</artifactId>
<version>1.23.0-SNAPSHOT</version>
<relativePath>../commons/pom.xml</relativePath>
</parent>
<properties>
<dependencies.version>${project.version}</dependencies.version>
<lsp4xml.version>0.14.0-SNAPSHOT</lsp4xml.version>
</properties>
<repositories>
<repository>
<id>lemminx-snapshots</id>
<url>https://repo.eclipse.org/content/repositories/lemminx-snapshots/</url>
<releases>
<enabled>false</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.eclipse.lsp4j</groupId>
<artifactId>org.eclipse.lsp4j</artifactId>
<version>${lsp4j-version}</version>
</dependency>
<dependency>
<groupId>org.eclipse.lemminx</groupId>
<artifactId>org.eclipse.lemminx</artifactId>
<version>${lsp4xml.version}</version>
<exclusions>
<exclusion>
<groupId>org.eclipse.lsp4j</groupId>
<artifactId>org.eclipse.lsp4j</artifactId>
</exclusion>
<exclusion>
<groupId>org.eclipse.lsp4j</groupId>
<artifactId>org.eclipse.lsp4j.jsonrpc</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.ide.vscode</groupId>
<artifactId>commons-lsp-extensions</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<stripVersion>true</stripVersion>
<outputDirectory>${project.build.directory}/dependencies</outputDirectory>
<overWriteReleases>true</overWriteReleases>
<overWriteSnapshots>true</overWriteSnapshots>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.felix</groupId>
<artifactId>maven-bundle-plugin</artifactId>
<version>6.0.3</version>
</plugin>
</plugins>
</build>
</project>

View File

@@ -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<IJavaProjectData> all();
IJavaProjectData get(String name);
void addListener(Consumer<IJavaProjectData> listener);
void removeListener(Consumer<IJavaProjectData> 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;
}
}

View File

@@ -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<String, IJavaProjectData> projectsCache = new ConcurrentHashMap<>();
private final String callbackCommandId;
private List<Consumer<IJavaProjectData>> 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<Object> 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<Object> args = callbackParams.getArguments();
//Args are deserialized as com.google.gson.JsonElements.
List<String> 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<IJavaProjectData> l : listeners) {
try {
l.accept(project);
} catch (Exception e) {
LOGGER.log(Level.SEVERE, e, null);
}
}
}
@Override
public Collection<IJavaProjectData> all() {
return projectsCache.values();
}
@Override
public IJavaProjectData get(String name) {
return projectsCache.get(name);
}
@Override
public void addListener(Consumer<IJavaProjectData> listener) {
listeners.add(listener);
}
@Override
public void removeListener(Consumer<IJavaProjectData> listener) {
listeners.remove(listener);
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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
* <code>spring.schemas</code> 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<String, String> typePublic;
private Map<String, String> typeUri;
private Map<String, String> 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 <code>systemId</code> on the classpath configured by
* the <code>file</code>'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<String, String> 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<String, String>();
this.typeUri = new ConcurrentHashMap<String, String>();
Map<String, NamespaceDefinition> namespaceDefinitionRegistry = new HashMap<String, NamespaceDefinition>();
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
* <code>resolvedPath</code>.
*/
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 <code>spring.schemas</code> files on
* the project classpath.
*
* @param classLoader
* The classloader that is used to load the properties
*/
private Map<String, String> getSchemaMappings(ResourceLoader classLoader) {
Map<String, String> handlerMappings = new ConcurrentHashMap<String, String>();
// 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 <K, V> void mergePropertiesIntoMap(/*@Nullable*/ Properties props, Map<K, V> 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;
}
}

View File

@@ -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<IJavaProjectData, Future<ProjectClasspathUriResolver>> projectResolvers =
new ConcurrentHashMap<IJavaProjectData, Future<ProjectClasspathUriResolver>>();
private ProjectResourceLoaderCache loaderCache;
final private Consumer<IJavaProjectData> projectListener = project -> {
projectResolvers.remove(project);
List<String> 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<ProjectClasspathUriResolver> future = projectResolvers.get(project);
if (future == null) {
Callable<ProjectClasspathUriResolver> createResolver = new Callable<ProjectClasspathUriResolver>() {
public ProjectClasspathUriResolver call() throws InterruptedException {
ProjectClasspathUriResolver resolver = new ProjectClasspathUriResolver(loaderCache, project, isCachingDisabled());
return resolver;
}
};
FutureTask<ProjectClasspathUriResolver> futureTask = new FutureTask<ProjectClasspathUriResolver>(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;
}
}

View File

@@ -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<String, AtomicLong> jarScanned = Collections.synchronizedMap(new HashMap<String, AtomicLong>());
// private static final boolean DEBUG = false;
private static Set<String> _fetchedResources = Collections.synchronizedSet(new HashSet<>());
private static ImmutableSet<String> fetchedResources = ImmutableSet.copyOf(_fetchedResources);
private URL[] urls;
private ResourceLoader parent;
private Set<String> _indexValidFor = ImmutableSet.of();
private ImmutableSetMultimap<String, String> 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<String> 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<String> getResourcesCollection(String resourceName) {
if (!shouldFilter(resourceName)) {
// long start = System.currentTimeMillis();
try {
ensureIndexed(resourceName);
Collection<String> 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<String, AtomicLong> 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.
* <p>
* 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<String, String> buildIndex(Predicate<String> interestingResourceNames) {
ImmutableSetMultimap.Builder<String, String> 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<String> interesttingResourceNames,
File file,
ImmutableSetMultimap.Builder<String, String> resources
) {
try {
Path rootDir = file.toPath();
FileVisitor<Path> visitor = new FileVisitor<Path>() {
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<String> interestingResourceNames, URL url, ImmutableSetMultimap.Builder<String, String> 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<URL> getResources(String resourceName) {
if (!shouldFilter(resourceName)) {
Stream<URL> 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;
}
}

View File

@@ -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<ResourceLoaderCacheEntry> resourceLoaderCache = new ArrayList<ResourceLoaderCacheEntry>(CACHE_SIZE);
// private IResourceChangeListener resourceChangeListener = null;
private IJavaProjectProvider javaProjectProvider;
public ProjectResourceLoaderCache(IJavaProjectProvider javaProjectProvider) {
this.javaProjectProvider = javaProjectProvider;
}
private ResourceLoader addResourceLoaderToCache(IJavaProjectData project, List<URL> 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<ResourceLoaderCacheEntry> obsoleteResourceLoaders = new ArrayList<ResourceLoaderCacheEntry>(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 <code>paths</code>.
*/
private void addClassPathUrls(IJavaProjectData project, List<URL> paths, Set<IJavaProjectData> 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<URL> 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 <code>project</code> and all depending projects.
* <p>
* Note: if <code>useParentResourceLoader</code> 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<URL> getClassPathUrls(IJavaProjectData project, ResourceLoader parentResourceLoader) {
// needs to be linked to preserve ordering
List<URL> paths = new ArrayList<URL>();
Set<IJavaProjectData> resolvedProjects = new HashSet<IJavaProjectData>();
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<URL> 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<ResourceLoaderCacheEntry>(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<URL> 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<URL> jars = new LinkedHashSet<URL>();
List<URL> dirs = new ArrayList<URL>();
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;
// }
// }
//
// }
}

View File

@@ -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.
*
* <p>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.
* <p>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"));
}
}

View File

@@ -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<URL> 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<URL> 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;
}
}
}

View File

@@ -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<String> schemaLocations = new CopyOnWriteArraySet<String>();
private Set<String> uris = new CopyOnWriteArraySet<String>();
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<String> locations = new ArrayList<String>(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<String> 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;
}
}

View File

@@ -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<NamespaceVersion> {
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();
}
}

View File

@@ -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<Document> documents = new Stack<Document>();
private Stack<Node> elements = new Stack<Node>();
private Node lastElement = null;
private Document lastDocument = null;
private Map<Document, SchemaLocations> schemaLocations = new HashMap<Document, SchemaLocations>();
/** 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 <code>schemaLocation</code> attribute and offers accessors to the
* mapping.
*/
public static class SchemaLocations {
private Map<String, String> mapping = new HashMap<String, String>();
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);
}
}
}

View File

@@ -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
* <code>url</code>.
*/
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;
}
}

View File

@@ -0,0 +1 @@
org.springframework.ide.vscode.xml.SpringXmlPlugin

View File

@@ -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());
}
}

View File

@@ -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"

View File

@@ -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/