org.orbeon.oxf.xforms.control.controls.XFormsInputControl.java Source code

Java tutorial

Introduction

Here is the source code for org.orbeon.oxf.xforms.control.controls.XFormsInputControl.java

Source

/**
 * Copyright (C) 2010 Orbeon, Inc.
 *
 * This program is free software; you can redistribute it and/or modify it under the terms of the
 * GNU Lesser General Public License as published by the Free Software Foundation; either version
 * 2.1 of the License, or (at your option) any later version.
 *
 * This program 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 Lesser General Public License for more details.
 *
 * The full text of the license is available at http://www.gnu.org/copyleft/lesser.html
 */
package org.orbeon.oxf.xforms.control.controls;

import org.dom4j.Element;
import org.dom4j.QName;
import org.orbeon.oxf.processor.MatchProcessor;
import org.orbeon.oxf.processor.Perl5MatchProcessor;
import org.orbeon.oxf.xforms.XFormsConstants;
import org.orbeon.oxf.xforms.XFormsProperties;
import org.orbeon.oxf.xforms.analysis.ElementAnalysis;
import org.orbeon.oxf.xforms.analysis.controls.LHHAAnalysis;
import org.orbeon.oxf.xforms.control.FocusableTrait;
import org.orbeon.oxf.xforms.control.XFormsControl;
import org.orbeon.oxf.xforms.control.XFormsValueControl;
import org.orbeon.oxf.xforms.xbl.XBLContainer;
import org.orbeon.oxf.xml.dom4j.Dom4jUtils;
import org.orbeon.saxon.om.Item;
import org.orbeon.saxon.om.ValueRepresentation;
import org.orbeon.saxon.value.CalendarValue;
import org.orbeon.saxon.value.DateValue;
import org.orbeon.saxon.value.StringValue;
import org.orbeon.saxon.value.TimeValue;
import scala.Tuple3;

import java.util.*;

/**
 * Represents an xforms:input control.
 */
public class XFormsInputControl extends XFormsValueControl implements FocusableTrait {

    // List of attributes to handle as AVTs
    private static final QName[] EXTENSION_ATTRIBUTES = { XFormsConstants.XXFORMS_SIZE_QNAME,
            XFormsConstants.XXFORMS_MAXLENGTH_QNAME, XFormsConstants.XXFORMS_AUTOCOMPLETE_QNAME };

    // Optional display format
    // TODO: Use static information in FormatTrait instead
    private final String format;
    private final String unformat;

    public XFormsInputControl(XBLContainer container, XFormsControl parent, Element element, String id) {
        super(container, parent, element, id);
        if (element != null) { // can be null in some unit tests only
            this.format = element.attributeValue(new QName("format", XFormsConstants.XXFORMS_NAMESPACE));
            this.unformat = element.attributeValue(new QName("unformat", XFormsConstants.XXFORMS_NAMESPACE));
        } else {
            this.format = this.unformat = null;
        }
    }

    @Override
    public QName[] getExtensionAttributes() {
        return EXTENSION_ATTRIBUTES;
    }

    public String getSize() {
        return getExtensionAttributeValue(XFormsConstants.XXFORMS_SIZE_QNAME);
    }

    public String getMaxlength() {
        return getExtensionAttributeValue(XFormsConstants.XXFORMS_MAXLENGTH_QNAME);
    }

    public String getAutocomplete() {
        return getExtensionAttributeValue(XFormsConstants.XXFORMS_AUTOCOMPLETE_QNAME);
    }

    @Override
    public Tuple3<String, String, String> getJavaScriptInitialization() {
        final PlaceHolderInfo placeHolderInfo = getPlaceholderInfo(staticControl(), this);
        if (placeHolderInfo != null) {
            // Control name becomes "label" or "hint". This is a special case as even non-external labels and hints can
            // have the placeholder appearance, and those are not really controls.
            return new Tuple3<String, String, String>(placeHolderInfo.isLabelPlaceholder ? "label" : "hint",
                    Dom4jUtils.qNameToExplodedQName(XFormsConstants.XFORMS_MINIMAL_APPEARANCE_QNAME),
                    getEffectiveId());
        } else {
            return null;
        }
    }

