/* * 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; import jetbrains.mps.extapi.model.EditableSModelBase; import jetbrains.mps.extapi.model.ModelWithAttributes; import jetbrains.mps.extapi.model.SModelBase; import jetbrains.mps.extapi.module.TransientSModule; import jetbrains.mps.generator.TransientModelsProvider.TransientSwapSpace; import jetbrains.mps.generator.impl.ModelVault; import jetbrains.mps.module.SDependencyImpl; import jetbrains.mps.project.AbstractModule; import jetbrains.mps.smodel.FastNodeFinderManager; import jetbrains.mps.smodel.ModelDependencyUpdate; import jetbrains.mps.smodel.ModelImports; import jetbrains.mps.smodel.SModelHeader; import jetbrains.mps.smodel.loading.ModelLoadingState; import jetbrains.mps.smodel.references.ImmatureReferencesTracker; import jetbrains.mps.util.containers.ConcurrentHashSet; import org.apache.log4j.LogManager; import org.apache.log4j.Logger; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.mps.openapi.model.SModel; import org.jetbrains.mps.openapi.model.SModelId; import org.jetbrains.mps.openapi.model.SModelReference; import org.jetbrains.mps.openapi.module.SDependency; import org.jetbrains.mps.openapi.module.SDependencyScope; import org.jetbrains.mps.openapi.module.SModule; import org.jetbrains.mps.openapi.module.SModuleReference; import org.jetbrains.mps.openapi.module.SRepository; import org.jetbrains.mps.openapi.persistence.ModelSaveException; import org.jetbrains.mps.openapi.persistence.NullDataSource; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.function.BiConsumer; public class TransientModelsModule extends AbstractModule implements TransientSModule { private static final Logger LOG = LogManager.getLogger(TransientModelsModule.class); private final TransientModelsProvider myComponent; private Set<SModel> myPublished = new ConcurrentHashSet<SModel>(); private final ModelVault<TransientSModelDescriptor> myModelVault = new ModelVault<TransientSModelDescriptor>(); private final Map<String, GenerationTrace> myTraces = new HashMap<String, GenerationTrace>(); /*package*/ TransientModelsModule(@NotNull TransientModelsProvider tmProvider, @NotNull SModuleReference moduleReference) { myComponent = tmProvider; setModuleReference(moduleReference); } public boolean hasPublished() { return !myPublished.isEmpty(); } @Override public void dispose() { clearAll(); super.dispose(); } public void clearAll() { removeAll(); dependenciesChanged(); myPublished.clear(); myModelVault.clear(); } public void removeAll() { for (SModel model : myModelVault.allModels()) { removeModel(model); } } public void clearUnused() { // mature references as a distinct step (not as part of unload()) just in case there are reference // between the models to publish and unload (hence, mature) in improper order may leave reference broken. for (TransientSModelDescriptor model : myModelVault.modelsToPublish()) { model.makeRefsMature(); } for (TransientSModelDescriptor model : myModelVault.modelsToPublish()) { unloadModel(model); } for (SModel model : myModelVault.modelsNotToPublish()) { removeModel(model); } } public boolean addModelToKeep(@NotNull SModelReference modelReference, boolean force) { assert isMyTransientModel(modelReference); if (force) { myModelVault.publish(modelReference); return true; } if (myModelVault.isPublished(modelReference)) { return true; } if (!myComponent.canKeepOneMore()) { // maximum number of models reached return false; } myModelVault.publish(modelReference); myComponent.decreaseKeptModels(); return true; } // to remove published model, one needs write access to a repository, // which is not always possible e.g. when a new checkpoint model replaces existing public void forgetModel(SModelReference modelReference, boolean forgetDependants) { assert isMyTransientModel(modelReference); myModelVault.forget(modelReference); if (forgetDependants) { for (TransientSModelDescriptor tm : myModelVault.allModels()) { for (SModelReference importElement : tm.getModelImports()) { if (modelReference.equals(importElement)) { myModelVault.forget(tm.getReference()); break; } } } } } // model removal doesn't affect list of models to publish. To unpublish a model, call #forgetModel() first public void removeModel(SModel md) { // FNF is poor in tracking transients models (unpublished models do not show up in a repository) // This code might need reconsideration once we have a distinct repository for transient modules (we'll either // get capability to track models, or FNFM will attach finders to a specific repo and dispose all of them at once // when transient repo is thrown away). FastNodeFinderManager.dispose(md); myModelVault.remove(md); if (myPublished.remove(md)) { unregisterModel((SModelBase) md); } if (md instanceof TransientSModelDescriptor) { ((TransientSModelDescriptor) md).dropModel(); } } private void unloadModel(TransientSModelDescriptor model) { model.unloadModelNoSave(); } public void publishAll() { for (TransientSModelDescriptor model : myModelVault.modelsToPublish()) { if (myPublished.add(model)) { registerModel(model); } } for (TransientSModelDescriptor model : myModelVault.modelsNotToPublish()) { if (myPublished.contains(model)) { removeModel(model); } } } public SModel createTransientModel(SModelReference modelReference) { TransientSModelDescriptor result = new TransientSModelDescriptor(modelReference); result.load(); myModelVault.add(result); return result; } public String toString() { return getModuleName() + " [transient models module]"; } // Purpose of this implementation is to resolve references to yet not public transient models private SModel findInVault(SModelId reference) { for (SModel m : myModelVault.allModels()) { if (reference.equals(m.getModelId())) { return m; } } return null; } @Override public SModel getModel(SModelId id) { SModel rv = super.getModel(id); if (rv != null) { return rv; } return findInVault(id); } public boolean isMyTransientModel(SModelReference modelRef) { return modelRef != null && myModelVault.known(modelRef); } /** * Module of any referenced model we can access through our repository (one of TransientModelsProvider) is deemed declared dependency. * There's little value to show 'out of scope' errors for transient nodes, that's why everything is here. * It used to be GMDM(originalModule, Compile), but I don't see any reason for that. */ @Override public Iterable<SDependency> getDeclaredDependencies() { assertCanRead(); // SModelOperations.validateLanguagesAndImports could update this set for us (if I override addDependency() to record values), // but I don't think the method deserves to survive, and its extra use doesn't help this. HashSet<SModelReference> referencedModels = new HashSet<>(); for (SModel m : getModels()) { // I'd love to collect importedModel.getModuleReference(), but GUID model references would leave out quite some module dependencies referencedModels.addAll(new ModelImports(m).getImportedModels()); } HashSet<SModule> deps = new HashSet<>(); for (SModelReference mr : referencedModels) { SModel model = mr.resolve(myComponent.getRepository()); if (model != null && model.getModule() != null) { deps.add(model.getModule()); } } ArrayList<SDependency> rv = new ArrayList<>(deps.size()); deps.forEach(m -> rv.add(new SDependencyImpl(m, SDependencyScope.DEFAULT, false))); return rv; } public GenerationTrace getTrace(SModelReference model) { return myTraces.get(model.getName().getLongName()); } public void publishTrace(@NotNull SModelReference model, @NotNull GenerationTrace trace) { myTraces.put(model.getName().getLongName(), trace); } public void changeModelReference(@NotNull SModel transientModel, @NotNull SModelReference newRef) { assert isMyTransientModel(transientModel.getReference()); ((TransientSModelDescriptor) transientModel).changeModelReference(newRef); } public final class TransientSModelDescriptor extends EditableSModelBase implements jetbrains.mps.extapi.model.TransientSModel, ModelWithAttributes { protected volatile TransientSModel mySModel; private boolean wasUnloaded = false; private ImmatureReferencesTracker myRefsTracker = new ImmatureReferencesTracker(); private TransientSModelDescriptor(@NotNull SModelReference modelRef) { super(modelRef, new NullDataSource()); myRefsTracker.attach(this,false); } @Override protected jetbrains.mps.smodel.SModel getCurrentModelInternal() { return mySModel; } @Override public final jetbrains.mps.smodel.SModel getSModelInternal() { if (mySModel != null) { return mySModel; } // FIXME code identical to BaseSpecialModelDescriptor final ModelLoadingState oldState; synchronized (this) { oldState = getLoadingState(); if (mySModel == null) { mySModel = createModel(); mySModel.setModelDescriptor(this); if (wasUnloaded) { // ensure imports are back // XXX don't ask me why we don't swap out models with imports, but bare nodes only. // TransientModelsModule is not necessarily inside a repository, need to take one // where it would end up if published SRepository repository = TransientModelsModule.this.myComponent.getRepository(); new ModelDependencyUpdate(this).updateUsedLanguages().updateImportedModels(repository); wasUnloaded = false; } setLoadingState(ModelLoadingState.FULLY_LOADED); } } fireModelStateChanged(oldState, ModelLoadingState.FULLY_LOADED); return mySModel; } @Override protected void assertCanChange() { // This model descriptor, unlike others, supports 'unloading' of model data. // IOW, has special handling for models that are already attached to a repository but its model data // could be restored and updated on load. Thus, we allow model modifications unless completely loaded. if (isLoaded()) { super.assertCanChange(); } } private TransientSModel createModel() { if (wasUnloaded) { LOG.debug("Re-loading " + getReference()); TransientSwapSpace swap = myComponent.getTransientSwapSpace(); if (swap == null) throw new IllegalStateException("no swap space"); TransientSModel m = swap.restoreFromSwap(getReference(), new TransientSModel(getReference())); if (m == null) { throw new IllegalStateException("lost swapped out model"); } return m; } else { return new TransientSModel(getReference()); } } @Override protected void doUnload() { if (!wasUnloaded) { LOG.debug("Un-loading " + getReference()); TransientSwapSpace swap = myComponent.getTransientSwapSpace(); if (swap == null || !swap.swapOut(mySModel)) { return; } super.doUnload(); // changes loading state as recorded in the descriptor wasUnloaded = true; mySModel = null; } } // Can't use openapi's unload as EditableSModelBase does save() on unload(), which is (likely? it's guess) not what // originally deemed necessary for transient models (although could have saveModel no-op or subclass EditableModelDescriptor), // thus mimics what SModelBase#unload does. // XXX consider subclassing EditableModelDescriptor and use unload() instead of this method directly /*package*/ void unloadModelNoSave() { final ModelLoadingState oldState = getLoadingState(); doUnload(); fireModelStateChanged(oldState, getLoadingState()); } // unlike unload, doesn't not swap out model data private void dropModel() { myRefsTracker.detach(); if (mySModel != null) { LOG.debug("Dropped " + getReference()); mySModel.setModelDescriptor(null); mySModel.dispose(); mySModel = null; setLoadingState(ModelLoadingState.NOT_LOADED); } } @Override public SModule getModule() { return TransientModelsModule.this; } @Override public boolean isChanged() { // TODO move transient models outside of the default repository; false here prevents model from saving return false; } @Override protected boolean saveModel() throws IOException, ModelSaveException { throw new UnsupportedOperationException(); } @Override public void rename(String newModelName, boolean changeFile) { throw new UnsupportedOperationException(); } @Override protected void reloadContents() { throw new UnsupportedOperationException(); } public void makeRefsMature() { myRefsTracker.makeMature(); } private SModelHeader getModelHeader() { getModelData(); // init mySModel field, just in case it hasn't been initialized return mySModel.getSModelHeader(); } @Override public void setAttribute(@NotNull String key, @Nullable String value) { getModelHeader().setOptionalProperty(key, value); } @Nullable @Override public String getAttribute(@NotNull String key) { return getModelHeader().getOptionalProperty(key); } @Override public void forEach(@NotNull BiConsumer<String, String> action) { getModelHeader().getOptionalProperties().forEach(action); } } }