zul/src/main/java/org/zkoss/zul/SimpleConstraint.java

Summary

Maintainability
F
1 wk
Test Coverage
/* SimpleConstraint.java

    Purpose:
        
    Description:
        
    History:
        Tue Jun 28 13:58:11     2005, Created by tomyeh

Copyright (C) 2005 Potix Corporation. All Rights Reserved.

{{IS_RIGHT
    This program is distributed under LGPL Version 2.1 in the hope that
    it will be useful, but WITHOUT ANY WARRANTY.
}}IS_RIGHT
*/
package org.zkoss.zul;

import java.util.Date;
import java.util.Iterator;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.zkoss.lang.Classes;
import org.zkoss.lang.Strings;
import org.zkoss.util.Dates;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.UiException;
import org.zkoss.zk.ui.WrongValueException;
import org.zkoss.zul.mesg.MZul;

/**
 * The default constraint supporting no empty, regular expressions and so on.
 *
 * <p>Depending on the component (such as {@link Intbox} and {@link Datebox},
 * you could combine the flags, such as {@link #NO_POSITIVE} + {@link #NO_ZERO}
 * to accept only negative number. 
 *
 * @author tomyeh
 */
public class SimpleConstraint implements Constraint, ClientConstraint, java.io.Serializable {
    private static final long serialVersionUID = 20070411L;

    /** Positive numbers are not allowed. */
    public static final int NO_POSITIVE = 0x0001;
    /** Negative numbers are not allowed. */
    public static final int NO_NEGATIVE = 0x0002;
    /** Zero numbers are not allowed. */
    public static final int NO_ZERO = 0x0004;
    /** Empty is not allowed.
     * If not specified, empty usually means null.
     */
    public static final int NO_EMPTY = 0x0100;
    /**
     * The value must match inside the data from ListModel only.
     */
    public static final int STRICT = 0x0200;
    /** Indicates this constraint requires the server validation.
     * It means, after the client validates the value successfully, it
     * will send the value to the server for further validation (by calling
     * {@link #validate}.
     * It is useful if the result of the regular expressions is different
     * at the client (with JavaScript) and the server with ava).
     */
    public static final int SERVER = 0x0400;
    /** Date in the future is not allowed. (Only date part is compared)
     */
    public static final int NO_FUTURE = NO_POSITIVE;
    /** Date in the past is not allowed. (Only date part is compared)
     */
    public static final int NO_PAST = NO_NEGATIVE;
    /** Today is not allowed. (Only date part is compared)
     */
    public static final int NO_TODAY = NO_ZERO;
    /** The Error-box position. 
     */
    public static final int BEFORE_START = 0x1000;
    /** The Error-box position. 
     */
    public static final int BEFORE_END = 0x2000;
    /** The Error-box position. 
     */
    public static final int END_BEFORE = 0x3000;
    /** The Error-box position. 
     */
    public static final int END_AFTER = 0x4000;
    /** The Error-box position. 
     */
    public static final int AFTER_END = 0x5000;
    /** The Error-box position. 
     */
    public static final int AFTER_START = 0x6000;
    /** The Error-box position. 
     */
    public static final int START_AFTER = 0x7000;
    /** The Error-box position. 
     */
    public static final int START_BEFORE = 0x8000;
    /** The Error-box position. 
     */
    public static final int OVERLAP = 0x9000;
    /** The Error-box position. 
     */
    public static final int OVERLAP_END = 0xa000;
    /** The Error-box position. 
     */
    public static final int OVERLAP_BEFORE = 0xb000;
    /** The Error-box position. 
     */
    public static final int OVERLAP_AFTER = 0xc000;
    /** The Error-box position. 
     */
    public static final int AT_POINTER = 0xd000;
    /** The Error-box position. 
     */
    public static final int AFTER_POINTER = 0xe000;

    /** The constraints. A combination of {@link #NO_POSITIVE} and others.
     */
    protected int _flags;
    protected boolean _finishParseCst = true;
    private Pattern _regex;
    private String _errmsg;
    private String _raw;
    private String _refined;

    /** Constructs a constraint with flags.
     *
     * @param flags a combination of {@link #NO_POSITIVE}, {@link #NO_NEGATIVE},
     * {@link #NO_ZERO}, and so on.
     */
    public SimpleConstraint(int flags) {
        this(flags, (Pattern) null, null);
    }