    @Override
    public void evaluateExternalValue() {

        assert isRelevant();

        final String internalValue = getValue();
        assert internalValue != null;

        final String updatedValue;

        final String typeName = getBuiltinTypeName();
        if (typeName != null) {
            if (typeName.equals("boolean")) {
                // xs:boolean

                // NOTE: We have decided that it did not make much sense to encrypt the value for boolean. This also poses
                // a problem since the server does not send an itemset for new booleans, therefore the client cannot know
                // the encrypted value of "true". So we do not encrypt values.

                updatedValue = Boolean.toString("true".equals(internalValue));
            } else {
                // Other types
                // For now, format only if the format attribute is present
                updatedValue = (format != null) ? getValueUseFormat(format) : internalValue;
            }
        } else {
            // No type, format if the format attribute is present
            updatedValue = (format != null) ? getValueUseFormat(format) : internalValue;
        }

        setExternalValue(updatedValue);
    }

    @Override
    public void storeExternalValue(String value) {
        // Store after converting
        super.storeExternalValue(convertFromExternalValue(value));

        // Tricky: mark the external value as dirty if there is a format, as the client will expect an up to date formatted value
        if (format != null) {
            markExternalValueDirty();
            containingDocument().getControls().markDirtySinceLastRequest(false);
        }
    }

    private String convertFromExternalValue(String externalValue) {
        final String typeName = getBuiltinTypeName();
        if (typeName != null) {
            if (typeName.equals("boolean")) {
                // Boolean input

                // NOTE: We have decided that it did not make much sense to encrypt the value for boolean. This also poses
                // a problem since the server does not send an itemset for new booleans, therefore the client cannot know
                // the encrypted value of "true". So we do not encrypt values.

                // Anything but "true" is "false"
                if (!externalValue.equals("true"))
                    externalValue = "false";
            } else if (containingDocument().getStaticState().isNoscript()) {
                // Noscript mode: value must be pre-processed on the server (in Ajax mode, ISO value is sent to server if possible)

                if ("date".equals(typeName)) {
                    // Date input
                    externalValue = externalValue.trim();
                    final Perl5MatchProcessor matcher = new Perl5MatchProcessor();
                    // TODO: like on client, must handle oxf.xforms.format.input.date
                    externalValue = parse(matcher, DATE_PARSE_PATTERNS, externalValue);
                } else if ("time".equals(typeName)) {
                    // Time input
                    externalValue = externalValue.trim();
                    final Perl5MatchProcessor matcher = new Perl5MatchProcessor();
                    // TODO: like on client, must handle oxf.xforms.format.input.time
                    externalValue = parse(matcher, TIME_PARSE_PATTERNS, externalValue);
                } else if ("dateTime".equals(typeName)) {
                    // Date + time input
                    externalValue = externalValue.trim();

                    // Split into date and time parts
                    // We use the same separator as the repeat separator. This is set in xforms-server-submit.xpl.
                    final String datePart = getDateTimeDatePart(externalValue,
                            XFormsConstants.REPEAT_HIERARCHY_SEPARATOR_1);
                    final String timePart = getDateTimeTimePart(externalValue,
                            XFormsConstants.REPEAT_HIERARCHY_SEPARATOR_1);

                    if (datePart.length() == 0 && timePart.length() == 0) {
                        // Special case of empty parts
                        externalValue = "";
                    } else {
                        // Parse and recombine with 'T' separator (result may be invalid dateTime, of course!)
                        final Perl5MatchProcessor matcher = new Perl5MatchProcessor();
                        externalValue = parse(matcher, DATE_PARSE_PATTERNS, datePart) + 'T'
                                + parse(matcher, TIME_PARSE_PATTERNS, timePart);
                    }
                } else {
                    externalValue = convertFromExternalValueUseUnformat(externalValue);
                }
            } else {
                externalValue = convertFromExternalValueUseUnformat(externalValue);
            }
        } else {
            externalValue = convertFromExternalValueUseUnformat(externalValue);
        }

        return externalValue;
    }

    private String convertFromExternalValueUseUnformat(String externalValue) {
        if (unformat != null) {
            final String result = evaluateAsString(unformat,
                    Collections.<Item>singletonList(StringValue.makeStringValue(externalValue)), 1);
            return (result != null) ? result : externalValue;
        } else {
            return externalValue;
        }
    }

    private static String getDateTimeDatePart(String value, char separator) {
        final int separatorIndex = value.indexOf(separator);
        if (separatorIndex == -1) {
            return value;
        } else {
            return value.substring(0, separatorIndex).trim();
        }
    }

