/*
* 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.smodel.event.SModelChildEvent;
import jetbrains.mps.smodel.event.SModelDevKitEvent;
import jetbrains.mps.smodel.event.SModelEvent;
import jetbrains.mps.smodel.event.SModelFileChangedEvent;
import jetbrains.mps.smodel.event.SModelImportEvent;
import jetbrains.mps.smodel.event.SModelLanguageEvent;
import jetbrains.mps.smodel.event.SModelListener;
import jetbrains.mps.smodel.event.SModelPropertyEvent;
import jetbrains.mps.smodel.event.SModelReferenceEvent;
import jetbrains.mps.smodel.event.SModelRenamedEvent;
import jetbrains.mps.smodel.event.SModelRootEvent;
import jetbrains.mps.smodel.loading.ModelLoadingState;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.repository.CommandListener;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
/**
* This class serves as a composite listener to events which come from multiple models during Command
*
* @see org.jetbrains.mps.openapi.module.ModelAccess#executeCommand(Runnable)
*/
public abstract class ModelsEventsCollector {
/**
* This lock should be used for synchronizing access to myEvents field.
* This field may be accessed without a model lock from the flush() method,
* so we should take care of this synchronization.
*/
private Object myEventsLock = new Object();
private List<SModelEvent> myEvents = new ArrayList<>();
private SModelListener myModelListener = new SModelDelegateListener();
private Set<SModel> myModelsToListen = new LinkedHashSet<SModel>();
private CommandListener myCommandListener = new MyCommandAdapter();
private volatile boolean myDisposed;
private boolean myIsInCommand;
public ModelsEventsCollector() {
ModelAccess.instance().addCommandListener(myCommandListener);
myIsInCommand = ModelAccess.instance().isInsideCommand();
}
public void startListeningToModel(@NotNull SModel sm) {
checkNotDisposed();
assert !myModelsToListen.contains(sm) :
"EventsCollector was already configured to listen for changes in this model descriptor: " + sm.getReference().toString();
myModelsToListen.add(sm);
((SModelInternal) sm).addModelListener(myModelListener);
}
public void stopListeningToModel(@NotNull SModel sm) {
checkNotDisposed();
((SModelInternal) sm).removeModelListener(myModelListener);
myModelsToListen.remove(sm);
}
public void flush() {
checkNotDisposed();
final List<SModelEvent> wrappedEvents;
synchronized (myEventsLock) {
if (myEvents.isEmpty()) {
return;
}
wrappedEvents = Collections.unmodifiableList(myEvents);
myEvents = new ArrayList<>();
}
ModelAccess.instance().runWriteAction(() -> eventsHappened(wrappedEvents));
}
/**
* invoked with a write lock
*/
protected abstract void eventsHappened(List<SModelEvent> events);
protected void clearCollectedEvents() {
checkNotDisposed();
synchronized (myEventsLock) {
myEvents.clear();
}
}
public void dispose() {
checkNotDisposed();
for (SModel sm : new LinkedHashSet<SModel>(myModelsToListen)) {
stopListeningToModel(sm);
}
ModelAccess.instance().removeCommandListener(myCommandListener);
myDisposed = true;
}
private void checkNotDisposed() {
if (myDisposed) {
throw new IllegalStateException("Disposed events collector was called: " + getClass());
}
}
private class MyCommandAdapter implements CommandListener {
@Override
public void commandStarted() {
if (myDisposed) {
return;
}
synchronized (myEventsLock) {
myEvents.clear();
}
myIsInCommand = true;
}
@Override
public void commandFinished() {
if (myDisposed) {
return;
}
flush();
myIsInCommand = false;
}
}
private class SModelDelegateListener implements SModelListener {
@Override
public void languageAdded(SModelLanguageEvent event) {
listenerInvoked(event);
}
@Override
public void languageRemoved(SModelLanguageEvent event) {
listenerInvoked(event);
}
@Override
public void importAdded(SModelImportEvent event) {
listenerInvoked(event);
}
@Override
public void importRemoved(SModelImportEvent event) {
listenerInvoked(event);
}
@Override
public void devkitAdded(SModelDevKitEvent event) {
listenerInvoked(event);
}
@Override
public void devkitRemoved(SModelDevKitEvent event) {
listenerInvoked(event);
}
@Override
public void rootAdded(SModelRootEvent event) {
listenerInvoked(event);
}
@Override
public void rootRemoved(SModelRootEvent event) {
listenerInvoked(event);
}
@Override
public void beforeRootRemoved(SModelRootEvent event) {
listenerInvoked(event);
}
@Override
public void beforeModelRenamed(SModelRenamedEvent event) {
listenerInvoked(event);
}
@Override
public void modelRenamed(SModelRenamedEvent event) {
listenerInvoked(event);
}
@Override
public void beforeModelFileChanged(SModelFileChangedEvent event) {
listenerInvoked(event);
}
@Override
public void modelFileChanged(SModelFileChangedEvent event) {
listenerInvoked(event);
}
@Override
public void propertyChanged(SModelPropertyEvent event) {
listenerInvoked(event);
}
@Override
public void childAdded(SModelChildEvent event) {
listenerInvoked(event);
}
@Override
public void childRemoved(SModelChildEvent event) {
listenerInvoked(event);
}
@Override
public void beforeChildRemoved(SModelChildEvent event) {
}
@Override
public void referenceAdded(SModelReferenceEvent event) {
listenerInvoked(event);
}
@Override
public void referenceRemoved(SModelReferenceEvent event) {
listenerInvoked(event);
}
@Override
public void modelSaved(SModel sm) {
}
@Override
public void modelLoadingStateChanged(SModel sm, ModelLoadingState newState) {
}
@Override
public void beforeModelDisposed(SModel sm) {
}
@NotNull
@Override
public SModelListenerPriority getPriority() {
return SModelListenerPriority.CLIENT;
}
private void listenerInvoked(SModelEvent event) {
checkNotDisposed();
if (event != null) {
if (!myIsInCommand && !(event instanceof SModelFileChangedEvent)) {
throw new IllegalStateException("Event outside of a command");
}
synchronized (myEventsLock) {
myEvents.add(event);
}
}
}
}
}