001package ca.uhn.fhir.model.primitive; 002 003/* 004 * #%L 005 * HAPI FHIR - Core Library 006 * %% 007 * Copyright (C) 2014 - 2016 University Health Network 008 * %% 009 * Licensed under the Apache License, Version 2.0 (the "License"); 010 * you may not use this file except in compliance with the License. 011 * You may obtain a copy of the License at 012 * 013 * http://www.apache.org/licenses/LICENSE-2.0 014 * 015 * Unless required by applicable law or agreed to in writing, software 016 * distributed under the License is distributed on an "AS IS" BASIS, 017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 018 * See the License for the specific language governing permissions and 019 * limitations under the License. 020 * #L% 021 */ 022import static ca.uhn.fhir.model.api.TemporalPrecisionEnum.DAY; 023import static ca.uhn.fhir.model.api.TemporalPrecisionEnum.MILLI; 024import static ca.uhn.fhir.model.api.TemporalPrecisionEnum.MONTH; 025import static ca.uhn.fhir.model.api.TemporalPrecisionEnum.SECOND; 026import static ca.uhn.fhir.model.api.TemporalPrecisionEnum.YEAR; 027 028import java.text.ParseException; 029import java.util.ArrayList; 030import java.util.Calendar; 031import java.util.Collections; 032import java.util.Date; 033import java.util.GregorianCalendar; 034import java.util.List; 035import java.util.TimeZone; 036import java.util.regex.Pattern; 037 038import org.apache.commons.lang3.Validate; 039import org.apache.commons.lang3.time.DateUtils; 040import org.apache.commons.lang3.time.FastDateFormat; 041 042import ca.uhn.fhir.model.api.BasePrimitive; 043import ca.uhn.fhir.model.api.TemporalPrecisionEnum; 044import ca.uhn.fhir.parser.DataFormatException; 045 046public abstract class BaseDateTimeDt extends BasePrimitive<Date> { 047 048 /* 049 * Add any new formatters to the static block below!! 050 */ 051 private static final List<FastDateFormat> ourFormatters; 052 053 private static final Pattern ourYearDashMonthDashDayPattern = Pattern.compile("[0-9]{4}-[0-9]{2}-[0-9]{2}"); 054 private static final Pattern ourYearDashMonthPattern = Pattern.compile("[0-9]{4}-[0-9]{2}"); 055 private static final FastDateFormat ourYearFormat = FastDateFormat.getInstance("yyyy"); 056 private static final FastDateFormat ourYearMonthDayFormat = FastDateFormat.getInstance("yyyy-MM-dd"); 057 private static final FastDateFormat ourYearMonthDayNoDashesFormat = FastDateFormat.getInstance("yyyyMMdd"); 058 private static final Pattern ourYearMonthDayPattern = Pattern.compile("[0-9]{4}[0-9]{2}[0-9]{2}"); 059 private static final FastDateFormat ourYearMonthDayTimeFormat = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ss"); 060 private static final FastDateFormat ourYearMonthDayTimeMilliFormat = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ss.SSS"); 061 private static final FastDateFormat ourYearMonthDayTimeMilliUTCZFormat = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", TimeZone.getTimeZone("UTC")); 062 private static final FastDateFormat ourYearMonthDayTimeMilliZoneFormat = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ss.SSSZZ"); 063 private static final FastDateFormat ourYearMonthDayTimeUTCZFormat = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ss'Z'", TimeZone.getTimeZone("UTC")); 064 private static final FastDateFormat ourYearMonthDayTimeZoneFormat = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ssZZ"); 065 private static final FastDateFormat ourYearMonthFormat = FastDateFormat.getInstance("yyyy-MM"); 066 private static final FastDateFormat ourYearMonthNoDashesFormat = FastDateFormat.getInstance("yyyyMM"); 067 private static final FastDateFormat ourHumanDateTimeFormat = FastDateFormat.getDateTimeInstance(FastDateFormat.MEDIUM, FastDateFormat.MEDIUM); 068 private static final FastDateFormat ourHumanDateFormat = FastDateFormat.getDateInstance(FastDateFormat.MEDIUM); 069 private static final Pattern ourYearMonthPattern = Pattern.compile("[0-9]{4}[0-9]{2}"); 070 private static final Pattern ourYearPattern = Pattern.compile("[0-9]{4}"); 071 private static final FastDateFormat ourYearMonthDayTimeMinsFormat = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm"); 072 private static final FastDateFormat ourYearMonthDayTimeMinsZoneFormat = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mmZZ"); 073 074 static { 075 ArrayList<FastDateFormat> formatters = new ArrayList<FastDateFormat>(); 076 formatters.add(ourYearFormat); 077 formatters.add(ourYearMonthDayFormat); 078 formatters.add(ourYearMonthDayNoDashesFormat); 079 formatters.add(ourYearMonthDayTimeFormat); 080 formatters.add(ourYearMonthDayTimeMilliFormat); 081 formatters.add(ourYearMonthDayTimeUTCZFormat); 082 formatters.add(ourYearMonthDayTimeMilliUTCZFormat); 083 formatters.add(ourYearMonthDayTimeMilliZoneFormat); 084 formatters.add(ourYearMonthDayTimeZoneFormat); 085 formatters.add(ourYearMonthFormat); 086 formatters.add(ourYearMonthNoDashesFormat); 087 ourFormatters = Collections.unmodifiableList(formatters); 088 } 089 090 private TemporalPrecisionEnum myPrecision = TemporalPrecisionEnum.SECOND; 091 092 private TimeZone myTimeZone; 093 private boolean myTimeZoneZulu = false; 094 private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseDateTimeDt.class); 095 096 /** 097 * Returns a human readable version of this date/time using the system local format. 098 * <p> 099 * <b>Note on time zones:</b> This method renders the value using the time zone that is contained within the value. For example, if this date object contains the value "2012-01-05T12:00:00-08:00", 100 * the human display will be rendered as "12:00:00" even if the application is being executed on a system in a different time zone. If this behaviour is not what you want, use 101 * {@link #toHumanDisplayLocalTimezone()} instead. 102 * </p> 103 */ 104 public String toHumanDisplay() { 105 TimeZone tz = getTimeZone(); 106 Calendar value = tz != null ? Calendar.getInstance(tz) : Calendar.getInstance(); 107 value.setTime(getValue()); 108 109 switch (getPrecision()) { 110 case YEAR: 111 case MONTH: 112 case DAY: 113 return ourHumanDateFormat.format(value); 114 case MILLI: 115 case SECOND: 116 default: 117 return ourHumanDateTimeFormat.format(value); 118 } 119 } 120 121 /** 122 * Returns a human readable version of this date/time using the system local format, converted to the local timezone if neccesary. 123 * 124 * @see #toHumanDisplay() for a method which does not convert the time to the local timezone before rendering it. 125 */ 126 public String toHumanDisplayLocalTimezone() { 127 switch (getPrecision()) { 128 case YEAR: 129 case MONTH: 130 case DAY: 131 return ourHumanDateFormat.format(getValue()); 132 case MILLI: 133 case SECOND: 134 default: 135 return ourHumanDateTimeFormat.format(getValue()); 136 } 137 } 138 139 /** 140 * Constructor 141 */ 142 public BaseDateTimeDt() { 143 // nothing 144 } 145 146 /** 147 * Constructor 148 * 149 * @throws DataFormatException 150 * If the specified precision is not allowed for this type 151 */ 152 public BaseDateTimeDt(Date theDate, TemporalPrecisionEnum thePrecision) { 153 setValue(theDate, thePrecision); 154 if (isPrecisionAllowed(thePrecision) == false) { 155 throw new DataFormatException("Invalid date/time string (datatype " + getClass().getSimpleName() + " does not support " + thePrecision + " precision): " + theDate); 156 } 157 } 158 159 /** 160 * Constructor 161 * 162 * @throws DataFormatException 163 * If the specified precision is not allowed for this type 164 */ 165 public BaseDateTimeDt(String theString) { 166 setValueAsString(theString); 167 if (isPrecisionAllowed(getPrecision()) == false) { 168 throw new DataFormatException("Invalid date/time string (datatype " + getClass().getSimpleName() + " does not support " + getPrecision() + " precision): " + theString); 169 } 170 } 171 172 /** 173 * Constructor 174 */ 175 public BaseDateTimeDt(Date theDate, TemporalPrecisionEnum thePrecision, TimeZone theTimeZone) { 176 this(theDate, thePrecision); 177 setTimeZone(theTimeZone); 178 } 179 180 private void clearTimeZone() { 181 myTimeZone = null; 182 myTimeZoneZulu = false; 183 } 184 185 @Override 186 protected String encode(Date theValue) { 187 if (theValue == null) { 188 return null; 189 } else { 190 GregorianCalendar cal; 191 if (myTimeZoneZulu) { 192 cal = new GregorianCalendar(TimeZone.getTimeZone("GMT")); 193 } else if (myTimeZone != null) { 194 cal = new GregorianCalendar(myTimeZone); 195 } else { 196 cal = new GregorianCalendar(); 197 } 198 cal.setTime(theValue); 199 200 switch (myPrecision) { 201 case DAY: 202 return ourYearMonthDayFormat.format(cal); 203 case MONTH: 204 return ourYearMonthFormat.format(cal); 205 case YEAR: 206 return ourYearFormat.format(cal); 207 case MINUTE: 208 if (myTimeZoneZulu) { 209 return ourYearMonthDayTimeMinsFormat.format(cal) + "Z"; 210 } else { 211 return ourYearMonthDayTimeMinsZoneFormat.format(cal); 212 } 213 case SECOND: 214 if (myTimeZoneZulu) { 215 return ourYearMonthDayTimeFormat.format(cal) + "Z"; 216 } else { 217 return ourYearMonthDayTimeZoneFormat.format(cal); 218 } 219 case MILLI: 220 if (myTimeZoneZulu) { 221 return ourYearMonthDayTimeMilliFormat.format(cal) + "Z"; 222 } else { 223 return ourYearMonthDayTimeMilliZoneFormat.format(cal); 224 } 225 } 226 throw new IllegalStateException("Invalid precision (this is a HAPI bug, shouldn't happen): " + myPrecision); 227 } 228 } 229 230 /** 231 * Returns the default precision for the given datatype 232 */ 233 protected abstract TemporalPrecisionEnum getDefaultPrecisionForDatatype(); 234 235 /** 236 * Gets the precision for this datatype (using the default for the given type if not set) 237 * 238 * @see #setPrecision(TemporalPrecisionEnum) 239 */ 240 public TemporalPrecisionEnum getPrecision() { 241 if (myPrecision == null) { 242 return getDefaultPrecisionForDatatype(); 243 } 244 return myPrecision; 245 } 246 247 /** 248 * Returns the TimeZone associated with this dateTime's value. May return <code>null</code> if no timezone was supplied. 249 */ 250 public TimeZone getTimeZone() { 251 return myTimeZone; 252 } 253 254 private boolean hasOffset(String theValue) { 255 boolean inTime = false; 256 for (int i = 0; i < theValue.length(); i++) { 257 switch (theValue.charAt(i)) { 258 case 'T': 259 inTime = true; 260 break; 261 case '+': 262 case '-': 263 if (inTime) { 264 return true; 265 } 266 break; 267 } 268 } 269 return false; 270 } 271 272 /** 273 * To be implemented by subclasses to indicate whether the given precision is allowed by this type 274 */ 275 abstract boolean isPrecisionAllowed(TemporalPrecisionEnum thePrecision); 276 277 public boolean isTimeZoneZulu() { 278 return myTimeZoneZulu; 279 } 280 281 /** 282 * Returns <code>true</code> if this object represents a date that is today's date 283 * 284 * @throws NullPointerException 285 * if {@link #getValue()} returns <code>null</code> 286 */ 287 public boolean isToday() { 288 Validate.notNull(getValue(), getClass().getSimpleName() + " contains null value"); 289 return DateUtils.isSameDay(new Date(), getValue()); 290 } 291 292 @Override 293 protected Date parse(String theValue) throws DataFormatException { 294 try { 295 if (theValue.length() == 4 && ourYearPattern.matcher(theValue).matches()) { 296 if (!isPrecisionAllowed(YEAR)) { 297 ourLog.debug("Invalid date/time string (datatype " + getClass().getSimpleName() + " does not support YEAR precision): " + theValue); 298 } 299 setPrecision(YEAR); 300 clearTimeZone(); 301 return ((ourYearFormat).parse(theValue)); 302 } else if (theValue.length() == 6 && ourYearMonthPattern.matcher(theValue).matches()) { 303 // Eg. 198401 (allow this just to be lenient) 304 if (!isPrecisionAllowed(MONTH)) { 305 ourLog.debug("Invalid date/time string (datatype " + getClass().getSimpleName() + " does not support DAY precision): " + theValue); 306 } 307 setPrecision(MONTH); 308 clearTimeZone(); 309 return ((ourYearMonthNoDashesFormat).parse(theValue)); 310 } else if (theValue.length() == 7 && ourYearDashMonthPattern.matcher(theValue).matches()) { 311 // E.g. 1984-01 (this is valid according to the spec) 312 if (!isPrecisionAllowed(MONTH)) { 313 ourLog.debug("Invalid date/time string (datatype " + getClass().getSimpleName() + " does not support MONTH precision): " + theValue); 314 } 315 setPrecision(MONTH); 316 clearTimeZone(); 317 return ((ourYearMonthFormat).parse(theValue)); 318 } else if (theValue.length() == 8 && ourYearMonthDayPattern.matcher(theValue).matches()) { 319 // Eg. 19840101 (allow this just to be lenient) 320 if (!isPrecisionAllowed(DAY)) { 321 ourLog.debug("Invalid date/time string (datatype " + getClass().getSimpleName() + " does not support DAY precision): " + theValue); 322 } 323 setPrecision(DAY); 324 clearTimeZone(); 325 return ((ourYearMonthDayNoDashesFormat).parse(theValue)); 326 } else if (theValue.length() == 10 && ourYearDashMonthDashDayPattern.matcher(theValue).matches()) { 327 // E.g. 1984-01-01 (this is valid according to the spec) 328 if (!isPrecisionAllowed(DAY)) { 329 ourLog.debug("Invalid date/time string (datatype " + getClass().getSimpleName() + " does not support DAY precision): " + theValue); 330 } 331 setPrecision(DAY); 332 clearTimeZone(); 333 return ((ourYearMonthDayFormat).parse(theValue)); 334 } else if (theValue.length() >= 18) { // date and time with possible time zone 335 int dotIndex = theValue.indexOf('.', 18); 336 boolean hasMillis = dotIndex > -1; 337 338 if (!hasMillis && !isPrecisionAllowed(SECOND)) { 339 ourLog.debug("Invalid date/time string (data type does not support SECONDS precision): " + theValue); 340 } else if (hasMillis && !isPrecisionAllowed(MILLI)) { 341 ourLog.debug("Invalid date/time string (data type " + getClass().getSimpleName() + " does not support MILLIS precision):" + theValue); 342 } 343 344 Date retVal; 345 if (hasMillis) { 346 try { 347 if (hasOffset(theValue)) { 348 retVal = ourYearMonthDayTimeMilliZoneFormat.parse(theValue); 349 } else if (theValue.endsWith("Z")) { 350 retVal = ourYearMonthDayTimeMilliUTCZFormat.parse(theValue); 351 } else { 352 retVal = ourYearMonthDayTimeMilliFormat.parse(theValue); 353 } 354 } catch (ParseException p2) { 355 throw new DataFormatException("Invalid data/time string (" + p2.getMessage() + "): " + theValue); 356 } 357 setTimeZone(theValue, hasMillis); 358 setPrecision(TemporalPrecisionEnum.MILLI); 359 } else { 360 try { 361 if (hasOffset(theValue)) { 362 retVal = ourYearMonthDayTimeZoneFormat.parse(theValue); 363 } else if (theValue.endsWith("Z")) { 364 retVal = ourYearMonthDayTimeUTCZFormat.parse(theValue); 365 } else { 366 retVal = ourYearMonthDayTimeFormat.parse(theValue); 367 } 368 } catch (ParseException p2) { 369 throw new DataFormatException("Invalid data/time string (" + p2.getMessage() + "): " + theValue); 370 } 371 372 setTimeZone(theValue, hasMillis); 373 setPrecision(TemporalPrecisionEnum.SECOND); 374 } 375 376 return retVal; 377 } else { 378 throw new DataFormatException("Invalid date/time string (invalid length): " + theValue); 379 } 380 } catch (ParseException e) { 381 throw new DataFormatException("Invalid date string (" + e.getMessage() + "): " + theValue); 382 } 383 } 384 385 /** 386 * Sets the precision for this datatype 387 * 388 * @throws DataFormatException 389 */ 390 public void setPrecision(TemporalPrecisionEnum thePrecision) throws DataFormatException { 391 if (thePrecision == null) { 392 throw new NullPointerException("Precision may not be null"); 393 } 394 myPrecision = thePrecision; 395 updateStringValue(); 396 } 397 398 private BaseDateTimeDt setTimeZone(String theValueString, boolean hasMillis) { 399 clearTimeZone(); 400 int timeZoneStart = 19; 401 if (hasMillis) 402 timeZoneStart += 4; 403 if (theValueString.endsWith("Z")) { 404 setTimeZoneZulu(true); 405 } else if (theValueString.indexOf("GMT", timeZoneStart) != -1) { 406 setTimeZone(TimeZone.getTimeZone(theValueString.substring(timeZoneStart))); 407 } else if (theValueString.indexOf('+', timeZoneStart) != -1 || theValueString.indexOf('-', timeZoneStart) != -1) { 408 setTimeZone(TimeZone.getTimeZone("GMT" + theValueString.substring(timeZoneStart))); 409 } 410 return this; 411 } 412 413 public BaseDateTimeDt setTimeZone(TimeZone theTimeZone) { 414 myTimeZone = theTimeZone; 415 updateStringValue(); 416 return this; 417 } 418 419 public BaseDateTimeDt setTimeZoneZulu(boolean theTimeZoneZulu) { 420 myTimeZoneZulu = theTimeZoneZulu; 421 updateStringValue(); 422 return this; 423 } 424 425 /** 426 * Sets the value for this type using the given Java Date object as the time, and using the default precision for this datatype, as well as the local timezone as determined by the local operating 427 * system. Both of these properties may be modified in subsequent calls if neccesary. 428 */ 429 @Override 430 public BaseDateTimeDt setValue(Date theValue) { 431 setValue(theValue, getDefaultPrecisionForDatatype()); 432 return this; 433 } 434 435 /** 436 * Sets the value for this type using the given Java Date object as the time, and using the specified precision, as well as the local timezone as determined by the local operating system. Both of 437 * these properties may be modified in subsequent calls if neccesary. 438 * 439 * @param theValue 440 * The date value 441 * @param thePrecision 442 * The precision 443 * @throws DataFormatException 444 */ 445 public void setValue(Date theValue, TemporalPrecisionEnum thePrecision) throws DataFormatException { 446 setTimeZone(TimeZone.getDefault()); 447 myPrecision = thePrecision; 448 super.setValue(theValue); 449 } 450 451 @Override 452 public void setValueAsString(String theValue) throws DataFormatException { 453 clearTimeZone(); 454 super.setValueAsString(theValue); 455 } 456 457 /** 458 * For unit tests only 459 */ 460 static List<FastDateFormat> getFormatters() { 461 return ourFormatters; 462 } 463 464}