    /** Constructs a constraint with flags and an error message.
     *
     * @param flags a combination of {@link #NO_POSITIVE}, {@link #NO_NEGATIVE},
     * {@link #NO_ZERO}, and so on.
     * @param errmsg the error message to display. Ignored if null or empty.
     */
    public SimpleConstraint(int flags, String errmsg) {
        this(flags, (Pattern) null, errmsg);
    }

    /** Constructs a regular-expression constraint.
     *
     * @param regex ignored if null or empty
     * @param errmsg the error message to display. Ignored if null or empty.
     * @since 8.0.1
     */
    public SimpleConstraint(Pattern regex, String errmsg) {
        this(0, regex, errmsg);
    }

    /** Constructs a constraint combining regular expression.
     *
     * @param flags a combination of {@link #NO_POSITIVE}, {@link #NO_NEGATIVE},
     * {@link #NO_ZERO}, and so on.
     * @param regex ignored if null or empty
     * @param errmsg the error message to display. Ignored if null or empty.
     * @since 8.0.1
     */
    public SimpleConstraint(int flags, Pattern regex, String errmsg) {
        _flags = flags;
        _regex = regex;
        _errmsg = errmsg == null || errmsg.length() == 0 ? null : errmsg;
        _raw = null;
    }

    /** Constructs a constraint with a list of constraints separated by comma.
     *
     * @param constraint a list of constraints separated by comma.
     * Example: <code>no positive</code>, <code>no zero</code>, or use '/'
     * to enclose the regular expression as follows. {@code /.+@.+\.[a-z]+/: email only}
     * @since 3.0.2
     */
    public SimpleConstraint(String constraint) {
        _raw = constraint;
        _finishParseCst = false;
    }

    private void parseCst(String constraint) {
        String regex = null, errmsg = null, regexFlags = "";
        l_out:
        for (int j = 0, k = 0, len = constraint.length(); k >= 0; j = k + 1) {
            for (; ; ++j) {
                if (j >= len)
                    break l_out; //done

                char cc = constraint.charAt(j);
                switch (cc) {
                    case '/':
                        boolean hasEndingSlash = false;
                        for (k = ++j; ; ++k) { //look for ending /
                            if (k >= len) { //no ending /
                                k = -1;
                                break;
                            }

                            cc = constraint.charAt(k);
                            if (cc == '/') {
                                hasEndingSlash = true;
                                break; //ending / found
                            }
                            if (cc == '\\')
                                ++k; //skip one
                        }
                        if (hasEndingSlash) {
                            String restCst = constraint.substring(k + 1);
                            Pattern pattern = Pattern.compile(".*?(?=,|:|$)");
                            final Matcher matcher = pattern.matcher(restCst);
                            if (matcher.find()) {
                                regexFlags = matcher.group(0).trim();
                            }
                        }
                        regex = k >= 0 ? constraint.substring(j, k) : constraint.substring(j);
                        if (regexFlags.length() > 0)
                            k += regexFlags.length(); // skip regex flags
                        continue l_out;
                    case ':':
                        errmsg = constraint.substring(j + 1).trim();
                        break l_out; //done
                }
                if (!Character.isWhitespace(cc))
                    break;
            }

            String s;
            for (k = j; ; ++k) {
                if (k >= len) {
                    s = constraint.substring(j);
                    k = -1;
                    break;
                }
                final char cc = constraint.charAt(k);
                if (cc == ',' || cc == ':' || cc == ';' || cc == '/') {
                    s = constraint.substring(j, k);
                    if (cc == ':' || cc == '/')
                        --k;
                    break;
                }
            }

            _flags |= parseConstraint(s.trim().toLowerCase(java.util.Locale.ENGLISH));
        }
        _regex = getRegex(regex, regexFlags);
        _errmsg = errmsg == null || errmsg.length() == 0 ? null : errmsg;
    }

    private Pattern getRegex(String regex, String regexFlags) {
        if (regex == null || regex.length() == 0)
            return null;
        if (regexFlags.length() == 0) {
            return Pattern.compile(regex);
        } else { // ZK-4863: add pattern flags
            int regexFlag = 0;
            if (regexFlags.contains("i"))
                regexFlag |= Pattern.CASE_INSENSITIVE;
            if (regexFlags.contains("m"))
                regexFlag |= Pattern.MULTILINE;
            if (regexFlags.contains("s"))
                regexFlag |= Pattern.DOTALL;
            if (regexFlags.contains("u"))
                regexFlag |= Pattern.UNICODE_CASE;

            return Pattern.compile(regex, regexFlag);
        }
    }

