1 /*
2 * $Id: DateTimeConverter.java,v 1.34.4.1 2007/09/26 19:02:55 rlubke Exp $
3 */
4
5 /*
6 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
7 *
8 * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
9 *
10 * The contents of this file are subject to the terms of either the GNU
11 * General Public License Version 2 only ("GPL") or the Common Development
12 * and Distribution License("CDDL") (collectively, the "License"). You
13 * may not use this file except in compliance with the License. You can obtain
14 * a copy of the License at https://glassfish.dev.java.net/public/CDDL+GPL.html
15 * or glassfish/bootstrap/legal/LICENSE.txt. See the License for the specific
16 * language governing permissions and limitations under the License.
17 *
18 * When distributing the software, include this License Header Notice in each
19 * file and include the License file at glassfish/bootstrap/legal/LICENSE.txt.
20 * Sun designates this particular file as subject to the "Classpath" exception
21 * as provided by Sun in the GPL Version 2 section of the License file that
22 * accompanied this code. If applicable, add the following below the License
23 * Header, with the fields enclosed by brackets [] replaced by your own
24 * identifying information: "Portions Copyrighted [year]
25 * [name of copyright owner]"
26 *
27 * Contributor(s):
28 *
29 * If you wish your version of this file to be governed by only the CDDL or
30 * only the GPL Version 2, indicate your decision by adding "[Contributor]
31 * elects to include this software in this distribution under the [CDDL or GPL
32 * Version 2] license." If you don't indicate a single choice of license, a
33 * recipient has the option to distribute your version of this file under
34 * either the CDDL, the GPL Version 2 or to extend the choice of license to
35 * its licensees as provided above. However, if you add GPL Version 2 code
36 * and therefore, elected the GPL Version 2 license, then the option applies
37 * only if the new code is made subject to such option by the copyright
38 * holder.
39 */
40
41 package javax.faces.convert;
42
43
44 import javax.faces.component.StateHolder;
45 import javax.faces.component.UIComponent;
46 import javax.faces.context.FacesContext;
47 import java.text.DateFormat;
48 import java.text.ParseException;
49 import java.text.SimpleDateFormat;
50 import java.util.Date;
51 import java.util.Locale;
52 import java.util.TimeZone;
53
54
55 /**
56 * <p>{@link Converter} implementation for <code>java.util.Date</code>
57 * values.</p>
58 * <p/>
59 * <p>The <code>getAsObject()</code> method parses a String into a
60 * <code>java.util.Date</code>, according to the following algorithm:</p>
61 * <ul>
62 * <li>If the specified String is null, return
63 * a <code>null</code>. Otherwise, trim leading and trailing
64 * whitespace before proceeding.</li>
65 * <li>If the specified String - after trimming - has a zero length,
66 * return <code>null</code>.</li>
67 * <li>If the <code>locale</code> property is not null,
68 * use that <code>Locale</code> for managing parsing. Otherwise, use the
69 * <code>Locale</code> from the <code>UIViewRoot</code>.</li>
70 * <li>If a <code>pattern</code> has been specified, its syntax must conform
71 * the rules specified by <code>java.text.SimpleDateFormat</code>. Such
72 * a pattern will be used to parse, and the <code>type</code>,
73 * <code>dateStyle</code>, and <code>timeStyle</code> properties
74 * will be ignored.</li>
75 * <li>If a <code>pattern</code> has not been specified, parsing will be based
76 * on the <code>type</code> property, which expects a date value, a time
77 * value, or both. Any date and time values included will be parsed in
78 * accordance to the styles specified by <code>dateStyle</code> and
79 * <code>timeStyle</code>, respectively.</li>
80 * <li>If a <code>timezone</code> has been specified, it must be passed
81 * to the underlying <code>DateFormat</code> instance. Otherwise
82 * the "GMT" timezone is used.</li>
83 * <li>In all cases, parsing must be non-lenient; the given string must
84 * strictly adhere to the parsing format.</li>
85 * </ul>
86 * <p/>
87 * <p>The <code>getAsString()</code> method expects a value of type
88 * <code>java.util.Date</code> (or a subclass), and creates a formatted
89 * String according to the following algorithm:</p>
90 * <ul>
91 * <li>If the specified value is null, return a zero-length String.</li>
92 * <li>If the specified value is a String, return it unmodified.</li>
93 * <li>If the <code>locale</code> property is not null,
94 * use that <code>Locale</code> for managing formatting. Otherwise, use the
95 * <code>Locale</code> from the <code>UIViewRoot</code>.</li>
96 * <li>If a <code>timezone</code> has been specified, it must be passed
97 * to the underlying <code>DateFormat</code> instance. Otherwise
98 * the "GMT" timezone is used.</li>
99 * <li>If a <code>pattern</code> has been specified, its syntax must conform
100 * the rules specified by <code>java.text.SimpleDateFormat</code>. Such
101 * a pattern will be used to format, and the <code>type</code>,
102 * <code>dateStyle</code>, and <code>timeStyle</code> properties
103 * will be ignored.</li>
104 * <li>If a <code>pattern</code> has not been specified, formatting will be
105 * based on the <code>type</code> property, which includes a date value,
106 * a time value, or both into the formatted String. Any date and time
107 * values included will be formatted in accordance to the styles specified
108 * by <code>dateStyle</code> and <code>timeStyle</code>, respectively.</li>
109 * </ul>
110 */
111
112 public class DateTimeConverter implements Converter, StateHolder {
113
114 // ------------------------------------------------------ Manifest Constants
115
116
117 /**
118 * <p>The standard converter id for this converter.</p>
119 */
120 public static final String CONVERTER_ID = "javax.faces.DateTime";
121
122 /**
123 * <p>The message identifier of the {@link javax.faces.application.FacesMessage} to be created if
124 * the conversion to <code>Date</code> fails. The message format
125 * string for this message may optionally include the following
126 * placeholders:
127 * <ul>
128 * <li><code>{0}</code> replaced by the unconverted value.</li>
129 * <li><code>{1}</code> replaced by an example value.</li>
130 * <li><code>{2}</code> replaced by a <code>String</code> whose value
131 * is the label of the input component that produced this message.</li>
132 * </ul></p>
133 */
134 public static final String DATE_ID =
135 "javax.faces.converter.DateTimeConverter.DATE";
136
137 /**
138 * <p>The message identifier of the {@link javax.faces.application.FacesMessage} to be created if
139 * the conversion to <code>Time</code> fails. The message format
140 * string for this message may optionally include the following
141 * placeholders:
142 * <ul>
143 * <li><code>{0}</code> replaced by the unconverted value.</li>
144 * <li><code>{1}</code> replaced by an example value.</li>
145 * <li><code>{2}</code> replaced by a <code>String</code> whose value
146 * is the label of the input component that produced this message.</li>
147 * </ul></p>
148 */
149 public static final String TIME_ID =
150 "javax.faces.converter.DateTimeConverter.TIME";
151
152 /**
153 * <p>The message identifier of the {@link javax.faces.application.FacesMessage} to be created if
154 * the conversion to <code>DateTime</code> fails. The message format
155 * string for this message may optionally include the following
156 * placeholders:
157 * <ul>
158 * <li><code>{0}</code> replaced by the unconverted value.</li>
159 * <li><code>{1}</code> replaced by an example value.</li>
160 * <li><code>{2}</code> replaced by a <code>String</code> whose value
161 * is the label of the input component that produced this message.</li>
162 * </ul></p>
163 */
164 public static final String DATETIME_ID =
165 "javax.faces.converter.DateTimeConverter.DATETIME";
166
167 /**
168 * <p>The message identifier of the {@link javax.faces.application.FacesMessage} to be created if
169 * the conversion of the <code>DateTime</code> value to
170 * <code>String</code> fails. The message format string for this message
171 * may optionally include the following placeholders:
172 * <ul>
173 * <li><code>{0}</code> relaced by the unconverted value.</li>
174 * <li><code>{1}</code> replaced by a <code>String</code> whose value
175 * is the label of the input component that produced this message.</li>
176 * </ul></p>
177 */
178 public static final String STRING_ID =
179 "javax.faces.converter.STRING";
180
181
182 private static final TimeZone DEFAULT_TIME_ZONE = TimeZone.getTimeZone("GMT");
183
184 // ------------------------------------------------------ Instance Variables
185
186
187 private String dateStyle = "default";
188 private Locale locale = null;
189 private String pattern = null;
190 private String timeStyle = "default";
191 private TimeZone timeZone = DEFAULT_TIME_ZONE;
192 private String type = "date";
193
194 // -------------------------------------------------------------- Properties
195
196
197 /**
198 * <p>Return the style to be used to format or parse dates. If not set,
199 * the default value, <code>default<code>, is returned.</p>
200 */
201 public String getDateStyle() {
202
203 return (this.dateStyle);
204
205 }
206
207
208 /**
209 * <p>Set the style to be used to format or parse dates. Valid values
210 * are <code>default</code>, <code>short</code>, <code>medium</code>,
211 * <code>long</code>, and <code>full</code>.
212 * An invalid value will cause a {@link ConverterException} when
213 * <code>getAsObject()</code> or <code>getAsString()</code> is called.</p>
214 *
215 * @param dateStyle The new style code
216 */
217 public void setDateStyle(String dateStyle) {
218
219 this.dateStyle = dateStyle;
220
221 }
222
223
224 /**
225 * <p>Return the <code>Locale</code> to be used when parsing or formatting
226 * dates and times. If not explicitly set, the <code>Locale</code> stored
227 * in the {@link javax.faces.component.UIViewRoot} for the current
228 * request is returned.</p>
229 */
230 public Locale getLocale() {
231
232 if (this.locale == null) {
233 this.locale =
234 getLocale(FacesContext.getCurrentInstance());
235 }
236 return (this.locale);
237
238 }
239
240
241 /**
242 * <p>Set the <code>Locale</code> to be used when parsing or formatting
243 * dates and times. If set to <code>null</code>, the <code>Locale</code>
244 * stored in the {@link javax.faces.component.UIViewRoot} for the current
245 * request will be utilized.</p>
246 *
247 * @param locale The new <code>Locale</code> (or <code>null</code>)
248 */
249 public void setLocale(Locale locale) {
250
251 this.locale = locale;
252
253 }
254
255
256 /**
257 * <p>Return the format pattern to be used when formatting and
258 * parsing dates and times.</p>
259 */
260 public String getPattern() {
261
262 return (this.pattern);
263
264 }
265
266
267 /**
268 * <p>Set the format pattern to be used when formatting and parsing
269 * dates and times. Valid values are those supported by
270 * <code>java.text.SimpleDateFormat</code>.
271 * An invalid value will cause a {@link ConverterException} when
272 * <code>getAsObject()</code> or <code>getAsString()</code> is called.</p>
273 *
274 * @param pattern The new format pattern
275 */
276 public void setPattern(String pattern) {
277
278 this.pattern = pattern;
279
280 }
281
282
283 /**
284 * <p>Return the style to be used to format or parse times. If not set,
285 * the default value, <code>default</code>, is returned.</p>
286 */
287 public String getTimeStyle() {
288
289 return (this.timeStyle);
290
291 }
292
293
294 /**
295 * <p>Set the style to be used to format or parse times. Valid values
296 * are <code>default</code>, <code>short</code>, <code>medium</code>,
297 * <code>long</code>, and <code>full</code>.
298 * An invalid value will cause a {@link ConverterException} when
299 * <code>getAsObject()</code> or <code>getAsString()</code> is called.</p>
300 *
301 * @param timeStyle The new style code
302 */
303 public void setTimeStyle(String timeStyle) {
304
305 this.timeStyle = timeStyle;
306
307 }
308
309
310 /**
311 * <p>Return the <code>TimeZone</code> used to interpret a time value.
312 * If not explicitly set, the default time zone of <code>GMT</code>
313 * returned.</p>
314 */
315 public TimeZone getTimeZone() {
316
317 return (this.timeZone);
318
319 }
320
321
322 /**
323 * <p>Set the <code>TimeZone</code> used to interpret a time value.</p>
324 *
325 * @param timeZone The new time zone
326 */
327 public void setTimeZone(TimeZone timeZone) {
328
329 this.timeZone = timeZone;
330
331 }
332
333
334 /**
335 * <p>Return the type of value to be formatted or parsed.
336 * If not explicitly set, the default type, <code>date</code>
337 * is returned.</p>
338 */
339 public String getType() {
340
341 return (this.type);
342
343 }
344
345
346 /**
347 * <p>Set the type of value to be formatted or parsed.
348 * Valid values are <code>both</code>, <code>date</code>, or
349 * <code>time</code>.
350 * An invalid value will cause a {@link ConverterException} when
351 * <code>getAsObject()</code> or <code>getAsString()</code> is called.</p>
352 *
353 * @param type The new date style
354 */
355 public void setType(String type) {
356
357 this.type = type;
358
359 }
360
361 // ------------------------------------------------------- Converter Methods
362
363 /**
364 * @throws ConverterException {@inheritDoc}
365 * @throws NullPointerException {@inheritDoc}
366 */
367 public Object getAsObject(FacesContext context, UIComponent component,
368 String value) {
369
370 if (context == null || component == null) {
371 throw new NullPointerException();
372 }
373
374 Object returnValue = null;
375 DateFormat parser = null;
376
377 try {
378
379 // If the specified value is null or zero-length, return null
380 if (value == null) {
381 return (null);
382 }
383 value = value.trim();
384 if (value.length() < 1) {
385 return (null);
386 }
387
388 // Identify the Locale to use for parsing
389 Locale locale = getLocale(context);
390
391 // Create and configure the parser to be used
392 parser = getDateFormat(locale);
393 if (null != timeZone) {
394 parser.setTimeZone(timeZone);
395 }
396
397 // Perform the requested parsing
398 returnValue = parser.parse(value);
399 } catch (ParseException e) {
400 if ("date".equals(type)) {
401 throw new ConverterException(MessageFactory.getMessage(
402 context, DATE_ID, value,
403 parser.format(new Date(System.currentTimeMillis())),
404 MessageFactory.getLabel(context, component)));
405 } else if ("time".equals(type)) {
406 throw new ConverterException(MessageFactory.getMessage(
407 context, TIME_ID, value,
408 parser.format(new Date(System.currentTimeMillis())),
409 MessageFactory.getLabel(context, component)));
410 } else if ("both".equals(type)) {
411 throw new ConverterException(MessageFactory.getMessage(
412 context, DATETIME_ID, value,
413 parser.format(new Date(System.currentTimeMillis())),
414 MessageFactory.getLabel(context, component)));
415 }
416 } catch (ConverterException e) {
417 throw e;
418 } catch (Exception e) {
419 throw new ConverterException(e);
420 }
421 return returnValue;
422 }
423
424 /**
425 * @throws ConverterException {@inheritDoc}
426 * @throws NullPointerException {@inheritDoc}
427 */
428 public String getAsString(FacesContext context, UIComponent component,
429 Object value) {
430
431 if (context == null || component == null) {
432 throw new NullPointerException();
433 }
434
435 try {
436
437 // If the specified value is null, return a zero-length String
438 if (value == null) {
439 return "";
440 }
441
442 // If the incoming value is still a string, play nice
443 // and return the value unmodified
444 if (value instanceof String) {
445 return (String) value;
446 }
447
448 // Identify the Locale to use for formatting
449 Locale locale = getLocale(context);
450
451 // Create and configure the formatter to be used
452 DateFormat formatter = getDateFormat(locale);
453 if (null != timeZone) {
454 formatter.setTimeZone(timeZone);
455 }
456
457 // Perform the requested formatting
458 return (formatter.format(value));
459
460 } catch (ConverterException e) {
461 throw new ConverterException(MessageFactory.getMessage(
462 context, STRING_ID, value,
463 MessageFactory.getLabel(context, component)), e);
464 } catch (Exception e) {
465 throw new ConverterException(MessageFactory.getMessage(
466 context, STRING_ID, value,
467 MessageFactory.getLabel(context, component)), e);
468 }
469 }
470
471 // --------------------------------------------------------- Private Methods
472
473
474 /**
475 * <p>Return a <code>DateFormat</code> instance to use for formatting
476 * and parsing in this {@link Converter}.</p>
477 *
478 * @param locale The <code>Locale</code> used to select formatting
479 * and parsing conventions
480 * @throws ConverterException if no instance can be created
481 */
482 private DateFormat getDateFormat(Locale locale) {
483
484 // PENDING(craigmcc) - Implement pooling if needed for performance?
485
486 if (pattern == null && type == null) {
487 throw new IllegalArgumentException("Either pattern or type must" +
488 " be specified.");
489 }
490
491 DateFormat df;
492 if (pattern != null) {
493 df = new SimpleDateFormat(pattern, locale);
494 } else if (type.equals("both")) {
495 df = DateFormat.getDateTimeInstance
496 (getStyle(dateStyle), getStyle(timeStyle), locale);
497 } else if (type.equals("date")) {
498 df = DateFormat.getDateInstance(getStyle(dateStyle), locale);
499 } else if (type.equals("time")) {
500 df = DateFormat.getTimeInstance(getStyle(timeStyle), locale);
501 } else {
502 // PENDING(craigmcc) - i18n
503 throw new IllegalArgumentException("Invalid type: " + type);
504 }
505 df.setLenient(false);
506 return (df);
507
508 }
509
510
511 /**
512 * <p>Return the <code>Locale</code> we will use for localizing our
513 * formatting and parsing processing.</p>
514 *
515 * @param context The {@link FacesContext} for the current request
516 */
517 private Locale getLocale(FacesContext context) {
518
519 // PENDING(craigmcc) - JSTL localization context?
520 Locale locale = this.locale;
521 if (locale == null) {
522 locale = context.getViewRoot().getLocale();
523 }
524 return (locale);
525
526 }
527
528
529 /**
530 * <p>Return the style constant for the specified style name.</p>
531 *
532 * @param name Name of the style for which to return a constant
533 * @throws ConverterException if the style name is not valid
534 */
535 private static int getStyle(String name) {
536
537 if ("default".equals(name)) {
538 return (DateFormat.DEFAULT);
539 } else if ("short".equals(name)) {
540 return (DateFormat.SHORT);
541 } else if ("medium".equals(name)) {
542 return (DateFormat.MEDIUM);
543 } else if ("long".equals(name)) {
544 return (DateFormat.LONG);
545 } else if ("full".equals(name)) {
546 return (DateFormat.FULL);
547 } else {
548 // PENDING(craigmcc) - i18n
549 throw new ConverterException("Invalid style '" + name + '\'');
550 }
551
552 }
553
554 // ----------------------------------------------------- StateHolder Methods
555
556
557 public Object saveState(FacesContext context) {
558
559 Object values[] = new Object[6];
560 values[0] = dateStyle;
561 values[1] = locale;
562 values[2] = pattern;
563 values[3] = timeStyle;
564 values[4] = timeZone;
565 values[5] = type;
566 return (values);
567
568 }
569
570
571 public void restoreState(FacesContext context, Object state) {
572
573 Object values[] = (Object[]) state;
574 dateStyle = (String) values[0];
575 locale = (Locale) values[1];
576 pattern = (String) values[2];
577 timeStyle = (String) values[3];
578 timeZone = (TimeZone) values[4];
579 type = (String) values[5];
580
581 }
582
583
584 private boolean transientFlag = false;
585
586
587 public boolean isTransient() {
588 return (transientFlag);
589 }
590
591
592 public void setTransient(boolean transientFlag) {
593 this.transientFlag = transientFlag;
594 }
595
596 }