DefaultModelChangeNotifier.java
/**
* Copyright (c) 2004-2025 Carnegie Mellon University and others. (see Contributors file).
* All Rights Reserved.
*
* NO WARRANTY. ALL MATERIAL IS FURNISHED ON AN "AS-IS" BASIS. CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY
* KIND, EITHER EXPRESSED OR IMPLIED, AS TO ANY MATTER INCLUDING, BUT NOT LIMITED TO, WARRANTY OF FITNESS FOR PURPOSE
* OR MERCHANTABILITY, EXCLUSIVITY, OR RESULTS OBTAINED FROM USE OF THE MATERIAL. CARNEGIE MELLON UNIVERSITY DOES NOT
* MAKE ANY WARRANTY OF ANY KIND WITH RESPECT TO FREEDOM FROM PATENT, TRADEMARK, OR COPYRIGHT INFRINGEMENT.
*
* This program and the accompanying materials are made available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
* SPDX-License-Identifier: EPL-2.0
*
* Created, in part, with funding and support from the United States Government. (see Acknowledgments file).
*
* This program includes and/or can make use of certain third party source code, object code, documentation and other
* files ("Third Party Software"). The Third Party Software that is used by this program is dependent upon your system
* configuration. By using this program, You agree to comply with any and all relevant Third Party Software terms and
* conditions contained in any such Third Party Software or separate license file distributed with such Third Party
* Software. The parties who own the Third Party Software ("Third Party Licensors") are intended third party benefici-
* aries to this license with respect to the terms applicable to their Third Party Software. Third Party Software li-
* censes only apply to the Third Party Software and not any other portion of this program or this program as a whole.
*/
package org.osate.ge.internal.services.impl;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceChangeEvent;
import org.eclipse.core.resources.IResourceChangeListener;
import org.eclipse.core.resources.IResourceDelta;
import org.eclipse.core.resources.IResourceDeltaVisitor;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.e4.core.contexts.IEclipseContext;
import org.eclipse.emf.common.util.URI;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.statushandlers.StatusManager;
import org.osate.ge.internal.GraphicalEditorException;
import org.osate.ge.internal.services.ModelChangeNotifier;
import org.osate.ge.internal.ui.xtext.AgeXtextUtil;
import org.osate.ge.internal.ui.xtext.XtextDocumentChangeListener;
import org.osgi.framework.FrameworkUtil;
/**
* {@link ModelChangeNotifier} implementation
*
*/
public class DefaultModelChangeNotifier implements ModelChangeNotifier {
private final ProjectDeltaVisitor projectVisitor = new ProjectDeltaVisitor();
private final AadlResourceUriCollectorVisitor resourceUriCollectorVisitor = new AadlResourceUriCollectorVisitor();
private final List<ChangeListener> changeListeners = new CopyOnWriteArrayList<>();
private Lock currentLock;
private final Set<URI> pendingChangedResourceUris = new HashSet<>();
private boolean hasModelChanged = false;
private boolean hasChangesWhileUnlocked = false;
/**
* Context function which instantiates this service
*/
public static class ContextFunction extends SimpleServiceContextFunction<ModelChangeNotifier> {
@Override
public ModelChangeNotifier createService(final IEclipseContext context) {
return new DefaultModelChangeNotifier();
}
@Override
protected void deactivate() {
// Dispose the service if it is valid
final ModelChangeNotifier service = getService();
if (service instanceof DefaultModelChangeNotifier) {
((DefaultModelChangeNotifier) service).dispose();
}
super.deactivate();
}
}
// Marks model has changed if a notification affects a project.
private class ProjectDeltaVisitor implements IResourceDeltaVisitor {
@Override
public boolean visit(final IResourceDelta delta) {
final IResource resource = delta.getResource();
// Trigger a change if the project itself has changed
if (resource.getType() == IResource.PROJECT && (delta.getKind() != IResourceDelta.CHANGED
|| (delta.getKind() == IResourceDelta.CHANGED && delta.getFlags() != 0))) {
hasModelChanged = true;
if (currentLock == null) {
hasChangesWhileUnlocked = true;
}
}
return resource.getType() == IResource.ROOT && !hasModelChanged;
}
}
private class AadlResourceUriCollectorVisitor implements IResourceDeltaVisitor {
@Override
public boolean visit(final IResourceDelta delta) {
final IResource resource = delta.getResource();
// aadlbin changes indicate an AADL file has been (re)built.
if (resource.getType() == IResource.FILE && ("aadl".equalsIgnoreCase(resource.getFileExtension())
|| "aadlbin".equalsIgnoreCase(resource.getFileExtension()))) {
final URI resourceUri = URI.createPlatformResourceURI(resource.getFullPath().toString(), true);
pendingChangedResourceUris.add(resourceUri);
return false;
}
return true;
}
}
private final IResourceChangeListener resourceChangeListener = event -> {
final IResourceDelta delta = event.getDelta();
if(delta != null) {
try {
delta.accept(resourceUriCollectorVisitor);
hasModelChanged = hasModelChanged || !pendingChangedResourceUris.isEmpty();
if (hasModelChanged) {
if (currentLock == null) {
hasChangesWhileUnlocked = true;
}
} else {
delta.accept(projectVisitor);
}
} catch (final CoreException e) {
// Log and ignore
StatusManager.getManager()
.handle(new Status(IStatus.ERROR, FrameworkUtil.getBundle(getClass()).getSymbolicName(),
"Error listening for resource changes", e), StatusManager.LOG);
}
Display.getDefault().syncExec(this::handleNotifications);
}
};
private XtextDocumentChangeListener xtextModelListener = resourceUri -> {
if (resourceUri == null) {
return;
}
final String platformString = resourceUri.toPlatformString(true);
if (platformString == null) {
return;
}
if (platformString.toLowerCase().endsWith(".aadl")) {
pendingChangedResourceUris.add(resourceUri);
hasModelChanged = true;
if (currentLock == null) {
hasChangesWhileUnlocked = true;
}
handleNotifications();
}
};
// Checks notifications. If there is a lock then it does nothing. Change notifications will wait until the lock is closed. If there isn't a lock, it
// notifies listeners of changes that are pending.
private synchronized void handleNotifications() {
if (currentLock == null && (!pendingChangedResourceUris.isEmpty() || hasModelChanged)) {
// Make copy of the flags so that recursive calls to handle notifications will not trigger an update for the same resources
final List<URI> changedResourceUrisBeingProcessed = new ArrayList<>(pendingChangedResourceUris);
final boolean hadModelChanged = hasModelChanged;
final boolean wasModelLocked = !hasChangesWhileUnlocked;
// Reset flags
pendingChangedResourceUris.clear();
hasModelChanged = false;
hasChangesWhileUnlocked = false;
// Send notifications. Notifications are sent using the display thread so that all diagrams updates will take place in the main thread and avoid
// updating the diagram while change notifications are being handled.
Display.getDefault().asyncExec(() -> {
// Send resource change notifications
for (final URI resourceUri : changedResourceUrisBeingProcessed) {
for (final ChangeListener listener : changeListeners) {
listener.resourceChanged(resourceUri);
}
}
// Send a single notification that the model has changed regardless of the number of changes
if (hadModelChanged) {
onModelChanged(wasModelLocked);
}
});
}
}
private DefaultModelChangeNotifier() {
// Register a resource change listener
final IWorkspace workspace = ResourcesPlugin.getWorkspace();
workspace.addResourceChangeListener(resourceChangeListener, IResourceChangeEvent.POST_BUILD);
// Listen for xtext model changes
AgeXtextUtil.addDocumentListener(xtextModelListener);
}
private void dispose() {
// Stop listening for xtext model changes
AgeXtextUtil.removeDocumentListener(xtextModelListener);
// Remove the resource change listener
final IWorkspace workspace = ResourcesPlugin.getWorkspace();
workspace.removeResourceChangeListener(resourceChangeListener);
}
private void onModelChanged(final boolean modelWasLocked) {
// Send a model changed notification
for(final ChangeListener listener : changeListeners) {
listener.modelChanged(modelWasLocked);
}
// Send an after model changed notification
for(final ChangeListener listener : changeListeners) {
listener.afterModelChangeNotification();
}
}
@Override
public void addChangeListener(final ChangeListener listener) {
changeListeners.add(Objects.requireNonNull(listener, "listener must not be null"));
}
@Override
public void removeChangeListener(final ChangeListener listener) {
changeListeners.remove(Objects.requireNonNull(listener, "listener must not be null"));
}
@Override
public synchronized Lock lock() {
if(currentLock != null) {
throw new GraphicalEditorException("Already locked");
}
// Create a new lock
currentLock = new Lock() {
@Override
public void close() {
synchronized (DefaultModelChangeNotifier.this) {
if (currentLock != this) {
throw new GraphicalEditorException("Not the current lock");
}
currentLock = null;
// Send pending notifications
handleNotifications();
}
}
};
return currentLock;
}
}