View Javadoc
1   package ca.uhn.fhir.model.primitive;
2   
3   /*
4    * #%L
5    * HAPI FHIR - Core Library
6    * %%
7    * Copyright (C) 2014 University Health Network
8    * %%
9    * Licensed under the Apache License, Version 2.0 (the "License");
10   * you may not use this file except in compliance with the License.
11   * You may obtain a copy of the License at
12   * 
13   *      http://www.apache.org/licenses/LICENSE-2.0
14   * 
15   * Unless required by applicable law or agreed to in writing, software
16   * distributed under the License is distributed on an "AS IS" BASIS,
17   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18   * See the License for the specific language governing permissions and
19   * limitations under the License.
20   * #L%
21   */
22  
23  import static ca.uhn.fhir.model.api.TemporalPrecisionEnum.*;
24  
25  import java.text.ParseException;
26  import java.util.Calendar;
27  import java.util.Date;
28  import java.util.GregorianCalendar;
29  import java.util.TimeZone;
30  import java.util.regex.Pattern;
31  
32  import javax.xml.bind.DatatypeConverter;
33  
34  import org.apache.commons.lang3.Validate;
35  import org.apache.commons.lang3.time.DateUtils;
36  import org.apache.commons.lang3.time.FastDateFormat;
37  
38  import ca.uhn.fhir.model.api.BasePrimitive;
39  import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
40  import ca.uhn.fhir.parser.DataFormatException;
41  
42  public abstract class BaseDateTimeDt extends BasePrimitive<Date> {
43  
44  	private static final Pattern ourYearDashMonthDashDayPattern = Pattern.compile("[0-9]{4}-[0-9]{2}-[0-9]{2}");
45  	private static final Pattern ourYearDashMonthPattern = Pattern.compile("[0-9]{4}-[0-9]{2}");
46  	private static final FastDateFormat ourYearFormat = FastDateFormat.getInstance("yyyy");
47  	private static final FastDateFormat ourYearMonthDayFormat = FastDateFormat.getInstance("yyyy-MM-dd");
48  	private static final FastDateFormat ourYearMonthDayNoDashesFormat = FastDateFormat.getInstance("yyyyMMdd");
49  	private static final Pattern ourYearMonthDayPattern = Pattern.compile("[0-9]{4}[0-9]{2}[0-9]{2}");
50  	private static final FastDateFormat ourYearMonthDayTimeFormat = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ss");
51  	private static final FastDateFormat ourYearMonthDayTimeMilliFormat = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ss.SSS");
52  	private static final FastDateFormat ourYearMonthDayTimeMilliZoneFormat = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ss.SSSZZ");
53  	private static final FastDateFormat ourYearMonthDayTimeZoneFormat = FastDateFormat.getInstance("yyyy-MM-dd'T'HH:mm:ssZZ");
54  	private static final FastDateFormat ourYearMonthFormat = FastDateFormat.getInstance("yyyy-MM");
55  	private static final FastDateFormat ourYearMonthNoDashesFormat = FastDateFormat.getInstance("yyyyMM");
56  	private static final Pattern ourYearMonthPattern = Pattern.compile("[0-9]{4}[0-9]{2}");
57  	private static final Pattern ourYearPattern = Pattern.compile("[0-9]{4}");
58  
59  	private TemporalPrecisionEnum myPrecision = TemporalPrecisionEnum.SECOND;
60  	private TimeZone myTimeZone;
61  	private boolean myTimeZoneZulu = false;
62  	private Date myValue;
63  
64  	/**
65  	 * Gets the precision for this datatype using field values from {@link Calendar}, such as {@link Calendar#MONTH}.
66  	 * Default is {@link Calendar#DAY_OF_MONTH}
67  	 * 
68  	 * @see #setPrecision(int)
69  	 */
70  	public TemporalPrecisionEnum getPrecision() {
71  		return myPrecision;
72  	}
73  
74  	public TimeZone getTimeZone() {
75  		return myTimeZone;
76  	}
77  
78  	@Override
79  	public Date getValue() {
80  		return myValue;
81  	}
82  
83  	@Override
84  	public String getValueAsString() {
85  		if (myValue == null) {
86  			return null;
87  		} else {
88  			switch (myPrecision) {
89  			case DAY:
90  				return ourYearMonthDayFormat.format(myValue);
91  			case MONTH:
92  				return ourYearMonthFormat.format(myValue);
93  			case YEAR:
94  				return ourYearFormat.format(myValue);
95  			case SECOND:
96  				if (myTimeZoneZulu) {
97  					GregorianCalendar cal = new GregorianCalendar(TimeZone.getTimeZone("GMT"));
98  					cal.setTime(myValue);
99  					return ourYearMonthDayTimeFormat.format(cal) + "Z";
100 				} else if (myTimeZone != null) {
101 					GregorianCalendar cal = new GregorianCalendar(myTimeZone);
102 					cal.setTime(myValue);
103 					return ourYearMonthDayTimeZoneFormat.format(cal);
104 				} else {
105 					return ourYearMonthDayTimeFormat.format(myValue);
106 				}
107 			case MILLI:
108 				if (myTimeZoneZulu) {
109 					GregorianCalendar cal = new GregorianCalendar(TimeZone.getTimeZone("GMT"));
110 					cal.setTime(myValue);
111 					return ourYearMonthDayTimeMilliFormat.format(cal) + "Z";
112 				} else if (myTimeZone != null) {
113 					GregorianCalendar cal = new GregorianCalendar(myTimeZone);
114 					cal.setTime(myValue);
115 					return ourYearMonthDayTimeMilliZoneFormat.format(cal);
116 				} else {
117 					return ourYearMonthDayTimeMilliFormat.format(myValue);
118 				}
119 			}
120 			throw new IllegalStateException("Invalid precision (this is a HAPI bug, shouldn't happen): " + myPrecision);
121 		}
122 	}
123 
124 	public boolean isTimeZoneZulu() {
125 		return myTimeZoneZulu;
126 	}
127 
128 	/**
129 	 * Returns <code>true</code> if this object represents a date that is today's date
130 	 * 
131 	 * @throws NullPointerException
132 	 *             if {@link #getValue()} returns <code>null</code>
133 	 */
134 	public boolean isToday() {
135 		Validate.notNull(myValue, getClass().getSimpleName() + " contains null value");
136 		return DateUtils.isSameDay(new Date(), myValue);
137 	}
138 
139 	/**
140 	 * Sets the precision for this datatype using field values from {@link Calendar}. Valid values are:
141 	 * <ul>
142 	 * <li>{@link Calendar#SECOND}
143 	 * <li>{@link Calendar#DAY_OF_MONTH}
144 	 * <li>{@link Calendar#MONTH}
145 	 * <li>{@link Calendar#YEAR}
146 	 * </ul>
147 	 * 
148 	 * @throws DataFormatException
149 	 */
150 	public void setPrecision(TemporalPrecisionEnum thePrecision) throws DataFormatException {
151 		if (thePrecision == null) {
152 			throw new NullPointerException("Precision may not be null");
153 		}
154 		myPrecision = thePrecision;
155 	}
156 
157 	public void setTimeZone(TimeZone theTimeZone) {
158 		myTimeZone = theTimeZone;
159 	}
160 
161 	public void setTimeZoneZulu(boolean theTimeZoneZulu) {
162 		myTimeZoneZulu = theTimeZoneZulu;
163 	}
164 
165 	@Override
166 	public void setValue(Date theValue) throws DataFormatException {
167 		myValue = theValue;
168 	}
169 
170 	@Override
171 	public void setValueAsString(String theValue) throws DataFormatException {
172 		try {
173 			if (theValue == null) {
174 				myValue = null;
175 				clearTimeZone();
176 			} else if (theValue.length() == 4 && ourYearPattern.matcher(theValue).matches()) {
177 				if (isPrecisionAllowed(YEAR)) {
178 					setValue((ourYearFormat).parse(theValue));
179 					setPrecision(YEAR);
180 					clearTimeZone();
181 				} else {
182 					throw new DataFormatException("Invalid date/time string (datatype " + getClass().getSimpleName() + " does not support YEAR precision): " + theValue);
183 				}
184 			} else if (theValue.length() == 6 && ourYearMonthPattern.matcher(theValue).matches()) {
185 				// Eg. 198401 (allow this just to be lenient)
186 				if (isPrecisionAllowed(MONTH)) {
187 					setValue((ourYearMonthNoDashesFormat).parse(theValue));
188 					setPrecision(MONTH);
189 					clearTimeZone();
190 				} else {
191 					throw new DataFormatException("Invalid date/time string (datatype " + getClass().getSimpleName() + " does not support DAY precision): " + theValue);
192 				}
193 			} else if (theValue.length() == 7 && ourYearDashMonthPattern.matcher(theValue).matches()) {
194 				// E.g. 1984-01 (this is valid according to the spec)
195 				if (isPrecisionAllowed(MONTH)) {
196 					setValue((ourYearMonthFormat).parse(theValue));
197 					setPrecision(MONTH);
198 					clearTimeZone();
199 				} else {
200 					throw new DataFormatException("Invalid date/time string (datatype " + getClass().getSimpleName() + " does not support MONTH precision): " + theValue);
201 				}
202 			} else if (theValue.length() == 8 && ourYearMonthDayPattern.matcher(theValue).matches()) {
203 				// Eg. 19840101 (allow this just to be lenient)
204 				if (isPrecisionAllowed(DAY)) {
205 					setValue((ourYearMonthDayNoDashesFormat).parse(theValue));
206 					setPrecision(DAY);
207 					clearTimeZone();
208 				} else {
209 					throw new DataFormatException("Invalid date/time string (datatype " + getClass().getSimpleName() + " does not support DAY precision): " + theValue);
210 				}
211 			} else if (theValue.length() == 10 && ourYearDashMonthDashDayPattern.matcher(theValue).matches()) {
212 				// E.g. 1984-01-01 (this is valid according to the spec)
213 				if (isPrecisionAllowed(DAY)) {
214 					setValue((ourYearMonthDayFormat).parse(theValue));
215 					setPrecision(DAY);
216 					clearTimeZone();
217 				} else {
218 					throw new DataFormatException("Invalid date/time string (datatype " + getClass().getSimpleName() + " does not support DAY precision): " + theValue);
219 				}
220 			} else if (theValue.length() >= 18) {
221 				int dotIndex = theValue.indexOf('.', 18);
222 				if (dotIndex == -1 && !isPrecisionAllowed(SECOND)) {
223 					throw new DataFormatException("Invalid date/time string (data type does not support SECONDS precision): " + theValue);
224 				} else if (dotIndex > -1 && !isPrecisionAllowed(MILLI)) {
225 					throw new DataFormatException("Invalid date/time string (data type " + getClass().getSimpleName() + " does not support MILLIS precision):" + theValue);
226 				}
227 
228 				Calendar cal;
229 				try {
230 					cal = DatatypeConverter.parseDateTime(theValue);
231 				} catch (IllegalArgumentException e) {
232 					throw new DataFormatException("Invalid data/time string (" + e.getMessage() + "): " + theValue);
233 				}
234 				myValue = cal.getTime();
235 				if (dotIndex == -1) {
236 					setPrecision(TemporalPrecisionEnum.SECOND);
237 				} else {
238 					setPrecision(TemporalPrecisionEnum.MILLI);
239 				}
240 
241 				clearTimeZone();
242 				if (theValue.endsWith("Z")) {
243 					myTimeZoneZulu = true;
244 				} else if (theValue.indexOf('+', 19) != -1 || theValue.indexOf('-', 19) != -1) {
245 					myTimeZone = cal.getTimeZone();
246 				}
247 
248 			} else {
249 				throw new DataFormatException("Invalid date/time string (invalid length): " + theValue);
250 			}
251 		} catch (ParseException e) {
252 			throw new DataFormatException("Invalid date string (" + e.getMessage() + "): " + theValue);
253 		}
254 	}
255 
256 	private void clearTimeZone() {
257 		myTimeZone = null;
258 		myTimeZoneZulu = false;
259 	}
260 
261 	/**
262 	 * To be implemented by subclasses to indicate whether the given precision is allowed by this type
263 	 */
264 	abstract boolean isPrecisionAllowed(TemporalPrecisionEnum thePrecision);
265 
266 }