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