/******************************************************************************* * Copyright (c) 2014 Bruno Medeiros and other Contributors. * 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: * Bruno Medeiros - initial API and implementation *******************************************************************************/ package dtool.engine; import static melnorme.utilbox.core.Assert.AssertNamespace.assertFail; import static melnorme.utilbox.core.Assert.AssertNamespace.assertNotNull; import static melnorme.utilbox.core.Assert.AssertNamespace.assertTrue; import static melnorme.utilbox.core.CoreUtil.areEqual; import static melnorme.utilbox.misc.StringUtil.substringUntilMatch; import java.io.IOException; import java.nio.file.Path; import java.util.HashMap; import dtool.ast.definitions.Module; import dtool.parser.DeeParser; import dtool.parser.DeeParserResult.ParsedModule; import melnorme.lang.tooling.context.ModuleSourceException; import melnorme.lang.utils.FileModificationDetectionHelper; import melnorme.lang.utils.ISimpleStatusLogger; import melnorme.utilbox.concurrency.ICancelMonitor; import melnorme.utilbox.concurrency.OperationCancellation; import melnorme.utilbox.misc.FileUtil; import melnorme.utilbox.misc.Location; import melnorme.utilbox.misc.StringUtil; /** * Manages a cache of parsed modules, indexed by file path */ public class ModuleParseCache { protected final ISimpleStatusLogger statusLogger; public ModuleParseCache(ISimpleStatusLogger statusLogger) { this.statusLogger = statusLogger; } protected final HashMap<String, CachedModuleEntry> cache = new HashMap<>(); /* ----------------- ----------------- */ public ParsedModule getParsedModule(Path filePath) throws ModuleSourceException { CachedModuleEntry entry = getEntry(filePath); try { return assertNotNull(entry.getParsedModule()); } catch (IOException e) { throw new ModuleSourceException(e); } } public ParsedModule getExistingParsedModule(Path filePath) { // TODO: don't create entry return getEntry(filePath).getExistingParsedModule(); } // util method public Module getExistingParsedModuleNode(Path filePath) { ParsedModule parsedModule = getExistingParsedModule(filePath); return parsedModule == null ? null : parsedModule.module; } /* ----------------- ----------------- */ protected String keyFromPath(Path filePath) { filePath = validatePath(filePath); return getKeyFromPath(filePath); } protected Path validatePath(Path filePath) { assertNotNull(filePath); //filePath can be relative //assertTrue(filePath.isAbsolute()); assertTrue(filePath.getNameCount() > 0); filePath = filePath.normalize(); return filePath; } protected String getKeyFromPath(Path filePath) { return filePath.toString(); } public CachedModuleEntry getEntry(Path filePath) { String key = keyFromPath(filePath); synchronized(this) { CachedModuleEntry entry = cache.get(key); if(entry == null) { entry = doCreateEntry(filePath); cache.put(key, entry); } return entry; } } protected CachedModuleEntry doCreateEntry(Path filePath) { return new CachedModuleEntry_Logged(filePath); } public ParsedModule setSourceAndParseModule(Path filePath, String source) { assertNotNull(source); return getEntry(filePath).setWorkingSourceAndParse(source); } public void discardWorkingCopy(Path filePath) { String key = keyFromPath(filePath); CachedModuleEntry entry = cache.get(key); if(entry != null) { entry.discardWorkingCopy(); } } /** * XXX: This method is currently only used by tests. * It would need review to make sure it actually works fully outside of that tests scenario, * especially with regards to concurrency. */ public void discardEntry(Path filePath) { String key = keyFromPath(filePath); synchronized(this) { cache.remove(key); } } public static class CachedModuleEntry { protected final Path filePath; protected final FileModificationDetectionHelper fileModDetectHelper; private String source = null; private boolean isWorkingCopy = false; private ParsedModule parsedModule = null; public CachedModuleEntry(Path filePath) { this.filePath = assertNotNull(filePath); Location fileLocation = Location.createValidOrNull(filePath); fileModDetectHelper = (fileLocation == null) ? null : new FileModificationDetectionHelper(fileLocation); } public Path getFilePath() { return filePath; } public synchronized ParsedModule getExistingParsedModule() { return parsedModule; } public synchronized boolean isWorkingCopy() { return isWorkingCopy; } public synchronized boolean isStale() { if(isWorkingCopy) { assertNotNull(source); return false; } assertNotNull(fileModDetectHelper); if(source == null || parsedModule == null) { return true; } return fileModDetectHelper.isModifiedSinceLastRead(); } public synchronized ParsedModule getParsedModule() throws IOException { try { return getParsedModule(null); } catch(OperationCancellation e) { throw assertFail(); // Not possible } } public ParsedModule getParsedModule(ICancelMonitor cancelMonitor) throws IOException, OperationCancellation { if(isStale()) { readSource(); } return doGetParsedModule(source, cancelMonitor); } protected void readSource() throws IOException { fileModDetectHelper.markRead(); // Update timestampe before reading contents. String fileContents = FileUtil.readStringFromFile(filePath, StringUtil.UTF8); // TODO: detect encoding setNewSource(fileContents); } protected void setNewSource(String newSource) { // We only set a new parsedModule if the new source is actually different from previous source. // Otherwise, as an optimization, we simply reuse the previous parsedModule if(!areEqual(source, newSource)) { source = newSource; parsedModule = null; } } /** * @return the parsed module from this cache, but only if is up-to-date with the underlying source. * If it is not, return null; * As such, this method will never cause a module to be parsed, any non-null result is a module * that had been parsed already. */ public synchronized ParsedModule getParsedModuleIfNotStale() { return getParsedModuleIfNotStale(true); } protected synchronized ParsedModule getParsedModuleIfNotStale(boolean attemptSourceRefresh) { if(!isStale()) { return parsedModule; } if(attemptSourceRefresh) { // Attemp an optimization, read the new source, and if it is the same as the previous one, // then keep the same parsed module. try { readSource(); return parsedModule; // parsedModule will remain the same if the source didn't change. } catch (IOException e) { return null; } } else { return null; } } public synchronized ParsedModule setWorkingSourceAndParse(String newSource) { assertNotNull(newSource); setWorkingSource(newSource); return doGetParsedModule(newSource); } public void setWorkingSource(String newSource) { setNewSource(newSource); isWorkingCopy = true; } protected ParsedModule doGetParsedModule(String source) { try { return doGetParsedModule(source, null); } catch(OperationCancellation e) { throw assertFail(); } } protected ParsedModule doGetParsedModule(String source, ICancelMonitor cancelMonitor) throws OperationCancellation { if(parsedModule == null) { parsedModule = DeeParser.parseSourceModule(source, filePath, cancelMonitor); parsedSource_after(); } return parsedModule; } protected void parsedSource_after() { } public synchronized void discardWorkingCopy() { if(isWorkingCopy) { isWorkingCopy = false; fileModDetectHelper.markStale(); // Mark file as modified discardWorkingCopy_after(); } } protected void discardWorkingCopy_after() { } public synchronized void runUnderEntryLock(Runnable runnable) { runnable.run(); } } public ParsedModule parseModuleWithNoLocation(String source, ICancelMonitor cm) throws OperationCancellation { statusLogger.logMessage("ParseCache: Parsed module with no location: " + substringUntilMatch(source, "\n")); return DeeParser.parseUnlocatedModule(source, "_unnamed", cm); } public class CachedModuleEntry_Logged extends CachedModuleEntry { public CachedModuleEntry_Logged(Path filePath) { super(filePath); } @Override protected void parsedSource_after() { String isWorkingCopySuffix = isWorkingCopy() ? " [WorkingCopy]" : ""; statusLogger.logMessage("ParseCache: Parsed module " + filePath + isWorkingCopySuffix); } @Override protected void discardWorkingCopy_after() { statusLogger.logMessage("ParseCache: Discarded working copy: " + filePath); } } }