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}