    /** Parses a list of constraints from a string to an integer
     * representing a combination of {@link #NO_POSITIVE} and other flags.
     *
     * @param constraint a list of constraints separated by comma.
     * Example: no positive, no zero
     */
    public static SimpleConstraint getInstance(String constraint) {
        return new SimpleConstraint(constraint);
    }

    /** Parses a constraint into an integer value.
     * For example, "no positive" is parsed to {@link #NO_POSITIVE}.
     *
     * <p>Deriving classes might override this to provide more constraints.
     *
     * @since 3.0.2
     */
    protected int parseConstraint(String constraint) throws UiException {
        if (constraint.equals("no positive"))
            return NO_POSITIVE;
        else if (constraint.equals("no negative"))
            return NO_NEGATIVE;
        else if (constraint.equals("no zero"))
            return NO_ZERO;
        else if (constraint.equals("no empty"))
            return NO_EMPTY;
        else if (constraint.equals("no future"))
            return NO_FUTURE;
        else if (constraint.equals("no past"))
            return NO_PAST;
        else if (constraint.equals("no today"))
            return NO_TODAY;
        else if (constraint.equals("strict"))
            return STRICT;
        else if (constraint.equals("server"))
            return SERVER;
        else if (constraint.equals("before_start"))
            return BEFORE_START;
        else if (constraint.equals("before_end"))
            return BEFORE_END;
        else if (constraint.equals("end_before"))
            return END_BEFORE;
        else if (constraint.equals("end_after"))
            return END_AFTER;
        else if (constraint.equals("after_end"))
            return AFTER_END;
        else if (constraint.equals("after_start"))
            return AFTER_START;
        else if (constraint.equals("start_after"))
            return START_AFTER;
        else if (constraint.equals("start_before"))
            return START_BEFORE;
        else if (constraint.equals("overlap"))
            return OVERLAP;
        else if (constraint.equals("overlap_end"))
            return OVERLAP_END;
        else if (constraint.equals("overlap_before"))
            return OVERLAP_BEFORE;
        else if (constraint.equals("overlap_after"))
            return OVERLAP_AFTER;
        else if (constraint.equals("at_pointer"))
            return AT_POINTER;
        else if (constraint.equals("after_pointer"))
            return AFTER_POINTER;
        else if (constraint.length() > 0)
            throw new UiException("Unknown constraint: " + constraint);
        return 0;
    }

    /**
     * Returns the constraint flags, i.e., a combination of
     * {@link #NO_POSITIVE}, {@link #NO_NEGATIVE}, {@link #STRICT} and others.
     *
     * @since 3.0.2
     */
    public int getFlags() {
        return _flags;
    }

    /** Returns the custom error message that shall be shown if an error occurs,
     * or null if no custom error message specified.
     */
    public String getErrorMessage(Component comp) {
        return _errmsg;
    }

    //-- Constraint --//
    public void validate(Component comp, Object value) throws WrongValueException {
        if (!_finishParseCst) {
            if (_raw != null) parseCst(_raw);
            _finishParseCst = true;
        }
        if (value == null) {
            if ((_flags & NO_EMPTY) != 0)
                throw wrongValue(comp, MZul.EMPTY_NOT_ALLOWED);
        } else if (value instanceof Number) {
            if ((_flags & (NO_POSITIVE | NO_NEGATIVE | NO_ZERO)) == 0)
                return; //nothing to check

            final int cmp = compareTo((Comparable) value, Classes.coerce(value.getClass(), null, false)); //compare to zero
            if (cmp > 0) {
                if ((_flags & NO_POSITIVE) != 0)
                    throw wrongValue(comp, getMessageForNumberDenied());
            } else if (cmp == 0) {
                if ((_flags & NO_ZERO) != 0)
                    throw wrongValue(comp, getMessageForNumberDenied());
            } else {
                if ((_flags & NO_NEGATIVE) != 0)
                    throw wrongValue(comp, getMessageForNumberDenied());
            }
        } else if (value instanceof String) {
            final String s = (String) value;
            if ((_flags & NO_EMPTY) != 0 && s.length() == 0)
                throw wrongValue(comp, MZul.EMPTY_NOT_ALLOWED);
            if (_regex != null && !_regex.matcher(s).matches())
                throw wrongValue(comp, MZul.ILLEGAL_VALUE);
            if ((_flags & STRICT) != 0) {
                if (s.length() > 0 && comp instanceof Combobox) {
                    for (Iterator it = ((Combobox) comp).getItems().iterator(); it.hasNext();) {
                        final Comboitem ci = (Comboitem) it.next();
                        if (!ci.isDisabled() && ci.isVisible() && s.equalsIgnoreCase(ci.getLabel()))
                            return;
                    }
                    throw wrongValue(comp, MZul.VALUE_NOT_MATCHED);
                }
            }
        } else if (value instanceof Date) {
            if ((_flags & (NO_FUTURE | NO_PAST | NO_TODAY)) == 0)
                return;
            final Date date = Dates.beginOfDate((Date) value, null);
            final int cmp = date.compareTo(Dates.today());
            if (cmp > 0) {
                if ((_flags & NO_FUTURE) != 0)
                    throw wrongValue(comp, getMessageForDateDenied());
            } else if (cmp == 0) {
                if ((_flags & NO_TODAY) != 0)
                    throw wrongValue(comp, getMessageForDateDenied());
            } else {
                if ((_flags & NO_PAST) != 0)
                    throw wrongValue(comp, getMessageForDateDenied());
            }
        }
    }

