diff --git a/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/UriUtil.java b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/UriUtil.java index d004451bf..0da4be4d2 100644 --- a/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/UriUtil.java +++ b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/UriUtil.java @@ -65,4 +65,20 @@ public class UriUtil { } } + public static File toFile(String docURI) { + try { + return new File(new URI(docURI)); + } catch (Exception e) { + return null; + } + } + + public static String toFileString(String docURI) { + try { + return new File(new URI(docURI)).getAbsolutePath(); + } catch (Exception e) { + return null; + } + } + } 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 0d6948545..96f27598b 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 @@ -90,7 +90,7 @@ public class SpringSymbolIndex implements InitializingBean { private final ConcurrentMap> symbolsByProject = new ConcurrentHashMap<>(); private final ExecutorService updateQueue = Executors.newSingleThreadExecutor(); - private SpringIndexer[] indexer; + private SpringIndexer[] indexers; private static final Logger log = LoggerFactory.getLogger(SpringSymbolIndex.class); @@ -153,7 +153,7 @@ public class SpringSymbolIndex implements InitializingBean { springIndexerXML = new SpringIndexerXML(handler, namespaceHandler, this.cache, projectFinder()); springIndexerJava = new SpringIndexerJava(handler, specificProviders, this.cache, projectFinder()); - this.indexer = new SpringIndexer[] {springIndexerJava}; + this.indexers = new SpringIndexer[] {springIndexerJava}; getWorkspaceService().onDidChangeWorkspaceFolders(evt -> { @@ -172,7 +172,7 @@ public class SpringSymbolIndex implements InitializingBean { if (BootJavaLanguageServerComponents.LANGUAGES.contains(document.getLanguageId())) { String docURI = document.getId().getUri(); String content = document.get(); - this.updateDocument(docURI, content); + this.updateDocument(docURI, content, "didSave event"); } }); config.addListener(evt -> { @@ -196,15 +196,15 @@ public class SpringSymbolIndex implements InitializingBean { createDocument(new TextDocumentIdentifier(file).getUri()); }); getWorkspaceService().getFileObserver().onFileChanged(globPattern, (file) -> { - updateDocument(new TextDocumentIdentifier(file).getUri(), null); + updateDocument(new TextDocumentIdentifier(file).getUri(), null, "file changed"); }); } public CompletableFuture configureIndexer(SymbolIndexConfig config) { List> futuresList = new ArrayList<>(); synchronized (this) { - if (config.isScanXml() && !(Arrays.asList(this.indexer).contains(springIndexerXML))) { - this.indexer = new SpringIndexer[] {springIndexerJava, springIndexerXML}; + if (config.isScanXml() && !(Arrays.asList(this.indexers).contains(springIndexerXML))) { + this.indexers = new SpringIndexer[] {springIndexerJava, springIndexerXML}; futuresList.add(CompletableFuture.runAsync(() -> springIndexerXML.setScanFolderGlobs(config.getXmlScanFoldersGlobs()))); List globPattern = Arrays.asList(springIndexerXML.getFileWatchPatterns()); @@ -216,12 +216,12 @@ public class SpringSymbolIndex implements InitializingBean { createDocument(new TextDocumentIdentifier(file).getUri()); }); watchXMLChangedRegistration = getWorkspaceService().getFileObserver().onFileChanged(globPattern, (file) -> { - updateDocument(new TextDocumentIdentifier(file).getUri(), null); + updateDocument(new TextDocumentIdentifier(file).getUri(), null, "xml changed"); }); } - else if (!config.isScanXml() && Arrays.asList(this.indexer).contains(springIndexerXML)) { - this.indexer = new SpringIndexer[] {springIndexerJava}; + else if (!config.isScanXml() && Arrays.asList(this.indexers).contains(springIndexerXML)) { + this.indexers = new SpringIndexer[] {springIndexerJava}; futuresList.add(CompletableFuture.runAsync(() -> springIndexerXML.setScanFolderGlobs(new String[0]))); getWorkspaceService().getFileObserver().unsubscribe(watchXMLChangedRegistration); @@ -267,9 +267,9 @@ public class SpringSymbolIndex implements InitializingBean { removeSymbolsByProject(project); @SuppressWarnings("unchecked") - CompletableFuture[] futures = new CompletableFuture[this.indexer.length]; - for (int i = 0; i < this.indexer.length; i++) { - InitializeProject initializeItem = new InitializeProject(project, this.indexer[i]); + CompletableFuture[] futures = new CompletableFuture[this.indexers.length]; + for (int i = 0; i < this.indexers.length; i++) { + InitializeProject initializeItem = new InitializeProject(project, this.indexers[i]); futures[i] = CompletableFuture.runAsync(initializeItem, this.updateQueue); } @@ -284,6 +284,10 @@ public class SpringSymbolIndex implements InitializingBean { } } + public SpringIndexerJava getJavaIndexer() { + return springIndexerJava; + } + public CompletableFuture deleteProject(IJavaProject project) { try { if (project.getElementName() == null) { @@ -291,7 +295,7 @@ public class SpringSymbolIndex implements InitializingBean { log.debug("Project with NULL name is being removed"); return CompletableFuture.completedFuture(null); } else { - DeleteProject initializeItem = new DeleteProject(project, this.indexer); + DeleteProject initializeItem = new DeleteProject(project, this.indexers); return CompletableFuture.runAsync(initializeItem, this.updateQueue); } } catch (Throwable e) { @@ -304,7 +308,7 @@ public class SpringSymbolIndex implements InitializingBean { synchronized(this) { List> futures = new ArrayList<>(); - for (SpringIndexer indexer : this.indexer) { + for (SpringIndexer indexer : this.indexers) { if (indexer.isInterestedIn(docURI)) { Optional maybeProject = projectFinder().find(new TextDocumentIdentifier(docURI)); @@ -320,9 +324,7 @@ public class SpringSymbolIndex implements InitializingBean { return ""; } }; - - UpdateItem updateItem = new UpdateItem(maybeProject.get(), docURI, lastModified, content, indexer); - futures.add(CompletableFuture.runAsync(updateItem, this.updateQueue)); + futures.add(CompletableFuture.runAsync(() -> updateItem(maybeProject.get(), docURI, lastModified, content, indexer), this.updateQueue)); } catch (Exception e) { log.error("", e); @@ -340,11 +342,12 @@ public class SpringSymbolIndex implements InitializingBean { return params.projectFinder; } - public CompletableFuture updateDocument(String docURI, String content) { + public CompletableFuture updateDocument(String docURI, String content, String reason) { + log.info("Update document [{}]: {}",reason, docURI); synchronized(this) { List> futures = new ArrayList<>(); - for (SpringIndexer indexer : this.indexer) { + for (SpringIndexer indexer : this.indexers) { if (indexer.isInterestedIn(docURI)) { Optional maybeProject = projectFinder().find(new TextDocumentIdentifier(docURI)); if (maybeProject.isPresent()) { @@ -365,8 +368,7 @@ public class SpringSymbolIndex implements InitializingBean { } }; - UpdateItem updateItem = new UpdateItem(maybeProject.get(), docURI, lastModified, contentSupplier, indexer); - futures.add(CompletableFuture.runAsync(updateItem, this.updateQueue)); + futures.add(updateItem(maybeProject.get(), docURI, lastModified, contentSupplier, indexer)); } catch (Exception e) { log.error("{}", e); @@ -383,7 +385,7 @@ public class SpringSymbolIndex implements InitializingBean { try { Optional maybeProject = projectFinder().find(new TextDocumentIdentifier(deletedDocURI)); if (maybeProject.isPresent()) { - DeleteItem deleteItem = new DeleteItem(maybeProject.get(), deletedDocURI, this.indexer); + DeleteItem deleteItem = new DeleteItem(maybeProject.get(), deletedDocURI, this.indexers); return CompletableFuture.runAsync(deleteItem, this.updateQueue); } } @@ -544,31 +546,17 @@ public class SpringSymbolIndex implements InitializingBean { } } - private class UpdateItem implements Runnable { - - private final String docURI; - private final Supplier content; - private final IJavaProject project; - private final SpringIndexer indexer; - private final long lastModified; - - public UpdateItem(IJavaProject project, String docURI, long lastModified, Supplier content, SpringIndexer indexer) { - this.project = project; - this.docURI = docURI; - this.lastModified = lastModified; - this.content = content; - this.indexer = indexer; - } - - @Override - public void run() { + CompletableFuture updateItem(IJavaProject project, String docURI, long lastModified, Supplier content, SpringIndexer indexer) { + log.debug("scheduling updateItem {}. {}, {}, {}", project.getElementName(), docURI, lastModified, indexer); + return CompletableFuture.runAsync(() -> { + log.debug("updateItem {}. {}, {}, {}", project.getElementName(), docURI, lastModified, indexer); try { removeSymbolsByDoc(project, docURI); indexer.updateFile(project, docURI, lastModified, content); } catch (Exception e) { log.error("{}", e); } - } + }, this.updateQueue); } private class DeleteItem implements Runnable { diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/RequestMappingSymbolProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/RequestMappingSymbolProvider.java index f78fb3814..8c7f3ea5f 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/RequestMappingSymbolProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/requestmapping/RequestMappingSymbolProvider.java @@ -15,7 +15,6 @@ import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Objects; -import java.util.stream.Collectors; import java.util.stream.Stream; import org.eclipse.jdt.core.dom.ASTNode; @@ -30,13 +29,13 @@ import org.eclipse.jdt.core.dom.TypeDeclaration; import org.eclipse.lsp4j.Location; import org.springframework.ide.vscode.boot.java.Annotations; import org.springframework.ide.vscode.boot.java.handlers.AbstractSymbolProvider; -import org.springframework.ide.vscode.boot.java.handlers.EnhancedSymbolInformation; -import org.springframework.ide.vscode.boot.java.handlers.SymbolProvider; import org.springframework.ide.vscode.boot.java.utils.ASTUtils; import org.springframework.ide.vscode.boot.java.utils.CachedSymbol; import org.springframework.ide.vscode.boot.java.utils.SpringIndexerJavaContext; import org.springframework.ide.vscode.commons.util.text.TextDocument; +import ch.qos.logback.core.Context; + /** * @author Martin Lippert */ @@ -48,11 +47,11 @@ public class RequestMappingSymbolProvider extends AbstractSymbolProvider { if (node.getParent() instanceof MethodDeclaration) { try { Location location = new Location(doc.getUri(), doc.toRange(node.getStartPosition(), node.getLength())); - String[] path = getPath(node); - String[] parentPath = getParentPath(node); - String[] methods = getMethod(node); - String[] contentTypes = getContentTypes(node); - String[] acceptTypes = getAcceptTypes(node); + String[] path = getPath(node, context); + String[] parentPath = getParentPath(node, context); + String[] methods = getMethod(node, context); + String[] contentTypes = getContentTypes(node, context); + String[] acceptTypes = getAcceptTypes(node, context); Stream stream = parentPath == null ? Stream.of("") : Arrays.stream(parentPath); stream.filter(Objects::nonNull) @@ -73,7 +72,7 @@ public class RequestMappingSymbolProvider extends AbstractSymbolProvider { } } - private String[] getMethod(Annotation node) { + private String[] getMethod(Annotation node, SpringIndexerJavaContext context) { String[] methods = null; if (node.isNormalAnnotation()) { @@ -86,7 +85,7 @@ public class RequestMappingSymbolProvider extends AbstractSymbolProvider { String valueName = pair.getName().getIdentifier(); if (valueName != null && valueName.equals("method")) { Expression expression = pair.getValue(); - methods = ASTUtils.getExpressionValueAsArray(expression); + methods = ASTUtils.getExpressionValueAsArray(expression, context::addDependency); break; } } @@ -98,14 +97,14 @@ public class RequestMappingSymbolProvider extends AbstractSymbolProvider { if (methods == null && node.getParent() instanceof MethodDeclaration) { Annotation parentAnnotation = getParentAnnotation(node); if (parentAnnotation != null) { - methods = getMethod(parentAnnotation); + methods = getMethod(parentAnnotation, context); } } return methods; } - private String[] getPath(Annotation node) { + private String[] getPath(Annotation node, SpringIndexerJavaContext context) { if (node.isNormalAnnotation()) { NormalAnnotation normNode = (NormalAnnotation) node; List values = normNode.values(); @@ -116,22 +115,22 @@ public class RequestMappingSymbolProvider extends AbstractSymbolProvider { String valueName = pair.getName().getIdentifier(); if (valueName != null && (valueName.equals("value") || valueName.equals("path"))) { Expression expression = pair.getValue(); - return ASTUtils.getExpressionValueAsArray(expression); + return ASTUtils.getExpressionValueAsArray(expression, context::addDependency); } } } } else if (node.isSingleMemberAnnotation()) { SingleMemberAnnotation singleNode = (SingleMemberAnnotation) node; Expression expression = singleNode.getValue(); - return ASTUtils.getExpressionValueAsArray(expression); + return ASTUtils.getExpressionValueAsArray(expression, context::addDependency); } return new String[] { "" }; } - private String[] getParentPath(Annotation node) { + private String[] getParentPath(Annotation node, SpringIndexerJavaContext context) { Annotation parentAnnotation = getParentAnnotation(node); - return parentAnnotation == null ? null : getPath(parentAnnotation); + return parentAnnotation == null ? null : getPath(parentAnnotation, context); } private Annotation getParentAnnotation(Annotation node) { @@ -178,7 +177,7 @@ public class RequestMappingSymbolProvider extends AbstractSymbolProvider { return null; } - private String[] getAcceptTypes(Annotation node) { + private String[] getAcceptTypes(Annotation node, SpringIndexerJavaContext context) { if (node.isNormalAnnotation()) { NormalAnnotation normNode = (NormalAnnotation) node; List values = normNode.values(); @@ -189,7 +188,7 @@ public class RequestMappingSymbolProvider extends AbstractSymbolProvider { String valueName = pair.getName().getIdentifier(); if (valueName != null && valueName.equals("consumes")) { Expression expression = pair.getValue(); - return ASTUtils.getExpressionValueAsArray(expression); + return ASTUtils.getExpressionValueAsArray(expression, context::addDependency); } } } @@ -197,7 +196,7 @@ public class RequestMappingSymbolProvider extends AbstractSymbolProvider { return new String[0]; } - private String[] getContentTypes(Annotation node) { + private String[] getContentTypes(Annotation node, SpringIndexerJavaContext context) { if (node.isNormalAnnotation()) { NormalAnnotation normNode = (NormalAnnotation) node; List values = normNode.values(); @@ -208,7 +207,7 @@ public class RequestMappingSymbolProvider extends AbstractSymbolProvider { String valueName = pair.getName().getIdentifier(); if (valueName != null && valueName.equals("produces")) { Expression expression = pair.getValue(); - return ASTUtils.getExpressionValueAsArray(expression); + return ASTUtils.getExpressionValueAsArray(expression, context::addDependency); } } } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/ASTUtils.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/ASTUtils.java index d4b4e8ee8..704182d53 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/ASTUtils.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/ASTUtils.java @@ -14,8 +14,10 @@ import java.util.Collection; import java.util.List; import java.util.Objects; import java.util.Optional; +import java.util.function.Consumer; import java.util.stream.Stream; +import org.eclipse.jdt.core.IJavaElement; import org.eclipse.jdt.core.dom.ASTNode; import org.eclipse.jdt.core.dom.Annotation; import org.eclipse.jdt.core.dom.ArrayInitializer; @@ -179,20 +181,24 @@ public class ASTUtils { } } - public static String getExpressionValueAsString(Expression exp) { + public static String getExpressionValueAsString(Expression exp, Consumer dependencies) { if (exp instanceof StringLiteral) { return getLiteralValue((StringLiteral) exp); } else if (exp instanceof Name) { IBinding binding = ((Name) exp).resolveBinding(); if (binding != null && binding.getKind() == IBinding.VARIABLE) { IVariableBinding varBinding = (IVariableBinding) binding; + ITypeBinding klass = varBinding.getDeclaringClass(); + if (klass!=null) { + dependencies.accept(klass); + } Object constValue = varBinding.getConstantValue(); if (constValue != null) { return constValue.toString(); } } if (exp instanceof QualifiedName) { - return getExpressionValueAsString(((QualifiedName) exp).getName()); + return getExpressionValueAsString(((QualifiedName) exp).getName(), dependencies); } else if (exp instanceof SimpleName) { return ((SimpleName) exp).getIdentifier(); @@ -206,13 +212,13 @@ public class ASTUtils { } @SuppressWarnings("unchecked") - public static String[] getExpressionValueAsArray(Expression exp) { + public static String[] getExpressionValueAsArray(Expression exp, Consumer dependencies) { if (exp instanceof ArrayInitializer) { ArrayInitializer array = (ArrayInitializer) exp; - return ((List) array.expressions()).stream().map(e -> getExpressionValueAsString(e)) + return ((List) array.expressions()).stream().map(e -> getExpressionValueAsString(e, dependencies)) .filter(Objects::nonNull).toArray(String[]::new); } else { - String rm = getExpressionValueAsString(exp); + String rm = getExpressionValueAsString(exp, dependencies); if (rm != null) { return new String[] { rm }; } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/FileScanListener.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/FileScanListener.java new file mode 100644 index 000000000..d09f8679d --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/FileScanListener.java @@ -0,0 +1,15 @@ +/******************************************************************************* + * Copyright (c) 2019 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.utils; + +public interface FileScanListener { + void fileScanned(String path); +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexerJava.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexerJava.java index a9764fd4d..78fbffc5b 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexerJava.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexerJava.java @@ -18,14 +18,17 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collection; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; import java.util.stream.Stream; import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.tuple.Pair; import org.eclipse.jdt.core.JavaCore; import org.eclipse.jdt.core.dom.AST; import org.eclipse.jdt.core.dom.ASTParser; @@ -54,12 +57,14 @@ import org.springframework.ide.vscode.commons.util.UriUtil; import org.springframework.ide.vscode.commons.util.text.TextDocument; import com.google.common.base.Supplier; +import com.google.common.collect.Multimap; +import com.google.common.collect.MultimapBuilder; /** * @author Martin Lippert */ public class SpringIndexerJava implements SpringIndexer { - + public static enum SCAN_PASS { ONE, TWO } @@ -71,7 +76,48 @@ public class SpringIndexerJava implements SpringIndexer { private final SymbolCache cache; private final JavaProjectFinder projectFinder; private boolean scanTestJavaSources = false; + private FileScanListener fileScanListener = null; //used by test code only + private final DependencyTracker dependencyTracker = new DependencyTracker(); + + public DependencyTracker getDependencyTracker() { + return dependencyTracker; + } + + public static class DependencyTracker { + + private Multimap dependencies = MultimapBuilder.hashKeys().hashSetValues().build(); + + public void addDependency(String sourceFile, ITypeBinding dependsOn) { + dependencies.put(sourceFile, dependsOn.getKey()); + } + + public void dump() { + log.info("=== Dependencies ==="); + for (String sourceFile : dependencies.keySet()) { + Collection values = dependencies.get(sourceFile); + if (!values.isEmpty()) + log.info(sourceFile + "=> "); + for (String v : values) { + log.info(" "+v); + } + } + log.info("======================"); + } + + public Multimap getAllDependencies() { + return dependencies; + } + + public void update(String file, Set dependenciesForFile) { + dependencies.replaceValues(file, dependenciesForFile); + } + + public void restore(Multimap deps) { + this.dependencies = deps; + } + } + public SpringIndexerJava(SymbolHandler symbolHandler, AnnotationHierarchyAwareLookup symbolProviders, SymbolCache cache, JavaProjectFinder projectFimder) { this.symbolHandler = symbolHandler; @@ -132,8 +178,87 @@ public class SpringIndexerJava implements SpringIndexer { } private void scanFile(IJavaProject project, String docURI, long lastModified, String content) throws Exception { + //TODO: optimise? Check last modified to avoid redundant scan. Reason: + // on saving a file, this may be triggered twice. Once when file is saved and once more because of a 'file changed' + // on file system. Looking at the timestamp in the cache we should be able to avoid a second scan of the exact same + // content. ASTParser parser = createParser(project, false); + String unitName = docURI.substring(docURI.lastIndexOf("/")); + parser.setUnitName(unitName); + log.debug("Scan file: {}", unitName); + parser.setSource(content.toCharArray()); + + CompilationUnit cu = (CompilationUnit) parser.createAST(null); + + if (cu != null) { + List generatedSymbols = new ArrayList(); + AtomicReference docRef = new AtomicReference<>(); + String file = UriUtil.toFileString(docURI); + Set changedTypes = new HashSet<>(); + SpringIndexerJavaContext context = new SpringIndexerJavaContext(project, cu, docURI, file, + lastModified, docRef, content, generatedSymbols, SCAN_PASS.ONE, new ArrayList<>(), changedTypes); + + scanAST(context); + + SymbolCacheKey cacheKey = getCacheKey(project); + this.cache.update(cacheKey, file, lastModified, generatedSymbols, context.getDependencies()); +// dependencyTracker.dump(); + + for (CachedSymbol symbol : generatedSymbols) { + symbolHandler.addSymbol(project, symbol.getDocURI(), symbol.getEnhancedSymbol()); + } + Set scannedFiles = new HashSet<>(); + scannedFiles.add(file); + fileScannedEvent(file); + scanAffectedFiles(project, changedTypes, scannedFiles); + } + } + + private void fileScannedEvent(String file) { + if (fileScanListener!=null) { + fileScanListener.fileScanned(file); + } + } + + private void scanAffectedFiles(IJavaProject project, Set changedTypes, Set scannedFiles) { + log.info("Start scanning affected files for types {}", changedTypes); + //TODO: optimise? When multiple files are 'affected', we could try to parse and scan them in batch. + // I.e. something similar to the 'scanFiles' method. + // That is probably more efficient than one by one. + + Multimap dependencies = dependencyTracker.getAllDependencies(); +// Collection filesToScan = new HashSet<>(); + boolean scannedAnyFiles; + do { + scannedAnyFiles = false; + for (String file : dependencies.keys()) { + try { + if (!scannedFiles.contains(file)) { + Collection dependsOn = dependencies.get(file); + if (dependsOn.stream().anyMatch(changedTypes::contains)) { + scannedFiles.add(file); + scannedAnyFiles = true; + log.debug("Should also scan affected file: {}", file); + File f = new File(file); + scanAffectedFile(project, UriUtil.toUri(f).toString(), f.lastModified(), FileUtils.readFileToString(f), changedTypes); + fileScannedEvent(file); + } + } + } catch (Exception e) { + log.debug("Problems scanning file {}", file, e); + } + } + if (scannedAnyFiles) { + log.debug("Some affected files where scanned, make another pass"); + } + } while (scannedAnyFiles); + log.info("Finished scanning affected files {}", scannedFiles); + } + + private void scanAffectedFile(IJavaProject project, String docURI, long lastModified, String content, Set changedTypes) throws Exception { + symbolHandler.removeSymbols(project, docURI); + ASTParser parser = createParser(project, false); String unitName = docURI.substring(docURI.lastIndexOf("/")); parser.setUnitName(unitName); parser.setSource(content.toCharArray()); @@ -143,27 +268,32 @@ public class SpringIndexerJava implements SpringIndexer { if (cu != null) { List generatedSymbols = new ArrayList(); AtomicReference docRef = new AtomicReference<>(); - String file = new File(new URI(docURI)).getAbsolutePath(); + File file = UriUtil.toFile(docURI); + if (file!=null) { + SpringIndexerJavaContext context = new SpringIndexerJavaContext( + project, cu, + docURI, file.toString(), lastModified, docRef, + content, generatedSymbols, + SCAN_PASS.ONE, new ArrayList<>(), changedTypes); + scanAST(context); - SpringIndexerJavaContext context = new SpringIndexerJavaContext(project, cu, docURI, file, - lastModified, docRef, content, generatedSymbols, SCAN_PASS.ONE, new ArrayList<>()); + SymbolCacheKey cacheKey = getCacheKey(project); + this.cache.update(cacheKey, file.getAbsolutePath(), lastModified, generatedSymbols, context.getDependencies()); +// dependencyTracker.dump(); - scanAST(context); - - SymbolCacheKey cacheKey = getCacheKey(project); - this.cache.update(cacheKey, file, lastModified, generatedSymbols); - - for (CachedSymbol symbol : generatedSymbols) { - symbolHandler.addSymbol(project, symbol.getDocURI(), symbol.getEnhancedSymbol()); + for (CachedSymbol symbol : generatedSymbols) { + symbolHandler.addSymbol(project, symbol.getDocURI(), symbol.getEnhancedSymbol()); + } } } } private void scanFiles(IJavaProject project, String[] javaFiles) throws Exception { SymbolCacheKey cacheKey = getCacheKey(project); - CachedSymbol[] symbols = this.cache.retrieve(cacheKey, javaFiles); + Pair> cached = this.cache.retrieve(cacheKey, javaFiles); - if (symbols == null) { + CachedSymbol[] symbols; + if (cached == null) { List generatedSymbols = new ArrayList(); log.info("scan java files, AST parse, pass 1 for files: {}", javaFiles.length); @@ -176,12 +306,16 @@ public class SpringIndexerJava implements SpringIndexer { scanFiles(project, pass2Files, generatedSymbols, SCAN_PASS.TWO); } - this.cache.store(cacheKey, javaFiles, generatedSymbols); + this.cache.store(cacheKey, javaFiles, generatedSymbols, dependencyTracker.getAllDependencies()); +// dependencyTracker.dump(); symbols = (CachedSymbol[]) generatedSymbols.toArray(new CachedSymbol[generatedSymbols.size()]); } else { + symbols = cached.getLeft(); log.info("scan java files used cached data: {} - no. of cached symbols retrieved: {}", project.getElementName(), symbols.length); + this.dependencyTracker.restore(cached.getRight()); + log.info("scan java files restored cached dependency data: {} - no. of cached dependencies: {}", cached.getRight().size()); } if (symbols != null) { @@ -206,7 +340,7 @@ public class SpringIndexerJava implements SpringIndexer { AtomicReference docRef = new AtomicReference<>(); SpringIndexerJavaContext context = new SpringIndexerJavaContext(project, cu, docURI, sourceFilePath, - lastModified, docRef, null, generatedSymbols, pass, nextPassFiles); + lastModified, docRef, null, generatedSymbols, pass, nextPassFiles, null); scanAST(context); } @@ -218,12 +352,18 @@ public class SpringIndexerJava implements SpringIndexer { } private void scanAST(final SpringIndexerJavaContext context) { - context.getCu().accept(new ASTVisitor() { @Override public boolean visit(TypeDeclaration node) { try { + Set changedTypes = context.getChangedTypes(); + if (changedTypes!=null) { + ITypeBinding changedType = node.resolveBinding(); + if (changedType!=null) { + changedTypes.add(changedType.getKey()); + } + } extractSymbolInformation(node, context); } catch (Exception e) { @@ -279,6 +419,7 @@ public class SpringIndexerJava implements SpringIndexer { return super.visit(node); } }); + dependencyTracker.update(context.getFile(), context.getDependencies());; } private void extractSymbolInformation(TypeDeclaration typeDeclaration, final SpringIndexerJavaContext context) throws Exception { @@ -471,4 +612,8 @@ public class SpringIndexerJava implements SpringIndexer { } } + public void setFileScanListener(FileScanListener fileScanListener) { + this.fileScanListener = fileScanListener; + } + } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexerJavaContext.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexerJavaContext.java index 1c496c477..58749885e 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexerJavaContext.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexerJavaContext.java @@ -10,10 +10,13 @@ *******************************************************************************/ package org.springframework.ide.vscode.boot.java.utils; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.concurrent.atomic.AtomicReference; import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.dom.ITypeBinding; import org.springframework.ide.vscode.boot.java.utils.SpringIndexerJava.SCAN_PASS; import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.util.text.TextDocument; @@ -33,10 +36,22 @@ public class SpringIndexerJavaContext { private final List generatedSymbols; private final SCAN_PASS pass; private final List nextPassFiles; + private final Set dependencies = new HashSet<>(); + private final Set dependentTypes; - public SpringIndexerJavaContext(IJavaProject project, CompilationUnit cu, String docURI, String file, long lastModified, - AtomicReference docRef, String content, List generatedSymbols, SCAN_PASS pass, - List nextPassFiles) { + public SpringIndexerJavaContext( + IJavaProject project, + CompilationUnit cu, + String docURI, + String file, + long lastModified, + AtomicReference docRef, + String content, + List generatedSymbols, + SCAN_PASS pass, + List nextPassFiles, + Set dependentTypes + ) { super(); this.project = project; this.cu = cu; @@ -48,6 +63,7 @@ public class SpringIndexerJavaContext { this.generatedSymbols = generatedSymbols; this.pass = pass; this.nextPassFiles = nextPassFiles; + this.dependentTypes = dependentTypes; } public IJavaProject getProject() { @@ -90,4 +106,16 @@ public class SpringIndexerJavaContext { return nextPassFiles; } + public Set getDependencies() { + return dependencies; + } + + public void addDependency(ITypeBinding dependsOn) { + dependencies.add(dependsOn.getKey()); + } + + public Set getChangedTypes() { + return dependentTypes; + } + } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexerXML.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexerXML.java index e1a61f711..ad5985012 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexerXML.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexerXML.java @@ -105,7 +105,7 @@ public class SpringIndexerXML implements SpringIndexer { long startTime = System.currentTimeMillis(); SymbolCacheKey cacheKey = getCacheKey(project); - CachedSymbol[] symbols = this.cache.retrieve(cacheKey, files); + CachedSymbol[] symbols = this.cache.retrieveSymbols(cacheKey, files); if (symbols == null) { List generatedSymbols = new ArrayList(); @@ -113,7 +113,7 @@ public class SpringIndexerXML implements SpringIndexer { scanFile(project, file, generatedSymbols); } - this.cache.store(cacheKey, files, generatedSymbols); + this.cache.store(cacheKey, files, generatedSymbols, null); symbols = (CachedSymbol[]) generatedSymbols.toArray(new CachedSymbol[generatedSymbols.size()]); } @@ -148,7 +148,7 @@ public class SpringIndexerXML implements SpringIndexer { SymbolCacheKey cacheKey = getCacheKey(project); String file = new File(new URI(docURI)).getAbsolutePath(); - this.cache.update(cacheKey, file, lastModified, generatedSymbols); + this.cache.update(cacheKey, file, lastModified, generatedSymbols, null); for (CachedSymbol symbol : generatedSymbols) { symbolHandler.addSymbol(project, symbol.getDocURI(), symbol.getEnhancedSymbol()); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SymbolCache.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SymbolCache.java index 9aa8d1893..d895b9364 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SymbolCache.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SymbolCache.java @@ -11,18 +11,27 @@ package org.springframework.ide.vscode.boot.java.utils; import java.util.List; +import java.util.Set; + +import org.apache.commons.lang3.tuple.Pair; + +import com.google.common.collect.Multimap; /** * @author Martin Lippert */ public interface SymbolCache { - void store(SymbolCacheKey cacheKey, String[] files, List generatedSymbols); - CachedSymbol[] retrieve(SymbolCacheKey cacheKey, String[] files); + void store(SymbolCacheKey cacheKey, String[] files, List generatedSymbols, Multimap dependencies); + Pair> retrieve(SymbolCacheKey cacheKey, String[] files); - void update(SymbolCacheKey cacheKey, String file, long lastModified, List generatedSymbols); + void update(SymbolCacheKey cacheKey, String file, long lastModified, List generatedSymbols, Set dependencies); void remove(SymbolCacheKey cacheKey); void removeFile(SymbolCacheKey symbolCacheKey, String file); + default CachedSymbol[] retrieveSymbols(SymbolCacheKey cacheKey, String[] files) { + Pair> r = retrieve(cacheKey, files); + return r!=null ? r.getLeft() : null; + } } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SymbolCacheOnDisc.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SymbolCacheOnDisc.java index ac21787d0..2b51fcd67 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SymbolCacheOnDisc.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SymbolCacheOnDisc.java @@ -17,18 +17,27 @@ import java.io.IOException; import java.lang.reflect.Type; import java.nio.file.Files; import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; import java.util.SortedMap; import java.util.TreeMap; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; +import org.apache.commons.lang3.tuple.Pair; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ide.vscode.boot.java.handlers.SymbolAddOnInformation; import org.springframework.ide.vscode.commons.util.UriUtil; +import com.google.common.collect.ImmutableMultimap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Multimap; +import com.google.common.collect.MultimapBuilder; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonDeserializationContext; @@ -69,7 +78,10 @@ public class SymbolCacheOnDisc implements SymbolCache { } @Override - public void store(SymbolCacheKey cacheKey, String[] files, List generatedSymbols) { + public void store(SymbolCacheKey cacheKey, String[] files, List generatedSymbols, Multimap dependencies) { + if (dependencies==null) { + dependencies = ImmutableMultimap.of(); + } SortedMap timestampedFiles = new TreeMap<>(); timestampedFiles = Arrays.stream(files) @@ -82,11 +94,11 @@ public class SymbolCacheOnDisc implements SymbolCache { } }, (v1,v2) -> { throw new RuntimeException(String.format("Duplicate key for values %s and %s", v1, v2));}, TreeMap::new)); - save(cacheKey, generatedSymbols, timestampedFiles); + save(cacheKey, generatedSymbols, timestampedFiles, dependencies.asMap()); } @Override - public CachedSymbol[] retrieve(SymbolCacheKey cacheKey, String[] files) { + public Pair> retrieve(SymbolCacheKey cacheKey, String[] files) { try { File cacheStore = new File(cacheDirectory, cacheKey.toString() + ".json"); if (cacheStore.exists()) { @@ -108,7 +120,15 @@ public class SymbolCacheOnDisc implements SymbolCache { this.stores.put(cacheKey, store); List symbols = store.getSymbols(); - return (CachedSymbol[]) symbols.toArray(new CachedSymbol[symbols.size()]); + Map> storedDependencies = store.getDependencies(); + Multimap dependencies = MultimapBuilder.hashKeys().hashSetValues().build(); + for (Entry> entry : storedDependencies.entrySet()) { + dependencies.replaceValues(entry.getKey(), entry.getValue()); + } + return Pair.of( + (CachedSymbol[]) symbols.toArray(new CachedSymbol[symbols.size()]), + MultimapBuilder.hashKeys().hashSetValues().build(dependencies) + ); } } } @@ -130,8 +150,9 @@ public class SymbolCacheOnDisc implements SymbolCache { List cachedSymbols = cacheStore.getSymbols().stream() .filter(cachedSymbol -> !cachedSymbol.getDocURI().equals(docURI)) .collect(Collectors.toList()); - - save(cacheKey, cachedSymbols, timestampedFiles); + Map> changedDeps = new HashMap<>(cacheStore.getDependencies()); + changedDeps.remove(file); + save(cacheKey, cachedSymbols, timestampedFiles, changedDeps); } } @@ -145,7 +166,10 @@ public class SymbolCacheOnDisc implements SymbolCache { } @Override - public void update(SymbolCacheKey cacheKey, String file, long lastModified, List generatedSymbols) { + public void update(SymbolCacheKey cacheKey, String file, long lastModified, List generatedSymbols, Set dependencies) { + if (dependencies==null) { + dependencies = ImmutableSet.of(); + } CacheStore cacheStore = this.stores.get(cacheKey); if (cacheStore != null) { @@ -159,14 +183,20 @@ public class SymbolCacheOnDisc implements SymbolCache { .collect(Collectors.toList()); cachedSymbols.addAll(generatedSymbols); - - save(cacheKey, cachedSymbols, timestampedFiles); + + Map> changedDependencies = new HashMap<>(cacheStore.getDependencies()); + if (dependencies.isEmpty()) { + changedDependencies.remove(file); + } else { + changedDependencies.put(file, ImmutableSet.copyOf(dependencies)); + } + save(cacheKey, cachedSymbols, timestampedFiles, changedDependencies); } } private void save(SymbolCacheKey cacheKey, List generatedSymbols, - SortedMap timestampedFiles) { - CacheStore store = new CacheStore(cacheKey.toString(), timestampedFiles, generatedSymbols); + SortedMap timestampedFiles, Map> dependencies) { + CacheStore store = new CacheStore(timestampedFiles, generatedSymbols, dependencies); this.stores.put(cacheKey, store); try (FileWriter writer = new FileWriter(new File(cacheDirectory, cacheKey.toString() + ".json"))) @@ -216,19 +246,19 @@ public class SymbolCacheOnDisc implements SymbolCache { */ private static class CacheStore { - private final String cacheKey; private final SortedMap timestampedFiles; private final List symbols; + private final Map> dependencies; - public CacheStore(String cacheKey, SortedMap timestampedFiles, List symbols) { + public CacheStore(SortedMap timestampedFiles, List symbols, Map> dependencies) { super(); - this.cacheKey = cacheKey; this.timestampedFiles = timestampedFiles; this.symbols = symbols; + this.dependencies = dependencies; } - public String getCacheKey() { - return cacheKey; + public Map> getDependencies() { + return dependencies; } public List getSymbols() { diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SymbolCacheVoid.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SymbolCacheVoid.java index b6f7e300f..5e568aef2 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SymbolCacheVoid.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SymbolCacheVoid.java @@ -11,6 +11,11 @@ package org.springframework.ide.vscode.boot.java.utils; import java.util.List; +import java.util.Set; + +import org.apache.commons.lang3.tuple.Pair; + +import com.google.common.collect.Multimap; /** * @author Martin Lippert @@ -18,16 +23,16 @@ import java.util.List; public class SymbolCacheVoid implements SymbolCache { @Override - public void store(SymbolCacheKey cacheKey, String[] files, List generatedSymbols) { + public void store(SymbolCacheKey cacheKey, String[] files, List generatedSymbols, Multimap dependencies) { } @Override - public CachedSymbol[] retrieve(SymbolCacheKey cacheKey, String[] files) { + public Pair> retrieve(SymbolCacheKey cacheKey, String[] files) { return null; } @Override - public void update(SymbolCacheKey cacheKey, String file, long lastModified, List generatedSymbols) { + public void update(SymbolCacheKey cacheKey, String file, long lastModified, List generatedSymbols, Set dependencies) { } @Override @@ -38,4 +43,5 @@ public class SymbolCacheVoid implements SymbolCache { public void removeFile(SymbolCacheKey symbolCacheKey, String file) { } + } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/metadata/AdHocSpringPropertyIndexProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/metadata/AdHocSpringPropertyIndexProvider.java index c993861de..df65b996d 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/metadata/AdHocSpringPropertyIndexProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/metadata/AdHocSpringPropertyIndexProvider.java @@ -63,9 +63,9 @@ public class AdHocSpringPropertyIndexProvider implements ProjectBasedPropertyInd "**/application.properties", "**/application.yml" ), changed -> { - log.info("File changed: "+changed); + log.debug("File changed: {}", changed); projectFinder.find(new TextDocumentIdentifier(changed)).ifPresent(project -> { - log.info("=> Project changed: "+project.getElementName()); + log.debug("=> Project changed: {}", project.getElementName()); indexes.invalidate(project); }); }); diff --git a/headless-services/spring-boot-language-server/src/main/resources/application.properties b/headless-services/spring-boot-language-server/src/main/resources/application.properties index dbf34f065..ab8019fe5 100644 --- a/headless-services/spring-boot-language-server/src/main/resources/application.properties +++ b/headless-services/spring-boot-language-server/src/main/resources/application.properties @@ -4,3 +4,7 @@ languageserver.completion-trigger-characters.xml: languageserver.completion-trigger-characters.spring-boot-properties-yaml: abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ. languageserver.completion-trigger-characters.spring-boot-properties: abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ. spring.main.banner-mode: off + +#logging.level.org.springframework.ide.vscode.boot.java.utils.SpringIndexerJava=debug +#logging.level.org.springframework.ide.vscode.boot.app.SpringSymbolIndex=debug +#logging.level.org.springframework.ide.vscode.commons.boot.app.cli.SpringBootApp=off \ No newline at end of file diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/requestmapping/test/RequestMappingDependentConstantChangedTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/requestmapping/test/RequestMappingDependentConstantChangedTest.java new file mode 100644 index 000000000..7d28dc54c --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/requestmapping/test/RequestMappingDependentConstantChangedTest.java @@ -0,0 +1,165 @@ +package org.springframework.ide.vscode.boot.java.requestmapping.test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +import org.apache.commons.io.FileUtils; +import org.eclipse.lsp4j.Location; +import org.eclipse.lsp4j.SymbolInformation; +import org.eclipse.lsp4j.TextDocumentIdentifier; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Import; +import org.springframework.ide.vscode.boot.app.SpringSymbolIndex; +import org.springframework.ide.vscode.boot.bootiful.BootLanguageServerTest; +import org.springframework.ide.vscode.boot.bootiful.SymbolProviderTestConf; +import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; +import org.springframework.ide.vscode.commons.maven.java.MavenJavaProject; +import org.springframework.ide.vscode.commons.util.UriUtil; +import org.springframework.ide.vscode.commons.util.text.LanguageId; +import org.springframework.ide.vscode.commons.util.text.TextDocument; +import org.springframework.ide.vscode.project.harness.BootLanguageServerHarness; +import org.springframework.ide.vscode.project.harness.ProjectsHarness; +import org.springframework.ide.vscode.project.harness.ProjectsHarness.CustomizableProjectContent; +import org.springframework.test.context.junit4.SpringRunner; + +@RunWith(SpringRunner.class) +@BootLanguageServerTest +@Import(SymbolProviderTestConf.class) +public class RequestMappingDependentConstantChangedTest { + + @Autowired private BootLanguageServerHarness harness; + @Autowired private JavaProjectFinder projectFinder; + @Autowired private SpringSymbolIndex indexer; + + ProjectsHarness projects = ProjectsHarness.INSTANCE; + private MavenJavaProject project; + private Path directory; + + @Before + public void setup() throws Exception { + harness.intialize(null); + + project = (MavenJavaProject) projects.mavenProject("test-request-mapping-symbols", false, new ProjectsHarness.ProjectCustomizer() { + @Override + public void customize(CustomizableProjectContent projectContent) throws Exception { + //dummy (forces every test to use a new copy of the project, we do this because project + //files are being mutated! + } + }); + directory = new File(project.getLocationUri()).toPath(); + + // trigger project creation + projectFinder.find(new TextDocumentIdentifier(project.getLocationUri().toString())).get(); + + CompletableFuture initProject = indexer.waitOperation(); + initProject.get(5, TimeUnit.SECONDS); + } + + @Test + public void testSimpleRequestMappingSymbolFromConstantInDifferentClass() throws Exception { + String docUri = directory.resolve("src/main/java/org/test/SimpleMappingClassWithConstantInDifferentClass.java").toUri().toString(); + String constantsUri = directory.resolve("src/main/java/org/test/Constants.java").toUri().toString(); + List symbols = indexer.getSymbols(docUri); + assertEquals(1, symbols.size()); + assertSymbol(docUri, "@/path/from/constant", "@RequestMapping(Constants.REQUEST_MAPPING_PATH)"); + + TestFileScanListener fileScanListener = new TestFileScanListener(); + indexer.getJavaIndexer().setFileScanListener(fileScanListener); + + replaceInFile(constantsUri, "path/from/constant", "/changed-path"); + fileScanListener.assertScannedUris(constantsUri, docUri); + fileScanListener.assertScannedUri(constantsUri, 1); + fileScanListener.assertScannedUri(docUri, 1); + + symbols = indexer.getSymbols(docUri); + assertSymbolCount(1, symbols); + assertSymbol(docUri, "@/changed-path", "@RequestMapping(Constants.REQUEST_MAPPING_PATH)"); + } + + @Test public void testCyclicalDependency() throws Exception { + //cyclical dependency between two files (ping refers pong and vice versa) + + String pingUri = directory.resolve("src/main/java/org/test/PingConstantRequestMapping.java").toUri().toString(); + String pongUri = directory.resolve("src/main/java/org/test/PongConstantRequestMapping.java").toUri().toString(); + + { + List symbols = indexer.getSymbols(pingUri); + for (SymbolInformation s : symbols) { + System.out.println(s.getName()); + } + assertSymbolCount(1, symbols); + assertSymbol(pingUri, "@/pong -- GET", "@GetMapping(PongConstantRequestMapping.PONG)"); + } + { + List symbols = indexer.getSymbols(pongUri); + assertSymbolCount(1, symbols); + assertSymbol(pongUri, "@/ping -- GET", "@GetMapping(PingConstantRequestMapping.PING)"); + } + + replaceInFile(pingUri, "/ping", "/changed"); + + { + List symbols = indexer.getSymbols(pingUri); + assertSymbolCount(1, symbols); + assertSymbol(pingUri, "@/pong -- GET", "@GetMapping(PongConstantRequestMapping.PONG)"); + } + { + List symbols = indexer.getSymbols(pongUri); + assertSymbolCount(1, symbols); + assertSymbol(pongUri, "@/changed -- GET", "@GetMapping(PingConstantRequestMapping.PING)"); + } + } + + ///////////////////////////////////////////////////////////////////////////////////////////////////// + + private void assertSymbolCount(int expectedCount, List symbols) { + if (symbols.size()!=expectedCount) { + StringBuilder found = new StringBuilder(); + for (SymbolInformation s : symbols) { + found.append(s.getName()); + found.append("\n"); + } + fail("Expected "+expectedCount+" symbols but found "+symbols.size()+":\n"+found); + } + } + + private void assertSymbol(String docUri, String name, String coveredText) throws Exception { + List symbols = indexer.getSymbols(docUri); + Optional maybeSymbol = symbols.stream().filter(s -> name.equals(s.getName())).findFirst(); + assertTrue(maybeSymbol.isPresent()); + + TextDocument doc = new TextDocument(docUri, LanguageId.JAVA); + doc.setText(FileUtils.readFileToString(UriUtil.toFile(docUri))); + + SymbolInformation symbol = maybeSymbol.get(); + Location loc = symbol.getLocation(); + assertEquals(docUri, loc.getUri()); + int start = doc.toOffset(loc.getRange().getStart()); + int end = doc.toOffset(loc.getRange().getEnd()); + String actualCoveredText = doc.textBetween(start, end); + assertEquals(coveredText, actualCoveredText); + } + + public void replaceInFile(String docUri, String find, String replace) throws Exception { + File target = UriUtil.toFile(docUri); + String oldContent = FileUtils.readFileToString(target, "UTF8"); + assertTrue(oldContent.contains(find)); + String newContent = oldContent.replace(find, replace); + FileUtils.write(target, newContent, "UTF8"); + + indexer.updateDocument(docUri, null, "triggered by test code").get(); + } +} diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/requestmapping/test/RequestMappingSymbolProviderTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/requestmapping/test/RequestMappingSymbolProviderTest.java index 4dbba78bf..cc6a0cc6b 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/requestmapping/test/RequestMappingSymbolProviderTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/requestmapping/test/RequestMappingSymbolProviderTest.java @@ -16,9 +16,12 @@ import static org.junit.Assert.assertTrue; import java.io.File; import java.util.Iterator; import java.util.List; +import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; +import org.apache.commons.io.FileUtils; +import org.eclipse.lsp4j.Location; import org.eclipse.lsp4j.SymbolInformation; import org.eclipse.lsp4j.TextDocumentIdentifier; import org.junit.Before; @@ -29,11 +32,17 @@ import org.springframework.context.annotation.Import; import org.springframework.ide.vscode.boot.app.SpringSymbolIndex; import org.springframework.ide.vscode.boot.bootiful.BootLanguageServerTest; import org.springframework.ide.vscode.boot.bootiful.SymbolProviderTestConf; +import org.springframework.ide.vscode.boot.java.utils.SpringIndexerJava.DependencyTracker; import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; +import org.springframework.ide.vscode.commons.util.UriUtil; +import org.springframework.ide.vscode.commons.util.text.LanguageId; +import org.springframework.ide.vscode.commons.util.text.TextDocument; import org.springframework.ide.vscode.project.harness.BootLanguageServerHarness; import org.springframework.ide.vscode.project.harness.ProjectsHarness; import org.springframework.test.context.junit4.SpringRunner; +import com.google.common.collect.ImmutableSet; + /** * @author Martin Lippert */ @@ -73,9 +82,45 @@ public class RequestMappingSymbolProviderTest { @Test public void testSimpleRequestMappingSymbolFromConstantInDifferentClass() throws Exception { String docUri = directory.toPath().resolve("src/main/java/org/test/SimpleMappingClassWithConstantInDifferentClass.java").toUri().toString(); + String constantsUri = directory.toPath().resolve("src/main/java/org/test/Constants.java").toUri().toString(); List symbols = indexer.getSymbols(docUri); assertEquals(1, symbols.size()); assertTrue(containsSymbol(symbols, "@/path/from/constant", docUri, 6, 1, 6, 48)); + + //Verify whether dependency tracker logics works properly for this example. + DependencyTracker dt = indexer.getJavaIndexer().getDependencyTracker(); + assertEquals(ImmutableSet.of("Lorg/test/Constants;"), dt.getAllDependencies().get(UriUtil.toFileString(docUri))); + + + TestFileScanListener fileScanListener = new TestFileScanListener(); + indexer.getJavaIndexer().setFileScanListener(fileScanListener); + indexer.updateDocument(constantsUri, FileUtils.readFileToString(UriUtil.toFile(constantsUri)), "test triggered").get(); + fileScanListener.assertScannedUris(constantsUri, docUri); + fileScanListener.assertScannedUri(constantsUri, 1); + fileScanListener.assertScannedUri(docUri, 1); + } + + @Test + public void testCyclicalRequestMappingDependency() throws Exception { + //Cyclical dependency: + //file a => file b => file a + //This has the potential to cause infinite loop. + + String pingUri = directory.toPath().resolve("src/main/java/org/test/PingConstantRequestMapping.java").toUri().toString(); + String pongUri = directory.toPath().resolve("src/main/java/org/test/PongConstantRequestMapping.java").toUri().toString(); + + assertSymbol(pingUri, "@/pong -- GET", "@GetMapping(PongConstantRequestMapping.PONG)"); + assertSymbol(pongUri, "@/ping -- GET", "@GetMapping(PingConstantRequestMapping.PING)"); + + TestFileScanListener fileScanListener = new TestFileScanListener(); + indexer.getJavaIndexer().setFileScanListener(fileScanListener); + indexer.updateDocument(pingUri, null, "test triggered").get(); + fileScanListener.assertScannedUris(pingUri, pongUri); + + fileScanListener.reset(); + fileScanListener.assertScannedUris(/*none*/); + indexer.updateDocument(pongUri, null, "test triggered").get(); + fileScanListener.assertScannedUris(pingUri, pongUri); } @Test @@ -190,4 +235,21 @@ public class RequestMappingSymbolProviderTest { return false; } + + private void assertSymbol(String docUri, String name, String coveredText) throws Exception { + List symbols = indexer.getSymbols(docUri); + Optional maybeSymbol = symbols.stream().filter(s -> name.equals(s.getName())).findFirst(); + assertTrue(maybeSymbol.isPresent()); + + TextDocument doc = new TextDocument(docUri, LanguageId.JAVA); + doc.setText(FileUtils.readFileToString(UriUtil.toFile(docUri))); + + SymbolInformation symbol = maybeSymbol.get(); + Location loc = symbol.getLocation(); + assertEquals(docUri, loc.getUri()); + int start = doc.toOffset(loc.getRange().getStart()); + int end = doc.toOffset(loc.getRange().getEnd()); + String actualCoveredText = doc.textBetween(start, end); + assertEquals(coveredText, actualCoveredText); + } } diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/requestmapping/test/TestFileScanListener.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/requestmapping/test/TestFileScanListener.java new file mode 100644 index 000000000..445c98c0d --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/requestmapping/test/TestFileScanListener.java @@ -0,0 +1,58 @@ +package org.springframework.ide.vscode.boot.java.requestmapping.test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.TreeSet; + +import org.springframework.ide.vscode.boot.java.utils.FileScanListener; +import org.springframework.ide.vscode.commons.util.UriUtil; + +public class TestFileScanListener implements FileScanListener { + public final List scannedFiles = new ArrayList(); + + @Override + public void fileScanned(String path) { + scannedFiles.add(path); + } + + public void assertScanned(String path) { + assertTrue(scannedFiles.contains(path)); + } + + public void assertScanned(String path, int scanCount) { + assertEquals(scanCount, scannedFiles.stream().filter(e -> e.equals(path)).count()); + } + + public void assertScannedUris(String... docUris) { + List docPaths = new ArrayList<>(); + for (String docUri : docUris) { + docPaths.add(UriUtil.toFileString(docUri)); + } + Collections.sort(docPaths); + StringBuilder expected = new StringBuilder(); + for (String string : docPaths) { + expected.append(string+"\n"); + } + TreeSet actualPaths = new TreeSet<>(); + for (String string : scannedFiles) { + actualPaths.add(string); + } + StringBuilder actual = new StringBuilder(); + for (String string : actualPaths) { + actual.append(string+"\n"); + } + assertEquals(expected.toString(), actual.toString()); + } + + public void assertScannedUri(String docUri, int scanCount) { + assertScanned(UriUtil.toFileString(docUri), scanCount); + } + + public void reset() { + scannedFiles.clear(); + } +} \ No newline at end of file diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/utils/test/SpringIndexerTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/utils/test/SpringIndexerTest.java index 4615aee25..acb826a7b 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/utils/test/SpringIndexerTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/utils/test/SpringIndexerTest.java @@ -161,7 +161,7 @@ public class SpringIndexerTest { assertTrue(containsSymbol(indexer.getSymbols(changedDocURI), "@/mapping1", changedDocURI)); String newContent = FileUtils.readFileToString(new File(new URI(changedDocURI))).replace("mapping1", "mapping1-CHANGED"); - CompletableFuture updateFuture = indexer.updateDocument(changedDocURI, newContent); + CompletableFuture updateFuture = indexer.updateDocument(changedDocURI, newContent, "test triggered"); updateFuture.get(5, TimeUnit.SECONDS); // check for updated index per document diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/utils/test/SymbolCacheOnDiscTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/utils/test/SymbolCacheOnDiscTest.java index a09df6fb1..54840cf61 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/utils/test/SymbolCacheOnDiscTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/utils/test/SymbolCacheOnDiscTest.java @@ -22,8 +22,10 @@ import java.nio.file.Paths; import java.nio.file.attribute.FileTime; import java.util.ArrayList; import java.util.List; +import java.util.Set; import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.tuple.Pair; import org.eclipse.lsp4j.Location; import org.eclipse.lsp4j.Position; import org.eclipse.lsp4j.Range; @@ -40,6 +42,12 @@ import org.springframework.ide.vscode.boot.java.utils.SymbolCacheKey; import org.springframework.ide.vscode.boot.java.utils.SymbolCacheOnDisc; import org.springframework.ide.vscode.commons.util.UriUtil; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMultimap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Multimap; +import com.google.common.collect.MultimapBuilder; + public class SymbolCacheOnDiscTest { private Path tempDir; @@ -58,7 +66,7 @@ public class SymbolCacheOnDiscTest { @Test public void testEmptyCache() throws Exception { - CachedSymbol[] result = cache.retrieve(new SymbolCacheKey("something", "0"), new String[0]); + Pair> result = cache.retrieve(new SymbolCacheKey("something", "0"), new String[0]); assertNull(result); } @@ -80,9 +88,15 @@ public class SymbolCacheOnDiscTest { EnhancedSymbolInformation enhancedSymbol = new EnhancedSymbolInformation(symbol, null); generatedSymbols.add(new CachedSymbol("", timeFile1.toMillis(), enhancedSymbol)); - cache.store(new SymbolCacheKey("somekey", "1"), files, generatedSymbols); + cache.store(new SymbolCacheKey("somekey", "1"), files, generatedSymbols, ImmutableMultimap.of( + file1.toString(), "file1dep1", + file2.toString(), "file2dep1", + file2.toString(), "file2dep2" + )); - CachedSymbol[] cachedSymbols = cache.retrieve(new SymbolCacheKey("somekey", "1"), files); + Pair> result = cache.retrieve(new SymbolCacheKey("somekey", "1"), files); + + CachedSymbol[] cachedSymbols = result.getLeft(); assertNotNull(cachedSymbols); assertEquals(1, cachedSymbols.length); @@ -90,6 +104,11 @@ public class SymbolCacheOnDiscTest { assertEquals(SymbolKind.Field, cachedSymbols[0].getEnhancedSymbol().getSymbol().getKind()); assertEquals(new Location("docURI", new Range(new Position(3, 10), new Position(3, 20))), cachedSymbols[0].getEnhancedSymbol().getSymbol().getLocation()); assertNull(cachedSymbols[0].getEnhancedSymbol().getAdditionalInformation()); + + Multimap dependencies = result.getRight(); + assertEquals(2, dependencies.keySet().size()); + assertEquals(dependencies.get(file1.toString()), ImmutableSet.of("file1dep1")); + assertEquals(dependencies.get(file2.toString()), ImmutableSet.of("file2dep1", "file2dep2")); } @Test @@ -110,10 +129,10 @@ public class SymbolCacheOnDiscTest { EnhancedSymbolInformation enhancedSymbol = new EnhancedSymbolInformation(symbol, null); generatedSymbols.add(new CachedSymbol("", timeFile1.toMillis(), enhancedSymbol)); - cache.store(new SymbolCacheKey("somekey", "1"), files, generatedSymbols); + cache.store(new SymbolCacheKey("somekey", "1"), files, generatedSymbols, null); - CachedSymbol[] cachedSymbols = cache.retrieve(new SymbolCacheKey("otherkey", "1"), files); - assertNull(cachedSymbols); + Pair> result = cache.retrieve(new SymbolCacheKey("otherkey", "1"), files); + assertNull(result); } @Test @@ -134,12 +153,15 @@ public class SymbolCacheOnDiscTest { EnhancedSymbolInformation enhancedSymbol = new EnhancedSymbolInformation(symbol, null); generatedSymbols.add(new CachedSymbol("", timeFile1.toMillis(), enhancedSymbol)); - cache.store(new SymbolCacheKey("somekey", "1"), files, generatedSymbols); + cache.store(new SymbolCacheKey("somekey", "1"), files, generatedSymbols, ImmutableMultimap.of( + file1.toString(), "file1dep", + file2.toString(), "file2dep" + )); assertTrue(file1.toFile().setLastModified(timeFile1.toMillis() + 1000)); - - CachedSymbol[] cachedSymbols = cache.retrieve(new SymbolCacheKey("somekey", "1"), files); - assertNull(cachedSymbols); + + Pair> result = cache.retrieve(new SymbolCacheKey("somekey", "1"), files); + assertNull(result); } @Test @@ -153,11 +175,10 @@ public class SymbolCacheOnDiscTest { Files.createFile(file3); String[] files = {file1.toString(), file2.toString()}; - cache.store(new SymbolCacheKey("somekey", "1"), files, new ArrayList<>()); + cache.store(new SymbolCacheKey("somekey", "1"), files, new ArrayList<>(), null); String[] moreFiles = {file1.toString(), file2.toString(), file3.toString()}; - CachedSymbol[] cachedSymbols = cache.retrieve(new SymbolCacheKey("somekey", "1"), moreFiles); - assertNull(cachedSymbols); + assertNull(cache.retrieve(new SymbolCacheKey("somekey", "1"), moreFiles)); } @Test @@ -171,21 +192,20 @@ public class SymbolCacheOnDiscTest { Files.createFile(file3); String[] files = {file1.toString(), file2.toString(), file3.toString()}; - cache.store(new SymbolCacheKey("somekey", "1"), files, new ArrayList<>()); + cache.store(new SymbolCacheKey("somekey", "1"), files, new ArrayList<>(), null); String[] fewerFiles = {file1.toString(), file2.toString()}; - CachedSymbol[] cachedSymbols = cache.retrieve(new SymbolCacheKey("somekey", "1"), fewerFiles); - assertNull(cachedSymbols); + assertNull(cache.retrieve(new SymbolCacheKey("somekey", "1"), fewerFiles)); } @Test public void testDeleteOldCacheFileIfNewOneIsStored() throws Exception { SymbolCacheKey key1 = new SymbolCacheKey("somekey", "1"); - cache.store(key1, new String[0], new ArrayList<>()); + cache.store(key1, new String[0], new ArrayList<>(), null); assertTrue(Files.exists(tempDir.resolve(Paths.get(key1.toString() + ".json")))); SymbolCacheKey key2 = new SymbolCacheKey("somekey", "2"); - cache.store(key2, new String[0], new ArrayList<>()); + cache.store(key2, new String[0], new ArrayList<>(), null); assertTrue(Files.exists(tempDir.resolve(Paths.get(key2.toString() + ".json")))); assertFalse(Files.exists(tempDir.resolve(Paths.get(key1.toString() + ".json")))); } @@ -207,9 +227,9 @@ public class SymbolCacheOnDiscTest { generatedSymbols.add(new CachedSymbol(doc1URI, timeFile1.toMillis(), enhancedSymbol)); - cache.store(new SymbolCacheKey("somekey", "1"), files, generatedSymbols); + cache.store(new SymbolCacheKey("somekey", "1"), files, generatedSymbols, null); - CachedSymbol[] cachedSymbols = cache.retrieve(new SymbolCacheKey("somekey", "1"), files); + CachedSymbol[] cachedSymbols = cache.retrieveSymbols(new SymbolCacheKey("somekey", "1"), files); assertNotNull(cachedSymbols); assertEquals(1, cachedSymbols.length); @@ -244,7 +264,7 @@ public class SymbolCacheOnDiscTest { EnhancedSymbolInformation enhancedSymbol1 = new EnhancedSymbolInformation(symbol1, null); generatedSymbols1.add(new CachedSymbol(doc1URI, timeFile1.toMillis(), enhancedSymbol1)); - cache.store(new SymbolCacheKey("somekey", "1"), files, generatedSymbols1); + cache.store(new SymbolCacheKey("somekey", "1"), files, generatedSymbols1, null); List generatedSymbols2 = new ArrayList<>(); symbol1 = new SymbolInformation("symbol1", SymbolKind.Field, new Location(doc1URI, new Range(new Position(3, 10), new Position(3, 20)))); @@ -257,13 +277,36 @@ public class SymbolCacheOnDiscTest { generatedSymbols2.add(new CachedSymbol(doc1URI, timeFile1.toMillis() + 2000, enhancedSymbol2)); assertTrue(file1.toFile().setLastModified(timeFile1.toMillis() + 2000)); - cache.update(new SymbolCacheKey("somekey", "1"), file1.toAbsolutePath().toString(), timeFile1.toMillis() + 2000, generatedSymbols2); + cache.update(new SymbolCacheKey("somekey", "1"), file1.toAbsolutePath().toString(), timeFile1.toMillis() + 2000, generatedSymbols2, null); - CachedSymbol[] cachedSymbols = cache.retrieve(new SymbolCacheKey("somekey", "1"), files); + CachedSymbol[] cachedSymbols = cache.retrieveSymbols(new SymbolCacheKey("somekey", "1"), files); assertNotNull(cachedSymbols); assertEquals(2, cachedSymbols.length); } + @Test + public void testDependencyAddedToExistingFile() throws Exception { + Path file1 = Paths.get(tempDir.toAbsolutePath().toString(), "tempFile1"); + + Files.createFile(file1); + + FileTime timeFile1 = Files.getLastModifiedTime(file1); + String[] files = {file1.toAbsolutePath().toString()}; + + List generatedSymbols = ImmutableList.of(); + + Multimap dependencies = ImmutableMultimap.of(file1.toString(), "dep1"); + cache.store(new SymbolCacheKey("somekey", "1"), files, generatedSymbols, dependencies); + + assertTrue(file1.toFile().setLastModified(timeFile1.toMillis() + 2000)); + Set dependencies2 = ImmutableSet.of("dep1", "dep2"); + cache.update(new SymbolCacheKey("somekey", "1"), file1.toAbsolutePath().toString(), timeFile1.toMillis() + 2000, generatedSymbols, dependencies2); + + Pair> result = cache.retrieve(new SymbolCacheKey("somekey", "1"), files); + assertNotNull(result); + assertEquals(ImmutableSet.of("dep1", "dep2"), result.getRight().get(file1.toString())); + } + @Test public void testSymbolRemovedFromExistingFile() throws Exception { Path file1 = Paths.get(tempDir.toAbsolutePath().toString(), "tempFile1"); @@ -280,18 +323,47 @@ public class SymbolCacheOnDiscTest { EnhancedSymbolInformation enhancedSymbol1 = new EnhancedSymbolInformation(symbol1, null); generatedSymbols1.add(new CachedSymbol(doc1URI, timeFile1.toMillis(), enhancedSymbol1)); - cache.store(new SymbolCacheKey("somekey", "1"), files, generatedSymbols1); + cache.store(new SymbolCacheKey("somekey", "1"), files, generatedSymbols1, null); List generatedSymbols2 = new ArrayList<>(); assertTrue(file1.toFile().setLastModified(timeFile1.toMillis() + 2000)); - cache.update(new SymbolCacheKey("somekey", "1"), file1.toAbsolutePath().toString(), timeFile1.toMillis() + 2000, generatedSymbols2); + cache.update(new SymbolCacheKey("somekey", "1"), file1.toAbsolutePath().toString(), timeFile1.toMillis() + 2000, generatedSymbols2, null); - CachedSymbol[] cachedSymbols = cache.retrieve(new SymbolCacheKey("somekey", "1"), files); + CachedSymbol[] cachedSymbols = cache.retrieveSymbols(new SymbolCacheKey("somekey", "1"), files); assertNotNull(cachedSymbols); assertEquals(0, cachedSymbols.length); } + @Test + public void testDependencyRemovedFromExistingFile() throws Exception { + Path file1 = Paths.get(tempDir.toAbsolutePath().toString(), "tempFile1"); + + Files.createFile(file1); + + FileTime timeFile1 = Files.getLastModifiedTime(file1); + String[] files = {file1.toAbsolutePath().toString()}; + + List generatedSymbols1 = ImmutableList.of(); + + ImmutableMultimap dependencies1 = ImmutableMultimap.of( + file1.toString(), "dep1", + file1.toString(), "dep2" + ); + + cache.store(new SymbolCacheKey("somekey", "1"), files, generatedSymbols1, dependencies1); + + List generatedSymbols2 = new ArrayList<>(); + assertTrue(file1.toFile().setLastModified(timeFile1.toMillis() + 2000)); + + Set dependencies2 = ImmutableSet.of("dep2"); + cache.update(new SymbolCacheKey("somekey", "1"), file1.toAbsolutePath().toString(), timeFile1.toMillis() + 2000, generatedSymbols2, dependencies2); + + Pair> result = cache.retrieve(new SymbolCacheKey("somekey", "1"), files); + assertNotNull(result); + assertEquals(ImmutableSet.of("dep2"), result.getRight().get(file1.toString())); + } + @Test public void testSymbolAddedToNewFile() throws Exception { Path file1 = Paths.get(tempDir.toAbsolutePath().toString(), "tempFile1"); @@ -312,7 +384,7 @@ public class SymbolCacheOnDiscTest { EnhancedSymbolInformation enhancedSymbol1 = new EnhancedSymbolInformation(symbol1, null); generatedSymbols1.add(new CachedSymbol(doc1URI, timeFile1.toMillis(), enhancedSymbol1)); - cache.store(new SymbolCacheKey("somekey", "1"), files, generatedSymbols1); + cache.store(new SymbolCacheKey("somekey", "1"), files, generatedSymbols1, null); List generatedSymbols2 = new ArrayList<>(); SymbolInformation symbol2 = new SymbolInformation("symbol2", SymbolKind.Interface, new Location(doc2URI, new Range(new Position(5, 5), new Position(5, 10)))); @@ -320,17 +392,47 @@ public class SymbolCacheOnDiscTest { generatedSymbols2.add(new CachedSymbol(doc2URI, timeFile2.toMillis(), enhancedSymbol2)); - cache.update(new SymbolCacheKey("somekey", "1"), file2.toString(), timeFile2.toMillis(), generatedSymbols2); + cache.update(new SymbolCacheKey("somekey", "1"), file2.toString(), timeFile2.toMillis(), generatedSymbols2, null); - CachedSymbol[] cachedSymbols = cache.retrieve(new SymbolCacheKey("somekey", "1"), new String[] {file1.toString(), file2.toString()}); + CachedSymbol[] cachedSymbols = cache.retrieveSymbols(new SymbolCacheKey("somekey", "1"), new String[] {file1.toString(), file2.toString()}); assertNotNull(cachedSymbols); assertEquals(2, cachedSymbols.length); } + @Test + public void testDependencyAddedToNewFile() throws Exception { + Path file1 = Paths.get(tempDir.toAbsolutePath().toString(), "tempFile1"); + Path file2 = Paths.get(tempDir.toAbsolutePath().toString(), "tempFile2"); + + Files.createFile(file1); + Files.createFile(file2); + + FileTime timeFile1 = Files.getLastModifiedTime(file1); + FileTime timeFile2 = Files.getLastModifiedTime(file2); + String[] files = {file1.toString()}; + + String doc1URI = UriUtil.toUri(file1.toFile()).toString(); + String doc2URI = UriUtil.toUri(file2.toFile()).toString(); + + List generatedSymbols1 = ImmutableList.of(); + Multimap dependencies1 = ImmutableMultimap.of( + file1.toString(), "dep1" + ); + cache.store(new SymbolCacheKey("somekey", "1"), files, generatedSymbols1, dependencies1); + + Set dependencies2 = ImmutableSet.of("dep2"); + cache.update(new SymbolCacheKey("somekey", "1"), file2.toString(), timeFile2.toMillis(), generatedSymbols1, dependencies2); + + Pair> result = cache.retrieve(new SymbolCacheKey("somekey", "1"), new String[] {file1.toString(), file2.toString()}); + assertNotNull(result); + assertEquals(ImmutableSet.of("dep2"), result.getRight().get(file2.toString())); + assertEquals(ImmutableSet.of("dep1"), result.getRight().get(file1.toString())); + } + @Test public void testProjectDeleted() throws Exception { SymbolCacheKey key1 = new SymbolCacheKey("somekey", "1"); - cache.store(key1, new String[0], new ArrayList<>()); + cache.store(key1, new String[0], new ArrayList<>(), null); assertTrue(Files.exists(tempDir.resolve(Paths.get(key1.toString() + ".json")))); cache.remove(key1); @@ -363,18 +465,26 @@ public class SymbolCacheOnDiscTest { generatedSymbols.add(new CachedSymbol(doc1URI, timeFile1.toMillis(), enhancedSymbol1)); generatedSymbols.add(new CachedSymbol(doc2URI, timeFile2.toMillis(), enhancedSymbol2)); - cache.store(new SymbolCacheKey("somekey", "1"), files, generatedSymbols); + Multimap dependencies = ImmutableMultimap.of( + file1.toString(), "dep1", + file2.toString(), "dep2" + ); + cache.store(new SymbolCacheKey("somekey", "1"), files, generatedSymbols, dependencies); cache.removeFile(new SymbolCacheKey("somekey", "1"), file1.toAbsolutePath().toString()); files = new String[] {file2.toAbsolutePath().toString()}; - CachedSymbol[] cachedSymbols = cache.retrieve(new SymbolCacheKey("somekey", "1"), files); - assertNotNull(cachedSymbols); + Pair> result = cache.retrieve(new SymbolCacheKey("somekey", "1"), files); + CachedSymbol[] cachedSymbols = result.getLeft(); + assertNotNull(result); assertEquals(1, cachedSymbols.length); assertEquals("symbol2", cachedSymbols[0].getEnhancedSymbol().getSymbol().getName()); assertEquals(SymbolKind.Field, cachedSymbols[0].getEnhancedSymbol().getSymbol().getKind()); assertEquals(new Location(doc2URI, new Range(new Position(5, 10), new Position(5, 20))), cachedSymbols[0].getEnhancedSymbol().getSymbol().getLocation()); assertNull(cachedSymbols[0].getEnhancedSymbol().getAdditionalInformation()); + + Multimap cachedDependencies = result.getRight(); + assertEquals(ImmutableSet.of(), cachedDependencies.get(file1.toString())); + assertEquals(ImmutableSet.of("dep2"), cachedDependencies.get(file2.toString())); } - } diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/project/harness/ProjectsHarness.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/project/harness/ProjectsHarness.java index c18f0b777..e9a476175 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/project/harness/ProjectsHarness.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/project/harness/ProjectsHarness.java @@ -10,6 +10,8 @@ *******************************************************************************/ package org.springframework.ide.vscode.project.harness; +import static org.junit.Assert.assertTrue; + import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; @@ -73,7 +75,7 @@ public class ProjectsHarness { File target = new File(projectRoot, path); IOUtil.pipe(new ByteArrayInputStream(content.getBytes("UTF8")), target); } - + public void createType(String fqName, String sourceCode) throws Exception { String sourceFile = sourceFolder()+"/"+fqName.replace('.', '/')+".java"; createFile(sourceFile, sourceCode); @@ -98,7 +100,7 @@ public class ProjectsHarness { this.fileObserver = fileObserver; } - public IJavaProject project(ProjectType type, String name, ProjectCustomizer customizer, boolean build) throws Exception { + public IJavaProject project(ProjectType type, String name, boolean build, ProjectCustomizer customizer) throws Exception { Tuple3 key = Tuples.of(type, name, customizer); return cache.get(key, () -> { Path baseProjectPath = getProjectPath(name); @@ -122,7 +124,7 @@ public class ProjectsHarness { } } - public IJavaProject project(ProjectType type, String name, boolean build) throws Exception { + private IJavaProject project(ProjectType type, String name, boolean build) throws Exception { return cache.get(type + "/" + name, () -> { Path testProjectPath = getProjectPath(name); return createProject(type, testProjectPath, build); @@ -138,8 +140,12 @@ public class ProjectsHarness { return Paths.get(resource); } + public MavenJavaProject mavenProject(String name, boolean build, ProjectCustomizer customizer) throws Exception { + return (MavenJavaProject) project(ProjectType.MAVEN, name, build, customizer); + } + public MavenJavaProject mavenProject(String name, ProjectCustomizer customizer) throws Exception { - return (MavenJavaProject) project(ProjectType.MAVEN, name, customizer, true); + return (MavenJavaProject) project(ProjectType.MAVEN, name, true, customizer); } public MavenJavaProject mavenProject(String name) throws Exception { diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-request-mapping-symbols/src/main/java/org/test/PingConstantRequestMapping.java b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-request-mapping-symbols/src/main/java/org/test/PingConstantRequestMapping.java new file mode 100644 index 000000000..692f9836e --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-request-mapping-symbols/src/main/java/org/test/PingConstantRequestMapping.java @@ -0,0 +1,15 @@ +package org.test; + +import org.springframework.web.bind.annotation.GetMapping; + +public class PingConstantRequestMapping { + + public final static String PING = "/ping"; + + @GetMapping(PongConstantRequestMapping.PONG) + public String hello() { + return "oy?"; + } + + +} diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-request-mapping-symbols/src/main/java/org/test/PongConstantRequestMapping.java b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-request-mapping-symbols/src/main/java/org/test/PongConstantRequestMapping.java new file mode 100644 index 000000000..92dce7621 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-request-mapping-symbols/src/main/java/org/test/PongConstantRequestMapping.java @@ -0,0 +1,14 @@ +package org.test; + +import org.springframework.web.bind.annotation.GetMapping; + +public class PongConstantRequestMapping { + + public static final String PONG="/pong"; + + @GetMapping(PingConstantRequestMapping.PING) + public String hello() { + return "oy?"; + } + +}