Skip to main content
aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorChristian W. Damus2019-03-22 17:51:02 +0000
committerEugen Neufeld2019-03-25 22:09:29 +0000
commitbe5b57b72ee8a6dbd8ad7b8b8b936a17c38ba912 (patch)
tree695d23540c509e11a8e6aa24a7c79824baf1abdc
parente76bec5c6c19ac528c41df129a3b73b458425ecb (diff)
downloadorg.eclipse.emf.ecp.core-be5b57b72ee8a6dbd8ad7b8b8b936a17c38ba912.tar.gz
org.eclipse.emf.ecp.core-be5b57b72ee8a6dbd8ad7b8b8b936a17c38ba912.tar.xz
org.eclipse.emf.ecp.core-be5b57b72ee8a6dbd8ad7b8b8b936a17c38ba912.zip
Bug 545686 - Keyboard navigation of tables with many validation problems is slow
Performance improvements in: - rendering checkbox columns in the grid control - do not change the child context when a selection change in the grid is within the same row (side-to-side between columns only) - compute exactly which row(s) to update for validation changes in a table view instead of all of them every time - optimize DiagnosticHelper for access to standard diagnostic data pattern Change-Id: Id723c43ba4b237f9b5bae01ee3be98213263f2cc Signed-off-by: Christian W. Damus <give.a.damus@gmail.com>
-rw-r--r--bundles/org.eclipse.emf.ecp.view.table.celleditor.rcp/src/org/eclipse/emf/ecp/view/spi/table/celleditor/rcp/NativeWidgetHelper.java114
-rw-r--r--bundles/org.eclipse.emf.ecp.view.table.ui.nebula.grid/src/org/eclipse/emf/ecp/view/spi/table/nebula/grid/GridControlDetailPanelRenderer.java10
-rw-r--r--bundles/org.eclipse.emf.ecp.view.table.ui.swt/src/org/eclipse/emf/ecp/view/internal/table/swt/RunnableManager.java217
-rw-r--r--bundles/org.eclipse.emf.ecp.view.table.ui.swt/src/org/eclipse/emf/ecp/view/spi/table/swt/TableControlSWTRenderer.java185
-rw-r--r--bundles/org.eclipse.emfforms.common.validation/src/org/eclipse/emfforms/common/internal/validation/DiagnosticHelper.java42
-rw-r--r--tests/org.eclipse.emf.ecp.view.table.ui.swt.test/src/org/eclipse/emf/ecp/view/spi/table/swt/SWTTable_PTest.java33
-rw-r--r--tests/org.eclipse.emf.ecp.view.table.ui.swt.test/src/org/eclipse/emf/ecp/view/table/ui/swt/test/RunnableManagerTest.java155
7 files changed, 667 insertions, 89 deletions
diff --git a/bundles/org.eclipse.emf.ecp.view.table.celleditor.rcp/src/org/eclipse/emf/ecp/view/spi/table/celleditor/rcp/NativeWidgetHelper.java b/bundles/org.eclipse.emf.ecp.view.table.celleditor.rcp/src/org/eclipse/emf/ecp/view/spi/table/celleditor/rcp/NativeWidgetHelper.java
index 30f423b20c..2b28310f8c 100644
--- a/bundles/org.eclipse.emf.ecp.view.table.celleditor.rcp/src/org/eclipse/emf/ecp/view/spi/table/celleditor/rcp/NativeWidgetHelper.java
+++ b/bundles/org.eclipse.emf.ecp.view.table.celleditor.rcp/src/org/eclipse/emf/ecp/view/spi/table/celleditor/rcp/NativeWidgetHelper.java
@@ -1,5 +1,5 @@
/*******************************************************************************
- * Copyright (c) 2011-2015 EclipseSource Muenchen GmbH and others.
+ * Copyright (c) 2011-2019 EclipseSource Muenchen GmbH and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
@@ -8,6 +8,7 @@
*
* Contributors:
* jfaltermeier - initial API and implementation
+ * Christian W. Damus - bug 545686
******************************************************************************/
/*******************************************************************************
* Copyright (c) 2006, 2014 Tom Schindl and others.
@@ -24,7 +25,8 @@
*******************************************************************************/
package org.eclipse.emf.ecp.view.spi.table.celleditor.rcp;
-import java.util.concurrent.Semaphore;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
import org.eclipse.emf.ecp.view.spi.util.swt.ImageRegistryService;
import org.eclipse.swt.SWT;
@@ -40,6 +42,8 @@ import org.eclipse.swt.widgets.Shell;
import org.osgi.framework.Bundle;
import org.osgi.framework.FrameworkUtil;
import org.osgi.framework.ServiceReference;
+import org.osgi.util.tracker.ServiceTracker;
+import org.osgi.util.tracker.ServiceTrackerCustomizer;
/**
* Util class for faking native widgets.
@@ -55,14 +59,25 @@ public final class NativeWidgetHelper {
private static final String CHECKED_DEFAULT = "icons/checked.png"; //$NON-NLS-1$
private static final String UNCHECKED_DEFAULT = "icons/unchecked.png"; //$NON-NLS-1$
- private static final Semaphore LOCK = new Semaphore(1, true);
+ private static final Lock LOCK = new ReentrantLock(true);
private static boolean initalized;
- private static ServiceReference<ImageRegistryService> imageRegistryServiceReference;
+ private static final Bundle BUNDLE;
+ private static final ServiceTracker<ImageRegistryService, ImageRegistryService> IMAGE_REGISTRY_SERVICE_TRACKER;
private static ImageData checked;
private static ImageData unchecked;
+ private static Image defaultCheckedImage;
+ private static Image defaultUncheckedImage;
+
+ static {
+ BUNDLE = FrameworkUtil.getBundle(NativeWidgetHelper.class);
+ IMAGE_REGISTRY_SERVICE_TRACKER = new ServiceTracker<>(BUNDLE.getBundleContext(), ImageRegistryService.class,
+ new ImageRegistryServiceCustomizer());
+ IMAGE_REGISTRY_SERVICE_TRACKER.open();
+ }
+
private NativeWidgetHelper() {
// util
}
@@ -76,16 +91,14 @@ public final class NativeWidgetHelper {
*/
public static void initCheckBoxImages(Control control) {
try {
- LOCK.acquire();
- } catch (final InterruptedException ex) {
- if (initalized) {
- return;
+ LOCK.lock();
+ if (!initalized) {
+ createCheckBoxImage(control, true);
+ createCheckBoxImage(control, false);
+ initalized = true;
}
- createCheckBoxImage(control, true);
- createCheckBoxImage(control, false);
- initalized = true;
} finally {
- LOCK.release();
+ LOCK.unlock();
}
}
@@ -100,27 +113,25 @@ public final class NativeWidgetHelper {
*/
public static Image getCheckBoxImage(Control control, CheckBoxState state) {
try {
- LOCK.acquire();
+ LOCK.lock();
switch (state) {
case checked:
if (checked != null) {
return new Image(control.getDisplay(), checked);
}
- return getImage(CHECKED_DEFAULT);
+ return defaultCheckedImage;
case unchecked:
if (unchecked != null) {
return new Image(control.getDisplay(), unchecked);
}
- return getImage(UNCHECKED_DEFAULT);
+ return defaultUncheckedImage;
default:
break;
}
- } catch (final InterruptedException ex) {
-
} finally {
- LOCK.release();
+ LOCK.unlock();
}
return null;
@@ -204,22 +215,6 @@ public final class NativeWidgetHelper {
return WS_CARBON.equals(ws) || WS_COCOA.equals(ws);
}
- private static Image getImage(String path) {
- final Bundle bundle = FrameworkUtil.getBundle(NativeWidgetHelper.class);
- final Image image = getImageRegistryService().getImage(bundle, path);
- bundle.getBundleContext().ungetService(imageRegistryServiceReference);
- return image;
- }
-
- private static ImageRegistryService getImageRegistryService() {
- final Bundle bundle = FrameworkUtil.getBundle(NativeWidgetHelper.class);
- if (imageRegistryServiceReference == null) {
- imageRegistryServiceReference = bundle.getBundleContext()
- .getServiceReference(ImageRegistryService.class);
- }
- return bundle.getBundleContext().getService(imageRegistryServiceReference);
- }
-
/**
* Enum describing the state of a checkbox.
*
@@ -238,4 +233,55 @@ public final class NativeWidgetHelper {
unchecked
}
+ /**
+ * Service-tracker customizer that uses the image-registry service, when it is
+ * discovered or updated, to get and cache the default checked and unchecked images.
+ */
+ private static final class ImageRegistryServiceCustomizer
+ implements ServiceTrackerCustomizer<ImageRegistryService, ImageRegistryService> {
+
+ @Override
+ public ImageRegistryService addingService(ServiceReference<ImageRegistryService> reference) {
+ final ImageRegistryService result = BUNDLE.getBundleContext().getService(reference);
+
+ onUI(() -> updateImages(result));
+
+ return result;
+ }
+
+ @Override
+ public void modifiedService(ServiceReference<ImageRegistryService> reference, ImageRegistryService service) {
+ onUI(() -> updateImages(service));
+ }
+
+ @Override
+ public void removedService(ServiceReference<ImageRegistryService> reference, ImageRegistryService service) {
+ onUI(() -> updateImages(null));
+ }
+
+ private void onUI(Runnable action) {
+ final Display display = Display.getCurrent();
+ if (display != null) {
+ action.run();
+ } else {
+ Display.getDefault().syncExec(action);
+ }
+ }
+
+ private void updateImages(ImageRegistryService imageRegistryService) {
+ LOCK.lock();
+
+ try {
+ if (imageRegistryService != null) {
+ defaultCheckedImage = imageRegistryService.getImage(BUNDLE, CHECKED_DEFAULT);
+ defaultUncheckedImage = imageRegistryService.getImage(BUNDLE, UNCHECKED_DEFAULT);
+ } else {
+ defaultCheckedImage = null;
+ defaultUncheckedImage = null;
+ }
+ } finally {
+ LOCK.unlock();
+ }
+ }
+ }
}
diff --git a/bundles/org.eclipse.emf.ecp.view.table.ui.nebula.grid/src/org/eclipse/emf/ecp/view/spi/table/nebula/grid/GridControlDetailPanelRenderer.java b/bundles/org.eclipse.emf.ecp.view.table.ui.nebula.grid/src/org/eclipse/emf/ecp/view/spi/table/nebula/grid/GridControlDetailPanelRenderer.java
index 41c397d62a..55e71032a9 100644
--- a/bundles/org.eclipse.emf.ecp.view.table.ui.nebula.grid/src/org/eclipse/emf/ecp/view/spi/table/nebula/grid/GridControlDetailPanelRenderer.java
+++ b/bundles/org.eclipse.emf.ecp.view.table.ui.nebula.grid/src/org/eclipse/emf/ecp/view/spi/table/nebula/grid/GridControlDetailPanelRenderer.java
@@ -1,5 +1,5 @@
/*******************************************************************************
- * Copyright (c) 2011-2018 EclipseSource Muenchen GmbH and others.
+ * Copyright (c) 2011-2019 EclipseSource Muenchen GmbH and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
@@ -8,6 +8,7 @@
*
* Contributors:
* Johannes Faltermeier - initial API and implementation
+ * Christian W. Damus - bug 545686
******************************************************************************/
package org.eclipse.emf.ecp.view.spi.table.nebula.grid;
@@ -286,12 +287,17 @@ public class GridControlDetailPanelRenderer extends GridControlSWTRenderer {
* @param selection the selection
*/
protected void handleSingleSelection(IStructuredSelection selection) {
+ // Did the selection actionally change? We may have stepped sideways in a row
+ final EObject object = (EObject) selection.getFirstElement();
+ if (ecpView != null && ecpView.getViewModelContext().getDomainModel() == object) {
+ return;
+ }
+
disposeDetail();
final Composite compositeToRenderOn = new Composite(detailPanel, SWT.NONE);
GridLayoutFactory.fillDefaults().numColumns(1).equalWidth(false).applyTo(compositeToRenderOn);
GridDataFactory.fillDefaults().grab(true, true).align(SWT.FILL, SWT.FILL).applyTo(compositeToRenderOn);
- final EObject object = (EObject) selection.getFirstElement();
renderSelectedObject(compositeToRenderOn, object);
border.layout(true, true);
scrolledComposite.setMinSize(detailPanel.computeSize(SWT.DEFAULT, SWT.DEFAULT));
diff --git a/bundles/org.eclipse.emf.ecp.view.table.ui.swt/src/org/eclipse/emf/ecp/view/internal/table/swt/RunnableManager.java b/bundles/org.eclipse.emf.ecp.view.table.ui.swt/src/org/eclipse/emf/ecp/view/internal/table/swt/RunnableManager.java
index 3d17830366..3e8c564eab 100644
--- a/bundles/org.eclipse.emf.ecp.view.table.ui.swt/src/org/eclipse/emf/ecp/view/internal/table/swt/RunnableManager.java
+++ b/bundles/org.eclipse.emf.ecp.view.table.ui.swt/src/org/eclipse/emf/ecp/view/internal/table/swt/RunnableManager.java
@@ -8,26 +8,41 @@
*
* Contributors:
* Edgar Mueller - initial API and implementation
- * Christian W. Damus - bug 544116
+ * Christian W. Damus - bugs 544116, 545686
******************************************************************************/
package org.eclipse.emf.ecp.view.internal.table.swt;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
import org.eclipse.swt.widgets.Display;
/**
+ * <p>
* A utility class that is capable of maintaining the running
* state of a {@link Runnable}, i.e. frequent calls of the {@link #executeAsync(Runnable)}
* with long running {@link Runnable}s will not result in each {@link Runnable} being called.
* This is useful, for instance, if the same {@link Runnable} is
* submitted multiple times unnecessarily.
- *
+ * </p>
+ * <p>
+ * If a runnable additionally implements the {@link BackgroundStage} interface, then
+ * it will be invoked on a background thread and when complete will be asked to
+ * {@linkplain BackgroundStage#getNextStage() provide a subsequent stage} to execute.
+ * This may similarly be a background stage, iterating the process, or may be a terminal
+ * runnable that then is posted on the UI thread to update the UI.
+ * </p>
*/
public class RunnableManager {
+ private final Lock lock = new ReentrantLock();
private final AtomicBoolean isRunning = new AtomicBoolean(false);
+ private final Condition runningCond = lock.newCondition();
private final AtomicReference<Runnable> pending = new AtomicReference<>();
private final Display display;
@@ -41,12 +56,22 @@ public class RunnableManager {
}
private void finish(Runnable runnable) {
- // validation finished
- isRunning.compareAndSet(true, false);
- // re-trigger validation if we have a pending request
- final Runnable next = pending.getAndSet(null);
- if (next != null) {
- executeAsync(next);
+ lock.lock();
+
+ try {
+ // task has finished
+ isRunning.compareAndSet(true, false);
+
+ // Running state has changed
+ runningCond.signalAll();
+
+ // trigger next task if we have a pending request
+ final Runnable next = pending.getAndSet(null);
+ if (next != null) {
+ executeAsync(next);
+ }
+ } finally {
+ lock.unlock();
}
}
@@ -69,10 +94,27 @@ public class RunnableManager {
* @param runnable the {@link Runnable} to be executed asynchronously
*/
public void executeAsync(final Runnable runnable) {
- if (isRunning.compareAndSet(false, true)) {
- getDisplay().asyncExec(createWrapperRunnable(runnable));
+ lock.lock();
+
+ try {
+ if (isRunning.compareAndSet(false, true)) {
+ doExecuteAsync(runnable);
+
+ // Running state has changed
+ runningCond.signalAll();
+ } else {
+ pending.compareAndSet(null, runnable);
+ }
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ private void doExecuteAsync(final Runnable runnable) {
+ if (runnable instanceof BackgroundStage) {
+ runStage(runnable, (BackgroundStage) runnable);
} else {
- pending.compareAndSet(null, runnable);
+ getDisplay().asyncExec(createWrapperRunnable(runnable));
}
}
@@ -95,4 +137,157 @@ public class RunnableManager {
return isRunning.get();
}
+ /**
+ * Wait until no task is running. Returns immediately if there is currently
+ * no task running.
+ *
+ * @throws InterruptedException if interrupted while waiting
+ * @since 1.21
+ */
+ public final void waitForIdle() throws InterruptedException {
+ if (!isRunning()) {
+ // Short-circuit
+ return;
+ }
+
+ final boolean busyWait = Display.getCurrent() == display;
+
+ lock.lock();
+
+ try {
+ out: while (isRunning()) {
+ if (busyWait) {
+ try {
+ lock.unlock();
+
+ // We can only finish on the display thread, which is
+ // performed by a runnable in this queue
+ do {
+ if (!isRunning()) {
+ break out;
+ }
+ } while (display.readAndDispatch());
+
+ display.sleep();
+ } finally {
+ lock.lock();
+ }
+ } else {
+ runningCond.await();
+ }
+ }
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ /**
+ * Wait until no task is running or the given {@code time} elapses.
+ * Returns immediately if there is currently no task running. Note that if called on the
+ * UI thread, then the actual wait time in case of time-out can be longer than requested
+ * because of UI event queue processing.
+ *
+ * @param time the amount of time to wait, or a non-positive amount to wait indefinitely
+ * as in {@link #waitForIdle()}
+ * @param unit the unit of measure of the {@code time} to wait
+ * @return {@code true} if on return there is no task running; {@code false} on time-out
+ * (which does not mean that since the time-out occurred the manager did not become idle)
+ *
+ * @throws InterruptedException if interrupted while waiting
+ * @since 1.21
+ */
+ public final boolean waitForIdle(long time, TimeUnit unit) throws InterruptedException {
+ if (time <= 0L) {
+ waitForIdle();
+ return true;
+ }
+
+ final long deadline = System.nanoTime() + unit.toNanos(time);
+
+ if (!isRunning()) {
+ // Short-circuit
+ return true;
+ }
+
+ final boolean busyWait = Display.getCurrent() == display;
+
+ lock.lock();
+
+ try {
+ out: while (isRunning()) {
+ final long remaining = deadline - System.nanoTime();
+ if (remaining <= 0L) {
+ return false;
+ }
+
+ if (busyWait) {
+ try {
+ lock.unlock();
+
+ // We can only finish on the display thread, which is
+ // performed by a runnable in this queue
+ do {
+ if (!isRunning()) {
+ break out;
+ }
+ } while (display.readAndDispatch());
+ } finally {
+ lock.lock();
+ }
+
+ runningCond.await(50L, TimeUnit.MILLISECONDS);
+ } else {
+ runningCond.awaitNanos(remaining);
+ }
+ }
+ } finally {
+ lock.unlock();
+ }
+
+ return true;
+ }
+
+ private void runStage(Runnable computation, BackgroundStage stage) {
+ CompletableFuture.runAsync(computation)
+ .whenComplete((result, exception) -> {
+ if (exception != null) {
+ // Computation failed. Just finish
+ finish(computation);
+ Activator.getInstance().log(exception);
+ } else {
+ // Send the next stage
+ final Runnable next = stage.getNextStage();
+ if (next == null) {
+ // Okay, then. Just finish
+ finish(computation);
+ } else {
+ doExecuteAsync(next);
+ }
+ }
+ });
+ }
+
+ //
+ // Nested types
+ //
+
+ /**
+ * An optional mix-in interface for a {@link Runnable} scheduled on the
+ * {@link RunnableManager} that should be run in a background thread and which
+ * produces a subsequent stage for further execution.
+ *
+ * @since 1.21
+ */
+ public interface BackgroundStage {
+ /**
+ * Provides the next stage of computation. If the result is another
+ * {@code BackgroundStage}, then it, too, will run in the background.
+ * The final stage is some {@link Runnable} that is not a {@code BackgroundStage}
+ * which then is posted on the display thread to update the UI.
+ *
+ * @return the next stage of computation/update
+ */
+ Runnable getNextStage();
+ }
+
}
diff --git a/bundles/org.eclipse.emf.ecp.view.table.ui.swt/src/org/eclipse/emf/ecp/view/spi/table/swt/TableControlSWTRenderer.java b/bundles/org.eclipse.emf.ecp.view.table.ui.swt/src/org/eclipse/emf/ecp/view/spi/table/swt/TableControlSWTRenderer.java
index a0e3c2831e..0185987783 100644
--- a/bundles/org.eclipse.emf.ecp.view.table.ui.swt/src/org/eclipse/emf/ecp/view/spi/table/swt/TableControlSWTRenderer.java
+++ b/bundles/org.eclipse.emf.ecp.view.table.ui.swt/src/org/eclipse/emf/ecp/view/spi/table/swt/TableControlSWTRenderer.java
@@ -9,25 +9,25 @@
* Contributors:
* Eugen Neufeld - initial API and implementation
* Johannes Faltermeier - refactorings
- * Christian W. Damus - bugs 544116, 544537
+ * Christian W. Damus - bugs 544116, 544537, 545686
******************************************************************************/
package org.eclipse.emf.ecp.view.spi.table.swt;
-import static java.util.stream.Collectors.toCollection;
-
import java.net.MalformedURLException;
import java.net.URL;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
import java.util.stream.Collectors;
-import java.util.stream.Stream;
import javax.inject.Inject;
@@ -1491,36 +1491,26 @@ public class TableControlSWTRenderer extends AbstractControlSWTRenderer<VTableCo
@Override
protected void applyValidation(VDiagnostic oldDiagnostic, VDiagnostic newDiagnostic) {
- final Stream<Object> oldDiagnostics = oldDiagnostic == null ? Stream.empty()
- : oldDiagnostic.getDiagnostics().stream();
- final Stream<Object> newDiagnostics = newDiagnostic == null ? Stream.empty()
- : newDiagnostic.getDiagnostics().stream();
-
- final Set<Object> updates = Stream.concat(oldDiagnostics, newDiagnostics)
- .map(this::getSubject).filter(Objects::nonNull).collect(toCollection(LinkedHashSet::new));
-
- runnableManager.executeAsync(new ApplyValidationRunnable(updates));
- }
-
- private Object getSubject(Object diagnostic) {
- Object result = null;
-
- if (diagnostic instanceof Diagnostic) {
- final List<?> data = ((Diagnostic) diagnostic).getData();
- result = data.isEmpty() ? null : data.get(0);
- }
-
- return result;
+ getRunnableManager().executeAsync(new ComputeValidationUpdate(oldDiagnostic, newDiagnostic));
}
@Override
protected void applyValidation() {
- if (!runnableManager.isRunning()) {
- runnableManager.executeAsync(new ApplyValidationRunnable());
+ if (!getRunnableManager().isRunning()) {
+ getRunnableManager().executeAsync(new ApplyValidationRunnable());
}
}
/**
+ * Obtain my runnable manager (visible for testability).
+ *
+ * @return my runnable manager
+ */
+ final RunnableManager getRunnableManager() {
+ return runnableManager;
+ }
+
+ /**
* Returns the add button created by the framework.
*
* @deprecated use {@link #getControlForAction(String)} instead
@@ -1999,6 +1989,149 @@ public class TableControlSWTRenderer extends AbstractControlSWTRenderer<VTableCo
}
/**
+ * A two-stage asynchronous task that first, in the background, computes the objects
+ * that have actually seen validation changes and then post the viewer update
+ * on the UI thread.
+ */
+ private final class ComputeValidationUpdate implements Runnable, RunnableManager.BackgroundStage {
+ private final Set<Diagnostic> oldDiagnostics;
+ private final Set<Diagnostic> newDiagnostics;
+
+ private Runnable update;
+
+ /**
+ * Initializes me with the old and new diagnostics from which to compute the objects
+ * that need to be updated in my viewer.
+ *
+ * @param oldDiagnostic the old validation state
+ * @param newDiagnostic the new validation state
+ */
+ ComputeValidationUpdate(VDiagnostic oldDiagnostic, VDiagnostic newDiagnostic) {
+ super();
+
+ // These have to be extracted on the calling thread because the ELists
+ // are not thread-safe
+ oldDiagnostics = getDiagnostics(oldDiagnostic);
+ newDiagnostics = getDiagnostics(newDiagnostic);
+ }
+
+ @Override
+ public Runnable getNextStage() {
+ return update;
+ }
+
+ @Override
+ public void run() {
+ // Compute the difference between the diagnostics to find what actually
+ // are the logical changes in validation that will need updates in the UI
+ final Set<Diagnostic> difference = difference(oldDiagnostics, newDiagnostics);
+ final Set<Object> updates = difference.stream().map(this::getSubject).filter(Objects::nonNull)
+ .collect(Collectors.toSet());
+ if (!updates.isEmpty()) {
+ update = new ApplyValidationRunnable(updates);
+ } // Otherwise we don't need the UI update stage
+ }
+
+ private Set<Diagnostic> getDiagnostics(VDiagnostic container) {
+ return container == null ? Collections.emptySet()
+ : container.getDiagnostics().stream()
+ .filter(Diagnostic.class::isInstance).map(Diagnostic.class::cast)
+ .collect(Collectors.toSet());
+ }
+
+ private Object getSubject(Diagnostic diagnostic) {
+ final List<?> data = diagnostic.getData();
+ return data.isEmpty() ? null : data.get(0);
+ }
+
+ private Set<Diagnostic> difference(Set<Diagnostic> set1, Set<Diagnostic> set2) {
+ // Diagnostics do not implement equals(), so we have to do it for them.
+ // Most straightforward approach is to use a tree set, which uses the
+ // comparator's zero result to test equality
+ final SortedSet<Diagnostic> sorted1 = new TreeSet<>(this::compare);
+ sorted1.addAll(set1);
+ final SortedSet<Diagnostic> sorted2 = new TreeSet<>(this::compare);
+ sorted2.addAll(set2);
+
+ // Difference each side
+ sorted1.removeAll(set2);
+ sorted2.removeAll(set1);
+
+ // And the union of that
+ sorted1.addAll(sorted2);
+
+ return sorted1;
+ }
+
+ private int compare(Diagnostic d1, Diagnostic d2) {
+ int result = d1.getSeverity() - d2.getSeverity();
+ if (result != 0) {
+ return result;
+ }
+
+ result = d1.getCode() - d2.getCode();
+ if (result != 0) {
+ return result;
+ }
+
+ result = compare(d1.getSource(), d2.getSource());
+ if (result != 0) {
+ return result;
+ }
+
+ result = compare(d1.getMessage(), d2.getMessage());
+ if (result != 0) {
+ return result;
+ }
+
+ result = compare(d1.getData(), d2.getData());
+
+ // The diagnostics from validation should not have exceptions and
+ // the children are immaterial because we've already flattened them
+
+ return result;
+ }
+
+ private int compare(String s1, String s2) {
+ if (s1 == null) {
+ return s2 == null ? 0 : -1;
+ }
+ if (s2 == null) {
+ return +1;
+ }
+ return s1.compareTo(s2);
+ }
+
+ private int compare(List<?> s1, List<?> s2) {
+ if (s1 == null) {
+ return s2 == null ? 0 : -1;
+ }
+ if (s2 == null) {
+ return +1;
+ }
+
+ final int size = s1.size();
+ int result = size - s2.size();
+ if (result != 0) {
+ return result;
+ }
+
+ int i = 0;
+ for (i = 0; i < size; i++) {
+ final Object e1 = s1.get(i);
+ final Object e2 = s2.get(i);
+ if (e1 != e2) {
+ // Arbitrary but stable order
+ result = System.identityHashCode(e1) - System.identityHashCode(e2);
+ break;
+ }
+ }
+
+ return result;
+ }
+ }
+
+ /**
* Runnable which is called by {@link TableControlSWTRenderer#applyValidation() applyValidation}.
*
*/
diff --git a/bundles/org.eclipse.emfforms.common.validation/src/org/eclipse/emfforms/common/internal/validation/DiagnosticHelper.java b/bundles/org.eclipse.emfforms.common.validation/src/org/eclipse/emfforms/common/internal/validation/DiagnosticHelper.java
index 00e248070f..288cbdb7a9 100644
--- a/bundles/org.eclipse.emfforms.common.validation/src/org/eclipse/emfforms/common/internal/validation/DiagnosticHelper.java
+++ b/bundles/org.eclipse.emfforms.common.validation/src/org/eclipse/emfforms/common/internal/validation/DiagnosticHelper.java
@@ -1,5 +1,5 @@
/*******************************************************************************
- * Copyright (c) 2017 EclipseSource Muenchen GmbH and others.
+ * Copyright (c) 2017-2019 EclipseSource Muenchen GmbH and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
@@ -8,6 +8,7 @@
*
* Contributors:
* Mat Hansen - initial API and implementation
+ * Christian W. Damus - bug 545686
******************************************************************************/
package org.eclipse.emfforms.common.internal.validation;
@@ -35,15 +36,25 @@ public final class DiagnosticHelper {
* @return the found {@link EStructuralFeature}, null if no {@link EStructuralFeature} is found
*/
public static EStructuralFeature getEStructuralFeature(List<?> data) {
- if (data == null || data.isEmpty()) {
+ // Exclude first object for cases when we validate an EStructuralFeature.
+ if (data == null || data.size() < 2) {
return null;
}
- // Exclude first object for cases when we validate an EStructuralFeature.
- for (final Object object : data.subList(1, data.size())) {
- if (EStructuralFeature.class.isInstance(object)) {
- return EStructuralFeature.class.cast(object);
+
+ // The usual case is that the structural feature is the second object.
+ final Object usual = data.get(1);
+ if (usual instanceof EStructuralFeature) {
+ return (EStructuralFeature) usual;
+ }
+
+ if (data.size() > 2) {
+ for (final Object object : data.subList(2, data.size())) {
+ if (EStructuralFeature.class.isInstance(object)) {
+ return EStructuralFeature.class.cast(object);
+ }
}
}
+
return null;
}
@@ -54,11 +65,24 @@ public final class DiagnosticHelper {
* @return the found {@link InternalEObject}, null if no {@link InternalEObject} is found
*/
public static InternalEObject getFirstInternalEObject(List<?> data) {
- for (final Object object : data) {
- if (InternalEObject.class.isInstance(object)) {
- return InternalEObject.class.cast(object);
+ if (data == null || data.isEmpty()) {
+ return null;
+ }
+
+ // The usual case is that the subject of the problem is the first object
+ final Object usual = data.get(0);
+ if (usual instanceof InternalEObject) {
+ return (InternalEObject) usual;
+ }
+
+ if (data.size() > 1) {
+ for (final Object object : data) {
+ if (InternalEObject.class.isInstance(object)) {
+ return InternalEObject.class.cast(object);
+ }
}
}
+
return null;
}
diff --git a/tests/org.eclipse.emf.ecp.view.table.ui.swt.test/src/org/eclipse/emf/ecp/view/spi/table/swt/SWTTable_PTest.java b/tests/org.eclipse.emf.ecp.view.table.ui.swt.test/src/org/eclipse/emf/ecp/view/spi/table/swt/SWTTable_PTest.java
index 6e6a2f7170..477fc4fadb 100644
--- a/tests/org.eclipse.emf.ecp.view.table.ui.swt.test/src/org/eclipse/emf/ecp/view/spi/table/swt/SWTTable_PTest.java
+++ b/tests/org.eclipse.emf.ecp.view.table.ui.swt.test/src/org/eclipse/emf/ecp/view/spi/table/swt/SWTTable_PTest.java
@@ -8,7 +8,7 @@
*
* Contributors:
* Johannes Faltermeier
- * Christian W. Damus - bugs 527740, 544116
+ * Christian W. Damus - bugs 527740, 544116, 545686
*
*******************************************************************************/
package org.eclipse.emf.ecp.view.spi.table.swt;
@@ -32,13 +32,13 @@ import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
-import java.util.ArrayList;
import java.util.Arrays;
+import java.util.HashSet;
import java.util.Hashtable;
import java.util.LinkedHashMap;
-import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
@@ -1056,7 +1056,7 @@ public class SWTTable_PTest {
// render
final ViewModelContext context = new ViewModelContextImpl(tableControl, domainElement);
- final List<String> requestedCells = new ArrayList<>();
+ final Set<String> requestedCells = new HashSet<>(); // Cell updates are unordered
final TableControlSWTRenderer tableRenderer = new TableControlSWTRenderer(tableControl, context,
context.getService(ReportService.class),
context.getService(EMFFormsDatabindingEMF.class),
@@ -1112,10 +1112,10 @@ public class SWTTable_PTest {
vdiag.getDiagnostics().add(diag);
tableControl.setDiagnostic(vdiag);
- SWTTestUtil.waitForUIThread();
+ waitFor(tableRenderer);
// Notably, we updated these two rows in order and *neither* "a" nor "d"
- assertThat(requestedCells, equalTo(Arrays.asList("b", "c")));
+ assertThat(requestedCells, equalTo(set("b", "c")));
}
@Test
@@ -1147,6 +1147,27 @@ public class SWTTable_PTest {
registration.unregister();
}
+ @SafeVarargs
+ static <T> Set<T> set(T... elements) {
+ return new HashSet<>(Arrays.asList(elements));
+ }
+
+ /**
+ * Wait for any pending updates in a table renderer to be completed.
+ *
+ * @param tableRenderer a table renderer to wait for
+ */
+ final void waitFor(TableControlSWTRenderer tableRenderer) {
+ try {
+ if (!tableRenderer.getRunnableManager().waitForIdle(1L, TimeUnit.SECONDS)) {
+ fail("Timed out waiting for table updates");
+ }
+ } catch (final InterruptedException e) {
+ fail("Interrupted waiting for table updates");
+ }
+
+ }
+
private static class ColumnImageTableRenderer extends TableControlSWTRenderer {
// BEGIN COMPLEX CODE
@Inject
diff --git a/tests/org.eclipse.emf.ecp.view.table.ui.swt.test/src/org/eclipse/emf/ecp/view/table/ui/swt/test/RunnableManagerTest.java b/tests/org.eclipse.emf.ecp.view.table.ui.swt.test/src/org/eclipse/emf/ecp/view/table/ui/swt/test/RunnableManagerTest.java
index 489a95e9f4..761fdf24cb 100644
--- a/tests/org.eclipse.emf.ecp.view.table.ui.swt.test/src/org/eclipse/emf/ecp/view/table/ui/swt/test/RunnableManagerTest.java
+++ b/tests/org.eclipse.emf.ecp.view.table.ui.swt.test/src/org/eclipse/emf/ecp/view/table/ui/swt/test/RunnableManagerTest.java
@@ -1,5 +1,5 @@
/*******************************************************************************
- * Copyright (c) 2011-2017 EclipseSource Muenchen GmbH and others.
+ * Copyright (c) 2011-2019 EclipseSource Muenchen GmbH and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
@@ -8,10 +8,17 @@
*
* Contributors:
* edgar - initial API and implementation
+ * Christian W. Damus - bug 545686
******************************************************************************/
package org.eclipse.emf.ecp.view.table.ui.swt.test;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.greaterThan;
+import static org.hamcrest.Matchers.greaterThanOrEqualTo;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.lessThanOrEqualTo;
import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
@@ -19,10 +26,13 @@ import static org.mockito.Mockito.mock;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import org.eclipse.emf.ecp.view.internal.table.swt.RunnableManager;
+import org.eclipse.emf.ecp.view.internal.table.swt.RunnableManager.BackgroundStage;
import org.eclipse.swt.widgets.Display;
+import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.invocation.InvocationOnMock;
@@ -60,6 +70,12 @@ public class RunnableManagerTest {
}
+ @AfterClass
+ public static void afterClass() {
+ ASYNC_POOL.shutdown();
+ POOL.shutdown();
+ }
+
@Test
public void managedRunnable() throws InterruptedException {
// setup
@@ -81,4 +97,141 @@ public class RunnableManagerTest {
assertTrue(counter.intValue() > asyncCounter.intValue());
}
+ @Test
+ public void backgroundStage() throws InterruptedException {
+ final AtomicInteger stageCount = new AtomicInteger();
+ final Stage stage = new Stage(stageCount::incrementAndGet);
+
+ final RunnableManager runnableManager = new RunnableManager(DISPLAY);
+
+ runnableManager.executeAsync(stage);
+ stage.join();
+
+ assertThat("Multi-step execution not completed", stageCount.get(), greaterThanOrEqualTo(3));
+ }
+
+ @Test
+ public void waitForIdle() throws InterruptedException {
+ final AtomicInteger countdown = new AtomicInteger(Stage.STAGE_COUNT);
+ final Runnable callback = () -> {
+ try {
+ Thread.sleep(100L);
+ } catch (final InterruptedException e) {
+ fail("Call-back interrupted");
+ }
+ countdown.decrementAndGet();
+ };
+
+ final Stage stage = new Stage(callback);
+
+ final RunnableManager runnableManager = new RunnableManager(DISPLAY);
+
+ runnableManager.executeAsync(stage);
+
+ runnableManager.waitForIdle();
+
+ assertThat("Wait returned early", countdown.get(), lessThanOrEqualTo(0));
+ }
+
+ @Test
+ public void waitForIdle_time() throws InterruptedException {
+ final AtomicInteger countdown = new AtomicInteger(Stage.STAGE_COUNT);
+ final Runnable callback = () -> {
+ try {
+ Thread.sleep(100L);
+ } catch (final InterruptedException e) {
+ fail("Call-back interrupted");
+ }
+ countdown.decrementAndGet();
+ };
+
+ final Stage stage = new Stage(callback);
+
+ final RunnableManager runnableManager = new RunnableManager(DISPLAY);
+
+ runnableManager.executeAsync(stage);
+
+ assertThat("Timed out", runnableManager.waitForIdle(5L, TimeUnit.SECONDS), is(true));
+
+ assertThat("Wait returned early", countdown.get(), lessThanOrEqualTo(0));
+ }
+
+ @Test
+ public void waitForIdle_timeout() throws InterruptedException {
+ final AtomicInteger countdown = new AtomicInteger(Stage.STAGE_COUNT);
+ final Runnable callback = () -> {
+ try {
+ Thread.sleep(1000L);
+ } catch (final InterruptedException e) {
+ fail("Call-back interrupted");
+ }
+ countdown.decrementAndGet();
+ };
+
+ final Stage stage = new Stage(callback);
+
+ final RunnableManager runnableManager = new RunnableManager(DISPLAY);
+
+ runnableManager.executeAsync(stage);
+
+ assertThat("Not timed out", runnableManager.waitForIdle(1L, TimeUnit.SECONDS), is(false));
+
+ assertThat("Task completed", countdown.get(), greaterThan(0));
+ }
+
+ //
+ // Nested types
+ //
+
+ private static class Stage implements Runnable, BackgroundStage {
+ static final int STAGE_COUNT = 3;
+
+ private final Runnable callback;
+
+ private int backgroundStages = STAGE_COUNT - 1;
+ private Runnable next;
+
+ private final CountDownLatch done = new CountDownLatch(1);
+
+ Stage(Runnable callback) {
+ super();
+
+ this.callback = callback;
+ }
+
+ @Override
+ public void run() {
+ // This is a stage
+ try {
+ callback.run();
+ } finally {
+ // This is a background stage
+ backgroundStages--;
+
+ if (backgroundStages <= 0) {
+ // Last stage is foreground
+ next = () -> {
+ try {
+ callback.run();
+ } finally {
+ done.countDown();
+ }
+ };
+ } else {
+ // Next stage is background
+ next = this;
+ }
+ }
+ }
+
+ @Override
+ public Runnable getNextStage() {
+ return next;
+ }
+
+ void join() throws InterruptedException {
+ done.await();
+ }
+ }
+
}

Back to the top