View Javadoc

1   package org.apache.velocity.tools.generic;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with 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,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.util.Collections;
23  import java.util.Calendar;
24  import java.util.Iterator;
25  import java.util.LinkedHashMap;
26  import java.util.Locale;
27  import java.util.Map;
28  import java.util.MissingResourceException;
29  import java.util.ResourceBundle;
30  
31  /**
32   * Tool for comparing {@link java.util.Date} and {@link Calendar} values
33   * in Velocity templates.  This is a subclass of {@link DateTool}
34   * and thus provides all the functionality of that tool and
35   * augments it with the ability to find the relationship between
36   * any date and the current date, or between any two dates.
37   * This comparison can result in either a textual representation
38   * of the relationship (e.g. "3 weeks, 2 days ago", "tomorrow", or
39   * "3 hrs away") or the value of a specific time unit may be requested.
40   * When using the textual representations, you can configure the
41   * tool to use alternate resource bundles and to skip over units
42   * you do not want to be included.
43   * <p><pre>
44   * Example of formatting the "current" date:
45   *  $date.whenIs('2005-07-04')                -> 1 year ago
46   *  $date.whenIs('2007-02-15').full           -> 1 year 32 weeks 2 days 17 hours 38 minutes 44 seconds 178 milliseconds ago
47   *  $date.whenIs('2007-02-15').days           -> -730
48   *  $date.whenIs($date.calendar)              -> now
49   *  $date.whenIs('2005-07-04', '2005-07-04')  -> same time
50   *  $date.difference('2005-07-04','2005-07-04')      -> 0 milliseconds
51   *  $date.difference('2005-07-04','2007-02-15').abbr -> 1 yr
52   *
53   * Example tools.xml config (if you want to use this with VelocityView):
54   * &lt;tools&gt;
55   *   &lt;toolbox scope="application"&gt;
56   *     &lt;tool class="org.apache.velocity.tools.generic.ComparisonDateTool"
57   *              format="yyyy-MM-dd" depth="1" skip="month,week,millisecond"
58   *              bundle="org.apache.velocity.tools.generic.times"/&gt;
59   *   &lt;/toolbox&gt;
60   * &lt;/tools&gt;
61   * </pre></p>
62   *
63   * @author Nathan Bubna
64   * @author Chris Townsen
65   * @since VelocityTools 1.4
66   * @version $Revision: 595822 $ $Date: 2006-04-04 12:35:17 -0700 (Tue, 04 Apr 2006) $
67   */
68  public class ComparisonDateTool extends DateTool
69  {
70      /** The number of milliseconds in a second. */
71      public static final long MILLIS_PER_SECOND = 1000l;
72      
73      /** The number of millseconds in a minute. */
74      public static final long MILLIS_PER_MINUTE = 60l * MILLIS_PER_SECOND;
75      
76      /** The number of milliseconds in an hour. */
77      public static final long MILLIS_PER_HOUR = 60l * MILLIS_PER_MINUTE;
78      
79      /** The number of milliseconds in a day. */
80      public static final long MILLIS_PER_DAY = 24l * MILLIS_PER_HOUR;
81      
82      /** The number of milliseconds in a week. */
83      public static final long MILLIS_PER_WEEK = 7l * MILLIS_PER_DAY;
84      
85      /** An approximation of the number of milliseconds in a month. */
86      public static final long MILLIS_PER_MONTH = 30l * MILLIS_PER_DAY;
87      
88      /** An approximation of the number of milliseconds in a year. */
89      public static final long MILLIS_PER_YEAR = 365l * MILLIS_PER_DAY;
90  
91      /** The key used for specifying a default locale via toolbox params. */
92      public static final String BUNDLE_NAME_KEY = "bundle";
93  
94      /** The key used for specifying a different default depth via toolbox params. */
95      public static final String DEPTH_KEY = "depth";
96  
97      /** The key used for specifying time units to be skipped over. */
98      public static final String SKIPPED_UNITS_KEY = "skip";
99  
100     /** The default path of the relative format resource bundles. */
101     public static final String DEFAULT_BUNDLE_NAME = 
102         "org.apache.velocity.tools.generic.times";
103 
104 
105     // time unit message keys
106     protected static final String MILLISECOND_KEY = "millisecond";
107     protected static final String SECOND_KEY = "second";
108     protected static final String MINUTE_KEY = "minute";
109     protected static final String HOUR_KEY = "hour";
110     protected static final String DAY_KEY = "day";
111     protected static final String WEEK_KEY = "week";
112     protected static final String MONTH_KEY = "month";
113     protected static final String YEAR_KEY = "year";
114 
115     /** Array of all time unit message keys to their millisecond conversion factor. */
116     protected static final Map TIME_UNITS;
117     static
118     {
119         Map units = new LinkedHashMap(8);
120         units.put(MILLISECOND_KEY, Long.valueOf(1));
121         units.put(SECOND_KEY, Long.valueOf(MILLIS_PER_SECOND));
122         units.put(MINUTE_KEY, Long.valueOf(MILLIS_PER_MINUTE));
123         units.put(HOUR_KEY, Long.valueOf(MILLIS_PER_HOUR));
124         units.put(DAY_KEY, Long.valueOf(MILLIS_PER_DAY));
125         units.put(WEEK_KEY, Long.valueOf(MILLIS_PER_WEEK));
126         units.put(MONTH_KEY, Long.valueOf(MILLIS_PER_MONTH));
127         units.put(YEAR_KEY, Long.valueOf(MILLIS_PER_YEAR));
128         TIME_UNITS = Collections.unmodifiableMap(units);
129     }
130 
131     // special message keys/prefixes/suffixes
132     protected static final String CURRENT_PREFIX = "current.";
133     protected static final String AFTER_KEY = "after";
134     protected static final String BEFORE_KEY = "before";
135     protected static final String EQUAL_KEY = "equal";
136     protected static final String ZERO_KEY = "zero";
137     protected static final String ABBR_SUFFIX = ".abbr";
138     protected static final String ONE_DAY_SUFFIX = ".day";
139     protected static final String PLURAL_SUFFIX = "s";
140 
141     // display types
142     protected static final int CURRENT_TYPE = 0;
143     protected static final int RELATIVE_TYPE = 1;
144     protected static final int DIFF_TYPE = 2;
145 
146     private String bundleName = DEFAULT_BUNDLE_NAME;
147     private ResourceBundle defaultBundle;
148     private Map timeUnits = TIME_UNITS;
149     private int depth = 1;
150 
151 
152     /**
153      * Calls the superclass implementation, then looks for a bundle name
154      * and any time units to be skipped.
155      */
156     protected void configure(ValueParser values)
157     {
158         // do DateTool config
159         super.configure(values);
160 
161         // look for an alternate bundle
162         String bundle = values.getString(BUNDLE_NAME_KEY);
163         if (bundle != null)
164         {
165             this.bundleName = bundle;
166         }
167 
168         this.depth = values.getInt(DEPTH_KEY, 1);
169 
170         // look for time units to be ignored
171         String[] skip = values.getStrings(SKIPPED_UNITS_KEY);
172         if (skip != null)
173         {
174             timeUnits = new LinkedHashMap(TIME_UNITS);
175             for (int i=0; i < skip.length; i++)
176             {
177                 timeUnits.remove(skip[i]);
178             }
179         }
180     }
181 
182     /**
183      * Retrieves the specified text resource.
184      */
185     protected String getText(String key, Locale locale)
186     {
187         Locale defaultLocale = getLocale();
188         ResourceBundle bundle = null;
189         // if there is no locale or the specified locale equals the tool's default
190         if (locale == null || locale.equals(defaultLocale))
191         {
192             if (defaultBundle == null)
193             {
194                 // load the bundle for the default locale
195                 try
196                 {
197                     // and cache it
198                     defaultBundle = ResourceBundle.getBundle(this.bundleName,
199                                                              defaultLocale);
200                 }
201                 catch (MissingResourceException e) {}
202             }
203 
204             // use the default locale's bundle
205             bundle = defaultBundle;
206         }
207         else
208         {
209             // load the bundle for the specified locale
210             try
211             {
212                 bundle = ResourceBundle.getBundle(this.bundleName, locale);
213             }
214             catch (MissingResourceException e) {}
215         }
216 
217         // if we found a bundle...
218         if (bundle != null)
219         {
220             try
221             {
222                 // try to return the specified key
223                 return bundle.getString(key);
224             }
225             catch (MissingResourceException e) {}
226         }
227 
228         // otherwise, return the key as an error
229         return "???" + key + "???";
230     }
231 
232 
233     // ------------------------- static millisecond conversions ----------------
234 
235     /**
236      * Returns the number of whole Years in the specified number of milliseconds.
237      */
238     public static long toYears(long ms)
239     {
240         return ms / MILLIS_PER_YEAR;
241     }
242 
243     /**
244      * Returns the number of whole Months in the specified number of milliseconds.
245      */
246     public static long toMonths(long ms)
247     {
248         return ms / MILLIS_PER_MONTH;
249     }
250 
251     /**
252      * Returns the number of whole Weeks in the specified number of milliseconds.
253      */
254     public static long toWeeks(long ms)
255     {
256         return ms / MILLIS_PER_WEEK;
257     }
258 
259     /**
260      * Returns the number of whole Days in the specified number of milliseconds.
261      */
262     public static long toDays(long ms)
263     {
264         return ms / MILLIS_PER_DAY;
265     }
266 
267     /**
268      * Returns the number of whole Hours in the specified number of milliseconds.
269      */
270     public static long toHours(long ms)
271     {
272         return ms / MILLIS_PER_HOUR;
273     }
274 
275     /**
276      * Returns the number of whole Minutes in the specified number of milliseconds.
277      */
278     public static long toMinutes(long ms)
279     {
280         return ms / MILLIS_PER_MINUTE;
281     }
282 
283     /**
284      * Returns the number of whole Seconds in the specified number of milliseconds.
285      */
286     public static long toSeconds(long ms)
287     {
288         return ms / MILLIS_PER_SECOND;
289     }
290 
291 
292     // ------------------------- date comparison ---------------------------
293 
294     /**
295      * Returns a {@link Comparison} between the result of
296      * {@link #getCalendar()} and the specified date.  The default
297      * rendering of that Comparison will be the largest unit difference
298      * between the dates followed by a description of their relative position.
299      * 
300      * @param then The date in question
301      */
302     public Comparison whenIs(Object then)
303     {
304         return compare(getCalendar(), then, CURRENT_TYPE);
305     }
306 
307     /**
308      * Returns a {@link Comparison} between the second specified date
309      * and the first specified date.  The default
310      * rendering of that Comparison will be the largest unit difference
311      * between the dates followed by a description of their relative position.
312      * 
313      * @param now The date to use as representative of "now"
314      * @param then The date in question
315      */
316     public Comparison whenIs(Object now, Object then)
317     {
318         return compare(now, then, RELATIVE_TYPE);
319     }
320 
321     /**
322      * Returns a {@link Comparison} between the result of
323      * the second specified date and the first specified date.  The default
324      * rendering of that Comparison will be the largest unit difference
325      * between the dates.
326      * 
327      * @param now The date to use as representative of "now"
328      * @param then The secondary date
329      */
330     public Comparison difference(Object now, Object then)
331     {
332         return compare(now, then, DIFF_TYPE);
333     }
334 
335     protected Comparison compare(Object now, Object then, int type)
336     {
337         Calendar calThen = toCalendar(then);
338         Calendar calNow = toCalendar(now);
339         if (calThen == null || calNow == null)
340         {
341             return null;
342         }
343 
344         long ms = calThen.getTimeInMillis() - calNow.getTimeInMillis();
345         return new Comparison(ms, type, this.depth, false, null);
346     }
347         
348 
349     /**
350      * @param ms The time in milliseconds
351      * @param type Whether the time should be represented as relative to "now",
352      *             relative to some other time, or as a mere difference.
353      * @param depth The maximum number of units deep to show
354      * @param abbr Whether the units should be abbreviated or not
355      * @param loc The locale to be used when looking up resources
356      */
357     protected String toString(long ms, int type, int depth,
358                               boolean abbr, Locale loc)
359     {
360         // first check if there is a difference
361         if (ms == 0)
362         {
363             String sameKey = (abbr) ? ABBR_SUFFIX : "";
364             if (type == CURRENT_TYPE)
365             {
366                 sameKey = CURRENT_PREFIX + EQUAL_KEY + sameKey;
367             }
368             else if (type == RELATIVE_TYPE)
369             {
370                 sameKey = EQUAL_KEY + sameKey;
371             }
372             else
373             {
374                 sameKey = ZERO_KEY + sameKey;
375             }
376             return getText(sameKey, loc);
377         }
378 
379         boolean isBefore = false;
380         if (ms < 0)
381         {
382             isBefore = true;
383             // convert() only works with positive values
384             ms *= -1;
385         }
386 
387         // get the base value
388         String friendly = toString(ms, depth, abbr, loc);
389 
390         // if we only want the difference...
391         if (type == DIFF_TYPE)
392         {
393             // add the sign (if negative)
394             if (isBefore)
395             {
396                 friendly = "-" + friendly;
397             }
398             // then return without direction suffix
399             return friendly;
400         }
401             
402         // otherwise, get the appropriate direction key
403         String directionKey = (isBefore) ? BEFORE_KEY : AFTER_KEY;
404         if (type == CURRENT_TYPE)
405         {
406             directionKey = CURRENT_PREFIX + directionKey;
407             
408             if (friendly != null && friendly.startsWith("1"))
409             {
410                 // check for the corner case of "1 day ago" or "1 day away"
411                 // and convert those to "yesterday" or "tomorrow"
412                 String dayKey = (abbr) ? DAY_KEY + ABBR_SUFFIX : DAY_KEY;
413                 if (friendly.equals("1 " + getText(dayKey, loc)))
414                 {
415                     // add .day
416                     directionKey += ONE_DAY_SUFFIX;
417                     // and return only the value of this key
418                     // (which means we throw away the friendly value
419                     //  and don't bother abbreviating things)
420                     return getText(directionKey, loc);
421                 }
422             }
423         }
424 
425         // in the default bundle, this doesn't change anything.
426         // but in may in user-provided bundles
427         if (abbr)
428         {
429             directionKey += ABBR_SUFFIX;
430         }
431 
432         // then combine them
433         return friendly +  " " + getText(directionKey, loc);
434     }
435 
436 
437     /**
438      * Converts the specified positive duration of milliseconds into larger
439      * units up to the specified number of positive units, beginning with the 
440      * largest positive unit.  e.g.
441      * <code>toString(181453, 3, false, null)</code> will return
442      * "3 minutes 1 second 453 milliseconds",
443      * <code>toString(181453, 2, false, null)</code> will return
444      * "3 minutes 1 second", and 
445      * <code>toString(180000, 2, true, null)</code> will return
446      * "3 min".
447      */
448     protected String toString(long diff, int maxUnitDepth,
449                               boolean abbreviate, Locale locale)
450     {
451         // these cases should be handled elsewhere
452         if (diff <= 0)
453         {
454             return null;
455         }
456         // can't go any deeper than we have units
457         if (maxUnitDepth > timeUnits.size())
458         {
459             maxUnitDepth = timeUnits.size();
460         }
461 
462         long value = 0;
463         long remainder = 0;
464 
465         // determine the largest unit and calculate the value and remainder
466         Iterator i = timeUnits.keySet().iterator();
467         String unitKey = (String)i.next();
468         Long unit = (Long)timeUnits.get(unitKey);
469         while (i.hasNext())
470         {
471             // get the next unit
472             String nextUnitKey = (String)i.next();
473             Long nextUnit = (Long)timeUnits.get(nextUnitKey);
474 
475             // e.g. if diff < <nextUnit>
476             if (diff < nextUnit.longValue())
477             {
478                 // then we're working with <unit>
479                 value = diff / unit.longValue();
480                 remainder = diff - (value * unit.longValue());
481                 break;
482             }
483 
484             // shift to the next unit
485             unitKey = nextUnitKey;
486             unit = nextUnit;
487         }
488 
489         // if it was years, then we haven't done the math yet
490         if (unitKey.equals(YEAR_KEY))
491         {
492             value = diff / unit.longValue();
493             remainder = diff - (value * unit.longValue());
494         }
495 
496         // select proper pluralization
497         if (value != 1)
498         {
499             unitKey += PLURAL_SUFFIX;
500         }
501 
502         if (abbreviate)
503         {
504             unitKey += ABBR_SUFFIX;
505         }
506 
507         // combine the value and the unit
508         String output = value + " " + getText(unitKey, locale);
509 
510         // recurse over the remainder if it exists and more units are allowed
511         if (maxUnitDepth > 1 && remainder > 0)
512         {
513             output += " " + toString(remainder, maxUnitDepth - 1,
514                                      abbreviate, locale);
515         }
516         return output;
517     }
518 
519 
520 
521     public class Comparison
522     {
523         private final long milliseconds;
524         private final int type;
525         private final int maxUnitDepth;
526         private final boolean abbreviate;
527         private final Locale locale;
528 
529         public Comparison(long ms, int type, int depth, boolean abbr, Locale loc)
530         {
531             this.milliseconds = ms;
532             this.type = type;
533             this.maxUnitDepth = depth;
534             this.abbreviate = abbr;
535             this.locale = loc;
536         }
537 
538         /**
539          * Sets whether or not this comparison is to be rendered in
540          * abbreviated form or not. By default, it is not abbreviated.
541          */
542         public Comparison abbr(boolean abbr)
543         {
544             return new Comparison(this.milliseconds, this.type,
545                                   this.maxUnitDepth, abbr, this.locale);
546         }
547 
548         /**
549          * Set the maximum number of units to render for this comparison.
550          * By default, this is set to 1 unit.
551          */
552         public Comparison depth(int depth)
553         {
554             return new Comparison(this.milliseconds, this.type,
555                                   depth, this.abbreviate, this.locale);
556         }
557 
558         /**
559          * Sets the locale used to look up the textual portions of the
560          * rendering. This defaults to the Locale configured for this tool,
561          * if any.  If no locale was configured, this defaults to the system
562          * default.
563          */
564         public Comparison locale(Locale loc)
565         {
566             return new Comparison(this.milliseconds, this.type,
567                                   this.maxUnitDepth, this.abbreviate, loc);
568         }
569 
570         /**
571          * Return the approximate number of years between the dates being compared.
572          */
573         public long getYears()
574         {
575             return ComparisonDateTool.toYears(this.milliseconds);
576         }
577 
578         /**
579          * Return the approximate number of months between the dates being compared.
580          */
581         public long getMonths()
582         {
583             return ComparisonDateTool.toMonths(this.milliseconds);
584         }
585 
586         /**
587          * Return the number of weeks between the dates being compared.
588          */
589         public long getWeeks()
590         {
591             return ComparisonDateTool.toWeeks(this.milliseconds);
592         }
593 
594         /**
595          * Return the number of days between the dates being compared.
596          */
597         public long getDays()
598         {
599             return ComparisonDateTool.toDays(this.milliseconds);
600         }
601 
602         /**
603          * Return the number of hours between the dates being compared.
604          */
605         public long getHours()
606         {
607             return ComparisonDateTool.toHours(this.milliseconds);
608         }
609 
610         /**
611          * Return the number of minutes between the dates being compared.
612          */
613         public long getMinutes()
614         {
615             return ComparisonDateTool.toMinutes(this.milliseconds);
616         }
617 
618         /**
619          * Return the number of seconds between the dates being compared.
620          */
621         public long getSeconds()
622         {
623             return ComparisonDateTool.toSeconds(this.milliseconds);
624         }
625 
626         /**
627          * Return the number of milliseconds between the dates being compared.
628          */
629         public long getMilliseconds()
630         {
631             return this.milliseconds;
632         }
633 
634         /**
635          * Sets the {@link #depth(int depth)} to which this comparison is rendered
636          * to the maximum number of time units available to the tool. By default,
637          * there are 8 units available, but the tool may be configured to "skip"
638          * any of the standard units, thus shortening the maximum depth.
639          */
640         public Comparison getFull()
641         {
642             return depth(ComparisonDateTool.this.timeUnits.size());
643         }
644 
645         /**
646          * Sets this comparison to be rendered as a 
647          * {@link ComparisonDateTool#difference}. This effectively means that
648          * the comparison will render as a period of time, without any suffix
649          * to describe the relative position of the dates being compared (e.g. "later"
650          * or "ago").
651          */
652         public Comparison getDifference()
653         {
654             return new Comparison(this.milliseconds, DIFF_TYPE,
655                                   this.maxUnitDepth, this.abbreviate, this.locale);
656         }
657 
658         /**
659          * Sets this comparison to be rendered as if it where generated using
660          * the {@link ComparisonDateTool#whenIs(Object now, Object then)} method.
661          * This effectively means that the comparison will render with a suffix
662          * to describe the relative position of the dates being compared (e.g. "later"
663          * or "ago").
664          */
665         public Comparison getRelative()
666         {
667             return new Comparison(this.milliseconds, RELATIVE_TYPE,
668                                   this.maxUnitDepth, this.abbreviate, this.locale);
669         }
670 
671         /**
672          * This is equivalent to calling {@link #abbr(boolean abbr)} with
673          * {@code true} as the argument, thus setting this comparison to be
674          * rendered in abbreviated form.
675          */
676         public Comparison getAbbr()
677         {
678             return abbr(true);
679         }
680 
681         /**
682          * Renders this comparison to a String.
683          */
684         public String toString()
685         {
686             return ComparisonDateTool.this.toString(this.milliseconds,
687                                                     this.type,
688                                                     this.maxUnitDepth,
689                                                     this.abbreviate,
690                                                     this.locale);
691         }
692     }
693 
694 }