/* * Copyright 2003-2017 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; import jetbrains.mps.project.ModuleId; import jetbrains.mps.project.structure.modules.ModuleReference; import jetbrains.mps.smodel.SModelId.ForeignSModelId; import jetbrains.mps.smodel.SModelId.ModelNameSModelId; import jetbrains.mps.util.Computable; import jetbrains.mps.util.EqualUtil; import jetbrains.mps.util.Pair; import jetbrains.mps.util.StringUtil; import jetbrains.mps.util.annotation.Hack; import jetbrains.mps.util.annotation.ToRemove; import org.apache.log4j.Logger; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.mps.annotations.Immutable; import org.jetbrains.mps.openapi.model.SModel; import org.jetbrains.mps.openapi.model.SModelId; import org.jetbrains.mps.openapi.model.SModelName; import org.jetbrains.mps.openapi.module.SModule; import org.jetbrains.mps.openapi.module.SModuleId; import org.jetbrains.mps.openapi.module.SModuleReference; import org.jetbrains.mps.openapi.module.SRepository; import org.jetbrains.mps.openapi.persistence.PersistenceFacade; import java.util.Objects; // FIXME move to [smodel] once dependencies from MPSModuleRepository and SModelRepository are gone @Immutable public final class SModelReference implements org.jetbrains.mps.openapi.model.SModelReference { private static Logger LOG = Logger.getLogger(SModelReference.class); @NotNull private final SModelId myModelId; @NotNull private final SModelName myModelName; @Nullable public final SModuleReference myModuleReference; /** * Use of this constructor is discouraged, favor {@link #SModelReference(SModuleReference, SModelId, SModelName)} instead */ public SModelReference(@Nullable SModuleReference module, @NotNull SModelId modelId, @NotNull String modelName) { this(module, modelId, new SModelName(modelName)); } public SModelReference(@Nullable SModuleReference module, @NotNull SModelId modelId, @NotNull SModelName modelName) { myModelId = modelId; myModelName = modelName; if (module == null) { if (!modelId.isGloballyUnique()) { throw new IllegalArgumentException(String.format("Only globally unique model id could be used without module specification: %s", modelId)); } } myModuleReference = module; } @NotNull @Override public org.jetbrains.mps.openapi.model.SModelId getModelId() { return myModelId; } @NotNull @Override public SModelName getName() { return myModelName; } @NotNull @Override public String getModelName() { return myModelName.getValue(); } @Nullable @Override public SModuleReference getModuleReference() { return myModuleReference; } @Override public SModel resolve(SRepository repo) { if (myModuleReference != null) { final SRepository repository; if (repo == null) { // see StaticReference, which seems to be the only place we pass null as repository repository = MPSModuleRepository.getInstance(); } else { repository = repo; } Computable<SModel> c = new Computable<SModel>() { @Override public SModel compute() { SModule module = repository.getModule(myModuleReference.getModuleId()); if (module == null) { return null; } return module.getModel(myModelId); } }; if (!repository.getModelAccess().canRead()) { LOG.warn("Attempt to resolve a model not from read action. What are you going to do with return value? Hint: at least, read. Please ensure proper model access then.", new Throwable()); return new ModelAccessHelper(repository).runReadAction(c); } else { return c.compute(); } } // FIXME !!! use supplied SRepository, not global model repo !!! // If there's no module reference, and model id is global, it's supposed we shall get the model from a global repository. // However, at the moment, there's no easy way to get model from SRepository (other than to iterate over all modules and models, // which doesn't sound like a good approach). Either shall provide method to find model from SRepository, or drop // 'globally unique' model id altogether. What's the benefit of them? // NOTE, shall tolerate null repo unless every single piece of code that expects StaticReference of a newly created node // hanging in the air to resolve. @see StaticReference#getTargetSModel if (SModelRepository.getInstance() != null) { // could be null in tests return SModelRepository.getInstance().getModelDescriptor(this); } return null; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null) return false; if (o instanceof SModelReference) { SModelReference that = (SModelReference) o; if (myModelId.equals(that.myModelId)) { if (myModelId.isGloballyUnique() && that.myModelId.isGloballyUnique()) { return true; } return Objects.equals(getModuleReference(), that.getModuleReference()); } } return false; } @Override public int hashCode() { int result = myModelId.hashCode(); // It's vital not to take module reference into account for models with globally unique ids as we need to match (e.g. in map keys) // model references in both formats (with and without module identity part). if (!myModelId.isGloballyUnique()) { result += 31 * getModuleReference().hashCode(); } return result; } /** * @deprecated This code shall move to private method of PersistenceRegistry, which would dispatch to proper * registered factories. Use {@link PersistenceFacade#createModelReference(String)} instead. * Format: <code>[ moduleID / ] modelID [ ([moduleName /] modelName ) ]</code> */ @Deprecated @ToRemove(version = 3.3) public static SModelReference parseReference(String s) { Pair<Pair<SModuleId, String>, Pair<SModelId, String>> parseResult = parseReference_internal(s); SModuleId moduleId = parseResult.o1.o1; String moduleName = parseResult.o1.o2; SModelId modelId = parseResult.o2.o1; String modelName = parseResult.o2.o2; SModuleReference moduleRef = moduleId != null || moduleName != null ? new jetbrains.mps.project.structure.modules.ModuleReference(moduleName, moduleId) : null; return new SModelReference(moduleRef, modelId, modelName); } public static Pair<Pair<SModuleId, String>, Pair<SModelId, String>> parseReference_internal(String s) { if (s == null) return null; s = s.trim(); int lParen = s.indexOf('('); int rParen = s.lastIndexOf(')'); String presentationPart = null; if (lParen > 0 && rParen == s.length() - 1) { presentationPart = s.substring(lParen + 1, rParen); s = s.substring(0, lParen); lParen = s.indexOf('('); rParen = s.lastIndexOf(')'); } if (lParen != -1 || rParen != -1) { throw new IllegalArgumentException("parentheses do not match in: `" + s + "'"); } SModuleId moduleId = null; int slash = s.indexOf('/'); if (slash >= 0) { // FIXME I wonder why there's no SModuleIdFactory and corresponding methods in PersistenceFacade moduleId = ModuleId.fromString(StringUtil.unescapeRefChars(s.substring(0, slash))); s = s.substring(slash + 1); } String modelIDString = StringUtil.unescapeRefChars(s); SModelId modelId; if (modelIDString.indexOf(':') >= 0) { PersistenceFacade facade = PersistenceFacade.getInstance(); // temporary: SModelReference can be created without active PersistenceFacade if (facade == null) { // FIXME get rid of facade == null case, if any // Besides, shall move the code to PersistenceRegistry, as it's responsible for prefixes and factory pick LOG.warn("Please report stacktrace, which would help us to find out improper MPS initialization sequence", new Throwable()); } modelId = facade != null ? facade.createModelId(modelIDString) : jetbrains.mps.smodel.SModelId.fromString(modelIDString); } else { // dead code? I suspect ModelNameSModelId, if any, would start with "m:" prefix and we'd never get into else clause // OTOH, there seems to be a special hack in toString(), that persists ModelNameSModelId without the prefix modelId = new ModelNameSModelId(modelIDString); } String moduleName = null; String modelName = null; if (presentationPart != null) { slash = presentationPart.indexOf('/'); if (slash >= 0) { moduleName = StringUtil.unescapeRefChars(presentationPart.substring(0, slash)); modelName = StringUtil.unescapeRefChars(presentationPart.substring(slash + 1)); } else { modelName = StringUtil.unescapeRefChars(presentationPart); } } if (modelName == null || modelName.isEmpty()) { modelName = modelId.getModelName(); if (modelName == null) { throw new IllegalArgumentException("incomplete model reference, presentation part is absent"); } } if (moduleId == null) { moduleId = extractModuleIdFromModelIdIfJavaStub(modelId); } if (isLegacyJavaStubModelId(modelId)) { modelId = newJavaPackageStubFromLegacy(modelId); } return new Pair(new Pair(moduleId, moduleName), new Pair(modelId, modelName)); } /** * This temporary code suites the purpose to homogenize java_stub model references, that used * to be kept in two different formats (one is "module id/model id including module id/(module name/model name)" * and another "model id including module id(module name/model name)". If there's module id anyway, why * would anyone keep it to model id then, and common patter for model reference (with module id coming first) shall be used. * * Once all model references to java stub are updated, this code shall cease to exist. * * IMPORTANT: there's a fly in the ointment, though - we shall read references of old models, and thus shall keep this code * forever. Perhaps, we can move it into persistence/vcs modules and bury it there? Another alternative is to introduce * new model identity to replace 'f:' identity, and leave dedicated SModelId factory for the legacy support in vcs/persistence only. */ @ToRemove(version = 3.3) @Nullable @Hack private static SModuleId extractModuleIdFromModelIdIfJavaStub(SModelId modelId) { if (isVerboseJavaStubModelId(modelId)) { String idValue = ((ForeignSModelId) modelId).getId(); String stereo = SModelStereotype.getStubStereotypeForId(LanguageID.JAVA); if (idValue.length() > stereo.length() + 2 && idValue.startsWith(stereo) && idValue.charAt(stereo.length()) == '#') { // two forms of legacy stub model id: // f:java_stub#module id#package name // f:java_stub#package name int secondHashIndex = idValue.indexOf('#', stereo.length() + 1); // there are two hash chars and non-empty package name if (secondHashIndex != -1 && idValue.length() > secondHashIndex) { return ModuleId.fromString(idValue.substring(stereo.length()+1, secondHashIndex)); } } } return null; } /** * IMPORTANT: see {@link #extractModuleIdFromModelIdIfJavaStub(SModelId)} for the reasons we didn't remove it. * * Compatibility code to migrate stub model id with module id to an 'honest' model id without module id. * * @return <code>true</code> if it's model id of java stub and it includes module id as it used to do in MPS 3.2 and earlier */ @Deprecated @ToRemove(version = 3.3) private static boolean isVerboseJavaStubModelId(SModelId id) { if (ForeignSModelId.TYPE.equals(id.getType()) && id instanceof ForeignSModelId) { String idValue = ((ForeignSModelId) id).getId(); String stereo = SModelStereotype.getStubStereotypeForId(LanguageID.JAVA); if (idValue.length() > stereo.length() + 2 && idValue.startsWith(stereo) && idValue.charAt(stereo.length()) == '#') { // legacy stub model id: f:java_stub#module id#package name // new stub model id: f:java_stub#package name int secondHashIndex = idValue.indexOf('#', stereo.length() + 1); // there are two hash chars and non-empty package name return secondHashIndex != -1 && idValue.length() > secondHashIndex; } } return false; } /** * IMPORTANT: see {@link #extractModuleIdFromModelIdIfJavaStub(SModelId)} for the reasons we didn't remove it. * @return <code>true</code> if it's model id of java stub in its legacy form (i.e. foreign, f:java_stub#...), either with or without module id part. */ @Deprecated @ToRemove(version = 3.3) private static boolean isLegacyJavaStubModelId(SModelId id) { if (ForeignSModelId.TYPE.equals(id.getType()) && id instanceof ForeignSModelId) { String idValue = ((ForeignSModelId) id).getId(); String stereo = SModelStereotype.getStubStereotypeForId(LanguageID.JAVA); return (idValue.length() > stereo.length() + 2 && idValue.startsWith(stereo) && idValue.charAt(stereo.length()) == '#'); } return false; } /** * Here we duplicate code of JavaPackageNameStub, not to introduce dependency to [java-stub] module */ @ToRemove(version = 3.3) @Hack private static SModelId newJavaPackageStubFromLegacy(SModelId id) { // pre: isLegacyJavaStubModel() String idValue = ((ForeignSModelId) id).getId(); int lastHash = idValue.lastIndexOf('#'); return PersistenceFacade.getInstance().createModelId(LanguageID.JAVA + ':' + idValue.substring(lastHash + 1)); } public String toString() { StringBuilder sb = new StringBuilder(); if (getModuleReference() != null && getModuleReference().getModuleId() != null) { sb.append(StringUtil.escapeRefChars(getModuleReference().getModuleId().toString())); sb.append("/"); } String modelId = myModelId instanceof ModelNameSModelId ? myModelId.getModelName() : myModelId.toString(); sb.append(StringUtil.escapeRefChars(modelId)); if (getModuleReference() == null && myModelName.getValue().equals(myModelId.getModelName())) { return sb.toString(); } sb.append("("); if (getModuleReference() != null && getModuleReference().getModuleName() != null) { sb.append(StringUtil.escapeRefChars(getModuleReference().getModuleName())); sb.append("/"); } if (!myModelName.getValue().equals(myModelId.getModelName())) { // no reason to write down model name if it's part of module id sb.append(StringUtil.escapeRefChars(myModelName.getValue())); } sb.append(")"); return sb.toString(); } /** * @see jetbrains.mps.project.structure.modules.ModuleReference#differs(SModuleReference, SModuleReference) */ public static boolean differs(org.jetbrains.mps.openapi.model.SModelReference ref1, org.jetbrains.mps.openapi.model.SModelReference ref2) { if (ref1 == null || ref2 == null) { return ref1 != ref2; } if (ModuleReference.differs(ref1.getModuleReference(), ref2.getModuleReference())) { return true; } return !(EqualUtil.equals(ref1.getModelId(), ref2.getModelId()) && EqualUtil.equals(ref1.getModelName(), ref2.getModelName())); } }