    001    // Copyright 2007, 2008, 2009, 2010, 2011 The Apache Software Foundation
    002    //
    003    // Licensed under the Apache License, Version 2.0 (the "License");
    004    // you may not use this file except in compliance with the License.
    005    // You may obtain a copy of the License at
    006    //
    007    // http://www.apache.org/licenses/LICENSE-2.0
    008    //
    009    // Unless required by applicable law or agreed to in writing, software
    010    // distributed under the License is distributed on an "AS IS" BASIS,
    011    // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    012    // See the License for the specific language governing permissions and
    013    // limitations under the License.
    014
    015    package org.apache.tapestry5.corelib.components;
    016
    017    import org.apache.tapestry5.*;
    018    import org.apache.tapestry5.annotations.*;
    019    import org.apache.tapestry5.corelib.base.AbstractField;
    020    import org.apache.tapestry5.ioc.Messages;
    021    import org.apache.tapestry5.ioc.annotations.Inject;
    022    import org.apache.tapestry5.ioc.internal.util.InternalUtils;
    023    import org.apache.tapestry5.json.JSONObject;
    024    import org.apache.tapestry5.services.ComponentDefaultProvider;
    025    import org.apache.tapestry5.services.Request;
    026    import org.apache.tapestry5.services.javascript.JavaScriptSupport;
    027
    028    import java.text.DateFormat;
    029    import java.text.ParseException;
    030    import java.text.SimpleDateFormat;
    031    import java.util.Date;
    032    import java.util.Locale;
    033
    034    /**
    035     * A component used to collect a provided date from the user using a client-side JavaScript calendar. Non-JavaScript
    036     * clients can simply type into a text field.
    037     * <p/>
    038     * One wierd aspect here is that, because client-side JavaScript formatting and parsing is so limited, we (currently)
    039     * use Ajax to send the user's input to the server for parsing (before raising the popup) and formatting (after closing
    040     * the popup). Weird and inefficient, but easier than writing client-side JavaScript for that purpose.
    041     * <p/>
    042     * Tapestry's DateField component is a wrapper around <a
    043     * href="http://webfx.eae.net/dhtml/datepicker/datepicker.html">WebFX DatePicker</a>.
    044     *
    045     * @tapestrydoc
    046     * @see Form
    047     * @see TextField
    048     */
    049    // TODO: More testing; see https://issues.apache.org/jira/browse/TAPESTRY-1844
    050    @Import(stack = "core-datefield")
    051    @Events(EventConstants.VALIDATE)
    052    public class DateField extends AbstractField
    053    {
    054        /**
    055         * The value parameter of a DateField must be a {@link java.util.Date}.
    056         */
    057        @Parameter(required = true, principal = true, autoconnect = true)
    058        private Date value;
    059
    060        /**
    061         * The format used to format <em>and parse</em> dates. This is typically specified as a string which is coerced to a
    062         * DateFormat. You should be aware that using a date format with a two digit year is problematic: Java (not
    063         * Tapestry) may get confused about the century.
    064         */
    065        @Parameter(required = true, allowNull = false, defaultPrefix = BindingConstants.LITERAL)
    066        private DateFormat format;
    067
    068        /**
    069         * If true, then the text field will be hidden, and only the icon for the date picker will be visible. The default
    070         * is false.
    071         */
    072        @Parameter
    073        private boolean hideTextField;
    074
    075        /**
    076         * The object that will perform input validation (which occurs after translation). The translate binding prefix is
    077         * generally used to provide this object in a declarative fashion.
    078         */
    079        @Parameter(defaultPrefix = BindingConstants.VALIDATE)
    080        @SuppressWarnings("unchecked")
    081        private FieldValidator<Object> validate;
    082
    083        @Parameter(defaultPrefix = BindingConstants.ASSET, value = "datefield.gif")
    084        private Asset icon;
    085
    086        /**
    087         * Used to override the component's message catalog.
    088         *
    089         * @since 5.2.0.0
    090         */
    091        @Parameter("componentResources.messages")
    092        private Messages messages;
    093
    094        @Environmental
    095        private JavaScriptSupport support;
    096
    097        @Environmental
    098        private ValidationTracker tracker;
    099
    100        @Inject
    101        private ComponentResources resources;
    102
    103        @Inject
    104        private Request request;
    105
    106        @Inject
    107        private Locale locale;
    108
    109        @Inject
    110        private ComponentDefaultProvider defaultProvider;
    111
    112        @Inject
    113        private FieldValidationSupport fieldValidationSupport;
    114
    115        private static final String RESULT = "result";
    116
    117        private static final String ERROR = "error";
    118        private static final String INPUT_PARAMETER = "input";
    119
    120        DateFormat defaultFormat()
    121        {
    122            DateFormat shortDateFormat = DateFormat.getDateInstance(DateFormat.SHORT, locale);
    123
    124            if (shortDateFormat instanceof SimpleDateFormat)
    125            {
    126                SimpleDateFormat simpleDateFormat = (SimpleDateFormat) shortDateFormat;
    127
    128                String pattern = simpleDateFormat.toPattern();
    129
    130                String revised = pattern.replaceAll("([^y])yy$", "$1yyyy");
    131
    132                return new SimpleDateFormat(revised);
    133            }
    134
    135            return shortDateFormat;
    136        }
    137
    138        /**
    139         * Computes a default value for the "validate" parameter using {@link ComponentDefaultProvider}.
    140         */
    141        final Binding defaultValidate()
    142        {
    143            return defaultProvider.defaultValidatorBinding("value", resources);
    144        }
    145
    146        /**
    147         * Ajax event handler, used when initiating the popup. The client sends the input value form the field to the server
    148         * to parse it according to the server-side format. The response contains a "result" key of the formatted date in a
    149         * format acceptable to the JavaScript Date() constructor. Alternately, an "error" key indicates the the input was
    150         * not formatted correct.
    151         */
    152        JSONObject onParse(@RequestParameter(INPUT_PARAMETER)
    153                           String input)
    154        {
    155            JSONObject response = new JSONObject();
    156
    157            try
    158            {
    159                Date date = format.parse(input);
    160
    161                response.put(RESULT, date.getTime());
    162            } catch (ParseException ex)
    163            {
    164                response.put(ERROR, ex.getMessage());
    165            }
    166
    167            return response;
    168        }
    169
    170        /**
    171         * Ajax event handler, used after the client-side popup completes. The client sends the date, formatted as
    172         * milliseconds since the epoch, to the server, which reformats it according to the server side format and returns
    173         * the result.
    174         */
    175        JSONObject onFormat(@RequestParameter(INPUT_PARAMETER)
    176                            String input)
    177        {
    178            JSONObject response = new JSONObject();
    179
    180            try
    181            {
    182                long millis = Long.parseLong(input);
    183
    184                Date date = new Date(millis);
    185
    186                response.put(RESULT, format.format(date));
    187            } catch (NumberFormatException ex)
    188            {
    189                response.put(ERROR, ex.getMessage());
    190            }
    191
    192            return response;
    193        }
    194
    195        void beginRender(MarkupWriter writer)
    196        {
    197            String value = tracker.getInput(this);
    198
    199            if (value == null)
    200                value = formatCurrentValue();
    201
    202            String clientId = getClientId();
    203            String triggerId = clientId + "-trigger";
    204
    205            writer.element("input",
    206
    207                    "type", hideTextField ? "hidden" : "text",
    208
    209                    "name", getControlName(),
    210
    211                    "id", clientId,
    212
    213                    "value", value);
    214
    215            writeDisabled(writer);
    216
    217            putPropertyNameIntoBeanValidationContext("value");
    218
    219            validate.render(writer);
    220
    221            removePropertyNameFromBeanValidationContext();
    222
    223            resources.renderInformalParameters(writer);
    224
    225            decorateInsideField();
    226
    227            writer.end();
    228
    229            // Now the trigger icon.
    230
    231            writer.element("img",
    232
    233                    "id", triggerId,
    234
    235                    "class", "t-calendar-trigger",
    236
    237                    "src", icon.toClientURL(),
    238
    239                    "alt", "[Show]");
    240            writer.end(); // img
    241
    242            JSONObject spec = new JSONObject();
    243
    244            spec.put("field", clientId);
    245            spec.put("parseURL", resources.createEventLink("parse").toURI());
    246            spec.put("formatURL", resources.createEventLink("format").toURI());
    247
    248            support.addInitializerCall("dateField", spec);
    249        }
    250
    251        private void writeDisabled(MarkupWriter writer)
    252        {
    253            if (isDisabled())
    254                writer.attributes("disabled", "disabled");
    255        }
    256
    257        private String formatCurrentValue()
    258        {
    259            if (value == null)
    260                return "";
    261
    262            return format.format(value);
    263        }
    264
    265        @Override
    266        protected void processSubmission(String controlName)
    267        {
    268            String value = request.getParameter(controlName);
    269
    270            tracker.recordInput(this, value);
    271
    272            Date parsedValue = null;
    273
    274            try
    275            {
    276                if (InternalUtils.isNonBlank(value))
    277                    parsedValue = format.parse(value);
    278            } catch (ParseException ex)
    279            {
    280                tracker.recordError(this, messages.format("date-value-not-parseable", value));
    281                return;
    282            }
    283
    284            putPropertyNameIntoBeanValidationContext("value");
    285            try
    286            {
    287                fieldValidationSupport.validate(parsedValue, resources, validate);
    288
    289                this.value = parsedValue;
    290            } catch (ValidationException ex)
    291            {
    292                tracker.recordError(this, ex.getMessage());
    293            }
    294
    295            removePropertyNameFromBeanValidationContext();
    296        }
    297
    298        void injectResources(ComponentResources resources)
    299        {
    300            this.resources = resources;
    301        }
    302
    303        @Override
    304        public boolean isRequired()
    305        {
    306            return validate.isRequired();
    307        }
    308    }