001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.tools.date;
003
004import java.text.DateFormat;
005import java.text.ParsePosition;
006import java.text.SimpleDateFormat;
007import java.util.Calendar;
008import java.util.Date;
009import java.util.GregorianCalendar;
010import java.util.Locale;
011import java.util.TimeZone;
012
013import javax.xml.datatype.DatatypeConfigurationException;
014import javax.xml.datatype.DatatypeFactory;
015import javax.xml.datatype.XMLGregorianCalendar;
016
017import org.openstreetmap.josm.Main;
018import org.openstreetmap.josm.data.preferences.BooleanProperty;
019import org.openstreetmap.josm.tools.CheckParameterUtil;
020
021/**
022 * A static utility class dealing with:
023 * <ul>
024 * <li>parsing XML date quickly and formatting a date to the XML UTC format regardless of current locale</li>
025 * <li>providing a single entry point for formatting dates to be displayed in JOSM GUI, based on user preferences</li>
026 * </ul>
027 * @author nenik
028 */
029public final class DateUtils {
030
031    private DateUtils() {
032        // Hide default constructor for utils classes
033    }
034
035    /**
036     * Property to enable display of ISO dates globally.
037     * @since 7299
038     */
039    public static final BooleanProperty PROP_ISO_DATES = new BooleanProperty("iso.dates", false);
040
041    /**
042     * A shared instance used for conversion between individual date fields
043     * and long millis time. It is guarded against conflict by the class lock.
044     * The shared instance is used because the construction, together
045     * with the timezone lookup, is very expensive.
046     */
047    private static GregorianCalendar calendar = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
048    private static final DatatypeFactory XML_DATE;
049
050    static {
051        calendar.setTimeInMillis(0);
052
053        DatatypeFactory fact = null;
054        try {
055            fact = DatatypeFactory.newInstance();
056        } catch(DatatypeConfigurationException ce) {
057            Main.error(ce);
058        }
059        XML_DATE = fact;
060    }
061
062    /**
063     * Parses XML date quickly, regardless of current locale.
064     * @param str The XML date as string
065     * @return The date
066     */
067    public static synchronized Date fromString(String str) {
068        // "2007-07-25T09:26:24{Z|{+|-}01:00}"
069        if (checkLayout(str, "xxxx-xx-xxTxx:xx:xxZ") ||
070                checkLayout(str, "xxxx-xx-xxTxx:xx:xx") ||
071                checkLayout(str, "xxxx-xx-xxTxx:xx:xx+xx:00") ||
072                checkLayout(str, "xxxx-xx-xxTxx:xx:xx-xx:00")) {
073            calendar.set(
074                parsePart(str, 0, 4),
075                parsePart(str, 5, 2)-1,
076                parsePart(str, 8, 2),
077                parsePart(str, 11, 2),
078                parsePart(str, 14,2),
079                parsePart(str, 17, 2));
080
081            if (str.length() == 25) {
082                int plusHr = parsePart(str, 20, 2);
083                int mul = str.charAt(19) == '+' ? -3600000 : 3600000;
084                calendar.setTimeInMillis(calendar.getTimeInMillis()+plusHr*mul);
085            }
086
087            return calendar.getTime();
088        } else if(checkLayout(str, "xxxx-xx-xxTxx:xx:xx.xxxZ") ||
089                checkLayout(str, "xxxx-xx-xxTxx:xx:xx.xxx") ||
090                checkLayout(str, "xxxx-xx-xxTxx:xx:xx.xxx+xx:00") ||
091                checkLayout(str, "xxxx-xx-xxTxx:xx:xx.xxx-xx:00")) {
092            calendar.set(
093                parsePart(str, 0, 4),
094                parsePart(str, 5, 2)-1,
095                parsePart(str, 8, 2),
096                parsePart(str, 11, 2),
097                parsePart(str, 14,2),
098                parsePart(str, 17, 2));
099            long millis = parsePart(str, 20, 3);
100            if (str.length() == 29)
101                millis += parsePart(str, 24, 2) * (str.charAt(23) == '+' ? -3600000 : 3600000);
102            calendar.setTimeInMillis(calendar.getTimeInMillis()+millis);
103
104            return calendar.getTime();
105        } else {
106            // example date format "18-AUG-08 13:33:03"
107            SimpleDateFormat f = new SimpleDateFormat("dd-MMM-yy HH:mm:ss");
108            Date d = f.parse(str, new ParsePosition(0));
109            if(d != null)
110                return d;
111        }
112
113        try {
114            return XML_DATE.newXMLGregorianCalendar(str).toGregorianCalendar().getTime();
115        } catch (Exception ex) {
116            return new Date();
117        }
118    }
119
120    /**
121     * Formats a date to the XML UTC format regardless of current locale.
122     * @param date The date to format
123     * @return The formatted date
124     */
125    public static synchronized String fromDate(Date date) {
126        calendar.setTime(date);
127        XMLGregorianCalendar xgc = XML_DATE.newXMLGregorianCalendar(calendar);
128        if (calendar.get(Calendar.MILLISECOND) == 0) xgc.setFractionalSecond(null);
129        return xgc.toXMLFormat();
130    }
131
132    private static boolean checkLayout(String text, String pattern) {
133        if (text.length() != pattern.length()) return false;
134        for (int i=0; i<pattern.length(); i++) {
135            char pc = pattern.charAt(i);
136            char tc = text.charAt(i);
137            if(pc == 'x' && tc >= '0' && tc <= '9') continue;
138            else if(pc == 'x' || pc != tc) return false;
139        }
140        return true;
141    }
142
143    private static int parsePart(String str, int off, int len) {
144        return Integer.valueOf(str.substring(off, off+len));
145    }
146
147    /**
148     * Returns a new {@code SimpleDateFormat} for date only, according to <a href="https://en.wikipedia.org/wiki/ISO_8601">ISO 8601</a>.
149     * @return a new ISO 8601 date format, for date only.
150     * @since 7299
151     */
152    public static final SimpleDateFormat newIsoDateFormat() {
153        return new SimpleDateFormat("yyyy-MM-dd");
154    }
155
156    /**
157     * Returns a new {@code SimpleDateFormat} for date and time, according to <a href="https://en.wikipedia.org/wiki/ISO_8601">ISO 8601</a>.
158     * @return a new ISO 8601 date format, for date and time.
159     * @since 7299
160     */
161    public static final SimpleDateFormat newIsoDateTimeFormat() {
162        return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX");
163    }
164
165    /**
166     * Returns a new {@code SimpleDateFormat} for date and time, according to format used in OSM API errors.
167     * @return a new date format, for date and time, to use for OSM API error handling.
168     * @since 7299
169     */
170    public static final SimpleDateFormat newOsmApiDateTimeFormat() {
171        // Example: "2010-09-07 14:39:41 UTC".
172        // Always parsed with US locale regardless of the current locale in JOSM
173        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z", Locale.US);
174    }
175
176    /**
177     * Returns the date format to be used for current user, based on user preferences.
178     * @param dateStyle The date style as described in {@link DateFormat#getDateInstance}. Ignored if "ISO dates" option is set
179     * @return The date format
180     * @since 7299
181     */
182    public static final DateFormat getDateFormat(int dateStyle) {
183        if (PROP_ISO_DATES.get()) {
184            return newIsoDateFormat();
185        } else {
186            return DateFormat.getDateInstance(dateStyle, Locale.getDefault());
187        }
188    }
189
190    /**
191     * Formats a date to be displayed to current user, based on user preferences.
192     * @param date The date to display. Must not be {@code null}
193     * @param dateStyle The date style as described in {@link DateFormat#getDateInstance}. Ignored if "ISO dates" option is set
194     * @return The formatted date
195     * @since 7299
196     */
197    public static final String formatDate(Date date, int dateStyle) {
198        CheckParameterUtil.ensureParameterNotNull(date, "date");
199        return getDateFormat(dateStyle).format(date);
200    }
201
202    /**
203     * Returns the time format to be used for current user, based on user preferences.
204     * @param timeStyle The time style as described in {@link DateFormat#getTimeInstance}. Ignored if "ISO dates" option is set
205     * @return The time format
206     * @since 7299
207     */
208    public static final DateFormat getTimeFormat(int timeStyle) {
209        if (PROP_ISO_DATES.get()) {
210            // This is not strictly conform to ISO 8601. We just want to avoid US-style times such as 3.30pm
211            return new SimpleDateFormat("HH:mm:ss");
212        } else {
213            return DateFormat.getTimeInstance(timeStyle, Locale.getDefault());
214        }
215    }
216    /**
217     * Formats a time to be displayed to current user, based on user preferences.
218     * @param time The time to display. Must not be {@code null}
219     * @param timeStyle The time style as described in {@link DateFormat#getTimeInstance}. Ignored if "ISO dates" option is set
220     * @return The formatted time
221     * @since 7299
222     */
223    public static final String formatTime(Date time, int timeStyle) {
224        CheckParameterUtil.ensureParameterNotNull(time, "time");
225        return getTimeFormat(timeStyle).format(time);
226    }
227
228    /**
229     * Returns the date/time format to be used for current user, based on user preferences.
230     * @param dateStyle The date style as described in {@link DateFormat#getDateTimeInstance}. Ignored if "ISO dates" option is set
231     * @param timeStyle The time style as described in {@code DateFormat.getDateTimeInstance}. Ignored if "ISO dates" option is set
232     * @return The date/time format
233     * @since 7299
234     */
235    public static final DateFormat getDateTimeFormat(int dateStyle, int timeStyle) {
236        if (PROP_ISO_DATES.get()) {
237            // This is not strictly conform to ISO 8601. We just want to avoid US-style times such as 3.30pm
238            // and we don't want to use the 'T' separator as a space character is much more readable
239            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
240        } else {
241            return DateFormat.getDateTimeInstance(dateStyle, timeStyle, Locale.getDefault());
242        }
243    }
244
245    /**
246     * Formats a date/time to be displayed to current user, based on user preferences.
247     * @param datetime The date/time to display. Must not be {@code null}
248     * @param dateStyle The date style as described in {@link DateFormat#getDateTimeInstance}. Ignored if "ISO dates" option is set
249     * @param timeStyle The time style as described in {@code DateFormat.getDateTimeInstance}. Ignored if "ISO dates" option is set
250     * @return The formatted date/time
251     * @since 7299
252     */
253    public static final String formatDateTime(Date datetime, int dateStyle, int timeStyle) {
254        CheckParameterUtil.ensureParameterNotNull(datetime, "datetime");
255        return getDateTimeFormat(dateStyle, timeStyle).format(datetime);
256    }
257}