    001// Copyright 2007-2014 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
    015package org.apache.tapestry5.corelib.components;
    016
    017import org.apache.tapestry5.*;
    018import org.apache.tapestry5.annotations.Events;
    019import org.apache.tapestry5.annotations.Import;
    020import org.apache.tapestry5.annotations.Parameter;
    021import org.apache.tapestry5.annotations.RequestParameter;
    022import org.apache.tapestry5.corelib.base.AbstractField;
    023import org.apache.tapestry5.ioc.Messages;
    024import org.apache.tapestry5.ioc.annotations.Inject;
    025import org.apache.tapestry5.ioc.annotations.Symbol;
    026import org.apache.tapestry5.ioc.internal.util.InternalUtils;
    027import org.apache.tapestry5.json.JSONObject;
    028import org.apache.tapestry5.services.ComponentDefaultProvider;
    029import org.apache.tapestry5.services.compatibility.DeprecationWarning;
    030
    031import java.text.DateFormat;
    032import java.text.ParseException;
    033import java.text.SimpleDateFormat;
    034import java.util.Date;
    035import java.util.Locale;
    036
    037/**
    038 * A component used to collect a provided date from the user using a client-side JavaScript calendar. Non-JavaScript
    039 * clients can simply type into a text field.
    040 * <p/>
    041 * One aspect here is that, because client-side JavaScript formatting and parsing is so limited, we (currently)
    042 * use Ajax to send the user's input to the server for parsing (before raising the popup) and formatting (after closing
    043 * the popup). Weird and inefficient, but easier than writing client-side JavaScript for that purpose.
    044 * <p/>
    045 * Tapestry's DateField component is a wrapper around <a
    046 * href="http://webfx.eae.net/dhtml/datepicker/datepicker.html">WebFX DatePicker</a>.
    047 *
    048 * @tapestrydoc
    049 * @see Form
    050 * @see TextField
    051 */
    052// TODO: More testing; see https://issues.apache.org/jira/browse/TAPESTRY-1844
    053@Import(stylesheet = "${tapestry.datepicker}/css/datepicker.css",
    054        module = "t5/core/datefield")
    055@Events(EventConstants.VALIDATE)
    056public class DateField extends AbstractField
    057{
    058    /**
    059     * The value parameter of a DateField must be a {@link java.util.Date}.
    060     */
    061    @Parameter(required = true, principal = true, autoconnect = true)
    062    private Date value;
    063
    064    /**
    065     * The format used to format <em>and parse</em> dates. This is typically specified as a string which is coerced to a
    066     * DateFormat. You should be aware that using a date format with a two digit year is problematic: Java (not
    067     * Tapestry) may get confused about the century.
    068     */
    069    @Parameter(required = true, allowNull = false, defaultPrefix = BindingConstants.LITERAL)
    070    private DateFormat format;
    071
    072    /**
    073     * When the <code>format</code> parameter isn't used, this parameter defines whether the
    074     * <code>DateFormat</code> created by this component will be lenient or not.
    075     * The default value of this parameter is the value of the {@link SymbolConstants#LENIENT_DATE_FORMAT}
    076     * symbol.
    077     * @see DateFormat#setLenient(boolean)
    078     * @see SymbolConstants#LENIENT_DATE_FORMAT
    079     * @since 5.4
    080     */
    081    @Parameter(principal = true)
    082    private boolean lenient;
    083
    084    /**
    085     * If true, then the text field will be hidden, and only the icon for the date picker will be visible. The default
    086     * is false.
    087     */
    088    @Parameter
    089    private boolean hideTextField;
    090
    091    /**
    092     * The object that will perform input validation (which occurs after translation). The translate binding prefix is
    093     * generally used to provide this object in a declarative fashion.
    094     */
    095    @Parameter(defaultPrefix = BindingConstants.VALIDATE)
    096    @SuppressWarnings("unchecked")
    097    private FieldValidator<Object> validate;
    098
    099    /**
    100     * Icon used for the date field trigger button. This was used in Tapestry 5.3 and earlier and is now ignored.
    101     *
    102     * @deprecated Deprecated in 5.4 with no replacement. The component leverages the Twitter Bootstrap glyphicons support.
    103     */
    104    @Parameter(defaultPrefix = BindingConstants.ASSET)
    105    private Asset icon;
    106
    107    /**
    108     * Used to override the component's message catalog.
    109     *
    110     * @since 5.2.0.0
    111     * @deprecated Since 5.4; override the global message key "core-date-value-not-parsable" instead (see {@link org.apache.tapestry5.services.messages.ComponentMessagesSource})
    112     */
    113    @Parameter("componentResources.messages")
    114    private Messages messages;
    115
    116    @Inject
    117    private Locale locale;
    118
    119    @Inject
    120    private DeprecationWarning deprecationWarning;
    121
    122    @Inject
    123    @Symbol(SymbolConstants.LENIENT_DATE_FORMAT)
    124    private boolean lenientDateFormatSymbolValue;
    125
    126    private static final String RESULT = "result";
    127
    128    private static final String ERROR = "error";
    129    private static final String INPUT_PARAMETER = "input";
    130
    131    void pageLoaded()
    132    {
    133        deprecationWarning.ignoredComponentParameters(resources, "icon");
    134    }
    135
    136    DateFormat defaultFormat()
    137    {
    138        DateFormat shortDateFormat = DateFormat.getDateInstance(DateFormat.SHORT, locale);
    139
    140        if (shortDateFormat instanceof SimpleDateFormat)
    141        {
    142            SimpleDateFormat simpleDateFormat = (SimpleDateFormat) shortDateFormat;
    143
    144            String pattern = simpleDateFormat.toPattern();
    145
    146            String revised = pattern.replaceAll("([^y])yy$", "$1yyyy");
    147
    148            final SimpleDateFormat revisedDateFormat = new SimpleDateFormat(revised);
    149            revisedDateFormat.setLenient(lenient);
    150            return revisedDateFormat;
    151        }
    152
    153        return shortDateFormat;
    154    }
    155
    156    /**
    157     * Computes a default value for the "validate" parameter using {@link ComponentDefaultProvider}.
    158     */
    159    final Binding defaultValidate()
    160    {
    161        return defaultProvider.defaultValidatorBinding("value", resources);
    162    }
    163
    164    final boolean defaultLenient() {
    165        return lenientDateFormatSymbolValue;
    166    }
    167
    168    /**
    169     * Ajax event handler, used when initiating the popup. The client sends the input value form the field to the server
    170     * to parse it according to the server-side format. The response contains a "result" key of the formatted date in a
    171     * format acceptable to the JavaScript Date() constructor. Alternately, an "error" key indicates the the input was
    172     * not formatted correct.
    173     */
    174    JSONObject onParse(@RequestParameter(INPUT_PARAMETER)
    175                       String input)
    176    {
    177        JSONObject response = new JSONObject();
    178
    179        try
    180        {
    181            Date date = format.parse(input);
    182
    183            response.put(RESULT, date.getTime());
    184        } catch (ParseException ex)
    185        {
    186            response.put(ERROR, ex.getMessage());
    187        }
    188
    189        return response;
    190    }
    191
    192    /**
    193     * Ajax event handler, used after the client-side popup completes. The client sends the date, formatted as
    194     * milliseconds since the epoch, to the server, which reformats it according to the server side format and returns
    195     * the result.
    196     */
    197    JSONObject onFormat(@RequestParameter(INPUT_PARAMETER)
    198                        String input)
    199    {
    200        JSONObject response = new JSONObject();
    201
    202        try
    203        {
    204            long millis = Long.parseLong(input);
    205
    206            Date date = new Date(millis);
    207
    208            response.put(RESULT, format.format(date));
    209        } catch (NumberFormatException ex)
    210        {
    211            response.put(ERROR, ex.getMessage());
    212        }
    213
    214        return response;
    215    }
    216
    217    void beginRender(MarkupWriter writer)
    218    {
    219        String value = validationTracker.getInput(this);
    220
    221        if (value == null)
    222        {
    223            value = formatCurrentValue();
    224        }
    225
    226        String clientId = getClientId();
    227
    228        writer.element("div",
    229                "data-component-type", "core/DateField",
    230                "data-parse-url", resources.createEventLink("parse").toString(),
    231                "data-format-url", resources.createEventLink("format").toString());
    232
    233        if (!hideTextField)
    234        {
    235            writer.attributes("class", "input-group");
    236        }
    237
    238        writer.element("input",
    239
    240                "type", hideTextField ? "hidden" : "text",
    241
    242                "class", cssClass,
    243
    244                "name", getControlName(),
    245
    246                "id", clientId,
    247
    248                "value", value);
    249
    250        writeDisabled(writer);
    251
    252        putPropertyNameIntoBeanValidationContext("value");
    253
    254        validate.render(writer);
    255
    256        removePropertyNameFromBeanValidationContext();
    257
    258        resources.renderInformalParameters(writer);
    259
    260        decorateInsideField();
    261
    262        writer.end();   // input
    263
    264        if (!hideTextField)
    265        {
    266            writer.element("span", "class", "input-group-btn");
    267        }
    268
    269        writer.element("button",
    270                "type", "button",
    271                "class", "btn btn-default",
    272                "alt", "[Show]");
    273
    274        writer.element("span", "class", "glyphicon glyphicon-calendar");
    275        writer.end(); // span
    276
    277        writer.end(); // button
    278
    279        if (!hideTextField)
    280        {
    281            writer.end();        // span.input-group-btn
    282        }
    283
    284        writer.end(); // outer div
    285    }
    286
    287    private void writeDisabled(MarkupWriter writer)
    288    {
    289        if (isDisabled())
    290            writer.attributes("disabled", "disabled");
    291    }
    292
    293    private String formatCurrentValue()
    294    {
    295        if (value == null)
    296            return "";
    297
    298        return format.format(value);
    299    }
    300
    301    @Override
    302    protected void processSubmission(String controlName)
    303    {
    304        String value = request.getParameter(controlName);
    305
    306        validationTracker.recordInput(this, value);
    307
    308        Date parsedValue = null;
    309
    310        try
    311        {
    312            if (InternalUtils.isNonBlank(value))
    313                parsedValue = format.parse(value);
    314        } catch (ParseException ex)
    315        {
    316            validationTracker.recordError(this, messages.format("core-date-value-not-parseable", value));
    317            return;
    318        }
    319
    320        putPropertyNameIntoBeanValidationContext("value");
    321        try
    322        {
    323            fieldValidationSupport.validate(parsedValue, resources, validate);
    324
    325            this.value = parsedValue;
    326        } catch (ValidationException ex)
    327        {
    328            validationTracker.recordError(this, ex.getMessage());
    329        }
    330
    331        removePropertyNameFromBeanValidationContext();
    332    }
    333
    334    void injectResources(ComponentResources resources)
    335    {
    336        this.resources = resources;
    337    }
    338
    339    @Override
    340    public boolean isRequired()
    341    {
    342        return validate.isRequired();
    343    }
    344}