/*
* Copyright 2003-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package jetbrains.mps.generator.cache;
import jetbrains.mps.cleanup.CleanupListener;
import jetbrains.mps.cleanup.CleanupManager;
import jetbrains.mps.components.CoreComponent;
import jetbrains.mps.smodel.RepoListenerRegistrar;
import jetbrains.mps.smodel.SModelOperations;
import jetbrains.mps.util.Pair;
import jetbrains.mps.vfs.IFile;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.model.SModelReference;
import org.jetbrains.mps.openapi.module.SModule;
import org.jetbrains.mps.openapi.module.SRepository;
import org.jetbrains.mps.openapi.module.SRepositoryContentAdapter;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* Per-repository, model-associated caches.
*/
public abstract class BaseModelCache<T> implements CoreComponent, CleanupListener {
// absence of model in the cache means we have no idea about present cache state.
// if model is in the cache, we do know both IFile and cached object
private final ConcurrentMap<SModelReference, Pair<IFile, T>> myCache = new ConcurrentHashMap<SModelReference, Pair<IFile, T>>();
protected final SRepository myRepository;
private final CleanupManager myCleanupManager;
private final SRepositoryContentAdapter myRepoListener = new MyRepositoryListener();
@Nullable
protected abstract T readCache(SModel model);
@NotNull
public abstract String getCacheFileName();
@Nullable
protected IFile getCacheFile(SModel modelDescriptor) {
IFile cachesDir = getCachesDirInternal(SModelOperations.getOutputCacheLocation(modelDescriptor));
if (cachesDir == null) {
return null;
}
return cachesDir.getDescendant(getCacheFileName());
}
// In fact, can be application-wide if we use compound key (repo+modelref)
protected BaseModelCache(SRepository repository, CleanupManager cleanupManager) {
myRepository = repository;
myCleanupManager = cleanupManager;
}
@Override
public void init() {
new RepoListenerRegistrar(myRepository, myRepoListener).attach();
myCleanupManager.addCleanupListener(this);
}
@Override
public void dispose() {
myCleanupManager.removeCleanupListener(this);
new RepoListenerRegistrar(myRepository, myRepoListener).detach();
}
@Nullable
public T get(@NotNull SModel model) {
final SModelReference mr = model.getReference();
Pair<IFile, T> rv = myCache.get(mr);
if (rv != null) {
return rv.o2;
}
IFile cacheFile = getCacheFile(model);
if (cacheFile == null) {
return null;
}
return readAndUpdateCache(cacheFile, model);
}
private T readAndUpdateCache(IFile cacheFile, SModel model) {
final SModelReference mr = model.getReference();
T cache = readCache(model);
if (cache == null) {
return null;
}
final Pair<IFile, T> entry = new Pair<IFile, T>(cacheFile, cache);
Pair<IFile, T> existing = myCache.putIfAbsent(mr, entry);
if (existing != null) {
return existing.o2;
}
return cache;
}
public void invalidateCacheForFile(IFile cacheFile) {
SModelReference mr = findCachedModelForFile(cacheFile);
if (mr == null) {
return;
}
myCache.remove(mr);
}
@Nullable
protected SModelReference findCachedModelForFile(IFile cacheFile) {
for (Entry<SModelReference, Pair<IFile, T>> entry : myCache.entrySet()) {
if (cacheFile.equals(entry.getValue().o1)) {
return entry.getKey();
}
}
return null;
}
@Nullable
protected IFile getCachesDirInternal(@Nullable IFile defaultCachesDir) {
// XXX there's little reason to keep this method, GenerationDependenciesCache shall override getCacheFile() directly.
return defaultCachesDir;
}
/**
* Invoke to set new cached value
*/
protected final void update(SModel model, T cache) {
final SModelReference mr = model.getReference();
Pair<IFile, T> entry = myCache.remove(mr);
if (entry != null) {
// decided not to update with incomplete entry, although perhaps it won't hurt (file == null))
myCache.put(mr, new Pair<IFile, T>(entry.o1, cache));
}
}
/**
* Forget cached state, if any; unlike {@link #discard(org.jetbrains.mps.openapi.model.SModel)} does not touch persisted/serialized state.
*/
public final void clean(@NotNull SModel model) {
myCache.remove(model.getReference());
}
protected final void clean(SModelReference modelRef) {
myCache.remove(modelRef);
}
/**
* Forget cached state and scrap any persisted/serialized state. Does its best to ensure serialized state got discarded, but doesn't guarantee that.
*/
public void discard(@NotNull SModel model) {
final Pair<IFile, T> removed = myCache.remove(model.getReference());
IFile cachedFile = removed == null ? null : removed.o1;
IFile actualCacheFile = getCacheFile(model);
if (actualCacheFile != null) {
actualCacheFile.delete();
}
if (cachedFile != null && cachedFile != actualCacheFile) {
cachedFile.delete();
}
}
@Override
public void performCleanup() {
myCache.clear();
}
private class MyRepositoryListener extends SRepositoryContentAdapter {
@Override
public void beforeModelRemoved(SModule module, SModel model) {
clean(model);
}
@Override
public void modelAdded(SModule module, SModel model) {
clean(model);
}
@Override
public void modelRenamed(SModule module, SModel model, SModelReference oldRef) {
clean(model);
}
@Override
public void modelReplaced(SModel model) {
clean(model);
}
}
}