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