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.io.NotSerializableException;
023import java.io.ObjectInputStream;
024import java.io.ObjectOutputStream;
025import java.lang.ref.Reference;
026import java.lang.ref.SoftReference;
027import java.text.DateFormat;
028import java.text.FieldPosition;
029import java.text.ParsePosition;
030import java.text.SimpleDateFormat;
031import java.util.Date;
032import javax.annotation.CheckForNull;
033import javax.annotation.Nullable;
034
035import static com.google.common.base.Preconditions.checkArgument;
036
037/**
038 * Parses and formats <a href="https://docs.oracle.com/javase/8/docs/api/java/text/SimpleDateFormat.html#rfc822timezone">RFC 822</a> dates.
039 * This class is thread-safe.
040 *
041 * @since 2.7
042 */
043public final class DateUtils {
044  public static final String DATE_FORMAT = "yyyy-MM-dd";
045  public static final String DATETIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ssZ";
046
047  private static final ThreadSafeDateFormat THREAD_SAFE_DATE_FORMAT = new ThreadSafeDateFormat(DATE_FORMAT);
048  private static final ThreadSafeDateFormat THREAD_SAFE_DATETIME_FORMAT = new ThreadSafeDateFormat(DATETIME_FORMAT);
049
050  private DateUtils() {
051  }
052
053  public static String formatDate(Date d) {
054    return THREAD_SAFE_DATE_FORMAT.format(d);
055  }
056
057  public static String formatDateTime(Date d) {
058    return THREAD_SAFE_DATETIME_FORMAT.format(d);
059  }
060
061  public static String formatDateTime(long ms) {
062    return THREAD_SAFE_DATETIME_FORMAT.format(new Date(ms));
063  }
064
065  public static String formatDateTimeNullSafe(@Nullable Date date) {
066    return date == null ? "" : THREAD_SAFE_DATETIME_FORMAT.format(date);
067  }
068
069  @CheckForNull
070  public static Date longToDate(@Nullable Long time) {
071    return time == null ? null : new Date(time);
072  }
073
074  @CheckForNull
075  public static Long dateToLong(@Nullable Date date) {
076    return date == null ? null : date.getTime();
077  }
078
079  /**
080   * @param s string in format {@link #DATE_FORMAT}
081   * @throws SonarException when string cannot be parsed
082   */
083  public static Date parseDate(String s) {
084    ParsePosition pos = new ParsePosition(0);
085    Date result = THREAD_SAFE_DATE_FORMAT.parse(s, pos);
086    if (pos.getIndex() != s.length()) {
087      throw new SonarException("The date '" + s + "' does not respect format '" + DATE_FORMAT + "'");
088    }
089    return result;
090  }
091
092  /**
093   * Parse format {@link #DATE_FORMAT}. This method never throws exception.
094   *
095   * @param s any string
096   * @return the date, {@code null} if parsing error or if parameter is {@code null}
097   * @since 3.0
098   */
099  @CheckForNull
100  public static Date parseDateQuietly(@Nullable String s) {
101    Date date = null;
102    if (s != null) {
103      try {
104        date = parseDate(s);
105      } catch (RuntimeException e) {
106        // ignore
107      }
108
109    }
110    return date;
111  }
112
113  /**
114   * @param s string in format {@link #DATETIME_FORMAT}
115   * @throws SonarException when string cannot be parsed
116   */
117
118  public static Date parseDateTime(String s) {
119    ParsePosition pos = new ParsePosition(0);
120    Date result = THREAD_SAFE_DATETIME_FORMAT.parse(s, pos);
121    if (pos.getIndex() != s.length()) {
122      throw new SonarException("The date '" + s + "' does not respect format '" + DATETIME_FORMAT + "'");
123    }
124    return result;
125  }
126
127  /**
128   * Parse format {@link #DATETIME_FORMAT}. This method never throws exception.
129   *
130   * @param s any string
131   * @return the datetime, {@code null} if parsing error or if parameter is {@code null}
132   */
133  @CheckForNull
134  public static Date parseDateTimeQuietly(@Nullable String s) {
135    Date datetime = null;
136    if (s != null) {
137      try {
138        datetime = parseDateTime(s);
139      } catch (RuntimeException e) {
140        // ignore
141      }
142
143    }
144    return datetime;
145  }
146
147  /**
148   * @throws IllegalArgumentException if stringDate is not a correctly formed date or datetime
149   * @return the datetime, {@code null} if stringDate is null
150   * @since 6.1
151   */
152  @CheckForNull
153  public static Date parseDateOrDateTime(@Nullable String stringDate) {
154    if (stringDate == null) {
155      return null;
156    }
157
158    Date date = parseDateTimeQuietly(stringDate);
159    if (date != null) {
160      return date;
161    }
162
163    date = parseDateQuietly(stringDate);
164    checkArgument(date != null, "Date '%s' cannot be parsed as either a date or date+time", stringDate);
165
166    return date;
167  }
168
169  /**
170   * @see #parseDateOrDateTime(String) 
171   */
172  @CheckForNull
173  public static Date parseStartingDateOrDateTime(@Nullable String stringDate) {
174    return parseDateOrDateTime(stringDate);
175  }
176
177  /**
178   * Return the datetime if @param stringDate is a datetime, date + 1 day if stringDate is a date.
179   * So '2016-09-01' would return a date equivalent to '2016-09-02T00:00:00+0000' in GMT
180   * @see #parseDateOrDateTime(String)
181   * @throws IllegalArgumentException if stringDate is not a correctly formed date or datetime
182   * @return the datetime, {@code null} if stringDate is null
183   * @since 6.1
184   */
185  @CheckForNull
186  public static Date parseEndingDateOrDateTime(@Nullable String stringDate) {
187    if (stringDate == null) {
188      return null;
189    }
190
191    Date date = parseDateTimeQuietly(stringDate);
192    if (date != null) {
193      return date;
194    }
195
196    date = parseDateQuietly(stringDate);
197    checkArgument(date != null, "Date '%s' cannot be parsed as either a date or date+time", stringDate);
198
199    return addDays(date, 1);
200  }
201
202  /**
203   * Adds a number of days to a date returning a new object.
204   * The original date object is unchanged.
205   *
206   * @param date  the date, not null
207   * @param numberOfDays  the amount to add, may be negative
208   * @return the new date object with the amount added
209   */
210  public static Date addDays(Date date, int numberOfDays) {
211    return org.apache.commons.lang.time.DateUtils.addDays(date, numberOfDays);
212  }
213
214  static class ThreadSafeDateFormat extends DateFormat {
215    private final String format;
216    private final ThreadLocal<Reference<DateFormat>> cache = new ThreadLocal<Reference<DateFormat>>() {
217      @Override
218      public Reference<DateFormat> get() {
219        Reference<DateFormat> softRef = super.get();
220        if (softRef == null || softRef.get() == null) {
221          SimpleDateFormat sdf = new SimpleDateFormat(format);
222          sdf.setLenient(false);
223          softRef = new SoftReference<>(sdf);
224          super.set(softRef);
225        }
226        return softRef;
227      }
228    };
229
230    ThreadSafeDateFormat(String format) {
231      this.format = format;
232    }
233
234    private DateFormat getDateFormat() {
235      return cache.get().get();
236    }
237
238    @Override
239    public StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition fieldPosition) {
240      return getDateFormat().format(date, toAppendTo, fieldPosition);
241    }
242
243    @Override
244    public Date parse(String source, ParsePosition pos) {
245      return getDateFormat().parse(source, pos);
246    }
247
248    private void readObject(ObjectInputStream ois) throws NotSerializableException {
249      throw new NotSerializableException();
250    }
251
252    private void writeObject(ObjectOutputStream ois) throws NotSerializableException {
253      throw new NotSerializableException();
254    }
255  }
256}