Add some beans to index from spring factories

This commit is contained in:
aboyko
2022-10-13 11:53:29 -04:00
parent 3cd3148068
commit f44c773be2
9 changed files with 331 additions and 16 deletions

View File

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

View File

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

View File

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

View File

@@ -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<String> globPattern = Arrays.asList(springIndexerJava.getFileWatchPatterns());
List<String> 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()) {

View File

@@ -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 ? '>' : '+');

View File

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

View File

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

View File

@@ -55,8 +55,7 @@ public class NotRegisteredBeansProblem implements RecipeSpringJavaProblemDescrip
private static final List<String> 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

View File

@@ -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<String> 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<EnhancedSymbolInformation> computeSymbols(IJavaProject project, String docURI, String content)
throws Exception {
return computeSymbols(docURI, content);
}
private List<EnhancedSymbolInformation> computeSymbols(String docURI, String content) {
ImmutableList.Builder<EnhancedSymbolInformation> 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<Path> 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<CachedSymbol> generatedSymbols = new ArrayList<CachedSymbol>();
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<CachedSymbol> scanFile(Path file) {
try {
String content = Files.readString(file);
ImmutableList.Builder<CachedSymbol> 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<Path> 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<Path> 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<CachedSymbol> 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<Path> 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);
}
}
}