/*******************************************************************************
* Copyright (c) 2014 Pivotal Software, Inc.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of 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://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License 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.
*
* Contributors:
* Pivotal Software, Inc. - initial API and implementation
********************************************************************************/
package org.cloudfoundry.ide.eclipse.server.ui.internal;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import org.cloudfoundry.ide.eclipse.server.core.internal.ValidationEvents;
import org.cloudfoundry.ide.eclipse.server.ui.internal.wizards.WizardStatusHandler;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.operation.IRunnableContext;
/**
* A validation event handler around a {@link ServerValidator} that acts both as
* a listener for incoming validation requests as well as a notifier of
* validation completion.
* <p/>
* As a listener, the validation handler will get notified when there are
* changes to account values (e.g. username changed or different cloud space
* selected), or when a user has explicitly requested a server authorisation
* (e.g., a user has pressed a "Validate" UI control), and if necessary, perform
* an account validation. Validations may be both local or can be server
* authorisations, depending on the type of event received by this wrapper.
* <p/>
* As a notifier, once a validation is completed, the handler will notify
* registered listeners that validation has been performed.
* <p/>
* The handler also keeps tracks of errors based on event types. This allows
* multiple notifiers, like different UI parts or other components, that fire
* events, to log errors.
* <p/>
* IMPORTANT NOTE: errors are tracked by event type. Therefore in order to CLEAR
* an error, an {@link IStatus#OK} event with the SAME event type must be fired,
* whether by the same notifier that fired the initial error, or any other
* notifier.
* <p/>
* For example, a credentials UI part fires an event type VALUE_FILLED, with an
* {@link IStatus#ERROR}, due to a missing value like a password in a UI
* control. Once this error has been corrected (password filled), the same
* VALUE_FILLED event must be fired with an {@link IStatus#OK} in order to clear
* that error. This event need not be fired by the same UI part that generated
* the initial error, it can be fired by some other component (maybe one that
* reads a stored password value), as long as the event type REMAINS the same.
* <p/>
* Otherwise if an OK status is fired with a different event type, or example,
* PASSWORD_FILLED, the error will NOT be cleared.
* <p/>
* Therefore event types in {@link PartChangeEvent} should NOT represent an
* error condition (e.g missing values). Rather they should indicate:
* <p/>
* 1. An action that was performed, like values being entered.
* <p/>
* 2. A validation request (e.g. validate locally or validate against a server).
* <p/>
* To indicate an error status associated with an event type, use the
* {@link IStatus} value in the {@link PartChangeEvent}
*/
public class ValidationEventHandler implements IPartChangeListener {
private ServerValidator validator;
private Map<Integer, PartChangeEvent> trackedStatus = new HashMap<Integer, PartChangeEvent>();
private Set<IPartChangeListener> listeners = new LinkedHashSet<IPartChangeListener>();
private Set<WizardStatusHandler> statusHandlers = new LinkedHashSet<WizardStatusHandler>();
public ValidationEventHandler(ServerValidator validator) {
this.validator = validator;
}
public ValidationEventHandler() {
}
public synchronized void updateValidator(ServerValidator validator) {
this.validator = validator;
}
/**
*
* @param handler wizard status handler that gets notified when an event
* status is received. This is different than a validation listener, in the
* sense that the status handler gets notified only when a status is meant
* to be displayed in a wizard, regardless of the event type, whereas a
* validation listener only gets notified if a validation process completed.
*/
public synchronized void addStatusHandler(WizardStatusHandler handler) {
if (handler != null) {
statusHandlers.add(handler);
}
}
/*
*
* (non-Javadoc)
*
* @see org.cloudfoundry.ide.eclipse.server.ui.internal.IPartChangeListener#
* handleChange
* (org.cloudfoundry.ide.eclipse.server.ui.internal.PartChangeEvent)
*/
public synchronized void handleChange(PartChangeEvent event) {
if (validator == null || event == null) {
return;
}
// See if the incoming event triggers a validation. If so the validation
// will
// generate its own event
PartChangeEvent validationEvent = getValidationEvent(event);
if (validationEvent != null) {
fireValidationEvent(validationEvent);
event = validationEvent;
}
// Track the event to see if it is an error, or clears an existing error
trackEvent(event);
// Notify status handlers of the event
handleStatus(event);
}
public synchronized void validate(IRunnableContext context) {
handleChange(new PartChangeEvent(context, Status.OK_STATUS, null, ValidationEvents.SERVER_AUTHORISATION));
}
/**
*
* @param listener to be notified after a validation event has been
* completed.
*/
public synchronized void addValidationListener(IPartChangeListener listener) {
if (listener != null) {
listeners.add(listener);
}
}
public synchronized void removeValidationListener(IPartChangeListener listener) {
listeners.remove(listener);
}
public synchronized boolean isOK() {
return getNextNonOKEvent() == null;
}
/**
*
* @return the next non {@link IStatus#OK} event that is tracked, or null if
* there are no remaining non-OK events (i.e all errors have been cleared)
*/
public synchronized IStatus getNextNonOKEvent() {
PartChangeEvent event = getNextTrackedEvent();
return event != null ? event.getStatus() : null;
}
protected void fireValidationEvent(PartChangeEvent event) {
if (event == null) {
return;
}
Object source = event.getSource() != null ? event.getSource().getSource() : null;
for (IPartChangeListener listener : listeners) {
// Do not notify the same source that fired the event
if (source != listener) {
listener.handleChange(event);
}
}
}
protected PartChangeEvent getNextTrackedEvent() {
for (PartChangeEvent event : trackedStatus.values()) {
return event;
}
return null;
}
protected ValidationStatus validate(IRunnableContext context, int eventType) {
ValidationStatus status = null;
switch (eventType) {
// Any local credential changes (e.g. changes in username or password in
// the UI), should only result in credentials validation, but not cloud
// space
// validation, as the latter is only performed after an explicit server
// validation event is received
case ValidationEvents.CREDENTIALS_FILLED:
status = validator.validate(false, false, context);
break;
// Indicates that a request was made to explicitly validate credentials
// against the server
case ValidationEvents.SERVER_AUTHORISATION:
status = validator.validate(true, true, context);
break;
// Any general validation event should only be a local validation.
// Remove
// server authorisations are handled separately above.
case ValidationEvents.VALIDATION:
status = validator.validate(false, true, context);
break;
}
return status;
}
protected void handleStatus(PartChangeEvent event) {
if (event == null || event.getStatus() == null || event.getStatus().isOK()) {
PartChangeEvent errorEvent = getNextTrackedEvent();
// Be sure to check for null error event as to not overwrite the
// incoming OK
// status event.
if (errorEvent != null) {
event = errorEvent;
}
}
for (WizardStatusHandler handler : statusHandlers) {
handler.handleChange(event);
}
}
/**
* Examines the given event to determine if validation should occur. If so,
* it will generate a separate Validation event after the validation has
* been completed.
* <p/>
* Returns null if the incoming event does NOT trigger a validation
* operation.
* <p/>
* Validation can only occur if the incoming event is {@link IStatus#OK}.
* <p/>
* Otherwise, if there is an issue indicated by the incoming event status,
* that should be resolved first before validation can occur (e.g. missing
* password, invalid server URL, invalid space selection, etc..)
* @param event
* @return Validation event IFF the incoming event triggers a validation
* operation. Otherwise, return null. It does NOT return the original event.
*/
protected PartChangeEvent getValidationEvent(PartChangeEvent event) {
// Do not validate if the incoming event is NOT OK.
if (event.getStatus() == null || !event.getStatus().isOK()) {
return null;
}
int eventType = event.getType();
IRunnableContext context = event.getData() instanceof IRunnableContext ? (IRunnableContext) event.getData()
: null;
ValidationStatus validationStatus = validate(context, eventType);
PartChangeEvent validationEvent = null;
if (validationStatus instanceof IAdaptable) {
validationEvent = (PartChangeEvent) ((IAdaptable) validationStatus).getAdapter(PartChangeEvent.class);
}
else {
validationEvent = new PartChangeEvent(null, validationStatus != null ? validationStatus.getStatus() : null,
new EventSource<ValidationEventHandler>(this), ValidationEvents.VALIDATION);
}
return validationEvent;
}
protected void trackEvent(PartChangeEvent event) {
// Check the status and determine if it is an error
IStatus status = event.getStatus();
// Process the status to determine if it is an error. If so, keep track
// of it. Errors can only
// be "cleared" by the same event source that generated the error.
if (status != null) {
// Only one error per event type is logged.
if (!status.isOK()) {
trackedStatus.put(event.getType(), event);
}
else {
trackedStatus.remove(event.getType());
}
}
}
}