/*
* 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.smodel.persistence.def.v9;
import jetbrains.mps.RuntimeFlags;
import jetbrains.mps.persistence.FilePerRootDataSource;
import jetbrains.mps.persistence.MetaModelInfoProvider;
import jetbrains.mps.persistence.registry.AggregationLinkInfo;
import jetbrains.mps.persistence.registry.AssociationLinkInfo;
import jetbrains.mps.persistence.registry.ConceptInfo;
import jetbrains.mps.persistence.registry.IdInfoRegistry;
import jetbrains.mps.persistence.registry.LangInfo;
import jetbrains.mps.persistence.registry.PropertyInfo;
import jetbrains.mps.smodel.DefaultSModel;
import jetbrains.mps.smodel.SModel;
import jetbrains.mps.smodel.SModel.ImportElement;
import jetbrains.mps.smodel.SModelHeader;
import jetbrains.mps.smodel.StaticReference;
import jetbrains.mps.smodel.adapter.ids.MetaIdHelper;
import jetbrains.mps.smodel.persistence.def.FilePerRootFormatUtil;
import jetbrains.mps.smodel.persistence.def.IModelWriter;
import jetbrains.mps.util.ToStringComparator;
import org.jdom.Document;
import org.jdom.Element;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.language.SContainmentLink;
import org.jetbrains.mps.openapi.language.SLanguage;
import org.jetbrains.mps.openapi.language.SProperty;
import org.jetbrains.mps.openapi.model.SModelReference;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.model.SNodeId;
import org.jetbrains.mps.openapi.model.SNodeUtil;
import org.jetbrains.mps.openapi.model.SReference;
import org.jetbrains.mps.openapi.module.SModuleReference;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
public class ModelWriter9 implements IModelWriter {
public static final int VERSION = 9;
private final MetaModelInfoProvider myMetaInfoProvider;
private IdInfoRegistry myMetaInfo;
private ImportsHelper myImportsHelper;
private final IdEncoder myIdEncoder = new IdEncoder();
public ModelWriter9(@NotNull MetaModelInfoProvider mmiProvider) {
myMetaInfoProvider = mmiProvider;
}
@Override
public Document saveModel(SModel sourceModel) {
myMetaInfo = new IdInfoRegistry();
new IdInfoCollector(myMetaInfo, myMetaInfoProvider).fill(sourceModel.getRootNodes());
myImportsHelper = new ImportsHelper(sourceModel.getReference());
// root element
Element rootElement = new Element(ModelPersistence9.MODEL);
rootElement.setAttribute(ModelPersistence9.REF, myIdEncoder.toText(sourceModel.getReference()));
//all model props common to one-file and per-root persistence
saveModelProperties(sourceModel, rootElement);
rootElement.addContent(buildRegistry());
// nodes
for (SNode root : sourceModel.getRootNodes()) {
rootElement.addContent(saveNode(root));
}
return new Document(rootElement);
}
private void saveModelProperties(SModel sourceModel, Element rootElement) {
// persistence tag
rootElement.addContent(createPersistenceElement());
// model properties
saveAdditionalProps(sourceModel, rootElement);
// languages
Element languagesElement = new Element(ModelPersistence9.LANGUAGES);
saveUsedLanguages(languagesElement, sourceModel);
saveEngagedLanguages(languagesElement, sourceModel);
saveDevkits(languagesElement, sourceModel);
rootElement.addContent(languagesElement);
// imports
Element importsElement = new Element(ModelPersistence9.IMPORTS);
saveImports(importsElement, sourceModel);
rootElement.addContent(importsElement);
}
private static Element createPersistenceElement() {
Element persistenceElement = new Element(ModelPersistence9.MODEL_PERSISTENCE);
persistenceElement.setAttribute(ModelPersistence9.VERSION, String.valueOf(VERSION));
return persistenceElement;
}
private Element buildRegistry() {
Element registry = new Element(ModelPersistence9.REGISTRY);
for (LangInfo ul : myMetaInfo.getLanguagesInUse()) {
Element lang = new Element(ModelPersistence9.REGISTRY_LANGUAGE);
lang.setAttribute(ModelPersistence9.ID, myIdEncoder.toText(ul.getLanguageId()));
lang.setAttribute(ModelPersistence9.NAME, ul.getName());
registry.addContent(lang);
for (ConceptInfo ci : ul.getConceptsInUse()) {
Element conceptElement = new Element(ModelPersistence9.REGISTRY_CONCEPT);
conceptElement.setAttribute(ModelPersistence9.ID, myIdEncoder.toText(ci.getConceptId()));
conceptElement.setAttribute(ModelPersistence9.NAME, ci.getName());
conceptElement.setAttribute(ModelPersistence9.FLAGS, ci.getImplementationKindText());
if (ci.isImplementationWithStub()) {
conceptElement.setAttribute(ModelPersistence9.STUB, myIdEncoder.toText(ci.getStubCounterpart()));
}
conceptElement.setAttribute(ModelPersistence9.INDEX, ci.getIndex());
for (PropertyInfo pi : ci.getPropertiesInUse()) {
Element e = new Element(ModelPersistence9.REGISTRY_PROPERTY);
e.setAttribute(ModelPersistence9.ID, myIdEncoder.toText(pi.getPropertyId()));
e.setAttribute(ModelPersistence9.NAME, pi.getName());
e.setAttribute(ModelPersistence9.INDEX, pi.getIndex());
conceptElement.addContent(e);
}
for (AssociationLinkInfo li : ci.getAssociationsInUse()) {
Element e = new Element(ModelPersistence9.REGISTRY_ASSOCIATION);
e.setAttribute(ModelPersistence9.ID, myIdEncoder.toText(li.getLinkId()));
e.setAttribute(ModelPersistence9.NAME, li.getName());
e.setAttribute(ModelPersistence9.INDEX, li.getIndex());
conceptElement.addContent(e);
}
for (AggregationLinkInfo li : ci.getAggregationsInUse()) {
Element e = new Element(ModelPersistence9.REGISTRY_AGGREGATION);
e.setAttribute(ModelPersistence9.ID, myIdEncoder.toText(li.getLinkId()));
e.setAttribute(ModelPersistence9.NAME, li.getName());
e.setAttribute(ModelPersistence9.INDEX, li.getIndex());
if (li.isUnordered()) {
e.setAttribute(ModelPersistence9.UNORDERED, Boolean.TRUE.toString());
}
conceptElement.addContent(e);
}
lang.addContent(conceptElement);
}
}
return registry;
}
private void saveAdditionalProps(SModel sourceModel, Element rootElement) {
if (!(sourceModel instanceof DefaultSModel)) return;
SModelHeader header = ((DefaultSModel) sourceModel).getSModelHeader();
if (header.isDoNotGenerate()) {
rootElement.setAttribute(SModelHeader.DO_NOT_GENERATE, Boolean.TRUE.toString());
}
for (Map.Entry<String, String> en : header.getOptionalProperties().entrySet()) {
Element attr = new Element(ModelPersistence9.MODEL_ATTRIBUTE);
attr.setAttribute(ModelPersistence9.NAME, en.getKey());
attr.setAttribute(ModelPersistence9.VALUE, en.getValue());
rootElement.addContent(attr);
}
}
private void saveImports(Element rootElement, SModel sourceModel) {
Set<SModelReference> crossModelReferences = collectCrossModelReferences(sourceModel);
for (ImportElement importElement : sourceModel.importedModels()) {
SModelReference modelRef = importElement.getModelReference();
crossModelReferences.remove(modelRef);
final String index = myImportsHelper.addModelImport(modelRef);
rootElement.addContent(createImportElement(modelRef, index, false));
}
SModelReference[] implicitImports = crossModelReferences.toArray(new SModelReference[crossModelReferences.size()]);
Arrays.sort(implicitImports, new ToStringComparator());
for (SModelReference implicitImport : crossModelReferences) {
final String index = myImportsHelper.addModelImport(implicitImport);
rootElement.addContent(createImportElement(implicitImport, index, true));
}
}
private Set<SModelReference> collectCrossModelReferences(SModel model) {
// Would be nice to re-use existing code, but don't have openapi.SModel here, unfortunately
// ModelDependencyScanner depScan = new ModelDependencyScanner().crossModelReferences(true).usedLanguages(false);
// depScan.walk(sourceModel);
Set<SModelReference> crossModelRefs = new LinkedHashSet<SModelReference>();
for (SNode r : model.getRootNodes()) {
for (SNode n : SNodeUtil.getDescendants(r)) {
for (SReference ref : n.getReferences()) {
SModelReference target = ref.getTargetSModelReference();
if (target != null) {
crossModelRefs.add(target);
}
}
}
}
crossModelRefs.remove(myImportsHelper.localModel());
return crossModelRefs;
}
private Element createImportElement(SModelReference modelRef, String index, boolean implicit) {
Element elem = new Element(ModelPersistence9.IMPORT);
elem.setAttribute(ModelPersistence9.INDEX, index);
elem.setAttribute(ModelPersistence9.REF, myIdEncoder.toText(modelRef));
if (implicit) {
elem.setAttribute(ModelPersistence9.IMPLICIT, Boolean.toString(true));
}
return elem;
}
private void saveDevkits(Element rootElement, SModel sourceModel) {
for (SModuleReference ref : sourceModel.importedDevkits()) {
Element devkitElem = new Element(ModelPersistence9.DEVKIT);
devkitElem.setAttribute(ModelPersistence9.REF, myIdEncoder.toText(ref));
rootElement.addContent(devkitElem);
}
}
private void saveEngagedLanguages(Element rootElement, SModel sourceModel) {
for (SLanguage l : sourceModel.getLanguagesEngagedOnGeneration()) {
Element languageElem = new Element(ModelPersistence9.ENGAGE_LANGUAGE);
languageElem.setAttribute(ModelPersistence9.ID, myIdEncoder.toText(MetaIdHelper.getLanguage(l)));
languageElem.setAttribute(ModelPersistence9.NAME, l.getQualifiedName());
rootElement.addContent(languageElem);
}
}
private void saveUsedLanguages(Element rootElement, SModel sourceModel) {
for (SLanguage l : sourceModel.usedLanguages()) {
Element languageElem = new Element(ModelPersistence9.USED_LANGUAGE);
languageElem.setAttribute(ModelPersistence9.ID, myIdEncoder.toText(MetaIdHelper.getLanguage(l)));
// although there's name of the language in the registry, don't want to rely on it:
// (a) the language might not be necessarily in actual use (thus registry won't list it)
// (b) in multi-stream persistence, registry is saved per-root, while usedLanguages are saved once for header stream
// (c) such a relation would require registry to be written and read before usedLanguages - just an extra thing to worry about
// Perhaps, once SLanguageAdapterById would tolerate null lang name (i.e we switch to ids completely), this attribute can be thrown away
languageElem.setAttribute(ModelPersistence9.NAME, l.getQualifiedName());
languageElem.setAttribute(ModelPersistence9.VERSION, Integer.toString(sourceModel.getLanguageImportVersion(l)));
rootElement.addContent(languageElem);
}
}
private Element saveNode(SNode node) {
Element nodeElement = new Element(ModelPersistence9.NODE);
final ConceptInfo conceptInfo = myMetaInfo.find(node.getConcept());
nodeElement.setAttribute(ModelPersistence9.CONCEPT_ID, conceptInfo.getIndex());
nodeElement.setAttribute(ModelPersistence9.ID, myIdEncoder.toText(node.getNodeId()));
final SContainmentLink roleInParent = node.getContainmentLink();
if (roleInParent != null) {
final AggregationLinkInfo aggregationLinkInfo = myMetaInfo.find(roleInParent);
setNotNullAttribute(nodeElement, ModelPersistence9.ROLE_ID, aggregationLinkInfo.getIndex());
}
for (SProperty pid : node.getProperties()) {
Element propertyElement = new Element(ModelPersistence9.NODE_PROPERTY);
final PropertyInfo propertyInfo = myMetaInfo.find(pid);
propertyElement.setAttribute(ModelPersistence9.ROLE_ID, propertyInfo.getIndex());
setNotNullAttribute(propertyElement, ModelPersistence9.VALUE, node.getProperty(pid));
nodeElement.addContent(propertyElement);
}
for (SReference reference : node.getReferences()) {
Element linkElement = new Element(ModelPersistence9.NODE_REFERENCE);
final AssociationLinkInfo associationLinkInfo = myMetaInfo.find(reference.getLink());
linkElement.setAttribute(ModelPersistence9.ROLE_ID, associationLinkInfo.getIndex());
final SModelReference targetModel = reference.getTargetSModelReference();
if (targetModel != null && myImportsHelper.isLocal(targetModel)) {
linkElement.setAttribute(ModelPersistence9.NODE, myIdEncoder.toTextLocal(reference));
} else {
linkElement.setAttribute(ModelPersistence9.TO, myIdEncoder.toTextExternal(myImportsHelper, reference));
}
setNotNullAttribute(linkElement, ModelPersistence9.RESOLVE, genResolveInfo(reference));
nodeElement.addContent(linkElement);
}
for (SNode childNode : node.getChildren()) {
nodeElement.addContent(saveNode(childNode));
}
return nodeElement;
}
@Override
public Map<String, Document> saveModelAsMultiStream(SModel sourceModel) {
myImportsHelper = new ImportsHelper(sourceModel.getReference()); // saveModelProperties->saveImports fills it
// header
Element headerRoot = new Element(ModelPersistence9.MODEL);
headerRoot.setAttribute(ModelPersistence9.REF, myIdEncoder.toText(sourceModel.getReference()));
headerRoot.setAttribute(ModelPersistence9.FILE_CONTENT, "header");
saveModelProperties(sourceModel, headerRoot);
final ImportsHelper wholeModelImports = myImportsHelper;
Map<String, Document> result = new HashMap<String, Document>();
result.put(FilePerRootDataSource.HEADER_FILE, new Document(headerRoot));
// roots
Map<SNodeId, String> rootToFile = FilePerRootFormatUtil.getStreamNames(sourceModel);
for (SNode root : sourceModel.getRootNodes()) {
Element rootElement = new Element(ModelPersistence9.MODEL);
rootElement.setAttribute(ModelPersistence9.REF, myIdEncoder.toText(sourceModel.getReference()));
rootElement.setAttribute(ModelPersistence9.FILE_CONTENT, "root");
rootElement.addContent(createPersistenceElement());
// collect imports of this particular root
final LinkedHashSet<SModelReference> usedImports = new LinkedHashSet<SModelReference>();
myImportsHelper = new ImportsHelper(sourceModel.getReference()) {
@Override
public String getIndex(@NotNull SModelReference modelReference) {
usedImports.add(modelReference);
return wholeModelImports.getIndex(modelReference);
}
};
// and its meta-info
myMetaInfo = new IdInfoRegistry();
new IdInfoCollector(myMetaInfo, myMetaInfoProvider).fill(Collections.singleton(root));
Element childElement = saveNode(root);
// record model imports
Element importsElement = new Element(ModelPersistence9.IMPORTS);
for (SModelReference modelRef : usedImports) {
importsElement.addContent(createImportElement(modelRef, wholeModelImports.getIndex(modelRef), true));
}
// used languages could be implicitly derived from the registry of used concepts
rootElement.addContent(importsElement);
// record meta-info registry for the given root
rootElement.addContent(buildRegistry());
//add root node, it should be added after imports/languages sections
rootElement.addContent(childElement);
result.put(rootToFile.get(root.getNodeId()), new Document(rootElement));
}
return result;
}
private static String genResolveInfo(@NotNull SReference ref) {
if (!(RuntimeFlags.isMergeDriverMode())) {
SNode target = (ref instanceof StaticReference ? ref.getTargetNode() : null);
if ((target != null)) {
String resolveInfo = jetbrains.mps.util.SNodeOperations.getResolveInfo(target);
if (resolveInfo != null) {
return resolveInfo;
}
}
}
return ((jetbrains.mps.smodel.SReference) ref).getResolveInfo();
}
public static void setNotNullAttribute(
@NotNull Element element,
@NotNull String attrName,
@Nullable String attrValue) {
if (attrValue != null) {
element.setAttribute(attrName, attrValue);
}
}
}