    private static String getDateTimeTimePart(String value, char separator) {
        final int separatorIndex = value.indexOf(separator);
        if (separatorIndex == -1) {
            return "";
        } else {
            return value.substring(separatorIndex + 1).trim();
        }
    }

    private static String parse(Perl5MatchProcessor matcher, ParsePattern[] patterns, String value) {
        for (final ParsePattern currentPattern : patterns) {
            final MatchProcessor.Result result = matcher.match(currentPattern.getRe(), value);
            if (result.matches) {
                // Pattern matches
                return currentPattern.handle(result);
            }
        }

        // Return value unmodified
        return value;
    }

    private interface ParsePattern {
        public String getRe();

        public String handle(MatchProcessor.Result result);
    }

    // See also patterns on xforms.js
    private static ParsePattern[] DATE_PARSE_PATTERNS = new ParsePattern[] {
            // TODO: remaining regexps from xforms.js?
            // Today
            // Tomorrow
            // Yesterday
            // 4th
            // 4th Jan
            // 4th Jan 2003
            // Jan 4th
            // Jan 4th 2003
            // next Tuesday - this is suspect due to weird meaning of "next"
            // last Tuesday

            // mm/dd/yyyy (American style)
            new ParsePattern() {
                public String getRe() {
                    return "^(\\d{1,2})\\/(\\d{1,2})\\/(\\d{2,4})$";
                }

                public String handle(MatchProcessor.Result result) {

                    final String year = result.groups.get(2);
                    final String month = result.groups.get(0);
                    final String day = result.groups.get(1);
                    // TODO: year on 2 or 3 digits
                    final DateValue value = new DateValue(Integer.parseInt(year), Byte.parseByte(month),
                            Byte.parseByte(day));
                    return value.getStringValue();
                }
            },
            // mm/dd (American style without year)
            new ParsePattern() {
                public String getRe() {
                    return "^(\\d{1,2})\\/(\\d{1,2})$";
                }

                public String handle(MatchProcessor.Result result) {

                    final String year = Integer.toString(new GregorianCalendar().get(Calendar.YEAR));// current year
                    final String month = result.groups.get(0);
                    final String day = result.groups.get(1);
                    final DateValue value = new DateValue(Integer.parseInt(year), Byte.parseByte(month),
                            Byte.parseByte(day));
                    return value.getStringValue();
                }
            },
            // dd.mm.yyyy (Swiss style)
            new ParsePattern() {
                public String getRe() {
                    return "^(\\d{1,2})\\.(\\d{1,2})\\.(\\d{2,4})$";
                }

                public String handle(MatchProcessor.Result result) {

                    final String year = result.groups.get(2);
                    final String month = result.groups.get(1);
                    final String day = result.groups.get(0);
                    // TODO: year on 2 or 3 digits
                    final DateValue value = new DateValue(Integer.parseInt(year), Byte.parseByte(month),
                            Byte.parseByte(day));
                    return value.getStringValue();
                }
            },
            // yyyy-mm-dd (ISO style)
            new ParsePattern() {
                public String getRe() {
                    return "(\\d{2,4})-(\\d{1,2})-(\\d{1,2})(Z|([+-]\\d{2}:\\d{2}))?";
                }

                public String handle(MatchProcessor.Result result) {

                    final String year = result.groups.get(0);
                    final String month = result.groups.get(1);
                    final String day = result.groups.get(2);
                    // TODO: year on 2 or 3 digits
                    final DateValue value = new DateValue(Integer.parseInt(year), Byte.parseByte(month),
                            Byte.parseByte(day));
                    return value.getStringValue();
                }
            } };

