diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/index/cache/IndexCacheOnDiscDeltaBased.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/index/cache/IndexCacheOnDiscDeltaBased.java new file mode 100644 index 000000000..cc3660b6b --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/index/cache/IndexCacheOnDiscDeltaBased.java @@ -0,0 +1,589 @@ +/******************************************************************************* + * Copyright (c) 2024 Broadcom + * 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: + * Broadcom - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.index.cache; + +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.lang.reflect.Array; +import java.lang.reflect.Type; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +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.eclipse.lsp4j.Location; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ide.vscode.boot.java.handlers.SymbolAddOnInformation; +import org.springframework.ide.vscode.commons.protocol.spring.AnnotationMetadata; +import org.springframework.ide.vscode.commons.protocol.spring.Bean; +import org.springframework.ide.vscode.commons.protocol.spring.DefaultValues; +import org.springframework.ide.vscode.commons.protocol.spring.InjectionPoint; +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; +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.reflect.TypeToken; +import com.google.gson.stream.JsonReader; + +/** + * @author Martin Lippert + */ +public class IndexCacheOnDiscDeltaBased implements IndexCache { + + private final File cacheDirectory; + + private static final Logger log = LoggerFactory.getLogger(IndexCacheOnDiscDeltaBased.class); + + public IndexCacheOnDiscDeltaBased(File cacheDirectory) { + this.cacheDirectory = cacheDirectory; + + if (!this.cacheDirectory.exists()) { + this.cacheDirectory.mkdirs(); + } + + if (!this.cacheDirectory.exists()) { + log.warn("symbol cache directory does not exist and cannot be created: " + this.cacheDirectory.toString()); + } + } + + @Override + public void store(IndexCacheKey cacheKey, String[] files, List elements, Multimap dependencies, Class type) { + if (dependencies == null) { + dependencies = ImmutableMultimap.of(); + } + + SortedMap timestampedFiles = new TreeMap<>(); + + timestampedFiles = Arrays.stream(files) + .filter(file -> new File(file).exists()) + .collect(Collectors.toMap(file -> file, file -> { + try { + return Files.getLastModifiedTime(new File(file).toPath()).toMillis(); + } catch (IOException e) { + throw new RuntimeException(e); + } + }, (v1,v2) -> { throw new RuntimeException(String.format("Duplicate key for values %s and %s", v1, v2));}, TreeMap::new)); + + IndexCacheStore store = new IndexCacheStore(timestampedFiles, elements, dependencies.asMap(), type); + persist(cacheKey, new DeltaSnapshot(store), false); + } + + @SuppressWarnings("unchecked") + @Override + public Pair> retrieve(IndexCacheKey cacheKey, String[] files, Class type) { + File cacheStore = new File(cacheDirectory, cacheKey.toString() + ".json"); + if (cacheStore.exists()) { + + try (JsonReader reader = new JsonReader(new FileReader(cacheStore))) { + IndexCacheStore store = retrieveStoreFromIncrementalStorage(cacheKey, type); + + SortedMap timestampedFiles = Arrays.stream(files) + .filter(file -> new File(file).exists()) + .collect(Collectors.toMap(file -> file, file -> { + try { + return Files.getLastModifiedTime(new File(file).toPath()).toMillis(); + } catch (IOException e) { + throw new RuntimeException(e); + } + }, (v1,v2) -> { throw new RuntimeException(String.format("Duplicate key for values %s and %s", v1, v2));}, TreeMap::new)); + + if (isFileMatch(timestampedFiles, store.getTimestampedFiles())) { + + List symbols = store.getSymbols(); + + Map> storedDependencies = store.getDependencies(); + Multimap dependencies = MultimapBuilder.hashKeys().hashSetValues().build(); + + if (storedDependencies!=null && !storedDependencies.isEmpty()) { + for (Entry> entry : storedDependencies.entrySet()) { + dependencies.replaceValues(entry.getKey(), entry.getValue()); + } + } + + return Pair.of( + (T[]) symbols.toArray((T[]) Array.newInstance(type, symbols.size())), + MultimapBuilder.hashKeys().hashSetValues().build(dependencies) + ); + } + } + catch (Exception e) { + log.error("error reading cached symbols", e); + } + } + return null; + } + + @Override + public void removeFile(IndexCacheKey cacheKey, String file, Class type) { + persist(cacheKey, new DeltaDelete(new String[] {file}), true); + } + + @Override + public void removeFiles(IndexCacheKey cacheKey, String[] files, Class type) { + persist(cacheKey, new DeltaDelete(files), true); + } + + @Override + public void remove(IndexCacheKey cacheKey) { + File cacheStore = new File(cacheDirectory, cacheKey.toString() + ".json"); + if (cacheStore.exists()) { + cacheStore.delete(); + } + } + + @Override + public void update(IndexCacheKey cacheKey, String file, long lastModified, + List generatedSymbols, Set dependencies, Class type) { + if (dependencies == null) { + dependencies = ImmutableSet.of(); + } + + // creating and storing delta + SortedMap timestampsDelta = new TreeMap<>(); + timestampsDelta.put(file, lastModified); + + Map> dependenciesDelta = new HashMap<>(); + dependenciesDelta.put(file, ImmutableSet.copyOf(dependencies)); + + IndexCacheStore deltaStore = new IndexCacheStore(timestampsDelta, generatedSymbols, dependenciesDelta, type); + persist(cacheKey, new DeltaUpdate(deltaStore), true); + } + + @Override + public void update(IndexCacheKey cacheKey, String[] files, long[] lastModified, + List generatedSymbols, Multimap dependencies, Class type) { + if (dependencies == null) { + dependencies = ImmutableMultimap.of(); + } + + // creating and storing delta + SortedMap timestampsDelta = new TreeMap<>(); + Map> dependenciesDelta = new HashMap<>(); + + for (int i = 0; i < files.length; i++) { + timestampsDelta.put(files[i], lastModified[i]); + dependenciesDelta.put(files[i], ImmutableSet.copyOf(dependencies.get(files[i]))); + } + + IndexCacheStore deltaStore = new IndexCacheStore(timestampsDelta, generatedSymbols, dependenciesDelta, type); + persist(cacheKey, new DeltaUpdate(deltaStore), true); + } + + @Override + public long getModificationTimestamp(IndexCacheKey cacheKey, String file) { +// IndexCacheStore cacheStore = this.stores.get(cacheKey); +// +// if (cacheStore != null) { +// Long result = cacheStore.getTimestampedFiles().get(file); +// if (result != null) { +// return result; +// } +// } + + return 0; + } + + 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 cleanupCache(IndexCacheKey cacheKey) { + File[] cacheFiles = this.cacheDirectory.listFiles(); + + for (int i = 0; i < cacheFiles.length; i++) { + String fileName = cacheFiles[i].getName(); + IndexCacheKey key = IndexCacheKey.parse(fileName); + + if (key != null && !key.equals(cacheKey) + && key.getProject().equals(cacheKey.getProject()) + && key.getIndexer().equals(cacheKey.getIndexer()) + && key.getCategory().equals(cacheKey.getCategory())) { + cacheFiles[i].delete(); + } + // cleanup old cache files without category information (pre 4.19.1 release) + else if (key != null && !key.equals(cacheKey) + && key.getProject().equals(cacheKey.getProject()) + && key.getIndexer().equals(cacheKey.getIndexer()) + && key.getCategory().equals("")) { + cacheFiles[i].delete(); + } + } + } + + private void persist(IndexCacheKey cacheKey, DeltaElement delta, boolean append) { + DeltaStorage deltaStorage = new DeltaStorage(delta); + + try (FileWriter writer = new FileWriter(new File(cacheDirectory, cacheKey.toString() + ".json"), append)) + { + Gson gson = createGson(); + gson.toJson(deltaStorage, writer); + + writer.write("\n"); + + cleanupCache(cacheKey); + } + catch (Exception e) { + log.error("cannot write symbol cache", e); + } + } + + private IndexCacheStore retrieveStoreFromIncrementalStorage(IndexCacheKey cacheKey, Class type) { + IndexCacheStore store = new IndexCacheStore<>(new TreeMap<>(), new ArrayList(), new HashMap<>(), type); + + File cacheStore = new File(cacheDirectory, cacheKey.toString() + ".json"); + if (cacheStore.exists()) { + + Gson gson = createGson(); + + + try (JsonReader reader = new JsonReader(new FileReader(cacheStore))) { + + DeltaStorage readElement; + while ((readElement = gson.fromJson(reader, DeltaStorage.class)) != null) { + DeltaElement delta = readElement.storedElement; + store = delta.apply(store); + } + } + catch (Exception e) { + log.error("error reading cached symbols", e); + } + } + return store; + } + + + + public static Gson createGson() { + return new GsonBuilder() + .registerTypeAdapter(DeltaStorage.class, new DeltaStorageAdapter()) + .registerTypeAdapter(SymbolAddOnInformation.class, new SymbolAddOnInformationAdapter()) + .registerTypeAdapter(Bean.class, new BeanJsonAdapter()) + .registerTypeAdapter(InjectionPoint.class, new InjectionPointJsonAdapter()) + .registerTypeAdapter(IndexCacheStore.class, new IndexCacheStoreAdapter()) + .create(); + } + + private static record DeltaStorage (DeltaElement storedElement) {} + + private static interface DeltaElement { + public IndexCacheStore apply(IndexCacheStore store); + } + + private static class DeltaDelete implements DeltaElement { + + private final String[] files; + + public DeltaDelete(String[] files) { + this.files = files; + } + + @Override + public IndexCacheStore apply(IndexCacheStore store) { + SortedMap timestampedFiles = store.getTimestampedFiles(); + Map> changedDeps = store.getDependencies(); + + Set docURIs = new HashSet<>(); + + for (String file : files) { + String docURI = UriUtil.toUri(new File(file)).toASCIIString(); + docURIs.add(docURI); + + timestampedFiles.remove(file); + changedDeps.remove(file); + } + + List symbols = store.getSymbols(); + for (Iterator iterator = symbols.iterator(); iterator.hasNext();) { + T t = iterator.next(); + + if (docURIs.contains(t.getDocURI())) { + iterator.remove(); + } + + } + + return store; + } + + } + + private static class DeltaUpdate implements DeltaElement { + + private IndexCacheStore deltaStore; + + public DeltaUpdate(IndexCacheStore deltaStore) { + this.deltaStore = deltaStore; + } + + @Override + public IndexCacheStore apply(IndexCacheStore store) { + SortedMap deltaTimestamps = deltaStore.getTimestampedFiles(); + SortedMap storeTimestamps = store.getTimestampedFiles(); + + Map> deltaDependencies = deltaStore.getDependencies(); + Map> storeDependencies = store.getDependencies(); + + List deltaSymbols = deltaStore.getSymbols(); + List storeSymbols = store.getSymbols(); + + Set allDocURIs = new HashSet<>(); + + for (Iterator iterator = deltaTimestamps.keySet().iterator(); iterator.hasNext();) { + String file = iterator.next(); + long timestamp = deltaTimestamps.get(file); + + // update cache internal map of timestamps per file + String docURI = UriUtil.toUri(new File(file)).toASCIIString(); + allDocURIs.add(docURI); + + storeTimestamps.put(file, timestamp); + + // update cache internal map of dependencies per file + Collection updatedDependencies = deltaDependencies.get(file); + if (updatedDependencies == null || updatedDependencies.isEmpty()) { + storeDependencies.remove(file); + } else { + storeDependencies.put(file, ImmutableSet.copyOf(updatedDependencies)); + } + + // update cache internal list of cached symbols (by removing old ones and adding all new ones) + for (Iterator symbols = storeSymbols.iterator(); symbols.hasNext();) { + if (allDocURIs.contains(symbols.next().getDocURI())) { + symbols.remove(); + } + } + + storeSymbols.addAll(deltaSymbols); + } + + return store; + } + + } + + private static class DeltaSnapshot implements DeltaElement { + + private IndexCacheStore store; + + public DeltaSnapshot(IndexCacheStore store) { + this.store = store; + } + + @Override + public IndexCacheStore apply(IndexCacheStore store) { + return this.store; + } + } + + + /** + * internal storage structure + */ + private static class IndexCacheStore { + + @SuppressWarnings("unused") + private final String elementType; + + private final SortedMap timestampedFiles; + private final List elements; + private final Map> dependencies; + + public IndexCacheStore(SortedMap timestampedFiles, List elements, Map> dependencies, Class elementType) { + this.timestampedFiles = timestampedFiles; + this.elements = elements; + this.dependencies = dependencies; + this.elementType = elementType.getName(); + } + + public Map> getDependencies() { + return dependencies; + } + + public List getSymbols() { + return elements; + } + + public SortedMap getTimestampedFiles() { + return timestampedFiles; + } + + } + + private static class IndexCacheStoreAdapter implements JsonDeserializer> { + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Override + public IndexCacheStore deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + JsonObject parsedObject = json.getAsJsonObject(); + + String className = parsedObject.get("elementType").getAsString(); + + try { + Class elementType = Class.forName(className); + + JsonElement elementsObject = parsedObject.get("elements"); + Type elementListType = TypeToken.getParameterized(List.class, elementType).getType(); + List elements = context.deserialize(elementsObject, elementListType); + + JsonElement timestampedFilesObject = parsedObject.get("timestampedFiles"); + Type timestampsMapType = TypeToken.getParameterized(SortedMap.class, String.class, Long.class).getType(); + SortedMap timestampedFiles = context.deserialize(timestampedFilesObject, timestampsMapType); + + JsonElement dependenciesObject = parsedObject.get("dependencies"); + Map dependencies = context.deserialize(dependenciesObject, HashMap.class); + + return new IndexCacheStore(timestampedFiles, elements, dependencies, elementType); + + } catch (ClassNotFoundException e) { + throw new JsonParseException("cannot parse data from index cache with element type: " + className, e); + } + + } + + } + + /** + * gson adapter to store subtype information for symbol addon informations + */ + private static class DeltaStorageAdapter implements JsonSerializer, JsonDeserializer { + + @Override + public JsonElement serialize(DeltaStorage deltaElement, Type typeOfSrc, JsonSerializationContext context) { + JsonObject result = new JsonObject(); + result.add("type", new JsonPrimitive(deltaElement.storedElement.getClass().getName())); + result.add("data", context.serialize(deltaElement.storedElement)); + return result; + } + + @Override + public DeltaStorage 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 new DeltaStorage(context.deserialize(element, Class.forName(className))); + } catch (ClassNotFoundException cnfe) { + throw new JsonParseException("cannot parse data from unknown SymbolAddOnInformation subtype: " + type, cnfe); + } + } + } + + /** + * 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); + } + } + } + + /** + * gson adapter to store subtype information for beans + */ + private static class BeanJsonAdapter implements JsonDeserializer { + + @Override + public Bean deserialize(JsonElement json, Type type, JsonDeserializationContext context) throws JsonParseException { + JsonObject parsedObject = json.getAsJsonObject(); + + String beanName = parsedObject.get("name").getAsString(); + String beanType = parsedObject.get("type").getAsString(); + + JsonElement locationObject = parsedObject.get("location"); + Location location = context.deserialize(locationObject, Location.class); + + JsonElement injectionPointObject = parsedObject.get("injectionPoints"); + InjectionPoint[] injectionPoints = context.deserialize(injectionPointObject, InjectionPoint[].class); + + JsonElement supertypesObject = parsedObject.get("supertypes"); + Set supertypes = context.deserialize(supertypesObject, Set.class); + + JsonElement annotationsObject = parsedObject.get("annotations"); + AnnotationMetadata[] annotations = annotationsObject == null ? DefaultValues.EMPTY_ANNOTATIONS : context.deserialize(annotationsObject, AnnotationMetadata[].class); + + return new Bean(beanName, beanType, location, injectionPoints, supertypes, annotations); + } + } + + private static class InjectionPointJsonAdapter implements JsonDeserializer { + + @Override + public InjectionPoint deserialize(JsonElement json, Type type, JsonDeserializationContext context) throws JsonParseException { + JsonObject parsedObject = json.getAsJsonObject(); + + String injectionPointName = parsedObject.get("name").getAsString(); + String injectionPointType = parsedObject.get("type").getAsString(); + + JsonElement locationObject = parsedObject.get("location"); + Location location = context.deserialize(locationObject, Location.class); + + JsonElement annotationsObject = parsedObject.get("annotations"); + AnnotationMetadata[] annotations = annotationsObject == null ? DefaultValues.EMPTY_ANNOTATIONS : context.deserialize(annotationsObject, AnnotationMetadata[].class); + + return new InjectionPoint(injectionPointName, injectionPointType, location, annotations); + } + } + +} diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/index/cache/test/IndexCacheOnDiscDeltaBasedTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/index/cache/test/IndexCacheOnDiscDeltaBasedTest.java new file mode 100644 index 000000000..ab3a942bb --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/index/cache/test/IndexCacheOnDiscDeltaBasedTest.java @@ -0,0 +1,704 @@ +/******************************************************************************* + * Copyright (c) 2024 Broadcom + * 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: + * Broadcom - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.index.cache.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +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 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; +import org.eclipse.lsp4j.SymbolKind; +import org.eclipse.lsp4j.WorkspaceSymbol; +import org.eclipse.lsp4j.jsonrpc.messages.Either; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.ide.vscode.boot.index.cache.AbstractIndexCacheable; +import org.springframework.ide.vscode.boot.index.cache.IndexCacheKey; +import org.springframework.ide.vscode.boot.index.cache.IndexCacheOnDiscDeltaBased; +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.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; + +public class IndexCacheOnDiscDeltaBasedTest { + + private static final String STORAGE_FILE_EXTENSION = ".json"; + + private static final IndexCacheKey CACHE_KEY_VERSION_1 = new IndexCacheKey("someProject", "someIndexer", "someCategory", "1"); + + private Path tempDir; + private IndexCacheOnDiscDeltaBased cache; + + @BeforeEach + public void setup() throws Exception { + tempDir = Files.createTempDirectory("cachetest"); + cache = new IndexCacheOnDiscDeltaBased(tempDir.toFile()); + } + + @AfterEach + public void deleteTempDir() throws Exception { + FileUtils.deleteDirectory(tempDir.toFile()); + } + + @Test + void testEmptyCache() throws Exception { + Pair> result = cache.retrieve(new IndexCacheKey("something", "someIndexer", "someCategory", "0"), new String[0], CachedSymbol.class); + assertNull(result); + } + + @Test + 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<>(); + WorkspaceSymbol symbol = new WorkspaceSymbol("symbol1", SymbolKind.Field, Either.forLeft(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(CACHE_KEY_VERSION_1, files, generatedSymbols, ImmutableMultimap.of( + file1.toString(), "file1dep1", + file2.toString(), "file2dep1", + file2.toString(), "file2dep2" + ), CachedSymbol.class); + + Pair> result = cache.retrieve(CACHE_KEY_VERSION_1, files, CachedSymbol.class); + + CachedSymbol[] cachedSymbols = result.getLeft(); + 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().getLeft()); + 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")); + + assertEquals(timeFile1.toMillis(), cache.getModificationTimestamp(CACHE_KEY_VERSION_1, file1.toString())); + assertEquals(0, cache.getModificationTimestamp(CACHE_KEY_VERSION_1, "random-non-existing-file")); + } + + @Test + 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<>(); + WorkspaceSymbol symbol = new WorkspaceSymbol("symbol1", SymbolKind.Field, Either.forLeft(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(CACHE_KEY_VERSION_1, files, generatedSymbols, null, CachedSymbol.class); + + Pair> result = cache.retrieve(new IndexCacheKey("someOtherProject", "someOtherIndexer", "someOtherCategory", "1"), files, CachedSymbol.class); + assertNull(result); + } + + @Test + 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<>(); + WorkspaceSymbol symbol = new WorkspaceSymbol("symbol1", SymbolKind.Field, Either.forLeft(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(CACHE_KEY_VERSION_1, files, generatedSymbols, ImmutableMultimap.of( + file1.toString(), "file1dep", + file2.toString(), "file2dep" + ), CachedSymbol.class); + + assertTrue(file1.toFile().setLastModified(timeFile1.toMillis() + 1000)); + + Pair> result = cache.retrieve(CACHE_KEY_VERSION_1, files, CachedSymbol.class); + assertNull(result); + } + + @Test + 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(CACHE_KEY_VERSION_1, files, new ArrayList<>(), null, CachedSymbol.class); + + String[] moreFiles = {file1.toString(), file2.toString(), file3.toString()}; + assertNull(cache.retrieve(CACHE_KEY_VERSION_1, moreFiles, CachedSymbol.class)); + } + + @Test + 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(CACHE_KEY_VERSION_1, files, new ArrayList<>(), null, CachedSymbol.class); + + String[] fewerFiles = {file1.toString(), file2.toString()}; + assertNull(cache.retrieve(CACHE_KEY_VERSION_1, fewerFiles, CachedSymbol.class)); + } + + @Test + void testDeleteOldCacheFileIfNewOneIsStored() throws Exception { + IndexCacheKey key1 = CACHE_KEY_VERSION_1; + cache.store(key1, new String[0], new ArrayList<>(), null, CachedSymbol.class); + assertTrue(Files.exists(tempDir.resolve(Paths.get(key1.toString() + STORAGE_FILE_EXTENSION)))); + + IndexCacheKey key2 = new IndexCacheKey("someProject", "someIndexer", "someCategory", "2"); + cache.store(key2, new String[0], new ArrayList<>(), null, CachedSymbol.class); + assertTrue(Files.exists(tempDir.resolve(Paths.get(key2.toString() + STORAGE_FILE_EXTENSION)))); + assertFalse(Files.exists(tempDir.resolve(Paths.get(key1.toString() + STORAGE_FILE_EXTENSION)))); + } + + @Test + void testDoNotRetrieveOldCacheDataIfNewerVersionIsStored() throws Exception { + IndexCacheKey key1 = CACHE_KEY_VERSION_1; + IndexCacheKey key2 = new IndexCacheKey("someProject", "someIndexer", "someCategory", "2"); + + cache.store(key1, new String[0], new ArrayList<>(), null, CachedSymbol.class); + assertNotNull(cache.retrieve(key1, new String[0], CachedSymbol.class)); + assertNull(cache.retrieve(key2, new String[0], CachedSymbol.class)); + + cache.store(key2, new String[0], new ArrayList<>(), null, CachedSymbol.class); + assertNull(cache.retrieve(key1, new String[0], CachedSymbol.class)); + assertNotNull(cache.retrieve(key2, new String[0], CachedSymbol.class)); + } + + @Test + void testDeleteOldCacheFileFromPreviousReleasesIfNewOneIsStored() throws Exception { + IndexCacheKey key1 = new IndexCacheKey("someProject", "someIndexer", "", "2"); + cache.store(key1, new String[0], new ArrayList<>(), null, CachedSymbol.class); + assertTrue(Files.exists(tempDir.resolve(Paths.get(key1.toString() + STORAGE_FILE_EXTENSION)))); + + IndexCacheKey key2 = new IndexCacheKey("someProject", "someIndexer", "someCategory", "2"); + cache.store(key2, new String[0], new ArrayList<>(), null, CachedSymbol.class); + assertTrue(Files.exists(tempDir.resolve(Paths.get(key2.toString() + STORAGE_FILE_EXTENSION)))); + assertFalse(Files.exists(tempDir.resolve(Paths.get(key1.toString() + STORAGE_FILE_EXTENSION)))); + } + + @Test + void testDoNotDeleteCacheFileFromOtherCategory() throws Exception { + IndexCacheKey key1 = new IndexCacheKey("someProject", "someIndexer", "someCategory", "2"); + cache.store(key1, new String[0], new ArrayList<>(), null, CachedSymbol.class); + assertTrue(Files.exists(tempDir.resolve(Paths.get(key1.toString() + STORAGE_FILE_EXTENSION)))); + + IndexCacheKey key2 = new IndexCacheKey("someProject", "someIndexer", "otherCategory", "2"); + cache.store(key2, new String[0], new ArrayList<>(), null, CachedSymbol.class); + assertTrue(Files.exists(tempDir.resolve(Paths.get(key2.toString() + STORAGE_FILE_EXTENSION)))); + assertTrue(Files.exists(tempDir.resolve(Paths.get(key1.toString() + STORAGE_FILE_EXTENSION)))); + } + + @Test + 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<>(); + + WorkspaceSymbol symbol = new WorkspaceSymbol("symbol1", SymbolKind.Field, Either.forLeft(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(CACHE_KEY_VERSION_1, files, generatedSymbols, null, CachedSymbol.class); + + CachedSymbol[] cachedSymbols = cache.retrieveSymbols(CACHE_KEY_VERSION_1, files, CachedSymbol.class); + 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().getLeft()); + + 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 + 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<>(); + WorkspaceSymbol symbol1 = new WorkspaceSymbol("symbol1", SymbolKind.Field, Either.forLeft(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(CACHE_KEY_VERSION_1, files, generatedSymbols1, null, CachedSymbol.class); + + List generatedSymbols2 = new ArrayList<>(); + symbol1 = new WorkspaceSymbol("symbol1", SymbolKind.Field, Either.forLeft(new Location(doc1URI, new Range(new Position(3, 10), new Position(3, 20))))); + enhancedSymbol1 = new EnhancedSymbolInformation(symbol1, null); + + WorkspaceSymbol symbol2 = new WorkspaceSymbol("symbol2", SymbolKind.Interface, Either.forLeft(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(CACHE_KEY_VERSION_1, file1.toAbsolutePath().toString(), timeFile1.toMillis() + 2000, generatedSymbols2, null, CachedSymbol.class); + + AbstractIndexCacheable[] cachedSymbols = cache.retrieveSymbols(CACHE_KEY_VERSION_1, files, CachedSymbol.class); + assertNotNull(cachedSymbols); + assertEquals(2, cachedSymbols.length); + + assertEquals(timeFile1.toMillis() + 2000, cache.getModificationTimestamp(CACHE_KEY_VERSION_1, file1.toString())); + } + + @Test + void testSymbolsAddedToMultipleFiles() throws Exception { + + // create 3 files with one symbol each + 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); + FileTime timeFile2 = Files.getLastModifiedTime(file2); + FileTime timeFile3 = Files.getLastModifiedTime(file3); + + String[] files = {file1.toAbsolutePath().toString(), file2.toAbsolutePath().toString(), file3.toAbsolutePath().toString()}; + + String doc1URI = UriUtil.toUri(file1.toFile()).toString(); + String doc2URI = UriUtil.toUri(file2.toFile()).toString(); + String doc3URI = UriUtil.toUri(file3.toFile()).toString(); + + List generatedSymbols = new ArrayList<>(); + WorkspaceSymbol symbol1 = new WorkspaceSymbol("symbol1", SymbolKind.Field, Either.forLeft(new Location(doc1URI, new Range(new Position(3, 10), new Position(3, 20))))); + EnhancedSymbolInformation enhancedSymbol1 = new EnhancedSymbolInformation(symbol1, null); + generatedSymbols.add(new CachedSymbol(doc1URI, timeFile1.toMillis(), enhancedSymbol1)); + + WorkspaceSymbol symbol2 = new WorkspaceSymbol("symbol2", SymbolKind.Field, Either.forLeft(new Location(doc2URI, new Range(new Position(3, 10), new Position(3, 20))))); + EnhancedSymbolInformation enhancedSymbol2 = new EnhancedSymbolInformation(symbol2, null); + generatedSymbols.add(new CachedSymbol(doc2URI, timeFile2.toMillis(), enhancedSymbol2)); + + WorkspaceSymbol symbol3 = new WorkspaceSymbol("symbol3", SymbolKind.Field, Either.forLeft(new Location(doc3URI, new Range(new Position(3, 10), new Position(3, 20))))); + EnhancedSymbolInformation enhancedSymbol3 = new EnhancedSymbolInformation(symbol3, null); + generatedSymbols.add(new CachedSymbol(doc3URI, timeFile3.toMillis(), enhancedSymbol3)); + + // store original version of the symbols to the cache + cache.store(CACHE_KEY_VERSION_1, files, generatedSymbols, null, CachedSymbol.class); + + + // create updated and new symbols + List updatedSymbols = new ArrayList<>(); + + WorkspaceSymbol updatedSymbol1 = new WorkspaceSymbol("symbol1", SymbolKind.Field, Either.forLeft(new Location(doc1URI, new Range(new Position(3, 10), new Position(3, 20))))); + EnhancedSymbolInformation updatedEnhancedSymbol1 = new EnhancedSymbolInformation(updatedSymbol1, null); + + WorkspaceSymbol newSymbol1 = new WorkspaceSymbol("symbol1-new", SymbolKind.Interface, Either.forLeft(new Location(doc1URI, new Range(new Position(5, 5), new Position(5, 10))))); + EnhancedSymbolInformation newEnhancedSymbol1 = new EnhancedSymbolInformation(newSymbol1, null); + + updatedSymbols.add(new CachedSymbol(doc1URI, timeFile1.toMillis() + 2000, updatedEnhancedSymbol1)); + updatedSymbols.add(new CachedSymbol(doc1URI, timeFile1.toMillis() + 2000, newEnhancedSymbol1)); + assertTrue(file1.toFile().setLastModified(timeFile1.toMillis() + 2000)); + + WorkspaceSymbol updatedSymbol2 = new WorkspaceSymbol("symbol2-updated", SymbolKind.Field, Either.forLeft(new Location(doc2URI, new Range(new Position(3, 10), new Position(3, 20))))); + EnhancedSymbolInformation updatedEnhancedSymbol2 = new EnhancedSymbolInformation(updatedSymbol2, null); + updatedSymbols.add(new CachedSymbol(doc2URI, timeFile2.toMillis() + 3000, updatedEnhancedSymbol2)); + assertTrue(file2.toFile().setLastModified(timeFile2.toMillis() + 3000)); + + String[] updatedFiles = new String[]{file1.toAbsolutePath().toString(), file2.toAbsolutePath().toString()}; + long[] updatedModificationTimestamps = new long[]{timeFile1.toMillis() + 2000, timeFile2.toMillis() + 3000}; + + // update multiple files in the cache + cache.update(CACHE_KEY_VERSION_1, updatedFiles, updatedModificationTimestamps, updatedSymbols, null, CachedSymbol.class); + + // double check whether all changes got stored and retrieved correctly + CachedSymbol[] cachedSymbols = cache.retrieveSymbols(CACHE_KEY_VERSION_1, files, CachedSymbol.class); + assertNotNull(cachedSymbols); + assertEquals(4, cachedSymbols.length); + + assertSymbol(updatedEnhancedSymbol1, cachedSymbols); + assertSymbol(newEnhancedSymbol1, cachedSymbols); + assertSymbol(updatedEnhancedSymbol2, cachedSymbols); + assertSymbol(enhancedSymbol3, cachedSymbols); + + assertEquals(timeFile1.toMillis() + 2000, cache.getModificationTimestamp(CACHE_KEY_VERSION_1, file1.toString())); + assertEquals(timeFile2.toMillis() + 3000, cache.getModificationTimestamp(CACHE_KEY_VERSION_1, file2.toString())); + assertEquals(timeFile3.toMillis(), cache.getModificationTimestamp(CACHE_KEY_VERSION_1, file3.toString())); + } + + private void assertSymbol(EnhancedSymbolInformation enhancedSymbol, CachedSymbol[] cachedSymbols) { + for (CachedSymbol cachedSymbol : cachedSymbols) { + WorkspaceSymbol symbol = cachedSymbol.getEnhancedSymbol().getSymbol(); + + if (symbol.toString().equals(enhancedSymbol.getSymbol().toString())) { + return; + } + } + + + fail("symbol not found: " + enhancedSymbol.getSymbol().toString()); + } + + @Test + 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(CACHE_KEY_VERSION_1, files, generatedSymbols, dependencies, CachedSymbol.class); + + assertTrue(file1.toFile().setLastModified(timeFile1.toMillis() + 2000)); + Set dependencies2 = ImmutableSet.of("dep1", "dep2"); + cache.update(CACHE_KEY_VERSION_1, file1.toAbsolutePath().toString(), timeFile1.toMillis() + 2000, generatedSymbols, dependencies2, CachedSymbol.class); + + Pair> result = cache.retrieve(CACHE_KEY_VERSION_1, files, CachedSymbol.class); + assertNotNull(result); + assertEquals(ImmutableSet.of("dep1", "dep2"), result.getRight().get(file1.toString())); + } + + @Test + 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<>(); + WorkspaceSymbol symbol1 = new WorkspaceSymbol("symbol1", SymbolKind.Field, Either.forLeft(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(CACHE_KEY_VERSION_1, files, generatedSymbols1, null, CachedSymbol.class); + + List generatedSymbols2 = new ArrayList<>(); + assertTrue(file1.toFile().setLastModified(timeFile1.toMillis() + 2000)); + + cache.update(CACHE_KEY_VERSION_1, file1.toAbsolutePath().toString(), timeFile1.toMillis() + 2000, generatedSymbols2, null, CachedSymbol.class); + + AbstractIndexCacheable[] cachedSymbols = cache.retrieveSymbols(CACHE_KEY_VERSION_1, files, CachedSymbol.class); + assertNotNull(cachedSymbols); + assertEquals(0, cachedSymbols.length); + } + + @Test + 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(CACHE_KEY_VERSION_1, files, generatedSymbols1, dependencies1, CachedSymbol.class); + + List generatedSymbols2 = new ArrayList<>(); + assertTrue(file1.toFile().setLastModified(timeFile1.toMillis() + 2000)); + + Set dependencies2 = ImmutableSet.of("dep2"); + cache.update(CACHE_KEY_VERSION_1, file1.toAbsolutePath().toString(), timeFile1.toMillis() + 2000, generatedSymbols2, dependencies2, CachedSymbol.class); + + Pair> result = cache.retrieve(CACHE_KEY_VERSION_1, files, CachedSymbol.class); + assertNotNull(result); + assertEquals(ImmutableSet.of("dep2"), result.getRight().get(file1.toString())); + } + + @Test + 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<>(); + WorkspaceSymbol symbol1 = new WorkspaceSymbol("symbol1", SymbolKind.Field, Either.forLeft(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(CACHE_KEY_VERSION_1, files, generatedSymbols1, null, CachedSymbol.class); + + List generatedSymbols2 = new ArrayList<>(); + WorkspaceSymbol symbol2 = new WorkspaceSymbol("symbol2", SymbolKind.Interface, Either.forLeft(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(CACHE_KEY_VERSION_1, file2.toString(), timeFile2.toMillis(), generatedSymbols2, null, CachedSymbol.class); + + AbstractIndexCacheable[] cachedSymbols = cache.retrieveSymbols(CACHE_KEY_VERSION_1, new String[]{file1.toString(), file2.toString()}, CachedSymbol.class); + assertNotNull(cachedSymbols); + assertEquals(2, cachedSymbols.length); + } + + @Test + 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 timeFile2 = Files.getLastModifiedTime(file2); + String[] files = {file1.toString()}; + + List generatedSymbols1 = ImmutableList.of(); + Multimap dependencies1 = ImmutableMultimap.of( + file1.toString(), "dep1" + ); + cache.store(CACHE_KEY_VERSION_1, files, generatedSymbols1, dependencies1, CachedSymbol.class); + + Set dependencies2 = ImmutableSet.of("dep2"); + cache.update(CACHE_KEY_VERSION_1, file2.toString(), timeFile2.toMillis(), generatedSymbols1, dependencies2, CachedSymbol.class); + + Pair> result = cache.retrieve(CACHE_KEY_VERSION_1, new String[]{file1.toString(), file2.toString()}, CachedSymbol.class); + assertNotNull(result); + assertEquals(ImmutableSet.of("dep2"), result.getRight().get(file2.toString())); + assertEquals(ImmutableSet.of("dep1"), result.getRight().get(file1.toString())); + } + + @Test + void testProjectDeleted() throws Exception { + IndexCacheKey key1 = CACHE_KEY_VERSION_1; + cache.store(key1, new String[0], new ArrayList<>(), null, CachedSymbol.class); + assertTrue(Files.exists(tempDir.resolve(Paths.get(key1.toString() + STORAGE_FILE_EXTENSION)))); + + cache.remove(key1); + assertFalse(Files.exists(tempDir.resolve(Paths.get(key1.toString() + STORAGE_FILE_EXTENSION)))); + } + + @Test + 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()).toASCIIString(); + String doc2URI = UriUtil.toUri(file2.toFile()).toASCIIString(); + + List generatedSymbols = new ArrayList<>(); + + WorkspaceSymbol symbol1 = new WorkspaceSymbol("symbol1", SymbolKind.Field, Either.forLeft(new Location(doc1URI, new Range(new Position(3, 10), new Position(3, 20))))); + EnhancedSymbolInformation enhancedSymbol1 = new EnhancedSymbolInformation(symbol1, null); + + WorkspaceSymbol symbol2 = new WorkspaceSymbol("symbol2", SymbolKind.Field, Either.forLeft(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)); + + Multimap dependencies = ImmutableMultimap.of( + file1.toString(), "dep1", + file2.toString(), "dep2" + ); + cache.store(CACHE_KEY_VERSION_1, files, generatedSymbols, dependencies, CachedSymbol.class); + cache.removeFile(CACHE_KEY_VERSION_1, file1.toAbsolutePath().toString(), CachedSymbol.class); + + files = new String[]{file2.toAbsolutePath().toString()}; + Pair> result = cache.retrieve(CACHE_KEY_VERSION_1, files, CachedSymbol.class); + 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().getLeft()); + assertNull(cachedSymbols[0].getEnhancedSymbol().getAdditionalInformation()); + + Multimap cachedDependencies = result.getRight(); + assertEquals(ImmutableSet.of(), cachedDependencies.get(file1.toString())); + assertEquals(ImmutableSet.of("dep2"), cachedDependencies.get(file2.toString())); + } + + @Test + void testMultipleFilesDeleted() 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"); + Path file4 = Paths.get(tempDir.toAbsolutePath().toString(), "tempFile4"); + + Files.createFile(file1); + Files.createFile(file2); + Files.createFile(file3); + Files.createFile(file4); + + FileTime timeFile1 = Files.getLastModifiedTime(file1); + FileTime timeFile2 = Files.getLastModifiedTime(file2); + FileTime timeFile3 = Files.getLastModifiedTime(file3); + FileTime timeFile4 = Files.getLastModifiedTime(file4); + String[] files = { + file1.toAbsolutePath().toString(), + file2.toAbsolutePath().toString(), + file3.toAbsolutePath().toString(), + file4.toAbsolutePath().toString() + }; + + String doc1URI = UriUtil.toUri(file1.toFile()).toASCIIString(); + String doc2URI = UriUtil.toUri(file2.toFile()).toASCIIString(); + String doc3URI = UriUtil.toUri(file3.toFile()).toASCIIString(); + String doc4URI = UriUtil.toUri(file4.toFile()).toASCIIString(); + + List generatedSymbols = new ArrayList<>(); + + WorkspaceSymbol symbol1 = new WorkspaceSymbol("symbol1", SymbolKind.Field, Either.forLeft(new Location(doc1URI, new Range(new Position(3, 10), new Position(3, 20))))); + EnhancedSymbolInformation enhancedSymbol1 = new EnhancedSymbolInformation(symbol1, null); + + WorkspaceSymbol symbol2 = new WorkspaceSymbol("symbol2", SymbolKind.Field, Either.forLeft(new Location(doc2URI, new Range(new Position(5, 10), new Position(5, 20))))); + EnhancedSymbolInformation enhancedSymbol2 = new EnhancedSymbolInformation(symbol2, null); + + WorkspaceSymbol symbol3 = new WorkspaceSymbol("symbol3", SymbolKind.Field, Either.forLeft(new Location(doc3URI, new Range(new Position(20, 11), new Position(20, 30))))); + EnhancedSymbolInformation enhancedSymbol3 = new EnhancedSymbolInformation(symbol3, null); + + WorkspaceSymbol symbol4 = new WorkspaceSymbol("symbol4", SymbolKind.Field, Either.forLeft(new Location(doc4URI, new Range(new Position(4, 4), new Position(5, 5))))); + EnhancedSymbolInformation enhancedSymbol4 = new EnhancedSymbolInformation(symbol4, null); + + generatedSymbols.add(new CachedSymbol(doc1URI, timeFile1.toMillis(), enhancedSymbol1)); + generatedSymbols.add(new CachedSymbol(doc2URI, timeFile2.toMillis(), enhancedSymbol2)); + generatedSymbols.add(new CachedSymbol(doc3URI, timeFile3.toMillis(), enhancedSymbol3)); + generatedSymbols.add(new CachedSymbol(doc4URI, timeFile4.toMillis(), enhancedSymbol4)); + + Multimap dependencies = ImmutableMultimap.of( + file1.toString(), "dep1", + file2.toString(), "dep2" + ); + cache.store(CACHE_KEY_VERSION_1, files, generatedSymbols, dependencies, CachedSymbol.class); +// cache.removeFile(CACHE_KEY_VERSION_1, file1.toAbsolutePath().toString(), CachedSymbol.class); +// cache.removeFile(CACHE_KEY_VERSION_1, file3.toAbsolutePath().toString(), CachedSymbol.class); + cache.removeFiles(CACHE_KEY_VERSION_1, new String[] {file1.toAbsolutePath().toString(), file3.toAbsolutePath().toString()}, CachedSymbol.class); + + files = new String[]{file2.toAbsolutePath().toString(), file4.toAbsolutePath().toString()}; + Pair> result = cache.retrieve(CACHE_KEY_VERSION_1, files, CachedSymbol.class); + CachedSymbol[] cachedSymbols = result.getLeft(); + assertNotNull(result); + assertEquals(2, 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().getLeft()); + assertNull(cachedSymbols[0].getEnhancedSymbol().getAdditionalInformation()); + + assertEquals("symbol4", cachedSymbols[1].getEnhancedSymbol().getSymbol().getName()); + assertEquals(SymbolKind.Field, cachedSymbols[1].getEnhancedSymbol().getSymbol().getKind()); + assertEquals(new Location(doc4URI, new Range(new Position(4, 4), new Position(5, 5))), cachedSymbols[1].getEnhancedSymbol().getSymbol().getLocation().getLeft()); + assertNull(cachedSymbols[1].getEnhancedSymbol().getAdditionalInformation()); + + Multimap cachedDependencies = result.getRight(); + assertEquals(ImmutableSet.of(), cachedDependencies.get(file1.toString())); + assertEquals(ImmutableSet.of("dep2"), cachedDependencies.get(file2.toString())); + } + +}