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 static 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 static 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 = Integer.parseInt(date.substring(0, 4));
186        int month = Integer.parseInt(date.substring(5, 7));
187        int day = Integer.parseInt(date.substring(8, 10));
188        int hour = Integer.parseInt(date.substring(11, 13));
189        int minute = Integer.parseInt(date.substring(14, 16));
190        int second = Integer.parseInt(date.substring(17, 19));
191
192        calendar.clear();
193        calendar.set(Calendar.YEAR, year);
194        calendar.set(Calendar.MONTH, month - 1);
195        calendar.set(Calendar.DAY_OF_MONTH, day);
196        calendar.set(Calendar.HOUR_OF_DAY, hour);
197        calendar.set(Calendar.MINUTE, minute);
198        calendar.set(Calendar.SECOND, second);
199
200        return calendar.getTime();
201    }
202
203    private Date parseLongStandardDate(String date) {
204        int year = Integer.parseInt(date.substring(0, 4));
205        int month = Integer.parseInt(date.substring(5, 7));
206        int day = Integer.parseInt(date.substring(8, 10));
207        int hour = Integer.parseInt(date.substring(11, 13));
208        int minute = Integer.parseInt(date.substring(14, 16));
209        int second = Integer.parseInt(date.substring(17, 19));
210        int millisecond = Integer.parseInt(date.substring(20, 23));
211
212        calendar.clear();
213        calendar.set(Calendar.YEAR, year);
214        calendar.set(Calendar.MONTH, month - 1);
215        calendar.set(Calendar.DAY_OF_MONTH, day);
216        calendar.set(Calendar.HOUR_OF_DAY, hour);
217        calendar.set(Calendar.MINUTE, minute);
218        calendar.set(Calendar.SECOND, second);
219        calendar.set(Calendar.MILLISECOND, millisecond);
220
221        return calendar.getTime();
222    }
223
224    /**
225     * Attempts to parse the specified date.
226     *
227     * @param date
228     *            The date to parse.
229     * @return The date.
230     * @throws ParseException
231     *             Occurs if the date does not match any of the supported date
232     *             formats.
233     */
234    public Date parse(String date) throws ParseException {
235        try {
236            if (isDateInShortStandardFormat(date)) {
237                return parseShortStandardDate(date);
238            } else if (isDateInLongStandardFormat(date)) {
239                return parseLongStandardDate(date);
240            } else {
241                return datatypeFactory.newXMLGregorianCalendar(date).toGregorianCalendar().getTime();
242            }
243
244        } catch (IllegalArgumentException e) {
245            return fallbackDateParser.parse(date);
246        }
247    }
248}