/* * 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); } } }