001    /*
002     * Apache License
003     * Version 2.0, January 2004
004     * http://www.apache.org/licenses/
005     *
006     * Copyright 2008-2010 by chenillekit.org
007     *
008     * Licensed under the Apache License, Version 2.0 (the "License");
009     * you may not use this file except in compliance with the License.
010     * You may obtain a copy of the License at
011     *
012     * http://www.apache.org/licenses/LICENSE-2.0
013     */
014    
015    package org.chenillekit.tapestry.core.components;
016    
017    import java.text.DateFormat;
018    import java.text.ParseException;
019    import java.text.SimpleDateFormat;
020    import java.util.Date;
021    import java.util.Locale;
022    
023    import org.apache.tapestry5.Asset;
024    import org.apache.tapestry5.Binding;
025    import org.apache.tapestry5.BindingConstants;
026    import org.apache.tapestry5.ComponentResources;
027    import org.apache.tapestry5.FieldValidationSupport;
028    import org.apache.tapestry5.FieldValidator;
029    import org.apache.tapestry5.MarkupWriter;
030    import org.apache.tapestry5.ValidationException;
031    import org.apache.tapestry5.ValidationTracker;
032    import org.apache.tapestry5.annotations.Environmental;
033    import org.apache.tapestry5.annotations.Import;
034    import org.apache.tapestry5.annotations.Parameter;
035    import org.apache.tapestry5.annotations.Path;
036    import org.apache.tapestry5.corelib.base.AbstractField;
037    import org.apache.tapestry5.ioc.Messages;
038    import org.apache.tapestry5.ioc.annotations.Inject;
039    import org.apache.tapestry5.ioc.internal.util.InternalUtils;
040    import org.apache.tapestry5.json.JSONObject;
041    import org.apache.tapestry5.services.ComponentDefaultProvider;
042    import org.apache.tapestry5.services.FieldValidatorDefaultSource;
043    import org.apache.tapestry5.services.Request;
044    import org.apache.tapestry5.services.javascript.JavaScriptSupport;
045    
046    /**
047     * A component used to collect a provided date/time from the user using a client-side JavaScript calendar. Non-JavaScript
048     * clients can simply type into a text field.
049     *
050     * @version $Id: DateTimeField.java 729 2010-11-03 19:51:08Z homburgs $
051     */
052    @Import(stylesheet = "datetimefield/datepicker.css",
053                    library = {
054                                    "datetimefield/datepicker.js",
055                                    "datetimefield/datepicker_lang.js",
056                                    "../prototype-base-extensions.js",
057                                    "../prototype-date-extensions.js"
058                    })
059    public class DateTimeField extends AbstractField
060    {
061            /**
062             * The value parameter of a DateField must be a {@link java.util.Date}.
063             */
064            @Parameter(required = true, principal = true)
065            private Date value;
066    
067            /**
068             * The object that will perform input validation (which occurs after translation). The translate binding prefix is
069             * generally used to provide this object in a declarative fashion.
070             */
071            @Parameter(defaultPrefix = BindingConstants.VALIDATE)
072            @SuppressWarnings("unchecked")
073            private FieldValidator<Object> validate;
074    
075            @Parameter(defaultPrefix = BindingConstants.ASSET, value = "datetimefield/calendar.png")
076            private Asset icon;
077    
078            /**
079             * the pattern describing the date and time format {@link java.text.SimpleDateFormat}.
080             */
081            @Parameter(defaultPrefix = BindingConstants.LITERAL, value = "MM/dd/yyyy")
082            private String datePattern;
083    
084            /**
085             * a boolean value determining whether to display the date picker. Defaults to true.
086             */
087            @Parameter(defaultPrefix = BindingConstants.PROP, value = "true")
088            private boolean datePicker;
089    
090            /**
091             * a boolean value determining whether to display the time picker. Defaults to false.
092             */
093            @Parameter(defaultPrefix = BindingConstants.PROP, value = "false")
094            private boolean timePicker;
095    
096            /**
097             * a boolean value determining whether to display the time picker next to the date picker (true) or under it (false, default).
098             */
099            @Parameter(defaultPrefix = BindingConstants.PROP, value = "false")
100            private boolean timePickerAdjacent;
101    
102            /**
103             * a boolean value determining whether to display the time in AM/PM or 24 hour notation. Defaults to false.
104             */
105            @Parameter(defaultPrefix = BindingConstants.PROP, value = "false")
106            private boolean use24hrs;
107    
108            /**
109             * a named javascript function, that executed after the date selected by the picker.
110             * there should one function parameter that holds the input dom element.
111             * This funtion should returns true or false.
112             */
113            @Parameter(defaultPrefix = BindingConstants.LITERAL, required = false)
114            private String afterUpdateElement;
115    
116            /**
117             * Specify whether or not date/time parsing is to be lenient.
118             * With lenient parsing, the parser may use heuristics to interpret inputs that do not precisely match this object's format.
119             * With strict parsing, inputs must match this object's format.
120             */
121            @Parameter(defaultPrefix = BindingConstants.PROP, value = "true")
122            private boolean lenient;
123    
124            @Environmental
125            private JavaScriptSupport javascriptSupport;
126    
127            @Environmental
128            private ValidationTracker tracker;
129    
130            @Inject
131            private ComponentResources resources;
132    
133            @Inject
134            private Messages messages;
135    
136            @Inject
137            private Request request;
138    
139            @Inject
140            private Locale locale;
141    
142            @Inject
143            private FieldValidatorDefaultSource fieldValidatorDefaultSource;
144    
145            @Inject
146            private FieldValidationSupport fieldValidationSupport;
147    
148            @Inject
149            private ComponentDefaultProvider defaultProvider;
150    
151            @Inject
152            @Path("datetimefield/clock.png")
153            private Asset clockAsset;
154    
155            /**
156             * For output, format nicely and unambiguously as four digits.
157             */
158            private DateFormat outputFormat;
159    
160            /**
161             * When the user types a value, they may only type two digits for the year; SimpleDateFormat will do something
162             * reasonable.  If they use the popup, it will be unambiguously 4 digits.
163             */
164            private DateFormat inputFormat;
165    
166            /**
167             * The default value is a property of the container whose name matches the component's id. May return null if the
168             * container does not have a matching property.
169             */
170            final Binding defaultValue()
171            {
172                    return defaultProvider.defaultBinding("value", resources);
173            }
174    
175            /**
176             * Computes a default value for the "validate" parameter using {@link org.apache.tapestry5.services.ComponentDefaultProvider}.
177             */
178            final Binding defaultValidate()
179            {
180                    return defaultProvider.defaultValidatorBinding("value", resources);
181            }
182    
183            /**
184             * Tapestry render phase method.
185             * Initialize temporary instance variables here.
186             */
187            void setupRender()
188            {
189                    outputFormat = new SimpleDateFormat(datePattern, locale);
190            }
191    
192    
193            void beginRender(MarkupWriter writer)
194            {
195                    Asset componentIcon;
196                    String value = tracker.getInput(this);
197    
198                    if (value == null) value = formatCurrentValue();
199    
200                    String clientId = getClientId();
201    
202                    writer.element("input",
203    
204                                               "type", "text",
205    
206                                               "class", "datepicker",
207    
208                                               "name", getControlName(),
209    
210                                               "id", clientId,
211    
212                                               "value", value);
213    
214                    writeDisabled(writer);
215    
216                    validate.render(writer);
217    
218                    resources.renderInformalParameters(writer);
219    
220                    decorateInsideField();
221    
222                    writer.end();
223    
224                    // The setup parameters passed to Calendar.setup():
225    
226                    JSONObject setup = new JSONObject();
227    
228                    if (!datePicker && !timePicker)
229                            throw new RuntimeException("both date- and timePicker set to false, that is senseless!");
230    
231                    if (!datePicker && timePicker)
232                            componentIcon = clockAsset;
233                    else
234                            componentIcon = icon;
235    
236                    setup.put("icon", componentIcon.toClientURL());
237                    setup.put("datePicker", datePicker);
238                    setup.put("timePicker", timePicker);
239                    setup.put("timePickerAdjacent", timePickerAdjacent);
240                    setup.put("use24hrs", use24hrs);
241                    setup.put("locale", locale.toString());
242    
243                    if (afterUpdateElement != null)
244                            setup.put("afterUpdateElement", afterUpdateElement);
245    
246                    if (datePicker && timePicker)
247                            setup.put("dateTimeFormat", datePattern);
248                    else if (datePicker)
249                            setup.put("dateFormat", datePattern);
250                    else
251                            setup.put("timeFormat", datePattern);
252    
253                    javascriptSupport.addScript("new Control.DatePicker('%s', %s);", getClientId(), setup);
254            }
255    
256            private void writeDisabled(MarkupWriter writer)
257            {
258                    if (isDisabled()) writer.attributes("disabled", "disabled");
259            }
260    
261    
262            private String formatCurrentValue()
263            {
264                    if (value == null) return "";
265    
266                    return outputFormat.format(value);
267            }
268    
269            @Override
270            protected void processSubmission(String elementName)
271            {
272                    String value = request.getParameter(elementName);
273    
274                    tracker.recordInput(this, value);
275    
276                    Date parsedValue = null;
277    
278                    try
279                    {
280                            if (InternalUtils.isNonBlank(value))
281                            {
282                                    inputFormat = new SimpleDateFormat(datePattern, locale);
283                                    inputFormat.setLenient(lenient);
284                                    parsedValue = inputFormat.parse(value);
285                            }
286    
287                    }
288                    catch (ParseException ex)
289                    {
290                            tracker.recordError(this, messages.format("date.not.parseable", value));
291                            return;
292                    }
293    
294                    try
295                    {
296                            fieldValidationSupport.validate(parsedValue, resources, validate);
297    
298                            this.value = parsedValue;
299                    }
300                    catch (ValidationException ex)
301                    {
302                            tracker.recordError(this, ex.getMessage());
303                    }
304            }
305    
306            void injectResources(ComponentResources resources)
307            {
308                    this.resources = resources;
309            }
310    
311            void injectMessages(Messages messages)
312            {
313                    this.messages = messages;
314            }
315    
316            @Override
317            public boolean isRequired()
318            {
319                    return validate.isRequired();
320            }
321    }