/*
* 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.persistence;
import jetbrains.mps.logging.Logger;
import jetbrains.mps.persistence.registry.ConceptInfo;
import jetbrains.mps.smodel.DebugRegistry;
import jetbrains.mps.smodel.ModelAccess;
import jetbrains.mps.smodel.adapter.ids.MetaIdFactory;
import jetbrains.mps.smodel.adapter.ids.MetaIdHelper;
import jetbrains.mps.smodel.adapter.ids.SConceptId;
import jetbrains.mps.smodel.adapter.ids.SContainmentLinkId;
import jetbrains.mps.smodel.adapter.ids.SLanguageId;
import jetbrains.mps.smodel.adapter.ids.SPropertyId;
import jetbrains.mps.smodel.adapter.ids.SReferenceLinkId;
import jetbrains.mps.smodel.adapter.structure.MetaAdapterFactoryByName;
import jetbrains.mps.smodel.language.ConceptRegistryUtil;
import jetbrains.mps.smodel.language.LanguageRegistry;
import jetbrains.mps.smodel.language.LanguageRuntime;
import jetbrains.mps.smodel.runtime.ConceptDescriptor;
import jetbrains.mps.smodel.runtime.ConceptKind;
import jetbrains.mps.smodel.runtime.LinkDescriptor;
import jetbrains.mps.smodel.runtime.PropertyDescriptor;
import jetbrains.mps.smodel.runtime.ReferenceDescriptor;
import jetbrains.mps.smodel.runtime.StaticScope;
import jetbrains.mps.util.annotation.ToRemove;
import org.apache.log4j.LogManager;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.language.SConcept;
import org.jetbrains.mps.openapi.model.SModelReference;
import org.jetbrains.mps.openapi.persistence.ModelLoadingOption;
import java.util.HashMap;
import java.util.Map;
/**
* PROVISIONAL API, DO NOT USE
* <p/>
* This class hides mechanism to access aspects of meta-model we need for persistence purposes.
* Generally, this information could be retrieved from SConcept/ConceptDescriptor and friends, however,
* there are two use-cases when we can't rely on general MPS mechanism:
* 1) Model merge, primarily (or only) merge driver (git-invoked conflict resolution tool).
* 2) Convert models to binary during build.
* <p/>
* In either case, we read model first, and then save, and need save to know (and keep) information we've read.
* <p/>
* The reasons not to use general MPS mechanism are:
* (a) We don't want to pay MPS start-up costs
* (b) Languages used in the models might not be available in classpath (Doesn't apply to (2) - we do know them during build)
* <p/>
* The idea is to fill this provider with information read, and use it from model write. This provider shall not survive
* single read/write pair for a given model. Although perhaps in the future we might utilize it to keep model-specific
* {@link jetbrains.mps.smodel.DebugRegistry}, which is global at the moment.
* <p/>
* To certain extent, this class serves to overcome limitations of SConcept API, as it doesn't expose e.g. scope or kind. Once (and if)
* we decide to expose these from SConcept, there would be no need in this mediator. Perhaps, it's the right way to go? XXX revisit
* <p/>
* Note, this class replaces {@link jetbrains.mps.persistence.ModelEnvironmentInfo} which was likely intended for the similar purpose,
* but is ugly and doesn't suite modern (v9-bis) persistence well.
* <p/>
* There's implementation for most use-cases, {@link jetbrains.mps.persistence.MetaModelInfoProvider.RegularMetaModelInfo}, which merely delegates
* to appropriate general MPS facilities, and doesn't support change (attempt to change is no-op).
* <p/>
* IMPLEMENTATION NOTE: use {@link jetbrains.mps.persistence.MetaModelInfoProvider.BaseMetaModelInfo} as base class, do not implement this interface directly.
* <p/>
* Instances are not thread-safe unless noted otherwise.
* <p/>
* All methods are allowed to return <code>null</code> to indicate provider knows nothing about attribute questioned.
* Setters arguments, except for id (i.e. name, concept kind, etc), may be <code>null</code> as well.
*
* @author Artem Tikhomirov
* @see jetbrains.mps.persistence.MetaModelInfoProvider.BaseMetaModelInfo
* @see jetbrains.mps.smodel.DebugRegistry
* @see jetbrains.mps.persistence.ModelEnvironmentInfo
*/
public interface MetaModelInfoProvider {
/**
* Boolean attribute to indicate whether we intend to use model read without access to regular (MPS instance) meta info.
* The models read with this option set to true, could be serialized without access to concept registry of MPS (of course,
* if respective ModelFactory supports this. Our default(aka xml) and binary do).
* @deprecated use {@link MetaInfoLoadingOption} instead
*/
@ToRemove(version = 3.7)
@Deprecated
String OPTION_KEEP_READ_METAINFO = "keep-metainfo";
enum MetaInfoLoadingOption implements ModelLoadingOption {
KEEP_READ
}
String getLanguageName(SLanguageId lang);
void setLanguageName(SLanguageId lang, String name);
/**
* @return FIXME qualified concept name at the moment, short name once we switch to short names in persistence
*/
String getConceptName(SConceptId concept);
void setConceptName(SConceptId concept, String name);
String getPropertyName(SPropertyId property);
void setPropertyName(SPropertyId property, String name);
String getAssociationName(SReferenceLinkId link);
void setAssociationName(SReferenceLinkId link, String name);
String getAggregationName(SContainmentLinkId link);
void setAggregationName(SContainmentLinkId link, String name);
ConceptKind getKind(SConceptId concept);
void setKind(SConceptId concept, ConceptKind kind);
StaticScope getScope(SConceptId concept);
void setScope(SConceptId concept, StaticScope scope);
Boolean isUnordered(SContainmentLinkId link);
void setUnordered(SContainmentLinkId link, boolean unordered);
/**
* This method makes sense only for concepts with
* {@link #getKind(jetbrains.mps.smodel.adapter.ids.SConceptId) kind} == {@link jetbrains.mps.smodel.runtime.ConceptKind#IMPLEMENTATION_WITH_STUB}
*/
SConceptId getStubConcept(SConceptId origin);
void setStubConcept(SConceptId origin, SConceptId stub);
/**
* Base implementation, clients shall extend this class rather than implement {@link jetbrains.mps.persistence.MetaModelInfoProvider} directly.
* Always answers <code>null</code>, and a no-op for updates.
*/
public static class BaseMetaModelInfo implements MetaModelInfoProvider {
@Override
public String getLanguageName(SLanguageId lang) {
return null;
}
@Override
public void setLanguageName(SLanguageId lang, String name) {
// intentionally no-op
}
@Override
public String getConceptName(SConceptId concept) {
return null;
}
@Override
public void setConceptName(SConceptId concept, String name) {
// intentionally no-op
}
@Override
public String getPropertyName(SPropertyId property) {
return null;
}
@Override
public void setPropertyName(SPropertyId property, String name) {
// intentionally no-op
}
@Override
public String getAssociationName(SReferenceLinkId link) {
return null;
}
@Override
public void setAssociationName(SReferenceLinkId link, String name) {
// intentionally no-op
}
@Override
public String getAggregationName(SContainmentLinkId link) {
return null;
}
@Override
public void setAggregationName(SContainmentLinkId link, String name) {
// intentionally no-op
}
@Override
public ConceptKind getKind(SConceptId concept) {
return null;
}
@Override
public void setKind(SConceptId concept, ConceptKind kind) {
// intentionally no-op
}
@Override
public StaticScope getScope(SConceptId concept) {
return null;
}
@Override
public void setScope(SConceptId concept, StaticScope scope) {
// intentionally no-op
}
@Override
public Boolean isUnordered(SContainmentLinkId link) {
return null;
}
@Override
public void setUnordered(SContainmentLinkId link, boolean unordered) {
// intentionally no-op
}
@Override
public SConceptId getStubConcept(SConceptId origin) {
return null;
}
@Override
public void setStubConcept(SConceptId origin, SConceptId stub) {
// intentionally left no-op
}
}
/**
* Default implementation to use in general MPS scenarios.
* Sort of {@link jetbrains.mps.smodel.runtime.ConceptDescriptor}, limited to methods essential for persistence.
* Ensures non-<code>null</code> values (empty strings for names to satisfy persistence) and reasonable defaults otherwise.
* <p/>
* Uses {@link jetbrains.mps.smodel.DebugRegistry} to retrieve names, if available.
* Setter methods update {@link jetbrains.mps.smodel.DebugRegistry}.
*/
public static class RegularMetaModelInfo extends BaseMetaModelInfo {
private static final Logger LOG = Logger.wrap(LogManager.getLogger(DefaultModelPersistence.class));
private SModelReference myModelRef;
public RegularMetaModelInfo(@Nullable SModelReference modelRef) {
myModelRef = modelRef;
}
@Override
public String getLanguageName(SLanguageId lang) {
final LanguageRuntime langRT = LanguageRegistry.getInstance().getLanguage(lang);
if (langRT != null) {
return langRT.getNamespace();
}
String name = DebugRegistry.getInstance().getLanguageName(lang);
return name == null ? "" : name;
}
@Override
public String getConceptName(SConceptId concept) {
ConceptDescriptor descriptor = ConceptRegistryUtil.getConceptDescriptor(concept);
if (descriptor != null) {
return descriptor.getConceptFqName();
}
String name = DebugRegistry.getInstance().getConceptName(concept);
return name == null ? "" : name;
}
@Override
public String getPropertyName(SPropertyId property) {
ConceptDescriptor descriptor = ConceptRegistryUtil.getConceptDescriptor(property.getConceptId());
if (descriptor != null) {
final PropertyDescriptor pd = descriptor.getPropertyDescriptor(property);
if (pd != null) {
return pd.getName();
}
}
String name = DebugRegistry.getInstance().getPropertyName(property);
return name == null ? "" : name;
}
@Override
public String getAssociationName(SReferenceLinkId link) {
ConceptDescriptor descriptor = ConceptRegistryUtil.getConceptDescriptor(link.getConceptId());
if (descriptor != null) {
final ReferenceDescriptor ld = descriptor.getRefDescriptor(link);
if (ld != null) {
return ld.getName();
}
}
String name = DebugRegistry.getInstance().getRefName(link);
return name == null ? "" : name;
}
@Override
public String getAggregationName(SContainmentLinkId link) {
ConceptDescriptor descriptor = ConceptRegistryUtil.getConceptDescriptor(link.getConceptId());
if (descriptor != null) {
final LinkDescriptor ld = descriptor.getLinkDescriptor(link);
if (ld != null) {
return ld.getName();
}
}
String name = DebugRegistry.getInstance().getLinkName(link);
return name == null ? "" : name;
}
@Override
public ConceptKind getKind(SConceptId concept) {
ConceptDescriptor descriptor = ConceptRegistryUtil.getConceptDescriptor(concept);
if (descriptor != null) {
return descriptor.getConceptKind();
}
return ConceptKind.NORMAL;
}
@Override
public StaticScope getScope(SConceptId concept) {
ConceptDescriptor descriptor = ConceptRegistryUtil.getConceptDescriptor(concept);
if (descriptor != null) {
return descriptor.getStaticScope();
}
return StaticScope.GLOBAL;
}
@Override
public Boolean isUnordered(SContainmentLinkId link) {
ConceptDescriptor descriptor = ConceptRegistryUtil.getConceptDescriptor(link.getConceptId());
if (descriptor != null) {
final LinkDescriptor ld = descriptor.getLinkDescriptor(link);
if (ld != null) {
return ld.isUnordered();
}
}
return Boolean.FALSE;
}
@Override
public SConceptId getStubConcept(SConceptId origin) {
String originFQName = getConceptName(origin);
if (originFQName == null) {
return null;
}
// FIXME move stub concept id to ConceptDescriptor
final String stubFQName = ConceptInfo.constructStubConceptName(originFQName);
if (!ModelAccess.instance().canRead()) {
String modelName = myModelRef == null ? "<unknown>" : myModelRef.getModelName();
LOG.error("Read action is needed to collect some non-AST properties of model " + modelName + ".\n" +
"Otherwise, StuffedMetaModelInfoProvider should be used, and this code should not be called.\n" +
"This error most possibly means that the model has stub concept attributes missing.\n" +
"This happens after merging models sometimes [MPS-23869].\n" +
"Possible fix is to open model " + modelName + " in MPS and re-save it\n", new Throwable());
return MetaIdFactory.INVALID_CONCEPT_ID;
}
final SConcept concept = MetaAdapterFactoryByName.getConcept(stubFQName);
if (!(concept.isValid())) {
return null;
}
return MetaIdHelper.getConcept(concept);
}
@Override
public void setLanguageName(SLanguageId lang, String name) {
DebugRegistry.getInstance().addLanguageName(lang, name);
}
@Override
public void setConceptName(SConceptId concept, String name) {
DebugRegistry.getInstance().addConceptName(concept, name);
}
@Override
public void setPropertyName(SPropertyId property, String name) {
DebugRegistry.getInstance().addPropertyName(property, name);
}
@Override
public void setAssociationName(SReferenceLinkId link, String name) {
DebugRegistry.getInstance().addRefName(link, name);
}
@Override
public void setAggregationName(SContainmentLinkId link, String name) {
DebugRegistry.getInstance().addLinkName(link, name);
}
}
/**
* Provider which is filled with desired information.
* Queries, if could not be resolved with the information filled in, are directed to delegate.
* Depending on actual delegate used, may answer <code>null</code>.
*/
public static class StuffedMetaModelInfo extends BaseMetaModelInfo {
private final Map<SLanguageId, String> myLanguageNames = new HashMap<SLanguageId, String>();
private final Map<SConceptId, String> myConceptNames = new HashMap<SConceptId, String>();
private final Map<SPropertyId, String> myPropertyNames = new HashMap<SPropertyId, String>();
private final Map<SReferenceLinkId, String> myAssociationNames = new HashMap<SReferenceLinkId, String>();
private final Map<SContainmentLinkId, String> myAggregationNames = new HashMap<SContainmentLinkId, String>();
private final Map<SContainmentLinkId, Boolean> myUnordered = new HashMap<SContainmentLinkId, Boolean>();
private final Map<SConceptId, StaticScope> myScope = new HashMap<SConceptId, StaticScope>();
private final Map<SConceptId, ConceptKind> myKind = new HashMap<SConceptId, ConceptKind>();
private final Map<SConceptId, SConceptId> myStubs = new HashMap<SConceptId, SConceptId>();
private final MetaModelInfoProvider myDelegate;
public StuffedMetaModelInfo(@NotNull MetaModelInfoProvider delegate) {
myDelegate = delegate;
}
/**
* Fill another StuffedMetaModelInfo instance with information known to this one.
* It might be necessary if we don't want to use this provider as is due to delegate.
* <p/>
* There would be no need in this method if we could pass proper StuffedMetaModelInfo
* instance right into modelFactory.load(). However, given Map(String,String) for options,
* we have to assume there might be inappropriate delegate which we can't rely on (i.e. merge has to
* combine few stuffed sources, asks them one by one, first one to answer wins)
*/
public void populate(@NotNull StuffedMetaModelInfo other) {
other.myLanguageNames.putAll(myLanguageNames);
other.myConceptNames.putAll(myConceptNames);
other.myPropertyNames.putAll(myPropertyNames);
other.myAssociationNames.putAll(myAssociationNames);
other.myAggregationNames.putAll(myAggregationNames);
other.myUnordered.putAll(myUnordered);
other.myScope.putAll(myScope);
other.myKind.putAll(myKind);
other.myStubs.putAll(myStubs);
}
@Override
public void setLanguageName(SLanguageId lang, String name) {
if (isEmpty(name)) {
myLanguageNames.remove(lang);
} else {
myLanguageNames.put(lang, name);
}
myDelegate.setLanguageName(lang, name);
}
@Override
public void setConceptName(SConceptId concept, String name) {
if (isEmpty(name)) {
myConceptNames.remove(concept);
} else {
myConceptNames.put(concept, name);
}
myDelegate.setConceptName(concept, name);
}
@Override
public void setPropertyName(SPropertyId property, String name) {
if (isEmpty(name)) {
myPropertyNames.remove(property);
} else {
myPropertyNames.put(property, name);
}
myDelegate.setPropertyName(property, name);
}
@Override
public void setAssociationName(SReferenceLinkId link, String name) {
if (isEmpty(name)) {
myAssociationNames.remove(link);
} else {
myAssociationNames.put(link, name);
}
myDelegate.setAssociationName(link, name);
}
@Override
public void setAggregationName(SContainmentLinkId link, String name) {
if (isEmpty(name)) {
myAggregationNames.remove(link);
} else {
myAggregationNames.put(link, name);
}
myDelegate.setAggregationName(link, name);
}
@Override
public void setKind(SConceptId concept, ConceptKind kind) {
if (kind == null) {
myKind.remove(concept);
} else {
myKind.put(concept, kind);
}
myDelegate.setKind(concept, kind);
}
@Override
public void setScope(SConceptId concept, StaticScope scope) {
if (scope == null) {
myScope.remove(concept);
} else {
myScope.put(concept, scope);
}
myDelegate.setScope(concept, scope);
}
@Override
public void setUnordered(SContainmentLinkId link, boolean unordered) {
myUnordered.put(link, unordered);
myDelegate.setUnordered(link, unordered);
}
@Override
public void setStubConcept(SConceptId origin, SConceptId stub) {
if (stub == null) {
myStubs.remove(origin);
} else {
myStubs.put(origin, stub);
}
myDelegate.setStubConcept(origin, stub);
}
private static boolean isEmpty(String name) {
return name == null || name.isEmpty();
}
@Override
public String getLanguageName(SLanguageId lang) {
String name = myLanguageNames.get(lang);
if (name != null) {
return name;
}
return myDelegate.getLanguageName(lang);
}
@Override
public String getConceptName(SConceptId concept) {
String name = myConceptNames.get(concept);
if (name != null) {
return name;
}
return myDelegate.getConceptName(concept);
}
@Override
public String getPropertyName(SPropertyId property) {
String name = myPropertyNames.get(property);
if (name != null) {
return name;
}
return myDelegate.getPropertyName(property);
}
@Override
public String getAssociationName(SReferenceLinkId link) {
String name = myAssociationNames.get(link);
if (name != null) {
return name;
}
return myDelegate.getAssociationName(link);
}
@Override
public String getAggregationName(SContainmentLinkId link) {
String name = myAggregationNames.get(link);
if (name != null) {
return name;
}
return myDelegate.getAggregationName(link);
}
@Override
public ConceptKind getKind(SConceptId concept) {
ConceptKind kind = myKind.get(concept);
if (kind != null) {
return kind;
}
return myDelegate.getKind(concept);
}
@Override
public StaticScope getScope(SConceptId concept) {
StaticScope scope = myScope.get(concept);
if (scope != null) {
return scope;
}
return myDelegate.getScope(concept);
}
@Override
public Boolean isUnordered(SContainmentLinkId link) {
Boolean unordered = myUnordered.get(link);
if (unordered != null) {
return unordered;
}
return myDelegate.isUnordered(link);
}
@Override
public SConceptId getStubConcept(SConceptId origin) {
SConceptId stub = myStubs.get(origin);
if (stub != null) {
return stub;
}
return myDelegate.getStubConcept(origin);
}
}
}