View Javadoc
1   /*
2    *  This file is part of the Wayback archival access software
3    *   (http://archive-access.sourceforge.net/projects/wayback/).
4    *
5    *  Licensed to the Internet Archive (IA) by one or more individual 
6    *  contributors. 
7    *
8    *  The IA licenses this file to You under the Apache License, Version 2.0
9    *  (the "License"); you may not use this file except in compliance with
10   *  the License.  You may obtain a copy of the License at
11   *
12   *      http://www.apache.org/licenses/LICENSE-2.0
13   *
14   *  Unless required by applicable law or agreed to in writing, software
15   *  distributed under the License is distributed on an "AS IS" BASIS,
16   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17   *  See the License for the specific language governing permissions and
18   *  limitations under the License.
19   */
20  package org.archive.wayback.util;
21  
22  import java.text.ParseException;
23  import java.util.Calendar;
24  import java.util.Date;
25  import java.util.Locale;
26  import java.util.Map;
27  import java.util.TimeZone;
28  
29  import org.archive.util.ArchiveUtils;
30  
31  
32  /**
33   * Represents a moment in time as a 14-digit string, and interally as a Date.
34   * 
35   * @author Brad Tofel
36   * @version $Date$, $Revision$
37   */
38  public class Timestamp {
39  
40  	private final static String LOWER_TIMESTAMP_LIMIT = "10000000000000";
41  	private final static String UPPER_TIMESTAMP_LIMIT = "29991939295959";
42  	private final static String YEAR_DEFAULT_LOWER_LIMIT      = "1996";
43          private static String YEAR_LOWER_LIMIT;
44  	private final static String MONTH_LOWER_LIMIT     = "01";
45  	private final static String MONTH_UPPER_LIMIT     = "12";
46  	private final static String DAY_LOWER_LIMIT       = "01";
47  	private final static String HOUR_UPPER_LIMIT      = "23";
48  	private final static String HOUR_LOWER_LIMIT      = "00";
49  	private final static String MINUTE_UPPER_LIMIT    = "59";
50  	private final static String MINUTE_LOWER_LIMIT    = "00";
51  	private final static String SECOND_UPPER_LIMIT    = "59";
52  	private final static String SECOND_LOWER_LIMIT    = "00";
53  	
54          // This variable holds the seconds since Epoch (January 1, 1970 00:00:00 GMT) 
55          // to the start of the selected YEAR_LOWER_LIMIT.
56  	private final static int SSE_YEAR_LOWER_LIMIT;
57  
58          private final static String[] months = new String[12];
59  
60  	static {
61              // Set up the starting year.
62              
63              YEAR_LOWER_LIMIT = System.getProperty("wayback.timestamp.startyear", YEAR_DEFAULT_LOWER_LIMIT);
64              
65              Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
66              cal.set(Integer.parseInt(YEAR_LOWER_LIMIT), 0, 1, 0, 0, 0);
67              SSE_YEAR_LOWER_LIMIT = (int)(cal.getTimeInMillis() / 1000);
68                  
69              // Set up the array of shorthanded month names.
70              Map <String, Integer> month = cal.getDisplayNames(Calendar.MONTH, Calendar.SHORT, Locale.getDefault());
71              for (String s:month.keySet()) {
72                  months[month.get(s)] = s;
73              }
74  	}
75          	
76  	private String dateStr = null;
77  	private Date date = null;
78  
79  	/**
80  	 * Constructor
81  	 */
82  	public Timestamp() {
83  	}
84  	
85  	/**
86  	 * Construct and initialize structure from a 14-digit String timestamp. If
87  	 * the argument is too short, or specifies an invalid timestamp, cleanup
88  	 * will be attempted to create the earliest legal timestamp given the input.
89  	 * @param dateStr from which to set date
90  	 */
91  	public Timestamp(final String dateStr) {
92  		setDate(dateStrToDate(dateStr));
93  	}
94  
95  	/**
96  	 * Construct and initialize structure from an integer number of seconds
97  	 * since the epoch.
98  	 * @param sse SecondsSinceEpoch
99  	 */
100 	public Timestamp(final int sse) {
101 		setSse(sse);
102 	}
103 
104 	/**
105 	 * Construct and initialize structure from an Date
106 	 * @param date from which date should be set
107 	 */
108 	public Timestamp(final Date date) {
109 		setDate(date);
110 	}
111 
112 	/**
113 	 * set internal structure using Date argument
114 	 * @param date from which date should be set
115 	 */
116 	public final void setDate(final Date date) {
117 		this.date = (Date) date.clone();
118 		dateStr = ArchiveUtils.get14DigitDate(date);
119 	}
120 	
121 	
122 	/**
123 	 * @return Date for this Timestamp
124 	 */
125 	public Date getDate() {
126 		return date;
127 	}
128 
129 	/**
130 	 * set internal structure using seconds since the epoch integer argument
131 	 * @param sse SecondsSinceEpoch
132 	 */
133 	public final void setSse(final int sse) {
134 		setDate(new Date(((long)sse) * 1000));
135 	}
136 	
137 	/**
138 	 * initialize interal data structures for this Timestamp from the 14-digit
139 	 * argument. Will clean up timestamp as needed to yield the ealiest
140 	 * possible timestamp given the possible partial or wrong argument.
141 	 * 
142 	 * @param dateStr containing the timestamp
143 	 */
144 	public void setDateStr(String dateStr) {
145 		setDate(dateStrToDate(dateStr));
146 	}
147 
148 	/**
149 	 * @return the 14-digit String representation of this Timestamp.
150 	 */
151 
152 	public String getDateStr() {
153 		return dateStr;
154 	}
155 
156 	/**
157 	 * @return the integer number of seconds since epoch represented by this
158 	 *         Timestamp.
159 	 */
160 	public int sse() {
161 		return Math.round(date.getTime() / 1000);
162 	}
163 
164 	/**
165 	 * function that calculates integer seconds between this records
166 	 * timeStamp and the arguments timeStamp. result is the absolute number of
167 	 * seconds difference.
168 	 * 
169 	 * @param otherTimeStamp to compare
170 	 * @return int absolute seconds between the argument and this records
171 	 *         timestamp.
172 	 */
173 	public int absDistanceFromTimestamp(final Timestamp otherTimeStamp) {
174 		return Math.abs(distanceFromTimestamp(otherTimeStamp));
175 	}
176 
177 	/**
178 	 * function that calculates integer seconds between this records
179 	 * timeStamp and the arguments timeStamp. result is negative if this records
180 	 * timeStamp is less than the argument, positive if it is greater, and 0 if
181 	 * the same.
182 	 * 
183 	 * @param otherTimeStamp to compare
184 	 * @return int milliseconds
185 	 */
186 	public int distanceFromTimestamp(final Timestamp otherTimeStamp) {
187 		return otherTimeStamp.sse() - sse();
188 	}
189 
190 	/**
191 	 * @return the year portion(first 4 digits) of this Timestamp
192 	 */
193 	public String getYear() {
194 		return this.dateStr.substring(0, 4);
195 	}
196 
197 	/**
198 	 * @return the month portion(digits 5-6) of this Timestamp
199 	 */
200 	public String getMonth() {
201 		return this.dateStr.substring(4, 6);
202 	}
203 
204 	/**
205 	 * @return the day portion(digits 7-8) of this Timestamp
206 	 */
207 	public String getDay() {
208 		return this.dateStr.substring(6, 8);
209 	}
210 
211 	/**
212 	 * @return user friendly String representation of the date of this
213 	 *         Timestamp. eg: "Jan 13, 1999"
214 	 */
215 	public String prettyDate() {
216 		String year = dateStr.substring(0, 4);
217 		String month = dateStr.substring(4, 6);
218 		String day = dateStr.substring(6, 8);
219 		int monthInt = Integer.parseInt(month) - 1;
220 		String prettyMonth = "UNK";
221 		if ((monthInt >= 0) && (monthInt < months.length)) {
222 			prettyMonth = months[monthInt];
223 		}
224 		return prettyMonth + " " + day + ", " + year;
225 	}
226 
227 	/**
228 	 * @return user friendly String representation of the Time of this
229 	 *         Timestamp.
230 	 */
231 	public String prettyTime() {
232 		return dateStr.substring(8, 10) + ":" + dateStr.substring(10, 12) + ":"
233 				+ dateStr.substring(12, 14);
234 	}
235 
236 	/**
237 	 * @return user friendly String representation of the Date and Time of this
238 	 *         Timestamp.
239 	 */
240 	public String prettyDateTime() {
241 		return prettyDate() + " " + prettyTime();
242 	}
243 
244 	/*
245 	 * 
246 	 * ALL STATIC METHOD BELOW HERE:
247 	 * =============================
248 	 * 
249 	 */
250 	private static String getDaysInMonthBound(int year, int month) {
251 	    Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
252             cal.set(Calendar.YEAR, year);
253 	    cal.set(Calendar.MONTH, month);
254             return new Integer(cal.getActualMaximum(Calendar.DAY_OF_MONTH)).toString();
255 	}
256         
257 	/**
258 	 * @param dateStr up to 14 digit String representing date
259 	 * @return a GMT Calendar object, set to the date represented
260 	 */
261 	public static Calendar dateStrToCalendar(final String dateStr) {
262 		Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
263 		String paddedDateStr = padStartDateStr(dateStr);
264 
265 		int iYear = Integer.parseInt(paddedDateStr.substring(0,4));
266 		int iMonth = Integer.parseInt(paddedDateStr.substring(4,6));
267 		int iDay = Integer.parseInt(paddedDateStr.substring(6,8));
268 		int iHour = Integer.parseInt(paddedDateStr.substring(8,10));
269 		int iMinute = Integer.parseInt(paddedDateStr.substring(10,12));
270 		int iSecond = Integer.parseInt(paddedDateStr.substring(12,14));
271 
272 		cal.set(Calendar.YEAR,iYear);
273 		cal.set(Calendar.MONTH,iMonth - 1);
274 		cal.set(Calendar.DAY_OF_MONTH,iDay);
275 		cal.set(Calendar.HOUR_OF_DAY,iHour);
276 		cal.set(Calendar.MINUTE,iMinute);
277 		cal.set(Calendar.SECOND,iSecond);
278 		cal.set(Calendar.MILLISECOND,0);
279 		
280 		return cal;
281 	}
282         
283 	/**
284 	 * cleanup the dateStr argument assuming earliest values, and return a
285 	 * GMT calendar set to the time described by the dateStr.
286 	 * 
287 	 * @param dateStr from which to create Calendar
288 	 * @return Calendar
289 	 */
290 	public static Date dateStrToDate(final String dateStr) {
291 		
292 		String paddedDateStr = padStartDateStr(dateStr);
293 		try {
294 			return ArchiveUtils.parse14DigitDate(paddedDateStr);
295 		} catch (ParseException e) {
296 			e.printStackTrace();
297 			// TODO: This is certainly not the right thing, but padStartDateStr
298 			// should ensure we *never* get here..
299 			return new Date((long)SSE_YEAR_LOWER_LIMIT * 1000);
300 		}
301 	}
302 
303 	private static String padDigits(String input, String min, String max, 
304 			String missing) {
305 		if(input == null) {
306 			input = "";
307 		}
308 		StringBuilder finalDigits = new StringBuilder();
309 		//String finalDigits = "";
310 		for(int i = 0; i < missing.length(); i++) {
311 			if(input.length() <= i) {
312 				finalDigits.append(missing.charAt(i));
313 			} else {
314 				char inc = input.charAt(i);
315 				char maxc = max.charAt(i);
316 				char minc = min.charAt(i);
317 				if(inc > maxc) {
318 					inc = maxc;
319 				} else if (inc < minc) {
320 					inc = minc;
321 				}
322 				finalDigits.append(inc);
323 			}
324 		}
325 		
326 		return finalDigits.toString();
327 	}
328 	
329 	private static String boundDigits(final String test, final String min, 
330 			final String max) {
331 		if(test.compareTo(min) < 0) {
332 			return min;
333 		} else if(test.compareTo(max) > 0) {
334 			return max;
335 		}
336 		return test;
337 	}
338 
339 	// check each of YEAR, MONTH, DAY, HOUR, MINUTE, SECOND to make sure they
340 	// are not too large or too small, factoring in the month, leap years, etc.
341 	// BUGBUG: Leap second bug here.. How long till someone notices?
342 	private static String boundTimestamp(String input) {
343 		StringBuilder boundTimestamp = new StringBuilder();
344 		
345 		if (input == null) {
346 			input = "";
347 		}
348 		// MAKE SURE THE YEAR IS WITHIN LEGAL BOUNDARIES:
349 		boundTimestamp.append(boundDigits(input.substring(0,4),
350 				YEAR_LOWER_LIMIT, 
351                                 String.valueOf(Calendar.getInstance(TimeZone.getTimeZone("GMT")).get(Calendar.YEAR) )));
352 
353 		// MAKE SURE THE MONTH IS WITHIN LEGAL BOUNDARIES:
354 		boundTimestamp.append(boundDigits(input.substring(4,6),
355 				MONTH_LOWER_LIMIT,MONTH_UPPER_LIMIT));
356 		
357 		// NOW DEPENDING ON THE YEAR + MONTH, MAKE SURE THE DAY OF MONTH IS
358 		// WITHIN LEGAL BOUNDARIES:
359 		int iYear = Integer.parseInt(boundTimestamp.substring(0,4));
360 		int iMonth = Integer.parseInt(boundTimestamp.substring(4,6));
361 		String maxDayOfMonth = getDaysInMonthBound(iYear, iMonth-1);
362 
363 		boundTimestamp.append(boundDigits(input.substring(6,8),
364 				DAY_LOWER_LIMIT,maxDayOfMonth));
365 		
366 		// MAKE SURE THE HOUR IS WITHIN LEGAL BOUNDARIES:
367 		boundTimestamp.append(boundDigits(input.substring(8,10),
368 				HOUR_LOWER_LIMIT,HOUR_UPPER_LIMIT));
369 		
370 		// MAKE SURE THE MINUTE IS WITHIN LEGAL BOUNDARIES:
371 		boundTimestamp.append(boundDigits(input.substring(10,12),
372 				MINUTE_LOWER_LIMIT,MINUTE_UPPER_LIMIT));
373 		
374 		// MAKE SURE THE SECOND IS WITHIN LEGAL BOUNDARIES:
375 		boundTimestamp.append(boundDigits(input.substring(12,14),
376 				SECOND_LOWER_LIMIT,SECOND_UPPER_LIMIT));
377 
378 		return boundTimestamp.toString();		
379 	}
380 
381 	/**
382 	 * clean up timestamp argument assuming latest possible values for missing 
383 	 * or bogus digits.
384 	 * @param timestamp String
385 	 * @return String
386 	 */
387 	public static String padEndDateStr(String timestamp) {
388 		return boundTimestamp(padDigits(timestamp,LOWER_TIMESTAMP_LIMIT,
389 				UPPER_TIMESTAMP_LIMIT,UPPER_TIMESTAMP_LIMIT));
390 	}
391 
392 	/**
393 	 * clean up timestamp argument assuming earliest possible values for missing
394 	 * or bogus digits.
395 	 * @param timestamp String
396 	 * @return String
397 	 */
398 	public static String padStartDateStr(String timestamp) {
399 		return boundTimestamp(padDigits(timestamp,LOWER_TIMESTAMP_LIMIT,
400 				UPPER_TIMESTAMP_LIMIT,LOWER_TIMESTAMP_LIMIT));
401 	}
402 
403 	/**
404 	 * @param dateStr containing timestamp
405 	 * @return Timestamp object representing the earliest date represented by
406 	 *         the (possibly) partial digit-string argument.
407 	 */
408 	public static Timestamp parseBefore(final String dateStr) {
409 		return new Timestamp(padStartDateStr(dateStr));
410 	}
411 
412 	/**
413 	 * @param dateStr containing timestamp
414 	 * @return Timestamp object representing the latest date represented by the
415 	 *         (possibly) partial digit-string argument.
416 	 */
417 	public static Timestamp parseAfter(final String dateStr) {
418 		return new Timestamp(padEndDateStr(dateStr));
419 	}
420 
421 	/**
422 	 * @param sse SecondsSinceEpoch
423 	 * @return Timestamp object representing the seconds since epoch argument.
424 	 */
425 	public static Timestamp fromSse(final int sse) {
426 		//String dateStr = ArchiveUtils.get14DigitDate(sse * 1000);
427 		return new Timestamp(sse);
428 	}
429 
430 	/**
431 	 * @return Timestamp object representing the current date.
432 	 */
433 	public static Timestamp currentTimestamp() {
434 		return new Timestamp(new Date());
435 	}
436     
437 	/**
438 	 * @return Timestamp object representing the latest possible date.
439 	 */
440 	public static Timestamp latestTimestamp() {
441 		return currentTimestamp();
442 	}
443 
444 	/**
445 	 * @return Timestamp object representing the earliest possible date.
446 	 */
447 	public static Timestamp earliestTimestamp() {
448 		return new Timestamp(SSE_YEAR_LOWER_LIMIT);
449 	}
450         
451         /** 
452          * Set the mimimum year for the service.
453          * @param year The four digit year to set the lower limit of years handled by the server.
454          */
455         public void setStartYear(int year) {
456             YEAR_LOWER_LIMIT = Integer.toString(year);
457         }
458         
459         /**
460 	 * @return The four digit start year of the interval.
461 	 */
462         public static int getStartYear() {
463             return Integer.parseInt(YEAR_LOWER_LIMIT);
464         }
465     
466         /**
467 	 * @return The four digit end year of the interval.
468 	 */
469         public static int getEndYear() {
470             return Calendar.getInstance(TimeZone.getTimeZone("GMT")).get(Calendar.YEAR);
471         }
472 }