/* * 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.project; import jetbrains.mps.extapi.module.EditableSModule; import jetbrains.mps.extapi.module.ModuleFacetBase; import jetbrains.mps.extapi.module.SModuleBase; import jetbrains.mps.extapi.persistence.FileBasedModelRoot; import jetbrains.mps.extapi.persistence.FileDataSource; import jetbrains.mps.extapi.persistence.ModelRootBase; import jetbrains.mps.library.ModulesMiner; import jetbrains.mps.module.SDependencyImpl; import jetbrains.mps.persistence.MementoImpl; import jetbrains.mps.persistence.PersistenceRegistry; import jetbrains.mps.project.facets.JavaModuleFacet; import jetbrains.mps.project.structure.model.ModelRootDescriptor; import jetbrains.mps.project.structure.modules.Dependency; import jetbrains.mps.project.structure.modules.DeploymentDescriptor; import jetbrains.mps.project.structure.modules.ModuleDescriptor; import jetbrains.mps.project.structure.modules.ModuleFacetDescriptor; import jetbrains.mps.smodel.BootstrapLanguages; import jetbrains.mps.smodel.DefaultScope; import jetbrains.mps.smodel.Generator; import jetbrains.mps.smodel.Language; import jetbrains.mps.smodel.ModuleRepositoryFacade; import jetbrains.mps.smodel.SModelInternal; import jetbrains.mps.smodel.SuspiciousModelHandler; import jetbrains.mps.util.EqualUtil; import jetbrains.mps.util.FileUtil; import jetbrains.mps.util.MacroHelper; import jetbrains.mps.util.MacrosFactory; import jetbrains.mps.util.PathManager; import jetbrains.mps.util.annotation.Hack; import jetbrains.mps.util.annotation.ToRemove; import jetbrains.mps.vfs.IFile; import jetbrains.mps.vfs.openapi.FileSystem; import jetbrains.mps.vfs.path.Path; import org.apache.log4j.LogManager; import org.apache.log4j.Logger; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.mps.openapi.language.SLanguage; import org.jetbrains.mps.openapi.model.EditableSModel; import org.jetbrains.mps.openapi.model.SModel; import org.jetbrains.mps.openapi.model.SModelName; import org.jetbrains.mps.openapi.module.FacetsFacade; import org.jetbrains.mps.openapi.module.SDependency; import org.jetbrains.mps.openapi.module.SDependencyScope; import org.jetbrains.mps.openapi.module.SModule; import org.jetbrains.mps.openapi.module.SModuleFacet; 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.module.SearchScope; import org.jetbrains.mps.openapi.persistence.Memento; import org.jetbrains.mps.openapi.persistence.ModelRoot; import org.jetbrains.mps.openapi.persistence.ModelRootFactory; import org.jetbrains.mps.openapi.persistence.PersistenceFacade; import java.io.File; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import static org.jetbrains.mps.openapi.module.FacetsFacade.FacetFactory; /** * First of all, this class serves as a file-based module. Obviously it requires a file which contains a persisted * module descriptor (see constructor). * Secondly, this class provides a common implementation of the module editing. Not only the implementation of * simple interface {@link EditableSModule} is here but also a special editing mechanism is suggested below. * Nonetheless there are several flaws. * * 1. We need to separate FileBasedModule from the AbstractModule in order to make the AbstractModule truly abstract. * 2. We need to enforce a special committing mechanism (for the module editing) which is only sketched in this class. * The {@link #getModuleDescriptor()} method in fact is just a public property which discloses all the internals of the module. * It is undoubtedly ought to be fixed. * Moreover the implementations of this method return the original descriptor (copy they must return!). [not the problem of the abstract module per se] * Suggestion [to be done]: * Rather the {@link AbstractModule} must possess a special {@code #getEditingHandle} which returns a class which in turn is able to accumulate * all the changes user desire to accomplish and when user is finished with editing commit all the changes with one invocation of {@code handle.commit()}. * [or something like this] * 3. Also this subclass serves another purpose: it introduces model roots and module facets into module. * I guess this logic might migrate to <code>SModuleBase</code>. * * AP * * @see ModuleDescriptor for the details */ public abstract class AbstractModule extends SModuleBase implements EditableSModule { private static final Logger LOG = LogManager.getLogger(AbstractModule.class); public static final String MODULE_DIR = "module"; public static final String CLASSES_GEN = "classes_gen"; public static final String CLASSES = "classes"; /** * All paths concerning a module must be either absolute or relative to this 'anchor' file. * This is a rational idea since keeping the same information twice does not make sense. * Moreover moving or renaming a module gets just simpler */ @Nullable protected final IFile myDescriptorFile; @NotNull private final FileSystem myFileSystem; private SModuleReference myModuleReference; private Set<ModelRoot> mySModelRoots = new LinkedHashSet<ModelRoot>(); private Set<ModuleFacetBase> myFacets = new LinkedHashSet<ModuleFacetBase>(); private ModuleScope myScope = new ModuleScope(); private boolean myChanged = false; private static jetbrains.mps.vfs.FileSystem getFSSingleton() { return jetbrains.mps.vfs.FileSystem.getInstance(); } @Deprecated protected AbstractModule() { this(getFSSingleton()); } protected AbstractModule(@NotNull FileSystem fileSystem) { myDescriptorFile = null; myFileSystem = fileSystem; } protected AbstractModule(@Nullable IFile descriptorFile) { myDescriptorFile = descriptorFile; if (descriptorFile != null) { myFileSystem = descriptorFile.getFileSystem(); } else { myFileSystem = getFSSingleton(); } } @NotNull public FileSystem getFileSystem() { return myFileSystem; } //----reference @Override public SModuleId getModuleId() { // assertCanRead(); @see getModuleReference() return getModuleReference().getModuleId(); } @Override public String getModuleName() { // assertCanRead(); @see getModuleReference() return getModuleReference().getModuleName(); } @Override public Iterable<SDependency> getDeclaredDependencies() { assertCanRead(); ModuleDescriptor descriptor = getModuleDescriptor(); if (descriptor == null) { return Collections.emptyList(); } HashSet<SDependency> result = new HashSet<SDependency>(); final SRepository repo = getRepository(); if (repo == null) { throw new IllegalStateException("It is not possible to resolve all declared dependencies with a null repository : module " + this); } // add declared dependencies for (Dependency d : descriptor.getDependencies()) { result.add(new SDependencyImpl(d.getModuleRef(), repo, d.getScope(), d.isReexport())); } // add dependencies provided by devkits as nonreexport dependencies for (SModuleReference usedDevkit : collectLanguagesAndDevkits().devkits) { final SModule devkit = usedDevkit.resolve(repo); if (DevKit.class.isInstance(devkit)) { for (Solution solution : ((DevKit) devkit).getAllExportedSolutions()) { result.add(new SDependencyImpl(solution.getModuleReference(), repo, SDependencyScope.DEFAULT, false)); } } } return result; } @Override public Set<SLanguage> getUsedLanguages() { assertCanRead(); return collectLanguagesAndDevkits().languages; } // fills collections with of imported languages and devkits. // Languages include directly imported and coming immediately through devkits; listed devkits are imported directly, without those they extend (why?). public LangAndDevkits collectLanguagesAndDevkits() { Set<SLanguage> usedLanguages = new LinkedHashSet<>(); Set<SModuleReference> devkits = new LinkedHashSet<>(); // perhaps, shall introduce ModuleImports similar to ModelImports to accomplish this? for (SModel m : getModels()) { final SModelInternal modelInternal = (SModelInternal) m; usedLanguages.addAll(modelInternal.importedLanguageIds()); devkits.addAll(modelInternal.importedDevkits()); } // XXX why don't we respect extended devkits here? final SRepository repository = getRepository(); if (repository != null) { for (SModuleReference devkitRef : devkits) { final SModule module = devkitRef.resolve(repository); if (module instanceof DevKit) { for (SLanguage l : ((DevKit) module).getAllExportedLanguageIds()) { usedLanguages.add(l); } } } } usedLanguages.add(BootstrapLanguages.getLangCore()); return new LangAndDevkits(usedLanguages, devkits); } public static class LangAndDevkits { public final Set<SLanguage> languages; public final Set<SModuleReference> devkits; public LangAndDevkits(@NotNull Set<SLanguage> languages, @NotNull Set<SModuleReference> devkits) { this.languages = languages; this.devkits = devkits; } } protected void setModuleReference(@NotNull SModuleReference reference) { assertCanChange(); assert myModuleReference == null || reference.getModuleId().equals(myModuleReference.getModuleId()) : "module id can't be changed"; myModuleReference = reference; } @Override @NotNull //module reference is immutable, so we cn return original public SModuleReference getModuleReference() { // assertCanRead(); ClassLoaderManager needs module reference. Do we need CLM to obtain read lock? return myModuleReference; } //----save //todo move to EditableModule class @Nullable public ModuleDescriptor getModuleDescriptor() { return null; } //todo should be replaced with events public final void setModuleDescriptor(ModuleDescriptor moduleDescriptor) { assertCanChange(); doSetModuleDescriptor(moduleDescriptor); setChanged(); reloadAfterDescriptorChange(); fireChanged(); dependenciesChanged(); } // no notifications are sent protected void doSetModuleDescriptor(ModuleDescriptor moduleDescriptor) { throw new UnsupportedOperationException(); } @Override public void setChanged() { assertCanChange(); if (isReadOnly()) { LOG.warn("Changing read-only module " + this); } myChanged = true; } @Override public void save() { assertCanChange(); myChanged = false; } //----adding different deps /** * FIXME module editing is generally done through descriptor and reload. Although I do not mind exposing add/remove methods here, it should be consistent! */ @Nullable public Dependency addDependency(@NotNull SModuleReference moduleRef, boolean reexport) { assertCanChange(); ModuleDescriptor descriptor = getModuleDescriptor(); if (descriptor == null) { return null; } for (Dependency dep : descriptor.getDependencies()) { if (!EqualUtil.equals(dep.getModuleRef(), moduleRef)) { continue; } if (reexport && !dep.isReexport()) { dep.setReexport(true); dependenciesChanged(); fireChanged(); setChanged(); } return dep; } Dependency dep = new Dependency(); dep.setModuleRef(moduleRef); dep.setReexport(reexport); descriptor.getDependencies().add(dep); dependenciesChanged(); fireChanged(); setChanged(); return dep; } public void removeDependency(@NotNull Dependency dependency) { assertCanChange(); ModuleDescriptor descriptor = getModuleDescriptor(); if (descriptor == null) { return; } if (!descriptor.getDependencies().contains(dependency)) { return; } descriptor.getDependencies().remove(dependency); dependenciesChanged(); fireChanged(); setChanged(); } //----languages & devkits /** * @deprecated shall be removed once tests in MPS plugin got fixed (FacetTests.testAddRemoveUsedLanguage(), testFacetInitialized() */ @Deprecated @ToRemove(version = 3.4) public final Collection<SModuleReference> getUsedLanguagesReferences() { assertCanRead(); ModuleDescriptor descriptor = getModuleDescriptor(); if (descriptor == null) { return Collections.emptySet(); } return Collections.unmodifiableCollection(descriptor.getUsedLanguages()); } //----stubs // FIXME: MPS-19756 // TODO: get rid of this code - generate the deployment descriptor during build process protected void updatePackagedDescriptor() { // things to do: // 1) load/prepare stub libraries (getAdditionalJavaStubPaths) from sources descriptor // 2) load/prepare stub model roots from sources descriptor // 3) load libraries from deployment descriptor (/classes_gen ?) // possible cases: // 1) without deployment descriptor (nothing to do; todo: ?) // 2) with deployment descriptor, without sources (to do: 3) // 3) with deployment descriptor, with sources (to do: 1,2,3) if (!isPackaged()) { return; } ModuleDescriptor descriptor = getModuleDescriptor(); if (descriptor == null) { return; } DeploymentDescriptor deplDescriptor = descriptor.getDeploymentDescriptor(); if (deplDescriptor == null) { return; } if (getDescriptorFile() == null) { // this implicitly filters out Generator modules from updatePackagedDescriptor(). // however, generators never got DD (now they do, and that's the reason we could get NPE here), // and the method doesn't look like something to persist forever, so I don't care to update generator's source descriptor. // Instead, proper locations have to be specified right in deployment time return; } final IFile bundleHomeFile = getDescriptorFile().getBundleHome(); if (bundleHomeFile == null) { return; } IFile newContentDir = bundleHomeFile.getParent(); if (newContentDir == null || !newContentDir.exists()) { return; } IFile sourcesDescriptorFile = ModulesMiner.getSourceDescriptorFile(getDescriptorFile(), deplDescriptor); if (sourcesDescriptorFile == null) { // todo: for now it's impossible assert descriptor instanceof DeploymentDescriptor; } else { assert !(descriptor instanceof DeploymentDescriptor); } // 1 && 2 if (sourcesDescriptorFile != null) { // stub model roots List<ModelRootDescriptor> toRemove = new ArrayList<ModelRootDescriptor>(); List<ModelRootDescriptor> toAdd = new ArrayList<ModelRootDescriptor>(); for (ModelRootDescriptor rootDescriptor : descriptor.getModelRootDescriptors()) { String rootDescriptorType = rootDescriptor.getType(); if (rootDescriptorType.equals(PersistenceRegistry.JAVA_CLASSES_ROOT)) { // trying to load old format from deployment descriptor String pathElement = rootDescriptor.getMemento().get("path"); boolean update = false; Memento newMemento = new MementoImpl(); if (pathElement != null) { // See JavaSourceStubModelRoot & JavaClassStubsModelRoot load methods need to replace with super String convertedPath = convertPath(pathElement, bundleHomeFile, sourcesDescriptorFile, descriptor); if (convertedPath != null) { newMemento.put("path", convertedPath); update = true; } } else { // there are few possible deployment layouts: // 1. App/Contents/languages/my.lang.jar + -src.jar // 2. App/Contents/plugins/<name>/languages/my.lang.jar + -src.jar + libraries from additional cp // (build language generator puts libraries there with the help of ArtifactsRelativePathHelper, base on extracted jar deps; // FWIW, build language ignores jars listed under stub models) // App/Contents/plugins/<name>/pluginSolutions/my.lang.pluginSolution.jar // App/Contents/plugins/<name>/lib/icons.jar (placed there by build language generator) // 3. Custom layout: // e.g. jetpad, which differs from (2) with lib/ full of cp jars // mps-core, with languageDesign/ and util/ nested under languages/ // mps-vcs, with cp jars under lib/ // // trying to load new format : replacing paths like **.jar!/module -> String contentPath = rootDescriptor.getMemento().get(FileBasedModelRoot.CONTENT_PATH); List<String> paths = new LinkedList<String>(); for (Memento sourceRoot : rootDescriptor.getMemento().getChildren(FileBasedModelRoot.SOURCE_ROOTS)) { paths.add(contentPath + File.separator + sourceRoot.get(FileBasedModelRoot.LOCATION)); } // contentPath = my.lang-src.jar!/module/xxx (provided original was ${module}/xxx; although some have ${mps-home} there) // bundleHomeFile == my.lang.jar // bundleParent == folder of my.lang.jar // e.g. for collections.trove.msd: // /plugins/mps-trove/languages/collections_trove.runtime.jar // /plugins/mps-trove/languages/trove-2.1.0.jar // and // <modelRoot contentPath="${module}" type="java_classes"> // <sourceRoot location="classes_gen" /> // <sourceRoot location="lib/trove-2.1.0.jar" /> // </modelRoot> // the code below makes no sense // DD for the module lists <library jar="trove-2.1.0.jar" />, which is likely the way file from languages/ is loaded newMemento.put(FileBasedModelRoot.CONTENT_PATH, newContentDir.getPath()); Memento newMementoChild = newMemento.createChild(FileBasedModelRoot.SOURCE_ROOTS); for (String path : paths) { String convertedPath = convertPath(path, bundleHomeFile, sourcesDescriptorFile, descriptor); if (convertedPath != null) { String newRelativeLocation = FileUtil.getRelativePath(FileUtil.getUnixPath(convertedPath), FileUtil.getUnixPath(newContentDir.getPath()), Path.UNIX_SEPARATOR); newMementoChild.put(FileBasedModelRoot.LOCATION, newRelativeLocation); update = true; } } } if (update) { toAdd.add(new ModelRootDescriptor(rootDescriptorType, newMemento)); } toRemove.add(rootDescriptor); } } descriptor.getModelRootDescriptors().removeAll(toRemove); descriptor.getModelRootDescriptors().addAll(toAdd); } // 3 // MD.getAdditionalJavaStubPaths() has been updated by ModulesMiner to point to correct location according to information from DD // Would be great to have IFile here directly, but alas, there's no well-established idea what's relation between MD and IFile/Path // the problem is that myFileSystem not necessarily match that of deployment descriptor (the one we used to create these paths in MM). for (String jarFile : descriptor.getAdditionalJavaStubPaths()) { IFile jar = myFileSystem.getFile(jarFile); if (jar.exists()) { // FIXME why do we expose *each* cp jar as model stub? Seems to be legacy, when stubModelEntry used to specify // both cp+stub, now there's distinct model root for that. HOWEVER, now it's only dd that points correctly to // library jars (filesystem-wise). While module-relative stub jars from deployed modules are ignored in the update cycle // above (module-src.jar!/module/ doesn't contain lib/stub.jar), and stub.jar is often part of CP, this code helps to get // stubs in deployed modules (e.g. check collections_trove.runtime) descriptor.getModelRootDescriptors().add(ModelRootDescriptor.getJavaStubsModelRoot(jar)); } } } /** * Convert path from sources module descriptor for using on distribution * /classes && /classes_gen converts to bundle home path * * @param originalPath Original path from sources module descriptor * @return Converted path, null if path meaningless on packaged module */ @Nullable private String convertPath(String originalPath, IFile bundleHome, IFile sourcesDescriptorFile, ModuleDescriptor descriptor) { MacroHelper macroHelper = MacrosFactory.forModuleFile(sourcesDescriptorFile); String canonicalPath = FileUtil.getCanonicalPath(originalPath); // /classes && /classes_gen hack String suffix = descriptor.getCompileInMPS() ? CLASSES_GEN : CLASSES; if (canonicalPath.endsWith(suffix)) { // MacrosFactory based on original descriptor file because we use original descriptor file for ModuleDescriptor reading, so all paths expanded to original descriptor file String classes = macroHelper.expandPath("${module}/" + suffix); if (FileUtil.getCanonicalPath(classes).equalsIgnoreCase(canonicalPath)) { return bundleHome.getPath(); } } else if (FileUtil.getCanonicalPath(bundleHome.getPath()).equalsIgnoreCase(canonicalPath)) { return bundleHome.getPath(); } // ${mps_home}/lib String mpsHomeLibPath = FileUtil.getCanonicalPath(PathManager.getHomePath() + File.separator + "lib"); if (FileUtil.isAncestor(mpsHomeLibPath, canonicalPath)) { return canonicalPath; } // we used to keep originalPath if it has a macro not known to MPS here. // However, the check has been deprecated in 2012 and thus removed. I'm not 100% sure what // 'meaningless' in the contract of the method means. Of course, unknown macros make no sense for us // and thus null is legitimate answer, OTOH, custom macros might have a lot of meaning to someone else. // // ignore paths starts from ${module}/${project} etc return null; } //---- @Override public Iterable<ModelRoot> getModelRoots() { // We check read lock here because mySModelRoots is updated inside write. assertCanRead(); return Collections.unmodifiableCollection(mySModelRoots); } protected void reloadAfterDescriptorChange() { initFacetsAndModels(); } private void initFacetsAndModels() { updatePackagedDescriptor(); updateFacets(); updateModelsSet(); } /** * For the time being, MPS enforces certain facets for modules (e.g. Java facet is essential for classloading mechanism). * As we move forward with facets story, we likely respect actual facets for the module (e.g. would force Java facet on module creation only) * Need to ensure classloading could deal with modules without Java facet, then can drop these mandatory facets altogether */ protected void collectMandatoryFacetTypes(Set<String> types) { types.add(JavaModuleFacet.FACET_TYPE); } protected ModuleFacetBase setupFacet(ModuleFacetBase facet, Memento memento) { if (!facet.setModule(this)) { return null; } facet.load(memento != null ? memento : new MementoImpl()); facet.attach(); return facet; } protected void updateFacets() { assertCanChange(); ModuleDescriptor descriptor = getModuleDescriptor(); if (descriptor == null) { return; } for (ModuleFacetBase facet : myFacets) { facet.dispose(); } myFacets.clear(); Map<String, Memento> config = new HashMap<String, Memento>(); for (ModuleFacetDescriptor facetDescriptors : descriptor.getModuleFacetDescriptors()) { config.put(facetDescriptors.getType(), facetDescriptors.getMemento()); } Set<String> types = new HashSet<String>(); collectMandatoryFacetTypes(types); types.addAll(config.keySet()); for (String facetType : types) { FacetFactory factory = FacetsFacade.getInstance().getFacetFactory(facetType); if (factory == null) { LOG.error("no registered factory for a facet with type=`" + facetType + "'"); continue; } SModuleFacet newFacet = factory.create(); if (!(newFacet instanceof ModuleFacetBase)) { LOG.error("broken facet factory: " + factory.getClass().getName()); continue; } ModuleFacetBase facet = (ModuleFacetBase) newFacet; Memento m = config.get(facetType); facet = setupFacet(facet, m); if (facet != null) { myFacets.add(facet); } } } public void onModuleLoad() { updateExternalReferences(); } @Override public boolean isReadOnly() { // assertCanRead(); getModuleSourceDir() doesn't require read, why isPackaged() does? return isPackaged(); } @Override public boolean isPackaged() { // assertCanRead(); getModuleSourceDir() doesn't require read, why isPackaged() does? return getModuleSourceDir() == null || getModuleSourceDir().isInArchive(); } /** * Module sources folder * In case of working on sources == dir with module descriptor * In case of working on distribution = {module-name}-src.jar/module/ * In case of Generator = sourceLanguage.getModuleSourceDir() * ${module} expands to this method */ public IFile getModuleSourceDir() { return myDescriptorFile != null ? myDescriptorFile.getParent() : null; } @Nullable public IFile getDescriptorFile() { // assertCanRead(); if getModuleSourceDir doesn't require read, why getDescriptorFile does? return myDescriptorFile; } public void setModuleVersion(int version) { getModuleDescriptor().setModuleVersion(version); fireChanged(); setChanged(); } public int getModuleVersion() { ModuleDescriptor descriptor = getModuleDescriptor(); return descriptor == null ? 0 : descriptor.getModuleVersion(); } // FIXME rename model roots public void rename(@NotNull String newName) throws DescriptorTargetFileAlreadyExistsException { SModuleReference oldRef = getModuleReference(); renameModels(getModuleName(), newName, true); save(); //see MPS-18743, need to save before setting descriptor ModuleDescriptor descriptor = getModuleDescriptor(); if (myDescriptorFile != null) { String newDescriptorName = newName + MPSExtentions.DOT + FileUtil.getExtension(myDescriptorFile.getName()); //noinspection ConstantConditions if (myDescriptorFile.getParent().getDescendant(newDescriptorName).exists()) { throw new DescriptorTargetFileAlreadyExistsException(myDescriptorFile, newDescriptorName); } myDescriptorFile.rename(newName + "." + FileUtil.getExtension(myDescriptorFile.getName())); } if (descriptor != null) { descriptor.setNamespace(newName); setModuleDescriptor(descriptor); } fireModuleRenamed(oldRef); } /** * Must be transferred to workbench or elsewhere as * a separate listening mechanism. An induced contract is * not part of the module/model api, it is our desire -- * I would rather move it to workbench * [AP] * Please do not use unless absolutely necessary */ /*@Deprecated*/ public void renameModels(String oldName, String newName, boolean moveModels) { //if module name is a prefix of it's model's name - rename the model, too for (SModel m : getModels()) { if (!m.isReadOnly()) { SModelName oldModelName = m.getName(); if (oldModelName.getNamespace().startsWith(oldName)) { if (m instanceof EditableSModel) { SModelName newModelName = new SModelName(newName + oldModelName.getNamespace().substring(oldName.length()), oldModelName.getSimpleName(), oldModelName.getStereotype()); ((EditableSModel) m).rename(newModelName.getValue(), moveModels && m.getSource() instanceof FileDataSource); } } } } } @NotNull public SearchScope getScope() { // assertCanRead(); what's the reason to guard access to the field? return myScope; } @Override public void attach(@NotNull SRepository repository) { super.attach(repository); initFacetsAndModels(); } @Override public String toString() { String namespace = getModuleName(); return namespace + " [module]"; } @Override public void dispose() { assertCanChange(); LOG.trace("Disposing the module " + this); for (ModuleFacetBase f : myFacets) { f.dispose(); } myFacets.clear(); for (ModelRoot m : mySModelRoots) { ((ModelRootBase) m).dispose(); } mySModelRoots.clear(); super.dispose(); } public List<String> getSourcePaths() { assertCanRead(); return new ArrayList<String>(SModuleOperations.getAllSourcePaths(this)); } public void updateModelsSet() { doUpdateModelsSet(); } protected Iterable<ModelRoot> loadRoots() { ModuleDescriptor descriptor = getModuleDescriptor(); if (descriptor == null) { return Collections.emptyList(); } List<ModelRoot> result = new ArrayList<ModelRoot>(); for (ModelRootDescriptor modelRoot : descriptor.getModelRootDescriptors()) { try { ModelRootFactory modelRootFactory = PersistenceFacade.getInstance().getModelRootFactory(modelRoot.getType()); if (modelRootFactory == null) { LOG.error("Unknown model root type: `" + modelRoot.getType() + "'. Requested by: " + this); continue; } ModelRoot root = modelRootFactory.create(); Memento mementoWithFS = new MementoWithFS(modelRoot.getMemento(), myFileSystem); root.load(mementoWithFS); result.add(root); } catch (Exception e) { LOG.error("Error loading models from root with type: `" + modelRoot.getType() + "'. Requested by: " + this, e); } } return result; } private void doUpdateModelsSet() { assertCanChange(); for (SModel model : getModels()) { if (model instanceof EditableSModel && ((EditableSModel) model).isChanged()) { LOG.warn( "Trying to reload module " + getModuleName() + " which contains a non-saved model '" + model.getName() + "'. To prevent data loss, MPS will not update models in this module. " + "Please save your work and restart MPS. See MPS-18743 for details." ); return; } } Set<ModelRoot> toRemove = new LinkedHashSet<>(mySModelRoots); Set<ModelRoot> toUpdate = new LinkedHashSet<>(mySModelRoots); Set<ModelRoot> toAttach = new LinkedHashSet<>(); for (ModelRoot root : loadRoots()) { try { if (mySModelRoots.contains(root)) { toRemove.remove(root); } else { toAttach.add(root); } } catch (Exception e) { LOG.error("Error loading models from root `" + root + "'. Requested by: " + this, e); } } toUpdate.removeAll(toRemove); for (ModelRoot modelRoot : toRemove) { ((ModelRootBase) modelRoot).dispose(); } mySModelRoots.removeAll(toRemove); for (ModelRoot modelRoot : toAttach) { ModelRootBase rootBase = (ModelRootBase) modelRoot; rootBase.setModule(this); mySModelRoots.add(modelRoot); rootBase.attach(); } for (ModelRoot modelRoot : toUpdate) { ((ModelRootBase) modelRoot).update(); } } public static void handleReadProblem(AbstractModule module, Exception e, boolean isInConflict) { SuspiciousModelHandler.getHandler().handleSuspiciousModule(module, isInConflict); LOG.error(e.getMessage()); e.printStackTrace(); } // unlike similar method in SModel, doesn't take SRepository now // according to present use cases, we iterate modules of a repository and update them, // hence it's superficial to pass repository in here (although might add one for consistency) public void updateExternalReferences() { ModuleDescriptor moduleDescriptor = getModuleDescriptor(); final SRepository repository = getRepository(); if (moduleDescriptor == null || repository == null) { return; } if (moduleDescriptor.updateModelRefs(repository)) { setChanged(); } if (moduleDescriptor.updateModuleRefs(repository)) { setChanged(); } } protected void dependenciesChanged() { // todo: review all usages after migration! // callback on dependencies (any of them) changed event // you can override this method with some invalidation action // call super.dependenciesChanged() at the end // todo: as we haven't dependencies listeners... myScope.invalidateCaches(); } protected ModuleDescriptor loadDescriptor() { return null; } @Override public boolean isChanged() { return myChanged; } @Nullable @Override public <T extends SModuleFacet> T getFacet(@NotNull Class<T> clazz) { for (SModuleFacet facet : getFacets()) { if (clazz.isInstance(facet)) { return clazz.cast(facet); } } return null; } @NotNull @Override public Iterable<SModuleFacet> getFacets() { return Collections.unmodifiableSet(myFacets); } public class ModuleScope extends DefaultScope { protected ModuleScope() { } public AbstractModule getModule() { return AbstractModule.this; } @Override protected Set<SModule> getInitialModules() { Set<SModule> result = new HashSet<SModule>(); result.add(AbstractModule.this); return result; } @Override protected Set<Language> getInitialUsedLanguages() { HashSet<Language> result = new HashSet<Language>(); for (SLanguage l : AbstractModule.this.getUsedLanguages()) { SModule langModule = l.getSourceModule(); if (langModule instanceof Language) { result.add((Language) langModule); } } if (AbstractModule.this instanceof Language) { result.add((Language) AbstractModule.this); // XXX why Language(SModule)#getUsedLanguages doesn't care about descriptor language being used? result.add(ModuleRepositoryFacade.getInstance().getModule(BootstrapLanguages.descriptorLanguageRef(), Language.class)); } if (AbstractModule.this instanceof Generator) { result.add(((Generator) AbstractModule.this).getSourceLanguage()); } return result; } public String toString() { return "Scope of module " + AbstractModule.this; } } /** * @deprecated this is internal method, ask ModuleDescriptor for persisted setting directly, if it's what you're * looking for (check {@link ProjectPathUtil#getGeneratorOutputPath(ModuleDescriptor)}. There ain't no such thing as output path for a module in general. * * This method is no longer used in MPS, do not resurrect its uses. Although it's not part of openapi, AbstractModule is often deemed as 'almost api', * left for one release. */ @Deprecated @ToRemove(version = 3.5) public IFile getOutputPath() { String outputPath = ProjectPathUtil.getGeneratorOutputPath(getModuleDescriptor()); return outputPath == null ? null : getFileSystem().getFile(outputPath); } @Override public int getUsedLanguageVersion(@NotNull SLanguage usedLanguage) { return getUsedLanguageVersion(usedLanguage, true); } /** * has a fallback if the usedLanguage is absent in the module descriptor. if it happens then returns simply the current usedLanguage version * * @param check is whether to show error for not found version * @deprecated hack for migration, will be gone after 3.4 */ @ToRemove(version = 3.4) @Hack @Deprecated public int getUsedLanguageVersion(@NotNull SLanguage usedLanguage, boolean check) { ModuleDescriptor moduleDescriptor = getModuleDescriptor(); if (!checkDescriptorNotNull(moduleDescriptor)) { return -1; } Integer res = moduleDescriptor.getLanguageVersions().get(usedLanguage); if (res == null) { if (check) { LOG.warn(String.format( "#getUsedLanguageVersion can't find a version for language %s in module %s, so it is falling back to the current version of the language. " + "Probably the language is not imported into this module or #validateLanguageVersions() was not called on this module in appropriate moment." + "NB: there might be migrations which must be applied, however they are not going to.", usedLanguage.getQualifiedName(), getModuleName()), new Throwable()); } return usedLanguage.getLanguageVersion(); } return res; } public int getDependencyVersion(@NotNull SModule dependency) { return getDependencyVersion(dependency, true); } /** * has a fallback if the dependency is absent in the module descriptor. if it happens then returns simply the current dep. module version * * @param check is whether to show error for not found version */ public int getDependencyVersion(@NotNull SModule dependency, boolean check) { ModuleDescriptor moduleDescriptor = getModuleDescriptor(); if (!checkDescriptorNotNull(moduleDescriptor)) { return -1; } Integer res = moduleDescriptor.getDependencyVersions().get(dependency.getModuleReference()); if (res == null) { if (check) { LOG.error( "#getDependencyVersion can't find a version for module " + dependency.getModuleName() + " in module " + getModuleName() + "." + " This can either mean that the module is not visible from this module or that " + "#validateDependencyVersions() was not called on this module in appropriate moment.", new Throwable()); } return ((AbstractModule) dependency).getModuleVersion(); } return res; } /** * @return true iff descriptor is not null */ @Contract("null -> false") private boolean checkDescriptorNotNull(ModuleDescriptor moduleDescriptor) { if (moduleDescriptor == null) { LOG.warn("Descriptor is null " + this + "; returning -1"); return false; } return true; } }