/*
* Copyright (c) 2008, SQL Power Group Inc.
*
* This file is part of SQL Power Library.
*
* SQL Power Library is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* SQL Power Library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package ca.sqlpower.validation.swingui;
import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import javax.swing.AbstractButton;
import javax.swing.Action;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JSpinner;
import javax.swing.JTable;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.table.TableModel;
import javax.swing.text.JTextComponent;
import org.apache.log4j.Logger;
import ca.sqlpower.validation.Status;
import ca.sqlpower.validation.ValidateResult;
import ca.sqlpower.validation.Validator;
/**
* This is the ValidationHandler for JComponents; supporting such
* classes as JTextComponent, JComboBox, AbstractButton.
* The FormValidationHandler keeps a List of JComponents and the validators,
* listens for changes of each JComponent and validates them, update the
* <b>StatusComponent</b> 'display' to the worst result and changes the
* background color of the problem JComponent to red or yellow.
* <br>
* -for JTextComponent, the validator needs to validate String (text in the component)
* <br>
* -for JComboBox, the validator needs to validate Object (Item in the component)
* <br>
* -for AbstractButton, the validator needs to validate boolean (status of the component)
* <br>
* -for Jtable, the validator needs to validate tableModel
* <br>
*/
public class FormValidationHandler implements ValidationHandler {
private static final Logger logger = Logger.getLogger(FormValidationHandler.class);
/** The color to use in the JComponent in the event of error */
protected final static Color COLOR_ERROR = new Color(255, 170, 170);
/** The color to use in the JComponent in the event of warnings */
protected final static Color COLOR_WARNING = Color.YELLOW;
/**
* A variable to store if the validator should ignore disabled fields
*/
private boolean skipDisabled;
/**
* a JComponent to display result
*/
private StatusComponent display;
/**
* a list of actions to be disabled if the validation fails
*/
private List<Action> actions;
private PropertyChangeSupport pcs = new PropertyChangeSupport(this);
/**
* List of combination of JComponent,validator...
*/
private List<ValidateObject> objects;
private ValidateResult worstValidationStatus;
/**
* True if this validation handler has handled at least one validation pass
* since the validation status was last reset. Mainly used
* for implementing hasUnsaveChanges() method in EditorPane.
*/
private boolean havePerformedValidation;
private class ValidateObject {
/**
* the Jcomponent, for save and set the background color
*/
private JComponent component;
/**
* the object we want to validate, could be the text in the JTextField
* or select item in the JComboBox, etc.
*/
private Object object;
/**
* the validator to do the validation
*/
private Validator validator;
private Color savedColor;
private ValidateResult result;
protected String getMessage() {
StringBuffer msg = new StringBuffer();
if ( getResult() != null ) {
msg.append("[").append(getResult().getStatus().name()).append("]");
msg.append(" ").append(getResult().getMessage());
} else {
msg.append(" unknown result");
}
return msg.toString();
}
protected ValidateObject(JComponent component, Validator validator) {
this.component = component;
this.validator = validator;
savedColor = component.getBackground();
}
/**
* calls validator to do the validation; object to be validated
* may be null, and handled by the validator
*/
protected void doValidate() {
if (skipDisabled && !component.isEnabled()) {
result = ValidateResult.createValidateResult(Status.OK, "");
} else {
result = validator.validate(object);
}
havePerformedValidation = true;
switch(result.getStatus()) {
case OK:
component.setBackground(savedColor);
break;
case WARN:
component.setBackground(COLOR_WARNING);
break;
case FAIL:
component.setBackground(COLOR_ERROR);
break;
}
}
protected Color getSavedColor() {
return savedColor;
}
protected ValidateResult getResult() {
return result;
}
protected void setObject(Object object) {
this.object = object;
}
protected JComponent getComponent() {
return component;
}
}
public FormValidationHandler(StatusComponent display) {
this(display,null,false);
}
public FormValidationHandler(StatusComponent display, List<Action> actions, boolean skipDisabled) {
this.display = display;
objects = new ArrayList<ValidateObject>();
this.actions = actions;
this.skipDisabled = skipDisabled;
}
public FormValidationHandler(StatusComponent display, List<Action> actions) {
this(display,actions,false);
}
public FormValidationHandler(StatusComponent display, boolean skipDisabled) {
this(display,null,skipDisabled);
}
public void setValidatedAction(Action action) {
actions = new ArrayList<Action>();
actions.add(action);
performFormValidation();
}
/**
* Add one Jcomponent and its validator to the List
*/
public void addValidateObject(final JComponent component, final Validator validator) {
final ValidateObject validateObject = new ValidateObject(component,validator);
objects.add(validateObject);
if (component instanceof JTextComponent) {
validateObject.setObject(((JTextComponent)component).getText());
((JTextComponent)component).getDocument()
.addDocumentListener(new DocumentListener(){
public void insertUpdate(DocumentEvent e) {
doStuff();
}
public void removeUpdate(DocumentEvent e) {
doStuff();
}
public void changedUpdate(DocumentEvent e) {
doStuff();
}
private void doStuff() {
validateObject.setObject(((JTextComponent)component).getText());
performFormValidation();
}
});
} else if ( component instanceof JComboBox ) {
validateObject.setObject(((JComboBox)component).getSelectedItem());
((JComboBox)component).addItemListener(new ItemListener(){
public void itemStateChanged(ItemEvent e) {
validateObject.setObject(((JComboBox)component).getSelectedItem());
performFormValidation();
}});
} else if ( component instanceof AbstractButton ) {
validateObject.setObject(((AbstractButton)component).isSelected());
((AbstractButton)component).addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent e) {
validateObject.setObject(((AbstractButton)component).isSelected());
performFormValidation();
}});
} else if ( component instanceof JTable ) {
JTable table = (JTable)component;
final TableModel tableModel = table.getModel();
validateObject.setObject(tableModel);
final TableModelListener tableModelListener = new TableModelListener(){
public void tableChanged(TableModelEvent arg0) {
validateObject.setObject(tableModel);
performFormValidation();
}};
tableModel.addTableModelListener(tableModelListener);
table.addPropertyChangeListener("model", new PropertyChangeListener(){
public void propertyChange(PropertyChangeEvent evt) {
TableModel old = (TableModel) evt.getOldValue();
old.removeTableModelListener(tableModelListener);
TableModel newModel = (TableModel) evt.getNewValue();
newModel.addTableModelListener(tableModelListener);
performFormValidation();
}
});
} else if (component instanceof JSpinner) {
final JSpinner jsp = (JSpinner)component;
jsp.addChangeListener(new ChangeListener(){
public void stateChanged(ChangeEvent e) {
validateObject.setObject(jsp.getValue());
performFormValidation();
}
});
jsp.addPropertyChangeListener(new PropertyChangeListener(){
public void propertyChange(PropertyChangeEvent evt) {
validateObject.setObject(jsp.getValue().toString());
performFormValidation();
}
});
} else {
throw new IllegalArgumentException("Unsupported JComponent type:"+
component.getClass());
}
performFormValidation();
}
/**
* Add a JtextComponent and JCheckBox and the validator to the List.
* Only validates the JtextComponent when JCheckBox isSelected agrees
* with the selected argument.
* @param textComponent The component you want to validate
* @param checkBox The checkbox the check for validation
* @param validator The validator that will validate. duh.
* @param selected This flag indicates if you want to validate when the checkbox
* is selected or when it isn't selected
* @param defaultValid This is a string to provide that will make the validator
* send back an OK message.
*/
public void addValidateObject(final JTextComponent textComponent,
final JCheckBox checkBox,
final Validator validator,
final boolean selected,
final String defaultValid) {
final ValidateObject validateObject = new ValidateObject(textComponent,validator);
objects.add(validateObject);
validateObject.setObject(textComponent.getText());
textComponent.getDocument().addDocumentListener(new DocumentListener() {
public void insertUpdate(DocumentEvent e) {
doStuff();
}
public void removeUpdate(DocumentEvent e) {
doStuff();
}
public void changedUpdate(DocumentEvent e) {
doStuff();
}
private void doStuff() {
if (selected == checkBox.isSelected()) {
validateObject.setObject(((JTextComponent)textComponent).getText());
} else {
validateObject.setObject(defaultValid);
}
performFormValidation();
}
});
checkBox.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
if (selected == checkBox.isSelected()) {
validateObject.setObject(((JTextComponent)textComponent).getText());
} else {
validateObject.setObject(defaultValid);
}
performFormValidation();
}
});
performFormValidation();
}
public void performFormValidation() {
ValidateResult worst = null;
for (ValidateObject o : objects) {
o.doValidate();
if ( o.getResult() == null ) {
} else if ( o.getResult().getStatus() == Status.FAIL &&
(worst == null || worst.getStatus() != Status.FAIL) ) {
worst = o.getResult();
} else if ( o.getResult().getStatus() == Status.WARN &&
( worst == null || worst.getStatus() == Status.OK) ) {
worst = o.getResult();
} else if ( worst == null ) {
worst = o.getResult();
}
}
setWorstValidationStatus(worst);
display.setResult(worst);
setValidatedActionsEnabled(getWorstValidationStatus() == null ||
getWorstValidationStatus().getStatus() != Status.FAIL);
}
/**
* Sets all of the validated {@link Action}s to either true or false.
*
* @param enabled
* true if the {@link Action}s should be enabled, false
* otherwise.
*/
public void setValidatedActionsEnabled(boolean enabled) {
if (actions != null){
for (Action a : actions) {
a.setEnabled(enabled);
}
}
}
public ValidateResult getWorstValidationStatus() {
return worstValidationStatus;
}
private void setWorstValidationStatus(ValidateResult result) {
ValidateResult oldResult = this.worstValidationStatus;
this.worstValidationStatus = result;
pcs.firePropertyChange("worstValidationStatus", oldResult, result);
}
public List<String> getFailResults() {
return getResults(Status.FAIL);
}
public List<String> getWarnResults() {
return getResults(Status.WARN);
}
private List<String> getResults(Status status) {
List <String> msg = new ArrayList<String>();
for ( ValidateObject o : objects ) {
if ( o.getResult() == null )
continue;
if ( o.getResult().getStatus() == status ) {
msg.add(o.getMessage());
}
}
return msg;
}
/**
* get the validate result of the given object, it should be one of the Jcomponent
* that added to the handler earlier.
* @param object -- one of the Jcomponent that added to the handler earlier.
* @return Validate Result
* @throws IllegalArgumentException if the object is not on the list
*/
public ValidateResult getResultOf(Object object) {
for ( ValidateObject o : objects ) {
if (object == o.getComponent() ) {
return o.getResult();
}
}
throw new IllegalArgumentException("Object:" +
(object==null?"null":object) + " not found!");
}
/**
* Stops the validator from looking at this object.
* This is nice when the item have been removed
* (eg a mungeComponent).
*
* @param com The component to remove.
*/
public void removeValidateObject(JComponent com) {
List<ValidateObject> removed = new LinkedList<ValidateObject>();
for (ValidateObject vo : objects) {
if (vo.component == com) {
removed.add(vo);
}
}
for (ValidateObject vo: removed) {
objects.remove(vo);
}
performFormValidation();
}
// listener stuff
public void addPropertyChangeListener(PropertyChangeListener listener) {
pcs.addPropertyChangeListener(listener);
}
public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) {
pcs.addPropertyChangeListener(propertyName, listener);
}
public void removePropertyChangeListener(PropertyChangeListener listener) {
pcs.removePropertyChangeListener(listener);
}
public void removePropertyChangeListener(String propertyName, PropertyChangeListener listener) {
pcs.removePropertyChangeListener(propertyName, listener);
}
public PropertyChangeSupport getPCS(){
return pcs;
}
/**
* See {@link #havePerformedValidation}.
*/
public boolean hasPerformedValidation() {
return havePerformedValidation;
}
/**
* Sets the havePerformedValidation property back to false.
*/
public void resetHasValidated() {
this.havePerformedValidation = false;
}
}