/*
* 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.generator.GenerationPlanBuilder;
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.RigidGenerationPlan;
import jetbrains.mps.generator.plan.CheckpointIdentity;
import jetbrains.mps.generator.plan.PlanIdentity;
import jetbrains.mps.generator.runtime.TemplateMappingConfiguration;
import jetbrains.mps.generator.runtime.TemplateModel;
import jetbrains.mps.generator.runtime.TemplateModule;
import jetbrains.mps.messages.IMessageHandler;
import jetbrains.mps.messages.LogHandler;
import jetbrains.mps.messages.Message;
import jetbrains.mps.messages.MessageKind;
import jetbrains.mps.smodel.language.GeneratorRuntime;
import jetbrains.mps.smodel.language.LanguageRegistry;
import jetbrains.mps.smodel.language.LanguageRuntime;
import org.apache.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.language.SLanguage;
import org.jetbrains.mps.openapi.module.SModule;
import org.jetbrains.mps.openapi.module.SModuleReference;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Though I hate the name as it doesn't tell anything, it's a plan builder for regular use, targeted
* for everyday scenarios like model make in IDE. Name alternatives are ScopedPlanBuilder, RestrictExtendsPB and others of the same degree of imperfection.
* Likely, we'll need different approaches to extension processing, and the name shall reflect the approach, but at the moment I can't come up with a better one.
* <p/>
* Supports all operations.
* <p/>
* IMPORTANT: Treats extensions to generators on a 'first come, first served basis'.
* Tries to consume as many extensions as possible for the first statement that calls for extensions.
* Extensions are looked up among generators denoted as 'engaged'. If generatorB extends generatorA, and there's 'apply with extensions generatorA',
* then generatorB would get into final transformation sequence only when it's listed among 'engaged' generators supplied at the builder's construction time.
* Note, this implementation use these generators to limit 'upper' boundary only, in the aforementioned example it doesn't matter whether list of 'engaged'
* includes generatorA, which gets into transformation sequence regardless of 'engaged' generators. Perhaps, this has to be changed (and ScopedPB would be
* more appropriate name then)
*
* @author Artem Tikhomirov
* @since 2017.1
*/
public class RegularPlanBuilder implements GenerationPlanBuilder {
private final LanguageRegistry myLanguageRegistry;
private final Collection<TemplateModule> myEngagedGenerators;
private final IMessageHandler myMessageHandler;
private final List<StepEntry> mySteps = new ArrayList<>();
public RegularPlanBuilder(@NotNull LanguageRegistry languageRegistry, Collection<TemplateModule> allEngagedGenerators) {
this(languageRegistry, allEngagedGenerators, new LogHandler(Logger.getLogger(RegularPlanBuilder.class)));
}
public RegularPlanBuilder(@NotNull LanguageRegistry languageRegistry, Collection<TemplateModule> allEngagedGenerators, @Nullable IMessageHandler messageHandler) {
myLanguageRegistry = languageRegistry;
myEngagedGenerators = allEngagedGenerators;
myMessageHandler = messageHandler == null ? IMessageHandler.NULL_HANDLER : messageHandler;
}
@Override
public void transformLanguage(@NotNull SLanguage... languages) {
ArrayList<TemplateModule> tm = new ArrayList<>(languages.length);
for (SLanguage language : languages) {
if (language == null) {
myMessageHandler.handle(new Message(MessageKind.ERROR, RegularPlanBuilder.class, "Request to apply null language"));
continue;
}
LanguageRuntime lr = myLanguageRegistry.getLanguage(language);
if (lr == null) {
myMessageHandler.handle(new Message(MessageKind.ERROR, RegularPlanBuilder.class, String.format("Language %s not found among deployed", language)));
continue;
}
lr.getGenerators().stream().filter(TemplateModule.class::isInstance).map(TemplateModule.class::cast).forEach(tm::add);
}
// Perhaps, shall record LanguageEntry and build set of templates when required?
mySteps.add(new TransformEntry(tm, true, false));
}
@Override
public void applyGenerator(@NotNull SModule... generators) {
mySteps.add(new TransformEntry(asTemplateModules(generators), true, false));
}
@Override
public void applyGeneratorWithExtended(@NotNull SModule ... generator) {
mySteps.add(new TransformEntry(asTemplateModules(generator), false, false));
}
@Override
public void applyGenerators(@NotNull Collection<SModuleReference> generators, @NotNull BuilderOption... options) {
boolean withExtended = BuilderOption.WithExtendedGenerators.presentIn(options);
boolean respectPriorityRules = withExtended && BuilderOption.WithPriorityRules.presentIn(options);
mySteps.add(new TransformEntry(asTemplateModules(generators), !withExtended, respectPriorityRules));
}
@Override
public void apply(@NotNull Collection<TemplateMappingConfiguration> tmc) {
mySteps.add(new PreparedEntry(new ArrayList<>(tmc)));
}
@Override
public void recordCheckpoint(@NotNull CheckpointIdentity cp) {
mySteps.add(new CheckpointEntry(cp, false));
}
@Override
public void synchronizeWithCheckpoint(@NotNull CheckpointIdentity cp) {
mySteps.add(new CheckpointEntry(cp, true));
}
@NotNull
@Override
public ModelGenerationPlan wrapUp(@NotNull PlanIdentity planIdentity) {
HashSet<TemplateModule> explicitlyMentioned = new HashSet<>();
mySteps.forEach(s -> s.reportInvolvedGenerators(explicitlyMentioned));
HashSet<TemplateModule> availableAsExt = new HashSet<>(myEngagedGenerators);
// FIXME quite ineffective way to deal with LanguageRuntime.getGenerators producing new instance of TemplateModule each time asked.
availableAsExt.removeIf(tm -> explicitlyMentioned.stream().anyMatch(m -> m.getModuleReference().equals(tm.getModuleReference())));
class S {
public final TemplateModule generator;
public final Collection<SModuleReference> directlyExtendedGenerators;
public S(TemplateModule g) {
generator = g;
// XXX use SModuleReference here as a workaround to deal with !TemplateModuleX.equals(TemplateModuleX) if obtained with distinct calls to LR.getGenerators()
directlyExtendedGenerators = g.getExtendedGenerators().stream().map(TemplateModule::getModuleReference).collect(Collectors.toList());
}
}
S[] topoOrder = new S[availableAsExt.size()]; // it's partial topo ordering, just for extended generators mentioned directly
int i = 0;
for (TemplateModule extCandidate : availableAsExt) {
topoOrder[i++] = new S(extCandidate);
}
Arrays.sort(topoOrder, (o1, o2) -> {
// o2 needs o1, then o1 < o2
if (o2.directlyExtendedGenerators.contains(o1.generator.getModuleReference())) {
return -1;
}
// o1 needs o2, then o1 > o2
if (o1.directlyExtendedGenerators.contains(o2.generator.getModuleReference())) {
return 1;
}
return 0;
});
// It's intentional (though not necessarily right) that we look into generators extended directly only, not transitive closure.
// The idea is that given C extends B extends A, and A.withExtensions and C among availableExt and no B whatsoever, I don't want C to show up.
//
/* C -> B -> A
* D -> A
* E -> B & D
* G -> E
* F -> E & A
* Extends direction is bottom to top:
* A__
* |\ \
* | \ \
* B D \
* |\ | \
* | \| |
* C E |
* |\ |
* | \|
* G F
* If A and B explicitly mentioned in a plan:
* For B: C, E, G, F
* For A: D, E, G, F
* If A, B and E explicitly mentioned in a plan:
* For A: D, F
* For B: C
* For E: G, F
*/
for (S s : topoOrder) {
for (StepEntry se : mySteps) {
se.registerIfIntersects(s.directlyExtendedGenerators, s.generator);
}
}
ArrayList<Step> steps = new ArrayList<>(mySteps.size());
mySteps.forEach(s -> s.createStep(RegularPlanBuilder.this, steps));
return new RigidGenerationPlan(planIdentity, steps);
}
private Collection<TemplateModule> asTemplateModules(@NotNull Collection<SModuleReference> generators) {
ArrayList<TemplateModule> tm = new ArrayList<>(generators.size());
for (SModuleReference generatorIdentity : generators) {
TemplateModule gr = findDeployedGenerator(generatorIdentity);
if (gr != null) {
tm.add(gr);
}
}
return tm;
}
private Collection<TemplateModule> asTemplateModules(@NotNull SModule... generators) {
ArrayList<TemplateModule> tm = new ArrayList<>(generators.length);
for (SModule generator : generators) {
if (generator == null) {
myMessageHandler.handle(new Message(MessageKind.ERROR, RegularPlanBuilder.class, "Request to transform with null generator"));
continue;
}
TemplateModule gr = findDeployedGenerator(generator.getModuleReference());
if (gr != null) {
tm.add(gr);
}
}
return tm;
}
@Nullable
private TemplateModule findDeployedGenerator(SModuleReference deployedGeneratorIdentity) {
GeneratorRuntime gr = myLanguageRegistry.getGenerator(deployedGeneratorIdentity);
if (gr instanceof TemplateModule) {
return ((TemplateModule) gr);
}
String msg = String.format(gr == null ? "Generator %s not found among deployed" : "Generator %s is not a TemplateModule", deployedGeneratorIdentity.getModuleName());
myMessageHandler.handle(new Message(MessageKind.ERROR, RegularPlanBuilder.class, msg).setHintObject(deployedGeneratorIdentity));
return null;
}
private interface StepEntry {
/**
* @param result collections to feed with generators of this step
*/
void reportInvolvedGenerators(Collection<TemplateModule> result);
/**
* @param directExtendedGenerators generators directly extended by {@code extCandidate}, just an handy, calculated-once set.
* @param extCandidate generator
* @return {@code true} if {@code extCandidate} has been consumed by the step as an extension (doesn't mean other steps could not consume it as well)
*/
void registerIfIntersects(Collection<SModuleReference> directExtendedGenerators, TemplateModule extCandidate);
/**
* @param planBuilder do I need this?
* @param steps ordered collection to receive new plan step(s) according to this entry.
*/
void createStep(RegularPlanBuilder planBuilder, List<Step> steps);
}
private static class TransformEntry implements StepEntry {
private final ArrayList<TemplateModule> myGenerators;
private final boolean myIsSealed; // true if no extensions are considered.
// true if shall read priority rules from specified generators and break this step further down to smaller according to these rules.
private final boolean myRespectPriorityRules;
private final ArrayList<TemplateModule> myExtensions = new ArrayList<>(4);
TransformEntry(Collection<TemplateModule> generators, boolean isSealed, boolean respectPriorityRules) {
myGenerators = new ArrayList<>(generators);
myIsSealed = isSealed;
myRespectPriorityRules = respectPriorityRules;
}
@Override
public void reportInvolvedGenerators(Collection<TemplateModule> result) {
result.addAll(myGenerators);
}
@Override
public void registerIfIntersects(Collection<SModuleReference> directExtendedGenerators, TemplateModule extCandidate) {
if (myIsSealed) {
return;
}
if (myExtensions.contains(extCandidate)) {
// already seen that one
return;
}
if (Stream.concat(myGenerators.stream(), myExtensions.stream()).map(TemplateModule::getModuleReference).anyMatch(directExtendedGenerators::contains)) {
myExtensions.add(extCandidate);
}
}
@Override
public void createStep(RegularPlanBuilder planBuilder, List<Step> steps) {
Stream<TemplateModule> generators = Stream.concat(myGenerators.stream(), myExtensions.stream());
if (!myIsSealed && myRespectPriorityRules) {
GenerationPartitioner gp = new GenerationPartitioner(generators.collect(Collectors.toList()));
for (List<TemplateMappingConfiguration> tmc4Step : gp.createMappingSets()) {
steps.add(new Transform(tmc4Step));
}
} else {
ArrayList<TemplateMappingConfiguration> tmc = new ArrayList<>();
generators.flatMap(tm -> tm.getModels().stream()).map(TemplateModel::getConfigurations).forEach(tmc::addAll);
steps.add(new Transform(tmc));
}
}
}
private static class CheckpointEntry implements StepEntry {
private final CheckpointIdentity myIdentity;
private final boolean myIsSynchOnly;
CheckpointEntry(CheckpointIdentity cpIdentity, boolean isSynchOnly) {
myIdentity = cpIdentity;
myIsSynchOnly = isSynchOnly;
}
@Override
public void reportInvolvedGenerators(Collection<TemplateModule> result) {
// no-op
}
@Override
public void registerIfIntersects(Collection<SModuleReference> directExtendedGenerators, TemplateModule extCandidate) {
// no-op
}
@Override
public void createStep(RegularPlanBuilder planBuilder, List<Step> steps) {
steps.add(new Checkpoint(myIdentity, myIsSynchOnly));
}
}
private static class PreparedEntry implements StepEntry {
private final List<TemplateMappingConfiguration> myElements;
PreparedEntry(List<TemplateMappingConfiguration> tmc) {
myElements = tmc;
}
@Override
public void reportInvolvedGenerators(Collection<TemplateModule> result) {
// Report tmc's module as 'involved', effectively telling that
// generators of explicitly specified TMCs are NOT available for consideration with
// 'generators with extensions' stmt. We treat explicitly specified MC as 'LD knows what to do with a generator'
for (TemplateMappingConfiguration tmc : myElements) {
result.add(tmc.getModel().getModule());
}
}
@Override
public void registerIfIntersects(Collection<SModuleReference> directExtendedGenerators, TemplateModule extCandidate) {
// no-op
}
@Override
public void createStep(RegularPlanBuilder planBuilder, List<Step> steps) {
steps.add(new Transform(myElements));
}
}
}