/*
* 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;
import jetbrains.mps.InternalFlag;
import jetbrains.mps.RuntimeFlags;
import jetbrains.mps.generator.GenerationCanceledException;
import jetbrains.mps.generator.GenerationOptions;
import jetbrains.mps.generator.GenerationParametersProvider;
import jetbrains.mps.generator.GenerationParametersProviderEx;
import jetbrains.mps.generator.GenerationSessionContext;
import jetbrains.mps.generator.GenerationStatus;
import jetbrains.mps.generator.GenerationTrace;
import jetbrains.mps.generator.IGeneratorLogger.ProblemDescription;
import jetbrains.mps.generator.ModelGenerationPlan;
import jetbrains.mps.generator.ModelGenerationPlan.Checkpoint;
import jetbrains.mps.generator.ModelGenerationPlan.Step;
import jetbrains.mps.generator.ModelGenerationPlan.Transform;
import jetbrains.mps.generator.TransientModelsModule;
import jetbrains.mps.generator.impl.GeneratorLoggerAdapter.BasicFactory;
import jetbrains.mps.generator.impl.GeneratorLoggerAdapter.RecordingFactory;
import jetbrains.mps.generator.impl.IGenerationTaskPool.ITaskPoolProvider;
import jetbrains.mps.generator.impl.TemplateGenerator.StepArguments;
import jetbrains.mps.generator.impl.cache.IntermediateCacheHelper;
import jetbrains.mps.generator.impl.cache.QueryProviderCache;
import jetbrains.mps.generator.impl.dependencies.DependenciesBuilder;
import jetbrains.mps.generator.impl.dependencies.IncrementalDependenciesBuilder;
import jetbrains.mps.generator.impl.plan.CheckpointState;
import jetbrains.mps.generator.impl.plan.Conflict;
import jetbrains.mps.generator.impl.plan.CrossModelEnvironment;
import jetbrains.mps.generator.impl.plan.GenerationPartitioningUtil;
import jetbrains.mps.generator.impl.plan.GenerationPlan;
import jetbrains.mps.generator.impl.plan.MapCfgComparator;
import jetbrains.mps.generator.impl.plan.ModelContentUtil;
import jetbrains.mps.generator.impl.plan.PlanSignature;
import jetbrains.mps.generator.plan.CheckpointIdentity;
import jetbrains.mps.generator.runtime.TemplateMappingConfiguration;
import jetbrains.mps.generator.runtime.TemplateMappingScript;
import jetbrains.mps.generator.runtime.TemplateModule;
import jetbrains.mps.logging.MPSAppenderBase;
import jetbrains.mps.messages.MessageKind;
import jetbrains.mps.smodel.FastNodeFinderManager;
import jetbrains.mps.smodel.Generator;
import jetbrains.mps.smodel.SModelStereotype;
import jetbrains.mps.smodel.adapter.structure.MetaAdapterFactoryByName;
import jetbrains.mps.util.NameUtil;
import jetbrains.mps.util.Pair;
import jetbrains.mps.util.performance.IPerformanceTracer;
import org.apache.log4j.Priority;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.language.SLanguage;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.model.SModelReference;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.model.SNodeReference;
import org.jetbrains.mps.openapi.module.SModuleReference;
import org.jetbrains.mps.openapi.persistence.PersistenceFacade;
import org.jetbrains.mps.openapi.util.ProgressMonitor;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
/**
* Igor Alshannikov
* Oct 26, 2005
* <p/>
* Created once per model generation.
*/
class GenerationSession {
private final ITaskPoolProvider myTaskPoolProvider;
private final SModel myOriginalInputModel;
private ModelGenerationPlan myGenerationPlan;
private final GenerationTrace myNewTrace;
private MPSAppenderBase myLoggingHandler;
private final RecordingFactory myLogRecorder;
private final GenerationSessionLogger myLogger;
private DependenciesBuilder myDependenciesBuilder;
private IntermediateCacheHelper myIntermediateCache;
// != null unless session is abandoned/disposed
private GenerationSessionContext mySessionContext;
private final IPerformanceTracer ttrace;
private StepArguments myStepArguments;
private QueryProviderCache myQuerySource;
private int myMajorStep = 0;
private int myMinorStep = -1;
private final GenerationOptions myGenerationOptions;
private final List<SModel> myTransientModelsToRecycle = new ArrayList<SModel>();
GenerationSession(@NotNull SModel inputModel, @NotNull GenControllerContext environment, ITaskPoolProvider taskPoolProvider,
GeneratorLoggerAdapter logger, TransientModelsModule transientModule, IPerformanceTracer performanceTracer, GenerationTrace genTrace) {
myTaskPoolProvider = taskPoolProvider;
myOriginalInputModel = inputModel;
myNewTrace = genTrace;
myLogRecorder = new RecordingFactory(new BasicFactory());
myLogger = new GenerationSessionLogger(logger, myLogRecorder);
ttrace = performanceTracer;
myGenerationOptions = environment.getOptions();
mySessionContext = new GenerationSessionContext(environment, transientModule, myLogger, myOriginalInputModel, performanceTracer);
}
GenerationStatus generateModel(ProgressMonitor monitor) throws GenerationCanceledException {
if (myMajorStep != 0) {
throw new GenerationCanceledException();
}
// create a plan
GenerationParametersProvider parametersProvider = myGenerationOptions.getParametersProvider();
ttrace.push("analyzing dependencies", false);
myGenerationPlan = myGenerationOptions.getCustomPlan(myOriginalInputModel);
if (myGenerationPlan == null) {
Collection<String> additionalLanguages =
parametersProvider instanceof GenerationParametersProviderEx
? ((GenerationParametersProviderEx) parametersProvider).getAdditionalLanguages(myOriginalInputModel)
: null;
List<SLanguage> extraLanguages = null;
if (additionalLanguages != null && !additionalLanguages.isEmpty()) {
extraLanguages = new ArrayList<SLanguage>(additionalLanguages.size());
for (String l : additionalLanguages) {
//this usage of by-name is reviewed
extraLanguages.add(MetaAdapterFactoryByName.getLanguage(l));
}
}
GenerationPlan gp;
myGenerationPlan = gp = new GenerationPlan(myOriginalInputModel, extraLanguages);
if (!checkGenerationPlan(gp) && myGenerationOptions.isStrictMode()) {
throw new GenerationCanceledException();
}
}
warnIfGenerateSelf(myGenerationPlan);
myQuerySource = new QueryProviderCache(myGenerationPlan, myLogger);
monitor.start("", 1 + myGenerationPlan.getSteps().size());
try {
// distinct helper instance to hold data from existing cache (myIntermediateCache keeps data of actual generation)
IntermediateCacheHelper cacheHelper = new IntermediateCacheHelper(myGenerationOptions.getIncrementalStrategy(), new PlanSignature(myOriginalInputModel, myGenerationPlan), ttrace);
IncrementalGenerationHandler incrementalHandler = new IncrementalGenerationHandler(myOriginalInputModel, mySessionContext.getRepository(),
myGenerationOptions, cacheHelper, null);
myDependenciesBuilder = incrementalHandler.createDependenciesBuilder();
if (incrementalHandler.canOptimize()) {
int ignored = incrementalHandler.getIgnoredRoots().size();
int total = incrementalHandler.getRootsCount();
myLogger.info((!incrementalHandler.canIgnoreConditionals() ? "" : "descriptors and ") + ignored + " of " + total + " roots are unchanged");
if (total > 0 && ignored == total && incrementalHandler.canIgnoreConditionals()) {
myLogger.info("generated files are up-to-date");
ttrace.pop();
return new GenerationStatus(myOriginalInputModel, null,
myDependenciesBuilder.getResult(myGenerationOptions.getIncrementalStrategy()), false, false, false);
}
if (!incrementalHandler.getRequiredRoots().isEmpty() || incrementalHandler.requireConditionals()) {
myLogger.info((!incrementalHandler.requireConditionals() ? "" :
"descriptors and ") + incrementalHandler.getRequiredRoots().size() + " roots can be used from cache");
}
if (myGenerationOptions.getTracingMode() != GenerationOptions.TRACE_OFF) {
myLogger.info("Processing:");
for (SNode node : myOriginalInputModel.getRootNodes()) {
if (incrementalHandler.getRequiredRoots().contains(node)) {
myLogger.info(String.format("%s (%s) (cache)", node.getName(), node.getConcept().getQualifiedName()));
} else if (!incrementalHandler.getIgnoredRoots().contains(node)) {
myLogger.info(String.format("%s (%s)", node.getName(), node.getConcept().getQualifiedName()));
}
}
}
}
monitor.advance(1);
if (InternalFlag.isInternalMode() && myGenerationOptions.isRebuildAll() && myGenerationOptions.isDebugIncrementalDependencies() && myDependenciesBuilder instanceof IncrementalDependenciesBuilder) {
myLogger.info("creating generated.trace");
((IncrementalDependenciesBuilder) myDependenciesBuilder).traceDependencyOrigins();
}
boolean success = false;
myIntermediateCache = new IntermediateCacheHelper(myGenerationOptions.getIncrementalStrategy(), new PlanSignature(myOriginalInputModel, myGenerationPlan), ttrace);
myIntermediateCache.createNew(myOriginalInputModel);
ttrace.pop();
try {
// prepare input model: make a clone so that rest of generator always works with transient model.
// This ensures each node got correct TracingUtil.ORIGINAL_INPUT_NODE (for traceInfo) in SNode.userObjects - there
// are templates out there that perform .copy on input nodes, and we have no chances to trace such nodes back. This used to work
// (with fallback to parent nodes) until in-place transformations brought cases when either regular model or a transient one
// serve as an input, which lead to different traceInfo (being more specific with transient model as input, as each node in transient does keep
// reference to origin)
// Another benefit is that FastNodeFinder (used throughout generator e.g. with model.nodes(Concept)) gives nodes in different order for
// regular and transient SModel (sorted by nodeid from DefaultFastNodeFinder, natural iteration order for TransientModelNodeFinder).
// Although this can be fixed in DFNF (not to sort, share impl for both FNF), it's still better to avoid possible differences.
// Last, but not least, there's planned switch to GeneratorSNode/GeneratorSModel to facilitate model reconstruction from delta
// and we'll need to switch to 'transient' (generator) model here anyway
SModel currInputModel = createTransientModel("0");
new CloneUtil(myOriginalInputModel, currInputModel).traceOriginalInput().cloneModelWithImports();
// inform DependencyBuilder about new input model (now it keeps map based on instances, once it's nodeid (or it's gone), there'd be no need for):
for (Iterator<SNode> it1 = myOriginalInputModel.getRootNodes().iterator(), it2 = currInputModel.getRootNodes().iterator(); it1.hasNext() && it2.hasNext();) {
myDependenciesBuilder.registerRoot(it2.next(), it1.next());
}
myDependenciesBuilder.updateModel(currInputModel);
SModel currOutput = null;
ttrace.push("steps", false);
ModelTransitions transitionTrace = new ModelTransitions(); // FIXME make it optional, if there are no Checkpoint steps, do not record transitions
transitionTrace.newTransition(null, myOriginalInputModel.getReference(), currInputModel);
for (myMajorStep = 0; myMajorStep < myGenerationPlan.getSteps().size(); myMajorStep++) {
Step planStep = myGenerationPlan.getSteps().get(myMajorStep);
if (planStep instanceof Transform) {
Transform transformStep = (Transform) planStep;
final List<TemplateMappingConfiguration> mappingConfigurations = transformStep.getTransformations();
if (mappingConfigurations.size() >= 1) {
final TemplateMappingConfiguration first = mappingConfigurations.get(0);
String n = GeneratorUtil.compactNamespace(first.getModel().getLongName());
monitor.step(String.format("step %d (%s#%s%s)", myMajorStep+1, n, first.getName(), mappingConfigurations.size() == 1 ? "" : "..."));
}
if (myLogger.needsInfo()) {
myLogger.info("executing step " + (myMajorStep + 1));
}
currOutput = executeMajorStep(monitor.subTask(1), currInputModel, transformStep, transitionTrace.getActiveTransition());
monitor.advance(0);
if (currOutput == null || myLogger.getErrorCount() > 0) {
break;
}
if (mappingConfigurations.isEmpty()) {
break;
}
currInputModel = currOutput;
} else if (planStep instanceof Checkpoint) {
Checkpoint checkpointStep = (Checkpoint) planStep;
if (!checkpointStep.isPersisted()) {
continue;
}
CheckpointIdentity checkpointIdentity = checkpointStep.getIdentity();
final CrossModelEnvironment xmodelEnv = mySessionContext.getCrossModelEnvironment();
SModel checkpointModel = xmodelEnv.createBlankCheckpointModel(myOriginalInputModel.getReference(), checkpointIdentity);
CheckpointStateBuilder cpBuilder = new CheckpointStateBuilder(currInputModel, checkpointModel, transitionTrace.getActiveTransition());
// myStepArguments may be null if Checkpoint is the very first step. Not quite sure it's legitimate scenario, though, need to think it over.
if (myStepArguments != null) {
// Shall populate state with last generator's MappingLabels. Note, ML could have been added from post-processing scripts. Generator
// instance could be different, we keep GeneratorMappings with step arguments, that span all pre/post scripts along with transformations.
GeneratorMappings stepLabels = myStepArguments.mappingLabels;
cpBuilder.addMappings(myOriginalInputModel, stepLabels);
}
CheckpointState cpState = cpBuilder.create(checkpointIdentity);
xmodelEnv.publishCheckpoint(myOriginalInputModel.getReference(), cpState);
transitionTrace.newTransition(checkpointStep, checkpointModel.getReference(), currInputModel);
myStepArguments = null; // XXX what if there are few subsequent CPs (e.g. from different plans), why do we clear step arguments and
// prevent other CPs from saving MLs?
}
}
ttrace.pop();
// we need this in order to prevent memory leaks from nodes which are reported to message view
// since session objects might include objects with disposed class loaders
mySessionContext.clearTransientObjects();
if (myGenerationOptions.isKeepOutputModel() && currOutput != null) {
mySessionContext.getModule().addModelToKeep(currOutput.getReference(), true);
}
GenerationStatus generationStatus = new GenerationStatus(myOriginalInputModel, currOutput,
myDependenciesBuilder.getResult(myGenerationOptions.getIncrementalStrategy()), myLogger.getErrorCount() > 0,
myLogger.getWarningCount() > 0, false);
generationStatus.setModelExports(mySessionContext.getExports().getExports());
generationStatus.setCrossModelEnvironment(mySessionContext.getCrossModelEnvironment());
success = generationStatus.isOk();
return generationStatus;
} catch (GenerationCanceledException gce) {
throw gce;
} catch (TemplateQueryException tqe) {
// XXX although it's tqe.getCause which is of interest, it might be reasonable to report
// tqe to the logger, as it might get configured outside and decide whether to report a TQE to end user or not
myLogger.handleException(tqe.getCause());
String msg = String.format("Generation failed for model '%s', unexpected error in generator query: %s", myOriginalInputModel.getName(), tqe.getMessage());
ProblemDescription pd;
if (tqe.getQueryContext() != null) {
pd = GeneratorUtil.describeIfExists(tqe.getQueryContext().getInputNode(), "input node");
} else {
pd = GeneratorUtil.describeInput(tqe.getTemplateContext());
}
myLogger.error(tqe.getTemplateModelLocation(), msg, pd);
return new GenerationStatus.ERROR(myOriginalInputModel);
} catch (GenerationFailureException gfe) {
final String nestedException;
if (gfe.getCause() != null) {
nestedException = gfe.getCause().toString();
} else {
nestedException = "";
}
String error = gfe.getMessage() == null ? gfe.toString() : gfe.getMessage();
String msg = String.format("Generation failed for model '%s': %s. %s", myOriginalInputModel.getName(), error, nestedException);
myLogger.handleException(gfe);
myLogger.error(gfe.getTemplateModelLocation(), msg, GeneratorUtil.describeInput(gfe.getTemplateContext()));
return new GenerationStatus.ERROR(myOriginalInputModel);
} catch (Exception e) {
myLogger.handleException(e);
myLogger.error(String.format("Generation failed for model '%s': %s", myOriginalInputModel.getName(), e.toString()));
return new GenerationStatus.ERROR(myOriginalInputModel);
} finally {
if (success) {
myIntermediateCache.commit();
} else {
myIntermediateCache.discard();
}
}
} finally {
monitor.done();
}
}
private SModel executeMajorStep(ProgressMonitor progress, SModel inputModel, Transform planStep, TransitionTrace transitionTrace) throws GenerationCanceledException, GenerationFailureException {
myMinorStep = -1;
List<TemplateMappingConfiguration> mappingConfigurations = new ArrayList<TemplateMappingConfiguration>(planStep.getTransformations());
if (myLogger.needsInfo()) {
printUsedLanguages(inputModel);
printMappingConfigurations("apply mapping configurations:", mappingConfigurations);
}
if (mappingConfigurations.isEmpty() && inputModel.getRootNodes().iterator().hasNext()) {
myLogger.warning("No mapping configurations for the step, skip generation of the model");
return inputModel;
}
// -- replace context
mySessionContext = new GenerationSessionContext(mySessionContext);
// -- filter mapping configurations
TemplateGenerator templateGenerator = new TemplateGenerator(mySessionContext, inputModel, null, new StepArguments(myDependenciesBuilder, myQuerySource));
LinkedList<TemplateMappingConfiguration> drop = new LinkedList<TemplateMappingConfiguration>();
for (TemplateMappingConfiguration c : mappingConfigurations) {
if (!c.isApplicable(templateGenerator)) {
drop.add(c);
}
}
if (!drop.isEmpty()) {
printMappingConfigurations("drop mapping configurations (not applicable):", drop);
}
mappingConfigurations.removeAll(drop);
if (mappingConfigurations.isEmpty()) {
// no applicable configurations found
if (myLogger.needsInfo() && inputModel.getRootNodes().iterator().hasNext()) {
myLogger.info("No generators left, skip generation of the model");
}
return inputModel;
}
// -- prepare generator
Collections.sort(mappingConfigurations, new MapCfgComparator());
GenPlanActiveStep activeStep = new GenPlanActiveStep(myGenerationPlan, planStep, mappingConfigurations);
try {
myStepArguments = new StepArguments(activeStep, myDependenciesBuilder, myNewTrace, new GeneratorMappings(myLogger), transitionTrace, myQuerySource);
SModel outputModel = executeMajorStepInternal(inputModel, progress);
if (myLogger.getErrorCount() > 0) {
myLogger.warning(String.format("model '%s' has been generated with errors", inputModel.getName()));
}
// myStepArguments = null;
return outputModel;
} finally {
recordAccessedTransientModels();
}
}
// precondition: myStepArguments initialized (!= null);
private SModel executeMajorStepInternal(SModel inputModel, ProgressMonitor progress) throws GenerationFailureException, GenerationCanceledException {
SModel currentInputModel = inputModel;
final boolean cloneInputModel = myGenerationOptions.isSaveTransientModels() && myGenerationOptions.applyTransformationsInplace();
// -----------------------
// run pre-processing scripts
// -----------------------
ttrace.push("pre-processing", false);
currentInputModel = preProcessModel(currentInputModel);
ttrace.pop();
SModel currentOutputModel = createTransientModel();
if (myLogger.needsInfo()) {
myLogger.info(
"generating model '" + currentInputModel.getModelName() + "' --> '" + currentOutputModel.getModelName() + "'");
}
boolean isPrimary = true;
// exit condition for secondary mapping
int secondaryMappingRepeatCount = 0;
while (true) {
if (myLogger.needsInfo() && !isPrimary /*only for 1+ minor steps*/) {
myLogger.info(String.format("next minor step '%s' --> '%s'",
SModelStereotype.getStereotype(currentInputModel), SModelStereotype.getStereotype(currentOutputModel)));
}
myNewTrace.nextStep(currentInputModel.getReference(), currentOutputModel.getReference());
final SModel intactInputModelClone = cloneInputModel ? cloneTransientModel(currentInputModel) : null;
final TemplateGenerator tg = prepareToApplyRules(currentInputModel, currentOutputModel);
boolean somethingHasBeenGenerated = false, applySucceed = false;
try {
somethingHasBeenGenerated = applyRules(tg, progress, isPrimary);
applySucceed = true;
if (!somethingHasBeenGenerated) {
myNewTrace.dropStep(currentInputModel.getReference(), currentOutputModel.getReference());
} else {
// next iteration ...
mySessionContext.clearTransientObjects();
isPrimary = false;
}
} finally {
// if apply fails with exception, I'd like to keep both current input and output.
final boolean generationFailed = !applySucceed;
final boolean inplaceChange = tg.getOutputModel() != currentOutputModel;
if (generationFailed) {
publishTransientModel(currentInputModel.getReference());
if (!inplaceChange) {
publishTransientModel(currentOutputModel.getReference());
}
}
if (cloneInputModel) {
// vault in transient module has two model instances with same reference, shall leave only one.
// either forcefully drop intactInputModelClone, or change reference of currentInputModel to be another one
if (inplaceChange && (somethingHasBeenGenerated || generationFailed)) {
// somethingHasBeenGenerated guards against last step without changes
publishTransientModel(intactInputModelClone.getReference());
// pretend inplace model outcome is from currentOutputModel
changeModelReference(currentInputModel, currentOutputModel.getReference());
// currentInputModel (with a model reference of current output) stays as input
//
// now, if we got here due to an error and not going to continue generation
if (generationFailed) {
// input model with in-place changes is published as output. Generally, there's no difference between
// intactInputModelClone and currentInputModel unless an error occurred in delta builder - we are in
// inplaceChange mode, all changes are applied at the very end, e.g. an exception in a query would get us here quite
// before any changes got a chance to show up in output. Nevertheless, it doesn't hurt to have an extra model here.
publishTransientModel(currentOutputModel.getReference());
// we shall discard instance of blank output model, as there's currentInputModel with its reference in the vault to get published
dropTransientModel(currentOutputModel);
}
} else {
dropTransientModel(intactInputModelClone);
}
}
}
if (!somethingHasBeenGenerated) {
dropTransientModel(currentOutputModel);
currentOutputModel = currentInputModel;
break;
} else {
SModel realOutputModel = tg.getOutputModel();
if (realOutputModel == currentOutputModel) { // 'honest' transformation, not in-place
recycleWasteModel(currentInputModel);
currentInputModel = currentOutputModel;
FastNodeFinderManager.dispose(currentInputModel); // why?!
} else {
assert currentInputModel == realOutputModel;
myDependenciesBuilder.dropModel();
// currentInputModel stays as input.
// in fact, can reuse output model here, but it's task to solve together with tracer (and how it would live with startTracing(same models)
dropTransientModel(currentOutputModel);
}
}
if (++secondaryMappingRepeatCount > 10) {
// TODO I'm not quite sure present log+GenericException is better than SpecificExceptionWithData and handling outside
logTenMinorStepsCountReached(tg.getOutputModel());
throw new GenerationFailureException("failed to generate output after 10 repeated mappings");
}
currentOutputModel = createTransientModel();
}
// -----------------------
// run post-processing scripts
// -----------------------
ttrace.push("post-processing", false);
currentOutputModel = postProcessModel(currentOutputModel);
ttrace.pop();
return currentOutputModel;
}
private void logTenMinorStepsCountReached(SModel realOutputModel) {
myLogger.error("failed to generate output after 10 repeated mappings");
myLogger.error("to get more diagnostic generate model with the 'save transient models' option");
}
@NotNull
private TemplateGenerator prepareToApplyRules(SModel currentInputModel, SModel currentOutputModel) {
myDependenciesBuilder.setOutputModel(currentOutputModel, myMajorStep, myMinorStep);
return myGenerationOptions.isGenerateInParallel()
? new ParallelTemplateGenerator(myTaskPoolProvider, mySessionContext, currentInputModel, currentOutputModel, myStepArguments)
: new TemplateGenerator(mySessionContext, currentInputModel, currentOutputModel, myStepArguments);
}
private boolean applyRules(TemplateGenerator tg, ProgressMonitor progress, final boolean isPrimary)
throws GenerationFailureException, GenerationCanceledException {
final SModel originalOutputModel = tg.getOutputModel();
ttrace.push(String.format("Step %d.%d", myMajorStep+1, myMinorStep), true);
final boolean hasChanges = tg.apply(progress, isPrimary);
ttrace.pop();
if (isPrimary || hasChanges) {
myIntermediateCache.store(myMajorStep, myMinorStep, tg, myDependenciesBuilder);
}
if (hasChanges) {
SModel realOutputModel = tg.getOutputModel();
myDependenciesBuilder.updateModel(realOutputModel);
} else {
// nothing has been generated
myDependenciesBuilder.dropModel();
if (!isPrimary) {
// we may need myMinorStep in postProcess, when we store TransientModelWithMetainfo
// applyRules did that for primary step regardless of hasChanges state, hence we decrement minorStep
// only on secondary no-change runs to forget about no-op applyRules.
// I consider this changes safer than to remove isPrimary check in applyRules (it's appealing
// to save TMWM only when there are changes) as it seems there's assumption about TMWM presence (if used) for each step.
myMinorStep--;
}
if (myLogger.needsInfo()) {
myLogger.info(String.format("unchanged, empty model '%s' removed", SModelStereotype.getStereotype(originalOutputModel)));
}
}
return hasChanges;
}
private SModel preProcessModel(SModel currentInputModel) throws GenerationFailureException {
final RuleManager ruleManager = myStepArguments.planStep.getRuleManager();
if (ruleManager.getPreProcessScripts().isEmpty()) {
return currentInputModel;
}
final boolean modifiesModel = ruleManager.getPreProcessScripts().modifiesModel();
// need to clone input model?
// generally, there's no need to have a copy to run a script, even if it modifies the model
// however, if we keep transients AND model is modified, it's handy to get a copy of the model to see the difference
final boolean needToCloneInputModel = modifiesModel && myGenerationOptions.isSaveTransientModels();
SModel toRecycle = null;
if (needToCloneInputModel) {
ttrace.push("model clone", false);
SModel currentInputModel_clone = createTransientModel();
if (myLogger.needsInfo()) {
myLogger.info(String.format("clone model '%s' --> '%s'", currentInputModel.getName(), currentInputModel_clone.getName()));
}
new CloneUtil(currentInputModel, currentInputModel_clone).cloneModelWithImports();
ttrace.pop();
myNewTrace.nextStep(currentInputModel.getReference(), currentInputModel_clone.getReference());
// probably we can forget about former input model here
toRecycle = currentInputModel;
currentInputModel = currentInputModel_clone;
myDependenciesBuilder.scriptApplied(currentInputModel); // scriptApplied for a blank copy to record old root to new root mapping
} else {
myNewTrace.nextStep(currentInputModel.getReference(), currentInputModel.getReference());
}
TemplateGenerator templateGenerator = new TemplateGenerator(mySessionContext, currentInputModel, currentInputModel, myStepArguments);
for (TemplateMappingScript preMappingScript : ruleManager.getPreProcessScripts().getScripts()) {
if (myLogger.needsInfo()) {
myLogger.info(preMappingScript.getScriptNode(), "pre-process " + preMappingScript.getLongName());
}
templateGenerator.executeScript(preMappingScript);
}
if (modifiesModel) {
myDependenciesBuilder.scriptApplied(currentInputModel);
}
if (needToCloneInputModel) {
myIntermediateCache.store(myMajorStep, myMinorStep, templateGenerator, myDependenciesBuilder);
recycleWasteModel(toRecycle);
}
myLogger.info("pre-processing finished");
return currentInputModel;
}
private SModel postProcessModel(SModel currentModel) throws GenerationFailureException {
final RuleManager ruleManager = myStepArguments.planStep.getRuleManager();
if (ruleManager.getPostProcessScripts().isEmpty()) {
return currentModel;
}
// post-processing script is deemed to modify model always
final boolean needToCloneModel = myGenerationOptions.isSaveTransientModels();
SModel toRecycle = null;
if (needToCloneModel) {
ttrace.push("model clone", false);
SModel currentOutputModel_clone = createTransientModel();
if (myLogger.needsInfo()) {
myLogger.info(String.format("clone model '%s' --> '%s'", currentModel.getName(), currentOutputModel_clone.getName()));
}
new CloneUtil(currentModel, currentOutputModel_clone).cloneModelWithImports();
ttrace.pop();
myNewTrace.nextStep(currentModel.getReference(), currentOutputModel_clone.getReference());
toRecycle = currentModel;
currentModel = currentOutputModel_clone;
myDependenciesBuilder.scriptApplied(currentModel);
} else {
myNewTrace.nextStep(currentModel.getReference(), currentModel.getReference());
// just in case post-script modifies model a lot, and we've got FNF there, prevent it being updated - it's cheaper to create new one at the next step
FastNodeFinderManager.dispose(currentModel);
}
// FIXME I don't need ruleManager, nor even DependencyManager to execute a script. Refactor QueryExecutionContext
TemplateGenerator templateGenerator = new TemplateGenerator(mySessionContext, currentModel, currentModel, myStepArguments);
for (TemplateMappingScript postMappingScript : ruleManager.getPostProcessScripts().getScripts()) {
if (myLogger.needsInfo()) {
myLogger.info(postMappingScript.getScriptNode(), "post-process " + postMappingScript.getLongName());
}
templateGenerator.executeScript(postMappingScript);
}
myDependenciesBuilder.scriptApplied(currentModel);
if (needToCloneModel) {
myIntermediateCache.store(myMajorStep, myMinorStep, templateGenerator, myDependenciesBuilder);
recycleWasteModel(toRecycle);
}
myLogger.info("post-processing finished");
return currentModel;
}
// XXX createOutputModel? - since the method has a side effect, increments myMinorStep count
private SModel createTransientModel() {
return createTransientModel(Integer.toString(myMajorStep + 1) + "_" + ++myMinorStep);
}
private SModel createTransientModel(String stereotype) {
TransientModelsModule module = mySessionContext.getModule();
String longName = NameUtil.getModelLongName(myOriginalInputModel);
final String transientModelName = longName + '@' + stereotype;
final SModelReference mr = PersistenceFacade.getInstance().createModelReference(module.getModuleReference(), jetbrains.mps.smodel.SModelId.generate(), transientModelName);
return module.createTransientModel(mr);
}
/**
* makes an identical copy of transient model, preserving model reference
*/
private SModel cloneTransientModel(SModel transientModel) {
TransientModelsModule module = mySessionContext.getModule();
final SModelReference mr = transientModel.getReference();
assert module.isMyTransientModel(mr);
SModel newModel = module.createTransientModel(mr);
new CloneUtil(transientModel, newModel).cloneModelWithImports();
return newModel;
}
private void changeModelReference(@NotNull SModel transientModel, @NotNull SModelReference newRef) {
TransientModelsModule module = mySessionContext.getModule();
module.changeModelReference(transientModel, newRef);
}
/**
* Dispose model and associated resources.
* The model is recycled unless we keep transients or there's a warning/error pointing to the model.
*/
private void recycleWasteModel(@NotNull SModel model) {
assert (model.getModule() instanceof TransientModelsModule);
myTransientModelsToRecycle.add(model);
}
// records the reference to model we'd like to see in transients, useful to forcefully
// expose models on failures
private void publishTransientModel(@NotNull SModelReference modelReference) {
mySessionContext.getModule().addModelToKeep(modelReference, true);
}
// forget particular transient model instance (doesn't affect list of published models)
// useful for models without changes
private void dropTransientModel(@NotNull SModel model) {
mySessionContext.getModule().removeModel(model);
}
private void warnIfGenerateSelf(ModelGenerationPlan generationPlan) {
// XXX why not to warn if I generate a model written in a language using this language's generator
// (i.e. intention aspect in lang.intention with lang.intention's generator). Is it generally ok (it is for intention,
// but e.g. for behaviors if they are used in generator it might not be the case)
if (myOriginalInputModel.getModule() instanceof Generator && SModelStereotype.isGeneratorModel(myOriginalInputModel)) {
SModuleReference me = myOriginalInputModel.getModule().getModuleReference();
for (TemplateModule t : generationPlan.getGenerators()) {
if (t.getModuleReference().equals(me)) {
myLogger.warning("the generator is used to generate itself: try to avoid using language constructs in its queries");
break;
}
}
}
}
private boolean checkGenerationPlan(GenerationPlan generationPlan) {
if (generationPlan.hasConflictingPriorityRules()) {
myLogger.error("Conflicting mapping priority rules encountered:");
for (Conflict c : generationPlan.getConflicts()) {
SModuleReference origin = c.getOrigin();
if (origin == null) {
// there might be conflicts due to implicit rules GenerationPlan adds. These rules don't belong to any
// generator, thus we use current input model as the origin of the conflict.
// XXX it might be reasonable to keep this logic deep in GP and restrict Conflict.getOrigin != null.
origin = myOriginalInputModel.getModule().getModuleReference();
}
myLogger.error(origin, c.getText());
}
myLogger.error("");
return false;
}
return true;
}
private void printUsedLanguages(SModel inputModel) {
List<SLanguage> references = new ArrayList<SLanguage>(ModelContentUtil.getUsedLanguages(inputModel));
Collections.sort(references, Comparator.comparing(SLanguage::getQualifiedName));
myLogger.info("languages used:");
for (SLanguage lang : references) {
myLogger.info(" " + lang);
}
}
private void printMappingConfigurations(String title, List<TemplateMappingConfiguration> mc) {
myLogger.info(title);
List<Pair<String, TemplateMappingConfiguration>> messages = GenerationPartitioningUtil.toStrings(mc);
for (Pair<String, TemplateMappingConfiguration> message : messages) {
myLogger.info(message.o2.getMappingNode(), String.format(" %s", message.o1));
}
}
private void recordAccessedTransientModels() {
Collection<SModelReference> modelToKeepCandidates = new LinkedHashSet<SModelReference>();
final TransientModelsModule transientsModule = mySessionContext.getModule();
if (keepTransientForMessageNavigation()) {
modelToKeepCandidates.addAll(myLogRecorder.ofKind(MessageKind.ERROR));
if (myGenerationOptions.isShowWarnings() && myGenerationOptions.isKeepModelsWithWarnings()) {
modelToKeepCandidates.addAll(myLogRecorder.ofKind(MessageKind.WARNING));
}
for (SModelReference mr : modelToKeepCandidates) {
if (transientsModule.isMyTransientModel(mr)) {
transientsModule.addModelToKeep(mr, false);
}
}
}
myLogRecorder.reset();
final boolean discardTransients = !myGenerationOptions.isSaveTransientModels();
for (SModel m : myTransientModelsToRecycle) {
if (discardTransients && !modelToKeepCandidates.contains(m.getReference())) {
// drop a model only if we don't save transients and don't keep this model due to errors/warnings
transientsModule.removeModel(m);
} else {
transientsModule.addModelToKeep(m.getReference(), true);
}
}
myTransientModelsToRecycle.clear();
}
private boolean keepTransientForMessageNavigation() {
return !RuntimeFlags.isTestMode();
}
public MPSAppenderBase getLoggingHandler() {
if (myLoggingHandler == null) {
myLoggingHandler = new MPSAppenderBase() {
@Override
protected void append(@NotNull Priority level, @NotNull String categoryName, @NotNull String message, @Nullable Throwable t,
@Nullable Object hintObject) {
if (hintObject instanceof SNode) {
final SModel m = ((SNode) hintObject).getModel();
myLogRecorder.record(MessageKind.fromPriority(level), m.getReference());
} else if (hintObject instanceof SModelReference) {
SModelReference mr = (SModelReference) hintObject;
myLogRecorder.record(MessageKind.fromPriority(level), mr);
} else if (hintObject instanceof SNodeReference) {
myLogRecorder.record(MessageKind.fromPriority(level), ((SNodeReference) hintObject).getModelReference());
}
}
};
}
return myLoggingHandler;
}
public void discardTransients() {
if (mySessionContext == null) return;
if (!myGenerationOptions.isSaveTransientModels()) {
mySessionContext.getModule().clearUnused();
}
myQuerySource.dispose();
mySessionContext = null;
}
}