001// ***************************************************************************************************************************
002// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file *
003// * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file        *
004// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance            *
005// * with the License.  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 distributed under the License is distributed on an  *
010// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the        *
011// * specific language governing permissions and limitations under the License.                                              *
012// ***************************************************************************************************************************
013package org.apache.juneau.utils;
014
015import static org.apache.juneau.internal.DateUtils.*;
016import static org.apache.juneau.internal.StringUtils.*;
017
018import java.text.*;
019import java.util.*;
020import java.util.concurrent.*;
021
022import javax.xml.bind.*;
023
024/**
025 * Utility class for converting {@link Calendar} and {@link Date} objects to common serialized forms.
026 */
027public class CalendarUtils {
028
029   private static final TimeZone GMT = TimeZone.getTimeZone("GMT");
030
031   /**
032    * Valid conversion formats.
033    */
034   public static enum Format {
035
036      /**
037       * Transform to ISO8601 date-time-local strings.
038       *
039       * <h5 class='section'>Example Output:</h5>
040       * <ul>
041       *    <li><js>"2001-07-04T15:30:45"</js>
042       * </ul>
043       *
044       * <h5 class='topic'>Example input:</h5>
045       * <ul>
046       *    <li><js>"2001-07-04T15:30:45"</js>
047       *    <li><js>"2001-07-04T15:30:45.1"</js>
048       *    <li><js>"2001-07-04T15:30"</js>
049       *    <li><js>"2001-07-04"</js>
050       *    <li><js>"2001-07"</js>
051       *    <li><js>"2001"</js>
052       * </ul>
053       */
054      ISO8601_DTL,
055
056      /**
057       * Transform to ISO8601 date-time strings.
058       *
059       * <h5 class='section'>Example Output:</h5>
060       * <ul>
061       *    <li><js>"2001-07-04T15:30:45-05:00"</js>
062       *    <li><js>"2001-07-04T15:30:45Z"</js>
063       * </ul>
064       *
065       * <h5 class='topic'>Example input:</h5>
066       * <ul>
067       *    <li><js>"2001-07-04T15:30:45-05:00"</js>
068       *    <li><js>"2001-07-04T15:30:45Z"</js>
069       *    <li><js>"2001-07-04T15:30:45.1Z"</js>
070       *    <li><js>"2001-07-04T15:30Z"</js>
071       *    <li><js>"2001-07-04"</js>
072       *    <li><js>"2001-07"</js>
073       *    <li><js>"2001"</js>
074       * </ul>
075       */
076      ISO8601_DT,
077
078      /**
079       * Same as {@link CalendarUtils.Format#ISO8601_DT}, except always serializes in GMT.
080       *
081       * <h5 class='section'>Example Output:</h5>
082       * <js>"2001-07-04T15:30:45Z"</js>
083       */
084      ISO8601_DTZ,
085
086      /**
087       * Same as {@link CalendarUtils.Format#ISO8601_DT} except serializes to millisecond precision.
088       *
089       * <h5 class='section'>Example Output:</h5>
090       * <js>"2001-07-04T15:30:45.123Z"</js>
091       */
092      ISO8601_DTP,
093
094      /**
095       * Same as {@link CalendarUtils.Format#ISO8601_DTZ} except serializes to millisecond precision.
096       *
097       * <h5 class='section'>Example Output:</h5>
098       * <js>"2001-07-04T15:30:45.123"</js>
099       */
100      ISO8601_DTPZ,
101
102      /**
103       * ISO8601 date only.
104       *
105       * <h5 class='section'>Example Output:</h5>
106       * <js>"2001-07-04"</js>
107       */
108      ISO8601_D,
109
110      /**
111       * Transform to {@link String Strings} using the {@code Date.toString()} method.
112       *
113       * <h5 class='section'>Example Output:</h5>
114       * <ul>
115       *    <li><js>"Wed Jul 04 15:30:45 EST 2001"</js>
116       * </ul>
117       */
118      TO_STRING,
119
120      /**
121       * Transform to RFC2822 date-time strings.
122       *
123       * <h5 class='section'>Example Output:</h5>
124       * <ul>
125       *    <li><js>"Sat, 03 Mar 2001 10:11:12 +0000"</js> <jc>// en_US</jc>
126       *    <li><js>"土, 03 3 2001 10:11:12 +0000"</js> <jc>// ja_JP</jc>
127       *    <li><js>"토, 03 3월 2001 10:11:12 +0000"</js> <jc>// ko_KR</jc>
128       * </ul>
129       */
130      RFC2822_DT,
131
132      /**
133       * Same as {@link CalendarUtils.Format#RFC2822_DT}, except always serializes in GMT.
134       *
135       * <h5 class='section'>Example Output:</h5>
136       * <ul>
137       *    <li><js>"Sat, 03 Mar 2001 10:11:12 GMT"</js> <jc>// en_US</jc>
138       *    <li><js>"土, 03 3 2001 10:11:12 GMT"</js> <jc>// ja_JP</jc>
139       *    <li><js>"토, 03 3월 2001 10:11:12 GMT"</js> <jc>// ko_KR</jc>
140       * </ul>
141       */
142      RFC2822_DTZ,
143
144      /**
145       * Transform to RFC2822 date strings.
146       *
147       * <h5 class='section'>Example Output:</h5>
148       * <ul>
149       *    <li><js>"03 Mar 2001"</js> <jc>// en_US</jc>
150       *    <li><js>"03 3 2001"</js> <jc>// ja_JP</jc>
151       *    <li><js>"03 3월 2001"</js> <jc>// ko_KR</jc>
152       * </ul>
153       */
154      RFC2822_D,
155
156      /**
157       * Transform to simple <js>"yyyy/MM/dd HH:mm:ss"</js> date-time strings.
158       *
159       * <h5 class='section'>Example Output:</h5>
160       * <ul>
161       *    <li><js>"2001/03/03 10:11:12"</js>
162       * </ul>
163       */
164      SIMPLE_DT,
165
166      /**
167       * Transform to simple <js>"yyyy/MM/dd"</js> date strings.
168       *
169       * <h5 class='section'>Example Output:</h5>
170       * <ul>
171       *    <li><js>"2001/03/03"</js>
172       * </ul>
173       */
174      SIMPLE_D,
175
176      /**
177       * Transform to simple <js>"HH:mm:ss"</js> time strings.
178       *
179       * <h5 class='section'>Example Output:</h5>
180       * <ul>
181       *    <li><js>"10:11:12"</js>
182       * </ul>
183       */
184      SIMPLE_T,
185
186      /**
187       * Transform to {@link DateFormat#FULL} date strings.
188       *
189       * <h5 class='section'>Example Output:</h5>
190       * <ul>
191       *    <li><js>"Saturday, March 3, 2001"</js> <jc>// en_US</jc>
192       *    <li><js>"2001年3月3日"</js> <jc>// ja_JP</jc>
193       *    <li><js>"2001년 3월 3일 토요일"</js> <jc>// ko_KR</jc>
194       * </ul>
195       */
196      FULL_D,
197
198      /**
199       * Transform to {@link DateFormat#LONG} date strings.
200       *
201       * <h5 class='section'>Example Output:</h5>
202       * <ul>
203       *    <li><js>"March 3, 2001"</js> <jc>// en_US</jc>
204       *    <li><js>"2001/03/03"</js> <jc>// ja_JP</jc>
205       *    <li><js>"2001년 3월 3일 (토)"</js> <jc>// ko_KR</jc>
206       * </ul>
207       */
208      LONG_D,
209
210      /**
211       * Transform to {@link DateFormat#MEDIUM} date strings.
212       *
213       * <h5 class='section'>Example Output:</h5>
214       * <ul>
215       *    <li><js>"Mar 3, 2001"</js> <jc>// en_US</jc>
216       *    <li><js>"2001/03/03"</js> <jc>// ja_JP</jc>
217       *    <li><js>"2001. 3. 3"</js> <jc>// ko_KR</jc>
218       * </ul>
219       */
220      MEDIUM_D,
221
222      /**
223       * Transform to {@link DateFormat#SHORT} date strings.
224       *
225       * <h5 class='section'>Example Output:</h5>
226       * <ul>
227       *    <li><js>"3/3/01"</js> <jc>// en_US</jc>
228       *    <li><js>"01/03/03"</js> <jc>// ja_JP</jc>
229       *    <li><js>"01. 3. 3"</js> <jc>// ko_KR</jc>
230       * </ul>
231       */
232      SHORT_D,
233
234      /**
235       * Transform to {@link DateFormat#FULL} time strings.
236       *
237       * <h5 class='section'>Example Output:</h5>
238       * <ul>
239       *    <li><js>"10:11:12 AM GMT"</js> <jc>// en_US</jc>
240       *    <li><js>"10時11分12秒 GMT"</js> <jc>// ja_JP</jc>
241       *    <li><js>"오전 10시 11분 12초 GMT"</js> <jc>// ko_KR</jc>
242       * </ul>
243       */
244      FULL_T,
245
246      /**
247       * Transform to {@link DateFormat#LONG} time strings.
248       *
249       * <h5 class='section'>Example Output:</h5>
250       * <ul>
251       *    <li><js>"10:11:12 AM GMT"</js> <jc>// en_US</jc>
252       *    <li><js>"10:11:12 GMT"</js> <jc>// ja_JP</jc>
253       *    <li><js>"오전 10시 11분 12초"</js> <jc>// ko_KR</jc>
254       * </ul>
255       */
256      LONG_T,
257
258      /**
259       * Transform to {@link DateFormat#MEDIUM} time strings.
260       *
261       * <h5 class='section'>Example Output:</h5>
262       * <ul>
263       *    <li><js>"10:11:12 AM"</js> <jc>// en_US</jc>
264       *    <li><js>"10:11:12"</js> <jc>// ja_JP</jc>
265       *    <li><js>"오전 10:11:12"</js> <jc>// ko_KR</jc>
266       * </ul>
267       */
268      MEDIUM_T,
269
270      /**
271       * Transform to {@link DateFormat#SHORT} time strings.
272       *
273       * <h5 class='section'>Example Output:</h5>
274       * <ul>
275       *    <li><js>"10:11 AM"</js> <jc>// en_US</jc>
276       *    <li><js>"10:11 AM"</js> <jc>// ja_JP</jc>
277       *    <li><js>"오전 10:11"</js> <jc>// ko_KR</jc>
278       * </ul>
279       */
280      SHORT_T,
281
282      /**
283       * Transform to {@link DateFormat#FULL} date-time strings.
284       *
285       * <h5 class='section'>Example Output:</h5>
286       * <ul>
287       *    <li><js>"Saturday, March 3, 2001 10:11:12 AM GMT"</js> <jc>// en_US</jc>
288       *    <li><js>"2001年3月3日 10時11分12秒 GMT"</js> <jc>// ja_JP</jc>
289       *    <li><js>"2001년 3월 3일 토요일 오전 10시 11분 12초 GMT"</js> <jc>// ko_KR</jc>
290       * </ul>
291       */
292      FULL_DT,
293
294      /**
295       * Transform to {@link DateFormat#LONG} date-time strings.
296       *
297       * <h5 class='section'>Example Output:</h5>
298       * <ul>
299       *    <li><js>"March 3, 2001 10:11:12 AM GMT"</js> <jc>// en_US</jc>
300       *    <li><js>"2001/03/03 10:11:12 GMT"</js> <jc>// ja_JP</jc>
301       *    <li><js>"2001년 3월 3일 (토) 오전 10시 11분 12초"</js> <jc>// ko_KR</jc>
302       * </ul>
303       */
304      LONG_DT,
305
306      /**
307       * Transform to {@link DateFormat#MEDIUM} date-time strings.
308       *
309       * <h5 class='section'>Example Output:</h5>
310       * <ul>
311       *    <li><js>"Mar 3, 2001 10:11:12 AM"</js> <jc>// en_US</jc>
312       *    <li><js>"2001/03/03 10:11:12"</js> <jc>// ja_JP</jc>
313       *    <li><js>"2001. 3. 3 오전 10:11:12"</js> <jc>// ko_KR</jc>
314       * </ul>
315       */
316      MEDIUM_DT,
317
318      /**
319       * Transform to {@link DateFormat#SHORT} date-time strings.
320       *
321       * <h5 class='section'>Example Output:</h5>
322       * <ul>
323       *    <li><js>"3/3/01 10:11 AM"</js> <jc>// en_US</jc>
324       *    <li><js>"01/03/03 10:11"</js> <jc>// ja_JP</jc>
325       *    <li><js>"01. 3. 3 오전 10:11"</js> <jc>// ko_KR</jc>
326       * </ul>
327       */
328      SHORT_DT
329   }
330
331   private static ThreadLocal<Map<DateFormatKey,DateFormat>> patternCache = new ThreadLocal<>();
332
333   static class DateFormatKey {
334      final CalendarUtils.Format format;
335      final Locale locale;
336      final TimeZone timeZone;
337      final int hashCode;
338
339      DateFormatKey(CalendarUtils.Format format, Locale locale, TimeZone timeZone) {
340         this.format = format;
341         this.locale = locale;
342         this.timeZone = timeZone;
343         this.hashCode = format.hashCode() + locale.hashCode() + timeZone.hashCode();
344      }
345
346      @Override
347      public int hashCode() {
348         return hashCode;
349      }
350
351      @Override
352      public boolean equals(Object o) {
353         if (o == null)
354            return false;
355         DateFormatKey key = (DateFormatKey)o;
356         return format.equals(key.format) && locale.equals(key.locale) && timeZone.equals(key.timeZone);
357      }
358   }
359
360
361   private static DateFormat getFormat(CalendarUtils.Format format, Locale locale, TimeZone timeZone) {
362
363      if (locale == null)
364         locale = Locale.getDefault();
365
366      if (timeZone == null)
367         timeZone = TimeZone.getDefault();
368
369      DateFormatKey key = new DateFormatKey(format, locale, timeZone);
370
371      Map<DateFormatKey,DateFormat> m1 = patternCache.get();
372      if (m1 == null) {
373         m1 = new ConcurrentHashMap<>();
374         patternCache.set(m1);
375      }
376
377      DateFormat df = m1.get(key);
378
379      if (df == null) {
380         String p = null;
381         switch (format) {
382            case ISO8601_DTL: p = "yyyy-MM-dd'T'HH:mm:ss"; break;
383            case ISO8601_D: p = "yyyy-MM-dd"; break;
384            case TO_STRING: p = "EEE MMM dd HH:mm:ss zzz yyyy"; break;
385            case RFC2822_DT: p = "EEE, dd MMM yyyy HH:mm:ss Z"; break;
386            case RFC2822_DTZ: p = "EEE, dd MMM yyyy HH:mm:ss 'GMT'"; break;
387            case RFC2822_D: p = "dd MMM yyyy"; break;
388            case SIMPLE_DT: p = "yyyy/MM/dd HH:mm:ss"; break;
389            case SIMPLE_D: p = "yyyy/MM/dd"; break;
390            case SIMPLE_T: p = "HH:mm:ss"; break;
391            case FULL_D: df = DateFormat.getDateInstance(DateFormat.FULL, locale); break;
392            case LONG_D: df = DateFormat.getDateInstance(DateFormat.LONG, locale); break;
393            case MEDIUM_D: df = DateFormat.getDateInstance(DateFormat.MEDIUM, locale); break;
394            case SHORT_D: df = DateFormat.getDateInstance(DateFormat.SHORT, locale); break;
395            case FULL_T: df = DateFormat.getTimeInstance(DateFormat.FULL, locale); break;
396            case LONG_T: df = DateFormat.getTimeInstance(DateFormat.LONG, locale); break;
397            case MEDIUM_T: df = DateFormat.getTimeInstance(DateFormat.MEDIUM, locale); break;
398            case SHORT_T: df = DateFormat.getTimeInstance(DateFormat.SHORT, locale); break;
399            case FULL_DT: df = DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL, locale); break;
400            case LONG_DT: df = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, locale); break;
401            case MEDIUM_DT: df = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM, locale); break;
402            case SHORT_DT: df = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, locale); break;
403            default: return null;
404         }
405         if (p != null) {
406            df = new SimpleDateFormat(p, locale);
407         }
408         if (df != null)
409            df.setTimeZone(timeZone);
410         m1.put(key, df);
411      }
412
413      return df;
414   }
415
416   /**
417    * Converts the specified calendar to a string of the specified format.
418    *
419    * @param c The calendar to serialize.
420    * @param format The date format.
421    * @param locale The locale to use.  If <jk>null</jk>, uses {@link Locale#getDefault()}.
422    * @param timeZone The time zone to use.  If <jk>null</jk>, uses {@link TimeZone#getDefault()}.
423    * @return The serialized date, or <jk>null</jk> if the calendar was <jk>null</jk>.
424    * @throws Exception
425    */
426   public static final String serialize(Calendar c, CalendarUtils.Format format, Locale locale, TimeZone timeZone) throws Exception {
427      if (c == null)
428         return null;
429      if (timeZone == null)
430         timeZone = c.getTimeZone();
431      switch(format) {
432         case ISO8601_DTL:
433         case ISO8601_D:
434         case RFC2822_D:
435         case RFC2822_DT:
436         case TO_STRING:
437         case FULL_D:
438         case FULL_DT:
439         case FULL_T:
440         case LONG_D:
441         case LONG_DT:
442         case LONG_T:
443         case MEDIUM_D:
444         case MEDIUM_DT:
445         case MEDIUM_T:
446         case SHORT_D:
447         case SHORT_DT:
448         case SHORT_T:
449         case SIMPLE_D:
450         case SIMPLE_DT:
451         case SIMPLE_T:
452            return serializeFromDateFormat(c.getTime(), format, locale, timeZone);
453         case ISO8601_DT:
454            return DatatypeConverter.printDateTime(setTimeZone(c, timeZone));
455         case ISO8601_DTP:
456            String s = DatatypeConverter.printDateTime(setTimeZone(c, timeZone));
457            return String.format("%s.%03d%s", s.substring(0, 19), c.get(Calendar.MILLISECOND), s.substring(19));
458         case ISO8601_DTZ:
459            if (c.getTimeZone().getRawOffset() != 0) {
460               Calendar c2 = Calendar.getInstance(GMT);
461               c2.setTime(c.getTime());
462               c = c2;
463            }
464            return DatatypeConverter.printDateTime(c);
465         case ISO8601_DTPZ:
466            if (c.getTimeZone().getRawOffset() != 0) {
467               Calendar c2 = Calendar.getInstance(GMT);
468               c2.setTime(c.getTime());
469               c = c2;
470            }
471            s = DatatypeConverter.printDateTime(c);
472            return String.format("%s.%03d%s", s.substring(0, 19), c.get(Calendar.MILLISECOND), s.substring(19));
473         case RFC2822_DTZ:
474            return serializeFromDateFormat(c.getTime(), format, locale, GMT);
475      default:
476         break;
477      }
478      return null;
479   }
480
481   /**
482    * Converts the specified date to a string of the specified format.
483    *
484    * @param format The date format.
485    * @param d The date to serialize.
486    * @param locale The locale to use.  If <jk>null</jk>, uses {@link Locale#getDefault()}.
487    * @param timeZone The time zone to use.  If <jk>null</jk>, uses {@link TimeZone#getDefault()}.
488    * @return The serialized date, or <jk>null</jk> if the calendar was <jk>null</jk>.
489    * @throws Exception
490    */
491   public static final String serialize(Date d, CalendarUtils.Format format, Locale locale, TimeZone timeZone) throws Exception {
492      if (d == null)
493         return null;
494      if (timeZone == null)
495         timeZone = TimeZone.getDefault();
496      switch(format) {
497         case ISO8601_DTL:
498         case ISO8601_D:
499         case RFC2822_D:
500         case RFC2822_DT:
501         case TO_STRING:
502         case FULL_D:
503         case FULL_DT:
504         case FULL_T:
505         case LONG_D:
506         case LONG_DT:
507         case LONG_T:
508         case MEDIUM_D:
509         case MEDIUM_DT:
510         case MEDIUM_T:
511         case SHORT_D:
512         case SHORT_DT:
513         case SHORT_T:
514         case SIMPLE_D:
515         case SIMPLE_DT:
516         case SIMPLE_T:
517            return serializeFromDateFormat(d, format, locale, timeZone);
518         case ISO8601_DT:
519            Calendar c = new GregorianCalendar();
520            c.setTime(d);
521            c.setTimeZone(timeZone);
522            return DatatypeConverter.printDateTime(c);
523         case ISO8601_DTP:
524            c = new GregorianCalendar();
525            c.setTime(d);
526            c.setTimeZone(timeZone);
527            String s = DatatypeConverter.printDateTime(setTimeZone(c, timeZone));
528            return String.format("%s.%03d%s", s.substring(0, 19), c.get(Calendar.MILLISECOND), s.substring(19));
529         case ISO8601_DTZ:
530            c = new GregorianCalendar();
531            c.setTime(d);
532            c.setTimeZone(GMT);
533            return DatatypeConverter.printDateTime(c);
534         case ISO8601_DTPZ:
535            c = new GregorianCalendar();
536            c.setTime(d);
537            c.setTimeZone(GMT);
538            s = DatatypeConverter.printDateTime(c);
539            return String.format("%s.%03d%s", s.substring(0, 19), c.get(Calendar.MILLISECOND), s.substring(19));
540         case RFC2822_DTZ:
541            return serializeFromDateFormat(d, format, locale, GMT);
542      }
543      return null;
544   }
545
546
547   /**
548    * Converts the specified serialized date back into a {@link Calendar} object.
549    *
550    * @param format The date format.
551    * @param in The serialized date.
552    * @param locale
553    *    The locale to use.
554    *    If <jk>null</jk>, uses {@link Locale#getDefault()}.
555    * @param timeZone
556    *    The timezone to assume if input string doesn't contain timezone info.
557    *    If <jk>null</jk>, uses {@link TimeZone#getDefault()}.
558    * @return The date as a {@link Calendar}, or <jk>null</jk> if the input was <jk>null</jk> or empty.
559    * @throws Exception
560    */
561   public static final Calendar parseCalendar(String in, CalendarUtils.Format format, Locale locale, TimeZone timeZone) throws Exception {
562      if (isEmpty(in))
563         return null;
564      if (timeZone == null)
565         timeZone = TimeZone.getDefault();
566      Date d = null;
567      switch(format) {
568
569         // These use DatatypeConverter to parse the date.
570         case ISO8601_DTL:
571         case ISO8601_DT:
572         case ISO8601_DTZ:
573         case ISO8601_DTP:
574         case ISO8601_DTPZ:
575         case ISO8601_D:
576            return DatatypeConverter.parseDateTime(toValidISO8601DT(in));
577
578         // These don't specify timezones, so we have to assume the timezone is whatever is specified.
579         case RFC2822_D:
580         case SIMPLE_DT:
581         case SIMPLE_D:
582         case SIMPLE_T:
583         case FULL_D:
584         case LONG_D:
585         case MEDIUM_D:
586         case SHORT_D:
587         case MEDIUM_T:
588         case SHORT_T:
589         case MEDIUM_DT:
590         case SHORT_DT:
591            d = getFormat(format, locale, GMT).parse(in);
592            d.setTime(d.getTime() - timeZone.getRawOffset());
593            break;
594
595         // This is always in GMT.
596         case RFC2822_DTZ:
597            DateFormat f  = getFormat(format, locale, GMT);
598            d = f.parse(in);
599            break;
600
601         // These specify timezones in the strings, so we don't use the specified timezone.
602         case TO_STRING:
603         case FULL_DT:
604         case FULL_T:
605         case LONG_DT:
606         case LONG_T:
607         case RFC2822_DT:
608            d = getFormat(format, locale, timeZone).parse(in);
609            break;
610      }
611      if (d == null)
612         return null;
613      Calendar c = new GregorianCalendar();
614      c.setTime(d);
615      c.setTimeZone(timeZone);
616      return c;
617   }
618
619   /**
620    * Converts the specified serialized date back into a {@link Date} object.
621    *
622    * @param format The date format.
623    * @param in The serialized date.
624    * @param locale
625    *    The locale to use.
626    *    If <jk>null</jk>, uses {@link Locale#getDefault()}.
627    * @param timeZone
628    *    The timezone to assume if input string doesn't contain timezone info.
629    *    If <jk>null</jk>, uses {@link TimeZone#getDefault()}.
630    * @return The date as a {@link Date}, or <jk>null</jk> if the input was <jk>null</jk> or empty.
631    * @throws Exception
632    */
633   public static final Date parseDate(String in, CalendarUtils.Format format, Locale locale, TimeZone timeZone) throws Exception {
634      if (isEmpty(in))
635         return null;
636      if (timeZone == null)
637         timeZone = TimeZone.getDefault();
638      switch(format) {
639
640         // These use DatatypeConverter to parse the date.
641         case ISO8601_DTL:
642         case ISO8601_D:
643         case ISO8601_DT:
644         case ISO8601_DTZ:
645         case ISO8601_DTP:
646         case ISO8601_DTPZ:
647            return DatatypeConverter.parseDateTime(toValidISO8601DT(in)).getTime();
648
649         // These don't specify timezones, so we have to assume the timezone is whatever is specified.
650         case FULL_D:
651         case LONG_D:
652         case MEDIUM_D:
653         case MEDIUM_DT:
654         case MEDIUM_T:
655         case RFC2822_D:
656         case SHORT_D:
657         case SHORT_DT:
658         case SHORT_T:
659         case SIMPLE_D:
660         case SIMPLE_DT:
661         case SIMPLE_T:
662            return getFormat(format, locale, timeZone).parse(in);
663
664         // This is always in GMT.
665         case RFC2822_DTZ:
666            Date d = getFormat(format, locale, TimeZone.getDefault()).parse(in);
667            d.setTime(d.getTime() + TimeZone.getDefault().getRawOffset());
668            return d;
669
670         // These specify timezones in the strings, so we don't use the specified timezone.
671         case TO_STRING:
672         case FULL_DT:
673         case FULL_T:
674         case LONG_DT:
675         case LONG_T:
676         case RFC2822_DT:
677            return getFormat(format, locale, timeZone).parse(in);
678
679      }
680      return null;
681   }
682
683   private static String serializeFromDateFormat(Date date, CalendarUtils.Format format, Locale locale, TimeZone timeZone) {
684      DateFormat df = getFormat(format, locale, timeZone);
685      String s = df.format(date);
686      return s;
687   }
688
689   private static Calendar setTimeZone(Calendar c, TimeZone tz) {
690      if (tz != null && ! tz.equals(c.getTimeZone())) {
691         c = (Calendar)c.clone();
692         c.setTimeZone(tz);
693      }
694      return c;
695   }
696}