blob: 45d3e9c6ed926162c0ab20469d8e9d981739fe00 [file] [log] [blame]
/*******************************************************************************
* 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
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Eugen - initial API and implementation
* Christian W. Damus - bugs 533522, 543160, 545686, 527686, 548761, 552127, 552715
******************************************************************************/
package org.eclipse.emf.ecp.view.internal.validation;
import static org.eclipse.emf.ecp.view.spi.validation.ValidationServiceConstants.PROPAGATION_LIMIT_KEY;
import static org.eclipse.emf.ecp.view.spi.validation.ValidationServiceConstants.PROPAGATION_UNLIMITED_VALUE;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.Queue;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.eclipse.core.runtime.IStatus;
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.BasicEList;
import org.eclipse.emf.common.util.Diagnostic;
import org.eclipse.emf.common.util.EList;
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.SubstitutionLabelProvider;
import org.eclipse.emf.ecore.InternalEObject;
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.VDomainModelReferenceSegment;
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.IncrementalValidationService;
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.ValidationUpdateListener;
import org.eclipse.emf.ecp.view.spi.validation.ViewValidationListener;
import org.eclipse.emf.edit.provider.ComposedAdapterFactory;
import org.eclipse.emf.edit.provider.ReflectiveItemProviderAdapterFactory;
import org.eclipse.emfforms.common.internal.validation.DiagnosticHelper;
import org.eclipse.emfforms.common.spi.validation.ValidationResultListener;
import org.eclipse.emfforms.spi.common.report.AbstractReport;
import org.eclipse.emfforms.spi.common.report.ReportService;
import org.eclipse.emfforms.spi.common.validation.DiagnosticFrequencyMap;
import org.eclipse.emfforms.spi.core.services.controlmapper.EMFFormsSettingToControlMapper;
import org.eclipse.emfforms.spi.core.services.controlmapper.SubControlMapper;
import org.eclipse.emfforms.spi.core.services.databinding.DatabindingFailedException;
import org.eclipse.emfforms.spi.core.services.databinding.DatabindingFailedReport;
import org.eclipse.emfforms.spi.core.services.mappingprovider.EMFFormsMappingProviderManager;
import org.eclipse.emfforms.spi.core.services.view.EMFFormsContextTracker;
import org.eclipse.emfforms.spi.core.services.view.EMFFormsViewContext;
import org.eclipse.emfforms.spi.localization.EMFFormsLocalizationService;
import org.eclipse.osgi.util.NLS;
/**
* Validation service that, once instantiated, synchronizes the validation result of a model element with its
* Renderable.
*
* @author Eugen Neufeld
*
*/
public class ValidationServiceImpl implements ValidationService, IncrementalValidationService {
/**
* The model change listener for the view model.
*
*/
private class ViewModelChangeListener implements ModelChangeAddRemoveListener {
private final EMFFormsViewContext context;
private boolean initialized;
ViewModelChangeListener(EMFFormsViewContext context) {
super();
this.context = context;
}
/**
* Start listening for changes in the view model.
*/
void start() {
initialized = true;
}
@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();
final VDomainModelReference domainModelReference = control.getDomainModelReference();
if (domainModelReference == null) {
return;
}
try {
handleControlNotification(notification, control, domainModelReference);
} catch (final DatabindingFailedException ex) {
reportService.report(new DatabindingFailedReport(ex));
return;
}
}
}
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((VElement) notification.getNotifier(), map);
break;
default:
break;
}
}
private void handleControlNotification(ModelChangeNotification notification, VControl control,
VDomainModelReference domainModelReference) throws DatabindingFailedException {
if (VViewPackage.eINSTANCE.getElement_Enabled() == notification.getRawNotification().getFeature()) {
control.setDiagnostic(null);
}
final Set<UniqueSetting> settingsForControl = controlMapper.getSettingsForControl(control);
final Set<EObject> eObjectsToValidate = new LinkedHashSet<EObject>();
for (final UniqueSetting setting : settingsForControl) {
eObjectsToValidate.add(setting.getEObject());
}
validate(eObjectsToValidate);
}
@Override
public void notifyAdd(Notifier notifier) {
if (!initialized) {
// Not interested in discover of the view model because we already validated everything
return;
}
if (VDomainModelReference.class.isInstance(notifier)
&& !VDomainModelReference.class.isInstance(EObject.class.cast(notifier).eContainer())
&& !VDomainModelReferenceSegment.class.isInstance(EObject.class.cast(notifier).eContainer())) {
final VDomainModelReference domainModelReference = VDomainModelReference.class.cast(notifier);
/*
* We only need to validate something if the new DMR belongs to a VControl. Only in this case there can
* be a rendered UI control which needs to show validation results for the DMR's settings.
*/
if (VControl.class.isInstance(domainModelReference.eContainer())) {
final Set<EObject> eObjectsToValidate = new LinkedHashSet<>();
final Set<UniqueSetting> settings = mappingProviderManager.getAllSettingsFor(domainModelReference,
context.getDomainModel());
for (final UniqueSetting setting : settings) {
final EObject object = setting.getEObject();
if (object != null) {
eObjectsToValidate.add(object);
}
}
validate(eObjectsToValidate);
}
}
}
@Override
public void notifyRemove(Notifier notifier) {
// do nothing
}
}
/**
* The {@link ValidationDomainModelChangeListener} for the domain model.
*
*/
private class ValidationDomainModelChangeListener implements ModelChangeAddRemoveListener {
private final ViewModelContext context;
ValidationDomainModelChangeListener(ViewModelContext context) {
super();
this.context = context;
}
@Override
public void notifyChange(ModelChangeNotification notification) {
if (ValidationNotification.class.isInstance(notification.getRawNotification())) {
validate(notification.getNotifier());
return;
}
if (isIgnore(notification)) {
return;
}
final Notification rawNotification = notification.getRawNotification();
final int eventType = rawNotification.getEventType();
// Special cases
if (eventType == Notification.ADD || eventType == Notification.ADD_MANY) {
handleAdd(notification);
return;
} else if (eventType == Notification.REMOVE || eventType == Notification.REMOVE_MANY) {
handleRemove(notification);
return;
}
// Default case
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);
}
}
}
}
/**
* Indicates whether the given {@link ModelChangeNotification} should be ignored.
*
* @param notification the {@link ModelChangeNotification} to check.
* @return {@code true} if the given notification should be ignored, {@code false} otherwise.
*/
private boolean isIgnore(ModelChangeNotification notification) {
if (notification.getRawNotification().isTouch()) {
return true;
}
final int eventType = notification.getRawNotification().getEventType();
if (eventType == Notification.REMOVING_ADAPTER) {
return true;
}
if (eventType == Notification.SET) {
if (EReference.class.isInstance(notification.getStructuralFeature())) {
final EReference eReference = EReference.class.cast(notification.getStructuralFeature());
if (eReference.isContainer() && notification.getRawNotification().getNewValue() == null) {
return true;
}
}
}
return false;
}
/**
* Handles the case when the given {@link ModelChangeNotification} originates from an ADD.
*
* @param notification
* the {@link ModelChangeNotification} to handle.
*/
@SuppressWarnings("unchecked")
private void handleAdd(ModelChangeNotification notification) {
final Set<EObject> toValidate = new LinkedHashSet<EObject>();
toValidate.add(notification.getNotifier());
// in case of not containment references
if (EReference.class.isInstance(notification.getStructuralFeature())) {
if (Notification.ADD == notification.getRawNotification().getEventType()) {
toValidate.addAll(getAllEObjects((EObject) notification.getRawNotification().getNewValue()));
} else {
toValidate.addAll((Collection<EObject>) notification.getRawNotification().getNewValue());
}
}
validate(toValidate);
}
/**
* Handles the case when the given {@link ModelChangeNotification} originates from a REMOVE.
*
* @param notification
* the {@link ModelChangeNotification} to handle.
*/
private void handleRemove(ModelChangeNotification notification) {
final Notification rawNotification = notification.getRawNotification();
if (rawNotification.getEventType() == Notification.REMOVE
&& EReference.class.isInstance(rawNotification.getFeature())) {
cleanControlDiagnostics(EObject.class.cast(notification.getNotifier()),
EReference.class.cast(rawNotification.getFeature()),
EObject.class.cast(rawNotification.getOldValue()));
}
// TODO JF since we now have an indexed dmr, this should clean diagnostics, too, doesn't it?
validate(notification.getNotifier());
}
@Override
public void notifyAdd(Notifier notifier) {
if (notifier == context.getDomainModel()) {
validate(getAllEObjectsToValidate(context));
}
}
@Override
public void notifyRemove(Notifier notifier) {
}
}
private org.eclipse.emfforms.common.spi.validation.ValidationService validationService;
private ValidationDomainModelChangeListener domainChangeListener;
private ViewModelContext rootContext;
private EMFFormsContextTracker contextTracker;
private final Map<EMFFormsViewContext, ViewModelChangeListener> viewModelChangeListeners = new HashMap<>();
private final Queue<EObject> validationQueue = new ConcurrentLinkedSetQueue<>();
private final Set<EObject> validated = Collections.newSetFromMap(new ConcurrentHashMap<>());
private final AtomicBoolean validationRunning = new AtomicBoolean(false);
// In a typical application, these lists will usually have zero or one element. In
// any case, uniqueness is ensured by the validation algorithm and so needs not be
// enforced by the collection, so just use simple lists
private final Map<UniqueSetting, List<Diagnostic>> currentUpdates = new ConcurrentHashMap<UniqueSetting, List<Diagnostic>>();
private final Function<Object, List<Diagnostic>> diagnosticListFactory = __ -> new ArrayList<>(3);
private ComposedAdapterFactory adapterFactory;
private ReportService reportService;
private EMFFormsLocalizationService l10n;
private ThresholdDiagnostic.Factory placeholderFactory;
// Maximal number of problems to propagate up the view hierarchy, or negative for no limit
private int propagationThreshold;
private final Set<ValidationUpdateListener> validationUpdateListeners = new LinkedHashSet<>();
private ValidationProviderHelper providerHelper;
@Override
public void instantiate(ViewModelContext context) {
rootContext = context;
reportService = context.getService(ReportService.class);
l10n = context.getService(EMFFormsLocalizationService.class);
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) {
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$
}
propagationThreshold = getPropagationThreshold();
validationService = new org.eclipse.emfforms.common.internal.validation.ValidationServiceImpl();
validationService.addDiagnosticFilter(this::ignoreDiagnostic);
validationService.addObjectFilter(this::skipValidation);
validationService.registerValidationResultListener(new ValidationResultListener() {
@Override
public void onValidate(EObject eObject, Diagnostic diagnostic) {
validated.add(eObject);
}
@Override
public void afterValidate(EObject eObject, Diagnostic diagnostic) {
// nothing to do here
}
});
adapterFactory = new ComposedAdapterFactory(new AdapterFactory[] {
new ReflectiveItemProviderAdapterFactory(),
new ComposedAdapterFactory(ComposedAdapterFactory.Descriptor.Registry.INSTANCE) });
final ViewSubstitutionLabelProviderFactory substitutionLabelProviderFactory = getSubstitutionLabelProviderFactory();
SubstitutionLabelProvider substitutionLabelProvider;
if (substitutionLabelProviderFactory != null) {
substitutionLabelProvider = substitutionLabelProviderFactory
.createSubstitutionLabelProvider(adapterFactory);
} else {
substitutionLabelProvider = new ECPSubstitutionLabelProvider(adapterFactory);
}
validationService.setSubstitutionLabelProvider(substitutionLabelProvider);
providerHelper.initialize();
domainChangeListener = new ValidationDomainModelChangeListener(context);
context.registerDomainChangeListener(domainChangeListener);
addViewModelChangeListener(context);
contextTracker = new EMFFormsContextTracker(rootContext);
contextTracker.onContextInitialized(this::contextInitialised)
.onChildContextAdded(this::childContextAdded)
.onChildContextRemoved(this::childContextRemoved)
.open();
}
private boolean skipValidation(EObject eObject) {
return validated.contains(eObject);
}
private boolean ignoreDiagnostic(EObject eObject, Diagnostic diagnostic) {
return !controlMapper.hasControlsFor(eObject);
}
private void cleanControlDiagnostics(EObject parent, EReference parentReference, EObject removedEObject) {
final Set<VElement> controls = controlMapper
.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(DiagnosticHelper.getFirstInternalEObject(diagnostic.getData()))) {
diagnosticsToRemove.add(diagnostic);
}
}
final EList<Object> diagnostics = new BasicEList<Object>(vControl.getDiagnostic().getDiagnostics());
diagnostics.removeAll(diagnosticsToRemove);
final VDiagnostic vDiagnostic = VViewFactory.eINSTANCE.createDiagnostic();
vDiagnostic.getDiagnostics().addAll(diagnostics);
vControl.setDiagnostic(vDiagnostic);
}
}
@Override
public void dispose() {
contextTracker.close();
viewModelChangeListeners.forEach((ctx, l) -> ctx.unregisterViewChangeListener(l));
viewModelChangeListeners.clear();
rootContext.unregisterDomainChangeListener(domainChangeListener);
providerHelper.dispose();
adapterFactory.dispose();
}
@Override
public int getPriority() {
return 1;
}
private void addViewModelChangeListener(EMFFormsViewContext context) {
final ViewModelChangeListener listener = new ViewModelChangeListener(context);
if (viewModelChangeListeners.putIfAbsent(context, listener) == null) {
context.registerViewChangeListener(listener);
listener.start();
}
}
private void removeViewModelChangeListener(EMFFormsViewContext context) {
final ViewModelChangeListener listener = viewModelChangeListeners.remove(context);
if (listener != null) {
context.unregisterViewChangeListener(listener);
}
}
/**
* 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;
}
private Collection<EObject> getAllEObjectsToValidate(ViewModelContext context) {
return getAllEObjectsToValidate(context, controlMapper);
}
private static Collection<EObject> getAllEObjectsToValidate(ViewModelContext context,
EMFFormsSettingToControlMapper controlMapper) {
if (context.getParentContext() == null || !(controlMapper instanceof SubControlMapper)) {
// Easy: validate the whole model
return controlMapper.getEObjectsWithSettings();
}
final SubControlMapper subMapper = (SubControlMapper) controlMapper;
final Collection<EObject> result = subMapper.getEObjectsWithSettings(context.getViewModel());
// And all container objects that have settings
for (ViewModelContext parent = context.getParentContext(); parent != null; parent = parent
.getParentContext()) {
final EObject parentObject = parent.getDomainModel();
if (controlMapper.hasControlsFor(parentObject)) {
result.add(parentObject);
}
}
return result;
}
@Override
public void validate(Iterable<? extends EObject> objects) {
objects.forEach(validationQueue::add);
processValidationQueue();
}
@Override
public void validate(Collection<EObject> eObjects) {
// Delegate the opposite direction to how the default interface method does
validationQueue.addAll(eObjects);
processValidationQueue();
}
/**
* Validate the given eObject.
*
* @param eObject the eObject to validate
*/
public void validate(EObject eObject) {
/**
* We are using a queue here to allow validators to add additional eObjects
* to the current validation run. This is because we actually want a diagnostics aggregate,
* otherwise consecutive runs would replace already existing diagnostics on the UI.
* This is probably not the best way to solve this problem, but it will do for now.
*/
validationQueue.offer(eObject);
processValidationQueue();
}
private void processValidationQueue() {
if (!initialized) {
return;
}
// prohibit re-entry in recursion
if (!validationRunning.compareAndSet(false, true)) {
return;
}
EObject toValidate;
while ((toValidate = validationQueue.poll()) != null) {
validateAndCollectSettings(toValidate);
}
update();
notifyListeners();
notifyUpdateListeners();
currentUpdates.clear();
validated.clear();
validationRunning.compareAndSet(true, 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() {
// prepare Map
final Map<VElement, Set<UniqueSetting>> vElementToSettingMap = prepareVElementToSettingMap();
final Map<VElement, VDiagnostic> controlDiagnosticMap = new LinkedHashMap<VElement, VDiagnostic>();
for (final VElement control : vElementToSettingMap.keySet()) {
updateControlDiagnostics(control, vElementToSettingMap, controlDiagnosticMap);
// 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) {
continue;
}
final EObject diagnosticEobject = DiagnosticHelper.getFirstInternalEObject(diagnostic.getData());
final EStructuralFeature eStructuralFeature = DiagnosticHelper
.getEStructuralFeature(diagnostic.getData());
if (diagnosticEobject == null || eStructuralFeature == null) {
continue;
}
// TODO performance
if (!isObjectStillValid(diagnosticEobject, eStructuralFeature, control)) {
continue;
}
final UniqueSetting uniqueSetting2 = UniqueSetting.createSetting(
diagnosticEobject, eStructuralFeature);
if (!currentUpdates.containsKey(uniqueSetting2)) {
controlDiagnosticMap.get(control).getDiagnostics().add(diagnosticObject);
}
}
}
updateAndPropagate(controlDiagnosticMap);
}
private void updateControlDiagnostics(final VElement control,
final Map<VElement, Set<UniqueSetting>> vElementToSettingMap,
final Map<VElement, VDiagnostic> controlDiagnosticMap) {
if (!controlDiagnosticMap.containsKey(control)) {
controlDiagnosticMap.put(control, VViewFactory.eINSTANCE.createDiagnostic());
}
for (final UniqueSetting uniqueSetting : vElementToSettingMap.get(control)) {
final List<Diagnostic> diagnostics = currentUpdates.get(uniqueSetting);
if (!diagnostics.isEmpty()) {
controlDiagnosticMap.get(control).getDiagnostics().addAll(diagnostics);
}
}
}
private Map<VElement, Set<UniqueSetting>> prepareVElementToSettingMap() {
final Map<VElement, Set<UniqueSetting>> result = new LinkedHashMap<VElement, Set<UniqueSetting>>();
final Function<VElement, Set<UniqueSetting>> setFactory = __ -> new LinkedHashSet<>();
for (final UniqueSetting uniqueSetting : currentUpdates.keySet()) {
final Set<VElement> controls = controlMapper.getControlsFor(uniqueSetting);
if (controls == null || controls.isEmpty()) {
continue;
}
for (final VElement control : controls) {
result.computeIfAbsent(control, setFactory).add(uniqueSetting);
}
}
return result;
}
private boolean isObjectStillValid(EObject diagnosticEobject, EStructuralFeature feature, VElement element) {
final UniqueSetting setting = UniqueSetting.createSetting(diagnosticEobject, feature);
return controlMapper.hasMapping(setting, element);
}
/**
* Update the diagnostics attached to the given mapped view-model elements and propagate
* roll-ups of those diagnostics up the view model hierarchy.
*
* @param controlDiagnosticMap the mapping of source diagnostics by (leaf) control
*/
private void updateAndPropagate(Map<VElement, VDiagnostic> controlDiagnosticMap) {
// First, set every control's diagnostics
controlDiagnosticMap.forEach(VElement::setDiagnostic);
// Breadth-first climb to the top of the view model tree, layer by layer. First,
// partition the controls into layers by depth. This map is sorted by decreasing
// depth (deepest first, to start there)
final NavigableMap<Integer, Set<VElement>> depthMap = mapByDepth(controlDiagnosticMap.keySet());
reevaluateToTop(depthMap, controlDiagnosticMap);
}
/**
* Compute a partitioning of view-model {@code elements} by their depth in the model.
* This is effectively the set of layers of the tree, collecting all elements at the same
* depth into subsets (these being the layers at each depth).
*
* @param elements the elements of the view model to partition
* @return the partition into layers, in decreasing order of depth; so, ordered from deepest,
* the bottom layer, to shallowest, the root
*/
private NavigableMap<Integer, Set<VElement>> mapByDepth(Collection<? extends VElement> elements) {
final Function<Integer, Set<VElement>> factory = __ -> new LinkedHashSet<>();
final NavigableMap<Integer, Set<VElement>> result = new TreeMap<>(
Comparator.<Integer> naturalOrder().reversed());
for (final VElement next : elements) {
result.computeIfAbsent(depth(next), factory).add(next);
}
return result;
}
/**
* Compute the depth of an {@code object} in its containing tree.
*
* @param object an object
* @return its depth (zero-based at the root)
*/
private int depth(EObject object) {
int result = 0;
for (EObject container = object.eContainer(); container != null; container = container.eContainer()) {
result = result + 1;
}
return result;
}
/**
* Obtain the unique set of containers of a bunch of {@code elements} view model.
*
* @param elements elements in the view model
* @return the complete set of view-model elements covering the containers of the {@code elements}
*/
private Set<VElement> uniqueContainers(Collection<? extends VElement> elements) {
return elements.stream().map(EObject::eContainer)
.filter(VElement.class::isInstance).map(VElement.class::cast)
.collect(Collectors.toCollection(LinkedHashSet::new));
}
/**
* Perform a breadth-first climb to the top of the view model tree, layer by layer,
* recalculating roll-ups of diagnostics for every container element to aggregate
* all of the diagnostics reported in its sub-tree.
*
* @param depthMap a mapping of layers of elements in the view model by depth, in
* order from bottom (deepest layer) to top (shallowest layer)
* @param controlDiagnosticMap the mapping of source diagnostics by (leaf) control
*/
private void reevaluateToTop(NavigableMap<Integer, Set<VElement>> depthMap,
Map<VElement, VDiagnostic> controlDiagnosticMap) {
Map.Entry<Integer, Set<VElement>> layer;
for (layer = depthMap.pollFirstEntry(); layer != null; layer = depthMap.pollFirstEntry()) {
final int depth = layer.getKey();
final Set<VElement> containers = uniqueContainers(layer.getValue());
containers.forEach(container -> reevaluate(container, controlDiagnosticMap));
// These containers are elements at the next layer up, so we need to iterate over them
final int depthUp = depth - 1;
if (depthUp >= 0) {
final Set<VElement> layerUp = depthMap.get(depthUp);
if (layerUp != null) {
layerUp.addAll(containers);
} else {
depthMap.put(depthUp, containers);
}
}
}
}
/**
* Recalculate the roll-up of diagnostics fo a given element in the view model tree.
*
* @param vElement an element in the view model tree
* @param controlDiagnosticMap the mapping of source diagnostics by (leaf) control
*/
private void reevaluate(VElement vElement, Map<VElement, VDiagnostic> controlDiagnosticMap) {
final VDiagnostic vDiagnostic = VViewFactory.eINSTANCE.createDiagnostic();
if (controlDiagnosticMap.containsKey(vElement)) {
vDiagnostic.getDiagnostics().addAll(controlDiagnosticMap.get(vElement).getDiagnostics());
}
// Propagate problems from children
final DiagnosticFrequencyMap freq = getFrequencyMap(vElement);
for (final Iterator<EObject> iter = vElement.eContents().iterator(); iter.hasNext();) {
final EObject eObject = iter.next();
if (!VElement.class.isInstance(eObject)) {
continue;
}
final VElement childElement = (VElement) eObject;
// check that the child is visible and enabled
if (childElement.getDiagnostic() != null && childElement.isEffectivelyEnabled()
&& childElement.isVisible()) {
final List<?> childDiagnostics = childElement.getDiagnostic().getDiagnostics();
// Note that we cannot fill up if we didn't add something
if (freq.addAll(childDiagnostics) && freq.isFull()
&& freq.getDiscardedSeverity() > Diagnostic.WARNING) {
// No need to scan further children because it can't get more
// full than this with a greater discarded severity
break;
}
}
}
if (!freq.isEmpty()) {
freq.appendTo(vDiagnostic.getDiagnostics());
appendPlaceholder(freq, vDiagnostic.getDiagnostics());
}
vElement.setDiagnostic(vDiagnostic);
}
/**
* Perform a breadth-first climb to the top of the view model tree, layer by layer,
* recalculating roll-ups of diagnostics for every container element starting from the
* given {@code element}. It is assumed unnecessary to recompute diagnostics for
* layers deeper than the {@code element} because only the {@code element} has changed.
* But the changes to its diagnostics necessarily can affect roll-ups for all elements
* above it in the tree.
*
* @param element an element in the view model for which to calculate its roll-up of
* diagnostics and, consequently, the roll-ups for all elements above it
* @param controlDiagnosticMap the mapping of source diagnostics by (leaf) control
*/
private void reevaluateToTop(VElement element, Map<VElement, VDiagnostic> controlDiagnosticMap) {
// Breadth-first climb to the top of the view model tree, layer by layer. First,
// partition the controls into layers by depth. This map is sorted by decreasing
// depth (deepest first, to start there)
final NavigableMap<Integer, Set<VElement>> depthMap = mapByDepth(controlDiagnosticMap.keySet());
// The initial set of containers to process is just the 'element'
final Set<VElement> containers = Collections.singleton(element);
// Everything below this element doesn't need to be re-evaluated: their diagnostics
// are assumed unchanged
final int cutoff = depth(element);
depthMap.navigableKeySet().headSet(cutoff, true).clear();
depthMap.put(cutoff, containers); // Start here
// Re-evaluate this element
reevaluate(element, controlDiagnosticMap);
// And then the rest
reevaluateToTop(depthMap, controlDiagnosticMap);
}
/**
* Get a frequency map appropriate for collecting the most severe (and throttled)
* diagnostics for a view model element.
*
* @param vElement the view model element
* @return its diagnostic frequency map
*/
DiagnosticFrequencyMap getFrequencyMap(VElement vElement) {
final DiagnosticFrequencyMap result = propagationThreshold < 0 ? DiagnosticFrequencyMap.unlimited()
: DiagnosticFrequencyMap.limitedTo(propagationThreshold);
result.addDiagnosticFilter(placeholderFactory.notThresholdDiagnostic());
return result;
}
/**
* If the {@code freq}uency map is {@link DiagnosticFrequencyMap#isFull() full}, then
* append one more diagnostic covering all those that were discarded (if any).
*
* @param freq a frequency map
* @param diagnostics diagnostics being collected
*/
private void appendPlaceholder(DiagnosticFrequencyMap freq, List<? super Diagnostic> diagnostics) {
if (freq.getDiscardedSeverity() > Diagnostic.OK) {
diagnostics.add(placeholderFactory.get(freq.getDiscardedSeverity()));
}
}
private void validateAndCollectSettings(EObject eObject) {
final long start = System.nanoTime();
try {
final Diagnostic diagnostic = validationService.validate(eObject);
if (diagnostic == null) { // happens if the eObject is being filtered
return;
}
for (final EStructuralFeature feature : eObject.eClass().getEAllStructuralFeatures()) {
final UniqueSetting uniqueSetting = UniqueSetting.createSetting(eObject, feature);
currentUpdates.computeIfAbsent(uniqueSetting, diagnosticListFactory);
}
analyzeDiagnostic(diagnostic);
} finally {
if (System.nanoTime() - start > 1000L * 1000L * 1000L) {
reportService.report(new AbstractReport(MessageFormat.format(
"Validation took longer than expected for EObject {0}", eObject, //$NON-NLS-1$
IStatus.INFO)));
}
}
}
private void analyzeDiagnostic(Diagnostic diagnostic) {
if (diagnostic.getData().size() > 1) {
final InternalEObject internalEObject = DiagnosticHelper.getFirstInternalEObject(diagnostic.getData());
final EStructuralFeature eStructuralFeature = DiagnosticHelper.getEStructuralFeature(diagnostic.getData());
if (internalEObject == null || eStructuralFeature == null) {
return;
}
if (!internalEObject.eClass().getEAllStructuralFeatures().contains(eStructuralFeature)) {
reportService.report(new AbstractReport(
MessageFormat.format(
"No Setting can be created for Diagnostic {0} since the EObject's EClass does not contain the Feature.", //$NON-NLS-1$
diagnostic),
IStatus.INFO));
return;
}
final Setting setting = internalEObject.eSetting(eStructuralFeature);
final UniqueSetting uniqueSetting = UniqueSetting.createSetting(setting);
currentUpdates.computeIfAbsent(uniqueSetting, diagnosticListFactory).add(diagnostic);
} else {
for (final Diagnostic childDiagnostic : diagnostic.getChildren()) {
analyzeDiagnostic(childDiagnostic);
}
}
}
@Override
public void addValidationProvider(ValidationProvider validationProvider) {
addValidationProvider(validationProvider, true);
}
@Override
public void addValidationProvider(ValidationProvider validationProvider, boolean revalidate) {
final ValidationProvider provider = providerHelper.wrap(validationProvider);
provider.setContext(rootContext);
validationService.addValidator(provider);
if (revalidate && rootContext != null) {
validate(getAllEObjectsToValidate(rootContext));
}
}
@Override
public void removeValidationProvider(ValidationProvider validationProvider) {
removeValidationProvider(validationProvider, true);
}
@Override
public void removeValidationProvider(ValidationProvider validationProvider, boolean revalidate) {
// 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));
}
}
private final Set<ViewValidationListener> validationListeners = new LinkedHashSet<ViewValidationListener>();
private EMFFormsMappingProviderManager mappingProviderManager;
private EMFFormsSettingToControlMapper controlMapper;
private boolean initialized;
@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 = rootContext.getViewModel().getDiagnostic();
if (diagnostic != null) {
for (final Object diagObject : diagnostic.getDiagnostics()) {
result.add((Diagnostic) diagObject);
}
}
return result;
}
@Override
public void deregisterValidationListener(ViewValidationListener listener) {
validationListeners.remove(listener);
}
@Override
public void childViewModelContextAdded(ViewModelContext childContext) {
// do nothing
}
private void childContextAdded(EMFFormsViewContext parentContext, VElement parentElement,
EMFFormsViewContext childContext) {
// We are getting this from a parent content that is a view-model context, so the
// child really ought to be one, also
if (childContext instanceof ViewModelContext) {
validate(getAllEObjectsToValidate((ViewModelContext) childContext));
}
addViewModelChangeListener(childContext);
}
private void childContextRemoved(EMFFormsViewContext parentContext, VElement parentElement,
EMFFormsViewContext childContext) {
removeViewModelChangeListener(childContext);
}
private void contextInitialised(EMFFormsViewContext context) {
if (context == rootContext) {
initialized = true;
validate(getAllEObjectsToValidate(rootContext));
}
}
/**
* Returns a {@link ViewSubstitutionLabelProviderFactory}, if any is registered.
*
* @return an instance of a {@link ViewSubstitutionLabelProviderFactory}, if any is available,
* {@code null} otherwise
*/
protected ViewSubstitutionLabelProviderFactory getSubstitutionLabelProviderFactory() {
if (rootContext.hasService(ViewSubstitutionLabelProviderFactory.class)) {
return rootContext.getService(ViewSubstitutionLabelProviderFactory.class);
}
return null;
}
/**
* Get the problems propagation limit from the annotation view model context.
*
* @return the propagation limit, or {@code -1} if unlimited
*
* @precondition the {@link ViewModelContext} is already set
* @precondition the {@link ReportService} is available
* @precondition the {@link EMFFormsLocalizationService} is available
*/
private int getPropagationThreshold() {
int result = -1; // Internal code for unlimited
final Object value = rootContext.getContextValue(PROPAGATION_LIMIT_KEY);
if (value instanceof Integer) {
final int intValue = (Integer) value;
if (intValue < 0) {
warn("ValidationServiceImpl_limitNegative", value); //$NON-NLS-1$
} else {
result = intValue;
}
} else if (value == null || PROPAGATION_UNLIMITED_VALUE.equals(value)) {
return result;
} else {
warn("ValidationServiceImpl_limitUnknown", value); //$NON-NLS-1$
}
return result;
}
/**
* Issue a warning composed of the indicated localized string with positional {@code arguments}.
*
* @param messageKey key to look up in the host bundle's localizations
* @param arguments zero-indexed arguments to substitute, if any, in the string
*/
protected void warn(String messageKey, Object... arguments) {
final String report = NLS.bind(l10n.getString(ValidationServiceImpl.class, messageKey), arguments);
reportService.report(new AbstractReport(report, IStatus.WARNING));
}
/**
* @since 1.22
*/
@Override
public void registerValidationUpdateListener(ValidationUpdateListener listener) {
validationUpdateListeners.add(listener);
}
/**
* @since 1.22
*/
@Override
public void deregisterValidationUpdateListener(ValidationUpdateListener listener) {
validationUpdateListeners.remove(listener);
}
private void notifyUpdateListeners() {
if (validationUpdateListeners.isEmpty()) {
return;
}
// Build the validation state
final Collection<Diagnostic> diagnostics = Collections.unmodifiableCollection(
currentUpdates.entrySet().stream()
.map(entry -> summarize(entry.getKey(), entry.getValue()))
.collect(Collectors.toList()));
validationUpdateListeners.forEach(l -> l.validationUpdated(diagnostics));
}
private Diagnostic summarize(UniqueSetting setting, List<Diagnostic> diagnostics) {
switch (diagnostics.size()) {
case 0:
return new BasicDiagnostic(Diagnostic.OK, Activator.PLUGIN_ID, 0, Diagnostic.OK_INSTANCE.getMessage(),
new Object[] { setting.getEObject(), setting.getEStructuralFeature() });
case 1:
return diagnostics.get(0);
default:
return new BasicDiagnostic(Activator.PLUGIN_ID, 0, diagnostics, "", //$NON-NLS-1$
new Object[] { setting.getEObject(), setting.getEStructuralFeature() });
}
}
}