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