/* * 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.impl.plan; import jetbrains.mps.extapi.model.ModelWithAttributes; import jetbrains.mps.generator.GenerationStatus; import jetbrains.mps.generator.TransientModelsModule; import jetbrains.mps.generator.TransientModelsProvider; import jetbrains.mps.generator.cache.CacheGenerator; import jetbrains.mps.generator.generationTypes.StreamHandler; import jetbrains.mps.generator.impl.CloneUtil; import jetbrains.mps.generator.impl.MappingLabelExtractor; import jetbrains.mps.generator.impl.ModelStreamManager; import jetbrains.mps.generator.impl.cache.MappingsMemento; import jetbrains.mps.generator.plan.CheckpointIdentity; import jetbrains.mps.generator.plan.PlanIdentity; import jetbrains.mps.smodel.ModelAccessHelper; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.mps.openapi.model.SModel; import org.jetbrains.mps.openapi.model.SModelName; import org.jetbrains.mps.openapi.model.SModelReference; import org.jetbrains.mps.openapi.persistence.PersistenceFacade; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; /** * Captures what outer world would like to tell generator about available cross-model reference targets. * <p> * FIXME likely, we shall not keep checkpoint models for actions other than true generate * (e.g. text preview). Still shall resolve cross model references, but #createCheckpoint shall become no-op. * OTOH, what if I preview 2 nodes ith cross-references from 2 different models? Kept, but separately? * <p> * XXX perhaps, can instantiate CME with PlanIdentity at hand (does plan change during generation run? It's per-model, after all). * * @author Artem Tikhomirov * @since 3.3 */ public class CrossModelEnvironment { private static final String GENERATION_PLAN = "generation-plan"; private static final String CHECKPOINT = "checkpoint"; // these are checkpoints for actual plan, for different models that are cross-referenced from the one being transformed private final HashMap<SModelReference, ModelCheckpoints> myTransientCheckpoints = new HashMap<>(); // these are CPs for all plans of x-referenced models private final HashMap<SModelReference, CheckpointVault> myPersistedCheckpoints = new HashMap<>(); // these are CPs for actual plan, loaded from persisted CPs and re-published as transient models private final HashMap<SModelReference, ModelCheckpoints> myExposedPersisted = new HashMap<>(); private final TransientModelsModule myModule; private final ModelStreamManager.Provider myStreamProvider; private final TransientModelsProvider myTransientModelProvider; public CrossModelEnvironment(TransientModelsProvider tmProvider, ModelStreamManager.Provider streamProvider) { myTransientModelProvider = tmProvider; myModule = tmProvider.getCheckpointsModule(); // FIXME in the future - populate from existing cp models // but for prototype, models visible within same make would suffice myStreamProvider = streamProvider; } /** * FIXME Given CP could be defined in a plan/CPSet other then the one being executed, is there any sense to * pass planIdentity, not CPIdentity here. Perhaps, could hide ModelCheckpoints concept altogether as * implementation detail? * * @return recorded checkpoints for the model, if any */ @Nullable public ModelCheckpoints getState(@NotNull SModel model) { ModelCheckpoints mcp = getTransientCheckpoints(model.getReference()); if (mcp != null) { return mcp; } // FIXME once accessed, perhaps ModelCheckpoints instance shall be kept in myTransientCheckpoints or myExposedPersisted? return getPersistedCheckpoints(model).getCheckpointsFor((m, cp) -> { // XXX for now, expose whole ModelCheckpoints at once, although just specific CheckpointState // (accessed later though MC.find) would suffice SModel exposed = createBlankCheckpointModel(model.getReference(), cp); new CloneUtil(m, exposed).cloneModelWithImports(); myModule.addModelToKeep(exposed.getReference(), true); return exposed; }); } @Nullable private ModelCheckpoints getTransientCheckpoints(SModelReference originModel) { ModelCheckpoints mcp = myTransientCheckpoints.get(originModel); if (mcp == null) { mcp = loadFromTransientModule(originModel.getName()); if (mcp != null) { myTransientCheckpoints.put(originModel, mcp); } } return mcp; } /** * look up checkpoint models in transient module * CP models may be there if they were generated as part of the same make session or exposed there due to 'copy' of persisted model * when a reference to persisted CP was resolved. * <p> * both parameters are !null */ private ModelCheckpoints loadFromTransientModule(SModelName originalModelName) { // XXX getCheckpointModelsFor iterates models of the module, hence needs a model read // OTOH, just a wrap with model read doesn't make sense here (models could get disposed right after the call), // so likely we shall populate myCheckpoints in constructor/dedicated method. Still, what about checkpoint model disposed *after* // I've collected all the relevant state for this class? // Not sure whether read shall be local to this class or external on constructor/initialization method // It seems to be an implementation detail that we traverse model and use its nodes to persist mapping label information (that's what we need RA for). return new ModelAccessHelper(myTransientModelProvider.getRepository()).runReadAction(() -> { String nameNoStereotype = originalModelName.getLongName(); ArrayList<CheckpointState> cpModels = new ArrayList<>(4); for (SModel m : myModule.getModels()) { if (!nameNoStereotype.equals(m.getName().getLongName()) || false == m instanceof ModelWithAttributes) { continue; } String gpAttrValue = ((ModelWithAttributes) m).getAttribute(GENERATION_PLAN); String cpAttrValue = ((ModelWithAttributes) m).getAttribute(CHECKPOINT); if (gpAttrValue == null || cpAttrValue == null) { continue; } PlanIdentity modelPlan = new PlanIdentity(gpAttrValue); CheckpointIdentity modelCheckpoint = new CheckpointIdentity(modelPlan, cpAttrValue /* here, persistent identity*/); // FIXME read and fill memento with MappingLabels // now, just restore it from debug root we've got there. Later (once true persistence is done), shall consider // option to keep mappings inside a model (not to bother with persistence) or to follow MappingsMemento approach with // custom serialization code (and to solve the issue of associated model streams serialized/managed (i.e. deleted) along with a cp model) MappingsMemento memento = new MappingLabelExtractor().restore(MappingLabelExtractor.findDebugNode(m)); cpModels.add(new CheckpointState(memento, m, modelCheckpoint)); } return cpModels.isEmpty() ? null : new ModelCheckpoints(cpModels); }); } /** * look up checkpoint models in persisted model-associated streams */ private CheckpointVault getPersistedCheckpoints(SModel model) { // FIXME synchronization - synchronized or concurrent map? CheckpointVault cpv = myPersistedCheckpoints.get(model.getReference()); if (cpv == null) { cpv = new CheckpointVault(myStreamProvider.getStreamManager(model)); cpv.readCheckpointRegistry(); myPersistedCheckpoints.put(model.getReference(), cpv); } return cpv; } /** * FIXME Not sure if it's right to pass CPI here, not CP. On one hand, we use CPI to identify any its use in any plan. * OTOH, here we construct name for a model being transformed (not *referenced*), and as such we care about specific CP in a specific plan, * not just its identity. */ /*package*/ static SModelName createCheckpointModelName(SModelReference originalModel, CheckpointIdentity step) { String longName = originalModel.getName().getLongName(); String stereotype = step.getPersistenceValue(); return new SModelName(longName, stereotype); } // originalModel is just to construct name/reference of the checkpoint model public SModel createBlankCheckpointModel(SModelReference originalModel, CheckpointIdentity step) { final SModelName transientModelName = createCheckpointModelName(originalModel, step); final SModelReference mr = PersistenceFacade.getInstance() .createModelReference(myModule.getModuleReference(), jetbrains.mps.smodel.SModelId.generate(), transientModelName.getValue()); SModel checkpointModel = myModule.createTransientModel(mr); assert checkpointModel instanceof ModelWithAttributes; ((ModelWithAttributes) checkpointModel).setAttribute(GENERATION_PLAN, step.getPlan().getName()); ((ModelWithAttributes) checkpointModel).setAttribute(CHECKPOINT, step.getName()); return checkpointModel; } public void publishCheckpoint(@NotNull SModelReference originalModel, @NotNull CheckpointState cpState) { myModule.addModelToKeep(cpState.getCheckpointModel().getReference(), true); ModelCheckpoints checkpoints = getTransientCheckpoints(originalModel); if (checkpoints == null) { // XXX what if there's one in persistent? Shall we copy it into transient and update with the code below? myTransientCheckpoints.put(originalModel, new ModelCheckpoints(cpState)); } else { CheckpointState replaced = checkpoints.updateAndDiscardOutdated(cpState); if (replaced == null) { return; } HashSet<SModelReference> forgottenCheckpoints = new HashSet<>(); ArrayDeque<CheckpointState> discarded = new ArrayDeque<>(); discarded.add(replaced); do { CheckpointState next = discarded.removeFirst(); // XXX once checkpoint model is removed, any other checkpoint model referencing it is broken, i.e. // m1@cp1 and m2@cp1, latter referencing the former, and we rebuild m1. Once we get here, we'd schedule m1@cp1 for removal // and at the end of the day we've got m1'@cp1 and m2@cp1 with references pointing to no-longer-existing m1@cp1. // Then, if there'd m3 to generate with the same plan, which references both m1 and m2, it's not clear how to match the two. // The question is, do we need to update references in other @cp1 models, shall we keep all models to preserve any other // checkpoint models (i.e. no forgetModel), or perhaps a dedicated SModelReference that resolves to whatever checkpoint is there. // // Present approach is to drop any model that depends on the one re-generated (resolve to latest CP model might still leave // broken references if m1 is changed, and it's not easy to match nodes of old m1@cp1 versus new m1@cp1, model reference won't suffice // as node id might be different, and we got no control over nodes as they are outcome of black-box ReferenceResolver code. SModelReference cpReference = next.getCheckpointModel().getReference(); forgottenCheckpoints.add(cpReference); myModule.forgetModel(cpReference, true); // drop any other checkpoints that may reference the one removed. We've scheduled for removal their respective // transient models already (above with forgetModel(..., true)), now it's time to forget CheckpointState. // Perhaps, shall forget models here explicitly, rather than do the same in TransientModelsModule.forgetModel(..., true) for (ModelCheckpoints mcp : myTransientCheckpoints.values()) { // intentionally don't skip mcp == checkpoints - we need to drop any further checkpoint models not only for // external dependencies, but for subsequent cp models of the same original one, provided they reference the one we've dropped. // Note that the cycle above drops only relevant cp model (compares checkpoint name). mcp.discardOutdated(forgottenCheckpoints, discarded); } } while (!discarded.isEmpty()); } } public static class CacheGen implements CacheGenerator { @Override public void generateCache(GenerationStatus status, StreamHandler handler) { CrossModelEnvironment cme = status.getCrossModelEnvironment(); if (cme == null) { return; } ModelCheckpoints mcp = cme.myTransientCheckpoints.get(status.getOriginalInputModel().getReference()); if (mcp == null) { // may happen if the model has been generated without a custom plan // FIXME we shall look into generation tasks rather than original input model, and there could be tasks // that do not produce ModelCheckpoints (i.e. are generated without a custom plan) return; } CheckpointVault cpVault = cme.getPersistedCheckpoints(status.getOriginalInputModel()); assert cpVault != null; cpVault.updateCheckpointsOf(mcp); cpVault.saveChanged(handler); } } }