View Javadoc
1   /*
2    * This file is part of ***  M y C o R e  ***
3    * See http://www.mycore.de/ for details.
4    *
5    * MyCoRe is free software: you can redistribute it and/or modify
6    * it under the terms of the GNU General Public License as published by
7    * the Free Software Foundation, either version 3 of the License, or
8    * (at your option) any later version.
9    *
10   * MyCoRe is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   * GNU General Public License for more details.
14   *
15   * You should have received a copy of the GNU General Public License
16   * along with MyCoRe.  If not, see <http://www.gnu.org/licenses/>.
17   */
18  
19  package org.mycore.datamodel.common;
20  
21  import java.text.DateFormat;
22  import java.text.MessageFormat;
23  import java.text.ParseException;
24  import java.time.DateTimeException;
25  import java.time.Instant;
26  import java.time.LocalDate;
27  import java.time.LocalDateTime;
28  import java.time.Year;
29  import java.time.YearMonth;
30  import java.time.ZoneId;
31  import java.time.ZonedDateTime;
32  import java.time.format.DateTimeFormatter;
33  import java.time.temporal.TemporalAccessor;
34  import java.util.Date;
35  import java.util.Locale;
36  import java.util.Objects;
37  import java.util.Optional;
38  import java.util.StringTokenizer;
39  import java.util.TimeZone;
40  
41  import org.apache.logging.log4j.LogManager;
42  import org.apache.logging.log4j.Logger;
43  import org.mycore.common.config.MCRConfiguration2;
44  
45  /**
46   * holds info about a specific point in time. This class is used for handling ISO 8601 like date and dateTime formatted
47   * strings.
48   * 
49   * @author Thomas Scheffler (yagee)
50   */
51  public class MCRISO8601Date {
52  
53      public static final String PROPERTY_STRICT_PARSING = "MCR.Metadata.SimpleDateFormat.StrictParsing";
54  
55      private static final Logger LOGGER = LogManager.getLogger(MCRISO8601Date.class);
56  
57      private DateTimeFormatter dateTimeFormatter = MCRISO8601FormatChooser.getFormatter(null, null);
58  
59      private TemporalAccessor dt;
60  
61      private MCRISO8601Format isoFormat;
62  
63      /**
64       * creates an empty instance. use {@link #setDate(String)} to parse a date/time by this instance.
65       */
66      public MCRISO8601Date() {
67  
68      }
69  
70      /**
71       * same as {@link #MCRISO8601Date()} and {@link #setDate(String)}.
72       * 
73       * @param isoString
74       *            a date or dateTime string as defined on <a href="http://www.w3.org/TR/NOTE-datetime">W3C Page</a>
75       */
76      public MCRISO8601Date(final String isoString) {
77          this();
78          setDate(isoString);
79      }
80  
81      private static Locale getLocale(final String locale) {
82          String lang = "", country = "";
83          final int pos = locale.indexOf("_");
84          if (pos > 0) {
85              lang = locale.substring(0, pos);
86              country = locale.substring(pos + 1);
87          } else {
88              lang = locale;
89          }
90          return new Locale(lang, country);
91      }
92  
93      public static MCRISO8601Date now() {
94          MCRISO8601Date instance = new MCRISO8601Date();
95          instance.setInstant(Instant.now());
96          return instance;
97      }
98  
99      /**
100      * formats the date to a String.
101      * 
102      * @param format
103      *            as in {@link MCRISO8601Format}
104      * @param locale
105      *            used by format process
106      * @return null if date is not set yet
107      */
108     public String format(final String format, final Locale locale) {
109         return format(format, locale, null);
110     }
111 
112     /**
113      * formats the date to a String.
114      * 
115      * @param format
116      *            as in {@link MCRISO8601Format}
117      * @param locale
118      *            used by format process
119      * @param timeZone
120      *            valid timeZone id, e.g. "Europe/Berlin", or null
121      * @return null if date is not set yet
122      */
123     public String format(final String format, final Locale locale, String timeZone) {
124         DateTimeFormatter df = DateTimeFormatter.ofPattern(format,
125             Optional.ofNullable(locale)
126                 .orElseGet(Locale::getDefault));
127         ZoneId zone = null;
128         if (timeZone != null) {
129             try {
130                 zone = ZoneId.of(timeZone);
131             } catch (DateTimeException e) {
132                 LOGGER.warn(e.getMessage());
133             }
134         }
135         if (zone == null) {
136             zone = ZoneId.systemDefault();
137         }
138         df = df.withZone(zone);
139         if (LOGGER.isDebugEnabled()) {
140             Object[] parameter = { dt, zone, dt != null ? df.format(dt) : null };
141             String msg = new MessageFormat("DateTime ''{0}'', using time zone ''{1}'', formatted: {2}", Locale.ROOT)
142                 .format(parameter);
143             LOGGER.debug(msg);
144         }
145         String formatted = null;
146         try {
147             formatted = dt == null ? null : !format.contains("G") ? df.format(dt) : df.format(dt).replace("-", "");
148         } catch (Exception e) {
149             LOGGER.error("Could not format date", e);
150         }
151         return formatted;
152     }
153 
154     /**
155      * returns the Date representing this element.
156      * 
157      * @return a new Date instance of the time set in this element
158      */
159     public final Date getDate() {
160         return dt == null ? null : Date.from(Instant.from(dt));
161     }
162 
163     /**
164      * @return the dt
165      */
166     public TemporalAccessor getDt() {
167         return dt;
168     }
169 
170     /**
171      * @return the isoFormat
172      */
173     public MCRISO8601Format getIsoFormat() {
174         return isoFormat;
175     }
176 
177     /**
178      * returns a ISO 8601 conform String using the current set format.
179      * 
180      * @return date in ISO 8601 format, or null if date is unset.
181      */
182     public final String getISOString() {
183         return dt == null ? null : dateTimeFormatter.format(dt);
184     }
185 
186     /**
187      * sets the date for this meta data object.
188      * 
189      * @param dt
190      *            Date object representing date String in Element
191      */
192     public void setDate(final Date dt) {
193         if (dt == null) {
194             this.dt = null;
195         } else {
196             this.dt = dt.toInstant();
197         }
198     }
199 
200     /**
201      * sets the date for this meta data object
202      * 
203      * @param isoString
204      *            Date in any form that is a valid W3C dateTime
205      */
206     public final void setDate(final String isoString) {
207         TemporalAccessor dt = null;
208         try {
209             dt = getDateTime(MCRISO8601FormatChooser.cropSecondFractions(isoString));
210         } catch (final RuntimeException e) {
211             final boolean strictParsingEnabled = true;
212             if (!strictParsingEnabled) {
213                 /*
214                  * Last line of defence against the worst dates of the universe ;o)
215                  */
216                 LOGGER.warn("Strict date parsing is disabled. This may result in incorrect dates.");
217                 dt = guessDateTime(isoString);
218             } else {
219                 LOGGER.debug("Error while parsing date, set date to NULL.", e);
220                 dt = null;
221             }
222         }
223         setInstant(dt);
224     }
225 
226     /**
227      * sets the input and output format. please use only the formats defined on the
228      * <a href="http://www.w3.org/TR/NOTE-datetime">W3C Page</a>, which are also exported as static fields by this
229      * class.
230      */
231     public void setFormat(final MCRISO8601Format isoFormat) {
232         this.isoFormat = isoFormat;
233         dateTimeFormatter = MCRISO8601FormatChooser.getFormatter(null, this.isoFormat);
234     }
235 
236     /**
237      * sets the input and output format. please use only the formats defined on the
238      * <a href="http://www.w3.org/TR/NOTE-datetime">W3C Page</a>, which are also exported as static fields by this
239      * class.
240      * 
241      * @param format
242      *            a format string that is valid conforming to xsd:duration schema type.
243      */
244     public void setFormat(String format) {
245         setFormat(MCRISO8601Format.getFormat(format));
246     }
247 
248     private TemporalAccessor getDateTime(final String timeString) {
249         dateTimeFormatter = MCRISO8601FormatChooser.getFormatter(timeString, isoFormat);
250         return dateTimeFormatter.parseBest(timeString, ZonedDateTime::from, LocalDateTime::from, LocalDate::from,
251             YearMonth::from,
252             Year::from);
253     }
254 
255     private TemporalAccessor guessDateTime(final String date) {
256         final String locales = MCRConfiguration2.getString("MCR.Metadata.SimpleDateFormat.Locales").orElse("de_DE");
257         final StringTokenizer tok = new StringTokenizer(locales, ",");
258         while (tok.hasMoreTokens()) {
259             final Locale locale = getLocale(tok.nextToken());
260             final DateFormat df = DateFormat.getDateInstance(DateFormat.SHORT, locale);
261             df.setTimeZone(TimeZone.getTimeZone("UTC"));
262             df.setLenient(true);
263             TemporalAccessor result = null;
264             try {
265                 final Date pDate = df.parse(date);
266                 result = pDate.toInstant();
267                 return result;
268             } catch (final ParseException e) {
269                 LOGGER.warn("Date guess failed for locale: {}", locale);
270                 //we need no big exception in the logs, if we can't guess what it is, a warning should be enough
271             }
272         }
273         LOGGER.error("Error trying to guess date for string: {}", date);
274         return null;
275     }
276 
277     private void setInstant(final TemporalAccessor dt) {
278         this.dt = dt;
279     }
280 
281     @Override
282     public boolean equals(Object obj) {
283         if (obj == null) {
284             return false;
285         }
286         if (getClass() != obj.getClass()) {
287             return false;
288         }
289         final MCRISO8601Date other = (MCRISO8601Date) obj;
290         return Objects.equals(this.dt, other.dt);
291     }
292 
293 }