/*
* 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.generator.impl;
import jetbrains.mps.generator.IGeneratorLogger;
import jetbrains.mps.generator.IGeneratorLogger.ProblemDescription;
import jetbrains.mps.generator.impl.cache.BrokenCacheException;
import jetbrains.mps.generator.impl.cache.MappingsMemento;
import jetbrains.mps.generator.impl.cache.TransientModelWithMetainfo;
import jetbrains.mps.generator.impl.dependencies.DependenciesBuilder;
import jetbrains.mps.generator.runtime.TemplateContext;
import jetbrains.mps.util.Pair;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.model.SNodeId;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;
/**
* Runtime state of mapping labels at some transformation step.
* {@link MappingsMemento} is persistence-friendly companion.
*
* Evgeny Gryaznov, Feb 16, 2010
*/
public final class GeneratorMappings {
private final IGeneratorLogger myLog;
/* mapping,input -> output */
private final ConcurrentMap<String, Map<SNode, Object>> myMappingNameAndInputNodeToOutputNodeMap = new ConcurrentHashMap<String, Map<SNode, Object>>();
/* input -> output */
private final ConcurrentMap<SNode, Object> myCopiedOutputNodeForInputNode = new ConcurrentHashMap<SNode, Object>();
/* new style map: Object means multiple nodes for the template */
private final ConcurrentMap<String, Object> myTemplateNodeIdToOutputNodeMap = new ConcurrentHashMap<String, Object>();
/* new style map: template,input -> output */
private final ConcurrentMap<Pair<String, SNode>, SNode> myTemplateNodeIdAndInputNodeToOutputNodeMap = new ConcurrentHashMap<Pair<String, SNode>, SNode>();
/*
* there might be few conditional roots, and we can't prevent them from using same ML (not too much sense, however)
*/
private final CopyOnWriteArrayList<Pair<String, SNode>> myConditionalRoots = new CopyOnWriteArrayList<>();
public GeneratorMappings(IGeneratorLogger log) {
myLog = log;
}
// add methods
void addOutputNodeByTemplateNode(String templateNodeId, @NotNull SNode outputNode) {
if (myTemplateNodeIdToOutputNodeMap.putIfAbsent(templateNodeId, outputNode) != null) {
myTemplateNodeIdToOutputNodeMap.put(templateNodeId, this);
}
}
void addOutputNodeByInputNodeAndMappingName(SNode inputNode, String mappingName, SNode outputNode) {
if (mappingName == null) {
return;
}
Map<SNode, Object> currentMapping = myMappingNameAndInputNodeToOutputNodeMap.get(mappingName);
if (currentMapping == null) {
myMappingNameAndInputNodeToOutputNodeMap.putIfAbsent(mappingName, new HashMap<SNode, Object>());
currentMapping = myMappingNameAndInputNodeToOutputNodeMap.get(mappingName);
}
synchronized (currentMapping) {
Object o = currentMapping.get(inputNode);
if (o == null) {
currentMapping.put(inputNode, outputNode);
} else if (o instanceof List) {
((List<SNode>) o).add(outputNode);
} else if (o != outputNode) {
List<SNode> list = new ArrayList<SNode>(4);
list.add((SNode) o);
list.add(outputNode);
currentMapping.put(inputNode, list);
} else {
// TODO warning
}
}
}
/**
* record a newly created node (generally, conditional root rule - no input node)
* @param mappingLabel label
* @param outputNode new node
*/
void addNewOutputNode(String mappingLabel, SNode outputNode) {
if (mappingLabel == null || outputNode == null) {
return;
}
myConditionalRoots.add(new Pair<>(mappingLabel, outputNode));
}
void addCopiedOutputNodeForInputNode(SNode inputNode, SNode outputNode) {
if (outputNode == null) {
return;
}
Object prev = myCopiedOutputNodeForInputNode.putIfAbsent(inputNode, outputNode);
// It's possible for the same pair input->output to get registered more than once,
// e.g. when a rule does COPY-SRC for input. Both the copy macro and the rule would
// try to register same output, and we shall not treat this as ambiguity. Generally it's
// odd to use COPY-SRC for rule's primary transformation, we've had only 1 use like this
// (build language, mapBuildProject did $WEAVE$ $COPY-SRC$).
if (prev != null && prev instanceof SNode && prev != outputNode) {
// ambiguity! store prev element (wrapped into Collection)
myCopiedOutputNodeForInputNode.put(inputNode, Collections.singletonList(prev));
}
}
void addOutputNodeByInputAndTemplateNode(SNode inputNode, String templateNodeId, SNode outputNode) {
// todo: combination of (templateN, inputN) -> outputN
// todo: is not unique
// todo: generator should report error on attempt to obtain not unique output-node
if (templateNodeId == null) {
return;
}
myTemplateNodeIdAndInputNodeToOutputNodeMap.put(new Pair<>(templateNodeId, inputNode), outputNode);
}
void addOutputNodeForContext(TemplateContext templateContext, String templateNodeId, SNode outputNode) {
// todo: combination of (templateN, inputN) -> outputN
// todo: is not unique
// todo: generator should report error on attempt to obtain not unique output-node
addOutputNodeByInputAndTemplateNode(templateContext.getInput(), templateNodeId, outputNode);
for (SNode historyInputNode : templateContext.getInputHistory()) {
Pair<String,SNode> key = new Pair<>(templateNodeId, historyInputNode);
myTemplateNodeIdAndInputNodeToOutputNodeMap.putIfAbsent(key, outputNode);
}
addOutputNodeByTemplateNode(templateNodeId, outputNode);
}
// find methods
public SNode findOutputNodeByTemplateNodeUnique(String templateNode) {
Object o = myTemplateNodeIdToOutputNodeMap.get(templateNode);
return o instanceof SNode ? (SNode) o : null;
}
public SNode findOutputNodeByInputNodeAndMappingName(@Nullable SNode inputNode, @Nullable String mappingName) {
if (mappingName == null) {
return null;
}
Map<SNode, Object> currentMapping = myMappingNameAndInputNodeToOutputNodeMap.get(mappingName);
if (currentMapping == null) {
return null;
}
Object o = currentMapping.get(inputNode);
if (o instanceof List) {
List<SNode> list = (List<SNode>) o;
ProblemDescription[] descr = new ProblemDescription[list.size() + 1];
for (int i = 0; i < list.size(); i++) {
descr[i] = GeneratorUtil.describe(list.get(i), "output");
}
descr[list.size()] = GeneratorUtil.describe(inputNode, "input");
String msg = "%d output nodes found for mapping label '%s'";
myLog.warning(inputNode == null ? null : inputNode.getReference(), String.format(msg, list.size(), mappingName), descr);
return list.get(0);
}
return (SNode) o;
}
public List<SNode> findAllOutputNodesByInputNodeAndMappingName(SNode inputNode, String mappingName) {
if (mappingName == null) {
return null;
}
Map<SNode, Object> currentMapping = myMappingNameAndInputNodeToOutputNodeMap.get(mappingName);
if (currentMapping == null) {
return null;
}
Object o = currentMapping.get(inputNode);
if (o == null) {
return Collections.emptyList();
}
if (o instanceof List) {
return ((List<SNode>) o);
}
return Collections.singletonList((SNode) o);
}
public SNode findCopiedOutputNodeForInputNode(@NotNull SNode inputNode) {
Object o = myCopiedOutputNodeForInputNode.get(inputNode);
if (o instanceof SNode) {
return (SNode) o;
}
if (o instanceof List) {
return (SNode) ((List) o).get(0);
}
return null;
}
public SNode findOutputNodeByInputAndTemplateNode(SNode inputNode, String templateNodeId) {
return myTemplateNodeIdAndInputNodeToOutputNodeMap.get(new Pair<>(templateNodeId, inputNode));
}
public boolean isInputNodeHasUniqueCopiedOutputNode(SNode inputNode) {
Object o = myCopiedOutputNodeForInputNode.get(inputNode);
return !(o instanceof List);
}
@Nullable
public SNode findNewOutputNode(@Nullable String mappingLabel) {
if (mappingLabel == null) {
// all other methods tolerate null parameters, why this one would not?
return null;
}
return myConditionalRoots.stream().filter(p -> mappingLabel.equals(p.o1)).findFirst().map(p -> p.o2).orElse(null);
}
// expose internal structure, to build GeneratorDebug_Mappings with MPS-coded DebugMappingsBuilder
/*package*/ Collection<String> getAvailableLabels() {
return myMappingNameAndInputNodeToOutputNodeMap.keySet();
}
/*package*/Map<SNode,Object> getMappings(String label) {
return myMappingNameAndInputNodeToOutputNodeMap.get(label);
}
/*package*/Set<String> getConditionalRootLabels() {
return myConditionalRoots.stream().map(p -> p.o1).collect(Collectors.toSet());
}
/*package*/List<SNode> getConditionalRoots(String label) {
return myConditionalRoots.stream().filter(p -> label.equals(p.o1)).map(p -> p.o2).collect(Collectors.toList());
}
// serialization
public void export(TransientModelWithMetainfo model, DependenciesBuilder builder) {
for (Entry<String, Map<SNode, Object>> o : myMappingNameAndInputNodeToOutputNodeMap.entrySet()) {
String label = o.getKey();
for (Entry<SNode, Object> i : o.getValue().entrySet()) {
SNode inputNode = i.getKey();
SNode originalRoot = inputNode == null ? null : builder.getOriginalForInput(inputNode.getContainingRoot());
MappingsMemento mappingsMemento = model.getMappingsMemento(originalRoot, true);
mappingsMemento.addOutputNodeByInputNodeAndMappingName(inputNode == null ? null : inputNode.getNodeId(), label, i.getValue());
}
}
for (Entry<SNode, Object> o : myCopiedOutputNodeForInputNode.entrySet()) {
SNode inputNode = o.getKey();
Object value = o.getValue();
if (value instanceof SNode) {
SNodeId targetId = ((SNode) value).getNodeId();
if (inputNode.getNodeId().equals(targetId)) {
continue; /* trivial */
}
MappingsMemento mappingsMemento = model.getMappingsMemento(builder.getOriginalForInput(inputNode.getContainingRoot()), true);
mappingsMemento.addOutputNodeByInputNode(inputNode.getNodeId(), targetId, true);
} else if (value instanceof List) {
SNodeId targetId = ((List<SNode>) value).get(0).getNodeId();
MappingsMemento mappingsMemento = model.getMappingsMemento(builder.getOriginalForInput(inputNode.getContainingRoot()), true);
mappingsMemento.addOutputNodeByInputNode(inputNode.getNodeId(), targetId, false);
}
}
}
/**
* Record MLs into checkpoint state, assuming output nodes of the mappings are from the model being marked as 'checkpoint',
* and input nodes being traced with transitionTrace
*/
public void export(CheckpointStateBuilder cp) {
for (Entry<String, Map<SNode, Object>> o : myMappingNameAndInputNodeToOutputNodeMap.entrySet()) {
String label = o.getKey();
for (Entry<SNode, Object> i : o.getValue().entrySet()) {
SNode inputNode = i.getKey();
if (inputNode == null) {
// FIXME shall I track nodes newly introduced at the given checkpoint step? Yes.
// However, for newly introduced nodes we keep separate collection, and there should be no
// null input nodes in this map. The check left just in case there's one (no check for null input in addOutput.. yet).
continue;
}
// perhaps, would be useful to record mappings even without original node (marked as 'useless')
// to ease debug (once there's mechanism to show mapping labels as part of transient model/module)
Object value = i.getValue();
if (value instanceof SNode) {
cp.record(inputNode, label, (SNode) value);
} else if (value instanceof Collection) {
@SuppressWarnings("unchecked")
Collection<SNode> collection = (Collection<SNode>) value;
cp.record(inputNode, label, collection);
}
}
}
myConditionalRoots.forEach(p -> cp.record(p.o1, p.o2));
}
public void importPersisted(MappingsMemento val, SModel inputModel, SModel outputModel) throws BrokenCacheException {
// labels
for (Entry<String, Map<SNodeId, Object>> e : val.getMappingNameAndInputNodeToOutputNodeMap().entrySet()) {
String mappingName = e.getKey();
Map<SNode, Object> currentMapping = myMappingNameAndInputNodeToOutputNodeMap.get(mappingName);
if (currentMapping == null) {
myMappingNameAndInputNodeToOutputNodeMap.putIfAbsent(mappingName, new HashMap<SNode, Object>());
currentMapping = myMappingNameAndInputNodeToOutputNodeMap.get(mappingName);
}
for (Entry<SNodeId, Object> n : e.getValue().entrySet()) {
SNodeId key = n.getKey();
SNode inputNode = null;
if (key != null) {
inputNode = inputModel.getNode(key);
if (inputNode == null) {
continue;
}
}
Object value = n.getValue();
if (value instanceof SNodeId) {
SNode outputNode = outputModel.getNode((SNodeId) value);
if (outputNode == null) {
continue;
}
addOutputNode(currentMapping, inputNode, outputNode);
} else if (value instanceof List) {
for (SNodeId id : (List<SNodeId>) value) {
SNode outputNode = outputModel.getNode(id);
if (outputNode == null) {
continue;
}
addOutputNode(currentMapping, inputNode, outputNode);
}
}
}
}
// output for input
for (Entry<SNodeId, Object> e : val.getCopiedOutputNodeForInputNode().entrySet()) {
SNode inputNode = inputModel.getNode(e.getKey());
if (inputNode == null) {
continue;
}
Object value = e.getValue();
if (value instanceof SNodeId) {
SNode outputNode = outputModel.getNode((SNodeId) value);
if (outputNode == null) {
continue;
}
myCopiedOutputNodeForInputNode.put(inputNode, outputNode);
} else if (value instanceof List) {
SNode outputNode = outputModel.getNode(((List<SNodeId>) value).get(0));
if (outputNode == null) {
continue;
}
myCopiedOutputNodeForInputNode.put(inputNode, Collections.singletonList(outputNode));
}
}
}
private void addOutputNode(@NotNull Map<SNode, Object> currentMapping, SNode inputNode, @NotNull SNode outputNode) throws BrokenCacheException {
Object o = currentMapping.get(inputNode);
if (o == null) {
currentMapping.put(inputNode, outputNode);
} else if (o instanceof List) {
((List<SNode>) o).add(outputNode);
} else if (o != outputNode) {
List<SNode> list = new ArrayList<SNode>(4);
list.add((SNode) o);
list.add(outputNode);
currentMapping.put(inputNode, list);
} else {
// TODO ?
}
}
}