1   package com.atlassian.core.cron.parser;
2   
3   import com.atlassian.core.util.collection.EasyList;
4   import com.atlassian.core.util.map.EasyMap;
5   import org.apache.commons.lang.StringUtils;
6   import org.apache.log4j.Logger;
7   
8   import java.util.ArrayList;
9   import java.util.Iterator;
10  import java.util.List;
11  import java.util.Map;
12  
13  /**
14   * Parser for the day of week part of a cron string. This class is responsible for parsing and validating the day of
15   * week entry. The {@link #isValid()} method refers only to what is supported by the cron editor for the day of week entry.
16   * <p/>
17   * Valid day of week means a numerical day of week (1-7) or string representation (MON-SUN) which can be separated
18   * by a ',' to indicate a list of days. You can also specify a single day (2) followed by '#' and a number (either
19   * 1, 2, 3, or 4). This represents the first, second, third, or fourth week in the month for that day (e.g. 2#2 means
20   * the second Monday of the month). This can also be a single day (2) followed by the character 'L' which indicates
21   * the last of that day in the month (e.g. 2L means the last monday in the month).
22   */
23  public class CronDayOfWeekEntry
24  {
25  
26      private static final Logger log = Logger.getLogger(CronDayOfWeekEntry.class);
27  
28      private static final String ORDINAL_SEPARATOR = "#";
29      private static final String LIST_SEPARATOR = ",";
30  
31      private static final String LAST = "L";
32  
33      private static final Map VALID_DAYS_MAP = EasyMap.build("MON", "2", "TUE", "3", "WED", "4", "THU", "5", "FRI", "6", "SAT", "7", "SUN", "1");
34      private static final List VALID_NUMERIC_ORDINAL_VALUES = EasyList.build("1", "2", "3", "4");
35  
36      /** All the characters that are derived from valid day values, the ordinal values, the separators. */
37      private static final String VALID_CHARACTERS = "MONTUEWEDTHUFRISATSUN1234567L#,?*";
38  
39      private boolean valid = true;
40      private String ordinal = null;
41      private final List specifiedDays;
42  
43      /**
44       * Parses the given cron entry.
45       *
46       * @param dayOfWeekEntry e.g. MON#2, 2#2 or MON,WED or 1,2,3 or 2L.
47       */
48      public CronDayOfWeekEntry(String dayOfWeekEntry)
49      {
50          specifiedDays = new ArrayList();
51          parseEntry(dayOfWeekEntry);
52      }
53  
54      /**
55       * Will tell you if a day has been specified in the cron string.
56       *
57       * @param dayStr can be any of 1-7, MON-SUN, mon-sun.
58       * @return true if the cron spec specified the passed in day, false otherwise.
59       */
60      public boolean isDaySpecified(String dayStr)
61      {
62          String day = getDayForValue(dayStr);
63          return day != null && specifiedDays.contains(day);
64      }
65  
66      /**
67       * Returns a number that represents the first, second third etc. day of the week in a month.
68       *
69       * @return the ordinal or -1 if this entry doesn't specify it.
70       */
71      public String getDayInMonthOrdinal()
72      {
73          return ordinal;
74      }
75  
76      /**
77       * Will create a comma separated list of the days' numeric values that are specifed by the cron string.
78       *
79       * @return string representing days (e.g. "1,2,3").
80       */
81      public String getDaysAsNumbers()
82      {
83          StringBuffer result = new StringBuffer();
84          int i = 0;
85          for (Iterator iterator = specifiedDays.iterator(); iterator.hasNext(); i++)
86          {
87              String day = (String) iterator.next();
88              result.append(day);
89              if (i + 1 < specifiedDays.size())
90              {
91                  result.append(",");
92              }
93          }
94          return result.toString();
95      }
96  
97      /**
98       * Returns true if the editor can handle the day of week field entry.
99       */
100     public boolean isValid()
101     {
102         return valid;
103     }
104 
105     private void parseEntry(String dayOfWeekEntry)
106     {
107         if (StringUtils.isBlank(dayOfWeekEntry))
108         {
109             log.debug("Tried to create a CronDayOfWeek with empty or null string.");
110             valid = false;
111         }
112         else if (!StringUtils.containsOnly(dayOfWeekEntry.toUpperCase(), VALID_CHARACTERS))
113         {
114             log.debug("Tried to create a CronDayOfWeek with invalid characters: " + dayOfWeekEntry);
115             valid = false;
116         }
117         else
118         {
119             dayOfWeekEntry = dayOfWeekEntry.toUpperCase();
120             // This is the case where we have an ordinal value and only one day specified
121             if (StringUtils.contains(dayOfWeekEntry, ORDINAL_SEPARATOR))
122             {
123                 parseOrdinalValue(dayOfWeekEntry);
124             }
125             // This is the case where we only have a comma separated list of days
126             else if (StringUtils.contains(dayOfWeekEntry, LIST_SEPARATOR))
127             {
128                 parseDaysOfWeek(dayOfWeekEntry);
129             }
130             else if (StringUtils.contains(dayOfWeekEntry, LAST))
131             {
132                 parseLastDayOfWeek(dayOfWeekEntry);
133             }
134             else {
135                 specifiedDays.add(dayOfWeekEntry);
136             }
137         }
138     }
139 
140     private void parseLastDayOfWeek(String dayOfWeekEntry)
141     {
142         if (!dayOfWeekEntry.endsWith(LAST))
143         {
144             log.debug("The L character which specifies last is not at the end of the day of week string.");
145             valid = false;
146         }
147         else
148         {
149             ordinal = LAST;
150             String dayOfWeekStr = dayOfWeekEntry.substring(0, dayOfWeekEntry.length() - 1);
151             String dayOfWeek = getDayForValue(dayOfWeekStr);
152             if (dayOfWeek != null)
153             {
154                 specifiedDays.add(dayOfWeek);
155             }
156             else
157             {
158                 log.debug("The value specfied as a day of week was invalid: " + dayOfWeekStr);
159                 valid = false;
160             }
161         }
162     }
163 
164     private void parseDaysOfWeek(String dayOfWeekEntry)
165     {
166         String[] days = StringUtils.split(dayOfWeekEntry, LIST_SEPARATOR);
167         if (days == null || days.length > 7)
168         {
169             log.debug("The days of week has specified more than 7, this is not valid: " + dayOfWeekEntry);
170             valid = false;
171         }
172         else
173         {
174             for (int i = 0; i < days.length; i++)
175             {
176                 String dayStr = days[i];
177                 String day = getDayForValue(dayStr);
178                 if (day != null)
179                 {
180                     specifiedDays.add(day);
181                 }
182                 else
183                 {
184                     log.debug("A day of week was specified that can not be mapped: " + dayStr);
185                     valid = false;
186                     break;
187                 }
188             }
189         }
190     }
191 
192     private void parseOrdinalValue(String dayOfWeekEntry)
193     {
194         String[] strings = StringUtils.split(dayOfWeekEntry, ORDINAL_SEPARATOR);
195         if (strings == null || strings.length != 2)
196         {
197             log.debug("The ordinal value specifed was not of the correct form: " + dayOfWeekEntry);
198             valid = false;
199         }
200         else
201         {
202             // The first string is the day
203             String dayString = getDayForValue(strings[0]);
204             // Only continue if we can map the day string to a day of week
205             if (dayString != null)
206             {
207                 specifiedDays.add(dayString);
208 
209                 // The second is the ordinal value
210                 String secondString = strings[1].toUpperCase();
211                 if (VALID_NUMERIC_ORDINAL_VALUES.contains(secondString))
212                 {
213                     ordinal = secondString;
214                 }
215                 else
216                 {
217                     log.debug("invalid ordinal value " + ordinal);
218                     valid = false;
219                 }
220             }
221         }
222     }
223 
224     private String getDayForValue(String dayString)
225     {
226         if (VALID_DAYS_MAP.values().contains(dayString.toUpperCase()))
227         {
228             return dayString;
229         }
230         else if (VALID_DAYS_MAP.containsKey(dayString))
231         {
232             return (String) VALID_DAYS_MAP.get(dayString);
233         }
234         log.debug("Unable to resolve a day of week for the string: " + dayString);
235         valid = false;
236         return null;
237     }
238 }