diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/CachedSymbol.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/CachedSymbol.java new file mode 100644 index 000000000..6cff82c72 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/CachedSymbol.java @@ -0,0 +1,39 @@ +/******************************************************************************* + * 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 + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.utils; + +import org.springframework.ide.vscode.boot.java.handlers.EnhancedSymbolInformation; + +public class CachedSymbol { + + private final String docURI; + private final long lastModified; + private final EnhancedSymbolInformation enhancedSymbol; + + public CachedSymbol(String docURI, long lastModified, EnhancedSymbolInformation enhancedSymbol) { + this.docURI = docURI; + this.lastModified = lastModified; + this.enhancedSymbol = enhancedSymbol; + } + + public EnhancedSymbolInformation getEnhancedSymbol() { + return enhancedSymbol; + } + + public String getDocURI() { + return docURI; + } + + public long getLastModified() { + return lastModified; + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexer.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexer.java index 5eab3aed5..65dcc6ad9 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexer.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringIndexer.java @@ -21,7 +21,10 @@ public interface SpringIndexer { boolean isInterestedIn(String docURI); void initializeProject(IJavaProject project) throws Exception; - void updateFile(IJavaProject project, String docURI, String content) throws Exception; + void removeProject(IJavaProject project) throws Exception; + + void updateFile(IJavaProject project, String docURI, long lastModified, String content) throws Exception; + void removeFile(IJavaProject project, String docURI) throws Exception; } 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 20274ad7d..b7a38e371 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 @@ -11,8 +11,10 @@ package org.springframework.ide.vscode.boot.java.utils; import java.io.File; +import java.net.URI; import java.nio.file.Files; import java.nio.file.Paths; +import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; @@ -20,6 +22,7 @@ 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.eclipse.jdt.core.JavaCore; import org.eclipse.jdt.core.dom.AST; import org.eclipse.jdt.core.dom.ASTParser; @@ -55,10 +58,12 @@ public class SpringIndexerJava implements SpringIndexer { private final SymbolHandler symbolHandler; private final AnnotationHierarchyAwareLookup symbolProviders; + private final SymbolCache cache; - public SpringIndexerJava(SymbolHandler symbolHandler, AnnotationHierarchyAwareLookup symbolProviders) { + public SpringIndexerJava(SymbolHandler symbolHandler, AnnotationHierarchyAwareLookup symbolProviders, SymbolCache cache) { this.symbolHandler = symbolHandler; this.symbolProviders = symbolProviders; + this.cache = cache; } @Override @@ -73,53 +78,37 @@ public class SpringIndexerJava implements SpringIndexer { @Override public void initializeProject(IJavaProject project) throws Exception { - List files = Files.walk(Paths.get(project.getLocationUri())) - .filter(path -> path.getFileName().toString().endsWith(".java")) - .filter(Files::isRegularFile) - .map(path -> path.toAbsolutePath().toString()) - .collect(Collectors.toList()); + String[] files = this.getFiles(project); - log.info("scan java files for symbols for project: " + project.getElementName() + " - no. of files: " + files.size()); + log.info("scan java files for symbols for project: " + project.getElementName() + " - no. of files: " + files.length); long startTime = System.currentTimeMillis(); - scanProject(project, (String[]) files.toArray(new String[files.size()])); + scanFiles(project, files); long endTime = System.currentTimeMillis(); log.info("scan java files for symbols for project: " + project.getElementName() + " took ms: " + (endTime - startTime)); } @Override - public void updateFile(IJavaProject project, String docURI, String content) throws Exception { - String[] classpathEntries = getClasspathEntries(project); - scanFile(project, docURI, content, classpathEntries); + public void removeProject(IJavaProject project) throws Exception { + SymbolCacheKey cacheKey = getCacheKey(project); + this.cache.remove(cacheKey); } - - private void scanProject(IJavaProject project, String[] files) { - try { - ASTParser parser = ASTParser.newParser(AST.JLS11); - String[] classpathEntries = getClasspathEntries(project); - - scanFiles(project, parser, files, classpathEntries); - } - catch (Exception e) { - log.error("error parsing all Java source files from project: " + project.getElementName(), e); - } + @Override + public void updateFile(IJavaProject project, String docURI, long lastModified, String content) throws Exception { + scanFile(project, docURI, lastModified, content); } - private void scanFile(IJavaProject project, String docURI, String content, String[] classpathEntries) throws Exception { - ASTParser parser = ASTParser.newParser(AST.JLS11); - Map options = JavaCore.getOptions(); - JavaCore.setComplianceOptions(JavaCore.VERSION_11, options); - parser.setCompilerOptions(options); - parser.setKind(ASTParser.K_COMPILATION_UNIT); - parser.setStatementsRecovery(true); - parser.setBindingsRecovery(true); - parser.setResolveBindings(true); - parser.setIgnoreMethodBodies(false); + @Override + public void removeFile(IJavaProject project, String docURI) throws Exception { + SymbolCacheKey cacheKey = getCacheKey(project); + String file = new File(new URI(docURI)).getAbsolutePath(); + this.cache.removeFile(cacheKey, file); + } - String[] sourceEntries = new String[] {}; - parser.setEnvironment(classpathEntries, sourceEntries, null, false); + private void scanFile(IJavaProject project, String docURI, long lastModified, String content) throws Exception { + ASTParser parser = createParser(project); String unitName = docURI.substring(docURI.lastIndexOf("/")); parser.setUnitName(unitName); @@ -128,44 +117,64 @@ public class SpringIndexerJava implements SpringIndexer { CompilationUnit cu = (CompilationUnit) parser.createAST(null); if (cu != null) { + List generatedSymbols = new ArrayList(); + AtomicReference docRef = new AtomicReference<>(); - scanAST(project, cu, docURI, docRef, content); + scanAST(project, cu, docURI, lastModified, docRef, content, generatedSymbols); + + SymbolCacheKey cacheKey = getCacheKey(project); + String file = new File(new URI(docURI)).getAbsolutePath(); + this.cache.update(cacheKey, file, lastModified, generatedSymbols); + + for (CachedSymbol symbol : generatedSymbols) { + symbolHandler.addSymbol(project, symbol.getDocURI(), symbol.getEnhancedSymbol()); + } } } - private void scanFiles(IJavaProject project, ASTParser parser, String[] javaFiles, String[] classpathEntries) throws Exception { + private void scanFiles(IJavaProject project, String[] javaFiles) throws Exception { + SymbolCacheKey cacheKey = getCacheKey(project); + CachedSymbol[] symbols = this.cache.retrieve(cacheKey, javaFiles); - Map options = JavaCore.getOptions(); - JavaCore.setComplianceOptions(JavaCore.VERSION_11, options); - parser.setCompilerOptions(options); - parser.setKind(ASTParser.K_COMPILATION_UNIT); - parser.setStatementsRecovery(true); - parser.setBindingsRecovery(true); - parser.setResolveBindings(true); - parser.setIgnoreMethodBodies(false); + if (symbols == null) { + List generatedSymbols = new ArrayList(); - String[] sourceEntries = new String[] {}; - parser.setEnvironment(classpathEntries, sourceEntries, null, false); + ASTParser parser = createParser(project); - FileASTRequestor requestor = new FileASTRequestor() { - @Override - public void acceptAST(String sourceFilePath, CompilationUnit cu) { - String docURI = UriUtil.toUri(new File(sourceFilePath)).toString(); - AtomicReference docRef = new AtomicReference<>(); - scanAST(project, cu, docURI, docRef, null); + FileASTRequestor requestor = new FileASTRequestor() { + @Override + public void acceptAST(String sourceFilePath, CompilationUnit cu) { + File file = new File(sourceFilePath); + String docURI = UriUtil.toUri(file).toString(); + long lastModified = file.lastModified(); + + AtomicReference docRef = new AtomicReference<>(); + scanAST(project, cu, docURI, lastModified, docRef, null, generatedSymbols); + } + }; + + parser.createASTs(javaFiles, null, new String[0], requestor, null); + this.cache.store(cacheKey, javaFiles, generatedSymbols); + } + else { + log.info("scan java 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()); } - }; - - parser.createASTs(javaFiles, null, new String[0], requestor, null); + } } - private void scanAST(final IJavaProject project, final CompilationUnit cu, final String docURI, AtomicReference docRef, final String content) { + private void scanAST(final IJavaProject project, final CompilationUnit cu, final String docURI, long lastModified, AtomicReference docRef, final String content, List generatedSymbols) { cu.accept(new ASTVisitor() { @Override public boolean visit(TypeDeclaration node) { try { - extractSymbolInformation(project, node, docURI, docRef, content); + extractSymbolInformation(project, node, docURI, docRef, content, lastModified, generatedSymbols); } catch (Exception e) { log.error("error extracting symbol information in project '" + project.getElementName() + "' - for docURI '" + docURI + "' - on node: " + node.toString(), e); @@ -176,7 +185,7 @@ public class SpringIndexerJava implements SpringIndexer { @Override public boolean visit(MethodDeclaration node) { try { - extractSymbolInformation(project, node, docURI, docRef, content); + extractSymbolInformation(project, node, docURI, docRef, content, lastModified, generatedSymbols); } catch (Exception e) { log.error("error extracting symbol information in project '" + project.getElementName() + "' - for docURI '" + docURI + "' - on node: " + node.toString(), e); @@ -187,7 +196,7 @@ public class SpringIndexerJava implements SpringIndexer { @Override public boolean visit(SingleMemberAnnotation node) { try { - extractSymbolInformation(project, node, docURI, docRef, content); + extractSymbolInformation(project, node, docURI, docRef, content, lastModified, generatedSymbols); } catch (Exception e) { log.error("error extracting symbol information in project '" + project.getElementName() + "' - for docURI '" + docURI + "' - on node: " + node.toString(), e); @@ -199,7 +208,7 @@ public class SpringIndexerJava implements SpringIndexer { @Override public boolean visit(NormalAnnotation node) { try { - extractSymbolInformation(project, node, docURI, docRef, content); + extractSymbolInformation(project, node, docURI, docRef, content, lastModified, generatedSymbols); } catch (Exception e) { log.error("error extracting symbol information in project '" + project.getElementName() + "' - for docURI '" + docURI + "' - on node: " + node.toString(), e); @@ -211,7 +220,7 @@ public class SpringIndexerJava implements SpringIndexer { @Override public boolean visit(MarkerAnnotation node) { try { - extractSymbolInformation(project, node, docURI, docRef, content); + extractSymbolInformation(project, node, docURI, docRef, content, lastModified, generatedSymbols); } catch (Exception e) { log.error("error extracting symbol information in project '" + project.getElementName() + "' - for docURI '" + docURI + "' - on node: " + node.toString(), e); @@ -222,7 +231,8 @@ public class SpringIndexerJava implements SpringIndexer { }); } - private void extractSymbolInformation(IJavaProject project, TypeDeclaration typeDeclaration, String docURI, AtomicReference docRef, String content) throws Exception { + private void extractSymbolInformation(IJavaProject project, TypeDeclaration typeDeclaration, String docURI, AtomicReference docRef, String content, + long lastModified, List generatedSymbols) throws Exception { Collection providers = symbolProviders.getAll(); if (!providers.isEmpty()) { TextDocument doc = DocumentUtils.getTempTextDocument(docURI, docRef, content); @@ -230,14 +240,15 @@ public class SpringIndexerJava implements SpringIndexer { Collection sbls = provider.getSymbols(typeDeclaration, doc); if (sbls != null) { sbls.forEach(enhancedSymbol -> { - symbolHandler.addSymbol(project, docURI, enhancedSymbol); + generatedSymbols.add(new CachedSymbol(docURI, lastModified, enhancedSymbol)); }); } } } } - private void extractSymbolInformation(IJavaProject project, MethodDeclaration methodDeclaration, String docURI, AtomicReference docRef, String content) throws Exception { + private void extractSymbolInformation(IJavaProject project, MethodDeclaration methodDeclaration, String docURI, AtomicReference docRef, String content, + long lastModified, List generatedSymbols) throws Exception { Collection providers = symbolProviders.getAll(); if (!providers.isEmpty()) { TextDocument doc = DocumentUtils.getTempTextDocument(docURI, docRef, content); @@ -245,14 +256,15 @@ public class SpringIndexerJava implements SpringIndexer { Collection sbls = provider.getSymbols(methodDeclaration, doc); if (sbls != null) { sbls.forEach(enhancedSymbol -> { - symbolHandler.addSymbol(project, docURI, enhancedSymbol); + generatedSymbols.add(new CachedSymbol(docURI, lastModified, enhancedSymbol)); }); } } } } - private void extractSymbolInformation(IJavaProject project, Annotation node, String docURI, AtomicReference docRef, String content) throws Exception { + private void extractSymbolInformation(IJavaProject project, Annotation node, String docURI, AtomicReference docRef, String content, + long lastModified, List generatedSymbols) throws Exception { ITypeBinding typeBinding = node.resolveTypeBinding(); if (typeBinding != null) { @@ -264,14 +276,15 @@ public class SpringIndexerJava implements SpringIndexer { Collection sbls = provider.getSymbols(node, typeBinding, metaAnnotations, doc); if (sbls != null) { sbls.forEach(enhancedSymbol -> { - symbolHandler.addSymbol(project, docURI, enhancedSymbol); + generatedSymbols.add(new CachedSymbol(docURI, lastModified, enhancedSymbol)); }); } } } else { SymbolInformation symbol = provideDefaultSymbol(project, node, docURI, docRef, content); if (symbol != null) { - symbolHandler.addSymbol(project, docURI, new EnhancedSymbolInformation(symbol, null)); + EnhancedSymbolInformation enhancedSymbol = new EnhancedSymbolInformation(symbol, null); + generatedSymbols.add(new CachedSymbol(docURI, lastModified, enhancedSymbol)); } } } @@ -295,6 +308,24 @@ public class SpringIndexerJava implements SpringIndexer { return null; } + private ASTParser createParser(IJavaProject project) throws Exception { + String[] classpathEntries = getClasspathEntries(project); + + ASTParser parser = ASTParser.newParser(AST.JLS11); + Map options = JavaCore.getOptions(); + JavaCore.setComplianceOptions(JavaCore.VERSION_11, options); + parser.setCompilerOptions(options); + parser.setKind(ASTParser.K_COMPILATION_UNIT); + parser.setStatementsRecovery(true); + parser.setBindingsRecovery(true); + parser.setResolveBindings(true); + parser.setIgnoreMethodBodies(false); + + String[] sourceEntries = new String[] {}; + parser.setEnvironment(classpathEntries, sourceEntries, null, false); + return parser; + } + private String[] getClasspathEntries(IJavaProject project) throws Exception { IClasspath classpath = project.getClasspath(); Stream classpathEntries = IClasspathUtil.getAllBinaryRoots(classpath).stream(); @@ -304,4 +335,24 @@ public class SpringIndexerJava implements SpringIndexer { .toArray(String[]::new); } + private String[] getFiles(IJavaProject project) throws Exception { + return Files.walk(Paths.get(project.getLocationUri())) + .filter(path -> path.getFileName().toString().endsWith(".java")) + .filter(Files::isRegularFile) + .map(path -> path.toAbsolutePath().toString()) + .toArray(String[]::new); + } + + private SymbolCacheKey getCacheKey(IJavaProject project) { + IClasspath classpath = project.getClasspath(); + Stream classpathEntries = IClasspathUtil.getAllBinaryRoots(classpath).stream(); + + String classpathIdentifier = classpathEntries + .filter(file -> file.exists()) + .map(file -> file.getAbsolutePath() + "#" + file.lastModified()) + .collect(Collectors.joining(",")); + + return new SymbolCacheKey(project.getElementName() + "-java-", DigestUtils.md5Hex(classpathIdentifier).toUpperCase()); + } + } 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 b5df4974d..e3d77e329 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 @@ -16,7 +16,6 @@ import java.nio.file.Paths; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; -import java.util.stream.Collectors; import org.apache.commons.io.FileUtils; import org.eclipse.lsp4xml.dom.DOMDocument; @@ -37,10 +36,12 @@ public class SpringIndexerXML implements SpringIndexer { private final SymbolHandler symbolHandler; private final Map namespaceHandler; + private final SymbolCache cache; - public SpringIndexerXML(SymbolHandler handler, Map namespaceHandler) { + public SpringIndexerXML(SymbolHandler handler, Map namespaceHandler, SymbolCache cache) { this.symbolHandler = handler; this.namespaceHandler = namespaceHandler; + this.cache = cache; } @Override @@ -55,30 +56,30 @@ public class SpringIndexerXML implements SpringIndexer { @Override public void initializeProject(IJavaProject project) throws Exception { - List files = Files.walk(Paths.get(project.getLocationUri())) - .filter(path -> path.getFileName().toString().endsWith(".xml")) - .filter(Files::isRegularFile) - .map(path -> path.toAbsolutePath().toString()) - .collect(Collectors.toList()); + String[] files = this.getFiles(project); - log.info("scan xml files for symbols for project: " + project.getElementName() + " - no. of files: " + files.size()); + log.info("scan xml files for symbols for project: " + project.getElementName() + " - no. of files: " + files.length); long startTime = System.currentTimeMillis(); - scanProject(project, (String[]) files.toArray(new String[files.size()])); + for (String file : files) { + scanFile(project, file); + } long endTime = System.currentTimeMillis(); log.info("scan xml files for symbols for project: " + project.getElementName() + " took ms: " + (endTime - startTime)); } @Override - public void updateFile(IJavaProject project, String docURI, String content) throws Exception { + public void removeProject(IJavaProject project) throws Exception { + } + + @Override + public void updateFile(IJavaProject project, String docURI, long lastModified, String content) throws Exception { scanFile(project, content, docURI); } - private void scanProject(IJavaProject project, String[] files) { - for (String file : files) { - scanFile(project, file); - } + @Override + public void removeFile(IJavaProject project, String docURI) throws Exception { } private void scanFile(IJavaProject project, String fileName) { @@ -86,6 +87,7 @@ public class SpringIndexerXML implements SpringIndexer { try { File file = new File(fileName); + long lastModified = file.lastModified(); String docURI = UriUtil.toUri(file).toString(); String fileContent = FileUtils.readFileToString(file); @@ -132,4 +134,13 @@ public class SpringIndexerXML implements SpringIndexer { } + + private String[] getFiles(IJavaProject project) throws Exception { + return Files.walk(Paths.get(project.getLocationUri())) + .filter(path -> path.getFileName().toString().endsWith(".xml")) + .filter(Files::isRegularFile) + .map(path -> path.toAbsolutePath().toString()) + .toArray(String[]::new); + } + } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringSymbolIndex.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringSymbolIndex.java index acfca2e8b..61814728d 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringSymbolIndex.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SpringSymbolIndex.java @@ -55,7 +55,6 @@ import org.springframework.ide.vscode.commons.util.StringUtil; public class SpringSymbolIndex { private static final String QUERY_PARAM_LOCATION_PREFIX = "locationPrefix:"; - private static final int MAX_NUMBER_OF_SYMBOLS_IN_RESPONSE = 50; private final SimpleLanguageServer server; @@ -73,6 +72,7 @@ public class SpringSymbolIndex { private final ExecutorService updateQueue; private SpringIndexer[] indexer; + private SymbolCache cache; private static final Logger log = LoggerFactory.getLogger(SpringSymbolIndex.class); @@ -124,6 +124,19 @@ public class SpringSymbolIndex { this.addonInformationByDoc = new ConcurrentHashMap<>(); this.addonInformationByProject = new ConcurrentHashMap<>(); + if ("true".equals(System.getProperty("boot.ls.symbolCache.enabled", "true"))) { + try { + this.cache = new SymbolCacheOnDisc(); + } + catch (Exception e) { + log.warn("symbol cache directory could not be created, no cache enabled"); + } + } + + if (this.cache == null) { + this.cache = new SymbolCacheVoid(); + } + SymbolHandler handler = new SymbolHandler() { @Override public void addSymbol(IJavaProject project, String docURI, EnhancedSymbolInformation enhancedSymbol) { @@ -133,8 +146,8 @@ public class SpringSymbolIndex { Map namespaceHandler = new HashMap<>(); namespaceHandler.put("http://www.springframework.org/schema/beans", new SpringIndexerXMLNamespaceHandlerBeans()); - springIndexerXML = new SpringIndexerXML(handler, namespaceHandler); - springIndexerJava = new SpringIndexerJava(handler, specificProviders); + springIndexerXML = new SpringIndexerXML(handler, namespaceHandler, this.cache); + springIndexerJava = new SpringIndexerJava(handler, specificProviders, this.cache); this.indexer = new SpringIndexer[] {springIndexerJava}; @@ -248,7 +261,7 @@ public class SpringSymbolIndex { log.debug("Project with NULL name is being removed"); return CompletableFuture.completedFuture(null); } else { - DeleteProject initializeItem = new DeleteProject(project); + DeleteProject initializeItem = new DeleteProject(project, this.indexer); return CompletableFuture.runAsync(initializeItem, this.updateQueue); } } catch (Throwable e) { @@ -267,8 +280,11 @@ public class SpringSymbolIndex { if (maybeProject.isPresent()) { try { - String content = FileUtils.readFileToString(new File(new URI(docURI))); - UpdateItem updateItem = new UpdateItem(maybeProject.get(), docURI, content, indexer); + File file = new File(new URI(docURI)); + long lastModified = file.lastModified(); + String content = FileUtils.readFileToString(file); + + UpdateItem updateItem = new UpdateItem(maybeProject.get(), docURI, lastModified, content, indexer); futures.add(CompletableFuture.runAsync(updateItem, this.updateQueue)); } catch (Exception e) { @@ -292,11 +308,14 @@ public class SpringSymbolIndex { Optional maybeProject = projectFinder.find(new TextDocumentIdentifier(docURI)); if (maybeProject.isPresent()) { try { + File file = new File(new URI(docURI)); + long lastModified = file.lastModified(); + if (content == null) { - content = FileUtils.readFileToString(new File(new URI(docURI))); + content = FileUtils.readFileToString(file); } - UpdateItem updateItem = new UpdateItem(maybeProject.get(), docURI, content, indexer); + UpdateItem updateItem = new UpdateItem(maybeProject.get(), docURI, lastModified, content, indexer); futures.add(CompletableFuture.runAsync(updateItem, this.updateQueue)); } catch (Exception e) { @@ -314,7 +333,7 @@ public class SpringSymbolIndex { try { Optional maybeProject = projectFinder.find(new TextDocumentIdentifier(deletedDocURI)); if (maybeProject.isPresent()) { - DeleteItem deleteItem = new DeleteItem(maybeProject.get(), deletedDocURI); + DeleteItem deleteItem = new DeleteItem(maybeProject.get(), deletedDocURI, this.indexer); return CompletableFuture.runAsync(deleteItem, this.updateQueue); } } @@ -459,10 +478,12 @@ public class SpringSymbolIndex { private final String content; private final IJavaProject project; private final SpringIndexer indexer; + private final long lastModified; - public UpdateItem(IJavaProject project, String docURI, String content, SpringIndexer indexer) { + public UpdateItem(IJavaProject project, String docURI, long lastModified, String content, SpringIndexer indexer) { this.project = project; this.docURI = docURI; + this.lastModified = lastModified; this.content = content; this.indexer = indexer; } @@ -471,7 +492,7 @@ public class SpringSymbolIndex { public void run() { try { removeSymbolsByDoc(project, docURI); - indexer.updateFile(project, docURI, content); + indexer.updateFile(project, docURI, lastModified, content); } catch (Exception e) { log.error("{}", e); } @@ -481,17 +502,22 @@ public class SpringSymbolIndex { private class DeleteItem implements Runnable { private final String docURI; - private IJavaProject project; + private final IJavaProject project; + private final SpringIndexer[] indexer; - public DeleteItem(IJavaProject project, String docURI) { + public DeleteItem(IJavaProject project, String docURI, SpringIndexer[] indexer) { this.project = project; this.docURI = docURI; + this.indexer = indexer; } @Override public void run() { try { removeSymbolsByDoc(project, docURI); + for (SpringIndexer index : this.indexer) { + index.removeFile(project, docURI); + } } catch (Exception e) { log.error("{}", e); } @@ -501,9 +527,11 @@ public class SpringSymbolIndex { private class DeleteProject implements Runnable { private final IJavaProject project; + private final SpringIndexer[] indexer; - public DeleteProject(IJavaProject project) { + public DeleteProject(IJavaProject project, SpringIndexer[] indexer) { this.project = project; + this.indexer = indexer; log.debug("{} created ", this); } @@ -512,6 +540,9 @@ public class SpringSymbolIndex { log.debug("{} starting...", this); try { removeSymbolsByProject(project); + for (SpringIndexer index : this.indexer) { + index.removeProject(project); + } log.debug("{} completed", this); } catch (Throwable e) { log.error("{} threw exception", this, e); 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 new file mode 100644 index 000000000..d7842bf13 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SymbolCache.java @@ -0,0 +1,28 @@ +/******************************************************************************* + * 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 + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.utils; + +import java.util.List; + +/** + * @author Martin Lippert + */ +public interface SymbolCache { + + void store(SymbolCacheKey cacheKey, String[] files, List generatedSymbols); + CachedSymbol[] retrieve(SymbolCacheKey cacheKey, String[] files); + + void update(SymbolCacheKey cacheKey, String file, long lastModified, List generatedSymbols); + + void remove(SymbolCacheKey cacheKey); + void removeFile(SymbolCacheKey symbolCacheKey, String file); + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SymbolCacheKey.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SymbolCacheKey.java new file mode 100644 index 000000000..17dd3e9e8 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SymbolCacheKey.java @@ -0,0 +1,81 @@ +/******************************************************************************* + * 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 + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.utils; + +/** + * @author Martin Lippert + */ +public class SymbolCacheKey { + + private static final String SEPARATOR = "-"; + + private final String primaryIdentifier; + private final String version; + + public SymbolCacheKey(String primaryIdentifier, String version) { + this.primaryIdentifier = primaryIdentifier; + this.version = version; + } + + public String getPrimaryIdentifier() { + return primaryIdentifier; + } + + public String getVersion() { + return version; + } + + @Override + public String toString() { + return primaryIdentifier + SEPARATOR + version; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((primaryIdentifier == null) ? 0 : primaryIdentifier.hashCode()); + result = prime * result + ((version == null) ? 0 : version.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + + SymbolCacheKey other = (SymbolCacheKey) obj; + return this.toString().equals(other.toString()); + } + + public static SymbolCacheKey parse(String fileName) { + if (fileName != null && fileName.length() > 0) { + int separatorIndex = fileName.lastIndexOf(SEPARATOR); + if (separatorIndex > 0) { + String primary = fileName.substring(0, separatorIndex); + String version = fileName.substring(separatorIndex + 1); + + int fileextensionIndex = version.lastIndexOf("."); + if (fileextensionIndex > 0) { + version = version.substring(0, fileextensionIndex); + } + + return new SymbolCacheKey(primary, version); + } + } + return 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 new file mode 100644 index 000000000..e0fb458de --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SymbolCacheOnDisc.java @@ -0,0 +1,258 @@ +/******************************************************************************* + * 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 + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.utils; + +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +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.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import com.google.gson.stream.JsonReader; + +/** + * @author Martin Lippert + */ +public class SymbolCacheOnDisc implements SymbolCache { + + private final File cacheDirectory; + private final Map stores; + + private static final Logger log = LoggerFactory.getLogger(SymbolCacheOnDisc.class); + + public SymbolCacheOnDisc() throws Exception { + this(new File(System.getProperty("user.home") + File.separatorChar + ".sts4" + File.separatorChar + ".symbolCache")); + } + + public SymbolCacheOnDisc(File cacheDirectory) throws Exception { + this.cacheDirectory = cacheDirectory; + this.stores = new ConcurrentHashMap<>(); + + if (!this.cacheDirectory.exists()) { + this.cacheDirectory.mkdirs(); + } + + if (!this.cacheDirectory.exists()) { + throw new Exception("symbol cache directory could not be created:"); + } + } + + @Override + public void store(SymbolCacheKey cacheKey, String[] files, List generatedSymbols) { + SortedMap timestampedFiles = new TreeMap<>(); + + timestampedFiles = Arrays.stream(files) + .filter(file -> new File(file).exists()) + .collect(Collectors.toMap(file -> file, file -> new File(file).lastModified(), (v1,v2) -> { throw new RuntimeException(String.format("Duplicate key for values %s and %s", v1, v2));}, TreeMap::new)); + + save(cacheKey, generatedSymbols, timestampedFiles); + } + + @Override + public CachedSymbol[] retrieve(SymbolCacheKey cacheKey, String[] files) { + try { + File cacheStore = new File(cacheDirectory, cacheKey.toString() + ".json"); + if (cacheStore.exists()) { + Gson gson = createGson(); + JsonReader reader = new JsonReader(new FileReader(cacheStore)); + CacheStore store = gson.fromJson(reader, CacheStore.class); + + SortedMap timestampedFiles = Arrays.stream(files) + .filter(file -> new File(file).exists()) + .collect(Collectors.toMap(file -> file, file -> new File(file).lastModified(), (v1,v2) -> { throw new RuntimeException(String.format("Duplicate key for values %s and %s", v1, v2));}, TreeMap::new)); + + if (isFileMatch(timestampedFiles, store.getTimestampedFiles())) { + this.stores.put(cacheKey, store); + + List symbols = store.getSymbols(); + return (CachedSymbol[]) symbols.toArray(new CachedSymbol[symbols.size()]); + } + } + } + catch (Exception e) { + log.error("error reading cached symbols", e); + } + return null; + } + + @Override + public void removeFile(SymbolCacheKey cacheKey, String file) { + CacheStore cacheStore = this.stores.get(cacheKey); + if (cacheStore != null) { + String docURI = UriUtil.toUri(new File(file)).toString(); + + SortedMap timestampedFiles = new TreeMap<>(cacheStore.getTimestampedFiles()); + timestampedFiles.remove(file); + + List cachedSymbols = cacheStore.getSymbols().stream() + .filter(cachedSymbol -> !cachedSymbol.getDocURI().equals(docURI)) + .collect(Collectors.toList()); + + save(cacheKey, cachedSymbols, timestampedFiles); + } + } + + @Override + public void remove(SymbolCacheKey cacheKey) { + File cacheStore = new File(cacheDirectory, cacheKey.toString() + ".json"); + if (cacheStore.exists()) { + cacheStore.delete(); + this.stores.remove(cacheKey); + } + } + + @Override + public void update(SymbolCacheKey cacheKey, String file, long lastModified, List generatedSymbols) { + CacheStore cacheStore = this.stores.get(cacheKey); + + if (cacheStore != null) { + String docURI = UriUtil.toUri(new File(file)).toString(); + + SortedMap timestampedFiles = new TreeMap<>(cacheStore.getTimestampedFiles()); + timestampedFiles.put(file, lastModified); + + List cachedSymbols = cacheStore.getSymbols().stream() + .filter(cachedSymbol -> !cachedSymbol.getDocURI().equals(docURI)) + .collect(Collectors.toList()); + + cachedSymbols.addAll(generatedSymbols); + + save(cacheKey, cachedSymbols, timestampedFiles); + } + } + + private void save(SymbolCacheKey cacheKey, List generatedSymbols, + SortedMap timestampedFiles) { + CacheStore store = new CacheStore(cacheKey.toString(), timestampedFiles, generatedSymbols); + this.stores.put(cacheKey, store); + + try (FileWriter writer = new FileWriter(new File(cacheDirectory, cacheKey.toString() + ".json"))) + { + Gson gson = createGson(); + gson.toJson(store, writer); + + cleanupCacheFiles(cacheKey); + } + catch (Exception e) { + log.error("cannot write symbol cache", e); + } + } + + private boolean isFileMatch(SortedMap files1, SortedMap files2) { + if (files1.size() != files2.size()) return false; + + for (String file : files1.keySet()) { + if (!files2.containsKey(file)) return false; + if (!files1.get(file).equals(files2.get(file))) return false; + } + + return true; + } + + private void cleanupCacheFiles(SymbolCacheKey cacheKey) { + File[] cacheFiles = this.cacheDirectory.listFiles(); + + for (int i = 0; i < cacheFiles.length; i++) { + String fileName = cacheFiles[i].getName(); + SymbolCacheKey key = SymbolCacheKey.parse(fileName); + + if (key != null && !key.equals(cacheKey) + && key.getPrimaryIdentifier().equals(cacheKey.getPrimaryIdentifier())) { + cacheFiles[i].delete(); + } + } + } + + private Gson createGson() { + return new GsonBuilder().registerTypeAdapter(SymbolAddOnInformation.class, new SymbolAddOnInformationAdapter()).create(); + } + + + /** + * internal storage structure + */ + private static class CacheStore { + + private final String cacheKey; + private final SortedMap timestampedFiles; + private final List symbols; + + public CacheStore(String cacheKey, SortedMap timestampedFiles, List symbols) { + super(); + this.cacheKey = cacheKey; + this.timestampedFiles = timestampedFiles; + this.symbols = symbols; + } + + public String getCacheKey() { + return cacheKey; + } + + public List getSymbols() { + return symbols; + } + + public SortedMap getTimestampedFiles() { + return timestampedFiles; + } + + } + + /** + * gson adapter to store subtype information for symbol addon informations + */ + private static class SymbolAddOnInformationAdapter implements JsonSerializer, JsonDeserializer { + + @Override + public JsonElement serialize(SymbolAddOnInformation addonInfo, Type typeOfSrc, JsonSerializationContext context) { + JsonObject result = new JsonObject(); + result.add("type", new JsonPrimitive(addonInfo.getClass().getName())); + result.add("data", context.serialize(addonInfo)); + return result; + } + + @Override + public SymbolAddOnInformation deserialize(JsonElement json, Type type, JsonDeserializationContext context) throws JsonParseException { + JsonObject parsedObject = json.getAsJsonObject(); + String className = parsedObject.get("type").getAsString(); + JsonElement element = parsedObject.get("data"); + + try { + return context.deserialize(element, Class.forName(className)); + } catch (ClassNotFoundException cnfe) { + throw new JsonParseException("cannot parse data from unknown SymbolAddOnInformation subtype: " + type, cnfe); + } + } + } + + +} 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 new file mode 100644 index 000000000..f20cfeceb --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/SymbolCacheVoid.java @@ -0,0 +1,41 @@ +/******************************************************************************* + * 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 + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.utils; + +import java.util.List; + +/** + * @author Martin Lippert + */ +public class SymbolCacheVoid implements SymbolCache { + + @Override + public void store(SymbolCacheKey cacheKey, String[] files, List generatedSymbols) { + } + + @Override + public CachedSymbol[] retrieve(SymbolCacheKey cacheKey, String[] files) { + return null; + } + + @Override + public void update(SymbolCacheKey cacheKey, String file, long lastModified, List generatedSymbols) { + } + + @Override + public void remove(SymbolCacheKey cacheKey) { + } + + @Override + public void removeFile(SymbolCacheKey symbolCacheKey, String file) { + } + +} diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/utils/test/SymbolCacheKeyTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/utils/test/SymbolCacheKeyTest.java new file mode 100644 index 000000000..3d9889432 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/utils/test/SymbolCacheKeyTest.java @@ -0,0 +1,78 @@ +/******************************************************************************* + * 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 + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.utils.test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; + +import org.junit.Test; +import org.springframework.ide.vscode.boot.java.utils.SymbolCacheKey; + +/** + * @author Martin Lippert + */ +public class SymbolCacheKeyTest { + + @Test + public void testCacheKey() { + SymbolCacheKey key = new SymbolCacheKey("primary", "version"); + + assertEquals("primary", key.getPrimaryIdentifier()); + assertEquals("version", key.getVersion()); + assertEquals("primary-version", key.toString()); + } + + @Test + public void testCacheKeyParsingFromFileName() { + SymbolCacheKey key = SymbolCacheKey.parse("primary-version.json"); + assertEquals("primary", key.getPrimaryIdentifier()); + assertEquals("version", key.getVersion()); + + key = SymbolCacheKey.parse("primary-name-with-separator-123ABC.json"); + assertEquals("primary-name-with-separator", key.getPrimaryIdentifier()); + assertEquals("123ABC", key.getVersion()); + } + + @Test + public void testCacheKeyParsingWithoutFileExtension() { + SymbolCacheKey key = SymbolCacheKey.parse("primary-version"); + assertEquals("primary", key.getPrimaryIdentifier()); + assertEquals("version", key.getVersion()); + + key = SymbolCacheKey.parse("primary-name-with-separator-123ABC"); + assertEquals("primary-name-with-separator", key.getPrimaryIdentifier()); + assertEquals("123ABC", key.getVersion()); + } + + @Test + public void testCacheKeyEquals() { + SymbolCacheKey key1 = new SymbolCacheKey("primary", "1"); + SymbolCacheKey key2 = new SymbolCacheKey("primary", "1"); + + SymbolCacheKey key3 = new SymbolCacheKey("primary", "2"); + SymbolCacheKey key4 = new SymbolCacheKey("secondary", "1"); + + assertEquals(key1, key1); + assertEquals(key2, key2); + assertEquals(key3, key3); + assertEquals(key4, key4); + + assertEquals(key1, key2); + + assertNotEquals(key1, key3); + assertNotEquals(key2, key3); + assertNotEquals(key3, key4); + assertNotEquals(key1, key4); + assertNotEquals(key4, key1); + assertNotEquals(key4, key2); + } + +} 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 new file mode 100644 index 000000000..1ee767465 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/utils/test/SymbolCacheOnDiscTest.java @@ -0,0 +1,380 @@ +/******************************************************************************* + * 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 + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.utils.test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.FileTime; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.io.FileUtils; +import org.eclipse.lsp4j.Location; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.SymbolInformation; +import org.eclipse.lsp4j.SymbolKind; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.ide.vscode.boot.java.handlers.EnhancedSymbolInformation; +import org.springframework.ide.vscode.boot.java.handlers.SymbolAddOnInformation; +import org.springframework.ide.vscode.boot.java.requestmapping.WebfluxElementsInformation; +import org.springframework.ide.vscode.boot.java.utils.CachedSymbol; +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; + +public class SymbolCacheOnDiscTest { + + private Path tempDir; + private SymbolCacheOnDisc cache; + + @Before + public void setup() throws Exception { + tempDir = Files.createTempDirectory("cachetest"); + cache = new SymbolCacheOnDisc(tempDir.toFile()); + } + + @After + public void deleteTempDir() throws Exception { + FileUtils.deleteDirectory(tempDir.toFile()); + } + + @Test + public void testEmptyCache() throws Exception { + CachedSymbol[] result = cache.retrieve(new SymbolCacheKey("something", "0"), new String[0]); + assertNull(result); + } + + @Test + public void testSimpleValidCache() throws Exception { + Path file1 = Paths.get(tempDir.toAbsolutePath().toString(), "tempFile1"); + Path file2 = Paths.get(tempDir.toAbsolutePath().toString(), "tempFile2"); + Path file3 = Paths.get(tempDir.toAbsolutePath().toString(), "tempFile3"); + + Files.createFile(file1); + Files.createFile(file2); + Files.createFile(file3); + + FileTime timeFile1 = Files.getLastModifiedTime(file1); + String[] files = {file1.toString(), file2.toString(), file3.toString()}; + + List generatedSymbols = new ArrayList<>(); + SymbolInformation symbol = new SymbolInformation("symbol1", SymbolKind.Field, new Location("docURI", new Range(new Position(3, 10), new Position(3, 20)))); + EnhancedSymbolInformation enhancedSymbol = new EnhancedSymbolInformation(symbol, null); + generatedSymbols.add(new CachedSymbol("", timeFile1.toMillis(), enhancedSymbol)); + + cache.store(new SymbolCacheKey("somekey", "1"), files, generatedSymbols); + + CachedSymbol[] cachedSymbols = cache.retrieve(new SymbolCacheKey("somekey", "1"), files); + assertNotNull(cachedSymbols); + assertEquals(1, cachedSymbols.length); + + assertEquals("symbol1", cachedSymbols[0].getEnhancedSymbol().getSymbol().getName()); + 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()); + } + + @Test + public void testDifferentCacheKey() throws Exception { + Path file1 = Paths.get(tempDir.toAbsolutePath().toString(), "tempFile1"); + Path file2 = Paths.get(tempDir.toAbsolutePath().toString(), "tempFile2"); + Path file3 = Paths.get(tempDir.toAbsolutePath().toString(), "tempFile3"); + + Files.createFile(file1); + Files.createFile(file2); + Files.createFile(file3); + + FileTime timeFile1 = Files.getLastModifiedTime(file1); + String[] files = {file1.toString(), file2.toString(), file3.toString()}; + + List generatedSymbols = new ArrayList<>(); + SymbolInformation symbol = new SymbolInformation("symbol1", SymbolKind.Field, new Location("docURI", new Range(new Position(3, 10), new Position(3, 20)))); + EnhancedSymbolInformation enhancedSymbol = new EnhancedSymbolInformation(symbol, null); + generatedSymbols.add(new CachedSymbol("", timeFile1.toMillis(), enhancedSymbol)); + + cache.store(new SymbolCacheKey("somekey", "1"), files, generatedSymbols); + + CachedSymbol[] cachedSymbols = cache.retrieve(new SymbolCacheKey("otherkey", "1"), files); + assertNull(cachedSymbols); + } + + @Test + public void testFileTouched() throws Exception { + Path file1 = Paths.get(tempDir.toAbsolutePath().toString(), "tempFile1"); + Path file2 = Paths.get(tempDir.toAbsolutePath().toString(), "tempFile2"); + Path file3 = Paths.get(tempDir.toAbsolutePath().toString(), "tempFile3"); + + Files.createFile(file1); + Files.createFile(file2); + Files.createFile(file3); + + FileTime timeFile1 = Files.getLastModifiedTime(file1); + String[] files = {file1.toString(), file2.toString(), file3.toString()}; + + List generatedSymbols = new ArrayList<>(); + SymbolInformation symbol = new SymbolInformation("symbol1", SymbolKind.Field, new Location("docURI", new Range(new Position(3, 10), new Position(3, 20)))); + EnhancedSymbolInformation enhancedSymbol = new EnhancedSymbolInformation(symbol, null); + generatedSymbols.add(new CachedSymbol("", timeFile1.toMillis(), enhancedSymbol)); + + cache.store(new SymbolCacheKey("somekey", "1"), files, generatedSymbols); + + assertTrue(file1.toFile().setLastModified(timeFile1.toMillis() + 1000)); + + CachedSymbol[] cachedSymbols = cache.retrieve(new SymbolCacheKey("somekey", "1"), files); + assertNull(cachedSymbols); + } + + @Test + public void testMoreFiles() throws Exception { + Path file1 = Paths.get(tempDir.toAbsolutePath().toString(), "tempFile1"); + Path file2 = Paths.get(tempDir.toAbsolutePath().toString(), "tempFile2"); + Path file3 = Paths.get(tempDir.toAbsolutePath().toString(), "tempFile3"); + + Files.createFile(file1); + Files.createFile(file2); + Files.createFile(file3); + + String[] files = {file1.toString(), file2.toString()}; + cache.store(new SymbolCacheKey("somekey", "1"), files, new ArrayList<>()); + + String[] moreFiles = {file1.toString(), file2.toString(), file3.toString()}; + CachedSymbol[] cachedSymbols = cache.retrieve(new SymbolCacheKey("somekey", "1"), moreFiles); + assertNull(cachedSymbols); + } + + @Test + public void testFewerFiles() throws Exception { + Path file1 = Paths.get(tempDir.toAbsolutePath().toString(), "tempFile1"); + Path file2 = Paths.get(tempDir.toAbsolutePath().toString(), "tempFile2"); + Path file3 = Paths.get(tempDir.toAbsolutePath().toString(), "tempFile3"); + + Files.createFile(file1); + Files.createFile(file2); + Files.createFile(file3); + + String[] files = {file1.toString(), file2.toString(), file3.toString()}; + cache.store(new SymbolCacheKey("somekey", "1"), files, new ArrayList<>()); + + String[] fewerFiles = {file1.toString(), file2.toString()}; + CachedSymbol[] cachedSymbols = cache.retrieve(new SymbolCacheKey("somekey", "1"), fewerFiles); + assertNull(cachedSymbols); + } + + @Test + public void testDeleteOldCacheFileIfNewOneIsStored() throws Exception { + SymbolCacheKey key1 = new SymbolCacheKey("somekey", "1"); + cache.store(key1, new String[0], new ArrayList<>()); + assertTrue(Files.exists(tempDir.resolve(Paths.get(key1.toString() + ".json")))); + + SymbolCacheKey key2 = new SymbolCacheKey("somekey", "2"); + cache.store(key2, new String[0], new ArrayList<>()); + assertTrue(Files.exists(tempDir.resolve(Paths.get(key2.toString() + ".json")))); + assertFalse(Files.exists(tempDir.resolve(Paths.get(key1.toString() + ".json")))); + } + + @Test + public void testEnhancedInformationSubclasses() throws Exception { + Path file1 = Paths.get(tempDir.toAbsolutePath().toString(), "tempFile1"); + Files.createFile(file1); + + FileTime timeFile1 = Files.getLastModifiedTime(file1); + String[] files = {file1.toString()}; + String doc1URI = UriUtil.toUri(file1.toFile()).toString(); + + List generatedSymbols = new ArrayList<>(); + + SymbolInformation symbol = new SymbolInformation("symbol1", SymbolKind.Field, new Location(doc1URI, new Range(new Position(3, 10), new Position(3, 20)))); + WebfluxElementsInformation addon = new WebfluxElementsInformation(new Range(new Position(4, 4), new Position(5, 5)), new Range(new Position(6, 6), new Position(7, 7))); + EnhancedSymbolInformation enhancedSymbol = new EnhancedSymbolInformation(symbol, new SymbolAddOnInformation[] {addon}); + + generatedSymbols.add(new CachedSymbol(doc1URI, timeFile1.toMillis(), enhancedSymbol)); + + cache.store(new SymbolCacheKey("somekey", "1"), files, generatedSymbols); + + CachedSymbol[] cachedSymbols = cache.retrieve(new SymbolCacheKey("somekey", "1"), files); + assertNotNull(cachedSymbols); + assertEquals(1, cachedSymbols.length); + + assertEquals("symbol1", cachedSymbols[0].getEnhancedSymbol().getSymbol().getName()); + assertEquals(SymbolKind.Field, cachedSymbols[0].getEnhancedSymbol().getSymbol().getKind()); + assertEquals(new Location(doc1URI, new Range(new Position(3, 10), new Position(3, 20))), cachedSymbols[0].getEnhancedSymbol().getSymbol().getLocation()); + + SymbolAddOnInformation[] retrievedAddOns = cachedSymbols[0].getEnhancedSymbol().getAdditionalInformation(); + assertNotNull(retrievedAddOns); + assertEquals(1, retrievedAddOns.length); + assertTrue(retrievedAddOns[0] instanceof WebfluxElementsInformation); + + Range[] ranges = ((WebfluxElementsInformation)retrievedAddOns[0]).getRanges(); + assertEquals(2, ranges.length); + assertEquals(new Range(new Position(4, 4), new Position(5, 5)), ranges[0]); + assertEquals(new Range(new Position(6, 6), new Position(7, 7)), ranges[1]); + } + + @Test + public void testSymbolAddedToExistingFile() throws Exception { + Path file1 = Paths.get(tempDir.toAbsolutePath().toString(), "tempFile1"); + + Files.createFile(file1); + + FileTime timeFile1 = Files.getLastModifiedTime(file1); + String[] files = {file1.toAbsolutePath().toString()}; + + String doc1URI = UriUtil.toUri(file1.toFile()).toString(); + + List generatedSymbols1 = new ArrayList<>(); + SymbolInformation symbol1 = new SymbolInformation("symbol1", SymbolKind.Field, new Location("docURI", new Range(new Position(3, 10), new Position(3, 20)))); + EnhancedSymbolInformation enhancedSymbol1 = new EnhancedSymbolInformation(symbol1, null); + generatedSymbols1.add(new CachedSymbol(doc1URI, timeFile1.toMillis(), enhancedSymbol1)); + + cache.store(new SymbolCacheKey("somekey", "1"), files, generatedSymbols1); + + List generatedSymbols2 = new ArrayList<>(); + symbol1 = new SymbolInformation("symbol1", SymbolKind.Field, new Location(doc1URI, new Range(new Position(3, 10), new Position(3, 20)))); + enhancedSymbol1 = new EnhancedSymbolInformation(symbol1, null); + + SymbolInformation symbol2 = new SymbolInformation("symbol2", SymbolKind.Interface, new Location(doc1URI, new Range(new Position(5, 5), new Position(5, 10)))); + EnhancedSymbolInformation enhancedSymbol2 = new EnhancedSymbolInformation(symbol2, null); + + generatedSymbols2.add(new CachedSymbol(doc1URI, timeFile1.toMillis() + 2000, enhancedSymbol1)); + 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); + + CachedSymbol[] cachedSymbols = cache.retrieve(new SymbolCacheKey("somekey", "1"), files); + assertNotNull(cachedSymbols); + assertEquals(2, cachedSymbols.length); + } + + @Test + public void testSymbolRemovedFromExistingFile() throws Exception { + Path file1 = Paths.get(tempDir.toAbsolutePath().toString(), "tempFile1"); + + Files.createFile(file1); + + FileTime timeFile1 = Files.getLastModifiedTime(file1); + String[] files = {file1.toAbsolutePath().toString()}; + + String doc1URI = UriUtil.toUri(file1.toFile()).toString(); + + List generatedSymbols1 = new ArrayList<>(); + SymbolInformation symbol1 = new SymbolInformation("symbol1", SymbolKind.Field, new Location("docURI", new Range(new Position(3, 10), new Position(3, 20)))); + EnhancedSymbolInformation enhancedSymbol1 = new EnhancedSymbolInformation(symbol1, null); + generatedSymbols1.add(new CachedSymbol(doc1URI, timeFile1.toMillis(), enhancedSymbol1)); + + cache.store(new SymbolCacheKey("somekey", "1"), files, generatedSymbols1); + + List generatedSymbols2 = new ArrayList<>(); + assertTrue(file1.toFile().setLastModified(timeFile1.toMillis() + 2000)); + + cache.update(new SymbolCacheKey("somekey", "1"), file1.toAbsolutePath().toString(), timeFile1.toMillis() + 2000, generatedSymbols2); + + CachedSymbol[] cachedSymbols = cache.retrieve(new SymbolCacheKey("somekey", "1"), files); + assertNotNull(cachedSymbols); + assertEquals(0, cachedSymbols.length); + } + + @Test + public void testSymbolAddedToNewFile() 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 = new ArrayList<>(); + SymbolInformation symbol1 = new SymbolInformation("symbol1", SymbolKind.Field, new Location(doc1URI, new Range(new Position(3, 10), new Position(3, 20)))); + EnhancedSymbolInformation enhancedSymbol1 = new EnhancedSymbolInformation(symbol1, null); + generatedSymbols1.add(new CachedSymbol(doc1URI, timeFile1.toMillis(), enhancedSymbol1)); + + cache.store(new SymbolCacheKey("somekey", "1"), files, generatedSymbols1); + + List generatedSymbols2 = new ArrayList<>(); + SymbolInformation symbol2 = new SymbolInformation("symbol2", SymbolKind.Interface, new Location(doc2URI, new Range(new Position(5, 5), new Position(5, 10)))); + EnhancedSymbolInformation enhancedSymbol2 = new EnhancedSymbolInformation(symbol2, null); + + generatedSymbols2.add(new CachedSymbol(doc2URI, timeFile2.toMillis(), enhancedSymbol2)); + + cache.update(new SymbolCacheKey("somekey", "1"), file2.toString(), timeFile2.toMillis(), generatedSymbols2); + + CachedSymbol[] cachedSymbols = cache.retrieve(new SymbolCacheKey("somekey", "1"), new String[] {file1.toString(), file2.toString()}); + assertNotNull(cachedSymbols); + assertEquals(2, cachedSymbols.length); + } + + @Test + public void testProjectDeleted() throws Exception { + SymbolCacheKey key1 = new SymbolCacheKey("somekey", "1"); + cache.store(key1, new String[0], new ArrayList<>()); + assertTrue(Files.exists(tempDir.resolve(Paths.get(key1.toString() + ".json")))); + + cache.remove(key1); + assertFalse(Files.exists(tempDir.resolve(Paths.get(key1.toString() + ".json")))); + } + + @Test + public void testFileDeleted() 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.toAbsolutePath().toString(), file2.toAbsolutePath().toString()}; + + String doc1URI = UriUtil.toUri(file1.toFile()).toString(); + String doc2URI = UriUtil.toUri(file2.toFile()).toString(); + + List generatedSymbols = new ArrayList<>(); + + SymbolInformation symbol1 = new SymbolInformation("symbol1", SymbolKind.Field, new Location(doc1URI, new Range(new Position(3, 10), new Position(3, 20)))); + EnhancedSymbolInformation enhancedSymbol1 = new EnhancedSymbolInformation(symbol1, null); + + SymbolInformation symbol2 = new SymbolInformation("symbol2", SymbolKind.Field, new Location(doc2URI, new Range(new Position(5, 10), new Position(5, 20)))); + EnhancedSymbolInformation enhancedSymbol2 = new EnhancedSymbolInformation(symbol2, null); + + generatedSymbols.add(new CachedSymbol(doc1URI, timeFile1.toMillis(), enhancedSymbol1)); + generatedSymbols.add(new CachedSymbol(doc2URI, timeFile2.toMillis(), enhancedSymbol2)); + + cache.store(new SymbolCacheKey("somekey", "1"), files, generatedSymbols); + 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); + 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()); + } + +}