    @SuppressWarnings("unchecked")
    private static int compareTo(Comparable v1, Object v2) {
        return v1.compareTo(v2);
    }

    private WrongValueException wrongValue(Component comp, int errcode) {
        return _errmsg != null ? new WrongValueException(comp, _errmsg) : new WrongValueException(comp, errcode);
    }

    private int getMessageForNumberDenied() {
        switch (_flags & (NO_POSITIVE | NO_NEGATIVE | NO_ZERO)) {
        case (NO_POSITIVE | NO_ZERO):
            return MZul.NO_POSITIVE_ZERO;
        case (NO_POSITIVE):
            return MZul.NO_POSITIVE;
        case (NO_NEGATIVE | NO_ZERO):
            return MZul.NO_NEGATIVE_ZERO;
        case (NO_NEGATIVE):
            return MZul.NO_NEGATIVE;
        case (NO_ZERO):
            return MZul.NO_ZERO;
        case (NO_POSITIVE | NO_NEGATIVE | NO_ZERO):
            return MZul.NO_POSITIVE_NEGATIVE_ZERO;
        case (NO_POSITIVE | NO_NEGATIVE):
            return MZul.NO_POSITIVE_NEGATIVE;
        }
        throw new InternalError();
    }

    private int getMessageForDateDenied() {
        switch (_flags & (NO_FUTURE | NO_PAST | NO_TODAY)) {
        case (NO_FUTURE | NO_TODAY):
            return MZul.NO_FUTURE_TODAY;
        case (NO_FUTURE):
            return MZul.NO_FUTURE;
        case (NO_PAST | NO_TODAY):
            return MZul.NO_PAST_TODAY;
        case (NO_PAST):
            return MZul.NO_PAST;
        case (NO_TODAY):
            return MZul.NO_TODAY;
        case (NO_FUTURE | NO_PAST | NO_TODAY):
            return MZul.NO_FUTURE_PAST_TODAY;
        case (NO_FUTURE | NO_PAST):
            return MZul.NO_FUTURE_PAST;
        }
        throw new InternalError();
    }

    //ClientConstraint//
    public String getClientConstraint() {
        if (_raw != null)
            return '\'' + Strings.escape(_raw, Strings.ESCAPE_JAVASCRIPT) + '\'';

        final StringBuffer sb = new StringBuffer("new zul.inp.SimpleConstraint(");
        if (_flags != 0 || _regex != null || _errmsg != null) {
            sb.append(_flags);
            if (_regex != null || _errmsg != null) {
                sb.append(',');
                if (_regex != null) {
                    sb.append('\'');
                    Strings.escape(sb, _regex.pattern(), Strings.ESCAPE_JAVASCRIPT);
                    sb.append('\'');
                } else
                    sb.append("null");
                if (_errmsg != null) {
                    sb.append(",'");
                    Strings.escape(sb, _errmsg, Strings.ESCAPE_JAVASCRIPT);
                    sb.append('\'');
                }
            }
        }
        return sb.append(')').toString();
    }

    /** Default: null (since it depends on zul.inp which is loaded for
    * all input widgets).
     */
    public String getClientPackages() {
        return null;
    }

    /**
     * Returns the raw value
     * @since 10.0.0
     */
    public String getRawValue() {
        return _raw;
    }
}