View Javadoc

1   /**
2    * Copyright 2005-2013 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.core.impl.datetime;
17  
18  import org.apache.commons.lang.StringUtils;
19  import org.apache.commons.lang.time.DurationFormatUtils;
20  import org.kuali.rice.core.api.datetime.DateTimeService;
21  import org.kuali.rice.core.api.CoreConstants;
22  import org.kuali.rice.core.api.config.property.ConfigContext;
23  import org.springframework.beans.factory.InitializingBean;
24  
25  import java.sql.Timestamp;
26  import java.text.DateFormat;
27  import java.text.ParseException;
28  import java.text.ParsePosition;
29  import java.text.SimpleDateFormat;
30  import java.util.Arrays;
31  import java.util.Calendar;
32  import java.util.Collections;
33  import java.util.Date;
34  import java.util.List;
35  
36  /**
37   * This class is the service implementation for a DateTime structure. This is
38   * the default, Kuali delivered implementation.
39   */
40  //@Transactional
41  public class DateTimeServiceImpl implements DateTimeService, InitializingBean {
42      /**
43       * Default date/time formats
44       */
45      private static final String STRING_TO_DATE_FORMATS = "MM/dd/yyyy hh:mm a;MM/dd/yy;MM/dd/yyyy;MM-dd-yy;MM-dd-yyyy;MMddyy;MMMM dd;yyyy;MM/dd/yy HH:mm:ss;MM/dd/yyyy HH:mm:ss;MM-dd-yy HH:mm:ss;MMddyy HH:mm:ss;MMMM dd HH:mm:ss;yyyy HH:mm:ss";
46      private static final String STRING_TO_TIMESTAMP_FORMATS = "MM/dd/yyyy hh:mm a;MM/dd/yy;MM/dd/yyyy;MM-dd-yy;MMddyy;MMMM dd;yyyy;MM/dd/yy HH:mm:ss;MM/dd/yyyy HH:mm:ss;MM-dd-yy HH:mm:ss;MMddyy HH:mm:ss;MMMM dd HH:mm:ss;yyyy HH:mm:ss";
47      private static final String DATE_TO_STRING_FORMAT_FOR_USER_INTERFACE = "MM/dd/yyyy";
48      private static final String TIMESTAMP_TO_STRING_FORMAT_FOR_USER_INTERFACE = "MM/dd/yyyy hh:mm a";
49      private static final String DATE_TO_STRING_FORMAT_FOR_FILE_NAME = "yyyyMMdd";
50      private static final String TIMESTAMP_TO_STRING_FORMAT_FOR_FILE_NAME = "yyyyMMdd-HH-mm-ss-S";
51  
52  	protected String[] stringToDateFormats;
53  	protected String[] stringToTimestampFormats;
54  	protected String dateToStringFormatForUserInterface;
55  	protected String timestampToStringFormatForUserInterface;
56  	protected String dateToStringFormatForFileName;
57  	protected String timestampToStringFormatForFileName;
58  
59  		
60  	/**
61  	 * @see org.kuali.rice.core.api.datetime.DateTimeService#toDateString(java.util.Date)
62  	 */
63  	public String toDateString(Date date) {
64  		return toString(date, dateToStringFormatForUserInterface);
65  	}
66  
67  	/**
68  	 * @see org.kuali.rice.core.api.datetime.DateTimeService#toDateTimeString(java.util.Date)
69  	 */
70  	public String toDateTimeString(Date date) {
71  		return toString(date, timestampToStringFormatForUserInterface);
72  	}
73  
74  	/**
75  	 * @see org.kuali.rice.core.api.datetime.DateTimeService#toString(java.util.Date,
76  	 *      java.lang.String)
77  	 */
78  	public String toString(Date date, String pattern) {
79  		DateFormat dateFormat = new SimpleDateFormat(pattern);
80  		dateFormat.setLenient(false);
81  		return dateFormat.format(date);
82  	}
83  
84  	/**
85  	 * @see org.kuali.rice.core.api.datetime.DateTimeService#getCurrentDate()
86  	 */
87  	public Date getCurrentDate() {
88  		Calendar c = Calendar.getInstance();
89  		c.setTime(new Date());
90  		return c.getTime();
91  	}
92  
93  	/**
94  	 * @see org.kuali.rice.core.api.datetime.DateTimeService#getCurrentTimestamp()
95  	 */
96  	public Timestamp getCurrentTimestamp() {
97  		return new java.sql.Timestamp(getCurrentDate().getTime());
98  	}
99  
100 	/**
101 	 * @see org.kuali.rice.core.api.datetime.DateTimeService#getCurrentSqlDate()
102 	 */
103 	public java.sql.Date getCurrentSqlDate() {
104 		return new java.sql.Date(getCurrentDate().getTime());
105 	}
106 
107 	/**
108 	 * @see org.kuali.rice.core.api.datetime.DateTimeService#getCurrentSqlDateMidnight()
109 	 */
110 	public java.sql.Date getCurrentSqlDateMidnight() {
111 		// simple and not unduely inefficient way to truncate the time component
112 		return java.sql.Date.valueOf(getCurrentSqlDate().toString());
113 	}
114 
115 	/**
116 	 * @see org.kuali.rice.core.api.datetime.DateTimeService#getCurrentCalendar()
117 	 */
118 	public Calendar getCurrentCalendar() {
119 		return getCalendar(getCurrentDate());
120 	}
121 
122 	/**
123 	 * @see org.kuali.rice.core.api.datetime.DateTimeService#getCalendar
124 	 */
125 	public Calendar getCalendar(Date date) {
126 		if (date == null) {
127 			throw new IllegalArgumentException("invalid (null) date");
128 		}
129 
130 		Calendar currentCalendar = Calendar.getInstance();
131 		currentCalendar.setTime(date);
132 
133 		return currentCalendar;
134 	}
135 
136 	/**
137 	 * Formats strings into dates using the format string in the KR-NS/All/STRING_TO_DATE_FORMATS parameter
138 	 *
139 	 * @see org.kuali.rice.core.api.datetime.DateTimeService#convertToDate(java.lang.String)
140 	 */
141 	public Date convertToDate(String dateString) throws ParseException {
142 		return parseAgainstFormatArray(dateString, stringToDateFormats);
143 	}
144 
145 	/**
146 	 * @see org.kuali.rice.core.api.datetime.DateTimeService#convertToDateTime(java.lang.String)
147 	 */
148 	public Date convertToDateTime(String dateTimeString) throws ParseException {
149 		if (StringUtils.isBlank(dateTimeString)) {
150 			throw new IllegalArgumentException("invalid (blank) date/time string");
151 		}
152 		return parseAgainstFormatArray(dateTimeString, stringToTimestampFormats);
153 	}
154 
155 	/**
156 	 * @see org.kuali.rice.core.api.datetime.DateTimeService#convertToSqlTimestamp(java.lang.String)
157 	 */
158 	public java.sql.Timestamp convertToSqlTimestamp(String timeString)
159 			throws ParseException {
160 		if (!StringUtils.isBlank(timeString)) {
161 			return new java.sql.Timestamp(convertToDateTime(timeString).getTime());
162 		}
163         return null;
164 	}
165 
166 	/**
167 	 * @see org.kuali.rice.core.api.datetime.DateTimeService#convertToSqlDate(java.lang.String)
168 	 */
169 	public java.sql.Date convertToSqlDate(String dateString)
170 			throws ParseException {
171 		if (StringUtils.isBlank(dateString)) {
172 			throw new IllegalArgumentException("invalid (blank) timeString");
173 		}
174 		Date date = parseAgainstFormatArray(dateString, stringToDateFormats);
175 		return new java.sql.Date(date.getTime());
176 	}
177 
178 	protected Date parseAgainstFormatArray(String dateString, String[] formats) throws ParseException {
179 		dateString = dateString.trim();
180 		StringBuffer exceptionMessage = new StringBuffer("Date or date/time string '")
181 				.append(dateString)
182 				.append("' could not be converted using any of the accepted formats: ");
183 		for (String dateFormatString : formats) {
184 			try {
185 				return parse(dateString, dateFormatString);
186 			} catch (ParseException e) {
187 				exceptionMessage.append(dateFormatString).append(
188 						" (error offset=").append(e.getErrorOffset()).append(
189 						"),");
190 			}
191 		}
192 		throw new ParseException(exceptionMessage.toString().substring(0,
193 				exceptionMessage.length() - 1), 0);
194 	}
195 
196 	/**
197 	 * @throws ParseException
198 	 * @see org.kuali.rice.core.api.datetime.DateTimeService#convertToSqlDate(java.sql.Timestamp)
199 	 */
200 	public java.sql.Date convertToSqlDate(Timestamp timestamp)
201 			throws ParseException {
202 		return new java.sql.Date(timestamp.getTime());
203 	}
204 
205 	public int dateDiff(Date startDate, Date endDate, boolean inclusive) {
206 		Calendar startDateCalendar = Calendar.getInstance();
207 		startDateCalendar.setTime(startDate);
208 
209 		Calendar endDateCalendar = Calendar.getInstance();
210 		endDateCalendar.setTime(endDate);
211 
212 		int startDateOffset = -(startDateCalendar.get(Calendar.ZONE_OFFSET) + startDateCalendar
213 				.get(Calendar.DST_OFFSET))
214 				/ (60 * 1000);
215 
216 		int endDateOffset = -(endDateCalendar.get(Calendar.ZONE_OFFSET) + endDateCalendar
217 				.get(Calendar.DST_OFFSET))
218 				/ (60 * 1000);
219 
220 		if (startDateOffset > endDateOffset) {
221 			startDateCalendar.add(Calendar.MINUTE, endDateOffset
222 					- startDateOffset);
223 		}
224 
225 		if (inclusive) {
226 			startDateCalendar.add(Calendar.DATE, -1);
227 		}
228 
229 		int dateDiff = Integer.parseInt(DurationFormatUtils.formatDuration(
230 				endDateCalendar.getTimeInMillis()
231 						- startDateCalendar.getTimeInMillis(), "d", true));
232 
233 		return dateDiff;
234 	}
235 
236 	protected Date parse(String dateString, String pattern) throws ParseException {
237 		if (!StringUtils.isBlank(dateString)) {
238 			DateFormat dateFormat = new SimpleDateFormat(pattern);
239 			dateFormat.setLenient(false);
240 			ParsePosition parsePosition = new ParsePosition(0);
241 			Date testDate = dateFormat.parse(dateString, parsePosition);
242 			
243 			// Ensure that the entire date String can be parsed by the current format.
244 			if (testDate == null) {
245 				throw new ParseException("The date that you provided is invalid.",parsePosition.getErrorIndex());
246 			} else if (parsePosition.getIndex() != dateString.length()) {
247 				throw new ParseException("The date that you provided is invalid.",parsePosition.getIndex());
248 			}
249 
250 			// Ensure that the date's year lies between 1000 and 9999, to help prevent database-related date errors.
251 			Calendar testCalendar = Calendar.getInstance();
252 			testCalendar.setLenient(false);
253 			testCalendar.setTime(testDate);
254 			if (testCalendar.get(Calendar.YEAR) < 1000 || testCalendar.get(Calendar.YEAR) > 9999) {
255 				throw new ParseException("The date that you provided is not between the years 1000 and 9999.",-1);
256 			}
257 			
258 			if(testCalendar.get(Calendar.YEAR) == 1970 && !pattern.contains("y".toLowerCase())){		    	
259 		    	Calendar curCalendar = Calendar.getInstance();
260 		    	curCalendar.setTime(new java.util.Date());
261 		    	testCalendar.set(Calendar.YEAR, curCalendar.get(Calendar.YEAR));
262 				testDate = testCalendar.getTime();
263 			}
264 			
265 			return testDate;
266 		}
267 		return null;
268 	}
269 
270 	/**
271 	 * @see org.kuali.rice.core.api.datetime.DateTimeService#toDateStringForFilename(java.util.Date)
272 	 */
273 	public String toDateStringForFilename(Date date) {
274 		SimpleDateFormat dateFormat = new SimpleDateFormat(dateToStringFormatForFileName);
275 		return dateFormat.format(date);
276 	}
277 
278 	/**
279 	 * @see org.kuali.rice.core.api.datetime.DateTimeService#toDateTimeStringForFilename(java.util.Date)
280 	 */
281 	public String toDateTimeStringForFilename(Date date) {
282 		SimpleDateFormat dateFormat = new SimpleDateFormat(timestampToStringFormatForFileName);
283 		return dateFormat.format(date);
284 	}
285 	
286 
287 	/**
288 	 * This overridden method ...
289 	 * 
290 	 * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
291 	 */
292 	@Override
293 	public void afterPropertiesSet() throws Exception {
294 		if (stringToDateFormats == null) {
295             stringToDateFormats = loadAndValidateFormats(CoreConstants.STRING_TO_DATE_FORMATS, STRING_TO_DATE_FORMATS);
296 		}
297 
298 		if (stringToTimestampFormats == null) {
299             stringToTimestampFormats = loadAndValidateFormats(CoreConstants.STRING_TO_TIMESTAMP_FORMATS, STRING_TO_TIMESTAMP_FORMATS);
300 		}
301 
302 		if (dateToStringFormatForUserInterface == null) {
303 			dateToStringFormatForUserInterface = loadAndValidateFormat(CoreConstants.DATE_TO_STRING_FORMAT_FOR_USER_INTERFACE, DATE_TO_STRING_FORMAT_FOR_USER_INTERFACE);
304 		}
305 
306 		if (timestampToStringFormatForUserInterface == null) {
307 			timestampToStringFormatForUserInterface = loadAndValidateFormat(CoreConstants.TIMESTAMP_TO_STRING_FORMAT_FOR_USER_INTERFACE, TIMESTAMP_TO_STRING_FORMAT_FOR_USER_INTERFACE);
308 		}
309 
310 		if (dateToStringFormatForFileName == null) {
311 			dateToStringFormatForFileName = loadAndValidateFormat(CoreConstants.DATE_TO_STRING_FORMAT_FOR_FILE_NAME, DATE_TO_STRING_FORMAT_FOR_FILE_NAME);
312 		}
313 
314 		if (timestampToStringFormatForFileName == null) {
315 			timestampToStringFormatForFileName = loadAndValidateFormat(CoreConstants.TIMESTAMP_TO_STRING_FORMAT_FOR_FILE_NAME, TIMESTAMP_TO_STRING_FORMAT_FOR_FILE_NAME);
316 		}
317 	}
318 
319     	/**
320 	 *
321 	 * The dateTime config vars are ';' seperated. This method should probably
322 	 * be on the config interface.
323 	 *
324 	 * @param configValue
325 	 * @return
326 	 */
327 	private List<String> parseConfigValues(String configValue) {
328 	    if (configValue == null || "".equals(configValue)) {
329 	        return Collections.emptyList();
330 	    }
331 	    return Arrays.asList(configValue.split(";"));
332 	}
333 
334     /**
335      * Loads a format string list from config or default
336      * @param property the config property
337      * @param deflt the default value
338      * @return the config or default value
339      */
340     private List<String> loadFormats(String property, String deflt) {
341         return parseConfigValues(loadFormat(property, deflt));
342     }
343 
344     /**
345      * Loads a format string list from the config or default and validates each entry
346      * @param property the config property
347      * @param deflt the default value
348      * @return string array of valid date/time formats
349      */
350     private String[] loadAndValidateFormats(String property, String deflt) {
351         List<String> dateFormatParams = loadFormats(property, deflt);
352 
353         String[] validFormats = new String[dateFormatParams.size()];
354 
355         for (int i = 0; i < dateFormatParams.size(); i++) {
356             String dateFormatParam = dateFormatParams.get(i);
357             if (StringUtils.isBlank(dateFormatParam)) {
358                 throw new IllegalArgumentException("Core/All/" + property + " parameter contains a blank semi-colon delimited substring");
359             }
360             else {
361                 // try to create a new SimpleDateFormat to try to detect illegal patterns
362                 new SimpleDateFormat(dateFormatParam);
363                 validFormats[i] = dateFormatParam;
364             }
365         }
366 
367         return validFormats;
368     }
369 
370     /**
371      * Loads a particular date format from the config, using a default for fallback
372      * @param property the config property
373      * @param deflt the default value
374      * @return the config value or default value
375      */
376     private String loadFormat(String property, String deflt) {
377         String format = ConfigContext.getCurrentContextConfig().getProperty(property);
378         if (StringUtils.isBlank(format)) {
379             format = deflt;
380         }
381         return format;
382     }
383 
384     /**
385      * Loads a particular date format from the config, using a default for fallback,
386      * and validates the format.
387      * @param property the config property
388      * @param deflt the default value
389      * @return the validated config value or default value
390      */
391     private String loadAndValidateFormat(String property, String deflt) {
392         String format = loadFormat(property, deflt);
393         // construct new SDF to make sure it's properly formatted
394         new SimpleDateFormat(format);
395         return format;
396     }
397 }