/** * Copyright (c) 2012 Eclipse contributors 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 */ package org.eclipse.emf.edit.ui.provider; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.EventObject; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.emf.common.command.AbstractCommand; import org.eclipse.emf.common.command.Command; import org.eclipse.emf.common.command.CommandStack; import org.eclipse.emf.common.command.CommandStackListener; import org.eclipse.emf.common.notify.Adapter; 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.ui.ImageURIRegistry; import org.eclipse.emf.common.ui.MarkerHelper; import org.eclipse.emf.common.ui.viewer.ColumnViewerInformationControlToolTipSupport; import org.eclipse.emf.common.ui.viewer.IStyledLabelDecorator; import org.eclipse.emf.common.ui.viewer.IUndecoratingLabelProvider; 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.EList; import org.eclipse.emf.common.util.URI; import org.eclipse.emf.common.util.UniqueEList; import org.eclipse.emf.ecore.EClass; import org.eclipse.emf.ecore.EObject; import org.eclipse.emf.ecore.InternalEObject; import org.eclipse.emf.ecore.resource.Resource; import org.eclipse.emf.ecore.resource.ResourceSet; import org.eclipse.emf.ecore.util.Diagnostician; import org.eclipse.emf.ecore.util.EContentAdapter; import org.eclipse.emf.ecore.util.EObjectValidator; import org.eclipse.emf.ecore.util.EcoreUtil; import org.eclipse.emf.edit.domain.AdapterFactoryEditingDomain; import org.eclipse.emf.edit.domain.EditingDomain; import org.eclipse.emf.edit.domain.IEditingDomainProvider; import org.eclipse.emf.edit.provider.ComposedImage; import org.eclipse.emf.edit.ui.EMFEditUIPlugin; import org.eclipse.emf.edit.ui.action.ValidateAction; import org.eclipse.emf.edit.ui.util.EditUIMarkerHelper; import org.eclipse.emf.edit.ui.view.ExtendedPropertySheetPage; import org.eclipse.jface.action.Action; import org.eclipse.jface.dialogs.IDialogSettings; import org.eclipse.jface.preference.JFacePreferences; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.jface.resource.JFaceResources; import org.eclipse.jface.viewers.CellLabelProvider; import org.eclipse.jface.viewers.ColumnViewer; import org.eclipse.jface.viewers.IContentProvider; import org.eclipse.jface.viewers.ILabelDecorator; import org.eclipse.jface.viewers.ILabelProvider; import org.eclipse.jface.viewers.ILabelProviderListener; import org.eclipse.jface.viewers.IStructuredContentProvider; import org.eclipse.jface.viewers.ITreeContentProvider; import org.eclipse.jface.viewers.StructuredViewer; import org.eclipse.jface.viewers.StyledString; import org.eclipse.jface.viewers.StyledString.Styler; import org.eclipse.jface.viewers.ViewerCell; import org.eclipse.swt.SWT; import org.eclipse.swt.browser.LocationEvent; import org.eclipse.swt.custom.BusyIndicator; import org.eclipse.swt.custom.StyleRange; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.TextStyle; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.ui.IWorkbenchPart; /** * A {@link ILabelDecorator label decorator} for associating {@link Diagnostic diagnostic} decorations with a {@link StructuredViewer structured viewer}'s content labels. * * @since 2.9 */ public class DiagnosticDecorator extends CellLabelProvider implements ILabelDecorator { /** * Converts special characters to entities except those in {@link #enquote(String) enquoted ranges}. */ public static String escapeContent(String content) { StringBuilder result = new StringBuilder(); boolean escape = true; for (int i = 0, length = content.length(); i < length; ++i) { char character = content.charAt(i); if (escape) { switch (character) { case '\002': { escape = false; break; } case '\003': { escape = true; break; } case '&': { result.append("&"); break; } case '<': { result.append("<"); break; } default: { result.append(character); break; } } } else { result.append(character); } } return result.toString(); } /** * Marks the string such that the content will not be subsequently {@link #escapeContent(String) escaped}. */ public static String enquote(String content) { return '\002' + content + '\003'; } private static final Pattern ENQUOTED_SEGMENT_PATTERN = Pattern.compile("\002<img src='[^']*'/> (?:<i>)?([^\003]*)(?:</i>)?\003"); /** * Cleans up the escaping and HTML tags inserted by the {@link LiveValidator live validator}. * It can be safely called on text not produces by the live validator because it only transformed {@link #enquote(String) enquoted} content. */ public static String strip(String content) { Matcher matcher = ENQUOTED_SEGMENT_PATTERN.matcher(content); if (matcher.find()) { StringBuilder result = new StringBuilder(); int start = 0; do { result.append(content.substring(start, matcher.start())); String label = matcher.group(1); result.append(label.replace("<", "<").replace("&", "&")); start = matcher.end(); } while (matcher.find()); result.append(content.substring(start)); return result.toString(); } else { return content; } } /** * A content adapter for monitoring a {@link ResourceSet resource set}'s resources. */ public static abstract class DiagnosticAdapter extends EContentAdapter { /** * Inform all diagnostic adapters associated with the notifier of new diagnostic information. */ public static void update(Notifier notifier, Diagnostic diagnostic) { for (Adapter adapter : notifier.eAdapters()) { if (adapter instanceof DiagnosticAdapter) { ((DiagnosticAdapter)adapter).updateDiagnostic(diagnostic); } } } protected abstract void updateDiagnostic(Diagnostic diagnostic); /** * This will typically compute a diagnostic for the resource and call {@link #updateDiagnostic(Diagnostic)}. */ protected abstract void handleResourceDiagnostics(List<Resource> resources); @Override public void notifyChanged(Notification notification) { Object notifier = notification.getNotifier(); if (notifier instanceof Resource) { if (!notification.isTouch()) { switch (notification.getFeatureID(Resource.class)) { case Resource.RESOURCE__IS_LOADED: { Resource resource = (Resource)notifier; handleResourceDiagnostics(Collections.singletonList(resource)); break; } case Resource.RESOURCE__ERRORS: case Resource.RESOURCE__WARNINGS: { // Avoid processing the resource diagnostics during loading and unloading. // I.e., during unloading, isLoaded will become false, though that notification is sent later during the processing, // then the contents, errors, and warnings are cleared, in that order, each producing a visible notification immediately. // Similarly, during loading, isLoaded will become true, though that notification will be sent later during the processing, // then contents, errors, and warnings can be populated in any order, each producing a visible notification immediately. // So ignoring these notifications when the resource isn't loaded or is in the process of loading, // ensures that we won't try to resolve marker proxies into the resource until it's fully unloaded, // i.e., when the final isLoaded notification is dispatched, // at which time the resource diagnostics will be handled by the switch case above. Resource.Internal resource = (Resource.Internal)notifier; if (resource.isLoaded() && !resource.isLoading()) { handleResourceDiagnostics(Collections.<Resource>singletonList(resource)); } break; } } } } else { // Be careful because we're adding the adapter on a background thread. // synchronized (notifier) { super.notifyChanged(notification); } } } @Override protected void setTarget(ResourceSet target) { super.setTarget(target); handleResourceDiagnostics(new ArrayList<Resource>(target.getResources())); } @Override protected void setTarget(Resource target) { basicSetTarget(target); } @Override protected void unsetTarget(Resource target) { basicUnsetTarget(target); } } /** * A listener that interprets links as navigable references to objects in the resource set. */ public static class EditingDomainLocationListener extends ColumnViewerInformationControlToolTipSupport.PathLocationListener { protected EditingDomain editingDomain; public EditingDomainLocationListener(EditingDomain editingDomain, StructuredViewer viewer) { super(viewer); this.editingDomain = editingDomain; } @Override public void changing(LocationEvent event) { EObject eObject = null; try { URI uri = URI.createURI(event.location); if (uri.hasFragment()) { eObject = editingDomain.getResourceSet().getEObject(uri, false); } } catch (Throwable throwable) { // Ignore when we can't find the object. } if (eObject != null) { event.doit = false; if (editingDomain instanceof AdapterFactoryEditingDomain) { setSelection(((AdapterFactoryEditingDomain)editingDomain).getWrapper(eObject)); } else { setSelection(eObject); } } else { super.changing(event); } } } public static class LiveValidator { public static class LiveValidationAction extends Action { public static final String LIVE_VALIDATOR_DIALOG_SETTINGS_KEY = "liveValidator"; protected EditingDomain domain; protected IDialogSettings dialogSettings; public LiveValidationAction(EditingDomain domain, IDialogSettings dialogSettings) { this(dialogSettings); this.domain = domain; update(); } public LiveValidationAction(IDialogSettings dialogSettings) { super(EMFEditUIPlugin.INSTANCE.getString("_UI_LiveValidation_menu_item")); setDescription(EMFEditUIPlugin.INSTANCE.getString("_UI_LiveValidation_simple_description")); this.dialogSettings = dialogSettings; } public EditingDomain getEditingDomain() { return domain; } public void setEditingDomain(EditingDomain domain) { this.domain = domain; } @Override public void run() { boolean checked = isChecked(); if (dialogSettings != null) { dialogSettings.put(LIVE_VALIDATOR_DIALOG_SETTINGS_KEY, checked); } update(); LiveValidator liveValidator = LIVE_VALIDATORS.get(domain); if (liveValidator != null) { ResourceSet resourceSet = domain.getResourceSet(); EList<Resource> resources = resourceSet.getResources(); if (checked) { liveValidator.scheduledResources.addAll(resources); liveValidator.scheduleValidation(); } else { for (Adapter adapter : resourceSet.eAdapters()) { if (adapter instanceof DiagnosticDecoratorAdapter) { DiagnosticDecoratorAdapter diagnosticDecoratorAdapter = (DiagnosticDecoratorAdapter)adapter; diagnosticDecoratorAdapter.refreshResourceDiagnostics(resources); } } } } } public void update() { setEnabled(domain != null); if (dialogSettings != null) { setChecked(dialogSettings.getBoolean(LIVE_VALIDATOR_DIALOG_SETTINGS_KEY)); } } public void setActiveWorkbenchPart(IWorkbenchPart workbenchPart) { setEditingDomain(workbenchPart instanceof IEditingDomainProvider ? ((IEditingDomainProvider)workbenchPart).getEditingDomain() : null); } } private final List<DiagnosticDecorator> diagnosticDecorators = new ArrayList<DiagnosticDecorator>(); protected EditingDomain editingDomain; protected IDialogSettings dialogSettings; protected AdapterFactory adapterFactory; protected ILabelProvider labelProvider; protected Job validationJob; protected List<Resource> scheduledResources = Collections.synchronizedList(new UniqueEList<Resource>()); public LiveValidator(final EditingDomain editingDomain, IDialogSettings dialogSettings) { this.editingDomain = editingDomain; this.dialogSettings = dialogSettings; adapterFactory = editingDomain instanceof AdapterFactoryEditingDomain ? ((AdapterFactoryEditingDomain)editingDomain).getAdapterFactory() : null; labelProvider = adapterFactory == null ? null : new AdapterFactoryLabelProvider(adapterFactory); editingDomain.getCommandStack().addCommandStackListener (new CommandStackListener() { public void commandStackChanged(EventObject event) { Command mostRecentCommand = ((CommandStack)event.getSource()).getMostRecentCommand(); if (!(mostRecentCommand instanceof AbstractCommand.NonDirtying)) { scheduledResources.addAll(editingDomain.getResourceSet().getResources()); scheduleValidation(); } } }); } public void scheduleValidation(Resource resource) { scheduledResources.add(resource); scheduleValidation(); } public void scheduleValidation() { if (validationJob == null && (dialogSettings == null || dialogSettings.getBoolean(LiveValidationAction.LIVE_VALIDATOR_DIALOG_SETTINGS_KEY))) { validationJob = new Job("Validation Job") { @Override protected IStatus run(final IProgressMonitor monitor) { try { Diagnostician diagnostician = new Diagnostician() { @Override public String getObjectLabel(EObject eObject) { String text = labelProvider != null && eObject.eIsProxy() ? ((InternalEObject)eObject).eProxyURI().toString() : labelProvider.getText(eObject); if (text == null || text.length() == 0) { text = "<i>null</i>"; } else { text = escapeContent(text); } Image image = labelProvider != null ? labelProvider.getImage(eObject) : null; if (image != null) { URI imageURI = ImageURIRegistry.INSTANCE.getImageURI(image); return enquote("<img src='" + imageURI + "'/> " + text); } else { return escapeContent(text); } } @Override public boolean validate(EClass eClass, EObject eObject, DiagnosticChain diagnostics, Map<Object, Object> context) { if (monitor.isCanceled()) { throw new RuntimeException(); } monitor.worked(1); return super.validate(eClass, eObject, diagnostics, context); } }; final ResourceSet resourceSet = editingDomain.getResourceSet(); List<Resource> resources = new UniqueEList<Resource>(Arrays.asList(scheduledResources.toArray(new Resource[0]))); scheduledResources.removeAll(resources); // Count all the objects we need to validate and resolve proxies. // int count = 0; for (Resource resource : resources) { synchronized (resource) { synchronized (resourceSet) { for (Iterator<EObject> i = resource.getAllContents(); i.hasNext(); ) { ++count; EObject eObject = i.next(); for (@SuppressWarnings("unused") EObject eCrossReference : eObject.eCrossReferences()) { if (monitor.isCanceled()) { throw new RuntimeException(); } } } } } } // Also count all the objects we need to validate from resources loaded because of proxy resolution. // for (List<Resource> moreResources = Arrays.asList(scheduledResources.toArray(new Resource[0])); !moreResources.isEmpty(); moreResources = Arrays.asList(scheduledResources.toArray(new Resource[0]))) { resources.addAll(moreResources); scheduledResources.removeAll(moreResources); for (Resource resource : moreResources) { synchronized (resource) { synchronized (resourceSet) { for (Iterator<EObject> i = resource.getAllContents(); i.hasNext(); ) { ++count; EObject eObject = i.next(); for (@SuppressWarnings("unused") EObject eCrossReference : eObject.eCrossReferences()) { if (monitor.isCanceled()) { throw new RuntimeException(); } } } } } } } monitor.beginTask("", count); final BasicDiagnostic diagnostic = new BasicDiagnostic (EObjectValidator.DIAGNOSTIC_SOURCE, 0, EMFEditUIPlugin.INSTANCE.getString("_UI_DiagnosisOfNObjects_message", new String[] { "" + resources.size() }), new Object [] { resourceSet } ); Map<Object, Object> context = diagnostician.createDefaultContext(); for (int i = 0; i < resources.size(); ++i) { Resource resource = resources.get(i); monitor.setTaskName(EMFEditUIPlugin.INSTANCE.getString("_UI_Validating_message", new Object [] { resource.getURI() })); final BasicDiagnostic resourceDiagnostic = new BasicDiagnostic (EObjectValidator.DIAGNOSTIC_SOURCE, 0, EMFEditUIPlugin.INSTANCE.getString("_UI_DiagnosisOfNObjects_message", new String[] { "1" }), new Object [] { resource } ); for (EObject eObject : resource.getContents()) { diagnostician.validate(eObject, resourceDiagnostic, context); context.remove(EObjectValidator.ROOT_OBJECT); } for (Diagnostic instrinsicDiagnostic : new EditUIMarkerHelper().getInstrinciDiagnostics(resource, false)) { resourceDiagnostic.add(instrinsicDiagnostic); } diagnostic.add(resourceDiagnostic); if (monitor.isCanceled()) { throw new RuntimeException(); } monitor.worked(1); } DiagnosticAdapter.update(resourceSet, diagnostic); Display.getDefault().asyncExec (new Runnable() { public void run() { validationJob = null; if (!monitor.isCanceled() && !scheduledResources.isEmpty()) { LiveValidator.this.scheduleValidation(); } } }); return Status.OK_STATUS; } catch (RuntimeException exception) { validationJob = null; return Status.CANCEL_STATUS; } } }; validationJob.setPriority(Job.DECORATE); validationJob.schedule(500); } } /** * Cancels the validation job, if there is one. * It blocks until the job is terminated. * * @since 2.12 */ public void cancelValidation() { final Job validationJob = this.validationJob; if (validationJob != null) { validationJob.cancel(); Display display = Display.getCurrent(); BusyIndicator.showWhile(display, new Runnable() { public void run() { while (validationJob.getState() == Job.RUNNING) { try { Thread.sleep(10); } catch (InterruptedException e) { // Ignore. } } } }); } } public void register(DiagnosticDecorator diagnosticDecorator) { diagnosticDecorators.add(diagnosticDecorator); } public void deregister(DiagnosticDecorator diagnosticDecorator) { if (diagnosticDecorators.remove(diagnosticDecorator) && diagnosticDecorators.isEmpty()) { dispose(); } } public void dispose() { LIVE_VALIDATORS.remove(editingDomain); if (validationJob != null) { validationJob.cancel(); } } } /** * An extended {@link DiagnosticDecorator} that handle style strings decoration. * @since 2.10 */ public static class Styled extends DiagnosticDecorator implements IStyledLabelDecorator { /** * Creates an instance that supports {@link LiveValidator live validation} and supports * {@link DiagnosticDecorator.LiveValidator.LiveValidationAction#LIVE_VALIDATOR_DIALOG_SETTINGS_KEY enablement} * via {@link IDialogSettings dialog setting}. */ public Styled(EditingDomain editingDomain, ExtendedPropertySheetPage propertySheetPage, IDialogSettings dialogSettings) { super(editingDomain, propertySheetPage, dialogSettings); } /** * Creates an instance that supports {@link LiveValidator live validation}. */ public Styled(EditingDomain editingDomain, ExtendedPropertySheetPage propertySheetPage) { super(editingDomain, propertySheetPage); } /** * Creates an instance that supports {@link LiveValidator live validation} and supports * {@link DiagnosticDecorator.LiveValidator.LiveValidationAction#LIVE_VALIDATOR_DIALOG_SETTINGS_KEY enablement} * via {@link IDialogSettings dialog setting}. */ public Styled(EditingDomain editingDomain, StructuredViewer viewer, IDialogSettings dialogSettings) { super(editingDomain, viewer, dialogSettings); } /** * Creates an instance that supports {@link LiveValidator live validation}. */ public Styled(EditingDomain editingDomain, StructuredViewer viewer) { super(editingDomain, viewer); } /** * Creates an instance that doesn't support {@link LiveValidator live validation}. * Only decorations explicitly produced from {@link ValidateAction} or those * {@link EditUIMarkerHelper#getMarkerDiagnostics(Object, org.eclipse.core.resources.IFile, boolean) * derived from markers} are displayed. */ public Styled(ResourceSet resourceSet, ExtendedPropertySheetPage propertySheetPage) { super(resourceSet, propertySheetPage); } /** * Creates an instance that doesn't support {@link LiveValidator live validation}. * Only decorations explicitly produced from {@link ValidateAction} or those * {@link EditUIMarkerHelper#getMarkerDiagnostics(Object, org.eclipse.core.resources.IFile, boolean) * derived from markers} are displayed. */ public Styled(ResourceSet resourceSet, StructuredViewer viewer) { super(resourceSet, viewer); } public StyledString decorateStyledText(StyledString styledString, Object object) { return styledString; } } /** * A styled diagnostic decorator that will decorate the given styled string by underlying it * with a {@link SWT#UNDERLINE_ERROR} underline style colored in {@link JFacePreferences#ERROR_COLOR}. * @author mbarbero * */ public static class StyledError extends DiagnosticDecorator.Styled implements IStyledLabelDecorator { public StyledError(EditingDomain editingDomain, ExtendedPropertySheetPage propertySheetPage, IDialogSettings dialogSettings) { super(editingDomain, propertySheetPage, dialogSettings); } public StyledError(EditingDomain editingDomain, ExtendedPropertySheetPage propertySheetPage) { super(editingDomain, propertySheetPage); } public StyledError(EditingDomain editingDomain, StructuredViewer viewer, IDialogSettings dialogSettings) { super(editingDomain, viewer, dialogSettings); } public StyledError(EditingDomain editingDomain, StructuredViewer viewer) { super(editingDomain, viewer); } public StyledError(ResourceSet resourceSet, ExtendedPropertySheetPage propertySheetPage) { super(resourceSet, propertySheetPage); } public StyledError(ResourceSet resourceSet, StructuredViewer viewer) { super(resourceSet, viewer); } /** * Decorate the given {@code styledString} by underlying the whole given {@code styledString} * with a {@link SWT#UNDERLINE_ERROR} underline style colored in {@link JFacePreferences#ERROR_COLOR}. */ @Override public StyledString decorateStyledText(StyledString styledString, Object object) { if (styledString == null || object == null) { throw new NullPointerException(); } Diagnostic diagnostic = getDecorations().get(object); if (diagnostic != null && diagnostic.getSeverity() >= Diagnostic.WARNING) { StyledString result = new StyledString(); StyleRange[] styleRanges = styledString.getStyleRanges(); String string = styledString.getString(); if (styleRanges.length == 0) { result.append(string, ErrorStyler.INSTANCE); } else { int start = 0; for (StyleRange range : styleRanges) { if (start < range.start) { result.append(string.substring(start, (range.start - start)), ErrorStyler.INSTANCE); } start = range.start + range.length; result.append(string.substring(range.start, start), new ComposedStyler(range, ErrorStyler.INSTANCE)); } if (start < styledString.length()) { result.append(string.substring(start, string.length()), ErrorStyler.INSTANCE); } } return result; } return styledString; } /** * A styler underlying the given text with a {@link SWT#UNDERLINE_ERROR} underline style colored * in {@link JFacePreferences#ERROR_COLOR}. * @since 2.10 */ public static final class ErrorStyler extends Styler { public static final Styler INSTANCE = new ErrorStyler(); @Override public void applyStyles(TextStyle textStyle) { textStyle.underline = true; textStyle.underlineStyle = SWT.UNDERLINE_ERROR; textStyle.underlineColor = JFaceResources.getColorRegistry().get(JFacePreferences.ERROR_COLOR); } } /** * An extended styler applying a wrapped base style first, and successively apply other * stylers to the given TextStyle, overriding any previous style. * * @since 2.10 */ public static class ComposedStyler extends Styler { protected TextStyle baseStyle; protected Styler[] stylers; public ComposedStyler(TextStyle baseStyle, Styler... stylers) { this.baseStyle = baseStyle; this.stylers = stylers; } @Override public void applyStyles(TextStyle textStyle) { textStyle.font = baseStyle.font; textStyle.metrics = baseStyle.metrics; textStyle.rise = baseStyle.rise; textStyle.background = baseStyle.background; textStyle.foreground = baseStyle.foreground; textStyle.borderColor = baseStyle.borderColor; textStyle.borderStyle = baseStyle.borderStyle; textStyle.strikeout = baseStyle.strikeout; textStyle.strikeoutColor = baseStyle.strikeoutColor; textStyle.underline = baseStyle.underline; textStyle.underlineStyle = baseStyle.underlineStyle; textStyle.underlineColor = baseStyle.underlineColor; textStyle.data = baseStyle.data; for (Styler styler : stylers) { styler.applyStyles(textStyle); } } } } public class DiagnosticDecoratorAdapter extends DiagnosticAdapter { protected class Dispatcher implements Runnable { protected Display display = Display.getDefault(); protected List<Diagnostic> diagnostics; protected int expectedSize; private boolean isDispatching; public void dispatch(Diagnostic diagnostic) { List<Diagnostic> currentDiagnostics = null; synchronized (this) { if (diagnostics == null) { diagnostics = new ArrayList<Diagnostic>(); } // Merge in the new diagnostics with the ones we have cached for dispatching. // updateDiagnotics(diagnostics, diagnostic); // If we didn't delay dispatching, then prepare to dispatch this to the decorator. // if (!dispatch()) { currentDiagnostics = diagnostics; diagnostics = null; } } // If we have are dispatching the diagnostics immediately... // if (currentDiagnostics != null) { dispatch(currentDiagnostics); } } /** * Returns <code>true</code> when a delayed dispatch has been scheduled. */ protected boolean dispatch() { // If we're not in the display thread... // if (Display.getCurrent() != display) { // Set up the expected size and do a delayed asynchronous call. // expectedSize = diagnostics.size(); display.asyncExec (new Runnable() { public void run() { display.timerExec(1000, Dispatcher.this); } }); return true; } else { return false; } } /** * Dispatches the diagnostics to the decorator. */ protected void dispatch(List<Diagnostic> diagnostics) { BasicDiagnostic resourceSetDiagnostic = new BasicDiagnostic (EObjectValidator.DIAGNOSTIC_SOURCE, 0, EMFEditUIPlugin.INSTANCE.getString("_UI_DiagnosisOfNObjects_message", new String[] { "" + diagnostics.size() }), new Object [] { resourceSet } ); for (Diagnostic diagnostic : diagnostics) { resourceSetDiagnostic.add(diagnostic); } if (!isDispatching) { try { isDispatching = true; handleDiagnostic(resourceSetDiagnostic); } finally { isDispatching = false; } } } public void run() { List<Diagnostic> currentDiagnostics; synchronized (this) { // If the diagnostics have already been dispatched, don't do anything else. // if (diagnostics == null) { return; } // If we've added more diagnostics since we started a delayed dispatch, delay a little longer. // if (diagnostics.size() != expectedSize) { expectedSize = diagnostics.size(); display.asyncExec (new Runnable() { public void run() { display.timerExec(1000, Dispatcher.this); } }); return; } currentDiagnostics = diagnostics; diagnostics = null; } // Dispatch the diagnostics immediately. // dispatch(currentDiagnostics); } } protected Dispatcher dispatcher; protected Dispatcher getDispatcher() { if (dispatcher == null) { dispatcher = new Dispatcher(); } return dispatcher; } @Override public void updateDiagnostic(Diagnostic diagnostic) { getDispatcher().dispatch(diagnostic); } @Override protected void handleResourceDiagnostics(List<Resource> resources) { final BasicDiagnostic diagnostic = new BasicDiagnostic (EObjectValidator.DIAGNOSTIC_SOURCE, 0, EMFEditUIPlugin.INSTANCE.getString("_UI_DiagnosisOfNObjects_message", new String[] { "" + resources.size() }), new Object [] { resourceSet } ); LiveValidator liveValidator = getLiveValidator(); if (liveValidator != null) { for (Resource resource : resources) { diagnostic.add(markerHelper.getMarkerDiagnostics(resource, null, false)); liveValidator.scheduleValidation(resource); } } updateDiagnostic(diagnostic); } protected void refreshResourceDiagnostics(List<Resource> resources) { final BasicDiagnostic diagnostic = new BasicDiagnostic (EObjectValidator.DIAGNOSTIC_SOURCE, 0, EMFEditUIPlugin.INSTANCE.getString("_UI_DiagnosisOfNObjects_message", new String[] { "" + resources.size() }), new Object [] { resourceSet } ); for (Resource resource : resources) { diagnostic.add(markerHelper.getMarkerDiagnostics(resource, null, false)); liveValidator.scheduleValidation(resource); } diagnostics.clear(); updateDiagnostic(diagnostic); } } private static final Map<EditingDomain, LiveValidator> LIVE_VALIDATORS = new HashMap<EditingDomain, LiveValidator>(); protected DiagnosticAdapter diagnosticAdapter; protected EditingDomain editingDomain; protected LiveValidator liveValidator; protected ResourceSet resourceSet; protected StructuredViewer viewer; protected ExtendedPropertySheetPage propertySheetPage; protected IDialogSettings dialogSettings; protected Map<Object, BasicDiagnostic> decorations = new HashMap<Object, BasicDiagnostic>(); protected MarkerHelper markerHelper = new EditUIMarkerHelper(); protected Object input; protected IContentProvider contentProvider; protected List<Diagnostic> diagnostics = new ArrayList<Diagnostic>(); /** * Creates an instance that doesn't support {@link LiveValidator live validation}. * Only decorations explicitly produced from {@link ValidateAction} or those {@link EditUIMarkerHelper#getMarkerDiagnostics(Object, org.eclipse.core.resources.IFile, boolean) derived from markers} are displayed. */ public DiagnosticDecorator(ResourceSet resourceSet, StructuredViewer viewer) { this.viewer = viewer; this.resourceSet = resourceSet; diagnosticAdapter = new DiagnosticDecoratorAdapter(); resourceSet.eAdapters().add(diagnosticAdapter); input = viewer.getInput(); contentProvider = viewer.getContentProvider(); } /** * Creates an instance that supports {@link LiveValidator live validation}. */ public DiagnosticDecorator(EditingDomain editingDomain, StructuredViewer viewer) { this(editingDomain, viewer, null); } /** * Creates an instance that supports {@link LiveValidator live validation} and supports {@link DiagnosticDecorator.LiveValidator.LiveValidationAction#LIVE_VALIDATOR_DIALOG_SETTINGS_KEY enablement} via {@link IDialogSettings dialog setting}. */ public DiagnosticDecorator(EditingDomain editingDomain, StructuredViewer viewer, IDialogSettings dialogSettings) { this.editingDomain = editingDomain; this.resourceSet = editingDomain.getResourceSet(); this.viewer = viewer; this.dialogSettings = dialogSettings; diagnosticAdapter = new DiagnosticDecoratorAdapter(); resourceSet.eAdapters().add(diagnosticAdapter); input = viewer.getInput(); contentProvider = viewer.getContentProvider(); } /** * Creates an instance that doesn't support {@link LiveValidator live validation}. * Only decorations explicitly produced from {@link ValidateAction} or those {@link EditUIMarkerHelper#getMarkerDiagnostics(Object, org.eclipse.core.resources.IFile, boolean) derived from markers} are displayed. */ public DiagnosticDecorator(ResourceSet resourceSet, ExtendedPropertySheetPage propertySheetPage) { this.resourceSet = resourceSet; this.propertySheetPage = propertySheetPage; diagnosticAdapter = new DiagnosticDecoratorAdapter(); resourceSet.eAdapters().add(diagnosticAdapter); input = propertySheetPage.getInput(); } /** * Creates an instance that supports {@link LiveValidator live validation}. */ public DiagnosticDecorator(EditingDomain editingDomain, ExtendedPropertySheetPage propertySheetPage) { this(editingDomain, propertySheetPage, null); } /** * Creates an instance that supports {@link LiveValidator live validation} and supports {@link DiagnosticDecorator.LiveValidator.LiveValidationAction#LIVE_VALIDATOR_DIALOG_SETTINGS_KEY enablement} via {@link IDialogSettings dialog setting}. */ public DiagnosticDecorator(EditingDomain editingDomain, ExtendedPropertySheetPage propertySheetPage, IDialogSettings dialogSettings) { this.editingDomain = editingDomain; this.resourceSet = editingDomain.getResourceSet(); this.propertySheetPage = propertySheetPage; this.dialogSettings = dialogSettings; diagnosticAdapter = new DiagnosticDecoratorAdapter(); resourceSet.eAdapters().add(diagnosticAdapter); input = propertySheetPage.getInput(); } /** * {@link LiveValidator#cancelValidation() Cancel}s the live validator's job, if there is one. * * @since 2.12 */ public static void cancel(EditingDomain editingDomain) { LiveValidator liveValidator = LIVE_VALIDATORS.get(editingDomain); if (liveValidator != null) { liveValidator.cancelValidation(); } } protected LiveValidator getLiveValidator() { if (liveValidator == null && editingDomain != null) { liveValidator = LIVE_VALIDATORS.get(editingDomain); if (liveValidator == null) { liveValidator = new LiveValidator(editingDomain, dialogSettings); LIVE_VALIDATORS.put(editingDomain, liveValidator); } liveValidator.register(this); } return liveValidator; } public String decorateText(String text, Object object) { return text; } public Image decorateImage(Image image, Object object) { Diagnostic diagnostic = getDecorations().get(object); if (diagnostic != null && diagnostic.getSeverity() >= Diagnostic.WARNING) { return decorate(image, diagnostic); } else { return image; } } public Image decorate(Image image, Diagnostic diagnostic) { if (image == null) { return ExtendedImageRegistry.INSTANCE.getImage(EMFEditUIPlugin.INSTANCE.getImage(diagnostic.getSeverity() == Diagnostic.WARNING ? "full/ovr16/warning_ovr.gif" : "full/ovr16/error_ovr.gif")); } else { List<Object> images = new ArrayList<Object>(2); images.add(image); images.add(EMFEditUIPlugin.INSTANCE.getImage(diagnostic.getSeverity() == Diagnostic.WARNING ? "full/ovr16/warning_ovr.gif" : "full/ovr16/error_ovr.gif")); ComposedImage composedImage = new DecoratedComposedImage(images); return ExtendedImageRegistry.INSTANCE.getImage(composedImage); } } private static final class DecoratedComposedImage extends ComposedImage { private DecoratedComposedImage(Collection<?> images) { super(images); } @Override public List<Point> getDrawPoints(Size size) { List<Point> result = new ArrayList<Point>(); result.add(new Point()); Point overlay = new Point(); overlay.y = 7; result.add(overlay); return result; } } public Map<Object, ? extends Diagnostic> getDecorations() { // Check that the current decorations are up-to-date with respect to the viewer input and content provider. // if (propertySheetPage != null) { List<?> input = propertySheetPage.getInput(); if (!input.equals(this.input)) { redecorate(); } this.input = input; } else { Object input = viewer.getInput(); IContentProvider contentProvider = viewer.getContentProvider(); if (input != null && !input.equals(this.input) || contentProvider != null && !contentProvider.equals(this.contentProvider)) { redecorate(); } this.input = input; this.contentProvider = contentProvider; } return decorations; } protected void updateDiagnotics(List<Diagnostic> diagnostics, Diagnostic diagnostic) { String markerSource = markerHelper.getDiagnosticSource(); String source = diagnostic.getSource(); if (markerSource.equals(source)) { // Diagnostics produced from markers are expected to have a root diagnostic with the resource as the data, // so we clean up old diagnostics that have the same source and for that resource. // for (Iterator<Diagnostic> i = diagnostics.iterator(); i.hasNext(); ) { Diagnostic oldDiagnostic = i.next(); if (markerSource.equals(oldDiagnostic.getSource()) && diagnostic.getData().equals(oldDiagnostic.getData())) { i.remove(); } } diagnostics.add(diagnostic); } else { // If the root diagnostic is for a resource set, we expect it to hold per-resource children. // Object object = diagnostic.getData().get(0); if (object instanceof ResourceSet) { for (Diagnostic child : diagnostic.getChildren()) { updateDiagnotics(diagnostics, child); } } else { // Generally these will be per resource diagnostics. // We clean up old diagnostics with a marker source or the same source and with the same data. // for (Iterator<Diagnostic> i = diagnostics.iterator(); i.hasNext(); ) { Diagnostic oldDiagnostic = i.next(); String oldSource = oldDiagnostic.getSource(); if ((source.equals(oldSource) || markerSource.equals(oldSource)) && diagnostic.getData().equals(oldDiagnostic.getData())) { i.remove(); } } diagnostics.add(diagnostic); } } } protected void handleDiagnostic(Diagnostic rootDiagnostic) { // Update the decorative diagnostics. // updateDiagnotics(diagnostics, rootDiagnostic); // Redecorate with the current diagnostics. // redecorate(); } protected void redecorate() { // If the viewer has input and is ready to receive decorations... // if (propertySheetPage != null ? propertySheetPage.getInput() != null : viewer.getInput() != null && viewer.getContentProvider() != null) { // Build up the map for root decorations. // Map<Object, BasicDiagnostic> objects = new HashMap<Object, BasicDiagnostic>(); for (Diagnostic diagnostic : diagnostics) { for (Diagnostic child : diagnostic.getChildren()) { List<?> data = child.getData(); if (!data.isEmpty()) { decorate(objects, data.get(0), child, null); } } } // Keep track of the old decorations and start building fresh new ones. // Map<Object, BasicDiagnostic> oldDecorations = decorations; decorations = new HashMap<Object, BasicDiagnostic>(); // Decorate based on the root decorations. // decorate(objects); if (propertySheetPage != null) { // If this is for the property sheet and there are decorations that need cleaning or there are new decorations to show, refresh the view. // if (!decorations.isEmpty() || !oldDecorations.isEmpty()) { final Control control = propertySheetPage.getControl(); if (!control.isDisposed()) { control.getDisplay().asyncExec (new Runnable() { public void run() { if (!control.isDisposed()) { propertySheetPage.refreshLabels(); } } }); } } } else { // Compute the objects that need to be refreshed. // final Set<Object> objectsToRefresh = new HashSet<Object>(); for (Map.Entry<Object, BasicDiagnostic> entry : decorations.entrySet()) { Object decoratedObject = entry.getKey(); BasicDiagnostic oldDiagnostic = oldDecorations.get(decoratedObject); if (oldDiagnostic == null || entry.getValue().getSeverity() != oldDiagnostic.getSeverity()) { // Each object that's newly decorated or has a different decoration needs to be refreshed. // objectsToRefresh.add(decoratedObject); } // Forget about this old decoration; either the object is already added for refreshing or it doesn't need refreshing because the severity is the same. // if (oldDiagnostic != null) { oldDecorations.remove(decoratedObject); } } // Also refresh all the object with old decorations that no longer have decorations. // objectsToRefresh.addAll(oldDecorations.keySet()); if (!objectsToRefresh.isEmpty()) { // Be careful to refresh asynchronously if the viewer is busy. // if (!(viewer instanceof ColumnViewer) || !((ColumnViewer)viewer).isBusy()) { viewer.update(objectsToRefresh.toArray(), null); } else { final Control control = viewer.getControl(); if (!control.isDisposed()) { control.getDisplay().asyncExec (new Runnable() { public void run() { if (!control.isDisposed()) { viewer.update(objectsToRefresh.toArray(), null); } } }); } } } } } } protected BasicDiagnostic decorate(Map<Object, BasicDiagnostic> decorations, Object object, Diagnostic diagnostic, List<Integer> path) { BasicDiagnostic oldDiagnostic = decorations.get(object); if (diagnostic != null) { if (oldDiagnostic == null) { oldDiagnostic = new BasicDiagnostic(null, 0, null, path == null ? new Object [] { object } : new Object [] { object, path.toArray(new Integer[path.size()]) }); decorations.put(object, oldDiagnostic); } String message = diagnostic.getMessage(); if (message != null) { List<?> data = diagnostic.getData(); for (Diagnostic childDiagnostic : oldDiagnostic.getChildren()) { if (childDiagnostic.getMessage().equals(message) && data.equals(childDiagnostic.getData())) { return oldDiagnostic; } } } oldDiagnostic.add(diagnostic); } return oldDiagnostic; } protected void decorate(Map<Object, BasicDiagnostic> objects) { if (propertySheetPage != null) { for (Object object : propertySheetPage.getInput()) { decorate(decorations, object, objects.get(object), null); } } else { Object input = viewer.getInput(); IContentProvider contentProvider = viewer.getContentProvider(); if (contentProvider instanceof IStructuredContentProvider) { ITreeContentProvider treeContentProvider = contentProvider instanceof ITreeContentProvider ? (ITreeContentProvider)contentProvider : null; int index = 0; List<Integer> path = new ArrayList<Integer>(); path.add(-1); for (Object object : ((IStructuredContentProvider)contentProvider).getElements(input)) { path.set(0, index++); BasicDiagnostic childDiagnostic = getDiagnostic(objects, object, path); Diagnostic objectDiagnostic = decorate(decorations, object, childDiagnostic, path); if (treeContentProvider != null) { Set<Object> visited = new HashSet<Object>(); objectDiagnostic = decorate(objects, treeContentProvider, visited, object, path); } decorate(decorations, input, objectDiagnostic, null); } } } } /** * @since 2.10 */ protected BasicDiagnostic getDiagnostic(Map<Object, BasicDiagnostic> objects, Object object, List<Integer> path) { BasicDiagnostic childDiagnostic = objects.get(object); if (childDiagnostic == null) { // Check if the unwrapped child is different and has diagnostics... // Object unwrappedChild = AdapterFactoryEditingDomain.unwrap(object); if (object != unwrappedChild) { childDiagnostic = objects.get(unwrappedChild); if (childDiagnostic != null) { // Create a new diagnostic wrapper for it. // childDiagnostic = new BasicDiagnostic (childDiagnostic.getSource(), childDiagnostic.getCode(), childDiagnostic.getChildren(), childDiagnostic.getMessage(), new Object[] { object, path.toArray(new Integer[path.size()]) }); objects.put(object, childDiagnostic); } } } return childDiagnostic; } protected BasicDiagnostic decorate(Map<Object, BasicDiagnostic> objects, ITreeContentProvider treeContentProvider, Set<Object> visited, Object object, List<Integer> path) { BasicDiagnostic result = decorations.get(object); if (visited.add(object)) { int index = 0; int last = path.size(); for (Object child : treeContentProvider.getChildren(object)) { path.add(index++); BasicDiagnostic childDiagnostic = getDiagnostic(objects, child, path); BasicDiagnostic childResult = decorate(decorations, child, childDiagnostic, path); childResult = decorate(objects, treeContentProvider, visited, child, path); path.remove(last); result = decorate(decorations, object, childResult, path); } } return result; } @Override public String getToolTipText(Object object) { BasicDiagnostic diagnostic = decorations.get(object); if (diagnostic != null) { ILabelProvider labelProvider = (ILabelProvider)viewer.getLabelProvider(); if (labelProvider instanceof IUndecoratingLabelProvider) { final IUndecoratingLabelProvider undecoratingLabelProvider = (IUndecoratingLabelProvider)labelProvider; labelProvider = new ILabelProvider() { public void removeListener(ILabelProviderListener listener) { undecoratingLabelProvider.removeListener(listener); } public boolean isLabelProperty(Object element, String property) { return undecoratingLabelProvider.isLabelProperty(element, property); } public void dispose() { undecoratingLabelProvider.dispose(); } public void addListener(ILabelProviderListener listener) { undecoratingLabelProvider.addListener(listener); } public String getText(Object element) { return undecoratingLabelProvider.getUndecoratedText(element); } public Image getImage(Object element) { return undecoratingLabelProvider.getUndecoratedImage(element); } }; } StringBuilder result = new StringBuilder(); buildToolTipText(result, labelProvider, diagnostic, object); return result.length() == 0 ? null : result.toString(); } else { return null; } } protected void buildToolTipText(StringBuilder result, ILabelProvider labelProvider, Diagnostic diagnostic, Object object) { List<Diagnostic> children = diagnostic.getChildren(); Diagnostic child = children.get(0); int index = 0; if (child.getData().contains(object)) { ++index; for (Diagnostic grandChild : child.getChildren()) { buildToolTipMessage(result, labelProvider, object, grandChild, 0); } } StringBuilder moreResults = new StringBuilder(); for (int size = children.size(); index < size; ++index) { child = children.get(index); buildMoreToolTipText(moreResults, labelProvider, child); } if (moreResults.length() != 0) { result.append("<h1>Problems on Children</h1>\n"); result.append(moreResults); } } protected void buildToolTipMessage(StringBuilder result, ILabelProvider labelProvider, Object object, Diagnostic diagnostic, int indentation) { String message = diagnostic.getMessage(); ImageDescriptor imageDescriptor = ExtendedImageRegistry.INSTANCE.getImageDescriptor (EMFEditUIPlugin.INSTANCE.getImage(diagnostic.getSeverity() == Diagnostic.WARNING ? "full/ovr16/warning_ovr.gif" : "full/ovr16/error_ovr.gif")); URI severityURI = ImageURIRegistry.INSTANCE.getImageURI(imageDescriptor); result.append("<div>"); for (int i = 0; i < indentation; ++i) { result.append("   "); } result.append("<img src='"); result.append(severityURI); result.append("'/> "); result.append(escapeContent(message)); result.append("</div>\n"); List<?> excludedObjects = object instanceof EObject ? ((EObject)object).eClass().getEAllStructuralFeatures() : Collections.emptyList(); for (Object data : diagnostic.getData()) { if (data != object && !excludedObjects.contains(data) && data instanceof EObject) { EObject eObject = (EObject)data; if (eObject.eResource() != null && eObject.eResource().getResourceSet() == resourceSet) { result.append("<div>"); for (int i = 0; i <= indentation; ++i) { result.append("   "); } String text = escapeContent(labelProvider.getText(data)); Image image = labelProvider.getImage(data); result.append("<img src='"); result.append(ImageURIRegistry.INSTANCE.getImageURI(image)); result.append("'/> <a href=\""); result.append(EcoreUtil.getURI((EObject)data)); result.append("\">"); result.append(text); result.append("</a></div>\n"); } } } for (Diagnostic child : diagnostic.getChildren()) { buildToolTipMessage(result, labelProvider, child.getData().get(0), child, indentation + 1); } } protected void buildMoreToolTipText(StringBuilder result, ILabelProvider labelProvider, Diagnostic diagnostic) { List<?> data = diagnostic.getData(); Object object = data.get(0); List<Diagnostic> children = diagnostic.getChildren(); Diagnostic child = children.get(0); int index = 0; if (child.getData().contains(object)) { ++index; if (data.size() > 1) { result.append("<div>"); Image image = labelProvider.getImage(object); if (image != null) { URI imageURI = ImageURIRegistry.INSTANCE.getImageURI(image); result.append("<img src='"); result.append(imageURI); result.append("'/> "); } result.append("<a href='"); result.append("path:"); Integer[] path = (Integer[])data.get(1); for (Integer segment : path) { result.append('/'); result.append(segment); } String text = escapeContent(labelProvider.getText(object)); if (text == null || text.length() == 0) { text = "<i>null</i>"; } result.append("'>"); result.append(text); result.append("</a></div>\n"); for (Diagnostic grandChild : child.getChildren()) { buildToolTipMessage(result, labelProvider, object, grandChild, 1); } } } for (int size = children.size(); index < size; ++index) { child = children.get(index); buildMoreToolTipText(result, labelProvider, child); } } @Override public void update(ViewerCell cell) { // Do nothing. } @Override public void dispose() { if (liveValidator != null) { liveValidator.deregister(this); } if (diagnosticAdapter != null) { resourceSet.eAdapters().remove(diagnosticAdapter); } super.dispose(); } }