Skip to main content
aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/toolsmiths/validation/org.eclipse.papyrus.toolsmiths.validation.common/src/org/eclipse/papyrus/toolsmiths/validation/common/quickfix/SimpleModelEditMarkerResolution.java')
-rw-r--r--plugins/toolsmiths/validation/org.eclipse.papyrus.toolsmiths.validation.common/src/org/eclipse/papyrus/toolsmiths/validation/common/quickfix/SimpleModelEditMarkerResolution.java274
1 files changed, 249 insertions, 25 deletions
diff --git a/plugins/toolsmiths/validation/org.eclipse.papyrus.toolsmiths.validation.common/src/org/eclipse/papyrus/toolsmiths/validation/common/quickfix/SimpleModelEditMarkerResolution.java b/plugins/toolsmiths/validation/org.eclipse.papyrus.toolsmiths.validation.common/src/org/eclipse/papyrus/toolsmiths/validation/common/quickfix/SimpleModelEditMarkerResolution.java
index e81d0348701..7d28de083c3 100644
--- a/plugins/toolsmiths/validation/org.eclipse.papyrus.toolsmiths.validation.common/src/org/eclipse/papyrus/toolsmiths/validation/common/quickfix/SimpleModelEditMarkerResolution.java
+++ b/plugins/toolsmiths/validation/org.eclipse.papyrus.toolsmiths.validation.common/src/org/eclipse/papyrus/toolsmiths/validation/common/quickfix/SimpleModelEditMarkerResolution.java
@@ -16,23 +16,57 @@
package org.eclipse.papyrus.toolsmiths.validation.common.quickfix;
import java.io.IOException;
-import java.util.Optional;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
import java.util.function.BiFunction;
+import java.util.stream.Stream;
import org.eclipse.core.resources.IMarker;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.NullProgressMonitor;
+import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.emf.common.command.BasicCommandStack;
import org.eclipse.emf.common.command.Command;
+import org.eclipse.emf.common.command.CommandWrapper;
+import org.eclipse.emf.common.command.CompoundCommand;
+import org.eclipse.emf.common.notify.Adapter;
+import org.eclipse.emf.common.notify.Notification;
+import org.eclipse.emf.common.notify.Notifier;
+import org.eclipse.emf.common.notify.impl.AdapterImpl;
import org.eclipse.emf.ecore.EObject;
+import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.resource.ResourceSet;
import org.eclipse.emf.edit.domain.AdapterFactoryEditingDomain;
import org.eclipse.emf.edit.domain.EditingDomain;
import org.eclipse.emf.edit.provider.ComposedAdapterFactory;
-import org.eclipse.papyrus.infra.emf.utils.EMFHelper;
+import org.eclipse.osgi.util.NLS;
+import org.eclipse.papyrus.infra.tools.util.TriFunction;
+import org.eclipse.papyrus.infra.tools.util.Try;
import org.eclipse.papyrus.toolsmiths.validation.common.Activator;
+import org.eclipse.papyrus.toolsmiths.validation.common.internal.messages.Messages;
+
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Multimaps;
/**
+ * <p>
* A marker resolution that resolves a problem by means of an EMF {@link Command}
- * that edits the model.
+ * that edits the model. Two factory methods are provided that use client-provided functions to
+ * compute commands:
+ * </p>
+ * <ul>
+ * <li>one (recommended for most cases) that {@linkplain #create(int, String, String, Class, TriFunction) includes the marker being resolved}
+ * as an input, to support extraction of arbitrary information from it about what needs fixing</li>
+ * <li>a simpler one that {@linkplain #create(int, String, String, Class, BiFunction) uses only the object targeted by the marker}.
+ * This is convenient, for example, for a resolution that just deletes the object or that otherwise doesn't need additional information
+ * from the marker</li>
+ * </ul>
*
* @param <T>
* the model object class on which I resolve the problem
@@ -43,9 +77,11 @@ public class SimpleModelEditMarkerResolution<T extends EObject> extends Abstract
private final String description;
private final Class<T> type;
- private final BiFunction<? super EditingDomain, ? super T, ? extends Command> commandFunction;
+ private final TriFunction<? super EditingDomain, ? super T, ? super IMarker, ? extends Command> commandFunction;
+
+ private boolean canMultiFix = true;
- protected SimpleModelEditMarkerResolution(int problemID, String label, String description, Class<T> type, BiFunction<? super EditingDomain, ? super T, ? extends Command> commandFunction) {
+ protected SimpleModelEditMarkerResolution(int problemID, String label, String description, Class<T> type, TriFunction<? super EditingDomain, ? super T, ? super IMarker, ? extends Command> commandFunction) {
super(problemID);
this.label = label;
@@ -70,13 +106,38 @@ public class SimpleModelEditMarkerResolution<T extends EObject> extends Abstract
* @param commandFunction
* a function that creates a command to edit the object. It will receive an editing domain either from
* an existing open editor or created on-the-fly for an off-line edit, plus the object loaded in that domain to edit
+ * and the problem marker to be resolved
*
* @return the marker resolution
*/
- public static <T extends EObject> SimpleModelEditMarkerResolution<T> create(int problemID, String label, String description, Class<T> type, BiFunction<? super EditingDomain, ? super T, ? extends Command> commandFunction) {
+ public static <T extends EObject> SimpleModelEditMarkerResolution<T> create(int problemID, String label, String description, Class<T> type, TriFunction<? super EditingDomain, ? super T, ? super IMarker, ? extends Command> commandFunction) {
return new SimpleModelEditMarkerResolution<>(problemID, label, description, type, commandFunction);
}
+ /**
+ * Create a new marker resolution that edits the model without reference to the specific problem marker being resolved.
+ *
+ * @param <T>
+ * the kind of object to edit
+ * @param problemID
+ * my marker resolution problem ID
+ * @param label
+ * a label to present to the user for the marker resolution
+ * @param description
+ * a description of the marker resolution to present to the user
+ * @param type
+ * the type of object to edit to resolve the marker
+ * @param commandFunction
+ * a function that creates a command to edit the object. It will receive an editing domain either from
+ * an existing open editor or created on-the-fly for an off-line edit, plus the object loaded in that domain to edit
+ *
+ * @return the marker resolution
+ */
+ public static <T extends EObject> SimpleModelEditMarkerResolution<T> create(int problemID, String label, String description, Class<T> type, BiFunction<? super EditingDomain, ? super T, ? extends Command> commandFunction) {
+ return new SimpleModelEditMarkerResolution<>(problemID, label, description, type,
+ (domain, object, __) -> commandFunction.apply(domain, object));
+ }
+
@Override
public String getLabel() {
return label;
@@ -89,32 +150,195 @@ public class SimpleModelEditMarkerResolution<T extends EObject> extends Abstract
@Override
public void run(IMarker marker) {
- Optional<EditingDomain> openDomain = CommonMarkerResolutionUtils.getOpenEditingDomain(marker);
- EditingDomain domain = openDomain.orElseGet(() -> new AdapterFactoryEditingDomain(new ComposedAdapterFactory(ComposedAdapterFactory.Descriptor.Registry.INSTANCE), new BasicCommandStack()));
+ run(new IMarker[] { marker }, new NullProgressMonitor());
+ }
+
+ @Override
+ public void run(IMarker[] markers, IProgressMonitor monitor) {
+ Multimap<Try<EditingDomain>, IMarker> openDomains = getOpenEditingDomains(markers);
+ if (openDomains.keySet().stream().anyMatch(Try::isFailure)) {
+ // User cancelled prompt to save a dirty editor
+ if (monitor != null) {
+ monitor.setCanceled(true);
+ }
+ return;
+ }
+
+ // Count the map size once for preparation of commands and once for execution
+ SubMonitor sub = SubMonitor.convert(monitor, 2 * openDomains.size());
try {
- CommonMarkerResolutionUtils.getModelObject(marker, type, domain).ifPresent(object -> {
- Command command = commandFunction.apply(domain, object);
- if (command != null) {
- domain.getCommandStack().execute(command);
+ for (Map.Entry<Try<EditingDomain>, Collection<IMarker>> next : openDomains.asMap().entrySet()) {
+ Try<EditingDomain> openDomain = next.getKey();
+ Collection<? extends IMarker> partition = next.getValue();
+
+ EditingDomain domain = openDomain.toOptional().orElseGet(this::createEditingDomain);
+ Adapter loadedResourceAdapter = new QuickFixLoadedResourcesAdapter(domain);
+
+ try {
+ CompoundCommand fixCommand = new CompoundCommand();
+
+ for (IMarker marker : partition) {
+ CommonMarkerResolutionUtils.getModelObject(marker, type, domain).ifPresent(object -> {
+ Command command = commandFunction.apply(domain, object, marker);
+ if (command != null) {
+ fixCommand.append(new CommandWrapper(command) {
+ @Override
+ public void execute() {
+ super.execute();
+ sub.worked(1); // Executed
+ }
+ });
+ }
+ sub.worked(1); // Prepared
+ });
+ }
+
+ Command command = fixCommand.unwrap();
+ if (command.canExecute()) {
+ domain.getCommandStack().execute(command);
+ } else {
+ // Skipped this partition for whatever reason
+ sub.worked(partition.size());
+ }
+ } finally {
+ // Save and unload any resources that the quick fix caused to be loaded and changed.
+ // Note that if we created the editing domain, then this will unload all of the
+ // resources in it, so any item-provider adapters will have been cleaned up, too
+ loadedResourceAdapter.getTarget().eAdapters().remove(loadedResourceAdapter);
+
+ // And then save the changes we made in the editor
+ openDomain.flatMap(CommonMarkerResolutionUtils::getEditor).ifPresent(editor -> editor.getSite().getPage().saveEditor(editor, false));
}
- });
+ }
} finally {
- if (openDomain.isEmpty()) {
- // We created a domain. Save the changes and unload it
- ResourceSet rset = domain.getResourceSet();
- if (!rset.getResources().isEmpty()) {
- try {
- rset.getResources().get(0).save(null);
- } catch (IOException e) {
- Activator.log.error("Failed to save marker resolution.", e); //$NON-NLS-1$
- }
+ sub.done();
+ }
+ }
+
+ /**
+ * Partition the given {@code markers} by resource and attempt to find an editing domain
+ * in an open editor for each.
+ *
+ * @param markers
+ * markers to be fixed
+ * @return a partition of markers by resource with attempts to find open editing domains
+ */
+ private Multimap<Try<EditingDomain>, IMarker> getOpenEditingDomains(IMarker[] markers) {
+ // First, partition the markers by resource
+ ListMultimap<IResource, IMarker> partitions = Multimaps.index(Arrays.asList(markers), IMarker::getResource);
+
+ ImmutableListMultimap.Builder<Try<EditingDomain>, IMarker> result = ImmutableListMultimap.builder();
+ for (IResource next : partitions.keySet()) {
+ List<IMarker> nextPartition = partitions.get(next);
+
+ IMarker marker = nextPartition.get(0);
+ result.putAll(CommonMarkerResolutionUtils.getOpenEditingDomain(marker), nextPartition);
+ }
+
+ return result.build();
+ }
+
+ private EditingDomain createEditingDomain() {
+ return new AdapterFactoryEditingDomain(
+ new ComposedAdapterFactory(ComposedAdapterFactory.Descriptor.Registry.INSTANCE),
+ new BasicCommandStack());
+ }
+
+ /**
+ * Disable fixing of multiple markers with this fix. This is usually only appropriate
+ * for a resolution that encodes a static change plan, not determined dynamically
+ * from the marker being fixed.
+ *
+ * @return myself for convenience of fluent configuration
+ */
+ public SimpleModelEditMarkerResolution<T> disableMultiFix() {
+ this.canMultiFix = false;
+ return this;
+ }
+
+ /**
+ * Query whether I can fix multiple markers. This is usually only not the case
+ * for a resolution that encodes a static change plan, not determined dynamically
+ * from the marker being fixed.
+ *
+ * @return whether I can fix multiple markers
+ */
+ public boolean canMultiFix() {
+ return canMultiFix;
+ }
+
+ @Override
+ protected Stream<IMarker> findSimilarMarkers(IMarker[] markers) {
+ return canMultiFix() ? super.findSimilarMarkers(markers) : Stream.empty();
+ }
+
+ //
+ // Nested types
+ //
+
+ /**
+ * An adapter on the resource-set that tracks resources loaded by the quick-fix command,
+ * so that when finished, they may be saved (if changed) and unloaded again to leave
+ * an open editor in ostensibly the same condition as the user last saw it.
+ */
+ private final class QuickFixLoadedResourcesAdapter extends AdapterImpl {
+
+ private final List<Resource> loadedByQuickFix = new ArrayList<>();
+
+ QuickFixLoadedResourcesAdapter(EditingDomain domain) {
+ super();
+
+ domain.getResourceSet().eAdapters().add(this);
+ }
+
+ @Override
+ public void notifyChanged(Notification msg) {
+ if (msg.getFeatureID(ResourceSet.class) == ResourceSet.RESOURCE_SET__RESOURCES) {
+ switch (msg.getEventType()) {
+ case Notification.ADD:
+ handleAdded((Resource) msg.getNewValue());
+ break;
+ case Notification.ADD_MANY:
+ ((Collection<?>) msg.getNewValue()).stream().map(Resource.class::cast).forEach(this::handleAdded);
+ break;
}
+ }
+ }
+
+ @Override
+ public void unsetTarget(Notifier oldTarget) {
+ // Save any resources that the quick-fix loaded and changed, then unload them.
+ loadedByQuickFix.forEach(this::saveAndUnload);
+ loadedByQuickFix.clear();
+
+ super.unsetTarget(oldTarget);
+ }
+
+ private void handleAdded(Resource resource) {
+ loadedByQuickFix.add(resource);
- EMFHelper.unload(domain.getResourceSet());
- ((ComposedAdapterFactory) ((AdapterFactoryEditingDomain) domain).getAdapterFactory()).dispose();
+ // We cannot rely on the "affected objects" of the commands to indicate which resources
+ // are modified by it because, e.g., AddCommand's affected objects are the added objects,
+ // not the object to which they are added. So, track modifications on every resource
+ resource.setTrackingModification(true);
+ }
+
+ private void saveAndUnload(Resource resource) {
+ // First, remove the resource from the resource set so that the editor doesn't see it
+ // when the workspace notifies about save and get confused about conflicting edits
+ // from outside the editor context because we are, in fact, doing this in the editor context
+ resource.getResourceSet().getResources().remove(resource);
+
+ try {
+ if (resource.isModified()) {
+ resource.save(Map.of(Resource.OPTION_SAVE_ONLY_IF_CHANGED, Resource.OPTION_SAVE_ONLY_IF_CHANGED_MEMORY_BUFFER));
+ }
+ } catch (IOException e) {
+ Activator.log.error(NLS.bind(Messages.SimpleModelEditMarkerResolution_0, resource.getURI(), getLabel()), e);
+ } finally {
+ resource.unload();
}
}
}
-
}

Back to the top