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 FallbackDateParser fallbackDateParser;
023    private 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 boolean isDateInShortStandardFormat(String date) {
043        char[] dateChars;
044        // We can only parse the date if it is in a very specific format.
045        // eg. 2007-09-23T08:25:43Z
046
047        if (date.length() != 20) {
048            return false;
049        }
050
051        dateChars = date.toCharArray();
052
053        // Make sure any fixed characters are in the correct place.
054        if (dateChars[4] != '-') {
055            return false;
056        }
057        if (dateChars[7] != '-') {
058            return false;
059        }
060        if (dateChars[10] != 'T') {
061            return false;
062        }
063        if (dateChars[13] != ':') {
064            return false;
065        }
066        if (dateChars[16] != ':') {
067            return false;
068        }
069        if (dateChars[19] != 'Z') {
070            return false;
071        }
072
073        // Ensure all remaining characters are numbers.
074        for (int i = 0; i < 4; i++) {
075            if (dateChars[i] < '0' || dateChars[i] > '9') {
076                return false;
077            }
078        }
079        for (int i = 5; i < 7; i++) {
080            if (dateChars[i] < '0' || dateChars[i] > '9') {
081                return false;
082            }
083        }
084        for (int i = 8; i < 10; i++) {
085            if (dateChars[i] < '0' || dateChars[i] > '9') {
086                return false;
087            }
088        }
089        for (int i = 11; i < 13; i++) {
090            if (dateChars[i] < '0' || dateChars[i] > '9') {
091                return false;
092            }
093        }
094        for (int i = 14; i < 16; i++) {
095            if (dateChars[i] < '0' || dateChars[i] > '9') {
096                return false;
097            }
098        }
099        for (int i = 17; i < 19; i++) {
100            if (dateChars[i] < '0' || dateChars[i] > '9') {
101                return false;
102            }
103        }
104
105        // No problems found so it is in the special case format.
106        return true;
107    }
108
109    private boolean isDateInLongStandardFormat(String date) {
110        char[] dateChars;
111        // We can only parse the date if it is in a very specific format.
112        // eg. 2007-09-23T08:25:43.000Z
113
114        if (date.length() != 24) {
115            return false;
116        }
117
118        dateChars = date.toCharArray();
119
120        // Make sure any fixed characters are in the correct place.
121        if (dateChars[4] != '-') {
122            return false;
123        }
124        if (dateChars[7] != '-') {
125            return false;
126        }
127        if (dateChars[10] != 'T') {
128            return false;
129        }
130        if (dateChars[13] != ':') {
131            return false;
132        }
133        if (dateChars[16] != ':') {
134            return false;
135        }
136        if (dateChars[19] != '.') {
137            return false;
138        }
139        if (dateChars[23] != 'Z') {
140            return false;
141        }
142
143        // Ensure all remaining characters are numbers.
144        for (int i = 0; i < 4; i++) {
145            if (dateChars[i] < '0' || dateChars[i] > '9') {
146                return false;
147            }
148        }
149        for (int i = 5; i < 7; i++) {
150            if (dateChars[i] < '0' || dateChars[i] > '9') {
151                return false;
152            }
153        }
154        for (int i = 8; i < 10; i++) {
155            if (dateChars[i] < '0' || dateChars[i] > '9') {
156                return false;
157            }
158        }
159        for (int i = 11; i < 13; i++) {
160            if (dateChars[i] < '0' || dateChars[i] > '9') {
161                return false;
162            }
163        }
164        for (int i = 14; i < 16; i++) {
165            if (dateChars[i] < '0' || dateChars[i] > '9') {
166                return false;
167            }
168        }
169        for (int i = 17; i < 19; i++) {
170            if (dateChars[i] < '0' || dateChars[i] > '9') {
171                return false;
172            }
173        }
174        for (int i = 20; i < 23; i++) {
175            if (dateChars[i] < '0' || dateChars[i] > '9') {
176                return false;
177            }
178        }
179
180        // No problems found so it is in the special case format.
181        return true;
182    }
183
184    private Date parseShortStandardDate(String date) {
185        int year;
186        int month;
187        int day;
188        int hour;
189        int minute;
190        int second;
191
192        year = Integer.parseInt(date.substring(0, 4));
193        month = Integer.parseInt(date.substring(5, 7));
194        day = Integer.parseInt(date.substring(8, 10));
195        hour = Integer.parseInt(date.substring(11, 13));
196        minute = Integer.parseInt(date.substring(14, 16));
197        second = Integer.parseInt(date.substring(17, 19));
198
199        calendar.clear();
200        calendar.set(Calendar.YEAR, year);
201        calendar.set(Calendar.MONTH, month - 1);
202        calendar.set(Calendar.DAY_OF_MONTH, day);
203        calendar.set(Calendar.HOUR_OF_DAY, hour);
204        calendar.set(Calendar.MINUTE, minute);
205        calendar.set(Calendar.SECOND, second);
206
207        return calendar.getTime();
208    }
209
210    private Date parseLongStandardDate(String date) {
211        int year;
212        int month;
213        int day;
214        int hour;
215        int minute;
216        int second;
217        int millisecond;
218
219        year = Integer.parseInt(date.substring(0, 4));
220        month = Integer.parseInt(date.substring(5, 7));
221        day = Integer.parseInt(date.substring(8, 10));
222        hour = Integer.parseInt(date.substring(11, 13));
223        minute = Integer.parseInt(date.substring(14, 16));
224        second = Integer.parseInt(date.substring(17, 19));
225        millisecond = Integer.parseInt(date.substring(20, 23));
226
227        calendar.clear();
228        calendar.set(Calendar.YEAR, year);
229        calendar.set(Calendar.MONTH, month - 1);
230        calendar.set(Calendar.DAY_OF_MONTH, day);
231        calendar.set(Calendar.HOUR_OF_DAY, hour);
232        calendar.set(Calendar.MINUTE, minute);
233        calendar.set(Calendar.SECOND, second);
234        calendar.set(Calendar.MILLISECOND, millisecond);
235
236        return calendar.getTime();
237    }
238
239    /**
240     * Attempts to parse the specified date.
241     *
242     * @param date
243     *            The date to parse.
244     * @return The date.
245     * @throws ParseException
246     *             Occurs if the date does not match any of the supported date
247     *             formats.
248     */
249    public Date parse(String date) throws ParseException {
250        try {
251            if (isDateInShortStandardFormat(date)) {
252                return parseShortStandardDate(date);
253            } else if (isDateInLongStandardFormat(date)) {
254                return parseLongStandardDate(date);
255            } else {
256                return datatypeFactory.newXMLGregorianCalendar(date).toGregorianCalendar().getTime();
257            }
258
259        } catch (IllegalArgumentException e) {
260            return fallbackDateParser.parse(date);
261        }
262    }
263}