001    //$HeadURL: https://svn.wald.intevation.org/svn/deegree/base/branches/2.3_testing/src/org/deegree/framework/util/DateUtil.java $
002    /*----------------------------------------------------------------------------
003     This file is part of deegree.
004     Copyright (C) 2001-2009 by:
005       Department of Geography, University of Bonn
006       http://www.geographie.uni-bonn.de/deegree/
007     and
008       lat/lon GmbH
009       http://lat-lon.de/
010       
011     Additional copyright notes:
012     Code has partly been taken from JBoss.
013     JBoss, Home of Professional Open Source.
014     Copyright 2008, Red Hat Middleware LLC, and individual contributors
015     as indicated by the @author tags. See the copyright.txt file in the
016     distribution for a full listing of individual contributors.
017    
018     This library is free software; you can redistribute it and/or modify it under
019     the terms of the GNU Lesser General Public License as published by the Free
020     Software Foundation; either version 2.1 of the License, or (at your option)
021     any later version.
022     This library is distributed in the hope that it will be useful, but WITHOUT
023     ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
024     FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
025     details.
026     You should have received a copy of the GNU Lesser General Public License
027     along with this library; if not, write to the Free Software Foundation, Inc.,
028     59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
029    
030     Contact information:
031    
032     lat/lon GmbH
033     Aennchenstr. 19, 53177 Bonn
034     Germany
035    
036     Department of Geography, University of Bonn
037     Prof. Dr. Klaus Greve
038     Postfach 1147, 53001 Bonn
039     Germany
040    
041     e-mail: info@deegree.org
042    ----------------------------------------------------------------------------*/
043    package org.deegree.framework.util;
044    
045    import java.text.DateFormat;
046    import java.text.ParseException;
047    import java.text.SimpleDateFormat;
048    import java.util.Calendar;
049    import java.util.Locale;
050    import java.util.TimeZone;
051    import java.util.regex.Matcher;
052    import java.util.regex.Pattern;
053    
054    /**
055     * Utilities for working with dates.
056     * <p>
057     * Many of the methods that convert dates to and from strings utilize the <a
058     * href="http://en.wikipedia.org/wiki/ISO_8601">ISO 8601:2004</a> standard string format
059     * <code>yyyy-MM-ddTHH:mm:ss.SSSZ</code>, where <blockquote>
060     * 
061     * <pre>
062     * Symbol   Meaning                 Presentation        Example
063     * ------   -------                 ------------        -------
064     * y        year                    (Number)            1996
065     * M        month in year           (Number)            07
066     * d        day in month            (Number)            10
067     * h        hour in am/pm (1&tilde;12)    (Number)            12
068     * H        hour in day (0&tilde;23)      (Number)            0
069     * m        minute in hour          (Number)            30
070     * s        second in minute        (Number)            55
071     * S        millisecond             (Number)            978
072     * Z        time zone               (Number)            -0600
073     * </pre>
074     * 
075     * </blockquote>
076     * </p>
077     * <p>
078     * This class is written to be thread safe. As {@link SimpleDateFormat} is not threadsafe, no shared instances are used.
079     * </p>
080     * 
081     * @author Randall Hauch
082     */
083    public class DateUtil {
084    
085        /**
086         * 
087         */
088        public static final String ISO_8601_2004_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSSZ";
089    
090        /**
091         * 
092         */
093        public static final String ISO_8601_2004_FORMAT_UTC = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";
094    
095        /**
096         * 
097         */
098        public static final String JDBC_TIMESTAMP_FORMAT = "yyyy-MM-dd HH:mm:ss.SSS";
099    
100        //private final static TimeZone GMT = TimeZone.getTimeZone( "GMT" );
101    
102        /**
103         * Parse the date contained in the supplied string and return a UTC Calendar object. The date must follow one of the
104         * standard ISO 8601 formats, of the form <code><i>datepart</i>T<i>timepart</i></code>, where
105         * <code><i>datepart</i></code> is one of the following forms:
106         * <p>
107         * <dl>
108         * <dt>YYYYMMDD</dt>
109         * <dd>The 4-digit year, the 2-digit month (00-12), and the 2-digit day of the month (00-31). The month and day are
110         * optional, but the month is required if the day is given.</dd>
111         * <dt>YYYY-MM-DD</dt>
112         * <dd>The 4-digit year, the 2-digit month (00-12), and the 2-digit day of the month (00-31). The month and day are
113         * optional, but the month is required if the day is given.</dd>
114         * <dt>YYYY-Www-D</dt>
115         * <dd>The 4-digit year followed by 'W', the 2-digit week number (00-53), and the day of the week (1-7). The day of
116         * week number is optional.</dd>
117         * <dt>YYYYWwwD</dt>
118         * <dd>The 4-digit year followed by 'W', the 2-digit week number (00-53), and the day of the week (1-7). The day of
119         * week number is optional.</dd>
120         * <dt>YYYY-DDD</dt>
121         * <dd>The 4-digit year followed by the 3-digit day of the year (000-365)</dd>
122         * <dt>YYYYDDD</dt>
123         * <dd>The 4-digit year followed by the 3-digit day of the year (000-365)</dd>
124         * </dl>
125         * </p>
126         * <p>
127         * The <code><i>timepart</i></code> consists of one of the following forms that contain the 2-digit hour (00-24),
128         * the 2-digit minutes (00-59), the 2-digit seconds (00-59), and the 1-to-3 digit milliseconds. The minutes, seconds
129         * and milliseconds are optional, but any component is required if it is followed by another component (e.g.,
130         * minutes are required if the seconds are given).
131         * <dl>
132         * <dt>hh:mm:ss.SSS</dt>
133         * <dt>hhmmssSSS</dt>
134         * </dl>
135         * </p>
136         * <p>
137         * followed by one of the following time zone definitions:
138         * <dt>Z</dt>
139         * <dd>The uppercase or lowercase 'Z' to denote UTC time</dd>
140         * <dt>&#177;hh:mm</dt>
141         * <dd>The 2-digit hour and the 2-digit minute offset from UTC</dd>
142         * <dt>&#177;hhmm</dt>
143         * <dd>The 2-digit hour and the 2-digit minute offset from UTC</dd>
144         * <dt>&#177;hh</dt>
145         * <dd>The 2-digit hour offset from UTC</dd>
146         * <dt>hh:mm</dt>
147         * <dd>The 2-digit hour and the 2-digit minute offset from UTC</dd>
148         * <dt>hhmm</dt>
149         * <dd>The 2-digit hour and the 2-digit minute offset from UTC</dd>
150         * <dt>hh</dt>
151         * <dd>The 2-digit hour offset from UTC</dd>
152         * </dl>
153         * </p>
154         * 
155         * @param dateString
156         *            the string containing the date to be parsed
157         * @return the parsed date as a {@link Calendar} object. The return value is always in UTC time zone. Conversion
158         *         occurs when necessary.
159         * @throws ParseException
160         *             if there is a problem parsing the string
161         */
162        public static Calendar parseISO8601Date( final String dateString )
163                                throws ParseException {
164            // Example: 2008-02-16T12:30:45.123-0600
165            // Example: 2008-W06-6
166            // Example: 2008-053
167            //
168            // Group Optional Field Description
169            // ----- -------- --------- ------------------------------------------
170            // 1 no 2008 4 digit year as a number
171            // 2 yes 02-16 or W06-6 or 053
172            // 3 yes W06-6
173            // 4 yes 06 2 digit week number (00-59)
174            // 5 yes 6 1 digit day of week as a number (1-7)
175            // 6 yes 02-16
176            // 7 yes 02 2 digit month as a number (00-19)
177            // 8 yes -16
178            // 9 yes 16 2 digit day of month as a number (00-39)
179            // 10 yes 02 2 digit month as a number (00-19)
180            // 11 yes 16 2 digit day of month as a number (00-39)
181            // 12 yes 234 3 digit day of year as a number (000-399)
182            // 13 yes T12:30:45.123-0600
183            // 14 yes 12 2 digit hour as a number (00-29)
184            // 15 yes 30 2 digit minute as a number (00-59)
185            // 16 yes :45.123
186            // 17 yes 45 2 digit second as a number (00-59)
187            // 18 yes .123
188            // 19 yes 123 1, 2 or 3 digit milliseconds as a number (000-999)
189            // 20 yes -0600
190            // 21 yes Z The letter 'Z' if in UTC
191            // 22 yes -06 1 or 2 digit time zone hour offset as a signed number
192            // 23 yes + the plus or minus in the time zone offset
193            // 24 yes 00 1 or 2 digit time zone hour offset as an unsigned number (00-29)
194            // 25 yes 00 1 or 2 digit time zone minute offset as a number (00-59)
195            final String regex = "^(\\d{4})-?(([wW]([012345]\\d)-?([1234567])?)|(([01]\\d)(-([0123]\\d))?)|([01]\\d)([0123]\\d)|([0123]\\d\\d))?(T([012]\\d):?([012345]\\d)?(:?([012345]\\d)(.(\\d{1,3}))?)?)?((Z)|(([+-])(\\d{2})):?(\\d{2})?)?$";
196            final Pattern pattern = Pattern.compile( regex );
197            final Matcher matcher = pattern.matcher( dateString );
198            if ( !matcher.matches() ) {
199                throw new ParseException( "error while parsing iso8601 date: " + dateString, 0 );
200            }
201            String year = matcher.group( 1 );
202            String week = matcher.group( 4 );
203            String dayOfWeek = matcher.group( 5 );
204            String month = matcher.group( 7 );
205            if ( month == null )
206                month = matcher.group( 10 );
207            String dayOfMonth = matcher.group( 9 );
208            if ( dayOfMonth == null )
209                dayOfMonth = matcher.group( 11 );
210            String dayOfYear = matcher.group( 12 );
211            String hourOfDay = matcher.group( 14 );
212            String minutesOfHour = matcher.group( 15 );
213            String seconds = matcher.group( 17 );
214            String milliseconds = matcher.group( 19 );
215            String timeZoneSign = matcher.group( 23 );
216            String timeZoneHour = matcher.group( 24 );
217            String timeZoneMinutes = matcher.group( 25 );
218    
219            boolean localeTime = true;
220            if ( timeZoneHour != null ) {
221                localeTime = false;
222            }
223            if ( matcher.group( 21 ) != null ) {
224                localeTime = false;
225                timeZoneHour = "00";
226                timeZoneMinutes = "00";
227            }
228    
229            // Create the calendar object and start setting the fields ...
230            Calendar calendar;
231            if ( localeTime ) {
232                calendar = Calendar.getInstance();
233            } else {
234                calendar = Calendar.getInstance( TimeZone.getTimeZone( "GMT" ) );
235            }
236            calendar.clear();
237    
238            // And start setting the fields. Note that Integer.parseInt should never fail, since we're checking for null and
239            // the
240            // regular expression should only have digits in these strings!
241            if ( year != null )
242                calendar.set( Calendar.YEAR, Integer.parseInt( year ) );
243            if ( month != null ) {
244                calendar.set( Calendar.MONTH, Integer.parseInt( month ) - 1 ); // month is zero-based!
245                if ( dayOfMonth != null )
246                    calendar.set( Calendar.DAY_OF_MONTH, Integer.parseInt( dayOfMonth ) );
247            } else if ( week != null ) {
248                calendar.set( Calendar.WEEK_OF_YEAR, Integer.parseInt( week ) );
249                if ( dayOfWeek != null )
250                    calendar.set( Calendar.DAY_OF_WEEK, Integer.parseInt( dayOfWeek ) );
251            } else if ( dayOfYear != null ) {
252                calendar.set( Calendar.DAY_OF_YEAR, Integer.parseInt( dayOfYear ) );
253            }
254            if ( hourOfDay != null )
255                calendar.set( Calendar.HOUR_OF_DAY, Integer.parseInt( hourOfDay ) );
256            if ( minutesOfHour != null )
257                calendar.set( Calendar.MINUTE, Integer.parseInt( minutesOfHour ) );
258            if ( seconds != null )
259                calendar.set( Calendar.SECOND, Integer.parseInt( seconds ) );
260            if ( milliseconds != null )
261                calendar.set( Calendar.MILLISECOND, Integer.parseInt( milliseconds ) );
262            if ( timeZoneHour != null ) {
263                int zoneOffsetInMillis = Integer.parseInt( timeZoneHour ) * 60 * 60 * 1000;
264                if ( "-".equals( timeZoneSign ) )
265                    zoneOffsetInMillis *= -1;
266                if ( timeZoneMinutes != null ) {
267                    int minuteOffsetInMillis = Integer.parseInt( timeZoneMinutes ) * 60 * 1000;
268                    if ( zoneOffsetInMillis < 0 ) {
269                        zoneOffsetInMillis -= minuteOffsetInMillis;
270                    } else {
271                        zoneOffsetInMillis += minuteOffsetInMillis;
272                    }
273                }
274                calendar.set( Calendar.ZONE_OFFSET, zoneOffsetInMillis );
275            }
276            // convert to utc time
277            Calendar result = Calendar.getInstance( TimeZone.getTimeZone( "GMT" ) );
278            result.setTime( calendar.getTime() );
279            return result;
280        }
281    
282        /**
283         * Obtain an ISO 8601:2004 string representation of the date given the supplied milliseconds since the epoch.
284         * 
285         * @param date
286         *            the date in calendar form
287         * @return the string in the {@link #ISO_8601_2004_FORMAT standard format}
288         */
289        public static String formatISO8601Date( final Calendar date ) {
290            if ( date.getTimeZone().getRawOffset() == 0 ) {
291                return new SimpleDateFormat( ISO_8601_2004_FORMAT_UTC ).format( date.getTime() );
292            }
293            return formatISO8601Date( date.getTime() );
294        }
295    
296        /**
297         * Obtain an ISO 8601:2004 string representation of the supplied date.
298         * 
299         * @param date
300         *            the date
301         * @return the string in the {@link #ISO_8601_2004_FORMAT standard format}
302         */
303        public static String formatISO8601Date( final java.util.Date date ) {
304            if ( date == null ) {
305                return "";
306            }
307            return new SimpleDateFormat( ISO_8601_2004_FORMAT ).format( date );
308        }
309    
310        /**
311         * Obtain an JDBC timestamp string representation of the supplied date.
312         * 
313         * @param date
314         *            the date
315         * @return the string in the JDBC timestamp format
316         */
317        public static String formatJDBCTimeStamp( final java.util.Date date ) {
318            return new SimpleDateFormat( JDBC_TIMESTAMP_FORMAT ).format( date );
319        }
320    
321        /**
322         * Return a string representation of the supplied date with the current default locale.
323         * 
324         * @param date
325         * @return the string in locale format
326         */
327        public static String formatLocaleDate( final java.util.Date date ) {
328            return formatLocaleDate( date, Locale.getDefault() );
329        }
330    
331        /**
332         * Return a string representation of the supplied date with the supplied locale.
333         * 
334         * @param date
335         * @param locale
336         * @return the string in locale format
337         */
338        public static String formatLocaleDate( final java.util.Date date, Locale locale ) {
339            return DateFormat.getDateTimeInstance( DateFormat.MEDIUM, DateFormat.LONG, locale ).format( date );
340        }
341    
342        private DateUtil() {
343            // Prevent instantiation
344        }
345    
346    }