001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.tools.date;
003
004import java.text.ParseException;
005import java.util.Calendar;
006import java.util.Date;
007import java.util.GregorianCalendar;
008import java.util.TimeZone;
009
010import javax.xml.datatype.DatatypeConfigurationException;
011import javax.xml.datatype.DatatypeFactory;
012
013/**
014 * Handles a number of different date formats encountered in OSM. This is built
015 * based on similar code in JOSM. This class is not threadsafe, a separate
016 * instance must be created per thread.
017 *
018 * @author Brett Henderson
019 */
020public class PrimaryDateParser {
021    private DatatypeFactory datatypeFactory;
022    private final FallbackDateParser fallbackDateParser;
023    private final Calendar calendar;
024
025    /**
026     * Creates a new instance.
027     */
028    public PrimaryDateParser() {
029        // Build an xml data type factory.
030        try {
031            datatypeFactory = DatatypeFactory.newInstance();
032
033        } catch (DatatypeConfigurationException e) {
034            throw new RuntimeException("Unable to instantiate xml datatype factory.", e);
035        }
036
037        fallbackDateParser = new FallbackDateParser();
038
039        calendar = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
040    }
041
042    private static boolean isDateInShortStandardFormat(String date) {
043        // We can only parse the date if it is in a very specific format.
044        // eg. 2007-09-23T08:25:43Z
045
046        if (date.length() != 20) {
047            return false;
048        }
049
050        char[] dateChars = date.toCharArray();
051
052        // Make sure any fixed characters are in the correct place.
053        if (dateChars[4] != '-') {
054            return false;
055        }
056        if (dateChars[7] != '-') {
057            return false;
058        }
059        if (dateChars[10] != 'T') {
060            return false;
061        }
062        if (dateChars[13] != ':') {
063            return false;
064        }
065        if (dateChars[16] != ':') {
066            return false;
067        }
068        if (dateChars[19] != 'Z') {
069            return false;
070        }
071
072        // Ensure all remaining characters are numbers.
073        for (int i = 0; i < 4; i++) {
074            if (dateChars[i] < '0' || dateChars[i] > '9') {
075                return false;
076            }
077        }
078        for (int i = 5; i < 7; i++) {
079            if (dateChars[i] < '0' || dateChars[i] > '9') {
080                return false;
081            }
082        }
083        for (int i = 8; i < 10; i++) {
084            if (dateChars[i] < '0' || dateChars[i] > '9') {
085                return false;
086            }
087        }
088        for (int i = 11; i < 13; i++) {
089            if (dateChars[i] < '0' || dateChars[i] > '9') {
090                return false;
091            }
092        }
093        for (int i = 14; i < 16; i++) {
094            if (dateChars[i] < '0' || dateChars[i] > '9') {
095                return false;
096            }
097        }
098        for (int i = 17; i < 19; i++) {
099            if (dateChars[i] < '0' || dateChars[i] > '9') {
100                return false;
101            }
102        }
103
104        // No problems found so it is in the special case format.
105        return true;
106    }
107
108    private static boolean isDateInLongStandardFormat(String date) {
109        // We can only parse the date if it is in a very specific format.
110        // eg. 2007-09-23T08:25:43.000Z
111
112        if (date.length() != 24) {
113            return false;
114        }
115
116        char[] dateChars = date.toCharArray();
117
118        // Make sure any fixed characters are in the correct place.
119        if (dateChars[4] != '-') {
120            return false;
121        }
122        if (dateChars[7] != '-') {
123            return false;
124        }
125        if (dateChars[10] != 'T') {
126            return false;
127        }
128        if (dateChars[13] != ':') {
129            return false;
130        }
131        if (dateChars[16] != ':') {
132            return false;
133        }
134        if (dateChars[19] != '.') {
135            return false;
136        }
137        if (dateChars[23] != 'Z') {
138            return false;
139        }
140
141        // Ensure all remaining characters are numbers.
142        for (int i = 0; i < 4; i++) {
143            if (dateChars[i] < '0' || dateChars[i] > '9') {
144                return false;
145            }
146        }
147        for (int i = 5; i < 7; i++) {
148            if (dateChars[i] < '0' || dateChars[i] > '9') {
149                return false;
150            }
151        }
152        for (int i = 8; i < 10; i++) {
153            if (dateChars[i] < '0' || dateChars[i] > '9') {
154                return false;
155            }
156        }
157        for (int i = 11; i < 13; i++) {
158            if (dateChars[i] < '0' || dateChars[i] > '9') {
159                return false;
160            }
161        }
162        for (int i = 14; i < 16; i++) {
163            if (dateChars[i] < '0' || dateChars[i] > '9') {
164                return false;
165            }
166        }
167        for (int i = 17; i < 19; i++) {
168            if (dateChars[i] < '0' || dateChars[i] > '9') {
169                return false;
170            }
171        }
172        for (int i = 20; i < 23; i++) {
173            if (dateChars[i] < '0' || dateChars[i] > '9') {
174                return false;
175            }
176        }
177
178        // No problems found so it is in the special case format.
179        return true;
180    }
181
182    private Date parseShortStandardDate(String date) {
183        int year = Integer.parseInt(date.substring(0, 4));
184        int month = Integer.parseInt(date.substring(5, 7));
185        int day = Integer.parseInt(date.substring(8, 10));
186        int hour = Integer.parseInt(date.substring(11, 13));
187        int minute = Integer.parseInt(date.substring(14, 16));
188        int second = Integer.parseInt(date.substring(17, 19));
189
190        calendar.clear();
191        calendar.set(Calendar.YEAR, year);
192        calendar.set(Calendar.MONTH, month - 1);
193        calendar.set(Calendar.DAY_OF_MONTH, day);
194        calendar.set(Calendar.HOUR_OF_DAY, hour);
195        calendar.set(Calendar.MINUTE, minute);
196        calendar.set(Calendar.SECOND, second);
197
198        return calendar.getTime();
199    }
200
201    private Date parseLongStandardDate(String date) {
202        int year = Integer.parseInt(date.substring(0, 4));
203        int month = Integer.parseInt(date.substring(5, 7));
204        int day = Integer.parseInt(date.substring(8, 10));
205        int hour = Integer.parseInt(date.substring(11, 13));
206        int minute = Integer.parseInt(date.substring(14, 16));
207        int second = Integer.parseInt(date.substring(17, 19));
208        int millisecond = Integer.parseInt(date.substring(20, 23));
209
210        calendar.clear();
211        calendar.set(Calendar.YEAR, year);
212        calendar.set(Calendar.MONTH, month - 1);
213        calendar.set(Calendar.DAY_OF_MONTH, day);
214        calendar.set(Calendar.HOUR_OF_DAY, hour);
215        calendar.set(Calendar.MINUTE, minute);
216        calendar.set(Calendar.SECOND, second);
217        calendar.set(Calendar.MILLISECOND, millisecond);
218
219        return calendar.getTime();
220    }
221
222    /**
223     * Attempts to parse the specified date.
224     *
225     * @param date
226     *            The date to parse.
227     * @return The date.
228     * @throws ParseException
229     *             Occurs if the date does not match any of the supported date
230     *             formats.
231     */
232    public Date parse(String date) throws ParseException {
233        try {
234            if (isDateInShortStandardFormat(date)) {
235                return parseShortStandardDate(date);
236            } else if (isDateInLongStandardFormat(date)) {
237                return parseLongStandardDate(date);
238            } else {
239                return datatypeFactory.newXMLGregorianCalendar(date).toGregorianCalendar().getTime();
240            }
241
242        } catch (IllegalArgumentException e) {
243            return fallbackDateParser.parse(date);
244        }
245    }
246}