View Javadoc
1   /*
2    * Redistribution and use of this software and associated documentation ("Software"), with or
3    * without modification, are permitted provided that the following conditions are met:
4    *
5    * 1. Redistributions of source code must retain copyright statements and notices. Redistributions
6    * must also contain a copy of this document.
7    *
8    * 2. Redistributions in binary form must reproduce the above copyright notice, this list of
9    * conditions and the following disclaimer in the documentation and/or other materials provided with
10   * the distribution.
11   *
12   * 3. The name "Exolab" must not be used to endorse or promote products derived from this Software
13   * without prior written permission of Intalio, Inc. For written permission, please contact
14   * info@exolab.org.
15   *
16   * 4. Products derived from this Software may not be called "Exolab" nor may "Exolab" appear in
17   * their names without prior written permission of Intalio, Inc. Exolab is a registered trademark of
18   * Intalio, Inc.
19   *
20   * 5. Due credit should be given to the Exolab Project (http://www.exolab.org/).
21   *
22   * THIS SOFTWARE IS PROVIDED BY INTALIO, INC. AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESSED OR
23   * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
24   * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL INTALIO, INC. OR ITS
25   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
26   * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27   * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
28   * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
29   * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30   *
31   * Copyright 2001 (C) Intalio, Inc. All Rights Reserved.
32   *
33   * $Id$ Date Author Changes 07/04/2002 Arnaud Support for milliseconds 04/18/2002 Arnaud String
34   * constructor 05/22/2000 Arnaud Blandin Created
35   */
36  package org.exolab.castor.types;
37  
38  import java.text.ParseException;
39  
40  import org.apache.commons.logging.Log;
41  import org.apache.commons.logging.LogFactory;
42  
43  /**
44   * This class is the representation of XML Schema datatype: <b>duration</b>.
45   * <p>
46   * This representation does not support the decimal fraction for the lowest order item.
47   * <p>
48   * The order relation provided by this implementation does not follow the guidelines of XML Schema
49   * Specification that defines only a partial order.
50   * <p>
51   * For more information see <a href="http://www.w3.org/TR/xmlschema-2/#duration"> X3C XML Schema
52   * Specification</a>.
53   *
54   * @author <a href="mailto:blandin@intalio.com">Arnaud Blandin</a>
55   * @author <a href="mailto:edward.kuns@aspect.com">Edward Kuns</a>
56   * @version $Revision$ $Date: 2006-04-25 15:08:23 -0600 (Tue, 25 Apr 2006) $
57   */
58  public class Duration implements java.io.Serializable {
59    /** SerialVersionUID. */
60    private static final long serialVersionUID = -6475091654291323029L;
61    /** Jakarta's common-logging logger. */
62    private static final Log LOG = LogFactory.getLog(Duration.class);
63  
64    /** Set to true and recompile to include debugging code in class. */
65    private static final boolean DEBUG = false;
66    /** the flag representing the 'T' position. */
67    private static final int TIME_FLAG = 8;
68  
69    /** the number of years. */
70    private short _year = 0;
71    /** the number of months. */
72    private short _month = 0;
73    /** the number of days. */
74    private short _day = 0;
75    /** the number of hours. */
76    private short _hour = 0;
77    /** the number of minutes. */
78    private short _minute = 0;
79    /** the number of seconds. */
80    private short _second = 0;
81    /** the potential number of milliseconds. */
82    private long _millisecond = 0;
83    /** true if the Duration is negative. */
84    private boolean _isNegative = false;
85  
86    /**
87     * default constructor.
88     */
89    public Duration() {
90      // Nothing to do
91    }
92  
93    /**
94     * Constructs a duration from a string.
95     * 
96     * @param duration the string representation of the duration to create
97     * @throws ParseException thrown when the string is not a valid duration
98     */
99    public Duration(String duration) throws ParseException {
100     parseDurationInternal(duration, this);
101   }
102 
103   /**
104    * This constructor fills in the duration fields according to the value of the long by calling
105    * setValue.
106    *
107    * @see #setValue
108    * @param l the long value of the Duration
109    */
110   public Duration(long l) {
111     long refSecond = 1000;
112     long refMinute = 60 * refSecond;
113     long refHour = 60 * refMinute;
114     long refDay = 24 * refHour;
115     long refMonth = (long) (30.42 * refDay);
116     long refYear = 12 * refMonth;
117 
118     if (DEBUG) {
119       System.out.println("In time duration Constructor");
120       System.out.println("long : " + l);
121     }
122 
123     if (l < 0) {
124       this.setNegative();
125       l = -l;
126     }
127 
128     short year = (short) (l / refYear);
129     l = l % refYear;
130     if (DEBUG) {
131       System.out.println("nb years:" + year);
132       System.out.println("New long : " + l);
133     }
134 
135     short month = (short) (l / refMonth);
136     l = l % refMonth;
137     if (DEBUG) {
138       System.out.println("nb months:" + month);
139       System.out.println("New long : " + l);
140       System.out.println(refDay);
141     }
142 
143     short day = (short) (l / refDay);
144     l = l % refDay;
145     if (DEBUG) {
146       System.out.println("nb days:" + day);
147       System.out.println("New long : " + l);
148     }
149 
150     short hour = (short) (l / refHour);
151     l = l % refHour;
152     if (DEBUG) {
153       System.out.println("nb hours:" + hour);
154       System.out.println("New long : " + l);
155     }
156 
157     short minute = (short) (l / refMinute);
158     l = l % refMinute;
159     if (DEBUG) {
160       System.out.println("nb minutes:" + minute);
161       System.out.println("New long : " + l);
162     }
163 
164     short seconds = (short) (l / refSecond);
165     l = l % refSecond;
166     if (DEBUG) {
167       System.out.println("nb seconds:" + seconds);
168     }
169 
170     long milliseconds = l;
171     if (DEBUG) {
172       System.out.println("nb milliseconds:" + milliseconds);
173     }
174 
175     this.setValue(year, month, day, hour, minute, seconds, milliseconds);
176   }
177 
178   // Set methods
179 
180   public void setYear(short year) {
181     if (year < 0) {
182       String err = "In a duration all fields have to be positive.";
183       throw new IllegalArgumentException(err);
184     }
185     _year = year;
186   }
187 
188   public void setMonth(short month) {
189     if (month < 0) {
190       String err = "In a duration all fields have to be positive.";
191       throw new IllegalArgumentException(err);
192     }
193     _month = month;
194   }
195 
196   public void setDay(short day) {
197     if (day < 0) {
198       String err = "In a duration all fields have to be positive.";
199       throw new IllegalArgumentException(err);
200     }
201     _day = day;
202   }
203 
204   public void setHour(short hour) {
205     if (hour < 0) {
206       String err = "In a duration all fields have to be positive.";
207       throw new IllegalArgumentException(err);
208     }
209     _hour = hour;
210   }
211 
212   public void setMinute(short minute) {
213     if (minute < 0) {
214       String err = "In a duration all fields have to be positive.";
215       throw new IllegalArgumentException(err);
216     }
217     _minute = minute;
218   }
219 
220   public void setSeconds(short second) {
221     if (second < 0) {
222       String err = "In a duration all fields have to be positive.";
223       throw new IllegalArgumentException(err);
224     }
225     _second = second;
226   }
227 
228   public void setMilli(long milli) {
229     if (milli < 0) {
230       String err = "In a duration all fields have to be positive.";
231       throw new IllegalArgumentException(err);
232     }
233     _millisecond = milli;
234   }
235 
236   public void setNegative() {
237     _isNegative = true;
238   }
239 
240   /**
241    * Fill in the fields of the duration with the given values
242    * 
243    * @param year the year value
244    * @param month the month value
245    * @param day the day value
246    * @param hour the hour value
247    * @param minute the minute value
248    * @param second the second value
249    * @param millisecond the second value
250    */
251   public void setValue(short year, short month, short day, short hour, short minute, short second,
252       long millisecond) {
253     this.setYear(year);
254     this.setMonth(month);
255     this.setDay(day);
256     this.setHour(hour);
257     this.setMinute(minute);
258     this.setSeconds(second);
259     this.setMilli(millisecond);
260   }
261 
262   // Get methods
263 
264   public short getYear() {
265     return _year;
266   }
267 
268   public short getMonth() {
269     return _month;
270   }
271 
272   public short getDay() {
273     return _day;
274   }
275 
276   public short getHour() {
277     return _hour;
278   }
279 
280   public short getMinute() {
281     return _minute;
282   }
283 
284   public short getSeconds() {
285     return _second;
286   }
287 
288   public long getMilli() {
289     return _millisecond;
290   }
291 
292   public boolean isNegative() {
293     return _isNegative;
294   }
295 
296   /**
297    * <p>
298    * Convert a duration into a long This long represents the duration in milliseconds.
299    * 
300    * @return a long representing the duration
301    */
302   public long toLong() {
303     long result = 0;
304 
305     // 30.42 days in a month (365/12) (Horner method)
306     result =
307         (long) (((((((_year * 12L) + _month) * 30.42 + _day) * 24L + _hour) * 60L + _minute) * 60L
308             + _second) * 1000L + _millisecond);
309 
310     result = isNegative() ? -result : result;
311     return result;
312   }
313 
314   /**
315    * Convert a duration into a String conforming to ISO8601 and
316    * <a href="http://www.w3.org/TR/xmlschema-2/#duration"> XML Schema specs </a>
317    *
318    * @return a string representing the duration
319    */
320   public String toString() {
321     // if the duration is empty, we choose as a standard to return "PTOS"
322     if (this.toLong() == 0) {
323       return "PT0S";
324     }
325 
326     StringBuilder result = new StringBuilder();
327     if (_isNegative) {
328       result.append('-');
329     }
330     result.append('P');
331 
332     if (_year != 0) {
333       result.append(_year).append('Y');
334     }
335 
336     if (_month != 0) {
337       result.append(_month).append('M');
338     }
339 
340     if (_day != 0) {
341       result.append(_day).append('D');
342     }
343 
344     boolean isThereTime = _hour != 0 || _minute != 0 || _second != 0 || _millisecond != 0;
345     if (isThereTime) {
346       result.append('T');
347 
348       if (_hour != 0) {
349         result.append(_hour).append('H');
350       }
351 
352       if (_minute != 0) {
353         result.append(_minute).append('M');
354       }
355 
356       if (_second != 0 || _millisecond != 0) {
357         result.append(_second);
358         if (_millisecond != 0) {
359           result.append('.');
360           if (_millisecond < 100) {
361             result.append('0');
362             if (_millisecond < 10)
363               result.append('0');
364           }
365           result.append(_millisecond);
366         }
367         result.append('S');
368       }
369     }
370 
371     return result.toString();
372   } // toString
373 
374   /**
375    * parse a String and convert it into a java.lang.Object
376    * 
377    * @param str the string to parse
378    * @return the java.lang.Object represented by the string
379    * @throws ParseException a parse exception is thrown if the string to parse does not follow the
380    *         rigth format (see the description of this class)
381    */
382   public static Object parse(String str) throws ParseException {
383     return parseDuration(str);
384   }
385 
386   /**
387    * <p>
388    * Parse the given string and return a time duration which represents this string.
389    * 
390    * @param str the string to parse
391    * @return a TimeDuration instance which represent the string
392    * @throws ParseException thrown when the string is not valid
393    */
394   public static Duration parseDuration(String str) throws ParseException {
395     Duration result = new Duration();
396     return parseDurationInternal(str, result);
397   }
398 
399   private static Duration parseDurationInternal(String str, Duration result) throws ParseException {
400     boolean isMilli = false;
401     if (str == null) {
402       throw new IllegalArgumentException("the string to be parsed must not be null");
403     }
404 
405     // str = "" means a null TimeDuration
406     if (str.length() == 0) {
407       return null;
408     }
409 
410     if (result == null) {
411       result = new Duration();
412     }
413 
414     char[] chars = str.toCharArray();
415     int idx = 0;
416 
417     if (chars[idx] == '-') {
418       ++idx;
419       result.setNegative();
420       if (idx >= chars.length) {
421         throw new ParseException("'-' is wrongly placed", 0);
422       }
423     }
424 
425     // -- make sure we start with 'P'
426     if (chars[idx] != 'P') {
427       throw new ParseException("Missing 'P' delimiter", idx);
428     }
429     ++idx;
430 
431     if (idx == chars.length) {
432       throw new ParseException("Bad format for a duration:" + str, idx);
433     }
434     int number = 0;
435     boolean hasNumber = false;
436 
437     // -- parse flags
438     // YMDTHMS = b1111111 (127)
439     // Year = 64, Month = 32, etc
440     int flags = 0;
441 
442     while (idx < chars.length) {
443 
444       char ch = chars[idx++];
445 
446       switch (ch) {
447 
448         // -- Year
449         case 'Y':
450           // -- check for error
451           if (flags > 0) {
452             String err = str + ":Syntax error, 'Y' must proceed all other delimiters.";
453             throw new ParseException(err, idx);
454           }
455           // --set flags
456           flags = 64;
457           if (hasNumber) {
458             result.setYear((short) number);
459             hasNumber = false;
460           } else {
461             String err = str + ":missing number of years before 'Y'";
462             throw new ParseException(err, idx);
463           }
464 
465           break;
466         // -- Month or Minute
467         case 'M':
468           // -- Either month or minute, check for T flag,
469           // -- if present then Minute, otherwise Month
470           if ((flags & TIME_FLAG) == 8) {
471 
472             // make sure no existing minute or second
473             // flags have been set.
474             if ((flags & 3) > 0) {
475               throw new ParseException(str + ": Syntax Error...", idx);
476             }
477             flags = flags | 2;
478             if (hasNumber) {
479               result.setMinute((short) number);
480               hasNumber = false;
481             } else {
482               String err = str + ": missing number of minutes before 'M'";
483               throw new ParseException(err, idx);
484             }
485           }
486           // -- Month
487           else {
488             // make sure no existing month, day or time
489             // flags have been set
490             if ((flags & 63) > 0) {
491               throw new ParseException(str + ":Syntax Error...", idx);
492             }
493             flags = flags | 32;
494             if (hasNumber) {
495               result.setMonth((short) number);
496               hasNumber = false;
497             } else {
498               String err = str + ":missing number of months before 'M'";
499               throw new ParseException(err, idx);
500             }
501           }
502           break;
503         // -- Day
504         case 'D':
505           // make sure no day or time flags have been set
506           if ((flags & 31) > 0) {
507             throw new ParseException(str + ":Syntax Error...", idx);
508           }
509           flags = flags | 16;
510           if (hasNumber) {
511             result.setDay((short) number);
512             hasNumber = false;
513           } else {
514             String err = str + ":missing number of days before 'D'";
515             throw new ParseException(err, idx);
516           }
517           break;
518         // -- Time
519         case 'T':
520           // make sure no T flag already exists
521           if ((flags & TIME_FLAG) == 8) {
522             String err = str + ":Syntax error, 'T' may not " + "exist more than once.";
523             throw new ParseException(err, idx);
524           }
525           flags = flags | 8;
526           break;
527         // -- Hour
528         case 'H':
529           // make sure no time flags have been set, but
530           // that T exists
531           if ((flags & 15) != 8) {
532             String err = null;
533             if ((flags & 8) != 8)
534               err = str + ": Missing 'T' before 'H'";
535             else
536               err = str + ": Syntax Error, 'H' must appear for 'M' or 'S'";
537             throw new ParseException(err, idx);
538           }
539           flags = flags | 4;
540           if (hasNumber) {
541             result.setHour((short) number);
542             hasNumber = false;
543           } else {
544             String err = str + ":missing number of hours before 'H'";
545             throw new ParseException(err, idx);
546           }
547           break;
548         case 'S':
549           if (flags != 0) {
550             // make sure T exists, but no 'S'
551             if ((flags & 8) != 8) {
552               String err = str + ": Missing 'T' before 'S'";
553               throw new ParseException(err, idx);
554             }
555             if ((flags & 1) == 1) {
556               String err = str + ": Syntax error 'S' may not exist more than once.";
557               throw new ParseException(err, idx);
558             }
559 
560             flags = flags | 1;
561             if (hasNumber) {
562               result.setSeconds((short) number);
563               hasNumber = false;
564             } else {
565               String err = str + ": missing number of seconds before 'S'";
566               throw new ParseException(err, idx);
567             }
568           } else {
569             if (hasNumber) {
570               // remove the "1" prefix used to assist in respecting the decimal place
571               String numb = Integer.toString(number).replaceFirst("1", "");
572 
573               number = Integer.parseInt(numb);
574               if (numb.length() < 3) {
575                 if (numb.length() < 2) {
576                   number = number * 10;
577                 }
578                 number = number * 10;
579               }
580               result.setMilli((long) number);
581               hasNumber = false;
582             } else {
583               String err = str + ": missing number of milliseconds before 'S'";
584               throw new ParseException(err, idx);
585             }
586           }
587 
588           break;
589 
590         case '.':
591 
592           // make sure T exists, but no 'S'
593           if ((flags & 8) != 8) {
594             String err = str + ": Missing 'T' before 'S'";
595             throw new ParseException(err, idx);
596           }
597 
598           if ((flags | 1) == 1) {
599             String err = str + ": Syntax error '.' may not exist more than once.";
600             throw new ParseException(err, idx);
601           }
602 
603           flags = 0;
604 
605           if (hasNumber) {
606             result.setSeconds((short) number);
607             hasNumber = false;
608           } else {
609             String err = str + ": missing number of seconds before 'S'";
610             throw new ParseException(err, idx);
611           }
612           isMilli = true;
613           break;
614 
615         default:
616           // make sure ch is a digit...
617           if ('0' <= ch && ch <= '9') {
618             if (hasNumber) {
619               number = (number * 10) + (ch - 48);
620             } else {
621               hasNumber = true;
622               if (isMilli) {
623                 // prefix with "1" temporarily to assist in respecting the decimal place
624                 number = Integer.parseInt("1" + (ch - 48));
625               } else {
626                 number = ch - 48;
627               }
628             }
629           } else {
630             throw new ParseException(str + ":Invalid character: " + ch, idx);
631           }
632           break;
633       }
634     }
635 
636     // -- check for T, but no HMS
637     if ((flags & 15) == 8) {
638       LOG.warn("Warning: " + str + ": T shall be omitted");
639     }
640 
641     if (hasNumber) {
642       throw new ParseException(str + ": expecting ending delimiter", idx);
643     }
644 
645     return result;
646   } // parse
647 
648 
649   /**
650    * {@inheritDoc} Overrides the java.lang.Object#hashcode method.
651    */
652   public int hashCode() {
653     return 37 * (_year ^ _month ^ _day ^ _hour ^ _minute ^ _second);
654   }
655 
656   /**
657    * {@inheritDoc} Override the java.lang.equals method
658    * 
659    * @see #equal
660    */
661   public boolean equals(Object object) {
662     if (object instanceof Duration) {
663       return equal((Duration) object);
664     }
665     return false;
666   }
667 
668   /**
669    * Returns true if the instance of TimeDuration has the same fields of the parameter
670    * 
671    * @param duration the time duration to compare
672    * @return true if equal, false if not
673    */
674   public boolean equal(Duration duration) {
675     boolean result = false;
676     if (duration == null) {
677       return result;
678     }
679     result = (_year == duration.getYear());
680     result = result && (_month == duration.getMonth());
681     result = result && (_day == duration.getDay());
682     result = result && (_hour == duration.getHour());
683     result = result && (_minute == duration.getMinute());
684     result = result && (_second == duration.getSeconds());
685     result = result && (_millisecond == duration.getMilli());
686     result = result && (this.isNegative() == duration.isNegative());
687     return result;
688   } // equals
689 
690   /**
691    * Returns true if the present instance of TimeDuration is greater than the parameter
692    * <p>
693    * Note This definition does not follow the XML SCHEMA RECOMMENDATION 05022001 the following total
694    * order relation is used : <tt>givent t1,t2 timeDuration types
695    * t1>t2 iff t1.toLong()>t2.toLong()</tt>
696    *
697    * @param duration the time duration to compare with the present instance
698    * @return true if the present instance is the greatest, false if not
699    */
700   public boolean isGreater(Duration duration) {
701     boolean result = false;
702     // to be optimized ??
703     result = this.toLong() > duration.toLong();
704     return result;
705   } // isGreater
706 
707 } // Duration