    // See also patterns on xforms.js
    private static ParsePattern[] TIME_PARSE_PATTERNS = new ParsePattern[] {
            // TODO: remaining regexps from xforms.js?
            // Now
            // TODO

            // 12:34:56 p.m.
            new ParsePattern() {
                public String getRe() {
                    return "^(\\d{1,2}):(\\d{1,2}):(\\d{1,2}) ?(p|pm|p\\.\\m\\.)$";
                }

                public String handle(MatchProcessor.Result result) {
                    byte hoursByte = Byte.parseByte(result.groups.get(0));
                    if (hoursByte < 12)
                        hoursByte += 12;

                    final String minutes = result.groups.get(1);
                    final String seconds = result.groups.get(2);
                    final TimeValue value = new TimeValue(hoursByte, Byte.parseByte(minutes),
                            Byte.parseByte(seconds), 0, CalendarValue.NO_TIMEZONE);
                    return value.getStringValue();
                }
            },
            // 12:34 p.m.
            new ParsePattern() {
                public String getRe() {
                    return "^(\\d{1,2}):(\\d{1,2}) ?(p|pm|p\\.\\m\\.)$";
                }

                public String handle(MatchProcessor.Result result) {
                    byte hoursByte = Byte.parseByte(result.groups.get(0));
                    if (hoursByte < 12)
                        hoursByte += 12;

                    final String minutes = result.groups.get(1);
                    final TimeValue value = new TimeValue(hoursByte, Byte.parseByte(minutes), (byte) 0, 0,
                            CalendarValue.NO_TIMEZONE);
                    return value.getStringValue();
                }
            },
            // 12 p.m.
            new ParsePattern() {
                public String getRe() {
                    return "^(\\d{1,2}) ?(p|pm|p\\.\\m\\.)$";
                }

                public String handle(MatchProcessor.Result result) {
                    byte hoursByte = Byte.parseByte(result.groups.get(0));
                    if (hoursByte < 12)
                        hoursByte += 12;

                    final TimeValue value = new TimeValue(hoursByte, (byte) 0, (byte) 0, 0,
                            CalendarValue.NO_TIMEZONE);
                    return value.getStringValue();
                }
            },
            // 12:34:56 (a.m.)
            new ParsePattern() {
                public String getRe() {
                    return "^(\\d{1,2}):(\\d{1,2}):(\\d{1,2}) ?(a|am|a\\.\\m\\.)?$";
                }

                public String handle(MatchProcessor.Result result) {
                    final String hours = result.groups.get(0);
                    final String minutes = result.groups.get(1);
                    final String seconds = result.groups.get(2);
                    final TimeValue value = new TimeValue(Byte.parseByte(hours), Byte.parseByte(minutes),
                            Byte.parseByte(seconds), 0, CalendarValue.NO_TIMEZONE);
                    return value.getStringValue();
                }
            },
            // 12:34 (a.m.)
            new ParsePattern() {
                public String getRe() {
                    return "^(\\d{1,2}):(\\d{1,2}) ?(a|am|a\\.\\m\\.)?$";
                }

                public String handle(MatchProcessor.Result result) {

                    final String hours = result.groups.get(0);
                    final String minutes = result.groups.get(1);
                    final TimeValue value = new TimeValue(Byte.parseByte(hours), Byte.parseByte(minutes), (byte) 0,
                            0, CalendarValue.NO_TIMEZONE);
                    return value.getStringValue();
                }
            },
            // 12 (a.m.)
            new ParsePattern() {
                public String getRe() {
                    return "^(\\d{1,2}) ?(a|am|a\\.\\m\\.)?$";
                }

                public String handle(MatchProcessor.Result result) {

                    final String hours = result.groups.get(0);
                    final TimeValue value = new TimeValue(Byte.parseByte(hours), (byte) 0, (byte) 0, 0,
                            CalendarValue.NO_TIMEZONE);
                    return value.getStringValue();
                }
            },
            // hhmmss
            /* TODO: JS code has this, need to implement same logic
            new ParsePattern() {
            public String getRe() {
                return "^(\\d{1,6})$";
            }
            public String handle(MatchProcessor.Result result) {
                
                final String all = result.groups.get(0);
                
            //                    var d = new Date();
            //                    var h = bits[1].substring(0,2);
            //                    var m = parseInt(bits[1].substring(2,4), 10);
            //                    var s = parseInt(bits[1].substring(4,6), 10);
            //                    if (isNaN(m)) {m = 0;}
            //                    if (isNaN(s)) {s = 0;}
            //                    d.setHours(parseInt(h, 10));
            //                    d.setMinutes(parseInt(m, 10));
            //                    d.setSeconds(parseInt(s, 10));
                
                
                final String minutes = result.groups.get(1);
                final TimeValue value = new TimeValue(Byte.parseByte(hours), Byte.parseByte(minutes), (byte) 0, 0, CalendarValue.NO_TIMEZONE);
                return value.getStringValue();
            }
            }
            */
    };

