View Javadoc

1   /*
2    * To change this template, choose Tools | Templates
3    * and open the template in the editor.
4    */
5   package org.kuali.student.enrollment.class2.courseofferingset.service.impl;
6   
7   import org.apache.commons.lang.StringUtils;
8   import org.apache.log4j.Logger;
9   import org.kuali.student.enrollment.class2.courseofferingset.service.facade.RolloverAssist;
10  import org.kuali.student.enrollment.courseoffering.dto.CourseOfferingInfo;
11  import org.kuali.student.enrollment.courseoffering.service.CourseOfferingService;
12  import org.kuali.student.enrollment.courseofferingset.dto.SocRolloverResultInfo;
13  import org.kuali.student.enrollment.courseofferingset.dto.SocRolloverResultItemInfo;
14  import org.kuali.student.enrollment.courseofferingset.service.CourseOfferingSetService;
15  import org.kuali.student.r2.common.dto.AttributeInfo;
16  import org.kuali.student.r2.common.dto.ContextInfo;
17  import org.kuali.student.r2.common.dto.ValidationResultInfo;
18  import org.kuali.student.r2.common.exceptions.AlreadyExistsException;
19  import org.kuali.student.r2.common.exceptions.DataValidationErrorException;
20  import org.kuali.student.r2.common.exceptions.InvalidParameterException;
21  import org.kuali.student.r2.common.exceptions.OperationFailedException;
22  import org.kuali.student.r2.common.util.RichTextHelper;
23  import org.kuali.student.r2.common.util.constants.CourseOfferingSetServiceConstants;
24  import org.kuali.student.r2.core.acal.service.AcademicCalendarService;
25  import org.kuali.student.r2.lum.course.service.CourseService;
26  
27  import java.util.ArrayList;
28  import java.util.Date;
29  import java.util.List;
30  
31  /**
32   *
33   * @author nwright
34   */
35  public class CourseOfferingRolloverRunner implements Runnable {
36  
37      final static Logger logger = Logger.getLogger(CourseOfferingRolloverRunner.class);
38      private CourseOfferingService coService;
39      private CourseOfferingSetService socService;
40      private CourseService courseService;
41      private AcademicCalendarService acalService;
42      private ContextInfo context;
43      private SocRolloverResultInfo result;
44      private RolloverAssist rolloverAssist;
45  
46      public RolloverAssist getRolloverAssist() {
47          return rolloverAssist;
48      }
49  
50      public void setRolloverAssist(RolloverAssist rolloverAssist) {
51          this.rolloverAssist = rolloverAssist;
52      }
53  
54      public CourseOfferingService getCoService() {
55          return coService;
56      }
57  
58      public void setCoService(CourseOfferingService coService) {
59          this.coService = coService;
60      }
61  
62      public CourseOfferingSetService getSocService() {
63          return socService;
64      }
65  
66      public void setSocService(CourseOfferingSetService socService) {
67          this.socService = socService;
68      }
69  
70      public CourseService getCourseService() {
71          return courseService;
72      }
73  
74      public void setCourseService(CourseService courseService) {
75          this.courseService = courseService;
76      }
77  
78      public AcademicCalendarService getAcalService() {
79          return acalService;
80      }
81  
82      public void setAcalService(AcademicCalendarService acalService) {
83          this.acalService = acalService;
84      }
85  
86      public ContextInfo getContext() {
87          return context;
88      }
89  
90      public void setContext(ContextInfo context) {
91          this.context = context;
92      }
93  
94      public SocRolloverResultInfo getResult() {
95          return result;
96      }
97  
98      public void setResult(SocRolloverResultInfo result) {
99          this.result = result;
100     }
101 
102     private void loadOptionKeys() {
103         this.skipIfAlreadyExists = getBooleanOption(CourseOfferingSetServiceConstants.SKIP_IF_ALREADY_EXISTS_OPTION_KEY, false);
104         this.logSuccesses = getBooleanOption(CourseOfferingSetServiceConstants.LOG_SUCCESSES_OPTION_KEY, false);
105         this.progressFrequency = getIntOption(CourseOfferingSetServiceConstants.LOG_FREQUENCY_OPTION_KEY_PREFIX, 10);
106         this.haltErrorsMax = getIntOption(CourseOfferingSetServiceConstants.HALT_ERRORS_MAX_OPTION_KEY_PREFIX, -1);
107 
108     }
109     // TODO: implement these options
110     private boolean skipIfAlreadyExists = false;
111     private boolean logSuccesses = false;
112     private int progressFrequency = 100;
113     private int haltErrorsMax = -1;
114 
115     private boolean getBooleanOption(String key, boolean defValue) {
116         for (String optionKey : this.result.getOptionKeys()) {
117             if (optionKey.equals(key)) {
118                 return true;
119             }
120         }
121         // default
122         return defValue;
123     }
124 
125     private int getIntOption(String keyPrefix, int defValue) {
126         for (String optionKey : this.result.getOptionKeys()) {
127             if (optionKey.startsWith(keyPrefix)) {
128                 return Integer.parseInt(optionKey);
129             }
130         }
131         // default
132         return defValue;
133     }
134 
135     ////
136     //// implement the run method
137     ////  
138     @Override
139     public void run() {
140         try {
141             runInternal();
142         } catch (Exception ex) {
143             try {
144                 this.result = socService.getSocRolloverResult(result.getId(), context);
145                 this.result.setStateKey(CourseOfferingSetServiceConstants.ABORTED_RESULT_STATE_KEY);
146                 this.result.setDateCompleted(new Date());
147                 this.result.setMessage(new RichTextHelper().fromPlain("Got an unexpected exception running rollover:\n"
148                         + ex.toString()));
149                 this.socService.updateSocRolloverResult(result.getId(), result, context);
150             } catch (Exception ex1) {
151                 logger.fatal(result, ex);
152                 throw new RuntimeException(ex1);
153             }
154         }
155     }
156 
157     private String _computeDiffInSeconds(Date start, Date end) {
158         long diffInMillis = end.getTime() - start.getTime();
159         int seconds = (int)(diffInMillis / 1000);  // Convert to seconds
160         int fraction = (int)(diffInMillis % 1000);
161         String fractionStr = "" + fraction;
162         while (fractionStr.length() < 3) {
163             fractionStr = "0" + fractionStr;
164         }
165         return seconds + "." + fractionStr + "s";
166     }
167 
168     private void _removeRolloverAssistIdFromContext(ContextInfo contextInfo) {
169         int index = 0;
170         for (AttributeInfo attr: contextInfo.getAttributes()) {
171             if (attr.getKey().equals(CourseOfferingSetServiceConstants.ROLLOVER_ASSIST_ID_DYNATTR_KEY)) {
172                 contextInfo.getAttributes().remove(index);
173                 break; // Assume it only shows up once
174             }
175             index++;
176         }
177     }
178     private void runInternal() throws Exception {
179         if (this.context == null) {
180             throw new NullPointerException("context not set");
181         }
182         loadOptionKeys();
183         // mark running
184         String resultId = result.getId();
185         result = this.socService.getSocRolloverResult(resultId, context);
186         result.setStateKey(CourseOfferingSetServiceConstants.RUNNING_RESULT_STATE_KEY);
187         this.socService.updateSocRolloverResult(result.getId(), result, context);
188         // Check if there are any course in the target soc
189         if (!skipIfAlreadyExists) {
190             List<String> targetCoIds = socService.getCourseOfferingIdsBySoc(this.result.getTargetSocId(), context);
191             if (!targetCoIds.isEmpty()) {
192                 throw new OperationFailedException(targetCoIds.size() + " course offerings already exist in the target soc");
193             }
194         }
195         // mark the number expected
196         List<String> sourceCoIds = socService.getCourseOfferingIdsBySoc(this.result.getSourceSocId(), context);
197         result = this.socService.getSocRolloverResult(result.getId(), context);
198         result.setItemsProcessed(0);
199         result.setItemsExpected(sourceCoIds.size());
200         this.socService.updateSocRolloverResult(result.getId(), result, context);
201 
202         // Start processing
203         int sourceCoIdsHandled = 0;
204         int aoRolledOver = 0;
205         int errors = 0;
206         List<SocRolloverResultItemInfo> items = new ArrayList<SocRolloverResultItemInfo>();
207         int count = 1;
208         Date origStart = new Date();
209         //
210         String rolloverAssistId = rolloverAssist.getRolloverId();
211         AttributeInfo attr = new AttributeInfo();
212         attr.setKey(CourseOfferingSetServiceConstants.ROLLOVER_ASSIST_ID_DYNATTR_KEY);
213         attr.setValue(rolloverAssistId);
214         context.getAttributes().add(attr);
215         Date start = origStart;
216         for (String sourceCoId : sourceCoIds) {
217             // System.out.println("processing: " + sourceCoId);
218             try {
219                 SocRolloverResultItemInfo item = rolloverOneCourseOfferingReturningItem(sourceCoId);
220                 Date end = new Date();
221                 String timeInSeconds = _computeDiffInSeconds(start, end);
222                 start = end; // Get ready for next one
223                 logger.info("(" + count + ") Processing: " + sourceCoId + " (" + timeInSeconds + ")");
224 
225                 items.add(item);
226                 reportProgressIfModulo(items, sourceCoIdsHandled);
227                 if (!CourseOfferingSetServiceConstants.SUCCESSFUL_RESULT_ITEM_STATES.contains(item.getStateKey())) {
228                     errors++;
229                     if (this.haltErrorsMax != -1) {
230                         if (errors > this.haltErrorsMax) {
231                             throw new OperationFailedException("Too many errors, exceeded the halt threshold: " + errors
232                                     + " out of " + sourceCoIdsHandled + " course offerings rolled over");
233                         }
234                     }
235                 }
236                 else {
237                     String aoCountStr = item.getAttributeValue(CourseOfferingSetServiceConstants.ACTIVITY_OFFERINGS_CREATED_SOC_ITEM_DYNAMIC_ATTRIBUTE);
238                     int aoCount = Integer.parseInt(aoCountStr);
239                     aoRolledOver += aoCount;
240                 }
241             } catch (Exception ex) {
242                 // log some conetxt for the exception
243                 logger.fatal("failed while processing the " + sourceCoIdsHandled + "th course offering " + sourceCoId, ex);
244                 throw ex;
245             }
246             sourceCoIdsHandled++;
247             count++;
248         }
249         Date end = new Date();
250         // Compute total rollover time in hours, minutes, and seconds
251         String totalTime = _computeTotalTimeString(origStart, end);
252 
253         logger.info("======= Finished processing rollover ======= (" + totalTime + ")");
254         _removeRolloverAssistIdFromContext(context); // KSENROLL-8062
255         reportProgress(items, sourceCoIdsHandled - errors);      // Items Processed = Items - Errors
256         // mark finished
257         result = socService.getSocRolloverResult(result.getId(), context);
258         result.setDateCompleted(new Date());
259         result.setCourseOfferingsCreated(sourceCoIdsHandled - errors);
260         result.setCourseOfferingsSkipped(errors);
261         result.setActivityOfferingsCreated(aoRolledOver);
262         result.setActivityOfferingsSkipped(0); // For now, we have no "failed" AOs that didn't rollover.
263         result.setStateKey(CourseOfferingSetServiceConstants.FINISHED_RESULT_STATE_KEY);
264         this.socService.updateSocRolloverResult(result.getId(), result, context);
265     }
266 
267     private String _computeTotalTimeString(Date start, Date end) {
268         long diffInMillis = end.getTime() - start.getTime();
269         int seconds = (int)(diffInMillis / 1000);  // Convert to seconds
270         int fraction = (int)(diffInMillis % 1000);
271         String fractionStr = "" + fraction;
272         while (fractionStr.length() < 3) {
273             fractionStr = "0" + fractionStr;
274         }
275         int minutes = seconds / 60;
276         seconds = seconds % 60;
277         int hours = minutes / 60;
278         minutes = hours % 60;
279         StringBuilder result = new StringBuilder();
280         if (hours > 0) {
281             result.append(hours);
282             result.append("h, ");
283         }
284         if (hours > 0 || minutes > 0) {
285             result.append(minutes);
286             result.append("m, ");
287         }
288         result.append(seconds);
289         result.append(".");
290         result.append(fractionStr);
291         result.append("s");
292         String str = result.toString();
293         return str;
294     }
295 
296     private void reportProgressIfModulo(List<SocRolloverResultItemInfo> items, int i) throws Exception {
297         int modulo = i % progressFrequency;
298         if (modulo != 0) {
299             return;
300         }
301         this.reportProgress(items, i);
302     }
303 
304     private void reportProgress(List<SocRolloverResultItemInfo> items, int i) throws Exception {
305         this.socService.updateSocRolloverProgress(result.getId(), i, context);
306         if (!this.logSuccesses) {
307             stripSuccesses(items);
308         }
309         if (!items.isEmpty()) {
310             Integer count = this.socService.createSocRolloverResultItems(result.getId(),
311                     CourseOfferingSetServiceConstants.CREATE_RESULT_ITEM_TYPE_KEY,
312                     items,
313                     context);
314         }
315         items.clear();
316     }
317 
318     private void stripSuccesses(List<SocRolloverResultItemInfo> items) {
319         List<SocRolloverResultItemInfo> list = new ArrayList<SocRolloverResultItemInfo>();
320         for (SocRolloverResultItemInfo item : items) {
321             if (!CourseOfferingSetServiceConstants.SUCCESSFUL_RESULT_ITEM_STATES.contains(item.getStateKey())) {
322                 list.add(item);
323             }
324         }
325         if (items.size() != list.size()) {
326             items.clear();
327             items.addAll(list);
328         }
329     }
330 
331     private SocRolloverResultItemInfo rolloverOneCourseOfferingReturningItem(String sourceCoId) throws Exception {
332         String error = null;
333         try {
334             SocRolloverResultItemInfo item = this.coService.rolloverCourseOffering(sourceCoId,
335                     this.result.getTargetTermId(),
336                     this.result.getOptionKeys(),
337                     context);
338             item.setSocRolloverResultId(result.getId());
339             return item;
340         } catch (AlreadyExistsException ex) {
341             error = ex.getMessage();
342         } catch (DataValidationErrorException ex) {
343             boolean firstTime = true;
344 
345             // This provides a better error message for display in rollover results page= (KSENROLL-4582)
346             //JIRA FIX : KSENROLL-8731 - Replaced StringBuffer with StringBuilder
347             StringBuilder errorBuffer = new StringBuilder("Validation error(s): ");
348             if (!StringUtils.isBlank(ex.getMessage())){
349                 errorBuffer.append(ex.getMessage());
350                 firstTime = false;
351             }
352             for (ValidationResultInfo info: ex.getValidationResults()) {
353                 if (firstTime) {
354                     firstTime = false;
355                 } else {
356                     errorBuffer.append(", ");
357                 }
358                 // Append on multiple error messages
359                 errorBuffer.append(info.getElement() + " has bad data: " + info.getInvalidData());
360             }
361             error = errorBuffer.toString();
362         } catch (InvalidParameterException ex) {
363             error = ex.getMessage();
364         } catch (Exception ex) {
365             // This is a catchall for unknown exceptions, possibly due to bad data.  The previous exceptions are considered
366             // expected exceptions.  By catching Exception, the rollover won't stop and will continue to process.
367             error = "Unexpected error rolling over course";
368             String mesg = ex.getMessage();
369             if (mesg != null) {
370                 error += ": (" + mesg + ")";
371             }
372             logger.warn("Unexpected error rolling over course", ex);
373         }
374         // got an error so process it
375         SocRolloverResultItemInfo item = new SocRolloverResultItemInfo();
376         item.setSocRolloverResultId(result.getId());
377         item.setSourceCourseOfferingId(sourceCoId);
378         item.setTypeKey(CourseOfferingSetServiceConstants.CREATE_RESULT_ITEM_TYPE_KEY);
379         item.setStateKey(CourseOfferingSetServiceConstants.ERROR_RESULT_ITEM_STATE_KEY);
380         item.setTargetCourseOfferingId(null);
381         item.setMessage(new RichTextHelper().fromPlain(error));
382         return item;
383     }
384 }