1
2
3
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
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
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
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
133 return defValue;
134 }
135
136
137
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);
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;
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
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
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
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
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
219 try {
220 SocRolloverResultItemInfo item = rolloverOneCourseOfferingReturningItem(sourceCoId);
221 Date end = new Date();
222 String timeInSeconds = _computeDiffInSeconds(start, end);
223 start = end;
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
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
252 String totalTime = _computeTotalTimeString(origStart, end);
253
254 logger.info("======= Finished processing rollover ======= ({})", totalTime);
255 _removeRolloverAssistIdFromContext(context);
256 reportProgress(items, sourceCoIdsHandled - errors);
257
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);
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);
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
347
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
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
367
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
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 }