1   package com.atlassian.core.cron.parser;
2   
3   import com.atlassian.core.util.collection.EasyList;
4   import org.apache.log4j.Logger;
5   
6   import java.util.List;
7   
8   /**
9    * Represents the hours part of a cron string. This class is responsible for parsing and validating the hours entry.
10   * The {@link #isValid()} method refers only to what is supported by the cron editor for the hours entry.
11   * <p/>
12   * Valid hours means a numerical hour (or range of the form x-y) with an optional trailing "/1", "/2" or "/3" to
13   * indicate the repeat increment in hours. This expects that numeric hours to be in 24 hour time format.
14   * <p/>
15   * The hours and meridian attributes depend on whether this is a range entry or a "run once" entry. For ranges, use
16   * the "from" and "to" methods.
17   */
18  public class CronHoursEntry
19  {
20      private static final Logger log = Logger.getLogger(CronHoursEntry.class);
21  
22      /**
23       * cronEntry should only contain legal characters are '/', '*', '-', and digit
24       */
25      private static final String REGEX_VALID = "[\\d*/-]+";
26  
27      /**
28       * Flag to indicate no increment part is present.
29       */
30      private static final int NO_INCREMENT_PART = -1;
31  
32      /**
33       * The set of hour increments that we accept. This is derived from the minimum requirements of the cron spec editor.
34       */
35      private static final List ACCEPTED_HOUR_INCREMENTS = EasyList.build(new Integer(NO_INCREMENT_PART), new Integer(1), new Integer(2), new Integer(3));
36  
37      static final String INCREMENT_DELIMITER = "/";
38      static final String RANGE_DELIMITER = "-";
39  
40      private static final MeridianHour NULL_MERIDIAN_HOUR = new MeridianHour(NO_INCREMENT_PART, null);
41  
42      private MeridianHour fromMeridianHour = NULL_MERIDIAN_HOUR;
43      private MeridianHour toMeridianHour = NULL_MERIDIAN_HOUR;
44  
45      private MeridianHour runOnceMeridianHour = NULL_MERIDIAN_HOUR;
46  
47      private int increment = NO_INCREMENT_PART;
48      private boolean valid = true;
49  
50      /**
51       * Parses the given value and establishes state based on this.
52       * @param cronEntry the hours field of a cron string.
53       */
54      public CronHoursEntry(String cronEntry)
55      {
56          if (cronEntry == null)
57          {
58              throw new IllegalArgumentException("Can not create a cron entry from a null value.");
59          }
60          parseEntry(cronEntry);
61      }
62  
63      /**
64       * Returns true only if the hours entry is valid with respect to the editor.
65       * @return true only if the editor can handle this hour part.
66       */
67      public boolean isValid()
68      {
69          return valid && ACCEPTED_HOUR_INCREMENTS.contains(new Integer(increment));
70      }
71  
72      /**
73       * Returns the lower bound of the hour range if this entry has a range.
74       *
75       * This end of the range is inclusive - e.g. if HoursFrom is 3PM and HoursTo is 5PM, the duration of the range
76       * is 2 hours.
77       *
78       * @return the lower bound or -1 if this is not a range hour entry.
79       */
80      public int getFrom()
81      {
82          return fromMeridianHour.getHour();
83      }
84  
85      /**
86       * Returns the upper bound of the hour range if this entry has a range.
87       *
88       * This end of the range is exclusive - e.g. if HoursFrom is 3PM and HoursTo is 5PM, the duration of the range
89       * is 2 hours.
90       *
91       * @return the upper bound or -1 if this is not a range hour entry.
92       */
93      public int getTo()
94      {
95          return toMeridianHour.getHour();
96      }
97  
98      /**
99       * Returns the meridian indicator @{link #AM} or @{link #AM} for the lower bound of a range entry.
100      *
101      * @return the meridian belonging to the lower bound hour or null if this is not a range entry.
102      */
103     public String getFromMeridian()
104     {
105         return fromMeridianHour.getMeridian();
106     }
107 
108     /**
109      * Returns the meridian indicator @{link #AM} or @{link #AM} for the upper bound of a range entry.
110      *
111      * @return the meridian belonging to the upper bound hour or null if this is not a range entry.
112      */
113     public String getToMeridian()
114     {
115         return toMeridianHour.getMeridian();
116     }
117 
118     /**
119      * Returns the single hour value for this entry if it is a run once entry.
120      *
121      * @return the hour value or -1 if this is not a run once hour entry.
122      */
123     public int getRunOnce()
124     {
125         return runOnceMeridianHour.getHour();
126     }
127 
128     /**
129      * Returns the meridian indicator @{link #AM} or @{link #AM} for the entry if it is a run once entry.
130      *
131      * @return the meridian belonging single hour value or null if this is not a run once entry.
132      */
133     public String getRunOnceMeridian()
134     {
135         return runOnceMeridianHour.getMeridian();
136     }
137 
138     /**
139      * Returns the increment or step size in hours or -1 if this entry has no increment.
140      *
141      * @return the period of repetition in hours.
142      */
143     public int getIncrement()
144     {
145         return increment;
146     }
147 
148     /**
149      * Indicates if this entry has an increment.
150      *
151      * @return true only if the entry has an increment part specified.
152      */
153     public boolean hasIncrement()
154     {
155         return increment != NO_INCREMENT_PART;
156     }
157 
158     public boolean isRunOnce()
159     {
160         return !NULL_MERIDIAN_HOUR.equals(runOnceMeridianHour);
161     }
162 
163     /**
164      * Sets the state of this entry based on the given cron entry including the validity.
165      *
166      * @param cronEntry the cron entry part for hours.
167      */
168     private void parseEntry(String cronEntry)
169     {
170         if (!cronEntry.matches(REGEX_VALID))
171         {
172             valid = false;
173         }
174         else
175         {
176             // a '*' denotes "every hour", so it has an increment of 1, and starts at 12 AM and finishes at 12 AM
177             if ("*".equals(cronEntry))
178             {
179                 this.increment = 1;
180                 this.fromMeridianHour = parseMeridianHour("0");
181                 this.toMeridianHour = this.fromMeridianHour;
182                 return;
183             }
184 
185             int slashIndex = cronEntry.indexOf(INCREMENT_DELIMITER);
186             if (slashIndex >= 0)
187             {
188                 String incrementStr = cronEntry.substring(slashIndex + 1, cronEntry.length());
189                 try
190                 {
191                     this.increment = Integer.parseInt(incrementStr);
192                 }
193                 catch (NumberFormatException nfe)
194                 {
195                     log.debug("The increment portion of the hour cron entry must be an integer.");
196                     valid = false;
197                 }
198                 //Chop this off, we don't need it anymore
199                 cronEntry = cronEntry.substring(0, slashIndex);
200             }
201 
202             int dashIndex = cronEntry.indexOf(RANGE_DELIMITER);
203             if (dashIndex >= 0)
204             {
205                 String fromStr = cronEntry.substring(0, dashIndex);
206                 this.fromMeridianHour = parseMeridianHour(fromStr);
207 
208                 String toStr = cronEntry.substring(dashIndex + 1, cronEntry.length());
209                 // JRA-13503: since toMeridianHour is exclusive, but cron expressions are inclusive,
210                 // we need to increment the "to" hour in the cron expression by 1 to get the correct representation
211                 this.toMeridianHour = parseMeridianHour(incrementHourByOne(toStr));
212             }
213             // if we have specified a "24-range" e.g. 4am - 4am, the cron entry will not contain the range delimiter,
214             // but it will have an increment.
215             else if (hasIncrement())
216             {
217                 this.fromMeridianHour = parseMeridianHour(cronEntry);
218                 this.toMeridianHour = parseMeridianHour(cronEntry);
219             }
220             else
221             {
222                 runOnceMeridianHour = parseMeridianHour(cronEntry);
223             }
224         }
225     }
226 
227     private MeridianHour parseMeridianHour(String twentyFourHour)
228     {
229         MeridianHour meridianHour;
230         // '*' means any hour, so default to 12AM
231         if ("*".equals(twentyFourHour))
232         {
233             twentyFourHour = "0";
234         }
235         meridianHour = MeridianHour.parseMeridianHour(twentyFourHour);
236         if (meridianHour == null)
237         {
238             valid = false;
239             meridianHour = NULL_MERIDIAN_HOUR;
240         }
241         return meridianHour;
242     }
243 
244     private String incrementHourByOne(String hour)
245     {
246         // don't use modulo operator here, because if the hour has
247         // exceeded the valid range, using modulo will change it
248         int h = Integer.parseInt(hour);
249         h = (h == 23) ? 0 : h + 1;
250         return "" + h;
251     }
252 
253 }