Bug 552715 - [Validation] Validation providers should be given the view model context

Add optional API for ValidationProvider implementors to get access to
the view model context.  Ensure in the ValidationServiceImpl that all
providers added to it, including registrations on the extension point,
get that context passed through the core validation service to which
they are attached.

Change-Id: I7ed27314a93a4195eb243dffc0780a4c2b70a166
Signed-off-by: Christian W. Damus <give.a.damus@gmail.com>
diff --git a/bundles/org.eclipse.emf.ecp.view.validation/src/org/eclipse/emf/ecp/view/internal/validation/ValidationProviderHelper.java b/bundles/org.eclipse.emf.ecp.view.validation/src/org/eclipse/emf/ecp/view/internal/validation/ValidationProviderHelper.java
index 34bdbc7..5e72eb1 100644
--- a/bundles/org.eclipse.emf.ecp.view.validation/src/org/eclipse/emf/ecp/view/internal/validation/ValidationProviderHelper.java
+++ b/bundles/org.eclipse.emf.ecp.view.validation/src/org/eclipse/emf/ecp/view/internal/validation/ValidationProviderHelper.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 2.0
@@ -10,34 +10,111 @@
  *
  * Contributors:
  * mat - initial API and implementation
+ * Christian W. Damus - bug 552715
  ******************************************************************************/
 package org.eclipse.emf.ecp.view.internal.validation;
 
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
 import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 import org.eclipse.core.runtime.CoreException;
 import org.eclipse.core.runtime.IConfigurationElement;
 import org.eclipse.core.runtime.IExtensionRegistry;
 import org.eclipse.core.runtime.Platform;
+import org.eclipse.emf.common.util.Diagnostic;
+import org.eclipse.emf.ecore.EObject;
+import org.eclipse.emf.ecp.view.spi.context.ViewModelContext;
 import org.eclipse.emf.ecp.view.spi.validation.ValidationProvider;
