All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.fs.commons.desktop.validation.ui.ValidationGroupImpl Maven / Gradle / Ivy

There is a newer version: 0.0.9-3
Show newest version
/*
 * Copyright 2002-2016 Jalal Kiswani.
 *
 * 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://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.
 */
package com.fs.commons.desktop.validation.ui;

import java.awt.EventQueue;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;

import javax.swing.AbstractButton;
import javax.swing.ButtonModel;
import javax.swing.ComboBoxModel;
import javax.swing.JComboBox;
import javax.swing.border.Border;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.Document;
import javax.swing.text.JTextComponent;

import com.fs.commons.desktop.validation.Problem;
import com.fs.commons.desktop.validation.Problems;
import com.fs.commons.desktop.validation.Validator;
import com.fs.commons.desktop.validation.builtin.FSValidators;

/**
 * A group which holds validators that run against components. The group is
 * passed an instance of ValidationUI in its constructor. This is
 * typically something that is able to show the user that there is a problem in
 * some way, possibly enabling and disabling a dialog's OK button or similar.
 * 

* There are two main reasons validation groups exist: *

    *
  • Often a change in one component triggers a change in the validity of * another component
  • *
  • There are several levels of problem, INFO, WARNING and FATAL. The user * should see FATAL messages even if the component their currently interacting * with is offering a warning
  • *
*

* A validation group is typically associated with a single panel. For * components this library supports out-of-the-box such as * JTextFields or JComboBoxes, simply call one of the * add() methods with your component and validators. For validating * your own components or ones this class doesn't have methods for, you * implement and add * ValidationListeners. *

* The contract of how a validation group works is as follows: *

    *
  • When an appropriate change in a component occurs, its content shall be * validated
  • *
  • If either a non-fatal problem or no problem is found when validating the * component, all other validators in the group may be run as follows
  • *
      *
    • If the validator for the component the user is currently interacting with * did not produce a problem, the worst (by the definition of * Problem.isWorseThan()) found is passed to the UI as the current problem. This * may be the first problem with ProblemKind.FATAL which is encountered, based * on the order validators were added to the group.
    • *
    • If the validator for the component the user is currently interacting with * did produce a problem, then if its kind() is * ProblemKind.FATAL, the UI's setProblem() method is called with * that problem. If the problem from the component the user is currently * interacting with is ProblemKind.INFO or * ProblemKind.WARNING, then all other validators in the group are * called. If any problem of a greater severity is found, that problem will be * passed to the group's UI's setProblem() method; otherwise the problem from * the current component is used.
    • *
    • If no problem was produced by the component the user is interacting with, * and not by any other, then the UI's clearProblem() method is * invoked.
    • *
    *
*

Validator Ordering

*

* For the methods which take multiple validators or multiple enum constants * from the Validators * class, order is important. Generally it is best to pass validators in order * of how specific they are - for example, if you are validating a URL you would * likely pass * *

 * group.add(f, Validators.REQUIRE_NON_EMPTY_STRING, Validators.NO_WHITESPACE, Validators.URL_MUST_BE_VALID);
 * 
* * so that the most general check is done first (if the string is empty or not); * the most specific test, which will actually try to construct a URL object and * report any MalformedURLException should be last. *

* Note: This class is only intended to be implemented by the Simple Validation * library. If you are subclassing it, you are probably doing something wrong. * * @author Tim Boudreau */ final class ValidationGroupImpl { /** * Create a new ValidationGroup. * * @param ui * The user interface * @return A new ValidationGroup */ static ValidationGroupImpl create(final ValidationUI[] ui) { if (ui == null) { throw new NullPointerException(); } return new ValidationGroupImpl(ui); } final MulticastValidationUI ui; private int suspendCount; private final List all = new ArrayList(10); private ComponentDecorator decorator = new ComponentDecorator(); ValidationGroupImpl parent; ValidationGroupImpl() { this(new ValidationUI[0]); } ValidationGroupImpl(final ValidationUI... ui) { if (ui == null) { throw new NullPointerException("UI null"); } this.ui = new MulticastValidationUI(); for (final ValidationUI uis : ui) { this.ui.add(uis); } } /** * Add a validator of button models - typically to see if any are selected. * * @see com.fs.commons.desktop.validation.builtin.FSValidators * @param buttons * The buttons * @param validator * A validator */ public final void add(final AbstractButton[] buttons, final Validator validator) { final ButtonModel[] mdls = new ButtonModel[buttons.length]; for (int i = 0; i < mdls.length; i++) { mdls[i] = buttons[i].getModel(); } add(mdls, validator); } public void add(final ButtonModel[] buttons, final Validator validator) { assert EventQueue.isDispatchThread() : "Must be called on event thread"; class V extends ValidationListener implements ItemListener { private boolean enabled(final ButtonModel[] b) { boolean result = true; for (final ButtonModel m : b) { result = m.isEnabled(); if (!result) { break; } } return result; } @Override public void itemStateChanged(final ItemEvent e) { validate(); } @Override public boolean validate(final Problems problems) { if (!enabled(buttons)) { return true; } return validator.validate(problems, null, buttons); } } final V v = new V(); add(v); } public void add(final JComboBox box, final ValidationStrategy strategy, final Validator validator) { assert EventQueue.isDispatchThread() : "Must be called on event thread"; final Border originalBorder = box.getBorder(); class V extends ValidationListener implements ItemListener, FocusListener { @Override public void focusGained(final FocusEvent e) { } @Override public void focusLost(final FocusEvent e) { validate(); } @Override public void itemStateChanged(final ItemEvent e) { validate(); } @Override public boolean validate(final Problems problems) { if (!box.isEnabled()) { return true; } final boolean result = validator.validate(problems, nameForComponent(box), box.getModel()); if (originalBorder != null) { if (result) { // Test to avoid unncessary re-layout if (box.getBorder() != originalBorder) { box.setBorder(originalBorder); } } else { final Problem p = problems.getLeadProblem(); box.setBorder(ValidationGroupImpl.this.decorator.createProblemBorder(box, originalBorder, p.severity())); } } return result; } } final V v = new V(); add(v); switch (strategy) { case DEFAULT: case ON_CHANGE_OR_ACTION: box.addItemListener(v); break; case ON_FOCUS_LOSS: box.addFocusListener(v); break; case INPUT_VERIFIER: box.setInputVerifier(v); break; default: throw new AssertionError(); } } /** * Add a combo box to be validated with the passed validation strategy using * the passed validators * * @param box * A combo box component * @param builtIns * One or more of the enums from the Validators * class which provide standard validation of many things */ public final void add(final JComboBox box, final ValidationStrategy strategy, final Validator... builtIns) { final Validator v = FSValidators.forComboBox(true, builtIns); add(box, strategy, v); } /** * Add a combo box to be validated with ValidationStrategy.DEFAULT using the * passed validator * * @param box * A text component such as a JTextField * @param validator * A validator */ public final void add(final JComboBox box, final Validator validator) { add(box, ValidationStrategy.DEFAULT, validator); } /** * Add a combo box to be validated with ValidationStrategy.DEFAULT using the * passed validators * * @param box * A combo box component * @param builtIns * One or more of the enums from the Validators * class which provide standard validation of many things */ public final void add(final JComboBox box, final Validator... builtIns) { add(box, ValidationStrategy.DEFAULT, builtIns); } public void add(final JTextComponent field, final ValidationStrategy strategy, final Validator validator) { assert EventQueue.isDispatchThread() : "Must be called on event thread"; final Border originalBorder = field.getBorder(); class V extends ValidationListener implements DocumentListener, FocusListener, Runnable { @Override public void changedUpdate(final DocumentEvent e) { removeUpdate(e); } @Override public void focusGained(final FocusEvent e) { } @Override public void focusLost(final FocusEvent e) { validate(); } @Override public void insertUpdate(final DocumentEvent e) { removeUpdate(e); } @Override public void removeUpdate(final DocumentEvent e) { // Documents can be legally updated from another thread, // but we will not run validation outside the EDT if (!EventQueue.isDispatchThread()) { EventQueue.invokeLater(this); } else { validate(); } } @Override public void run() { validate(); } @Override public boolean validate(final Problems problems) { if (!field.isEnabled()) { return true; } final boolean result = validator.validate(problems, nameForComponent(field), field.getDocument()); if (originalBorder != null) { if (result) { // Test to avoid unncessary re-layout if (field.getBorder() != originalBorder) { field.setBorder(originalBorder); } } else { assert field != null : "Field null"; // NOi18N assert ValidationGroupImpl.this.decorator != null : "Decorator null"; // NOI18N final Problem p = problems.getLeadProblem(); assert p != null : "A validator has returned false from" + // NOI18N " validate(), but no validator has added any" + // NOI18N "problems to the problem set."; // NOI18N final Border border = ValidationGroupImpl.this.decorator.createProblemBorder(field, originalBorder, p.severity()); field.setBorder(border); } } return result; } } final V v = new V(); add(v); switch (strategy) { case DEFAULT: case ON_CHANGE_OR_ACTION: field.getDocument().addDocumentListener(v); break; case INPUT_VERIFIER: field.setInputVerifier(v); break; case ON_FOCUS_LOSS: field.addFocusListener(v); break; } } /** * Add a text control to be validated with the passed validation strategy * using the passed validators * * @param comp * A text control such as a JTextField * @param builtIns * One or more of the enums from the Validators * class which provide standard validation of many things */ public final void add(final JTextComponent comp, final ValidationStrategy strategy, final Validator... builtIns) { final Validator v = FSValidators.forDocument(true, builtIns); add(comp, strategy, v); } /** * Add a text component to be validated with ValidationStrategy.DEFAULT * using the passed validator * * @param comp * A text component such as a JTextField * @param validator * A validator */ public final void add(final JTextComponent comp, final Validator validator) { add(comp, ValidationStrategy.DEFAULT, validator); } /** * Add a text component to be validated with ValidationStrategy.DEFAULT * using the passed validators * * @param comp * A text component such as a JTextField * @param builtIns * One or more of the enums from the Validators * class which provide standard validation of many things */ public final void add(final JTextComponent comp, final Validator... builtIns) { add(comp, ValidationStrategy.DEFAULT, builtIns); } public void add(final ValidationListener listener) { if (this.parent != null) { this.parent.add(listener); } else { listener.setValidationGroup(this); } this.all.add(listener); } void addUI(final ValidationUI real) { if (!contains(real)) { this.ui.add(real); } } public void addValidationGroup(final ValidationGroupImpl group, final boolean useUI) { assert EventQueue.isDispatchThread() : "Not in event thread"; // NOI18N assert noOverlap(this.all, group.all); if (group.parent != null) { throw new IllegalStateException("Cannot add to a group that has " + // NOI18N "already been added to another group"); // NOI18N } this.all.addAll(group.all); for (final ValidationListener l : this.all) { l.setValidationGroup(this); } if (useUI) { addUI(new GroupSpecificValidationUI(group, group.ui)); } else { // Clear any existing problem - validateAll() will recreate // it if need-be group.ui.clearProblem(); } group.setParent(this); validateAll(null); } boolean contains(final ValidationUI ui) { return this.ui == ui || this.ui.contains(ui); } ComponentDecorator getComponentDecorator() { return this.decorator; } ValidationUI getUI() { return this.ui; } boolean isSuspended() { return this.suspendCount > 0 || this.parent != null && this.parent.isSuspended(); } /** * Disable validation and invoke a runnable. This method is useful in UIs * where a change in one component can trigger changes in another component, * and you do not want validation to be triggered because a component was * programmatically updated. *

* For example, say you have a dialog that lets you create a new Servlet * source file. As the user types the servlet name, web.xml entries are * updated to match, and these are also in fields in the same dialog. Since * the updated web.xml entries are being programmatically (and presumably * correctly) generated, those changes should not trigger a useless * validation run. Wrap such generation code in a Runnable and pass it to * this method when making programmatic changes to the contents of the UI. *

* The runnable is run synchronously, but no changes made to components * while the runnable is running will trigger validation. *

* When the last runnable exits, validateAll(null) will be called to run * validation against the entire newly updated UI. *

* This method is reentrant - a call to updateComponents can trigger another * call to updateComponents without triggering multiple calls to * validateAll() on each Runnable's exit. * * @param run * A runnable which makes changes to the contents of one or more * components in the UI which should not trigger validation */ public final void modifyComponents(final Runnable run) { if (this.parent != null) { this.parent.modifyComponents(run); } else { this.suspendCount++; try { run.run(); } finally { this.suspendCount--; if (!isSuspended()) { validateAll(null); } } } } private boolean noOverlap(final List all, final List all0) { final HashSet s = new HashSet(); s.addAll(all); s.addAll(all0); return s.size() == all.size() + all0.size(); } void removeUI(final ValidationUI ui) { if (contains(ui)) { this.ui.remove(ui); } } public void removeValidationGroup(final ValidationGroupImpl group) { assert EventQueue.isDispatchThread() : "Not in event thread"; // NOI18N if (group == this) { throw new IllegalArgumentException("Removing from self"); // NOI18N } this.all.removeAll(group.all); for (final ValidationListener l : group.all) { l.setValidationGroup(group); } this.ui.removeUI(group); group.setParent(null); validateAll(null); } public void setComponentDecorator(final ComponentDecorator decorator) { assert EventQueue.isDispatchThread() : "Not on event thread"; // NOI18N if (decorator == null) { throw new NullPointerException("Null decorator"); // NOI18N } this.decorator = decorator; } void setParent(final ValidationGroupImpl group) { if (this.parent != null && group != null) { // parent.setParent (group); throw new IllegalStateException("Already has parent " + this.parent); } else { this.parent = group; } } public Problem validateAll(final ValidationListener trigger) { assert EventQueue.isDispatchThread() : "Must be called on event thread"; if (this.parent != null) { return this.parent.validateAll(trigger); } else { if (isSuspended()) { return null; } final Problems p = new Problems(); for (final ValidationListener a : this.all) { if (a == trigger) { continue; } a.validate(p); if (p.hasFatal()) { break; } } if (!p.isEmpty()) { final Problem problem = p.getLeadProblem(); return problem; } return null; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy