/*
* Copyright 2008-2013 Amazon Technologies, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at:
*
* http://aws.amazon.com/apache2.0
*
* This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
* OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and
* limitations under the License.
*/
package com.amazonaws.eclipse.core.ui.wizards;
import org.eclipse.core.databinding.observable.map.IObservableMap;
import org.eclipse.core.databinding.observable.map.WritableMap;
import org.eclipse.core.databinding.observable.value.IObservableValue;
import org.eclipse.core.databinding.validation.MultiValidator;
import org.eclipse.core.databinding.validation.ValidationStatus;
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.swt.widgets.Display;
/**
* An Eclipse MultiValidator that runs a two-phase validation process
* using pluggable InputValidator strategies. First, an optional synchronous
* validation phase is run to check for well-formed input. If this phase
* passes, an optional asynchronous phase is run to eg make a call to a
* remote service to check an existence constraint.
* <p/>
* Both phases are optional, although a TwoPhaseValidator with a no-op for
* both phases is a particularly inefficient way of doing nothing.
*/
class TwoPhaseValidator extends MultiValidator {
/**
* How long to wait before actually running async validation, to cut
* down on churn when the user is actively changing the input.
*/
private static final long ASYNC_DELAY_MILLIS = 200;
private final IObservableValue observableInput;
private final InputValidator syncValidator;
private final InputValidator asyncValidator;
/**
* A cache of values we've previously validated asynchronously; if we
* see these values again, we'll be lazy and simply report the cached
* value rather than re-running (potentially-expensive) async validation.
* <p/>
* The cache is not size-bound, since it's <i>probably</i> not an issue
* in practice, and WritableMap doesn't expose an easy way to expire
* entries from the cache.
* <p/>
* Access is protected by a lock on the TwoPhaseValidator.
*/
private final IObservableMap asyncCache;
/**
* The currently-scheduled async validation job (or null if no job
* is currently scheduled). We remember this so we can cancel the
* job if the input changes before the job has actually started
* running.
* <p/>
* Access is protected by a lock on the TwoPhaseValidator.
*/
private Job asyncValidationJob;
/**
* Constructor.
*
* @param observableInput the value to validate
* @param syncValidator the optional synchronous validator
* @param asyncValidator the optional asynchronous validator
*/
public TwoPhaseValidator(
final IObservableValue observableInput,
final InputValidator syncValidator,
final InputValidator asyncValidator
) {
this.observableInput = observableInput;
this.syncValidator = syncValidator;
this.asyncValidator = asyncValidator;
if (asyncValidator == null) {
asyncCache = null;
} else {
asyncCache = new WritableMap();
// Observe the cache; the background validation job will write
// it's status to the cache. If the user hasn't changed the input
// value since we started the async validation, we'll find the
// result in the cache when revalidating and update the UI
// as appropriate.
super.observeValidatedMap(asyncCache);
}
}
/**
* Validate the current input value.
*
* @return an OK status if the value is valid, an error otherwise
*/
@Override
protected IStatus validate() {
Object input = observableInput.getValue();
if (syncValidator != null) {
// Run synchronous validation synchronously
IStatus rval = syncValidator.validate(input);
if (!rval.isOK()) {
return rval;
}
}
if (asyncValidator == null) {
// Nothing else to do, just report OK.
return ValidationStatus.ok();
}
synchronized (this) {
// If there is a pending async validation job, cancel it; the
// value has changed, so it is no longer relevant.
if (asyncValidationJob != null) {
asyncValidationJob.cancel();
asyncValidationJob = null;
}
// Check for a cached validation of the current value; if there
// is one, we can go ahead and return that rather than kicking
// off a background validation.
IStatus cachedStatus = (IStatus) asyncCache.get(input);
if (cachedStatus != null) {
return cachedStatus;
}
// No cached validation status; schedule an async validation job
// to run if the value stays stable for a little while. In the
// meantime, report that we're still in the middle of validating.
asyncValidationJob = new AsyncValidationJob(input);
asyncValidationJob.schedule(ASYNC_DELAY_MILLIS);
return ValidationStatus.error("Validating...");
}
}
/**
* A background job that runs our asyncValidator with a given input
* and calls back to the UI thread when it's finished to report whether
* the input was valid or not
*/
private class AsyncValidationJob extends Job {
private final Object input;
/**
* Constructor input the input to validate
*/
public AsyncValidationJob(final Object input) {
super("AWS Toolkit Async Validation Job");
super.setPriority(Job.DECORATE);
this.input = input;
}
/**
* Run the async validator and call back to the UI thread with its
* result.
*/
@Override
protected IStatus run(final IProgressMonitor monitor) {
if (monitor.isCanceled()) {
return Status.CANCEL_STATUS;
}
final IStatus rval = asyncValidator.validate(input);
Display.getDefault().asyncExec(new Runnable() {
public void run() {
synchronized (TwoPhaseValidator.this) {
// If we haven't been canceled and replaced,
// deregister us so we can be GC'd.
if (asyncValidationJob == AsyncValidationJob.this) {
asyncValidationJob = null;
}
// Drop the status into the cache. The
// TwoPhaseValidator is watching the cache, and so
// validation will re-run, picking up the cached
// value this time.
asyncCache.put(input, rval);
}
}
});
return Status.OK_STATUS;
}
}
}