001/* 002 * SonarQube 003 * Copyright (C) 2009-2017 SonarSource SA 004 * mailto:info AT sonarsource DOT com 005 * 006 * This program is free software; you can redistribute it and/or 007 * modify it under the terms of the GNU Lesser General Public 008 * License as published by the Free Software Foundation; either 009 * version 3 of the License, or (at your option) any later version. 010 * 011 * This program is distributed in the hope that it will be useful, 012 * but WITHOUT ANY WARRANTY; without even the implied warranty of 013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 014 * Lesser General Public License for more details. 015 * 016 * You should have received a copy of the GNU Lesser General Public License 017 * along with this program; if not, write to the Free Software Foundation, 018 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 019 */ 020package org.sonar.api.utils; 021 022import java.time.Instant; 023import java.time.LocalDate; 024import java.time.OffsetDateTime; 025import java.time.ZoneId; 026import java.time.format.DateTimeFormatter; 027import java.time.format.DateTimeParseException; 028import java.time.temporal.ChronoUnit; 029import java.util.Date; 030import javax.annotation.CheckForNull; 031import javax.annotation.Nullable; 032 033import static com.google.common.base.Preconditions.checkArgument; 034 035/** 036 * Parses and formats <a href="https://docs.oracle.com/javase/8/docs/api/java/text/SimpleDateFormat.html#rfc822timezone">RFC 822</a> dates. 037 * This class is thread-safe. 038 * 039 * @since 2.7 040 */ 041public final class DateUtils { 042 public static final String DATE_FORMAT = "yyyy-MM-dd"; 043 public static final String DATETIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ssZ"; 044 045 private static final DateTimeFormatter DATETIME_FORMATTER = DateTimeFormatter.ofPattern(DATETIME_FORMAT); 046 047 private DateUtils() { 048 } 049 050 /** 051 * Warning: relies on default timezone! 052 */ 053 public static String formatDate(Date d) { 054 return d.toInstant().atZone(ZoneId.systemDefault()).toLocalDate().toString(); 055 } 056 057 /** 058 * Warning: relies on default timezone! 059 */ 060 public static String formatDateTime(Date d) { 061 return formatDateTime(OffsetDateTime.ofInstant(d.toInstant(), ZoneId.systemDefault())); 062 } 063 064 /** 065 * Warning: relies on default timezone! 066 */ 067 public static String formatDateTime(long ms) { 068 return formatDateTime(OffsetDateTime.ofInstant(Instant.ofEpochMilli(ms), ZoneId.systemDefault())); 069 } 070 071 /** 072 * @since 6.6 073 */ 074 public static String formatDateTime(OffsetDateTime dt) { 075 return DATETIME_FORMATTER.format(dt); 076 } 077 078 /** 079 * Warning: relies on default timezone! 080 */ 081 public static String formatDateTimeNullSafe(@Nullable Date date) { 082 return date == null ? "" : formatDateTime(date); 083 } 084 085 @CheckForNull 086 public static Date longToDate(@Nullable Long time) { 087 return time == null ? null : new Date(time); 088 } 089 090 @CheckForNull 091 public static Long dateToLong(@Nullable Date date) { 092 return date == null ? null : date.getTime(); 093 } 094 095 /** 096 * Return a date at the start of day. 097 * @param s string in format {@link #DATE_FORMAT} 098 * @throws SonarException when string cannot be parsed 099 */ 100 public static Date parseDate(String s) { 101 return Date.from(parseLocalDate(s).atStartOfDay(ZoneId.systemDefault()).toInstant()); 102 } 103 104 /** 105 * Parse format {@link #DATE_FORMAT}. This method never throws exception. 106 * 107 * @param s any string 108 * @return the date, {@code null} if parsing error or if parameter is {@code null} 109 * @since 3.0 110 */ 111 @CheckForNull 112 public static Date parseDateQuietly(@Nullable String s) { 113 Date date = null; 114 if (s != null) { 115 try { 116 date = parseDate(s); 117 } catch (RuntimeException e) { 118 // ignore 119 } 120 121 } 122 return date; 123 } 124 125 /** 126 * @since 6.6 127 */ 128 public static LocalDate parseLocalDate(String s) { 129 try { 130 return LocalDate.parse(s); 131 } catch (DateTimeParseException e) { 132 throw MessageException.of("The date '" + s + "' does not respect format '" + DATE_FORMAT + "'", e); 133 } 134 } 135 136 /** 137 * Parse format {@link #DATE_FORMAT}. This method never throws exception. 138 * 139 * @param s any string 140 * @return the date, {@code null} if parsing error or if parameter is {@code null} 141 * @since 6.6 142 */ 143 @CheckForNull 144 public static LocalDate parseLocalDateQuietly(@Nullable String s) { 145 LocalDate date = null; 146 if (s != null) { 147 try { 148 date = parseLocalDate(s); 149 } catch (RuntimeException e) { 150 // ignore 151 } 152 153 } 154 return date; 155 } 156 157 /** 158 * @param s string in format {@link #DATETIME_FORMAT} 159 * @throws SonarException when string cannot be parsed 160 */ 161 public static Date parseDateTime(String s) { 162 return Date.from(parseOffsetDateTime(s).toInstant()); 163 } 164 165 /** 166 * @param s string in format {@link #DATETIME_FORMAT} 167 * @throws SonarException when string cannot be parsed 168 * @since 6.6 169 */ 170 public static OffsetDateTime parseOffsetDateTime(String s) { 171 try { 172 return OffsetDateTime.parse(s, DATETIME_FORMATTER); 173 } catch (DateTimeParseException e) { 174 throw MessageException.of("The date '" + s + "' does not respect format '" + DATETIME_FORMAT + "'", e); 175 } 176 } 177 178 /** 179 * Parse format {@link #DATETIME_FORMAT}. This method never throws exception. 180 * 181 * @param s any string 182 * @return the datetime, {@code null} if parsing error or if parameter is {@code null} 183 */ 184 @CheckForNull 185 public static Date parseDateTimeQuietly(@Nullable String s) { 186 Date datetime = null; 187 if (s != null) { 188 try { 189 datetime = parseDateTime(s); 190 } catch (RuntimeException e) { 191 // ignore 192 } 193 194 } 195 return datetime; 196 } 197 198 /** 199 * Parse format {@link #DATETIME_FORMAT}. This method never throws exception. 200 * 201 * @param s any string 202 * @return the datetime, {@code null} if parsing error or if parameter is {@code null} 203 * @since 6.6 204 */ 205 @CheckForNull 206 public static OffsetDateTime parseOffsetDateTimeQuietly(@Nullable String s) { 207 OffsetDateTime datetime = null; 208 if (s != null) { 209 try { 210 datetime = parseOffsetDateTime(s); 211 } catch (RuntimeException e) { 212 // ignore 213 } 214 215 } 216 return datetime; 217 } 218 219 /** 220 * Warning: may rely on default timezone! 221 * @throws IllegalArgumentException if stringDate is not a correctly formed date or datetime 222 * @return the datetime, {@code null} if stringDate is null 223 * @since 6.1 224 */ 225 @CheckForNull 226 public static Date parseDateOrDateTime(@Nullable String stringDate) { 227 if (stringDate == null) { 228 return null; 229 } 230 231 OffsetDateTime odt = parseOffsetDateTimeQuietly(stringDate); 232 if (odt != null) { 233 return Date.from(odt.toInstant()); 234 } 235 236 LocalDate ld = parseLocalDateQuietly(stringDate); 237 checkArgument(ld != null, "Date '%s' cannot be parsed as either a date or date+time", stringDate); 238 239 return Date.from(ld.atStartOfDay(ZoneId.systemDefault()).toInstant()); 240 } 241 242 /** 243 * Warning: may rely on default timezone! 244 * @see #parseDateOrDateTime(String) 245 */ 246 @CheckForNull 247 public static Date parseStartingDateOrDateTime(@Nullable String stringDate) { 248 return parseDateOrDateTime(stringDate); 249 } 250 251 /** 252 * Return the datetime if @param stringDate is a datetime, date + 1 day if stringDate is a date. 253 * So '2016-09-01' would return a date equivalent to '2016-09-02T00:00:00+0000' in GMT (Warning: relies on default timezone!) 254 * @see #parseDateOrDateTime(String) 255 * @throws IllegalArgumentException if stringDate is not a correctly formed date or datetime 256 * @return the datetime, {@code null} if stringDate is null 257 * @since 6.1 258 */ 259 @CheckForNull 260 public static Date parseEndingDateOrDateTime(@Nullable String stringDate) { 261 if (stringDate == null) { 262 return null; 263 } 264 265 Date date = parseDateTimeQuietly(stringDate); 266 if (date != null) { 267 return date; 268 } 269 270 date = parseDateQuietly(stringDate); 271 checkArgument(date != null, "Date '%s' cannot be parsed as either a date or date+time", stringDate); 272 273 return addDays(date, 1); 274 } 275 276 /** 277 * Adds a number of days to a date returning a new object. 278 * The original date object is unchanged. 279 * 280 * @param date the date, not null 281 * @param numberOfDays the amount to add, may be negative 282 * @return the new date object with the amount added 283 */ 284 public static Date addDays(Date date, int numberOfDays) { 285 return Date.from(date.toInstant().plus(numberOfDays, ChronoUnit.DAYS)); 286 } 287 288 @CheckForNull 289 public static Date truncateToSeconds(@Nullable Date d) { 290 if (d == null) { 291 return null; 292 } 293 return truncateToSecondsImpl(d); 294 } 295 296 public static long truncateToSeconds(long dateTime) { 297 return truncateToSecondsImpl(new Date(dateTime)).getTime(); 298 } 299 300 private static Date truncateToSecondsImpl(Date d) { 301 Instant instant = d.toInstant(); 302 instant = instant.truncatedTo(ChronoUnit.SECONDS); 303 return Date.from(instant); 304 } 305 306}