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 }