/* * 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.project.dependency; import jetbrains.mps.project.DevKit; import jetbrains.mps.smodel.Language; import jetbrains.mps.smodel.ModuleRepositoryFacade; import jetbrains.mps.smodel.SModelAdapter; import jetbrains.mps.smodel.SModelInternal; import jetbrains.mps.smodel.adapter.structure.MetaAdapterFactory; import jetbrains.mps.smodel.event.SModelDevKitEvent; import jetbrains.mps.smodel.event.SModelLanguageEvent; import org.jetbrains.annotations.NotNull; import org.jetbrains.mps.openapi.language.SLanguage; import org.jetbrains.mps.openapi.model.SModel; import org.jetbrains.mps.openapi.module.SModule; import org.jetbrains.mps.openapi.module.SModuleReference; import org.jetbrains.mps.openapi.module.SRepository; import org.jetbrains.mps.openapi.module.SRepositoryContentAdapter; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; /** * Build (and optionally maintain) set of all languages, imported directly and indirectly. * The manager represents snapshot of all imported languages and doesn't update in unless {@link #invalidate() invalidated}. * With {@link #trackModelChanges()} it tracks changes in the designated model and updates own state appropriately. * With {@link #trackRepositoryChanges(org.jetbrains.mps.openapi.module.SRepository)}, changes to repository * would invalidate the manager. * <p>Generally, there are two distinct patterns in using this manager, "lifecycle" and "snapshot":</p> * <pre> * ModelDependenciesManager mdm = new ModelDependenciesManager(model).trackModelChanges().trackRepositoryChanges(repo); * process(mdm.getAllImportedLanguages()); * ... * // changes in models are reflected in MDM * process(mdm.getAllImportedLanguages()); * ... * // much later * mdm.dispose(); * </pre> * vs. * <pre> * ModelDependenciesManager mdm = new ModelDependenciesManager(model) * process(mdm.getAllImportedModels()); * mdm.dispose(); * </pre> * <p/> * FIXME perhaps, worth moving to subpackage of j.m.smodel, as it's pure model functionality, unrelated to project */ public class ModelDependenciesManager { private SModel myModel; private MyModuleWatcher myModuleWatcher; private MySModelWatcher myModelWatcher; private volatile Collection<SLanguage> myCachedDeps; public ModelDependenciesManager(SModel model) { myModel = model; } /** * @return snapshot of model dependencies (up-to-date state depends on listeners installed) */ public Collection<SLanguage> getAllImportedLanguagesIds() { final SModel model = myModel; if (model == null) throw new IllegalStateException("access after disposal"); Collection<SLanguage> tlVal = myCachedDeps; if (tlVal == null) { // I can live with expense of two+ threads building identical set simultaneously (microseconds) // and competing to set it to save use of synchronization primitives tlVal = buildAllLanguages(model, new LinkedHashSet<SLanguage>()); myCachedDeps = tlVal = Collections.unmodifiableCollection(tlVal); } return tlVal; } public Collection<SModuleReference> getAllImportedLanguages() { List<SModuleReference> result = new ArrayList<SModuleReference>(); for (SLanguage lang : getAllImportedLanguagesIds()) { SModule sourceModule = lang.getSourceModule(); if (sourceModule!=null) { result.add(sourceModule.getModuleReference()); } } return result; } public void dispose() { if (myModelWatcher != null) { myModelWatcher.dispose(); myModelWatcher = null; } if (myModuleWatcher != null) { myModuleWatcher.dispose(); myModuleWatcher = null; } myCachedDeps = null; myModel = null; } protected SModel getModel() { return myModel; } protected boolean isDependency(SLanguage langId) { Collection<SLanguage> tlVal = myCachedDeps; return tlVal != null && tlVal.contains(langId); } public void invalidate() { myCachedDeps = null; } protected Collection<SLanguage> buildAllLanguages(@NotNull SModel model, @NotNull Collection<SLanguage> result) { for (SLanguage lang : ((jetbrains.mps.smodel.SModelInternal) model).importedLanguageIds()) { handle(lang, result); } for (SModuleReference dk : ((jetbrains.mps.smodel.SModelInternal) model).importedDevkits()) { DevKit devkit = ModuleRepositoryFacade.getInstance().getModule(dk, DevKit.class); if (devkit == null) continue; handle(devkit, result); } return result; } /** * Process language reference dependency * * @param lang reference to language module, never <code>null</code>. Language it points to not necessarily resolves * @param retval collection to fill with languages of interest */ protected void handle(SLanguage lang, Collection<SLanguage> retval) { retval.add(lang); } /** * Process devkit dependency * * @param devkit reference to devkit, not <code>null</code>. * @param retval collection to fill with languages of interest */ protected void handle(DevKit devkit, Collection<SLanguage> retval) { for (SLanguage dkLang : devkit.getAllExportedLanguageIds()) { handle(dkLang, retval); } } /** * Attach a listener to the model to track dependencies added through SModelInternal * * @return <code>this</code> for convenience */ public ModelDependenciesManager trackModelChanges() { if (myModelWatcher == null) { myModelWatcher = new MySModelWatcher(this); } return this; } /** * Attach a listener to given repository to reflect changes in model's dependencies * * @return <code>this</code> for convenience */ public ModelDependenciesManager trackRepositoryChanges(SRepository repository) { if (myModuleWatcher != null && myModuleWatcher.myRepository != repository) { myModuleWatcher.dispose(); } myModuleWatcher = new MyModuleWatcher(this, repository); return this; } private static class MySModelWatcher extends SModelAdapter { private final ModelDependenciesManager myDepManager; private SModel mySModelDescriptor; private MySModelWatcher(ModelDependenciesManager mdm) { myDepManager = mdm; mySModelDescriptor = mdm.getModel(); registerSelf(); } @Override public void devkitAdded(SModelDevKitEvent event) { myDepManager.invalidate(); } @Override public void devkitRemoved(SModelDevKitEvent event) { myDepManager.invalidate(); } @Override public void languageAdded(SModelLanguageEvent event) { myDepManager.invalidate(); } @Override public void languageRemoved(SModelLanguageEvent event) { myDepManager.invalidate(); } public void dispose() { unregisterSelf(); this.mySModelDescriptor = null; } private void registerSelf() { ((SModelInternal) mySModelDescriptor).addModelListener(this); } private void unregisterSelf() { ((SModelInternal) mySModelDescriptor).removeModelListener(this); } } private static class MyModuleWatcher extends SRepositoryContentAdapter { private final SRepository myRepository; private final ModelDependenciesManager myDepManager; private MyModuleWatcher(ModelDependenciesManager mdm, SRepository repository) { myDepManager = mdm; myRepository = repository; subscribeTo(myRepository); } @Override public void beforeModuleRemoved(@NotNull SModule module) { invalidateIfWatching(module); } @Override public void moduleChanged(SModule module) { invalidateIfWatching(module); } @Override public void modelAdded(SModule module, SModel model) { invalidateIfWatching(module); } private void invalidateIfWatching(SModule module) { if ((module instanceof Language)) { SLanguage languageId = MetaAdapterFactory.getLanguage(module.getModuleReference()); if (myDepManager.isDependency(languageId)) { myDepManager.invalidate(); } } } public void dispose() { unsubscribeFrom(myRepository); } } }