    /**
     * Convenience method for handler: return the value of the first input field.
     *
     * @return                  value to store in the first input field
     */
    public String getFirstValueUseFormat() {
        final String result;

        if (isRelevant()) {
            final String typeName = getBuiltinTypeName();
            if ("date".equals(typeName) || "time".equals(typeName)) {
                // Format value specially
                result = formatSubValue(getFirstValueType(), getValue());
            } else if ("dateTime".equals(typeName)) {
                // Format value specially
                // Extract date part
                final String datePart = getDateTimeDatePart(getValue(), 'T');
                result = formatSubValue(getFirstValueType(), datePart);
            } else {
                // Regular case, use external value
                result = getExternalValue();
            }
        } else {
            result = null;
        }

        return (result != null) ? result : "";
    }

    /**
     * Convenience method for handler: return the value of the second input field.
     *
     * @return                  value to store in the second input field
     */
    public String getSecondValueUseFormat() {
        final String result;

        if (isRelevant()) {
            final String typeName = getBuiltinTypeName();
            if ("dateTime".equals(typeName)) {
                // Format value specially
                // Extract time part
                final String timePart = getDateTimeTimePart(getValue(), 'T');
                result = formatSubValue(getSecondValueType(), timePart);
            } else {
                // N/A
                result = null;
            }
        } else {
            result = null;
        }

        return (result != null) ? result : "";
    }

    /**
     * Convenience method for handler: return a formatted value for read-only output.
     *
     * @return                  formatted value
     */
    public String getReadonlyValueUseFormat() {
        return isRelevant() ? getValueUseFormat(format) : null;
    }

    private String formatSubValue(String valueType, String value) {

        final Map<String, ValueRepresentation> variables = new HashMap<String, ValueRepresentation>();
        variables.put("v", new StringValue(value));

        final Item boundItem = getBoundItem();
        if (boundItem == null) {
            // No need to format
            return null;
        } else {
            // Format
            final String xpathExpression = "if ($v castable as xs:" + valueType + ") then format-" + valueType
                    + "(xs:" + valueType + "($v), '"
                    + XFormsProperties.getTypeInputFormat(containingDocument(), valueType)
                    + "', 'en', (), ()) else $v";

            return evaluateAsString(boundItem, xpathExpression, XFormsValueControl.FORMAT_NAMESPACE_MAPPING(),
                    variables);
        }
    }

    public String getFirstValueType() {
        final String typeName = getBuiltinTypeName();
        if ("dateTime".equals(typeName)) {
            return "date";
        } else {
            return typeName;
        }
    }

    public String getSecondValueType() {
        final String typeName = getBuiltinTypeName();
        if ("dateTime".equals(typeName)) {
            return "time";
        } else {
            return null;
        }
    }

    public static String testParseTime(String value) {
        final Perl5MatchProcessor matcher = new Perl5MatchProcessor();
        return parse(matcher, XFormsInputControl.TIME_PARSE_PATTERNS, value);
    }

    public static String testParseDate(String value) {
        final Perl5MatchProcessor matcher = new Perl5MatchProcessor();
        return parse(matcher, XFormsInputControl.DATE_PARSE_PATTERNS, value);
    }

    public static class PlaceHolderInfo {
        public final boolean isLabelPlaceholder;
        public final boolean isHintPlaceholder;
        public final String placeholder; // null if no placeholder, "" if placeholder and non-concrete control, placeholder value otherwise

        public PlaceHolderInfo(boolean isLabelPlaceholder, boolean isHintPlaceholder, String placeholder) {
            this.isLabelPlaceholder = isLabelPlaceholder;
            this.isHintPlaceholder = isHintPlaceholder;
            this.placeholder = placeholder;
        }
    }

    public static PlaceHolderInfo getPlaceholderInfo(ElementAnalysis elementAnalysis, XFormsControl control) {
        // Handle placeholder

        final boolean isLabelPlaceholder = LHHAAnalysis.hasLHHAPlaceholder(elementAnalysis, "label");
        final boolean isHintPlaceholder = !isLabelPlaceholder
                && LHHAAnalysis.hasLHHAPlaceholder(elementAnalysis, "hint");

        if (isLabelPlaceholder || isHintPlaceholder) {
            final String placeholderValue; // null if no placeholder, "" if placeholder and non-concrete control, placeholder value otherwise
            if (control != null && control.isRelevant()) {
                if (isLabelPlaceholder)
                    placeholderValue = control.getLabel();
                else
                    placeholderValue = control.getHint();
            } else {
                placeholderValue = "";
            }

            return new PlaceHolderInfo(isLabelPlaceholder, isHintPlaceholder, placeholderValue);
        } else {
            return null;
        }
    }
}