/*
* 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.ide.devkit.generator;
import jetbrains.mps.generator.GenerationSettingsProvider;
import jetbrains.mps.generator.GenerationTrace;
import jetbrains.mps.generator.IGenerationSettings.GenTraceSettings;
import jetbrains.mps.ide.devkit.generator.TraceNodeUI.Kind;
import jetbrains.mps.util.Pair;
import jetbrains.mps.util.SNodeOperations;
import jetbrains.mps.util.containers.MultiMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.model.SModelName;
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.SRepository;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.Set;
/**
* Visitor to compose trace tree. Either resort to
* {@link #buildBackTrace(GenerationTrace, SNode, SRepository)} and
* {@link #buildTrace(GenerationTrace, SNode, SRepository)} default builders, or use directly:
* <pre>
* GenerationTrace trace = ...;
* TraceBuilderUI builder = new TraceBulderUI();
* builder.excludeEmptySteps(careAboutStepsWithoutChanges).furtherConfig(...);
* trace.walkForward(builder);
* myViewComponent.show(builder.getResult());
* </pre>
*
* Default builders resort to values from {@link jetbrains.mps.generator.IGenerationSettings}.
*
* Note, the builder assumes current thread holds read lock.
* @author Artem Tikhomirov
*/
public class TraceBuilderUI implements GenerationTrace.Visitor {
private Collection<TraceNodeUI> myResult;
private TraceNodeUI myStepNode;
private StepChanges myStepChange;
private boolean myExcludeEmptySteps = true;
private boolean myCompactTemplates = false;
private boolean myGroupByStep = true;
private final SRepository myTemplateSources;
private enum NodeGrouping { Change, Input, Output }
private NodeGrouping myChangeGrouping = NodeGrouping.Change;
/**
* @param templateSources repository with source models for templates that produced trace being visualized.
* Not necessarily the same as the one transient nodes come from.
*/
public TraceBuilderUI(@Nullable SRepository templateSources) {
myTemplateSources = templateSources;
myResult = new ArrayList<TraceNodeUI>();
}
public TraceBuilderUI excludeEmptySteps(boolean excludeEmptySteps) {
myExcludeEmptySteps = excludeEmptySteps;
return this;
}
/**
* @param compactTemplates {@code true} make sense only if there are source models for templates are available (and repository they reside in was
* an argument in {@link #TraceBuilderUI(SRepository)}.
*/
public TraceBuilderUI compactTemplates(boolean compactTemplates) {
myCompactTemplates = compactTemplates;
return this;
}
public TraceBuilderUI groupByStep(boolean groupByStep) {
myGroupByStep = groupByStep;
return this;
}
public TraceBuilderUI groupByInputNode() {
myChangeGrouping = NodeGrouping.Input;
return this;
}
public TraceBuilderUI groupByOutputNode() {
myChangeGrouping = NodeGrouping.Output;
return this;
}
/*package*/ Collection<TraceNodeUI> getResult() {
closeStepNode();
ArrayList<TraceNodeUI> rv = new ArrayList<TraceNodeUI>(myResult.size());
if (myExcludeEmptySteps) {
for (TraceNodeUI n : myResult) {
if (n.hasChildren()) {
rv.add(n);
}
}
return rv;
} else {
return myResult;
}
}
@Override
public void beginStep(@NotNull SModelReference input, @NotNull SModelReference output) {
final SModelName modelName1 = input.getName();
final SModelName modelName2 = output.getName();
final String from;
final String to;
if (modelName1.getLongName().equals(modelName2.getLongName())) {
from = modelName1.getStereotype();
to = modelName2.getStereotype();
} else {
from = modelName1.getValue();
to = modelName2.getValue();
}
myStepNode = new TraceNodeUI(String.format("Phase %s->%s", from, to), Icons.COLLECTION, null);
myStepChange = new StepChanges();
}
@Override
public void endStep(@NotNull SModelReference input, @NotNull SModelReference output) {
closeStepNode();
}
@Override
public void change(@NotNull SNodeReference input, @NotNull SNodeReference output, @NotNull SNodeReference template) {
myStepChange.record(input, output, template);
}
private void closeStepNode() {
if (myStepNode == null) {
return;
}
switch (myChangeGrouping) {
case Change:
addIndividualChanges();
break;
case Input:
addChangesByInput();
break;
case Output:
addChangesByOutput();
break;
}
if (myGroupByStep) {
myResult.add(myStepNode);
} else {
for (TraceNodeUI n : myStepNode.getChildren()) {
myResult.add(n);
}
}
myStepNode = null;
}
private void addIndividualChanges() {
for (Pair<SNodeReference,SNodeReference> p : myStepChange.individualChanges()) {
TraceNodeUI in = new TraceNodeUI(TraceNodeUI.Kind.INPUT, p.o1);
TraceNodeUI out = new TraceNodeUI(TraceNodeUI.Kind.OUTPUT, p.o2);
for (TraceNodeUI templates : compactTemplates(myStepChange.templates(p))) {
in.addChild(templates);
}
in.addChild(out);
myStepNode.addChild(in);
}
}
private void addChangesByInput() {
for (SNodeReference in : myStepChange.inputs()) {
TraceNodeUI inNode = new TraceNodeUI(Kind.INPUT, in);
for (SNodeReference out : myStepChange.outputForInput(in)) {
inNode.addChild(new TraceNodeUI(Kind.OUTPUT, out));
}
for (TraceNodeUI templateNode : compactTemplates(myStepChange.templatesForInput(in))) {
inNode.addChild(templateNode);
}
myStepNode.addChild(inNode);
}
}
private void addChangesByOutput() {
for (SNodeReference out : myStepChange.outputs()) {
TraceNodeUI outNode = new TraceNodeUI(Kind.OUTPUT, out);
for (SNodeReference in : myStepChange.inputsForOutput(out)) {
outNode.addChild(new TraceNodeUI(Kind.INPUT, in));
}
for (TraceNodeUI templateNode : compactTemplates(myStepChange.templatesForOutput(out))) {
outNode.addChild(templateNode);
}
myStepNode.addChild(outNode);
}
}
private Collection<TraceNodeUI> compactTemplates(Collection<SNodeReference> templateNodes) {
ArrayList<TraceNodeUI> rv = new ArrayList<TraceNodeUI>();
if (!myCompactTemplates || myTemplateSources == null) {
for (SNodeReference r : templateNodes) {
rv.add(new TraceNodeUI(Kind.TEMPLATE, r));
}
return rv;
}
// compactByNavigateTarget();
LinkedHashSet<SNode> mostSpecificTemplates = new LinkedHashSet<SNode>();
L1: for (SNodeReference t : templateNodes) {
SNode templateNode = t == null ? null : t.resolve(myTemplateSources);
if (templateNode == null) {
rv.add(new TraceNodeUI(Kind.TEMPLATE, t));
continue;
}
for (SNode tn : new ArrayList<SNode>(mostSpecificTemplates)) {
if (tn.getContainingRoot() == templateNode.getContainingRoot()) {
// within same hierarchy
if (SNodeOperations.isAncestor(tn, templateNode)) {
// templateNode is more specific template than the one we already got in mostSpecificTemplates
mostSpecificTemplates.remove(tn);
mostSpecificTemplates.add(templateNode);
continue L1;
} else if (SNodeOperations.isAncestor(templateNode, tn)) {
// templateNode is enclosing template, forget it
continue L1;
}// else unrelated, two independent descendants, continue looking through most specific templates found.
}
}
// no related templates found, record present one
mostSpecificTemplates.add(templateNode);
}
for (SNode n : mostSpecificTemplates) {
rv.add(new TraceNodeUI(Kind.TEMPLATE, n.getReference()));
}
return rv;
}
/**
* Handy default forward trace composer.
*/
public static Collection<TraceNodeUI> buildTrace(@NotNull GenerationTrace trace, @NotNull SNode node, @Nullable SRepository templateSources) {
final TraceBuilderUI v = defaults(new TraceBuilderUI(templateSources));
if (!GenerationSettingsProvider.getInstance().getGenerationSettings().getTraceSettings().isGroupByChange()) {
v.groupByInputNode();
}
trace.walkForward(node, v);
return v.getResult();
}
public static Collection<TraceNodeUI> buildBackTrace(@NotNull GenerationTrace trace, @NotNull final SNode node, @Nullable SRepository templateSources) {
final TraceBuilderUI v = defaults(new TraceBuilderUI(templateSources));
if (!GenerationSettingsProvider.getInstance().getGenerationSettings().getTraceSettings().isGroupByChange()) {
v.groupByOutputNode();
}
trace.walkBackward(node, v);
return v.getResult();
}
/**
* populate builder with settings from {@link GenerationSettingsProvider}
* @param builder
* @return
*/
public static TraceBuilderUI defaults(TraceBuilderUI builder) {
GenTraceSettings s = GenerationSettingsProvider.getInstance().getGenerationSettings().getTraceSettings();
return builder.excludeEmptySteps(!s.isShowEmptySteps()).compactTemplates(s.isCompactTemplates()).groupByStep(s.isGroupByStep());
}
private static class StepChanges {
private final MultiMap<Pair<SNodeReference, SNodeReference>, SNodeReference> myGroupedChanges;
private final Set<SNodeReference> myInputs, myOutputs;
public StepChanges() {
myGroupedChanges = new MultiMap<>();
myInputs = new LinkedHashSet<>();
myOutputs = new LinkedHashSet<>();
}
public void record(@NotNull SNodeReference input, @NotNull SNodeReference output, @NotNull SNodeReference template) {
myGroupedChanges.putValue(new Pair<>(input, output), template);
myInputs.add(input);
myOutputs.add(output);
}
public Collection<Pair<SNodeReference, SNodeReference>> individualChanges() {
return myGroupedChanges.keySet();
}
public Collection<SNodeReference> templates(Pair<SNodeReference, SNodeReference> change) {
return myGroupedChanges.get(change);
}
public Collection<SNodeReference> inputs() {
return myInputs;
}
public Collection<SNodeReference> outputs() {
return myOutputs;
}
public Collection<SNodeReference> outputForInput(SNodeReference in) {
assert myInputs.contains(in);
LinkedHashSet<SNodeReference> rv = new LinkedHashSet<SNodeReference>();
for (Pair<SNodeReference, SNodeReference> change : filter(in, null)) {
rv.add(change.o2);
}
assert !rv.isEmpty();
return rv;
}
public Collection<SNodeReference> inputsForOutput(SNodeReference out) {
assert myOutputs.contains(out);
LinkedHashSet<SNodeReference> rv = new LinkedHashSet<SNodeReference>();
for (Pair<SNodeReference, SNodeReference> change : filter(null, out)) {
rv.add(change.o1);
}
assert !rv.isEmpty();
return rv;
}
public Collection<SNodeReference> templatesForInput(SNodeReference in) {
assert myInputs.contains(in);
return templates(filter(in, null));
}
public Collection<SNodeReference> templatesForOutput(SNodeReference out) {
assert myOutputs.contains(out);
return templates(filter(null, out));
}
private Collection<SNodeReference> templates(Collection<Pair<SNodeReference, SNodeReference>> filter) {
LinkedHashSet<SNodeReference> rv = new LinkedHashSet<SNodeReference>();
for (Pair<SNodeReference, SNodeReference> change : filter) {
rv.addAll(myGroupedChanges.get(change));
}
assert filter.isEmpty() || !rv.isEmpty();
return rv;
}
// if both in and out set, treated as OR
private Collection<Pair<SNodeReference, SNodeReference>> filter(SNodeReference in, SNodeReference out) {
ArrayList<Pair<SNodeReference, SNodeReference>> rv = new ArrayList<Pair<SNodeReference, SNodeReference>>();
for (Pair<SNodeReference, SNodeReference> change : myGroupedChanges.keySet()) {
boolean match1 = in != null && change.o1.equals(in);
boolean match2 = out != null && change.o2.equals(out);
if (match1 || match2) {
rv.add(change);
}
}
return rv;
}
}
}