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}