diff --git a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/DelegatingStreamConnectionProvider.java b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/DelegatingStreamConnectionProvider.java index 2564aa151..2bfad7b87 100644 --- a/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/DelegatingStreamConnectionProvider.java +++ b/eclipse-language-servers/org.springframework.tooling.boot.ls/src/org/springframework/tooling/boot/ls/DelegatingStreamConnectionProvider.java @@ -142,7 +142,8 @@ public class DelegatingStreamConnectionProvider implements StreamConnectionProvi FileSystems.getDefault().getPathMatcher("glob:**/*.java"), FileSystems.getDefault().getPathMatcher("glob:**/*.json"), FileSystems.getDefault().getPathMatcher("glob:**/*.yml"), - FileSystems.getDefault().getPathMatcher("glob:**/*.properties") + FileSystems.getDefault().getPathMatcher("glob:**/*.properties"), + FileSystems.getDefault().getPathMatcher("glob:**/META-INF/spring/*.factories") ))); //Add remote boot apps listener diff --git a/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/text/LanguageId.java b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/text/LanguageId.java index 0a8a385f1..4d4dd2be8 100644 --- a/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/text/LanguageId.java +++ b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/text/LanguageId.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2017, 2019 Pivotal, Inc. + * Copyright (c) 2017, 2022 Pivotal, Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -31,6 +31,7 @@ public class LanguageId { public static final LanguageId BOSH_CLOUD_CONFIG = of("bosh-cloud-config"); public static final LanguageId BOOT_PROPERTIES = of("spring-boot-properties"); public static final LanguageId BOOT_PROPERTIES_YAML = of("spring-boot-properties-yaml"); + public static final LanguageId SPRING_FACTORIES = of("spring-factories"); private final String id; diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerInitializer.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerInitializer.java index 161a1c5c4..c7d44ec3f 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerInitializer.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerInitializer.java @@ -264,10 +264,10 @@ public class BootLanguageServerInitializer implements InitializingBean { projectReconcileRequests.put(uri, Mono.delay(Duration.ofMillis(100)) .publishOn(projectReconcileScheduler) .doOnSuccess(l -> { + projectReconcileRequests.remove(uri); projectFinder.find(new TextDocumentIdentifier(uri.toString())).ifPresent(p -> { projectReconciler.reconcile(p, doc -> server.createProblemCollector(doc)); }); - projectReconcileRequests.remove(uri); }) .subscribe()); } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/SpringSymbolIndex.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/SpringSymbolIndex.java index bdd0343a2..68c8cd22c 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/SpringSymbolIndex.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/SpringSymbolIndex.java @@ -49,6 +49,7 @@ import org.springframework.ide.vscode.boot.java.handlers.EnhancedSymbolInformati import org.springframework.ide.vscode.boot.java.handlers.SymbolAddOnInformation; import org.springframework.ide.vscode.boot.java.handlers.SymbolProvider; import org.springframework.ide.vscode.boot.java.utils.DocumentDescriptor; +import org.springframework.ide.vscode.boot.java.utils.SpringFactoriesIndexer; import org.springframework.ide.vscode.boot.java.utils.SpringIndexer; import org.springframework.ide.vscode.boot.java.utils.SpringIndexerJava; import org.springframework.ide.vscode.boot.java.utils.SpringIndexerXML; @@ -125,6 +126,7 @@ public class SpringSymbolIndex implements InitializingBean { private SpringIndexerXML springIndexerXML; private SpringIndexerJava springIndexerJava; + private SpringFactoriesIndexer factoriesIndexer; private String watchXMLDeleteRegistration; private String watchXMLCreatedRegistration; @@ -163,8 +165,9 @@ public class SpringSymbolIndex implements InitializingBean { namespaceHandler.put("http://www.springframework.org/schema/beans", new SpringIndexerXMLNamespaceHandlerBeans()); springIndexerXML = new SpringIndexerXML(handler, namespaceHandler, this.cache, projectFinder()); springIndexerJava = new SpringIndexerJava(handler, specificProviders, this.cache, projectFinder()); + factoriesIndexer = new SpringFactoriesIndexer(handler, cache); - this.indexers = new SpringIndexer[] {springIndexerJava}; + this.indexers = new SpringIndexer[] {springIndexerJava, factoriesIndexer}; getWorkspaceService().onDidChangeWorkspaceFolders(evt -> { @@ -206,7 +209,8 @@ public class SpringSymbolIndex implements InitializingBean { } public void serverInitialized() { - List globPattern = Arrays.asList(springIndexerJava.getFileWatchPatterns()); + List globPattern = Stream.concat(Arrays.stream(springIndexerJava.getFileWatchPatterns()), Arrays.stream(factoriesIndexer.getFileWatchPatterns())) + .collect(Collectors.toList()); getWorkspaceService().getFileObserver().onFilesDeleted(globPattern, (files) -> { deleteDocuments(files); @@ -222,11 +226,11 @@ public class SpringSymbolIndex implements InitializingBean { public void configureIndexer(SymbolIndexConfig config) { synchronized (this) { if (config.isScanXml() && !(Arrays.asList(this.indexers).contains(springIndexerXML))) { - this.indexers = new SpringIndexer[] { springIndexerJava, springIndexerXML }; + this.indexers = new SpringIndexer[] { springIndexerJava, factoriesIndexer, springIndexerXML }; springIndexerXML.updateScanFolders(config.getXmlScanFolders()); addXmlFileListeners(Arrays.asList(springIndexerXML.getFileWatchPatterns())); } else if (!config.isScanXml() && Arrays.asList(this.indexers).contains(springIndexerXML)) { - this.indexers = new SpringIndexer[] { springIndexerJava }; + this.indexers = new SpringIndexer[] { springIndexerJava, factoriesIndexer }; springIndexerXML.updateScanFolders(new String[0]); removeXmlFileListeners(); } else if (config.isScanXml()) { diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/BeansSymbolProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/BeansSymbolProvider.java index a52307e98..65d06f5b6 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/BeansSymbolProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/BeansSymbolProvider.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2017, 2019 Pivotal, Inc. + * Copyright (c) 2017, 2022 Pivotal, Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -127,7 +127,7 @@ public class BeansSymbolProvider extends AbstractSymbolProvider { } } - protected String beanLabel(boolean isFunctionBean, String beanName, String beanType, String markerString) { + public static String beanLabel(boolean isFunctionBean, String beanName, String beanType, String markerString) { StringBuilder symbolLabel = new StringBuilder(); symbolLabel.append('@'); symbolLabel.append(isFunctionBean ? '>' : '+'); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/SpringFactoryInformation.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/SpringFactoryInformation.java new file mode 100644 index 000000000..2c54389b7 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/SpringFactoryInformation.java @@ -0,0 +1,27 @@ +/******************************************************************************* + * Copyright (c) 2022 VMware, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * VMware, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.beans; + +import org.springframework.ide.vscode.boot.java.handlers.SymbolAddOnInformation; + +public class SpringFactoryInformation implements SymbolAddOnInformation { + + private String key; + + public SpringFactoryInformation(String key) { + this.key = key; + } + + public String getKey() { + return key; + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/reconcile/BeanPostProcessingIgnoreInAotProblem.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/reconcile/BeanPostProcessingIgnoreInAotProblem.java index 55ff31298..753e76916 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/reconcile/BeanPostProcessingIgnoreInAotProblem.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/reconcile/BeanPostProcessingIgnoreInAotProblem.java @@ -56,11 +56,7 @@ public class BeanPostProcessingIgnoreInAotProblem implements RecipeSpringJavaPro .withFixes( new FixDescriptor(RECIPE_ID, List.of(uri), RecipeCodeActionDescriptor.buildLabel(LABEL, RecipeScope.NODE)) .withRangeScope(classDecl.getMarkers().findFirst(Range.class).orElse(null)) - .withRecipeScope(RecipeScope.NODE), - new FixDescriptor(RECIPE_ID, List.of(uri), RecipeCodeActionDescriptor.buildLabel(LABEL, RecipeScope.FILE)) - .withRecipeScope(RecipeScope.FILE), - new FixDescriptor(RECIPE_ID, List.of(uri), RecipeCodeActionDescriptor.buildLabel(LABEL, RecipeScope.PROJECT)) - .withRecipeScope(RecipeScope.PROJECT) + .withRecipeScope(RecipeScope.NODE) ); if (methods.isEmpty()) { // Didn't find a method. Default implementation return true therefore mark it. diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/reconcile/NotRegisteredBeansProblem.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/reconcile/NotRegisteredBeansProblem.java index 26db3a937..614ac1a63 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/reconcile/NotRegisteredBeansProblem.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/reconcile/NotRegisteredBeansProblem.java @@ -55,8 +55,7 @@ public class NotRegisteredBeansProblem implements RecipeSpringJavaProblemDescrip private static final List AOT_BEANS = List.of( "org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor", - "org.springframework.beans.factory.aot.BeanRegistrationAotProcessor", - "org.springframework.beans.factory.aot.RuntimeHintsRegistrar" + "org.springframework.beans.factory.aot.BeanRegistrationAotProcessor" ); @Override diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringFactoriesIndexer.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringFactoriesIndexer.java new file mode 100644 index 000000000..40b50d128 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringFactoriesIndexer.java @@ -0,0 +1,287 @@ +/******************************************************************************* + * Copyright (c) 2022 VMware, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * VMware, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.utils; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.commons.codec.digest.DigestUtils; +import org.eclipse.lsp4j.Location; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.SymbolKind; +import org.eclipse.lsp4j.WorkspaceSymbol; +import org.eclipse.lsp4j.jsonrpc.messages.Either; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ide.vscode.boot.java.beans.BeanUtils; +import org.springframework.ide.vscode.boot.java.beans.BeansSymbolAddOnInformation; +import org.springframework.ide.vscode.boot.java.beans.BeansSymbolProvider; +import org.springframework.ide.vscode.boot.java.beans.SpringFactoryInformation; +import org.springframework.ide.vscode.boot.java.handlers.EnhancedSymbolInformation; +import org.springframework.ide.vscode.boot.java.handlers.SymbolAddOnInformation; +import org.springframework.ide.vscode.commons.java.IClasspathUtil; +import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.protocol.java.Classpath; +import org.springframework.ide.vscode.commons.util.text.LanguageId; +import org.springframework.ide.vscode.commons.util.text.Region; +import org.springframework.ide.vscode.commons.util.text.TextDocument; +import org.springframework.ide.vscode.java.properties.antlr.parser.AntlrParser; +import org.springframework.ide.vscode.java.properties.parser.PropertiesAst; +import org.springframework.ide.vscode.java.properties.parser.PropertiesAst.KeyValuePair; +import org.springframework.ide.vscode.java.properties.parser.PropertiesAst.Node; + +import com.google.common.collect.ImmutableList; + +public class SpringFactoriesIndexer implements SpringIndexer { + + private static final Logger log = LoggerFactory.getLogger(SpringFactoriesIndexer.class); + + private static final String FILE_PATTERN = "**/META-INF/spring/*.factories"; + + private static final PathMatcher FILE_GLOB_PATTERN = FileSystems.getDefault().getPathMatcher("glob:" + FILE_PATTERN); + + private static final Set KEYS = Set.of( + "org.springframework.aot.hint.RuntimeHintsRegistrar", + "org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor", + "org.springframework.beans.factory.aot.BeanRegistrationAotProcessor" + ); + + private final SymbolHandler symbolHandler; + private final SymbolCache cache; + + public SpringFactoriesIndexer(SymbolHandler symbolHandler, SymbolCache cache) { + super(); + this.symbolHandler = symbolHandler; + this.cache = cache; + } + + @Override + public String[] getFileWatchPatterns() { + return new String[] { + FILE_PATTERN + }; + } + + @Override + public boolean isInterestedIn(String docURI) { + Path path = Paths.get(URI.create(docURI)); + return FILE_GLOB_PATTERN.matches(path); + } + + @Override + public List computeSymbols(IJavaProject project, String docURI, String content) + throws Exception { + return computeSymbols(docURI, content); + } + + private List computeSymbols(String docURI, String content) { + ImmutableList.Builder symbols = ImmutableList.builder(); + PropertiesAst ast = new AntlrParser().parse(content).ast; + if (ast != null) { + for (Node n : ast.getNodes(KeyValuePair.class::isInstance)) { + KeyValuePair pair = (KeyValuePair) n; + String key = pair.getKey().decode(); + + if (KEYS.contains(key)) { + String value = pair.getValue().decode(); + + TextDocument doc = new TextDocument(docURI, LanguageId.SPRING_FACTORIES, 0, content); + + for(String fqName : value.split("\\s*,\\s*")) { + try { + String simpleName = getSimpleName(fqName); + String beanId = BeanUtils.getBeanNameFromType(simpleName); + Range range = doc.toRange(new Region(pair.getOffset(), pair.getLength())); + symbols.add(new EnhancedSymbolInformation(new WorkspaceSymbol( + BeansSymbolProvider.beanLabel(false, beanId, fqName, Paths.get(URI.create(docURI)).getFileName().toString()), + SymbolKind.Interface, + Either.forLeft(new Location(docURI, range))), new SymbolAddOnInformation[] { + new BeansSymbolAddOnInformation(beanId, fqName), + new SpringFactoryInformation(key) + })); + } catch (Exception e) { + log.error("", e); + } + } + } + } + } + return symbols.build(); + } + + private static String getSimpleName(String fqName) { + int idx = fqName.lastIndexOf('.'); + if (idx >= 0 && idx < fqName.length() - 1) { + return fqName.substring(idx + 1); + } + return fqName; + } + + private SymbolCacheKey getCacheKey(IJavaProject project) { + String filesIndentifier = getFiles(project).stream() + .filter(f -> Files.isRegularFile(f)) + .map(f -> { + try { + return f.toAbsolutePath().toString() + "#" + Files.getLastModifiedTime(f).toMillis(); + } catch (IOException e) { + log.error("", e); + return f.toAbsolutePath().toString() + "#0"; + } + }) + .collect(Collectors.joining(",")); + return new SymbolCacheKey(project.getElementName() + "-factories-", DigestUtils.md5Hex(filesIndentifier).toUpperCase()); + } + + + @Override + public void initializeProject(IJavaProject project) throws Exception { + long startTime = System.currentTimeMillis(); + List files = getFiles(project); + String[] filesStr = files.stream().map(f -> f.toAbsolutePath().toString()).toArray(String[]::new); + + log.info("scan factories files for symbols for project: " + project.getElementName() + " - no. of files: " + files.size()); + + SymbolCacheKey cacheKey = getCacheKey(project); + + CachedSymbol[] symbols = this.cache.retrieveSymbols(cacheKey, filesStr); + if (symbols == null) { + List generatedSymbols = new ArrayList(); + + for (Path file : files) { + generatedSymbols.addAll(scanFile(file)); + } + + this.cache.store(cacheKey, filesStr, generatedSymbols, null); + + symbols = (CachedSymbol[]) generatedSymbols.toArray(new CachedSymbol[generatedSymbols.size()]); + } + else { + log.info("scan factories files used cached data: " + project.getElementName() + " - no. of cached symbols retrieved: " + symbols.length); + } + + if (symbols != null) { + for (int i = 0; i < symbols.length; i++) { + CachedSymbol symbol = symbols[i]; + symbolHandler.addSymbol(project, symbol.getDocURI(), symbol.getEnhancedSymbol()); + } + } + + long endTime = System.currentTimeMillis(); + + log.info("scan factories files for symbols for project: " + project.getElementName() + " took ms: " + (endTime - startTime)); + + } + + private List scanFile(Path file) { + try { + String content = Files.readString(file); + ImmutableList.Builder builder = ImmutableList.builder(); + long lastModified = Files.getLastModifiedTime(file).toMillis(); + String docUri = file.toUri().toString(); + for (EnhancedSymbolInformation s : computeSymbols(file.toUri().toString(), content)) { + builder.add(new CachedSymbol(docUri, lastModified, s)); + } + return builder.build(); + } catch (IOException e) { + log.error("", e); + return Collections.emptyList(); + } + } + + private List getFiles(IJavaProject project) { + try { + return project.getClasspath().getClasspathEntries().stream() + .filter(Classpath::isProjectSource) + .map(cpe -> new File(cpe.getPath()).toPath()) + .map(p -> p.resolve("META-INF").resolve("spring")) + .filter(Files::isDirectory) + .flatMap(d -> { + try { + return Files.list(d); + } catch (IOException e) { + // ignore + return Stream.empty(); + } + }) + .filter(p -> p.toString().endsWith(".factories")) + .collect(Collectors.toList()); + } catch (Exception e) { + log.error("", e); + return Collections.emptyList(); + } + + } + + @Override + public void removeProject(IJavaProject project) throws Exception { + SymbolCacheKey cacheKey = getCacheKey(project); + this.cache.remove(cacheKey); + } + + @Override + public void updateFile(IJavaProject project, DocumentDescriptor updatedDoc, String content) throws Exception { + this.symbolHandler.removeSymbols(project, updatedDoc.getDocURI()); + + List outputFolders = IClasspathUtil.getOutputFolders(project.getClasspath()).map(f -> f.toPath()).collect(Collectors.toList()); + String docURI = updatedDoc.getDocURI(); + Path path = Paths.get(URI.create(docURI)); + if (!outputFolders.stream().anyMatch(out -> path.startsWith(out))) { + List generatedSymbols = scanFile(path); + + SymbolCacheKey cacheKey = getCacheKey(project); + String file = new File(new URI(docURI)).getAbsolutePath(); + this.cache.update(cacheKey, file, updatedDoc.getLastModified(), generatedSymbols, null); + + for (CachedSymbol symbol : generatedSymbols) { + symbolHandler.addSymbol(project, symbol.getDocURI(), symbol.getEnhancedSymbol()); + } + } + } + + @Override + public void updateFiles(IJavaProject project, DocumentDescriptor[] updatedDocs) throws Exception { + SymbolCacheKey key = getCacheKey(project); + List outputFolders = IClasspathUtil.getOutputFolders(project.getClasspath()).map(f -> f.toPath()).collect(Collectors.toList()); + for (DocumentDescriptor d : updatedDocs) { + Path path = Paths.get(URI.create(d.getDocURI())); + if (!outputFolders.stream().anyMatch(out -> path.startsWith(out))) { + if (Files.isRegularFile(path)) { + updateFile(project, d, Files.readString(path)); + } else { + String file = new File(new URI(d.getDocURI())).getAbsolutePath(); + cache.removeFile(key, file); + } + } + } + } + + @Override + public void removeFiles(IJavaProject project, String[] docURIs) throws Exception { + SymbolCacheKey key = getCacheKey(project); + for (String docUri : docURIs) { + String file = new File(new URI(docUri)).getAbsolutePath(); + cache.removeFile(key, file); + } + } + +}