Friday, November 12, 2010

Simple Swing Validation using InputVerifier

There are many frameworks available out there for swing validation, like Simple Validation and JGoodies Validation and both being open source. But it looked to me, using these frameworks would require me to change a lot of my existing GUI code. And being a NetBeans user this looked tedious to me(as most of the GUI code is generated by the IDE). Then I came across Michael Urban's post - "Building a Swing Validation Package with InputVerifier". I have just extended his implementation to suit my needs, like instead of showing a popup for the error message, I wanted an icon painted right in the textfield like in netbeans.


P.S. The GTK Look and Feel does'nt allow a custom background in the JTextField. If you run this application under a different look and feel you could see the background color changing as well.

We will be implementing the verify method in the InputVerifier class. This method takes  JComponent as a parameter, and returns true if the component passed the validation, false otherwise.

Now lets first create a class which extends the InputVerifier. In this class we have an abstract method ErrorDefinition which takes a JComponent, to be validates as a parameter and return an object of the Error class.This is the method where we define our custom error and return an Error object with the error type being any one of the four case Error.NO_ERROR Error.INFO, Error.WARNING or Error.ERROR.

The actual work in this class in done in the verify method. The verify method is called by java when the user tries to remove the focus from the component. if the verify method returns true then you'll be able remove the focus from the textfield and if this method returns false you'll not be allowed to tab out or remove focus from the textfield. We return false only if error type is Error.Error otherwise we paint an warning icon or info icon in the textfield but allow the user to move away from the textfield.

package org.gpl.validation;

import java.awt.Color;
import javax.swing.InputVerifier;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.border.Border;
import org.gpl.border.IconBorder;

/**
 * This class handles all the details of validation and
 * decorating the component
 * @author Naveed Quadri
 */
public abstract class ErrorProvider extends InputVerifier {

    private Border originalBorder;
    private Color originalBackgroundColor;
    private String originalTooltipText;
    private Object parent;

    /**
     *
     * @param c The JComponent to be validated.
     */
    public ErrorProvider(JComponent c) {
        originalBorder = c.getBorder();
        originalBackgroundColor = c.getBackground();
        originalTooltipText = c.getToolTipText();
        
    }

    /**
     *
     * @param parent A JFrame that implements the ValidationStatus interface.
     * @param c The JComponent to be validated.
     */
    public ErrorProvider(JFrame parent, JComponent c) {
        this(c);
        this.parent = parent;
    }

    /**
     * 
     * @param parent A JDialog that implements the ValidationStatus interface.
     * @param c The JComponent to be validated.
     */
    public ErrorProvider(JDialog parent, JComponent c) {
        this(c);
        this.parent = parent;
    }

    /**
     * Define your custom Error in this method and return an Error Object.
     * @param c The JComponent to be validated.
     * @return Error
     * @see Error
     */
    protected abstract Error ErrorDefinition(JComponent c);

    /**
     * This method is called by Java when a component needs to be validated.
     * @param c The JCOmponent being validated
     * @return
     */
    @Override
    public boolean verify(JComponent c) {
        Error error = ErrorDefinition(c);
        if (error.getErrorType() == Error.NO_ERROR) {
            //revert back all changes made to the component
            c.setBackground(originalBackgroundColor);
            c.setBorder(originalBorder);
            c.setToolTipText(originalTooltipText);
        } else {
            c.setBorder(new IconBorder(error.getImage(), originalBorder));
            c.setBackground(error.getColor());
            c.setToolTipText(error.getMessage());
        }
        if (error.getErrorType() == Error.ERROR) {
            if (parent instanceof ValidationStatus) {
                ((ValidationStatus) parent).reportStatus(false);
            }
            return false;
        } else {
            if (parent instanceof ValidationStatus) {
                ((ValidationStatus) parent).reportStatus(true);
            }
            return true;
        }

    }
}


Now the Error class. The only way to instantiate the class this is by using the two argument constructor which takes errorType and error message. The error message s displayed in the tooltip. getColor() and getImage() methods return a suitable Color and Icon depending on the error type.

package org.gpl.validation;

import java.awt.Color;
import javax.swing.ImageIcon;
import org.gpl.utilities.ResourceManager;

/**
 *
 * @author Naveed Quadri
 */
public class Error {

    /**
     * No Error
     */
    public static final int NO_ERROR = 0;
    /**
     * Just an information
     */
    public static final int INFO = 1;
    /**
     * A warning
     */
    public static final int WARNING = 2;
    /**
     * A fatal error
     */
    public static final int ERROR = 3;
    
    private ResourceManager resourceManager;
    private int errorType;
    private String message;

    /**
     * 
     * @param errorType Type of the error
     * @param message to be displayed in the tooltip
     */
    public Error(int errorType, String message) {
        this.errorType = errorType;
        this.message = message;
        resourceManager = new ResourceManager(Error.class.getResource("Settings.properties").getPath());
    }

    /**
     *
     * @return errorType
     */
    protected int getErrorType() {
        return errorType;
    }

    /**
     * 
     * @return message
     */
    protected String getMessage() {
        return message;
    }

