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