1   package com.atlassian.core.util;
2   
3   import java.text.DecimalFormat;
4   import java.text.NumberFormat;
5   import java.text.ParsePosition;
6   import java.util.HashMap;
7   import java.util.Locale;
8   import java.util.Map;
9   import java.util.regex.Matcher;
10  import java.util.regex.Pattern;
11  
12  import com.atlassian.core.i18n.I18nTextProvider;
13  import com.atlassian.core.util.DateUtils.Duration;
14  
15  import org.apache.commons.lang.StringUtils;
16  
17  /**
18   * i18n-aware parsing of duration strings. Understands the output of {@link DateUtils}' formats.
19   */
20  public class DurationUtils
21  {
22      /**
23       * resource key for day unit
24       */
25      private static final String UNIT_DAY = "core.durationutils.unit.day";
26  
27      /**
28       * resource key for hour unit
29       */
30      private static final String UNIT_HOUR = "core.durationutils.unit.hour";
31  
32      /**
33       * resource key for minute unit.
34       */
35      private static final String UNIT_MINUTE = "core.durationutils.unit.minute";
36  
37      // A number, optional whitespace and an optional non-whitespace unit token with an optional ignored comma after
38      private static final Pattern COUNT_WITH_OPTIONAL_UNITS = Pattern.compile("([,\\.\\xA0'\\p{Nd}]+)\\s*(?:([^,\\s]+),?)?\\s*");
39      
40      public static long getDurationSeconds(String durationStr, long secondsPerDay, long secondsPerWeek, final Duration defaultUnit,
41              Locale locale, I18nTextProvider i18n)
42          throws InvalidDurationException
43      {
44          Map<String, Duration> durationTokens = getDurationTokens(i18n);
45          
46          return getDurationSeconds(durationStr, secondsPerDay, secondsPerWeek, defaultUnit, locale, durationTokens);
47      }
48      
49      public static long getDurationSeconds(String durationStr, long secondsPerDay, long secondsPerWeek, final Duration defaultUnit,
50              Locale locale, Map<String, Duration> tokens)
51          throws InvalidDurationException
52      {
53          if (StringUtils.isBlank(durationStr))
54          {
55              return 0;
56          }
57  
58          durationStr = durationStr.trim();
59          
60          NumberFormat nf = DecimalFormat.getNumberInstance(locale);
61  
62          long seconds = 0;
63  
64          Matcher m = COUNT_WITH_OPTIONAL_UNITS.matcher(durationStr);
65          
66          while (m.lookingAt())
67          {
68              // Use ParsePosition to detect a parse that stops before the string ends
69              ParsePosition pp = new ParsePosition(0);
70              
71              String number = m.group(1);
72              
73              Number n = nf.parse(number, pp);
74              
75              if (pp.getIndex() != number.length())
76              {
77                  throw new InvalidDurationException("Bad number '" + number + "' in duration string '" + durationStr + "'");
78              }
79              
80              String unitName = m.group(2);
81              
82              Duration unit;
83              if (unitName != null)
84              {
85                  unit = tokens.get(unitName);
86                  if (unit == null)
87                  {
88                      throw new InvalidDurationException("No unit for '" + unitName + "'");
89                  }
90              }
91              else
92              {
93                  unit = defaultUnit;
94              }
95              
96              long s = (long) (unit.getModifiedSeconds(secondsPerDay, secondsPerWeek) * n.doubleValue());
97              
98              if (unit != defaultUnit && s % 60 != 0)
99              {
100                 throw new InvalidDurationException("Durations must be in whole minutes");
101             }
102             
103             seconds += s;
104             
105             m.region(m.end(), durationStr.length());
106         }
107         
108         if (m.regionStart() != durationStr.length())
109         {
110             throw new InvalidDurationException("Invalid characters in duration: " + durationStr);
111         }
112         
113         return seconds;
114     }
115 
116     public static Map<String, Duration> getDurationTokens(I18nTextProvider i18n)
117     {
118         Map<String, Duration> tokens = new HashMap<String, DateUtils.Duration>();
119         
120         tokens.put(getDurationToken(i18n, UNIT_DAY), Duration.DAY);
121         tokens.put(getDurationToken(i18n, UNIT_HOUR), Duration.HOUR);
122         tokens.put(getDurationToken(i18n, UNIT_MINUTE), Duration.MINUTE);
123         
124         for (Duration d : Duration.values())
125         {
126             String n = d.name().toLowerCase();
127             tokens.put(i18n.getText("core.dateutils." + n), d);
128             tokens.put(i18n.getText("core.dateutils." + n + "s"), d);
129         }
130         
131         return tokens;
132     }
133 
134     private static String getDurationToken(I18nTextProvider i18n, String unit)
135     {
136         String s = i18n.getText(unit);
137         return s.replace("{0}", "").trim();
138     }
139 }