/*
* 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.smodel.impl;
import jetbrains.mps.smodel.Language;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.mps.openapi.event.SNodeAddEvent;
import org.jetbrains.mps.openapi.event.SNodeRemoveEvent;
import org.jetbrains.mps.openapi.event.SPropertyChangeEvent;
import org.jetbrains.mps.openapi.event.SReferenceChangeEvent;
import org.jetbrains.mps.openapi.model.EditableSModel;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.model.SModel.Problem;
import org.jetbrains.mps.openapi.model.SModelListener;
import org.jetbrains.mps.openapi.model.SModelReference;
import org.jetbrains.mps.openapi.model.SNodeChangeListener;
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.SRepositoryAttachListener;
import org.jetbrains.mps.openapi.module.SRepositoryListenerBase;
import java.util.HashSet;
import java.util.Set;
/**
* Tracks changes to language concepts throughout whole repository.
* <pre>
* SModel modelWithInstancesOfSomeConcepts;
* StructureAspectChangeTracker t = new StructureAspectChangeTracker(null, new ModuleListener() {
* void structureAspectChanged(...) {
* revalidateCaches();
* }
* });
* t.attachTo(model.getRepository());
* ...
* // code that relies on meta-model, and needs to update once meta-model changes (e.g. caches by concept FQN)
* SNode n;
* myCacheMap.put(n.getConcept().getQualifiedName(), ...);
* ...
* // much later
* t.detachFrom(model.getRepository());
* </pre>
* @author Artem Tikhomirov
*/
public final class StructureAspectChangeTracker extends SRepositoryListenerBase implements SRepositoryAttachListener, SNodeChangeListener, SModelListener {
// I assume model changes come in a single thread, as well as commandFinished notification, and do not care to synchronize
private final Set<SModelReference> myChangedModels = new HashSet<SModelReference>();
private final Set<SModuleReference> myChangedModules = new HashSet<SModuleReference>();
private final ModelListener myModelListener;
private final ModuleListener myModuleListener;
// it's reasonable to share SACT instance and to register listeners with it, but at the moment there's no place where
// I can keep this instance, hence use single listener approach.
public StructureAspectChangeTracker(ModelListener l1, ModuleListener l2) {
assert l1 != null || l2 != null;
myModelListener = l1;
myModuleListener = l2;
}
@Override
public void startListening(@NotNull SRepository repository) {
for (SModule module : repository.getModules()) {
startListening(module);
}
}
@Override
public void stopListening(SRepository repository) {
for (SModule module : repository.getModules()) {
stopListening(module);
}
}
private boolean isLanguageAndCanChange(SModule module) {
return !module.isReadOnly() && module instanceof Language;
}
private void startListening(SModule module) {
if (!isLanguageAndCanChange(module)) {
return;
}
final SModel structureModel = ((Language) module).getStructureModelDescriptor();
if (structureModel == null) {
return;
}
if (structureModel instanceof EditableSModel) {
((EditableSModel) structureModel).addChangeListener(this);
}
structureModel.addModelListener(this);
}
private void stopListening(SModule module) {
if (!isLanguageAndCanChange(module)) {
return;
}
final SModel structureModel = ((Language) module).getStructureModelDescriptor();
if (structureModel == null) {
return;
}
if (structureModel instanceof EditableSModel) {
((EditableSModel) structureModel).removeChangeListener(this);
}
structureModel.removeModelListener(this);
}
@Override
public void moduleAdded(@NotNull SModule module) {
startListening(module);
}
@Override
public void beforeModuleRemoved(@NotNull SModule module) {
stopListening(module);
}
@Override
public void nodeAdded(@NotNull SNodeAddEvent event) {
structureModelChanged(event.getModel());
}
@Override
public void nodeRemoved(@NotNull SNodeRemoveEvent event) {
structureModelChanged(event.getModel());
}
@Override
public void propertyChanged(@NotNull SPropertyChangeEvent event) {
// e.g. 'name' property change constitutes concept rename refactoring
structureModelChanged(event.getModel());
}
@Override
public void referenceChanged(@NotNull SReferenceChangeEvent event) {
// e.g. adding 'extends' references might change concept hierarchy completely
structureModelChanged(event.getModel());
}
private void structureModelChanged(SModel model) {
if (myModelListener != null) {
myChangedModels.add(model.getReference());
}
if (myModuleListener != null) {
myChangedModules.add(model.getModule().getModuleReference());
}
}
@Override
public void commandFinished(SRepository repository) {
fireStructureAspectChanged();
}
//
// SModelListener - care about model unload/replace, fire structure aspect changed event right away
//
@Override
public void modelLoaded(SModel model, boolean partially) {
// no-op
}
@Override
public void modelReplaced(SModel model) {
structureModelChanged(model);
fireStructureAspectChanged();
}
@Override
public void modelUnloaded(SModel model) {
structureModelChanged(model);
fireStructureAspectChanged();
}
@Override
public void modelSaved(SModel model) {
// no-op
}
@Override
public void conflictDetected(SModel model) {
// no-op
}
@Override
public void problemsDetected(SModel model, Iterable<Problem> problems) {
// no-op
}
@Override
public void modelAttached(SModel model, SRepository repository) {
// no-op
}
@Override
public void modelDetached(SModel model, SRepository repository) {
// no-op
}
//
// SModelListener
//
private void fireStructureAspectChanged() {
if (!myChangedModels.isEmpty()) {
HashSet<SModelReference> copy = new HashSet<SModelReference>(myChangedModels);
myChangedModels.clear();
myModelListener.structureAspectChanged(copy);
}
if (!myChangedModules.isEmpty()) {
HashSet<SModuleReference> copy = new HashSet<SModuleReference>(myChangedModules);
myChangedModules.clear();
myModuleListener.structureAspectChanged(copy);
}
}
public interface ModelListener {
void structureAspectChanged(Set<SModelReference> changedStructureModels);
}
public interface ModuleListener {
void structureAspectChanged(Set<SModuleReference> changedModules);
}
}