EmbeddedTextModificationAction.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.ba.ui;
import java.io.IOException;
import java.util.Objects;
import java.util.function.Supplier;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IncrementalProjectBuilder;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.emf.transaction.RecordingCommand;
import org.eclipse.emf.transaction.TransactionalEditingDomain;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IEditorReference;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.statushandlers.StatusManager;
import org.eclipse.xtext.resource.SaveOptions;
import org.eclipse.xtext.resource.XtextResource;
import org.eclipse.xtext.ui.editor.XtextEditor;
import org.eclipse.xtext.ui.editor.model.IXtextDocument;
import org.eclipse.xtext.util.concurrent.IUnitOfWork;
import org.eclipse.xtext.util.concurrent.IUnitOfWork.Void;
import org.osate.aadl2.modelsupport.Activator;
import org.osate.ge.ProjectUtil;
import org.osate.ge.aadl2.AadlGraphicalEditorException;
import org.osate.ge.ba.ui.properties.EditableEmbeddedTextValue;
import org.osate.ge.ba.util.BehaviorAnnexXtextUtil;
import org.osate.ge.internal.services.AgeAction;
import org.osate.ge.internal.services.ModelChangeNotifier;
import org.osate.ge.internal.services.ModelChangeNotifier.Lock;
/**
* Modification process to be executed to update embedded text resources
*/
class EmbeddedTextModificationAction implements AgeAction {
private final ModelChangeNotifier modelChangeNotifier;
private final Supplier<EmbeddedTextModificationAction> embeddedEditingActionSupplier;
/**
* Instantiates an Embedded Text Modification action when the editor is open
* @param xtextDocument is the open Xtext Document to be modified
* @param modelChangeNotifier notifies of model changes
* @param source is the full modified AADL source to replace in the xtextDocument
*/
public EmbeddedTextModificationAction(final IXtextDocument xtextDocument,
final ModelChangeNotifier modelChangeNotifier, final String source) {
this.modelChangeNotifier = Objects.requireNonNull(modelChangeNotifier, "modelChangeNotifier cannot be null");
embeddedEditingActionSupplier = () -> {
// Get original text for undo/redo
final String originalText = BehaviorAnnexXtextUtil.getText(xtextDocument, null);
// Modify the document
prepareToEditDocument(xtextDocument);
xtextDocument.set(source);
// Call readonly on the document. This will should cause Xtext's reconciler
// to be called to ensure the document matches the model and trigger model change events.
xtextDocument.readOnly(res -> null);
ProjectUtil.getProject(xtextDocument.getResourceURI());
// Return the undo/redo action
return new EmbeddedTextModificationAction(xtextDocument, modelChangeNotifier, originalText);
};
}
/**
* Instantiates an Embedded Text Modification Action when the editor is closed
* @param xtextResource is the Xtext Resource to be modified
* @param modelChangeNotifier notifies of model changes
* @param embeddedTextValue is the text information used for editing embedded AADL source
*/
public EmbeddedTextModificationAction(final XtextResource xtextResource,
final ModelChangeNotifier modelChangeNotifier,
final EditableEmbeddedTextValue embeddedTextValue) {
this.modelChangeNotifier = Objects.requireNonNull(modelChangeNotifier, "modelChangeNotifier cannot be null");
final TransactionalEditingDomain editingDomain = TransactionalEditingDomain.Factory.INSTANCE
.getEditingDomain(xtextResource.getResourceSet());
embeddedEditingActionSupplier = () -> {
// Get original text for undo
final String originalText = BehaviorAnnexXtextUtil.getText(null, xtextResource);
// Modify and save the xtext resource
final Void<XtextResource> work = createUpdateProcess(embeddedTextValue);
final RecordingCommand cmd = createRecordingCommand(editingDomain, work, xtextResource);
executeCommand(editingDomain, cmd, xtextResource);
save(xtextResource);
buildProject(ProjectUtil.getProjectOrThrow(xtextResource));
// Return the undo/redo action
return new EmbeddedTextModificationAction(editingDomain, xtextResource, modelChangeNotifier,
originalText);
};
}
/**
* Private constructor for undo/redo action after modification while editor is closed
*/
private EmbeddedTextModificationAction(final TransactionalEditingDomain editingDomain,
final XtextResource xtextResource,
ModelChangeNotifier modelChangeNotifier, final String originalText) {
this.modelChangeNotifier = Objects.requireNonNull(modelChangeNotifier, "modelChangeNotifier cannot be null");
embeddedEditingActionSupplier = () -> {
final String undoRedoOriginalText = BehaviorAnnexXtextUtil.getText(null, xtextResource);
// Modify and save the xtext resource
final Void<XtextResource> work = createUpdateProcess(null, originalText);
final RecordingCommand cmd = createRecordingCommand(editingDomain, work, xtextResource);
executeCommand(editingDomain, cmd, xtextResource);
save(xtextResource);
buildProject(ProjectUtil.getProjectOrThrow(xtextResource));
// Return the undo/redo action
return new EmbeddedTextModificationAction(editingDomain, xtextResource, modelChangeNotifier,
undoRedoOriginalText);
};
}
/**
* Creates an update process for {@link XtextResource}.
* If the specified {@link EditableEmbeddedTextValue} is not null, a section of the
* {@link XtextResource}'s source text will be updated with the specified sourceText value.
* If the specified {@link EditableEmbeddedTextValue} is null, the {@link XtextResource}'s entire
* source text will be set to the specified sourceText value.
* @param embeddedTextValue holds the values used to make a partial update to the {@link XtextResource}'s source text
* @param sourceText is the text to update the {@link XtextResource}'s source text with
*/
private Void<XtextResource> createUpdateProcess(final EditableEmbeddedTextValue embeddedTextValue,
final String sourceText) {
return embeddedTextValue != null
? createPartialUpdateProcess(embeddedTextValue.getPrefix().length(), embeddedTextValue.getUpdateLength(), sourceText)
: createUndoRedoProcess(sourceText);
}
private Void<XtextResource> createUpdateProcess(final EditableEmbeddedTextValue embeddedTextValue) {
return createUpdateProcess(embeddedTextValue, embeddedTextValue.getEditableText());
}
/**
* Creates a process to replace a specific section the existing source text of the {@link XtextResource} with the specified source text.
* @param updateOffset is the offset of the text to be replaced
* @param updateLength is the length of text to be replaced
* @param partialSrcText is the source text to update the section with
*/
private Void<XtextResource> createPartialUpdateProcess(final int updateOffset, final int updateLength,
final String partialSrcText) {
return new IUnitOfWork.Void<XtextResource>() {
@Override
public void process(final XtextResource resource) throws Exception {
// Replace text at specified index and length with new text value
resource.update(updateOffset, updateLength, partialSrcText);
}
};
}
/**
* Creates a process to replace the existing source text of the {@link XtextResource} with the specified source text.
* Used for Undo/Redo functionality.
*/
private Void<XtextResource> createUndoRedoProcess(final String srcText) {
return new IUnitOfWork.Void<XtextResource>() {
@Override
public void process(final XtextResource resource) throws Exception {
resource.reparse(srcText);
}
};
}
// Command to be executed
private RecordingCommand createRecordingCommand(final TransactionalEditingDomain editingDomain,
final Void<XtextResource> work, final XtextResource xtextResource) {
return new RecordingCommand(editingDomain) {
@Override
protected void doExecute() {
try {
work.exec(xtextResource);
} catch (final Exception e) {
throw new AadlGraphicalEditorException(e);
}
}
};
}
@Override
public AgeAction execute() {
EmbeddedTextModificationAction undoRedoAction;
try (final Lock lock = modelChangeNotifier.lock()) {
undoRedoAction = embeddedEditingActionSupplier.get();
}
// Return action to restore original source text upon undo or redo
return undoRedoAction;
}
private void save(final XtextResource xtextResource) {
try {
xtextResource.save(SaveOptions.newBuilder().format().getOptions().toOptionsMap());
} catch (final IOException e) {
throw new AadlGraphicalEditorException("Unable to save resource", e);
}
}
private void buildProject(final IProject project) {
// Build the project to prevent reference resolver from using old objects.
try {
project.build(IncrementalProjectBuilder.INCREMENTAL_BUILD, new NullProgressMonitor());
} catch (final CoreException e) {
// Ignore any errors that occur while building the project
StatusManager.getManager().handle(e, Activator.PLUGIN_ID);
}
}
private void executeCommand(final TransactionalEditingDomain editingDomain, final RecordingCommand cmd,
final XtextResource xtextResource) {
editingDomain.getCommandStack().execute(cmd);
// Run the serializer. Otherwise if an invalid modification is made, the resource could be erased.
// Sanity check to ensure that we don't save if the modification caused serialization to fail.
// We need to undo to restore the resource to a valid state because the resource may still in use by the owner of the resource(such as the graphical
// editor)
final String serializedSrc = xtextResource.getSerializer().serialize(xtextResource.getContents().get(0));
final boolean modificationSuccessful = serializedSrc != null && !serializedSrc.trim().isEmpty();
if (!modificationSuccessful) {
if (!editingDomain.getCommandStack().canUndo() || editingDomain.getCommandStack().getUndoCommand() != cmd) {
throw new AadlGraphicalEditorException(
"Property modification failed and unable to undo. Unexpected state.");
}
editingDomain.getCommandStack().undo();
}
}
/**
* It is important to validate the Xtext editor input state to ensure the document can be edited before editing the document.
* The editor may change its internal state to prepare for editing as part of validation.
* Otherwise, the document may not be properly changed or notifications
* may not be received.
* @param doc the xtext document to prepare to edit.
*/
private static void prepareToEditDocument(final IXtextDocument xtextDocument) {
for (final IEditorReference editorRef : PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage()
.getEditorReferences()) {
final IEditorPart editor = editorRef.getEditor(false);
if (editor instanceof XtextEditor) {
final XtextEditor xtextEditor = (XtextEditor) editor;
if (xtextEditor.getDocument() == xtextDocument) {
if (!xtextEditor.validateEditorInputState()) {
throw new AadlGraphicalEditorException(
"Unable to edit Xtext document. Editor input validation failed.");
}
break;
}
}
}
}
}