/* * Copyright (c) 2013 Data Harmonisation Panel * * All rights reserved. This program and the accompanying materials are made * available under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * You should have received a copy of the GNU Lesser General Public License * along with this distribution. If not, see <http://www.gnu.org/licenses/>. * * Contributors: * Data Harmonisation Panel <http://www.dhpanel.eu> */ package eu.esdihumboldt.hale.ui.util.source; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.ReentrantLock; 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.jface.text.DocumentEvent; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IDocumentListener; import org.eclipse.jface.text.source.IAnnotationModel; import org.eclipse.jface.text.source.IOverviewRuler; import org.eclipse.jface.text.source.IVerticalRuler; import org.eclipse.jface.text.source.SourceViewer; import org.eclipse.jface.util.IPropertyChangeListener; import org.eclipse.jface.util.PropertyChangeEvent; import org.eclipse.swt.widgets.Composite; import de.fhg.igd.slf4jplus.ALogger; import de.fhg.igd.slf4jplus.ALoggerFactory; import eu.esdihumboldt.hale.ui.util.jobs.ExclusiveSchedulingRule; /** * Source viewer that validates its content on document changes asynchronously * in a Job. * * @author Simon Templer */ public class ValidatingSourceViewer extends SourceViewer { private static final ALogger log = ALoggerFactory.getLogger(ValidatingSourceViewer.class); /** * Validation scheduling delay in milliseconds. */ private static final int VALIDATE_DELAY = 500; private final AtomicBoolean validationEnabled = new AtomicBoolean(true); private final ReentrantLock changeLock = new ReentrantLock(); /** * If the document has changed since validation. Protected by lock. */ private boolean changed = true; /** * If the document was valid in the last validation run. */ private boolean valid = false; private IDocumentListener documentListener; private IDocument lastDocument; private Job validateJob; /** * Name of the property holding the state if the viewer's document is valid. */ public static final String PROPERTY_VALID = "valid"; /** * Name of the property holding the state if the viewer validation is * enabled. */ public static final String PROPERTY_VALIDATION_ENABLED = "validationEnabled"; private final Set<IPropertyChangeListener> propertyChangeListeners = new CopyOnWriteArraySet<>(); private final SourceValidator validator; /** * Constructs a new validating source viewer. * * @param parent the parent of the viewer's control * @param ruler the vertical ruler used by this source viewer * @param styles the SWT style bits for the viewer's control * @param validator the source validator */ public ValidatingSourceViewer(Composite parent, IVerticalRuler ruler, int styles, SourceValidator validator) { super(parent, ruler, styles); this.validator = validator; init(); } /** * Constructs a new validating source viewer. * * @param parent the parent of the viewer's control * @param verticalRuler the vertical ruler used by this source viewer * @param overviewRuler the overview ruler * @param showAnnotationsOverview <code>true</code> if the overview ruler * should be visible, <code>false</code> otherwise * @param styles the SWT style bits for the viewer's control * @param validator the source validator */ public ValidatingSourceViewer(Composite parent, IVerticalRuler verticalRuler, IOverviewRuler overviewRuler, boolean showAnnotationsOverview, int styles, SourceValidator validator) { super(parent, verticalRuler, overviewRuler, showAnnotationsOverview, styles); this.validator = validator; init(); } /** * Validate the document content. The default implementation always returns * <code>true</code>. * * @param content the document content * @return if the content is valid */ protected final boolean validate(String content) { return validator.validate(content); } /** * Initialize the Job and listener. */ protected void init() { validateJob = new Job("Source viewer validation") { @Override public boolean shouldRun() { return validationEnabled.get(); } @Override public boolean shouldSchedule() { return validationEnabled.get(); } @Override protected IStatus run(IProgressMonitor monitor) { String content; changeLock.lock(); try { if (!changed) { return Status.OK_STATUS; } IDocument doc = getDocument(); if (doc != null) { content = doc.get(); } else { content = ""; } changed = false; } finally { changeLock.unlock(); } boolean success = false; try { // this is the potentially long running stuff success = validate(content); } catch (Exception e) { // ignore, but log log.warn("Error validating document content", e); success = false; } boolean notify = false; changeLock.lock(); try { /* * Only notify listeners if the document was not changed in * the meantime and the valid state is different than * before. */ notify = !changed && valid != success; if (notify) { // set result valid = success; } } finally { changeLock.unlock(); } if (notify) { PropertyChangeEvent event = new PropertyChangeEvent( ValidatingSourceViewer.this, PROPERTY_VALID, !success, success); notifyOnPropertyChange(event); } return Status.OK_STATUS; } }; validateJob.setUser(false); validateJob.setRule(new ExclusiveSchedulingRule(this)); documentListener = new IDocumentListener() { @Override public void documentChanged(DocumentEvent event) { scheduleValidation(); } @Override public void documentAboutToBeChanged(DocumentEvent event) { // do nothing } }; } /** * Notify listeners on a property change event. * * @param event the event */ protected void notifyOnPropertyChange(PropertyChangeEvent event) { for (IPropertyChangeListener listener : propertyChangeListeners) { try { listener.propertyChange(event); } catch (Exception e) { log.error("Error notifying listener on property change", e); } } } /** * Force new validation. */ public void forceUpdate() { scheduleValidation(); } /** * Schedule the validation job. */ protected void scheduleValidation() { changeLock.lock(); try { changed = true; } finally { changeLock.unlock(); } // schedule validation validateJob.schedule(VALIDATE_DELAY); } /** * Get the result of the last validation. * * @return the validation */ public boolean isValid() { changeLock.lock(); try { return valid; } finally { changeLock.unlock(); } } @Override public void setDocument(IDocument document) { super.setDocument(document); updateListener(document); } private void updateListener(IDocument document) { if (lastDocument != document) { if (lastDocument != null) { lastDocument.removeDocumentListener(documentListener); } if (document != null) { document.addDocumentListener(documentListener); } lastDocument = document; if (document != null) { // initial validation scheduleValidation(); } } } @Override public void setDocument(IDocument document, int visibleRegionOffset, int visibleRegionLength) { super.setDocument(document, visibleRegionOffset, visibleRegionLength); updateListener(document); } @Override public void setDocument(IDocument document, IAnnotationModel annotationModel) { super.setDocument(document, annotationModel); updateListener(document); } @Override public void setDocument(IDocument document, IAnnotationModel annotationModel, int modelRangeOffset, int modelRangeLength) { super.setDocument(document, annotationModel, modelRangeOffset, modelRangeLength); updateListener(document); } /** * Add a property change listener. It will be notified on changes to the * {@value #PROPERTY_VALID} property. * * @param listener the listener to add */ public void addPropertyChangeListener(IPropertyChangeListener listener) { propertyChangeListeners.add(listener); } /** * Remove a property change listener. * * @param listener the listener to add */ public void removePropertyChangeListener(IPropertyChangeListener listener) { propertyChangeListeners.remove(listener); } /** * @return if the validation is currently enabled */ public boolean isValidationEnabled() { return validationEnabled.get(); } /** * Enable or disable the automatic validation. * * @param enabled <code>true</code> if the validation should be enabled, * <code>false</code> if it should be disabled */ public void setValidationEnabled(boolean enabled) { boolean old = validationEnabled.getAndSet(enabled); if (old != enabled) { PropertyChangeEvent event = new PropertyChangeEvent(ValidatingSourceViewer.this, PROPERTY_VALIDATION_ENABLED, old, enabled); notifyOnPropertyChange(event); if (enabled) { // force validation forceUpdate(); } } } }