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
015package org.apache.tapestry5.corelib.components;
016
017import org.apache.tapestry5.*;
018import org.apache.tapestry5.annotations.*;
019import org.apache.tapestry5.corelib.base.AbstractField;
020import org.apache.tapestry5.ioc.Messages;
021import org.apache.tapestry5.ioc.annotations.Inject;
022import org.apache.tapestry5.ioc.internal.util.InternalUtils;
023import org.apache.tapestry5.json.JSONObject;
024import org.apache.tapestry5.services.ComponentDefaultProvider;
025import org.apache.tapestry5.services.Request;
026import org.apache.tapestry5.services.javascript.JavaScriptSupport;
027
028import java.text.DateFormat;
029import java.text.ParseException;
030import java.text.SimpleDateFormat;
031import java.util.Date;
032import 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)
052public 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}