blob: 56872c8d8c1b4f2cd51bdaa7886680dcd50d985b [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2011-2014 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
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Eugen - initial API and implementation
******************************************************************************/
package org.eclipse.emf.ecp.view.internal.validation;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
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.notify.AdapterFactory;
import org.eclipse.emf.common.notify.Notification;
import org.eclipse.emf.common.notify.Notifier;
import org.eclipse.emf.common.util.BasicDiagnostic;
import org.eclipse.emf.common.util.Diagnostic;
import org.eclipse.emf.common.util.DiagnosticChain;
import org.eclipse.emf.common.util.TreeIterator;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EReference;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.emf.ecore.EStructuralFeature.Setting;
import org.eclipse.emf.ecore.EValidator;
import org.eclipse.emf.ecore.InternalEObject;
import org.eclipse.emf.ecore.util.Diagnostician;
import org.eclipse.emf.ecore.util.EObjectValidator;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.emf.ecp.common.spi.UniqueSetting;
import org.eclipse.emf.ecp.view.spi.context.ViewModelContext;
import org.eclipse.emf.ecp.view.spi.model.ModelChangeAddRemoveListener;
import org.eclipse.emf.ecp.view.spi.model.ModelChangeNotification;
import org.eclipse.emf.ecp.view.spi.model.VControl;
import org.eclipse.emf.ecp.view.spi.model.VDiagnostic;
import org.eclipse.emf.ecp.view.spi.model.VDomainModelReference;
import org.eclipse.emf.ecp.view.spi.model.VElement;
import org.eclipse.emf.ecp.view.spi.model.VViewFactory;
import org.eclipse.emf.ecp.view.spi.model.VViewPackage;
import org.eclipse.emf.ecp.view.spi.validation.ValidationProvider;
import org.eclipse.emf.ecp.view.spi.validation.ValidationService;
import org.eclipse.emf.ecp.view.spi.validation.ViewValidationListener;
import org.eclipse.emf.edit.provider.ComposedAdapterFactory;
import org.eclipse.emf.edit.provider.ReflectiveItemProviderAdapterFactory;
/**
* Validation service that, once instantiated, synchronizes the validation result of a model element with its
* Renderable.
*
* @author Eugen Neufeld
*
*/
public class ValidationServiceImpl implements ValidationService {
/**
* The {@link ValidationDomainModelChangeListener} for the view model.
*
*/
private class ViewModelChangeListener implements ModelChangeAddRemoveListener {
@Override
public void notifyChange(ModelChangeNotification notification) {
if (VViewPackage.eINSTANCE.getElement_Enabled() == notification.getRawNotification()
.getFeature()
|| VViewPackage.eINSTANCE.getElement_Visible() == notification.getRawNotification()
.getFeature()) {
if (VViewPackage.eINSTANCE.getControl().isInstance(notification.getNotifier())) {
final VControl control = (VControl) notification.getNotifier();
if (VViewPackage.eINSTANCE.getElement_Enabled() == notification.getRawNotification().getFeature()) {
control.setDiagnostic(null);
}
final VDomainModelReference domainModelReference = control.getDomainModelReference();
if (domainModelReference == null) {
return;
}
final Iterator<Setting> settings = domainModelReference.getIterator();
final Set<EObject> eObjectsToValidate = new LinkedHashSet<EObject>();
while (settings.hasNext()) {
eObjectsToValidate.add(settings.next().getEObject());
}
for (final EObject eObject : eObjectsToValidate) {
validate(eObject);
}
}
}
if (!VElement.class.isInstance(notification.getNotifier())) {
return;
}
switch (notification.getRawNotification().getEventType()) {
case Notification.REMOVE:
case Notification.REMOVE_MANY:
final Map<VElement, VDiagnostic> map = Collections.emptyMap();
reevaluateToTop(notification.getNotifier(), map);
break;
default:
break;
}
}
@Override
public void notifyAdd(Notifier notifier) {
if (VControl.class.isInstance(notifier)) {
final VDomainModelReference domainModelReference = VControl.class.cast(notifier)
.getDomainModelReference();
if (domainModelReference == null) {
return;
}
final Iterator<Setting> settings = domainModelReference.getIterator();
final Set<EObject> eObjectsToValidate = new LinkedHashSet<EObject>();
while (settings.hasNext()) {
eObjectsToValidate.add(settings.next().getEObject());
}
for (final EObject eObject : eObjectsToValidate) {
validate(eObject);
}
}
}
@Override
public void notifyRemove(Notifier notifier) {
// do nothing
}
}
/**
* The {@link ValidationDomainModelChangeListener} for the domain model.
*
*/
private class ValidationDomainModelChangeListener implements ModelChangeAddRemoveListener {
@SuppressWarnings("unchecked")
@Override
public void notifyChange(ModelChangeNotification notification) {
if (ValidationNotification.class.isInstance(notification.getRawNotification())) {
validate(notification.getNotifier());
return;
}
final Notification rawNotification = notification.getRawNotification();
switch (rawNotification.getEventType()) {
// FIXME: move add, remove to add/remove instead of doing here
case Notification.ADD:
final Set<EObject> toValidate = new LinkedHashSet<EObject>();
toValidate.add(notification.getNotifier());
// in case of not containment references
if (EReference.class.isInstance(notification.getStructuralFeature())) {
toValidate.addAll(getAllEObjects((EObject) notification.getRawNotification().getNewValue()));
}
validate(toValidate);
break;
case Notification.ADD_MANY:
validate(notification.getNotifier());
// in case of not containment references
if (EReference.class.isInstance(notification.getStructuralFeature())) {
validate((Collection<EObject>) notification.getRawNotification().getNewValue());
}
break;
case Notification.REMOVE:
if (EReference.class.isInstance(rawNotification.getFeature())) {
cleanControlDiagnostics(EObject.class.cast(notification.getNotifier()),
EReference.class.cast(rawNotification.getFeature()),
EObject.class.cast(rawNotification.getOldValue()));
}
//$FALL-THROUGH$
case Notification.REMOVE_MANY:
// TODO JF since we now have an indexed dmr, this should clean diagnostics, too, doesn't it?
validate(getAllEObjects(notification.getNotifier()));
break;
case Notification.REMOVING_ADAPTER:
break;
default:
validate(notification.getNotifier());
if (EReference.class.isInstance(notification.getStructuralFeature())) {
if (notification.getRawNotification().getNewValue() != null) {
final Object newValue = notification.getRawNotification().getNewValue();
/*
* unset on a list has a boolean as a new value. therefore we need to check if new value is an
* EObject
*/
if (EObject.class.isInstance(newValue)) {
validate((EObject) newValue);
}
}
}
}
}
@Override
public void notifyAdd(Notifier notifier) {
}
@Override
public void notifyRemove(Notifier notifier) {
}
}
private final Set<ValidationProvider> validationProviders = new LinkedHashSet<ValidationProvider>();
private ValidationDomainModelChangeListener domainChangeListener;
private ViewModelChangeListener viewChangeListener;
private ViewModelContext context;
private final Queue<EObject> validationQueue = new LinkedList<EObject>();
private final Set<EObject> validated = new LinkedHashSet<EObject>();
private boolean validationRunning;
private final Map<UniqueSetting, VDiagnostic> currentUpdates = new LinkedHashMap<UniqueSetting, VDiagnostic>();
/**
* {@inheritDoc}
*
* @see org.eclipse.emf.ecp.view.spi.context.ViewModelService#instantiate(org.eclipse.emf.ecp.view.spi.context.ViewModelContext)
*/
@Override
public void instantiate(ViewModelContext context) {
this.context = context;
final VElement renderable = context.getViewModel();
if (renderable == null) {
throw new IllegalStateException("View model must not be null"); //$NON-NLS-1$
}
final EObject domainModel = context.getDomainModel();
if (domainModel == null) {
throw new IllegalStateException("Domain model must not be null"); //$NON-NLS-1$
}
readValidationProvider();
domainChangeListener = new ValidationDomainModelChangeListener();
viewChangeListener = new ViewModelChangeListener();
context.registerDomainChangeListener(domainChangeListener);
context.registerViewChangeListener(viewChangeListener);
validate(getAllEObjects(domainModel));
}
private void cleanControlDiagnostics(EObject parent, EReference parentReference, EObject removedEObject) {
final Set<VElement> controls = context.getControlsFor(UniqueSetting.createSetting(parent, parentReference));
if (controls == null) {
return;
}
for (final VElement vControl : controls) {
if (vControl == null) {
continue;
}
if (vControl.getDiagnostic() == null) {
continue;
}
final Set<Object> diagnosticsToRemove = new LinkedHashSet<Object>();
for (final Object diagnosticObject : vControl.getDiagnostic().getDiagnostics()) {
final Diagnostic diagnostic = Diagnostic.class.cast(diagnosticObject);
if (diagnostic.getData().size() < 1) {
continue;
}
if (removedEObject.equals(diagnostic.getData().get(0))) {
diagnosticsToRemove.add(diagnostic);
}
}
vControl.getDiagnostic().getDiagnostics().removeAll(diagnosticsToRemove);
}
}
private void readValidationProvider() {
final IExtensionRegistry extensionRegistry = Platform.getExtensionRegistry();
if (extensionRegistry == null) {
return;
}
final IConfigurationElement[] controls = extensionRegistry
.getConfigurationElementsFor("org.eclipse.emf.ecp.view.validation.validationProvider"); //$NON-NLS-1$
for (final IConfigurationElement e : controls) {
try {
final ValidationProvider validationProvider = (ValidationProvider) e.createExecutableExtension("class"); //$NON-NLS-1$
validationProviders.add(validationProvider);
} catch (final CoreException e1) {
Activator.logException(e1);
}
}
}
/**
* {@inheritDoc}
*
* @see org.eclipse.emf.ecp.view.spi.context.ViewModelService#dispose()
*/
@Override
public void dispose() {
context.unregisterDomainChangeListener(domainChangeListener);
context.unregisterViewChangeListener(viewChangeListener);
}
/**
* {@inheritDoc}
*
* @see org.eclipse.emf.ecp.view.spi.context.ViewModelService#getPriority()
*/
@Override
public int getPriority() {
return 3;
}
/**
* Returns a collection of all direct and indirect child-EObjects including the parent.
*
* @param eObject the parent
* @return all eobjects
*/
private Collection<EObject> getAllEObjects(EObject eObject) {
final List<EObject> result = new ArrayList<EObject>();
result.add(eObject);
final TreeIterator<EObject> iterator = EcoreUtil.getAllContents(eObject, false);
while (iterator.hasNext()) {
result.add(iterator.next());
}
return result;
}
/**
* {@inheritDoc}
*
* @see org.eclipse.emf.ecp.view.spi.validation.ValidationService#validate(java.util.Collection)
*/
@Override
public void validate(Collection<EObject> eObjects) {
for (final EObject eObject : eObjects) {
if (validated.contains(eObject)) {
continue;
}
validated.add(eObject);
validationQueue.offer(eObject);
}
processValidationQueue();
}
/**
* Validate the given eObject.
*
* @param eObject the eObject to validate
*/
public void validate(EObject eObject) {
if (!validated.contains(eObject)) {
validated.add(eObject);
validationQueue.offer(eObject);
processValidationQueue();
}
}
private void processValidationQueue() {
// prohibit reentry in recursion
if (validationRunning) {
return;
}
validationRunning = true;
EObject toValidate;
while ((toValidate = validationQueue.poll()) != null) {
validateAndCollectSettings(toValidate);
}
update();
notifyListeners();
currentUpdates.clear();
validated.clear();
validationRunning = false;
}
/**
* Notifies all listeners.
*/
public void notifyListeners() {
if (validationListeners.size() > 0) {
final Set<Diagnostic> result = getDiagnosticResult();
for (final ViewValidationListener l : validationListeners) {
l.onNewValidation(result);
}
}
}
private void update() {
final Map<VElement, VDiagnostic> controlDiagnosticMap = new LinkedHashMap<VElement, VDiagnostic>();
for (final UniqueSetting uniqueSetting : currentUpdates.keySet()) {
final Set<VElement> controls = context.getControlsFor(uniqueSetting);
if (controls == null) {
continue;
}
for (final VElement control : controls) {
if (!controlDiagnosticMap.containsKey(control)) {
controlDiagnosticMap.put(control, VViewFactory.eINSTANCE.createDiagnostic());
}
// TODO Performance
controlDiagnosticMap.get(control).getDiagnostics()
.removeAll(currentUpdates.get(uniqueSetting).getDiagnostics());
controlDiagnosticMap.get(control).getDiagnostics()
.addAll(currentUpdates.get(uniqueSetting).getDiagnostics());
// add all diagnostics of control which are not in the currentUpdates
if (control.getDiagnostic() == null) {
continue;
}
for (final Object diagnosticObject : control.getDiagnostic().getDiagnostics()) {
final Diagnostic diagnostic = Diagnostic.class.cast(diagnosticObject);
if (diagnostic.getData().size() < 2 || !EObject.class.isInstance(diagnostic.getData().get(0))
|| !EStructuralFeature.class.isInstance(diagnostic.getData().get(1))) {
continue;
}
final EObject diagnosticEobject = EObject.class.cast(diagnostic.getData().get(0));
// TODO performance
if (!isObjectStillValid(diagnosticEobject)) {
continue;
}
final UniqueSetting uniqueSetting2 = UniqueSetting.createSetting(
diagnosticEobject,
EStructuralFeature.class.cast(diagnostic.getData().get(1)));
if (!currentUpdates.containsKey(uniqueSetting2)) {
controlDiagnosticMap.get(control).getDiagnostics().add(diagnosticObject);
}
}
}
}
updateAndPropagate(controlDiagnosticMap);
}
private boolean isObjectStillValid(EObject diagnosticEobject) {
EObject toCheck = diagnosticEobject;
while (toCheck != null && toCheck != context.getDomainModel()) {
toCheck = toCheck.eContainer();
}
return toCheck == context.getDomainModel();
}
private void updateAndPropagate(Map<VElement, VDiagnostic> controlDiagnosticMap) {
for (final VElement control : controlDiagnosticMap.keySet()) {
control.setDiagnostic(controlDiagnosticMap.get(control));
reevaluateToTop(control.eContainer(), controlDiagnosticMap);
}
}
private void reevaluateToTop(EObject parent, Map<VElement, VDiagnostic> controlDiagnosticMap) {
while (parent != null) {
final EObject newParent = parent.eContainer();
if (!VElement.class.isInstance(parent)) {
parent = newParent;
continue;
}
final VElement vElement = (VElement) parent;
final VDiagnostic vDiagnostic = VViewFactory.eINSTANCE.createDiagnostic();
if (controlDiagnosticMap.containsKey(vElement)) {
vDiagnostic.getDiagnostics().addAll(controlDiagnosticMap.get(vElement).getDiagnostics());
}
for (final EObject eObject : vElement.eContents()) {
if (!VElement.class.isInstance(eObject)) {
continue;
}
final VElement childElement = (VElement) eObject;
// check that the child is visible and enabled
if (childElement.getDiagnostic() != null && childElement.isEnabled() && childElement.isVisible()) {
vDiagnostic.getDiagnostics().addAll(childElement.getDiagnostic().getDiagnostics());
}
}
vElement.setDiagnostic(vDiagnostic);
parent = newParent;
}
}
private void validateAndCollectSettings(EObject eObject) {
final Diagnostic diagnostic = getDiagnosticForEObject(eObject);
for (final EStructuralFeature feature : eObject.eClass().getEAllStructuralFeatures()) {
final UniqueSetting uniqueSetting = UniqueSetting.createSetting(eObject, feature);
if (!currentUpdates.containsKey(uniqueSetting)) {
currentUpdates.put(uniqueSetting, VViewFactory.eINSTANCE.createDiagnostic());
}
}
analyzeDiagnostic(diagnostic);
}
private void analyzeDiagnostic(Diagnostic diagnostic) {
if (diagnostic.getData().size() > 1) {
if (InternalEObject.class.isInstance(diagnostic.getData().get(0))
&& EStructuralFeature.class.isInstance(diagnostic.getData().get(1))) {
final InternalEObject internalEObject = (InternalEObject) diagnostic.getData().get(0);
final EStructuralFeature eStructuralFeature = (EStructuralFeature) diagnostic.getData().get(1);
final Setting setting = internalEObject.eSetting(eStructuralFeature);
final UniqueSetting uniqueSetting = UniqueSetting.createSetting(setting);
if (!currentUpdates.containsKey(uniqueSetting)) {
currentUpdates.put(uniqueSetting, VViewFactory.eINSTANCE.createDiagnostic());
}
currentUpdates.get(uniqueSetting).getDiagnostics().add(diagnostic);
}
}
else {
for (final Diagnostic childDiagnostic : diagnostic.getChildren()) {
analyzeDiagnostic(childDiagnostic);
}
}
}
/**
* Computes the {@link Diagnostic} for the given eObject.
*
* @param object the eObject to validate
* @return the diagnostic
*/
public Diagnostic getDiagnosticForEObject(EObject object) {
EValidator validator = EValidator.Registry.INSTANCE.getEValidator(object.eClass().getEPackage());
final BasicDiagnostic diagnostics = Diagnostician.INSTANCE.createDefaultDiagnostic(object);
if (validator == null) {
validator = new EObjectValidator();
}
final ComposedAdapterFactory adapterFactory = new ComposedAdapterFactory(new AdapterFactory[] {
new ReflectiveItemProviderAdapterFactory(),
new ComposedAdapterFactory(ComposedAdapterFactory.Descriptor.Registry.INSTANCE) });
final Map<Object, Object> context = new LinkedHashMap<Object, Object>();
context.put(EValidator.SubstitutionLabelProvider.class, new ECPSubstitutionLabelProvider(adapterFactory));
context.put(EValidator.class, validator);
validator.validate(object, diagnostics, context);
adapterFactory.dispose();
final Map<EStructuralFeature, DiagnosticChain> diagnosticMap = new LinkedHashMap<EStructuralFeature, DiagnosticChain>();
for (final Diagnostic child : diagnostics.getChildren()) {
if (DiagnosticChain.class.isInstance(child) && checkDiagnosticData(child)) {
diagnosticMap.put((EStructuralFeature) child.getData().get(1), (DiagnosticChain) child);
}
}
for (final ValidationProvider validationProvider : validationProviders) {
final List<Diagnostic> additionValidation = validationProvider.validate(object);
for (final Diagnostic additionDiagnostic : additionValidation) {
if (diagnosticMap.containsKey(additionDiagnostic.getData().get(1))) {
diagnosticMap.get(additionDiagnostic.getData().get(1)).add(additionDiagnostic);
} else {
diagnostics.add(additionDiagnostic);
}
}
}
return diagnostics;
}
private boolean checkDiagnosticData(Diagnostic diagnostic) {
final List<?> data = diagnostic.getData();
if (data.size() < 2) {
return false;
}
if (!EObject.class.isInstance(data.get(0))) {
return false;
}
if (!EStructuralFeature.class.isInstance(data.get(1))) {
return false;
}
return true;
}
/**
*
* {@inheritDoc}
*
* @see org.eclipse.emf.ecp.view.spi.validation.ValidationService#addValidationProvider(org.eclipse.emf.ecp.view.spi.validation.ValidationProvider)
*/
@Override
public void addValidationProvider(ValidationProvider validationProvider) {
validationProviders.add(validationProvider);
validate(getAllEObjects(context.getDomainModel()));
}
/**
*
* {@inheritDoc}
*
* @see org.eclipse.emf.ecp.view.spi.validation.ValidationService#removeValidationProvider(org.eclipse.emf.ecp.view.spi.validation.ValidationProvider)
*/
@Override
public void removeValidationProvider(ValidationProvider validationProvider) {
validationProviders.remove(validationProvider);
validate(getAllEObjects(context.getDomainModel()));
}
private final Set<ViewValidationListener> validationListeners = new LinkedHashSet<ViewValidationListener>();
/**
* {@inheritDoc}
*
* @see org.eclipse.emf.ecp.view.spi.validation.ValidationService#registerValidationListener(org.eclipse.emf.ecp.view.spi.validation.ViewValidationListener)
*/
@Override
public void registerValidationListener(ViewValidationListener listener) {
validationListeners.add(listener);
listener.onNewValidation(getDiagnosticResult());
}
private Set<Diagnostic> getDiagnosticResult() {
final Set<Diagnostic> result = new LinkedHashSet<Diagnostic>();
final VDiagnostic diagnostic = context.getViewModel().getDiagnostic();
if (diagnostic != null) {
for (final Object diagObject : diagnostic.getDiagnostics()) {
result.add((Diagnostic) diagObject);
}
}
return result;
}
/**
* {@inheritDoc}
*
* @see org.eclipse.emf.ecp.view.spi.validation.ValidationService#deregisterValidationListener(org.eclipse.emf.ecp.view.spi.validation.ViewValidationListener)
*/
@Override
public void deregisterValidationListener(ViewValidationListener listener) {
validationListeners.remove(listener);
}
/**
* {@inheritDoc}
*
* @see org.eclipse.emf.ecp.view.spi.context.GlobalViewModelService#childViewModelContextAdded(org.eclipse.emf.ecp.view.spi.context.ViewModelContext)
*/
@Override
public void childViewModelContextAdded(ViewModelContext childContext) {
// do nothing
}
}