LtkRenameAction.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.ui;
import java.lang.reflect.InvocationTargetException;
import java.util.Objects;
import org.eclipse.core.resources.IncrementalProjectBuilder;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Status;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.window.Window;
import org.eclipse.ltk.core.refactoring.Change;
import org.eclipse.ltk.core.refactoring.RefactoringStatus;
import org.eclipse.ltk.core.refactoring.participants.ProcessorBasedRefactoring;
import org.eclipse.ltk.core.refactoring.participants.RefactoringProcessor;
import org.eclipse.ltk.ui.refactoring.RefactoringUI;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IEditorReference;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.actions.WorkspaceModifyOperation;
import org.eclipse.ui.statushandlers.StatusManager;
import org.eclipse.xtext.ui.editor.XtextEditor;
import org.eclipse.xtext.ui.refactoring.impl.AbstractRenameProcessor;
import org.eclipse.xtext.ui.refactoring.ui.SyncUtil;
import org.osate.ge.internal.GraphicalEditorException;
import org.osate.ge.internal.services.AgeAction;
import org.osate.ge.internal.services.ModelChangeNotifier;
import org.osate.ge.internal.services.ModelChangeNotifier.Lock;
import org.osate.ge.internal.services.ProjectProvider;
import org.osate.xtext.aadl2.ui.internal.Aadl2Activator;
import org.osgi.framework.FrameworkUtil;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.name.Named;
/**
* Action which renames a model element using a Language Toolkit (LTK) rename refactoring.
*
*/
@SuppressWarnings("restriction")
public class LtkRenameAction implements AgeAction {
private final ProjectProvider projectProvider;
private final ModelChangeNotifier modelChangeNotifier;
private final BusinessObjectSupplier boSupplier;
private final String originalName;
private final String newName;
/**
* Interface for providing the action with the model element to be renamed. This is required because elements are invalidated whenever a
* rename occurs, this is needed to ensure undo and redo actions can retrieve the appropriate model element.
*
*/
public static interface BusinessObjectSupplier {
/**
* Returns the business object that should be renamed based on its current name
* @param currentName is the current name of the object
* @return the business object to rename
*/
EObject getBusinessObject(final String currentName);
}
/**
* Creates a new instance
* @param projectProvider the provider which returns the project being modified. The project is built during the modification process.
* @param modelChangeNotifier the model change notifier used to lock the model
* @param boSupplier the supplier of the model element to modify
* @param newName the name to which the model element should be renamed
* @param originalName the current name of the model element.
*/
public LtkRenameAction(final ProjectProvider projectProvider, final ModelChangeNotifier modelChangeNotifier,
final BusinessObjectSupplier boSupplier,
final String newName, final String originalName) {
this.projectProvider = Objects.requireNonNull(projectProvider, "projectProvider must not be null");
this.modelChangeNotifier = Objects.requireNonNull(modelChangeNotifier, "modelChangeNotifier must not be null");
this.boSupplier = Objects.requireNonNull(boSupplier, "boSupplier must not be null");
this.newName = newName;
this.originalName = originalName;
}
@Override
public boolean canExecute() {
return boSupplier.getBusinessObject(originalName) != null && newName != null;
}
@Override
public boolean isValid() {
return boSupplier.getBusinessObject(originalName) != null;
}
@Override
public AgeAction execute() {
final EObject bo = boSupplier.getBusinessObject(originalName);
if (bo == null) {
throw new GraphicalEditorException("Unable to retrieve business object to rename.");
}
return renameWithLtk(bo, newName)
? new LtkRenameAction(projectProvider, modelChangeNotifier, boSupplier, originalName, newName)
: null;
}
/**
* Renames the specified model element using an LTK rename refactoring.
* @param bo the model element to rename
* @param value the new name for the model element
* @return true if the rename occurred
*/
private boolean renameWithLtk(final EObject bo, final String value) {
// Lock the diagram to treat all model change notifications as part of the current action.
// Prevent model notification changes from being sent until after the refactoring
try (Lock lock = modelChangeNotifier.lock()) {
// Rename the element using LTK
final ProcessorBasedRefactoring renameRefactoring = RenameUtil.getRenameRefactoring(bo);
final RefactoringStatus refactoringStatus = prepareAndCheck(renameRefactoring, value);
if (!refactoringStatus.isOK()) {
final Dialog dlg = RefactoringUI.createRefactoringStatusDialog(refactoringStatus,
Display.getCurrent().getActiveShell(), "Refactoring", false);
if (dlg.open() != Window.OK) {
// Abort
return false;
}
}
try {
final Change change = renameRefactoring.createChange(new NullProgressMonitor());
new WorkspaceModifyOperation() {
@Override
protected void execute(IProgressMonitor monitor) throws CoreException {
// Perform the modification
change.perform(monitor);
// Build the project, reconcile all open AADL text editors and then build again.
// This seems to be the best way to ensure that all the model change events have been
// queued before the model change notification lock is released
buildProject();
ensureReconciled() ;
buildProject();
}
}.run(null);
} catch (final InterruptedException e) {
Thread.currentThread().interrupt();
throw new GraphicalEditorException(e);
} catch (final RuntimeException | InvocationTargetException | CoreException e) {
throw new GraphicalEditorException(e);
}
}
return true;
}
/**
* Performs an incremental build of the project returned by the project provider
*/
private void buildProject() {
try {
projectProvider.getProject().build(IncrementalProjectBuilder.INCREMENTAL_BUILD, new NullProgressMonitor());
} catch (final CoreException e) {
// Log and ignore any errors that occur while building the project
StatusManager.getManager()
.handle(new Status(IStatus.ERROR, FrameworkUtil.getBundle(getClass()).getSymbolicName(),
"Error building projects during rename", e), StatusManager.LOG);
}
}
/**
* Ensure that all AADL text editors have been reconciled.
*/
private void ensureReconciled() {
final String languageName = getLanguageName();
for (final IEditorReference editorRef : PlatformUI.getWorkbench()
.getActiveWorkbenchWindow()
.getActivePage()
.getEditorReferences()) {
final IEditorPart editor = editorRef.getEditor(false);
if (editor instanceof XtextEditor) {
final XtextEditor xtextEditor = (XtextEditor) editor;
// Only ensure reconciliation of AADL editors
if (Objects.equals(xtextEditor.getLanguageName(), languageName)) {
final SyncUtil syncUtil = Aadl2Activator.getInstance()
.getInjector(Aadl2Activator.ORG_OSATE_XTEXT_AADL2_AADL2)
.getInstance(SyncUtil.class);
// Only waiting once will result in the reconciler processing a change outside the lock.
// Doing it twice appears to wait for pending runs of the reconciler.
syncUtil.waitForReconciler(xtextEditor);
syncUtil.waitForReconciler(xtextEditor);
}
}
}
}
/**
* Configures the refactoring to rename the model element to the specified name and checks conditions.
* @param refactoring the refactoring object which will be used to rename the model element.
* @param newName the new name for the model element
* @return the status of the refactoring operation. An OK status indicates that the refactoring can proceed.
*/
private static RefactoringStatus prepareAndCheck(final ProcessorBasedRefactoring refactoring,
final String newName) {
try {
if (refactoring == null) {
return RefactoringStatus.createFatalErrorStatus("Refactoring is null");
}
final RefactoringProcessor refactoringProcessor = refactoring.getProcessor();
if (!(refactoringProcessor instanceof AbstractRenameProcessor)) {
return RefactoringStatus
.createFatalErrorStatus("refactoringProcessor is not an AbstractRenameProcessor");
}
// Set the name
((AbstractRenameProcessor) refactoringProcessor).setNewName(newName);
final RefactoringStatus initialStatus = refactoring.checkInitialConditions(new NullProgressMonitor());
if (!initialStatus.isOK()) {
return initialStatus;
}
final RefactoringStatus finalStatus = refactoring.checkFinalConditions(new NullProgressMonitor());
if (!finalStatus.isOK()) {
return finalStatus;
}
} catch (final CoreException ex) {
return RefactoringStatus.create(ex.getStatus());
}
return new RefactoringStatus();
}
/**
* Class that is used with a dependency injector to retrieve the language name used the AADL Xtext editors
*
*/
private static class LanguageNameRetriever {
@Inject
@Named(org.eclipse.xtext.Constants.LANGUAGE_NAME)
public String languageName;
}
/**
* Retrieves the AADL xtext language name by injecting it into a new object.
* @return the language name used by AADL Xtext editors.
*/
private static String getLanguageName() {
final Injector injector = Objects.requireNonNull(
Aadl2Activator.getInstance().getInjector(Aadl2Activator.ORG_OSATE_XTEXT_AADL2_AADL2),
"Unable to retrieve injector");
final LanguageNameRetriever obj = injector.getInstance(LanguageNameRetriever.class);
return obj.languageName;
}
}