    /**
     * Get a suitable color depending on the error type
     * @return A color
     */
    public Color getColor() {
        switch (errorType) {
            case ERROR:
                return new Color(resourceManager.readInteger("ErrorProvider.ErrorColor",16));
            case INFO:
                return new Color(resourceManager.readInteger("ErrorProvider.InfoColor",16));
            case NO_ERROR:
                return Color.WHITE; //any random color,as we'll be using the original color from the component
            case WARNING:
                return new Color(resourceManager.readInteger("ErrorProvider.WarningColor",16));
            default:
                throw new IllegalArgumentException("Not a valid error type");
        }
    }

    /**
     * Get a suitable icon depending on the icon
     * @return ImageIcon
     */
    public ImageIcon getImage() {
        switch (errorType) {
            case ERROR:
                return resourceManager.readImage("ErrorProvider.ErrorIcon");
            case INFO:
                return resourceManager.readImage("ErrorProvider.InfoIcon");
            case NO_ERROR:
                return null;
            case WARNING:
                return resourceManager.readImage("ErrorProvider.WarningIcon");
            default:
                throw new IllegalArgumentException("Not a valid error type");
        }
    }
}


ValidationStatus is an interface that can be optionally implemented by the JDialog or JFrame to recieve the validation status of the component.If the validation is successful the method reportStatus will be called with true as the argument or with false otherwise.

package org.gpl.validation;

/**
 *
 * @author Naveed Quadri
 */
public interface ValidationStatus {

    public void reportStatus(boolean result);
    
}


The IconBorder class is where the icon is painted as part of the components border. All the work here is done in paintBorder method. This class constructor takes an icon and original border of the component. The original border is used to respect the component's current border while drawing th icon. This means if the component already has a colored  line border, then that will be drawn first and an icon will be added to it.

package org.gpl.border;

import java.awt.Component;
import java.awt.Graphics;
import java.awt.Insets;
import java.net.URL;
import javax.swing.ImageIcon;
import javax.swing.border.AbstractBorder;
import javax.swing.border.Border;

/**
 * Class to draw an Icon Border around the JComponent
 * 
*           --------------------
 *           |                * |
 *           --------------------
 *   where * is the posistion of the icon
 * 
* @version 1b * @author Naveed Quadri */ public class IconBorder extends AbstractBorder { /** * icon to draw */ private ImageIcon icon; /** * The actual border of the component. Draws any component decorations you may have * and then adds the icon */ private Border originalBorder; /** * Creates an Icon Border with the specified icon * @param icon icon to draw * @param originalBorder The actual border of the component. Draws any component decorations you may have and then adds the icon */ public IconBorder(ImageIcon icon, Border originalBorder) { this.icon = icon; this.originalBorder = originalBorder; } /** * Reads the icon from the specified URL and creates an Icon Border from the read Icon * @param imageURL * @param originalBorder */ public IconBorder(URL imageURL,Border originalBorder) { this(new ImageIcon(java.awt.Toolkit.getDefaultToolkit().createImage(imageURL)),originalBorder); } @Override public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) { originalBorder.paintBorder(c, g, x, y, width, height); Insets insets = getBorderInsets(c); int by = (c.getHeight() / 2) - (icon.getIconHeight() / 2); int w = Math.max(2, insets.left); int bx = x + width - (icon.getIconHeight() + (w * 2)) + 2; g.translate(bx, by); g.drawImage(icon.getImage(), x, y,icon.getImageObserver()); } /** * Returns the insets of the border. * @param c the component for which this border insets value applies */ @Override public Insets getBorderInsets(Component c) { return originalBorder.getBorderInsets(c); } /** * Returns whether or not the border is opaque. */ @Override public boolean isBorderOpaque() { return false; } }

Thats all to it. Now extend the ErrorProvider class, implement the errorDefinition method and use it in the JTextfield.setInputVerifier(new MyErrorProvider(JTextfield,"errormessage");

The complete source with some builtin ErrorProvider implementations can be downloaded here.

5 comments:

  1. Hi!!!

    I have a questions, where is the class ResourceManager?, I don't see it.

    Thanks!!!!

    ReplyDelete
  2. Hi carlos, you shud find the ResourceManager class in download link provided at the end of the post

    ReplyDelete
  3. This comment has been removed by the author.

    ReplyDelete
  4. I found this post very useful. Muchos gracias. In netbeans the ResourceManager class you provided didnt work for me as it tried to load a resource url like /home/me/.../ErrorProvider.jar!/org/gpj/validator/Settings.properties which failed in my setup (linux based - not sure if this is relevant). Anyway I fixed this by ignoring the settings file and just hard coding the urls to the icons. The background colors were irrelevant to me because, as you say in the article, they are ignored by the GTK look and feel.

    ReplyDelete
  5. I found this article very useful, great work!

    Under which license is your code distributed? I would like to use your library for an open source project which is licensed under an Apache 2.0 License. Your package naming implies, that it is under a GPL license. Is this true? If yes, that would be really a pitty, since itis not compatible with ASF 2.0...

    ReplyDelete