/*
* Copyright 2003-2016 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.textgen.trace;
import jetbrains.mps.cleanup.CleanupManager;
import jetbrains.mps.generator.GenerationStatus;
import jetbrains.mps.generator.cache.BaseModelCache;
import jetbrains.mps.generator.cache.CacheGenerator;
import jetbrains.mps.generator.cache.ParseFacility;
import jetbrains.mps.generator.cache.ParseFacility.Parser;
import jetbrains.mps.generator.generationTypes.StreamHandler;
import jetbrains.mps.generator.impl.dependencies.GenerationRootDependencies;
import jetbrains.mps.project.SModuleOperations;
import jetbrains.mps.util.JDOMUtil;
import jetbrains.mps.vfs.FileSystem;
import jetbrains.mps.vfs.IFile;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.module.SModule;
import org.jetbrains.mps.openapi.module.SRepository;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* The reason [generator] needs [debuginfo-api], which is otherwise textgen-specific (moreover, BL-textgen)
*/
public class TraceInfoCache extends BaseModelCache<DebugInfo> {
public static final String TRACE_FILE_NAME = "trace.info";
private static TraceInfoCache INSTANCE;
private List<TraceInfoCache.TraceInfoResourceProvider> myProviders = new CopyOnWriteArrayList<TraceInfoCache.TraceInfoResourceProvider>();
private final JavaTraceInfoResourceProvider myJavaTraceInfoProvider = new JavaTraceInfoResourceProvider();
public TraceInfoCache(SRepository repository, CleanupManager manager) {
super(repository, manager);
}
@Override
public void init() {
if (INSTANCE != null) {
throw new IllegalStateException("double initialization");
}
INSTANCE = this;
super.init();
// todo: move (but remember that java provider is used in idea plugin as well)
myProviders.add(myJavaTraceInfoProvider);
}
@Override
public void dispose() {
myProviders.remove(myJavaTraceInfoProvider);
super.dispose();
INSTANCE = null;
}
@NotNull
@Override
public String getCacheFileName() {
return TRACE_FILE_NAME;
}
@Override
protected DebugInfo readCache(final SModel sm) {
return loadCacheFromUrl(getCacheUrl(sm));
}
private DebugInfo loadCacheFromUrl(URL url) {
return new ParseFacility<DebugInfo>(getClass(), new CacheParser()).input(url).parseSilently();
}
@Nullable
private URL getCacheUrl(@NotNull SModel sm) {
final SModule module = sm.getModule();
String resourceName = traceInfoResourceName(sm);
for (TraceInfoCache.TraceInfoResourceProvider provider : myProviders) {
URL url = provider.getResource(module, resourceName);
if (url != null) {
return url;
}
}
return null;
}
@Override
@Nullable
public IFile getCacheFile(@NotNull SModel modelDescriptor) {
URL cacheUrl = getCacheUrl(modelDescriptor);
return cacheUrl == null ? null : TraceInfoCache.getFileByURL(cacheUrl);
}
private String traceInfoResourceName(SModel sm) {
String longName = sm.getModelName();
int atIndex = longName.indexOf('@');
if (atIndex > 0) {
longName = longName.substring(0, atIndex);
}
return longName.replace(".", "/") + "/" + TRACE_FILE_NAME;
}
// XXX revisit. IFAIU, this method to get locally-generated trace.info, not the one bundled. Although the approach is questionable
@Nullable
public DebugInfo getLastGeneratedDebugInfo(@NotNull SModel model) {
String generatorOutputPath = SModuleOperations.getOutputPathFor(model);
if ((generatorOutputPath == null || generatorOutputPath.length() == 0)) {
return null;
}
IFile traceInfoFile = FileSystem.getInstance().getFileByPath(generatorOutputPath).getDescendant(traceInfoResourceName(model));
if (!(traceInfoFile.exists())) {
return null;
}
try {
URL url = new File(traceInfoFile.getPath().replace("/", File.separator)).toURI().toURL();
return loadCacheFromUrl(url);
} catch (MalformedURLException e) {
return null;
}
}
public CacheGenerator newCacheGenerator(@Nullable DebugInfo newInfo) {
return new CacheGen(newInfo);
}
public void addResourceProvider(TraceInfoCache.TraceInfoResourceProvider provider) {
myProviders.add(provider);
}
public void removeResourceProvider(TraceInfoCache.TraceInfoResourceProvider provider) {
myProviders.remove(provider);
}
public static TraceInfoCache getInstance() {
return INSTANCE;
}
@Nullable
private static IFile getFileByURL(@NotNull URL url) {
String file = url.getFile();
if (file.isEmpty()) {
return null;
}
// if this is a jar, it starts with file:, so we remove the prefix
String prefix = "file:";
if (file.startsWith(prefix)) {
file = file.substring(prefix.length());
}
return FileSystem.getInstance().getFileByPath(file);
}
public interface TraceInfoResourceProvider {
/**
* Provider returns url to the requested trace.info resource file with respect to the particular module
* @param module which is supposed to own the requested trace.info resource file
* @param resourceName full path to the trace.info resource file
* @return null if the trace.info could not be found
*/
@Nullable URL getResource(@NotNull SModule module, String resourceName);
}
private class CacheGen implements CacheGenerator {
private final DebugInfo myInfoNew;
public CacheGen(DebugInfo newInfo) {
myInfoNew = newInfo;
}
@Override
public void generateCache(GenerationStatus status, StreamHandler handler) {
DebugInfo cache = updateUnchanged(status);
if (cache == null) {
return;
}
update(status.getOriginalInputModel(), cache);
handler.saveStream(getCacheFileName(), SerializeSupport.serialize(cache));
}
private DebugInfo updateUnchanged(GenerationStatus genStatus) {
if (myInfoNew == null) {
return null;
}
// complete debug info with info for roots that did not changed and therefore were not generated
// we get debug info for them from cache
DebugInfo cachedDebugInfo = TraceInfoCache.this.getLastGeneratedDebugInfo(genStatus.getOriginalInputModel());
if (cachedDebugInfo != null) {
List<String> unchangedFiles = new ArrayList<String>();
for (GenerationRootDependencies dependency : genStatus.getDependencies().getUnchangedDependencies()) {
unchangedFiles.addAll(dependency.getFiles());
}
completeDebugInfoFromCache(cachedDebugInfo, myInfoNew, unchangedFiles);
}
return myInfoNew;
}
}
static void completeDebugInfoFromCache(@NotNull DebugInfo cachedDebugInfo, @NotNull DebugInfo generatedDebugInfo, Collection<String> unchangedFiles) {
Set<String> files = new HashSet<String>(unchangedFiles);
for (DebugInfoRoot cachedRoot : cachedDebugInfo.getRoots()) {
DebugInfoRoot generatedRoot = generatedDebugInfo.getRootInfo(cachedRoot.getNodeRef());
boolean newFromCache = false;
if (generatedRoot == null) {
generatedRoot = new DebugInfoRoot(cachedRoot.getNodeRef());
newFromCache = true;
}
for (TraceablePositionInfo position : cachedRoot.getPositions()) {
if (files.contains(position.getFileName())) {
generatedRoot.addPosition(position);
}
}
for (ScopePositionInfo position : cachedRoot.getScopePositions()) {
if (files.contains(position.getFileName())) {
generatedRoot.addScopePosition(position);
}
}
for (UnitPositionInfo position : cachedRoot.getUnitPositions()) {
if (files.contains(position.getFileName())) {
generatedRoot.addUnitPosition(position);
}
}
if (newFromCache) {
// if a node is removed, generatedDebugInfo won't have an entry for it, while cachedDebugInfo has.
// no position from this cached info, however, would pass unchangedFiles filter, and generatedDebugInfo
// would stay empty. Here, we detect this case and drop stale debug info entries
final boolean noCachedData = generatedRoot.getPositions().isEmpty() && generatedRoot.getScopePositions().isEmpty() && generatedRoot.getUnitPositions().isEmpty();
if (!noCachedData) {
generatedDebugInfo.putRootInfo(generatedRoot);
}
}
}
}
private static class CacheParser implements Parser<DebugInfo> {
@Override
public DebugInfo load(InputStream is) throws IOException {
try {
Document doc = JDOMUtil.loadDocument(is);
final Element rootElement = doc.getRootElement();
return SerializeSupport.restore(rootElement);
} catch (JDOMException ex) {
throw new IOException(ex);
}
}
}
}