1   package com.atlassian.core.cron.generator;
2   
3   import com.atlassian.core.cron.CronEditorBean;
4   import com.atlassian.core.util.DateUtils;
5   import org.apache.commons.lang.StringUtils;
6   
7   /**
8    * Used to generate a cron string based on the state of a {@link com.atlassian.core.cron.CronEditorBean}.
9    */
10  public class CronExpressionGenerator
11  {
12      private static final String DAY_IN_MONTH_SEPARATOR = "#";
13      private static final String LAST_DAY_IN_MONTH_FLAG = "L";
14  
15      /**
16       * This is a utility method that will process the parameters that the view put into the form and create a
17       * cron string from the inputs. This cron string must be validated, there is no guarantee that this output is
18       * a valid cron string.
19       *
20       * @param cronEditorBean the state of the editor form to use for input.
21       * @return a cron string that represents the user inputs in cron format.
22       */
23      public String getCronExpressionFromInput(CronEditorBean cronEditorBean)
24      {
25          String cronSpec = null;
26  
27          if (cronEditorBean.isDailyMode())
28          {
29              //generate a 'daily' spec
30              cronSpec = generateDailySpec(cronEditorBean) + " ? * *";
31          }
32          else if (cronEditorBean.isDayPerWeekMode())
33          {
34              //generate a 'days per week' spec
35              cronSpec = generateDailySpec(cronEditorBean) + " ? * " + generateDaysOfWeekSpec(cronEditorBean);
36          }
37          else if (cronEditorBean.isDaysPerMonthMode())
38          {
39              //generate a 'days per month' spec
40              cronSpec = generateDailySpec(cronEditorBean) + " " + generateDaysOfMonthOptSpec(cronEditorBean);
41          }
42          else if (cronEditorBean.isAdvancedMode())
43          {
44              cronSpec = cronEditorBean.getCronString();
45          }
46  
47          return cronSpec;
48      }
49  
50      /**
51       * This method can generate the fourth, fifth and sixth elements of the cron string: days of month, months of year
52       * (left as *) and days of week.
53       * <p/>
54       * This method reads the daysOfMonthOpt select radio buttons and determines which 'Days of Month' cron option the
55       * user has chosen and delegates to one of two helper methods: generateDayOfMonthSpec() or
56       * generateDayOfWeekOfMonthSpec().
57       *
58       * @return a string that represents this portion of the cron string.
59       */
60      String generateDaysOfMonthOptSpec(CronEditorBean cronEditorBean)
61      {
62          //delegate to helper method
63          if (cronEditorBean.isDayOfWeekOfMonth())
64          {
65              return generateDayOfWeekOfMonthSpec(cronEditorBean);
66          }
67          else
68          {
69              return generateDayOfMonthSpec(cronEditorBean);
70          }
71      }
72  
73      String generateDayOfWeekOfMonthSpec(CronEditorBean cronEditorBean)
74      {
75          String dayInMonthOrdinal = cronEditorBean.getDayInMonthOrdinal();
76          if (dayInMonthOrdinal == null)
77          {
78              throw new IllegalStateException("You must have an ordinal set when generating the day of week of month cron portion: " + cronEditorBean.getCronString());
79          }
80          if (!LAST_DAY_IN_MONTH_FLAG.equalsIgnoreCase(dayInMonthOrdinal))
81          {
82              dayInMonthOrdinal = DAY_IN_MONTH_SEPARATOR + dayInMonthOrdinal;
83          }
84          String specifiedDaysPerWeek = cronEditorBean.getSpecifiedDaysPerWeek();
85          if (specifiedDaysPerWeek == null)
86          {
87              throw new IllegalStateException("The days per week must be specified when creating a days per week cron portion: " + cronEditorBean.getCronString());
88          }
89          String specSegment = specifiedDaysPerWeek + dayInMonthOrdinal;
90          return "? * " + specSegment;
91      }
92  
93      /**
94       * This function returns a string representing the last three cron elements. The day of the month is resolved from
95       * the monthDay select control.
96       * <p/>
97       * Possible segments generated by this method are of the form: '1-31 * ?'
98       */
99      String generateDayOfMonthSpec(CronEditorBean cronEditorBean)
100     {
101         String monthDay = cronEditorBean.getDayOfMonth();
102         if (monthDay == null)
103         {
104             throw new IllegalStateException("The day of month must not be null when creating a day of month cron portion: " + cronEditorBean.getCronString());
105         }
106         return monthDay + " * ?";
107     }
108 
109     /**
110      * This method generates the last mandatory element of the cron string: days of the week.
111      * <p/>
112      * The check boxes representing the days of the week are looped through and incrementally appended to a string
113      * which is then returned.
114      */
115     String generateDaysOfWeekSpec(CronEditorBean cronEditorBean)
116     {
117         if (StringUtils.isBlank(cronEditorBean.getSpecifiedDaysPerWeek()))
118         {
119             throw new IllegalStateException("The days per week must be specified when creating a days per week cron portion: " + cronEditorBean.getCronString());
120         }
121         return cronEditorBean.getSpecifiedDaysPerWeek();
122     }
123 
124     /**
125      * This method generates the first three elements of the cron string: seconds, minutes and hours.
126      */
127     String generateDailySpec(CronEditorBean cronEditorBean)
128     {
129         //resolve base string from frequency select control
130         StringBuffer dailyString = new StringBuffer("0 ");
131 
132         int increment = getIntFromString(cronEditorBean.getIncrementInMinutes());
133 
134         //specify a precise time
135         if (increment == 0 || cronEditorBean.isDaysPerMonthMode())
136         {
137 
138             if (cronEditorBean.getHoursRunOnceMeridian() == null)
139             {
140                 throw new IllegalStateException("You must specify a run once hour meridian when generating a daily spec with no interval: " + cronEditorBean.getCronString());
141             }
142             if (cronEditorBean.getHoursRunOnce() == null)
143             {
144                 throw new IllegalStateException("You must specify a run once hour when generating a daily spec with no interval: " + cronEditorBean.getCronString());
145             }
146             if (cronEditorBean.getMinutes() == null)
147             {
148                 throw new IllegalStateException("You must specify a minutes when generating a daily spec with no interval: " + cronEditorBean.getCronString());
149             }
150 
151             //read & clean input from "at" controls
152             int atHours = getIntFromString(cronEditorBean.getHoursRunOnce());
153             int atMins = getIntFromString(cronEditorBean.getMinutes());
154 
155             atHours = DateUtils.get24HourTime(cronEditorBean.getHoursRunOnceMeridian(), atHours);
156 
157             //replace base string tokens
158             dailyString.append(atMins);
159             dailyString.append(" ");
160             dailyString.append(atHours);
161         }
162         //specify a time range
163         else
164         {
165             // The minutes field is always 0
166             dailyString.append("0");
167             // If the increment is a minute increment
168             if (increment < 60)
169             {
170                 dailyString.append("/");
171                 dailyString.append(increment);
172             }
173 
174             dailyString.append(" ");
175 
176             // Check that we have what we need
177             if (cronEditorBean.getHoursFrom() == null)
178             {
179                 throw new IllegalStateException("You must specify a from hour when generating a daily spec with an interval: " + cronEditorBean.getCronString());
180             }
181             if (cronEditorBean.getHoursFromMeridian() == null)
182             {
183                 throw new IllegalStateException("You must specify a from hour meridian when generating a daily spec with an interval: " + cronEditorBean.getCronString());
184             }
185 
186             if (cronEditorBean.getHoursTo() == null)
187             {
188                 throw new IllegalStateException("You must specify a to hour when generating a daily spec with an interval: " + cronEditorBean.getCronString());
189             }
190             if (cronEditorBean.getHoursToMeridian() == null)
191             {
192                 throw new IllegalStateException("You must specify a to hour meridian when generating a daily spec with an interval: " + cronEditorBean.getCronString());
193             }
194 
195             //read & clean input from "from" & "till" controls
196             int fromHours = DateUtils.get24HourTime(cronEditorBean.getHoursFromMeridian(), getIntFromString(cronEditorBean.getHoursFrom()));
197             int toHours = DateUtils.get24HourTime(cronEditorBean.getHoursToMeridian(), getIntFromString(cronEditorBean.getHoursTo()));
198 
199             int hourIncrement = increment / 60;
200 
201             if (cronEditorBean.is24HourRange())
202             {
203                 dailyString.append("*");
204             }
205             else
206             {
207                 dailyString.append(fromHours);
208                 dailyString.append("-");
209                 // JRA-13503: since cronEditorBean.getHoursTo() is exclusive, but cron expressions are inclusive,
210                 // we need to decrement the "to" hour from the bean by 1 to get the correct representation
211                 dailyString.append(decrementHourByOne(toHours));
212             }
213 
214             if (hourIncrement >= 1)
215             {
216                 dailyString.append("/");
217                 dailyString.append(hourIncrement);
218             }
219         }
220 
221         return dailyString.toString();
222     }
223 
224     int getIntFromString(String string)
225     {
226         if (string != null && !StringUtils.isEmpty(string))
227         {
228             return Integer.parseInt(string);
229         }
230         return 0;
231     }
232     
233     private int decrementHourByOne(int hour)
234     {
235         return hour == 0 ? 23 : hour - 1;
236     }
237 }