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