+import org.eclipse.emfforms.common.spi.validation.ValidationService;
 
 /**
  * Helper class for fetching ECP validators.
- * See ValidationService#addValidator(org.eclipse.emfforms.common.spi.validation.Validator)
+ * See {@link ValidationService#addValidator(org.eclipse.emfforms.common.spi.validation.Validator)}.
  *
  * @author Mat Hansen <mhansen@eclipsesource.com>
  */
 public final class ValidationProviderHelper {
 
-	private ValidationProviderHelper() {
+	private final Map<ValidationProvider, ValidationProvider> providers = new HashMap<>();
+	private final ViewModelContext context;
+	private final ValidationServiceImpl validationService;
+
+	/**
+	 * Initializes me with the validation service that owns me.
+	 *
+	 * @param context the view model context
+	 * @param validationService my owner
+	 */
+	ValidationProviderHelper(ViewModelContext context, ValidationServiceImpl validationService) {
+		super();
+
+		this.context = context;
+		this.validationService = validationService;
+	}
+
+	/**
+	 * Query the registered validation providers.
+	 *
+	 * @return the registered validation providers
+	 */
+	public Set<ValidationProvider> getValidationProviders() {
+		return Collections.unmodifiableSet(providers.keySet());
+	}
+
+	/**
+	 * Initialize my providers.
+	 */
+	public void initialize() {
+		fetchValidationProviders().forEach(p -> validationService.addValidationProvider(p, false));
+	}
+
+	/**
+	 * Dispose and clear out the registered validation providers.
+	 */
+	public void dispose() {
+		try {
+			providers.keySet().forEach(p -> validationService.removeValidationProvider(p, false));
+		} finally {
+			providers.clear();
+		}
+	}
+
+	/**
+	 * Wrap a validation {@code provider} to inject my view model {@code context} through the core validation service.
+	 *
+	 * @param provider the provider to wrap
+	 *
+	 * @return the wrapper
+	 */
+	ValidationProvider wrap(ValidationProvider provider) {
+		return providers.computeIfAbsent(provider, Wrapper::new);
+	}
+
+	/**
+	 * Unwrap a validation {@code provider}.
+	 *
+	 * @param provider the provider to unwrap (usually a wrapper)
+	 *
+	 * @return the unwrapped provider
+	 */
+	ValidationProvider unwrap(ValidationProvider provider) {
+		return provider instanceof Wrapper ? ((Wrapper) provider).delegate : provider;
 	}
 
 	/**
 	 * Fetch all known ECP validators using the ECP validationProvider extension point.
 	 *
 	 * @return the validators found
+	 *
+	 * @deprecated Since 1.23, use instances of this class, instead
 	 */
+	@Deprecated
 	public static Set<ValidationProvider> fetchValidationProviders() {
 		final Set<ValidationProvider> providers = new LinkedHashSet<ValidationProvider>();
 
@@ -58,4 +135,56 @@
 		return providers;
 	}
 
+	//
+	// Nested types
+	//
+
+	/**
+	 * A wrapper for a validation provider that injects the current view model context into it.
+	 */
+	private final class Wrapper implements ValidationProvider {
+
+		private final ValidationProvider delegate;
+
+		Wrapper(ValidationProvider delegate) {
+			super();
+
+			this.delegate = delegate;
+		}
+
+		@Override
+		public void setContext(ViewModelContext context) {
+			delegate.setContext(context);
+		}
+
+		@Override
+		public void unsetContext(ViewModelContext context) {
+			delegate.unsetContext(context);
+		}
+
+		@SuppressWarnings("unchecked") // The core validation service doesn't write to the list
+		@Override
+		public List<Diagnostic> validate(EObject eObject) {
+			final Iterable<? extends Diagnostic> delegated = delegate.validate(context, eObject);
+			if (delegated == null) {
+				return Collections.emptyList();
+			}
+			if (delegated instanceof List<?>) {
+				return (List<Diagnostic>) delegated;
+			}
+			if (delegated instanceof Collection<?>) {
+				return new ArrayList<>((Collection<Diagnostic>) delegated);
+			}
+			final List<Diagnostic> result = new ArrayList<Diagnostic>();
+			delegated.forEach(result::add);
+			return result;
+		}
+
+		@Override
+		public Iterable<? extends Diagnostic> validate(ViewModelContext userContext, EObject object) {
+			return delegate.validate(userContext, object);
+		}
+
+	}
+
 }
diff --git a/bundles/org.eclipse.emf.ecp.view.validation/src/org/eclipse/emf/ecp/view/internal/validation/ValidationServiceImpl.java b/bundles/org.eclipse.emf.ecp.view.validation/src/org/eclipse/emf/ecp/view/internal/validation/ValidationServiceImpl.java
index e747b00..2075c87 100644
--- a/bundles/org.eclipse.emf.ecp.view.validation/src/org/eclipse/emf/ecp/view/internal/validation/ValidationServiceImpl.java
+++ b/bundles/org.eclipse.emf.ecp.view.validation/src/org/eclipse/emf/ecp/view/internal/validation/ValidationServiceImpl.java
@@ -10,7 +10,7 @@
  *
  * Contributors:
  * Eugen - initial API and implementation
- * Christian W. Damus - bugs 533522, 543160, 545686, 527686, 548761, 552127
+ * Christian W. Damus - bugs 533522, 543160, 545686, 527686, 548761, 552127, 552715
  ******************************************************************************/
 package org.eclipse.emf.ecp.view.internal.validation;
 
@@ -358,6 +358,7 @@
 	private int propagationThreshold;
 
 	private final Set<ValidationUpdateListener> validationUpdateListeners = new LinkedHashSet<>();
+	private ValidationProviderHelper providerHelper;
 
 	@Override
 	public void instantiate(ViewModelContext context) {
@@ -367,6 +368,7 @@
 		placeholderFactory = new ThresholdDiagnostic.Factory(l10n);
 		mappingProviderManager = context.getService(EMFFormsMappingProviderManager.class);
 		controlMapper = context.getService(EMFFormsSettingToControlMapper.class);
+		providerHelper = new ValidationProviderHelper(context, this);
 		final VElement renderable = context.getViewModel();
 
 		if (renderable == null) {
@@ -413,7 +415,7 @@
 
 		validationService.setSubstitutionLabelProvider(substitutionLabelProvider);
 
-		registerValidationProviders();
+		providerHelper.initialize();
 
 		domainChangeListener = new ValidationDomainModelChangeListener(context);
 		context.registerDomainChangeListener(domainChangeListener);
@@ -461,18 +463,13 @@
 		}
 	}
 
-	private void registerValidationProviders() {
-		for (final ValidationProvider provider : ValidationProviderHelper.fetchValidationProviders()) {
-			validationService.addValidator(provider);
-		}
-	}
-
 	@Override
 	public void dispose() {
 		contextTracker.close();
 		viewModelChangeListeners.forEach((ctx, l) -> ctx.unregisterViewChangeListener(l));
 		viewModelChangeListeners.clear();
 		rootContext.unregisterDomainChangeListener(domainChangeListener);
+		providerHelper.dispose();
 		adapterFactory.dispose();
 	}
 
@@ -938,7 +935,9 @@
 
 	@Override
 	public void addValidationProvider(ValidationProvider validationProvider, boolean revalidate) {
-		validationService.addValidator(validationProvider);
+		final ValidationProvider provider = providerHelper.wrap(validationProvider);
+		provider.setContext(rootContext);
+		validationService.addValidator(provider);
 		if (revalidate && rootContext != null) {
 			validate(getAllEObjectsToValidate(rootContext));
 		}
@@ -951,7 +950,10 @@
 
 	@Override
 	public void removeValidationProvider(ValidationProvider validationProvider, boolean revalidate) {
-		validationService.removeValidator(validationProvider);
+		// Get the wrapper that we made on adding this provider
+		final ValidationProvider provider = providerHelper.wrap(validationProvider);
+		validationService.removeValidator(provider);
+		provider.unsetContext(rootContext);
 		if (revalidate && rootContext != null) {
 			validate(getAllEObjectsToValidate(rootContext));
 		}
diff --git a/bundles/org.eclipse.emf.ecp.view.validation/src/org/eclipse/emf/ecp/view/spi/validation/ValidationProvider.java b/bundles/org.eclipse.emf.ecp.view.validation/src/org/eclipse/emf/ecp/view/spi/validation/ValidationProvider.java
index c47adf8..27c6505 100644
--- a/bundles/org.eclipse.emf.ecp.view.validation/src/org/eclipse/emf/ecp/view/spi/validation/ValidationProvider.java
+++ b/bundles/org.eclipse.emf.ecp.view.validation/src/org/eclipse/emf/ecp/view/spi/validation/ValidationProvider.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2011-2013 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 2.0
@@ -10,20 +10,103 @@
  *
  * Contributors:
  * Eugen Neufeld - initial API and implementation
+ * Christian W. Damus - bug 552715
  ******************************************************************************/
 package org.eclipse.emf.ecp.view.spi.validation;
 
+import java.util.List;
+
+import org.eclipse.emf.common.util.Diagnostic;
+import org.eclipse.emf.ecore.EObject;
+import org.eclipse.emf.ecp.view.spi.context.ViewModelContext;
 import org.eclipse.emfforms.common.spi.validation.Validator;
 
 /**
+ * <p>
  * The ValidationService calls the providers after the validation with EMF.
  * By providing an own provider, one can extend the EMF validation by providing additional validation rules.
+ * </p>
+ * <p>
+ * As of the 1.23 release, for validation that requires the current view model context, consider using a subclass of the
+ * nested {@link ContextSensitive} class.
+ * </p>
  *
  * @author Eugen Neufeld
  * @since 1.5
  *
  */
-// TODO mark as deprecated
 public interface ValidationProvider extends Validator {
 
+	/**
+	 * Initialize me in the view model {@code context} of the {@link ValidationService} to which I have been added.
+	 * Note that I could be added to validation services in more than one context.
+	 *
+	 * @param context the context of the {@link ValidationService} to which I have been added
+	 *
+	 * @since 1.23
+	 */
+	default void setContext(ViewModelContext context) {
+		// Nothing to do
+	}
+
+	/**
+	 * Notify me that I have been removed from the {@link ValidationService} in the given {@code context}.
+	 * Note that I may still be used in validation services in other contexts.
+	 *
+	 * @param context the context of the {@link ValidationService} from which I have been removed
+	 *
+	 * @since 1.23
+	 */
+	default void unsetContext(ViewModelContext context) {
+		// Nothing to do
+	}
+
+	/**
+	 * Validate an {@code object} in a view model {@code context}.
+	 *
+	 * @param context the view model context in which validation is occurring
+	 * @param object the object to validate
+	 * @return the results of validation of the {@code object}, or {@code null} if none
+	 *
+	 * @since 1.23
+	 */
+	default Iterable<? extends Diagnostic> validate(ViewModelContext context, EObject object) {
+		return validate(object);
+	}
+
+	//
+	// Nested types
+	//
+
+	/**
+	 * A context-sensitive {@link ValidationProvider} that implements the
+	 * {@link ValidationProvider#validate(ViewModelContext, EObject)}
+	 * method to the exclusion of {@link Validator#validate(EObject)}.
+	 *
+	 * @since 1.23
+	 */
+	abstract class ContextSensitive implements ValidationProvider {
+
+		/**
+		 * Initializes me.
+		 */
+		public ContextSensitive() {
+			super();
+		}
+
+		/**
+		 * Un-implements the inherited method.
+		 *
+		 * @throws UnsupportedOperationException always
+		 */
+		@Override
+		public final List<Diagnostic> validate(EObject eObject) {
+			throw new UnsupportedOperationException("validate(EObject)"); //$NON-NLS-1$
+		}
+
+		@Override
+		public abstract Iterable<? extends Diagnostic> validate(ViewModelContext context, EObject object);
+
+	}
+
 }
diff --git a/tests/org.eclipse.emf.ecp.view.validation.bean.test/META-INF/MANIFEST.MF b/tests/org.eclipse.emf.ecp.view.validation.bean.test/META-INF/MANIFEST.MF
index 5a18664..922de75 100644
--- a/tests/org.eclipse.emf.ecp.view.validation.bean.test/META-INF/MANIFEST.MF
+++ b/tests/org.eclipse.emf.ecp.view.validation.bean.test/META-INF/MANIFEST.MF
@@ -18,3 +18,4 @@
 Automatic-Module-Name: org.eclipse.emf.ecp.view.validation.bean.test
 Bundle-ActivationPolicy: lazy
 Bundle-RequiredExecutionEnvironment: JavaSE-1.8
+Import-Package: org.eclipse.emf.ecp.view.spi.context;version="[1.23.0,2.0.0)"
diff --git a/tests/org.eclipse.emf.ecp.view.validation.test/src/org/eclipse/emf/ecp/view/validation/test/ValidationProvider_PTest.java b/tests/org.eclipse.emf.ecp.view.validation.test/src/org/eclipse/emf/ecp/view/validation/test/ValidationProvider_PTest.java
index d1dc9e8..9233125 100644
--- a/tests/org.eclipse.emf.ecp.view.validation.test/src/org/eclipse/emf/ecp/view/validation/test/ValidationProvider_PTest.java
+++ b/tests/org.eclipse.emf.ecp.view.validation.test/src/org/eclipse/emf/ecp/view/validation/test/ValidationProvider_PTest.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2011-2013 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 2.0
@@ -10,9 +10,12 @@
  *
  * Contributors:
  * Eugen Neufeld - initial API and implementation
+ * Christian W. Damus - bug 552715
  ******************************************************************************/
 package org.eclipse.emf.ecp.view.validation.test;
 
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
@@ -252,4 +255,74 @@
 		validationService.validate(Collections.singleton(EObject.class.cast(computer)));
 		assertEquals(Diagnostic.OK, control.getDiagnostic().getHighestSeverity());
 	}
+
+	/**
+	 * @see <a href="http://eclip.se/552715">bug 552715</a>
+	 */
+	@Test
+	public void testValidationProviderInitializedWithContext() {
+		final ViewModelContext[] capturedContext = { null };
+
+		validationService.addValidationProvider(new ValidationProvider() {
+
+			@Override
+			public List<Diagnostic> validate(EObject eObject) {
+				return Collections.emptyList();
+			}
+
+			@Override
+			public void setContext(ViewModelContext context) {
+				capturedContext[0] = context;
+			}
+		});
+
+		assertThat("No view-model context captured", capturedContext[0], notNullValue());
+	}
+
+	/**
+	 * @see <a href="http://eclip.se/552715">bug 552715</a>
+	 */
+	@Test
+	public void testValidationProviderDisposedWithContext() {
+		final ViewModelContext[] capturedContext = { null };
+
+		final ValidationProvider provider = new ValidationProvider() {
+
+			@Override
+			public List<Diagnostic> validate(EObject eObject) {
+				return Collections.emptyList();
+			}
+
+			@Override
+			public void unsetContext(ViewModelContext context) {
+				capturedContext[0] = context;
+			}
+		};
+		validationService.addValidationProvider(provider);
+		validationService.removeValidationProvider(provider);
+
+		assertThat("No view-model context captured", capturedContext[0], notNullValue());
+	}
+
+	/**
+	 * @see <a href="http://eclip.se/552715">bug 552715</a>
+	 */
+	@Test
+	public void testValidationProviderInvokedWithContext() {
+		final ViewModelContext[] capturedContext = { null };
+
+		final ValidationProvider provider = new ValidationProvider.ContextSensitive() {
+
+			@Override
+			public Iterable<? extends Diagnostic> validate(ViewModelContext context, EObject object) {
+				capturedContext[0] = context;
+				return Collections.emptyList();
+			}
+		};
+
+		validationService.addValidationProvider(provider);
+
+		assertThat("No view-model context captured", capturedContext[0], notNullValue());
+	}
+
 }