/* * 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.components.CoreComponent; import jetbrains.mps.extapi.module.EditableSModule; import jetbrains.mps.extapi.module.SModuleBase; import jetbrains.mps.extapi.module.SRepositoryBase; import jetbrains.mps.extapi.module.SRepositoryExt; import jetbrains.mps.project.AbstractModule; import jetbrains.mps.project.Project; import jetbrains.mps.project.ProjectManager; import jetbrains.mps.util.annotation.ToRemove; import jetbrains.mps.util.containers.ManyToManyMap; import org.apache.log4j.LogManager; import org.apache.log4j.Logger; import org.jetbrains.annotations.NotNull; import org.jetbrains.mps.openapi.model.EditableSModel; import org.jetbrains.mps.openapi.module.RepositoryAccess; 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.SearchScope; import org.jetbrains.mps.openapi.repository.CommandListener; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashSet; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; public class MPSModuleRepository extends SRepositoryBase implements CoreComponent, SRepositoryExt { private static final Logger LOG = LogManager.getLogger(MPSModuleRepository.class); private static MPSModuleRepository ourInstance; private final GlobalModelAccess myGlobalModelAccess; private final CommandListener myCommandListener; private Set<SModule> myModules = new LinkedHashSet<>(); private Map<SModuleId, SModule> myIdToModuleMap = new ConcurrentHashMap<>(); private ManyToManyMap<SModule, MPSModuleOwner> myModuleToOwners = new ManyToManyMap<>(); /** * Use {@link org.jetbrains.mps.openapi.module.SRepository} from the project whenever it is possible * <p> * Currently the context object is an MPS project class * * @see jetbrains.mps.project.Project * <p> * It can provide a repository and a model access * {@link jetbrains.mps.project.Project#getModelAccess()} * {@link jetbrains.mps.project.Project#getRepository()}} * <p> * So in each case you must have an MPS project within your scope and request SRepository explicitily from the project. * <p/> * To access register/unregister methods for modules, consider using {@link SRepositoryExt} * @since 3.2 * @deprecated */ @Deprecated @ToRemove(version = 3.4) public static MPSModuleRepository getInstance() { return ourInstance; } public MPSModuleRepository() { myGlobalModelAccess = new GlobalModelAccess(); myCommandListener = new CommandListener() { @Override public void commandStarted() { fireCommandStarted(); } @Override public void commandFinished() { fireCommandFinished(); } }; } @Override public void init() { super.init(); if (ourInstance != null) { throw new IllegalStateException("already initialized"); } ourInstance = this; myGlobalModelAccess.addCommandListener(myCommandListener); } @Override public void dispose() { myGlobalModelAccess.removeCommandListener(myCommandListener); ourInstance = null; super.dispose(); } //-----------------register/unregister-merge----------- @Override public <T extends SModule> T registerModule(@NotNull T moduleToRegister, @NotNull MPSModuleOwner owner) { getModelAccess().checkWriteAccess(); SModuleId moduleId = moduleToRegister.getModuleReference().getModuleId(); String moduleFqName = moduleToRegister.getModuleName(); AbstractModule aModuleToRegister = (AbstractModule) moduleToRegister; SModule existing = getModule(moduleId); if (existing != null) { //paranoid check relates to MPS-24219 if (existing.getClass() != moduleToRegister.getClass()) { throw new RuntimeException("Module to register has class " + moduleToRegister.getClass().getSimpleName() + ", while there's already another module with the same id registered with class " + existing.getClass().getSimpleName()); } if (!Objects.equals(existing.getModuleName(), moduleFqName)) { String msg = "Trying to register a module with the same identity but different name. There's module '%s' in the repository, and new module is '%s'.\n" + "Original module comes from %s, contesting from %s"; LOG.error(String.format(msg, existing.getModuleName(), moduleFqName, ((AbstractModule) existing).getDescriptorFile(), aModuleToRegister.getDescriptorFile())); } myModuleToOwners.addLink(existing, owner); return (T) existing; } myIdToModuleMap.put(moduleToRegister.getModuleId(), moduleToRegister); myModules.add(moduleToRegister); checkModelsAreNotChanged(aModuleToRegister); aModuleToRegister.attach(this); myModuleToOwners.addLink(moduleToRegister, owner); invalidateCaches(); fireModuleAdded(moduleToRegister); return moduleToRegister; } // Adding not saved model can cause data loss, see MPS-18743. private void checkModelsAreNotChanged(AbstractModule aModuleToRegister) { for (org.jetbrains.mps.openapi.model.SModel model : aModuleToRegister.getModels()) { if (model instanceof EditableSModel && ((EditableSModel) model).isChanged()) { LOG.error("Added a module with unsaved model to a repository. " + "Modify models that are not added to a module or modify them when they are in repo already", new Throwable()); break; } } } public void unregisterModules(Collection<SModule> modules, MPSModuleOwner owner) { Collection<SModule> modulesToDispose = new ArrayList<>(); for (SModule module : modules) { if (doUnregisterModule(module, owner)) { modulesToDispose.add(module); } } if (modulesToDispose.isEmpty()) { return; } invalidateCaches(); for (SModule module : modulesToDispose) { fireModuleRemoved(module.getModuleReference()); ((SModuleBase) module).dispose(); } } @Override public void unregisterModule(@NotNull SModule module, @NotNull MPSModuleOwner owner) { getModelAccess().checkWriteAccess(); boolean moduleRemoved = doUnregisterModule(module, owner); invalidateCaches(); if (moduleRemoved) { fireModuleRemoved(module.getModuleReference()); ((SModuleBase) module).dispose(); } } /** * Unregister specified module from specified owner and conditionally remove module from ModuleRepository if there * are no more owners. * <p/> * Clients are responsible for: * - calling invalidateCaches() * - firing moduleRemoved/repositoryChanged notifications if module was removed/was not removed from ModuleRepository * - disposing module if it was removed * * @return true if module was removed from ModuleRepository */ private boolean doUnregisterModule(SModule module, MPSModuleOwner owner) { getModelAccess().checkWriteAccess(); if (!myModules.contains(module)) { throw new IllegalArgumentException("Trying to unregister non-registered module: " + module); } myModuleToOwners.removeLink(module, owner); boolean remove = myModuleToOwners.getByFirst(module).isEmpty(); if (remove) { fireBeforeModuleRemoved(module); myModules.remove(module); myIdToModuleMap.remove(module.getModuleReference().getModuleId()); return true; } return false; } //---------------get by----------------------------- @NotNull @Override public org.jetbrains.mps.openapi.module.ModelAccess getModelAccess() { return myGlobalModelAccess; } @Override public RepositoryAccess getRepositoryAccess() { return null; } public Set<SModule> getModules(MPSModuleOwner moduleOwner) { //todo assertCanRead(); return Collections.unmodifiableSet(myModuleToOwners.getBySecond(moduleOwner)); } public Set<MPSModuleOwner> getOwners(SModule module) { getModelAccess().checkReadAccess(); return Collections.unmodifiableSet(myModuleToOwners.getByFirst(module)); } /** * @deprecated the repository must be able to contain two modules with the same name. * Thus one cannot rely on the module - name one-to-one correspondence. */ @Deprecated @ToRemove(version = 3.4) /*package*/ SModule getModuleByFqName(@NotNull String fqName) { LOG.error("Use of MPSModuleRepository.getModuleByFqName(String) may yield wrong result due to ambiguity. This method gives first module with matching name"); getModelAccess().checkReadAccess(); // if getModule(SModuleId) checks, why not byName()? return myModules.stream().filter(m -> fqName.equals(m.getModuleName())).findFirst().orElse(null); } @Override public SModule getModule(@NotNull SModuleId id) { getModelAccess().checkReadAccess(); return myIdToModuleMap.get(id); } @NotNull @Override public Iterable<SModule> getModules() { getModelAccess().checkReadAccess(); return Collections.unmodifiableSet(myModules); } //-------------------------------------------------- // TODO: !! // FIXME: we should invalidate caches only in specific modules // The problem is that the scope collects transitive dependencies as well public void invalidateCaches() { getModelAccess().runReadAction(new Runnable() { @Override public void run() { for (Project p : ProjectManager.getInstance().getOpenedProjects()) { p.getScope().invalidateCaches(); } for (SModule m : getModules()) { SearchScope moduleScope = ((AbstractModule) m).getScope(); ((AbstractModule.ModuleScope) moduleScope).invalidateCaches(); } } }); } //------------------listeners-------------------- @Override public void saveAll() { getModelAccess().checkWriteAccess(); long beginTime = System.nanoTime(); LOG.debug("Saving repository"); try { for (SModule module : getModules()) { if (module instanceof EditableSModule) { EditableSModule editableModule = (EditableSModule) module; if (editableModule.isChanged()) { editableModule.save(); } } } SModelRepository.getInstance().saveAll(); } finally { LOG.info(String.format("Saving of the repository took %.3f s", (System.nanoTime() - beginTime) / 1e9)); } } //-------------------DEPRECATED @Deprecated //use ModuleRepositoryFacade instead public SModule getModule(@NotNull SModuleReference ref) { return ModuleRepositoryFacade.getInstance().getModule(ref); } }