001    /*
002     * JBoss, Home of Professional Open Source.
003     * Copyright 2008, Red Hat Middleware LLC, and individual contributors
004     * as indicated by the @author tags. See the copyright.txt file in the
005     * distribution for a full listing of individual contributors. 
006     *
007     * This is free software; you can redistribute it and/or modify it
008     * under the terms of the GNU Lesser General Public License as
009     * published by the Free Software Foundation; either version 2.1 of
010     * the License, or (at your option) any later version.
011     *
012     * This software is distributed in the hope that it will be useful,
013     * but WITHOUT ANY WARRANTY; without even the implied warranty of
014     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015     * Lesser General Public License for more details.
016     *
017     * You should have received a copy of the GNU Lesser General Public
018     * License along with this software; if not, write to the Free
019     * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
020     * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
021     */
022    package org.jboss.dna.common.util;
023    
024    import java.text.DateFormat;
025    import java.text.ParseException;
026    import java.text.SimpleDateFormat;
027    import java.util.Calendar;
028    import java.util.Date;
029    import java.util.Locale;
030    import java.util.regex.Matcher;
031    import java.util.regex.Pattern;
032    import net.jcip.annotations.ThreadSafe;
033    import org.jboss.dna.common.CommonI18n;
034    
035    /**
036     * Utilities for working with dates.
037     * <p>
038     * Many of the methods that convert dates to and from strings utilize the <a href="http://en.wikipedia.org/wiki/ISO_8601">ISO
039     * 8601:2004</a> standard string format <code>yyyy-MM-ddTHH:mm:ss.SSSZ</code>, where <blockquote>
040     * 
041     * <pre>
042     * Symbol   Meaning                 Presentation        Example
043     * ------   -------                 ------------        -------
044     * y        year                    (Number)            1996
045     * M        month in year           (Number)            07
046     * d        day in month            (Number)            10
047     * h        hour in am/pm (1&tilde;12)    (Number)            12
048     * H        hour in day (0&tilde;23)      (Number)            0
049     * m        minute in hour          (Number)            30
050     * s        second in minute        (Number)            55
051     * S        millisecond             (Number)            978
052     * Z        time zone               (Number)            -0600
053     * </pre>
054     * 
055     * </blockquote>
056     * </p>
057     * <p>
058     * This class is written to be thread safe. As {@link SimpleDateFormat} is not threadsafe, no shared instances are used.
059     * </p>
060     * @author Randall Hauch
061     */
062    @ThreadSafe
063    public class DateUtil {
064    
065        public static final String ISO_8601_2004_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSSZ";
066    
067        /**
068         * Parse the date contained in the supplied string. The date must follow one of the standard ISO 8601 formats, of the form
069         * <code><i>datepart</i>T<i>timepart</i></code>, where <code><i>datepart</i></code> is one of the following forms:
070         * <p>
071         * <dl>
072         * <dt>YYYYMMDD</dt>
073         * <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 optional,
074         * but the month is required if the day is given.</dd>
075         * <dt>YYYY-MM-DD</dt>
076         * <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 optional,
077         * but the month is required if the day is given.</dd>
078         * <dt>YYYY-Www-D</dt>
079         * <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 week
080         * number is optional.</dd>
081         * <dt>YYYYWwwD</dt>
082         * <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 week
083         * number is optional.</dd>
084         * <dt>YYYY-DDD</dt>
085         * <dd>The 4-digit year followed by the 3-digit day of the year (000-365)</dd>
086         * <dt>YYYYDDD</dt>
087         * <dd>The 4-digit year followed by the 3-digit day of the year (000-365)</dd>
088         * </dl>
089         * </p>
090         * <p>
091         * The <code><i>timepart</i></code> consists of one of the following forms that contain the 2-digit hour (00-24), the
092         * 2-digit minutes (00-59), the 2-digit seconds (00-59), and the 1-to-3 digit milliseconds. The minutes, seconds and
093         * milliseconds are optional, but any component is required if it is followed by another component (e.g., minutes are required
094         * if the seconds are given).
095         * <dl>
096         * <dt>hh:mm:ss.SSS</dt>
097         * <dt>hhmmssSSS</dt>
098         * </dl>
099         * </p>
100         * <p>
101         * followed by one of the following time zone definitions:
102         * <dt>Z</dt>
103         * <dd>The uppercase or lowercase 'Z' to denote UTC time</dd>
104         * <dt>&#177;hh:mm</dt>
105         * <dd>The 2-digit hour and the 2-digit minute offset from UTC</dd>
106         * <dt>&#177;hhmm</dt>
107         * <dd>The 2-digit hour and the 2-digit minute offset from UTC</dd>
108         * <dt>&#177;hh</dt>
109         * <dd>The 2-digit hour offset from UTC</dd>
110         * <dt>hh:mm</dt>
111         * <dd>The 2-digit hour and the 2-digit minute offset from UTC</dd>
112         * <dt>hhmm</dt>
113         * <dd>The 2-digit hour and the 2-digit minute offset from UTC</dd>
114         * <dt>hh</dt>
115         * <dd>The 2-digit hour offset from UTC</dd>
116         * </dl>
117         * </p>
118         * @param dateString the string containing the date to be parsed
119         * @return the parsed date as a {@link Calendar} object.
120         * @throws ParseException if there is a problem parsing the string
121         */
122        public static Calendar getCalendarFromStandardString( final String dateString ) throws ParseException {
123            // Example: 2008-02-16T12:30:45.123-0600
124            // Example: 2008-W06-6
125            // Example: 2008-053
126            //
127            // Group Optional Field Description
128            // ----- -------- --------- ------------------------------------------
129            // 1 no 2008 4 digit year as a number
130            // 2 yes 02-16 or W06-6 or 053
131            // 3 yes W06-6
132            // 4 yes 06 2 digit week number (00-59)
133            // 5 yes 6 1 digit day of week as a number (1-7)
134            // 6 yes 02-16
135            // 7 yes 02 2 digit month as a number (00-19)
136            // 8 yes -16
137            // 9 yes 16 2 digit day of month as a number (00-39)
138            // 10 yes 02 2 digit month as a number (00-19)
139            // 11 yes 16 2 digit day of month as a number (00-39)
140            // 12 yes 234 3 digit day of year as a number (000-399)
141            // 13 yes T12:30:45.123-0600
142            // 14 yes 12 2 digit hour as a number (00-29)
143            // 15 yes 30 2 digit minute as a number (00-59)
144            // 16 yes :45.123
145            // 17 yes 45 2 digit second as a number (00-59)
146            // 18 yes .123
147            // 19 yes 123 1, 2 or 3 digit milliseconds as a number (000-999)
148            // 20 yes -0600
149            // 21 yes Z The letter 'Z' if in UTC
150            // 22 yes -06 1 or 2 digit time zone hour offset as a signed number
151            // 23 yes + the plus or minus in the time zone offset
152            // 24 yes 00 1 or 2 digit time zone hour offset as an unsigned number (00-29)
153            // 25 yes 00 1 or 2 digit time zone minute offset as a number (00-59)
154            final String regex =
155                "^(\\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})?)?)?$";
156            final Pattern pattern = Pattern.compile(regex);
157            final Matcher matcher = pattern.matcher(dateString);
158            if (!matcher.matches()) {
159                throw new ParseException(CommonI18n.dateParsingFailure.text(dateString), 0);
160            }
161            String year = matcher.group(1);
162            String week = matcher.group(4);
163            String dayOfWeek = matcher.group(5);
164            String month = matcher.group(7);
165            if (month == null) month = matcher.group(10);
166            String dayOfMonth = matcher.group(9);
167            if (dayOfMonth == null) dayOfMonth = matcher.group(11);
168            String dayOfYear = matcher.group(12);
169            String hourOfDay = matcher.group(14);
170            String minutesOfHour = matcher.group(15);
171            String seconds = matcher.group(17);
172            String milliseconds = matcher.group(19);
173            String timeZoneSign = matcher.group(23);
174            String timeZoneHour = matcher.group(24);
175            String timeZoneMinutes = matcher.group(25);
176            if (matcher.group(21) != null) {
177                timeZoneHour = "00";
178                timeZoneMinutes = "00";
179            }
180    
181            // Create the calendar object and start setting the fields ...
182            Calendar calendar = Calendar.getInstance();
183            calendar.clear();
184    
185            // And start setting the fields. Note that Integer.parseInt should never fail, since we're checking for null and the
186            // regular expression should only have digits in these strings!
187            if (year != null) calendar.set(Calendar.YEAR, Integer.parseInt(year));
188            if (month != null) {
189                calendar.set(Calendar.MONTH, Integer.parseInt(month) - 1); // month is zero-based!
190                if (dayOfMonth != null) calendar.set(Calendar.DAY_OF_MONTH, Integer.parseInt(dayOfMonth));
191            } else if (week != null) {
192                calendar.set(Calendar.WEEK_OF_YEAR, Integer.parseInt(week));
193                if (dayOfWeek != null) calendar.set(Calendar.DAY_OF_WEEK, Integer.parseInt(dayOfWeek));
194            } else if (dayOfYear != null) {
195                calendar.set(Calendar.DAY_OF_YEAR, Integer.parseInt(dayOfYear));
196            }
197            if (hourOfDay != null) calendar.set(Calendar.HOUR_OF_DAY, Integer.parseInt(hourOfDay));
198            if (minutesOfHour != null) calendar.set(Calendar.MINUTE, Integer.parseInt(minutesOfHour));
199            if (seconds != null) calendar.set(Calendar.SECOND, Integer.parseInt(seconds));
200            if (milliseconds != null) calendar.set(Calendar.MILLISECOND, Integer.parseInt(milliseconds));
201            if (timeZoneHour != null) {
202                int zoneOffsetInMillis = Integer.parseInt(timeZoneHour) * 60 * 60 * 1000;
203                if ("-".equals(timeZoneSign)) zoneOffsetInMillis *= -1;
204                if (timeZoneMinutes != null) {
205                    int minuteOffsetInMillis = Integer.parseInt(timeZoneMinutes) * 60 * 1000;
206                    if (zoneOffsetInMillis < 0) {
207                        zoneOffsetInMillis -= minuteOffsetInMillis;
208                    } else {
209                        zoneOffsetInMillis += minuteOffsetInMillis;
210                    }
211                }
212                calendar.set(Calendar.ZONE_OFFSET, zoneOffsetInMillis);
213            }
214            return calendar;
215        }
216    
217        /**
218         * Parse the date contained in the supplied string. This method simply calls {@link Calendar#getTime()} on the result of
219         * {@link #getCalendarFromStandardString(String)}.
220         * @param dateString the string containing the date to be parsed
221         * @return the parsed date as a {@link Calendar} object.
222         * @throws ParseException if there is a problem parsing the string
223         * @see #getCalendarFromStandardString(String)
224         */
225        public static Date getDateFromStandardString( String dateString ) throws ParseException {
226            return getCalendarFromStandardString(dateString).getTime();
227        }
228    
229        /**
230         * Obtain an ISO 8601:2004 string representation of the date given the supplied milliseconds since the epoch.
231         * @param millisecondsSinceEpoch the milliseconds for the date
232         * @return the string in the {@link #ISO_8601_2004_FORMAT standard format}
233         * @see #getDateAsStandardString(Date)
234         * @see #getDateFromStandardString(String)
235         * @see #getCalendarFromStandardString(String)
236         */
237        public static String getDateAsStandardString( final long millisecondsSinceEpoch ) {
238            return getDateAsStandardString(new Date(millisecondsSinceEpoch));
239        }
240    
241        /**
242         * Obtain an ISO 8601:2004 string representation of the date given the supplied milliseconds since the epoch.
243         * @param date the date in calendar form
244         * @return the string in the {@link #ISO_8601_2004_FORMAT standard format}
245         * @see #getDateAsStandardString(Date)
246         * @see #getDateFromStandardString(String)
247         * @see #getCalendarFromStandardString(String)
248         */
249        public static String getDateAsStandardString( final Calendar date ) {
250            return getDateAsStandardString(date.getTime());
251        }
252    
253        /**
254         * Obtain an ISO 8601:2004 string representation of the supplied date.
255         * @param date the date
256         * @return the string in the {@link #ISO_8601_2004_FORMAT standard format}
257         * @see #getDateAsStandardString(long)
258         * @see #getDateFromStandardString(String)
259         * @see #getCalendarFromStandardString(String)
260         */
261        public static String getDateAsStandardString( final java.util.Date date ) {
262            return new SimpleDateFormat(ISO_8601_2004_FORMAT).format(date);
263        }
264    
265        public static String getDateAsStringForCurrentLocale( final java.util.Date date ) {
266            return getDateAsStringForLocale(date, Locale.getDefault());
267        }
268    
269        public static String getDateAsStringForLocale( final java.util.Date date, Locale locale ) {
270            return DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.LONG).format(date);
271        }
272    
273        private DateUtil() {
274            // Prevent instantiation
275        }
276    
277    }