View Javadoc

1   /*
2    * Copyright 2008 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 1.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl1.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.student.r2.lum.course.service.assembler;
17  
18  
19  
20  import org.apache.log4j.Logger;
21  import org.kuali.student.common.util.UUIDHelper;
22  import org.kuali.student.r1.common.assembly.BOAssembler;
23  import org.kuali.student.r1.common.assembly.BaseDTOAssemblyNode;
24  import org.kuali.student.r1.common.assembly.BaseDTOAssemblyNode.NodeOperation;
25  import org.kuali.student.r2.common.assembler.AssemblyException;
26  import org.kuali.student.r2.common.dto.AttributeInfo;
27  import org.kuali.student.r2.common.dto.ContextInfo;
28  import org.kuali.student.r2.common.dto.DtoConstants;
29  import org.kuali.student.r2.common.dto.RichTextInfo;
30  import org.kuali.student.r2.common.exceptions.DoesNotExistException;
31  import org.kuali.student.r2.common.exceptions.InvalidParameterException;
32  import org.kuali.student.r2.common.exceptions.MissingParameterException;
33  import org.kuali.student.r2.common.exceptions.OperationFailedException;
34  import org.kuali.student.r2.common.exceptions.PermissionDeniedException;
35  import org.kuali.student.r2.core.atp.dto.AtpInfo;
36  import org.kuali.student.r2.core.atp.service.AtpService;
37  import org.kuali.student.r2.lum.clu.dto.AdminOrgInfo;
38  import org.kuali.student.r2.lum.clu.dto.CluAccountingInfo;
39  import org.kuali.student.r2.lum.clu.dto.CluCluRelationInfo;
40  import org.kuali.student.r2.lum.clu.dto.CluFeeInfo;
41  import org.kuali.student.r2.lum.clu.dto.CluFeeRecordInfo;
42  import org.kuali.student.r2.lum.clu.dto.CluIdentifierInfo;
43  import org.kuali.student.r2.lum.clu.dto.CluInfo;
44  import org.kuali.student.r2.lum.clu.dto.CluLoRelationInfo;
45  import org.kuali.student.r2.lum.clu.dto.CluResultInfo;
46  import org.kuali.student.r2.lum.clu.dto.LuCodeInfo;
47  import org.kuali.student.r2.lum.clu.dto.ResultOptionInfo;
48  import org.kuali.student.r2.lum.clu.service.CluService;
49  import org.kuali.student.r2.lum.course.dto.CourseCrossListingInfo;
50  import org.kuali.student.r2.lum.course.dto.CourseExpenditureInfo;
51  import org.kuali.student.r2.lum.course.dto.CourseFeeInfo;
52  import org.kuali.student.r2.lum.course.dto.CourseInfo;
53  import org.kuali.student.r2.lum.course.dto.CourseJointInfo;
54  import org.kuali.student.r2.lum.course.dto.CourseRevenueInfo;
55  import org.kuali.student.r2.lum.course.dto.CourseVariationInfo;
56  import org.kuali.student.r2.lum.course.dto.FormatInfo;
57  import org.kuali.student.r2.lum.course.dto.LoDisplayInfo;
58  import org.kuali.student.r2.lum.lo.dto.LoInfo;
59  import org.kuali.student.r2.lum.lo.service.LearningObjectiveService;
60  import org.kuali.student.r2.lum.lrc.dto.ResultValueInfo;
61  import org.kuali.student.r2.lum.lrc.dto.ResultValueRangeInfo;
62  import org.kuali.student.r2.lum.lrc.dto.ResultValuesGroupInfo;
63  import org.kuali.student.r2.lum.lrc.service.LRCService;
64  import org.kuali.student.r2.lum.service.assembler.CluAssemblerUtils;
65  import org.kuali.student.r2.lum.util.constants.LrcServiceConstants;
66  import org.springframework.util.StringUtils;
67  
68  import java.util.ArrayList;
69  import java.util.Collections;
70  import java.util.Comparator;
71  import java.util.Date;
72  import java.util.HashMap;
73  import java.util.HashSet;
74  import java.util.Iterator;
75  import java.util.List;
76  import java.util.Map;
77  import java.util.Map.Entry;
78  import java.util.Set;
79  
80  /**
81   * Assembler for CourseInfo. Provides assemble and disassemble operation on
82   * CourseInfo from/to CluInfo and other base DTOs
83   *
84   * @author Kuali Student Team
85   *
86   */
87  
88  public class CourseAssembler implements BOAssembler<CourseInfo, CluInfo> {
89  
90      final static Logger LOG = Logger.getLogger(CourseAssembler.class);
91      private CluService cluService;
92      private FormatAssembler formatAssembler;
93      private CourseJointAssembler courseJointAssembler;
94      private LoAssembler loAssembler;
95      private LearningObjectiveService loService;
96      private CluAssemblerUtils cluAssemblerUtils;
97      private LRCService lrcService;
98      private AtpService atpService;
99      private float defaultCreditIncrement = 1.0f;
100     //
101     @Override
102     public CourseInfo assemble(CluInfo clu, CourseInfo courseInfo,
103                                boolean shallowBuild,ContextInfo contextInfo) throws AssemblyException {
104 
105         CourseInfo course = (null != courseInfo) ? courseInfo
106                 : new CourseInfo();
107 
108         // Copy all the data from the clu to the course
109 
110         course.setAttributes(clu.getAttributes());
111         course.setCampusLocations(clu.getCampusLocations());
112         course.setCode(clu.getOfficialIdentifier().getCode());
113         course.setCourseNumberSuffix(clu.getOfficialIdentifier()
114                 .getSuffixCode());
115         course.setLevel(clu.getOfficialIdentifier().getLevel());
116         course.setOutOfClassHours(clu.getIntensity());
117         course.setInstructors(clu.getInstructors());
118         course.setStartTerm(clu.getExpectedFirstAtp());
119         course.setEndTerm(clu.getLastAtp());
120         course.setCourseTitle(clu.getOfficialIdentifier().getLongName());
121 
122         // CrossListings
123         List<CourseCrossListingInfo> crossListings = assembleCrossListings(clu.getAlternateIdentifiers());
124         course.setCrossListings(crossListings);
125 
126         //Variation
127         List<CourseVariationInfo> variations = assembleVariations(clu.getAlternateIdentifiers());
128         course.setVariations(variations);
129 
130 //        course.setDepartment(clu.getPrimaryAdminOrg().getOrgId());
131         if (course.getUnitsDeployment() == null) {
132             course.setUnitsDeployment(new ArrayList<String>());
133         }
134         if (course.getUnitsContentOwner() == null) {
135             course.setUnitsContentOwner(new ArrayList<String>());
136         }
137         List<String> courseAdminOrgs = new ArrayList<String>();
138         List<String> courseSubjectOrgs = new ArrayList<String>();
139         for(AdminOrgInfo adminOrg: clu.getAdminOrgs()){
140             if (adminOrg.getTypeKey().equals(CourseAssemblerConstants.ADMIN_ORG)) {
141                 courseAdminOrgs.add(adminOrg.getOrgId());
142             }
143             if (adminOrg.getTypeKey().equals(CourseAssemblerConstants.SUBJECT_ORG)) {
144                 courseSubjectOrgs.add(adminOrg.getOrgId());
145             }
146         }
147         course.setUnitsDeployment(courseAdminOrgs);
148         course.setUnitsContentOwner(courseSubjectOrgs);
149         course.setDescr(clu.getDescr());
150         course.setDuration(clu.getStdDuration());
151         course.setEffectiveDate(clu.getEffectiveDate());
152         course.setExpirationDate(clu.getExpirationDate());
153 
154         //Fees
155         //Fee justification
156         List<CourseFeeInfo> fees = new ArrayList<CourseFeeInfo>();
157         List<CourseRevenueInfo> revenues = new ArrayList<CourseRevenueInfo>();
158         if(clu.getFeeInfo() != null){
159             course.setFeeJustification(clu.getFeeInfo().getDescr());
160 
161             //Fees and revenues come from the same place but revenues have a special feeType
162             for(CluFeeRecordInfo cluFeeRecord: clu.getFeeInfo().getCluFeeRecords()){
163                 String feeType = cluFeeRecord.getFeeType();
164                 if(CourseAssemblerConstants.COURSE_FINANCIALS_REVENUE_TYPE.equals(feeType)){
165                     CourseRevenueInfo courseRevenue = new CourseRevenueInfo();
166                     courseRevenue.setFeeType(feeType);
167                     courseRevenue.setAffiliatedOrgs(cluFeeRecord.getAffiliatedOrgs());
168                     courseRevenue.setAttributes(cluFeeRecord.getAttributes());
169                     courseRevenue.setId(cluFeeRecord.getId());
170                     courseRevenue.setMeta(cluFeeRecord.getMeta());
171                     revenues.add(courseRevenue);
172                 }else{
173                     CourseFeeInfo courseFee = new CourseFeeInfo();
174                     courseFee.setFeeType(feeType);
175                     courseFee.setRateType(cluFeeRecord.getRateType());
176                     courseFee.setDescr(cluFeeRecord.getDescr());
177                     courseFee.setMeta(cluFeeRecord.getMeta());
178                     courseFee.setId(cluFeeRecord.getId());
179                     courseFee.setFeeAmounts(cluFeeRecord.getFeeAmounts());
180                     courseFee.setAttributes(cluFeeRecord.getAttributes());
181                     fees.add(courseFee);
182                 }
183             }
184         }
185         course.setFees(fees);
186         course.setRevenues(revenues);
187         //Expenditures are mapped from accounting info
188         if(course.getExpenditure() == null || clu.getAccountingInfo() == null){
189             course.setExpenditure(new CourseExpenditureInfo());
190         }
191         if(clu.getAccountingInfo() != null){
192             course.getExpenditure().setAffiliatedOrgs(clu.getAccountingInfo().getAffiliatedOrgs());
193         }
194 
195         course.setId(clu.getId());
196         course.setTypeKey(clu.getTypeKey());
197         course.setTermsOffered(clu.getOfferedAtpTypes());
198         course.setPrimaryInstructor(clu.getPrimaryInstructor());
199         course.setInstructors(clu.getInstructors());
200         course.setStateKey(clu.getStateKey());
201         course.setSubjectArea(clu.getOfficialIdentifier().getDivision());
202         course.setTranscriptTitle(clu.getOfficialIdentifier().getShortName());
203         course.setMeta(clu.getMeta());
204         course.setVersion(clu.getVersion());
205 
206 
207         //Special topics code
208         course.setSpecialTopicsCourse(false);
209         for(LuCodeInfo luCode : clu.getLuCodes()){
210             if(CourseAssemblerConstants.COURSE_CODE_SPECIAL_TOPICS.equals(luCode.getType())){
211                 course.setSpecialTopicsCourse(Boolean.parseBoolean(luCode.getValue()));
212                 break;
213             }
214         }
215         //Pilot Course code
216         course.setPilotCourse(false);
217         for(LuCodeInfo luCode : clu.getLuCodes()){
218             if(CourseAssemblerConstants.COURSE_CODE_PILOT_COURSE.equals(luCode.getType())){
219                 course.setPilotCourse(Boolean.parseBoolean(luCode.getValue()));
220                 break;
221             }
222         }
223 
224         // Don't make any changes to nested datastructures if this is
225         if (!shallowBuild) {
226             try {
227                 // Use the cluService to find Joints, then convert and add to the
228                 // course
229                 List<CluCluRelationInfo> cluClus = cluService.getCluCluRelationsByClu(clu.getId(),contextInfo);
230 
231                 for (CluCluRelationInfo cluRel : cluClus) {
232                     if (cluRel.getTypeKey().equals(CourseAssemblerConstants.JOINT_RELATION_TYPE)) {
233                         CourseJointInfo jointInfo = null;
234                         if(cluRel.getCluId().equals(clu.getId())){
235                             jointInfo = courseJointAssembler.assemble(cluRel, cluRel.getRelatedCluId(), null, false, contextInfo);
236                         }else{
237                             jointInfo = courseJointAssembler.assemble(cluRel, cluRel.getCluId(), null, false, contextInfo);
238                         }
239                         if (jointInfo != null){
240                             course.getJoints().add(jointInfo);
241                         }
242                     }
243                 }
244             } catch (DoesNotExistException e) {
245             } catch (Exception e) {
246                 throw new AssemblyException("Error getting course joints", e);
247             }
248 
249             try {
250                 // Use the cluService to find formats, then convert and add to
251                 // the course
252                 List<CluInfo> formats = cluService.getRelatedClusByCluAndRelationType(course
253                         .getId(),
254                         CourseAssemblerConstants.COURSE_FORMAT_RELATION_TYPE , new ContextInfo());
255 
256                 for (CluInfo format : formats) {
257                     FormatInfo formatInfo = formatAssembler.assemble(format,
258                             null, false,contextInfo);
259                     course.getFormats().add(formatInfo);
260                 }
261 
262             } catch (DoesNotExistException e) {
263             } catch (Exception e) {
264                 throw new AssemblyException("Error getting related formats", e);
265             }
266 
267             try{
268                 //Set Credit and Grading options
269                 List<CluResultInfo> cluResults = cluService.getCluResultByClu(course.getId(),contextInfo);
270 
271                 List<ResultValuesGroupInfo> creditOptions = assembleCreditOptions(cluResults, contextInfo);
272 
273                 course.setCreditOptions(creditOptions);
274 
275                 List<String> gradingOptions = assembleGradingOptions(cluResults);
276 
277                 course.setGradingOptions(gradingOptions);
278             } catch (DoesNotExistException e){
279             } catch (Exception e) {
280                 throw new AssemblyException("Error getting course results", e);
281             }
282 
283             //Learning Objectives
284             //course.getCourseSpecificLOs().addAll(cluAssemblerUtils.assembleLos(course.getId(), shallowBuild, contextInfo));
285 
286         }
287 
288         //Remove special cases for grading options
289         boolean isAudit = course.getGradingOptions().remove(CourseAssemblerConstants.COURSE_RESULT_COMP_GRADE_AUDIT);
290         //course.setAttributeValue(CourseAssemblerConstants.COURSE_RESULT_COMP_ATTR_AUDIT, Boolean.toString(isAudit));
291         return course;
292     }
293 
294     @Override
295     public BaseDTOAssemblyNode<CourseInfo, CluInfo> disassemble(
296             CourseInfo course, NodeOperation operation,ContextInfo contextInfo)
297             throws AssemblyException, PermissionDeniedException {
298 
299         if (course == null) {
300             // FIXME Unsure now if this is an exception or just return null or
301             // empty assemblyNode
302             LOG.error("Course to disassemble is null!");
303             throw new AssemblyException("Course can not be null");
304         }
305 
306         BaseDTOAssemblyNode<CourseInfo, CluInfo> result = new BaseDTOAssemblyNode<CourseInfo, CluInfo>(
307                 this);
308 
309         CluInfo clu;
310         try {
311             clu = (NodeOperation.UPDATE == operation) ? cluService.getClu(course.getId(),contextInfo) : new CluInfo();
312         } catch (Exception e) {
313             throw new AssemblyException("Error getting existing learning unit during course update", e);
314         }
315 
316         // Create the id if it's not there already(important for creating
317         // relations)
318         clu.setId(UUIDHelper.genStringUUID(course.getId()));
319         if (null == course.getId()) {
320             course.setId(clu.getId());
321         }
322         clu.setTypeKey(CourseAssemblerConstants.COURSE_TYPE);
323         clu.setStateKey(course.getStateKey());
324         clu.setIsEnrollable(false);
325 
326         CluIdentifierInfo identifier = new CluIdentifierInfo();
327         identifier.setTypeKey(CourseAssemblerConstants.COURSE_OFFICIAL_IDENT_TYPE);
328         identifier.setStateKey(course.getStateKey());
329         identifier.setLongName(course.getCourseTitle());
330         identifier.setShortName(course.getTranscriptTitle());
331         identifier.setSuffixCode(course.getCourseNumberSuffix());
332         identifier.setDivision(course.getSubjectArea());
333         identifier.setCode(course.getCode());
334 
335         //Custom logic to set the level, if level not provided
336         if(StringUtils.hasText(course.getLevel())) {
337             identifier.setLevel(course.getLevel());
338         } else if(course.getCourseNumberSuffix()!=null&&course.getCourseNumberSuffix().length()>=3){
339             identifier.setLevel(course.getCourseNumberSuffix().substring(0, 1)+"00");
340         }
341 
342         clu.setOfficialIdentifier(identifier);
343 
344         clu.setAdminOrgs(new ArrayList<AdminOrgInfo>());
345 
346         // Use the Course Variation assembler to disassemble the variations
347 
348         // copy all fields
349         //Remove any existing variations or crosslistings
350         for(Iterator<CluIdentifierInfo> iter = clu.getAlternateIdentifiers().iterator();iter.hasNext();){
351             CluIdentifierInfo cluIdentifier = iter.next();
352             if(CourseAssemblerConstants.COURSE_VARIATION_IDENT_TYPE.equals(cluIdentifier.getTypeKey()) ||
353                     CourseAssemblerConstants.COURSE_CROSSLISTING_IDENT_TYPE.equals(cluIdentifier.getTypeKey()) ){
354                 iter.remove();
355             }
356         }
357         //Add in variations
358         for(CourseVariationInfo variation:course.getVariations()){
359             CluIdentifierInfo cluIdentifier = new CluIdentifierInfo();
360             cluIdentifier.setId(variation.getId());
361             cluIdentifier.setTypeKey(CourseAssemblerConstants.COURSE_VARIATION_IDENT_TYPE);
362             cluIdentifier.setCode(identifier.getCode());
363             cluIdentifier.setSuffixCode(course.getCourseNumberSuffix());
364             cluIdentifier.setDivision(course.getSubjectArea());
365             cluIdentifier.setVariation(variation.getVariationCode());
366             cluIdentifier.setLongName(variation.getVariationTitle());
367             cluIdentifier.setStateKey(course.getStateKey());
368             clu.getAlternateIdentifiers().add(cluIdentifier);
369         }
370         //Add in crosslistings
371         for(CourseCrossListingInfo crossListing:course.getCrossListings()){
372             CluIdentifierInfo cluIdentifier = new CluIdentifierInfo();
373             cluIdentifier.setId(crossListing.getId());
374             cluIdentifier.setTypeKey(CourseAssemblerConstants.COURSE_CROSSLISTING_IDENT_TYPE);
375             cluIdentifier.setSuffixCode(crossListing.getCourseNumberSuffix());
376             cluIdentifier.setDivision(crossListing.getSubjectArea());
377             cluIdentifier.setStateKey(course.getStateKey());
378             cluIdentifier.setOrgId(crossListing.getSubjectOrgId());
379             cluIdentifier.setAttributes(crossListing.getAttributes());
380             cluIdentifier.setCode(crossListing.getCode());
381             clu.getAlternateIdentifiers().add(cluIdentifier);
382         }
383 
384         List<AdminOrgInfo> adminOrgInfos = new ArrayList<AdminOrgInfo>();
385         for(String org:course.getUnitsDeployment()){
386             AdminOrgInfo adminOrg = new AdminOrgInfo();
387             adminOrg.setTypeKey(CourseAssemblerConstants.ADMIN_ORG);
388             adminOrg.setOrgId(org);
389             adminOrgInfos.add(adminOrg);
390         }
391         clu.getAdminOrgs().addAll(adminOrgInfos);
392 
393         List<AdminOrgInfo> subjectOrgs = new ArrayList<AdminOrgInfo>();
394         for (String subOrg : course.getUnitsContentOwner()) {
395             AdminOrgInfo subjectOrg = new AdminOrgInfo();
396             subjectOrg.setTypeKey(CourseAssemblerConstants.SUBJECT_ORG);
397             subjectOrg.setOrgId(subOrg);
398             subjectOrgs.add(subjectOrg);
399         }
400         clu.getAdminOrgs().addAll(subjectOrgs);
401 
402 
403         clu.setAttributes(course.getAttributes());
404         clu.setCampusLocations(course.getCampusLocations());
405         clu.setDescr(course.getDescr());
406         clu.setStdDuration(course.getDuration());
407 
408         //Default course effective dates to the atps if entered
409         if(course.getStartTerm() != null){
410             try {
411                 AtpInfo startAtp = atpService.getAtp(course.getStartTerm(), contextInfo);
412                 course.setEffectiveDate(startAtp.getStartDate());
413             } catch (Exception e) {
414                 throw new AssemblyException("Error getting start term Atp.",e);
415             }
416         }
417         if(course.getEndTerm() != null){
418             try {
419                 AtpInfo endAtp = atpService.getAtp(course.getEndTerm(), contextInfo);
420                 course.setExpirationDate(endAtp.getEndDate());
421             } catch (Exception e) {
422                 throw new AssemblyException("Error getting end term Atp.",e);
423             }
424         }
425 
426         clu.setEffectiveDate(course.getEffectiveDate());
427         clu.setExpirationDate(course.getExpirationDate());
428 
429         clu.setOfferedAtpTypes(course.getTermsOffered());
430         clu.setPrimaryInstructor(course.getPrimaryInstructor());
431 
432         clu.setIntensity(course.getOutOfClassHours());
433         clu.setInstructors(course.getInstructors());
434 
435         clu.setExpectedFirstAtp(course.getStartTerm());
436         clu.setLastAtp(course.getEndTerm());
437 
438         clu.setMeta(course.getMeta());
439         clu.setVersion(course.getVersion());
440 
441         // Add the Clu to the result
442         result.setNodeData(clu);
443         result.setOperation(operation);
444         result.setBusinessDTORef(course);
445 
446         // Use the Format assembler to disassemble the formats and relations
447         List<BaseDTOAssemblyNode<?, ?>> formatResults;
448         try {
449             formatResults = disassembleFormats(clu.getId(), course, operation,contextInfo);
450             result.getChildNodes().addAll(formatResults);
451 
452         } catch (DoesNotExistException e) {
453         } catch (Exception e) {
454             throw new AssemblyException("Error while disassembling format", e);
455         }
456 
457         // Use the CourseJoint assembler to disassemble the CourseJoints and
458         // relations
459         List<BaseDTOAssemblyNode<?, ?>> courseJointResults = disassembleJoints(clu.getId(), course, operation,contextInfo);
460         result.getChildNodes().addAll(courseJointResults);
461 
462         //Disassemble the CluResults (grading and credit options)
463         //Special code to take audit from attributes and put into options
464         for (AttributeInfo attr : course.getAttributes()) {
465             if (attr.getKey() != null){
466                 if (attr.getKey().equals(CourseAssemblerConstants.COURSE_RESULT_COMP_ATTR_AUDIT)
467                         && "true".equals(attr.getValue()) ){
468                     if(!course.getGradingOptions().contains(CourseAssemblerConstants.COURSE_RESULT_COMP_GRADE_AUDIT)){
469                         course.getGradingOptions().add(CourseAssemblerConstants.COURSE_RESULT_COMP_GRADE_AUDIT);
470                     }
471                     break;
472                 }
473             }
474         }
475 
476         // Patch to disassemble pass/fail option as well (KSLAB-2633)
477         for (AttributeInfo attr : course.getAttributes()){
478             if (attr.getKey() != null){
479                 if (attr.getKey().equals(CourseAssemblerConstants.COURSE_RESULT_COMP_ATTR_PASSFAIL)
480                         && "true".equals(attr.getValue()) ){
481                     if(!course.getGradingOptions().contains(CourseAssemblerConstants.COURSE_RESULT_COMP_GRADE_PASSFAIL)){
482                         course.getGradingOptions().add(CourseAssemblerConstants.COURSE_RESULT_COMP_GRADE_PASSFAIL);
483                     }
484                     break;
485                 }
486             }
487         }
488         
489       
490         List<CluResultInfo> cluResultList;
491         try {
492             cluResultList = cluService.getCluResultByClu(clu.getId(),contextInfo);
493         } catch (DoesNotExistException e) {
494             cluResultList = Collections.emptyList();
495         } catch (Exception e) {
496             throw new AssemblyException("Error getting cluResults", e);
497         }
498 
499         List<BaseDTOAssemblyNode<?, ?>> creditOutcomes = disassembleCreditOutcomes(course, clu, cluResultList, operation, contextInfo);
500         result.getChildNodes().addAll(creditOutcomes);
501         BaseDTOAssemblyNode<?, ?> gradingOptions = disassembleGradingOptions(clu.getId(), course.getStateKey(), course.getGradingOptions(), cluResultList, operation);
502         result.getChildNodes().add(gradingOptions);
503 
504         //Use the LoAssembler to disassemble Los
505         try {
506             List<BaseDTOAssemblyNode<?, ?>> loResults;
507             loResults = disassembleLos(clu.getId(), course, operation,contextInfo);
508             result.getChildNodes().addAll(loResults);
509         } catch (Exception e) {
510             throw new AssemblyException("Error while disassembling los", e);
511         }
512 
513         //add the special topics code if it did not exist, or remove if it was not wanted
514         boolean alreadyHadSpecialTopicsCode = false;
515         for(Iterator<LuCodeInfo> luCodeIterator = clu.getLuCodes().iterator();luCodeIterator.hasNext();){
516             LuCodeInfo luCode = luCodeIterator.next();
517             if(CourseAssemblerConstants.COURSE_CODE_SPECIAL_TOPICS.equals(luCode.getType())){
518                 alreadyHadSpecialTopicsCode = true;
519                 if(!course.isSpecialTopicsCourse()){
520                     luCodeIterator.remove();
521                 }
522                 break;
523             }
524         }
525         if(!alreadyHadSpecialTopicsCode && course.isSpecialTopicsCourse()){
526             LuCodeInfo luCode = new LuCodeInfo();
527             luCode.setType(CourseAssemblerConstants.COURSE_CODE_SPECIAL_TOPICS);
528             luCode.setValue("true");
529             clu.getLuCodes().add(luCode);
530         }
531 
532         //add the special topics code if it did not exist, or remove if it was not wanted
533         boolean alreadyHadPilotCourseCode = false;
534         for(Iterator<LuCodeInfo> luCodeIterator = clu.getLuCodes().iterator();luCodeIterator.hasNext();){
535             LuCodeInfo luCode = luCodeIterator.next();
536             if(CourseAssemblerConstants.COURSE_CODE_PILOT_COURSE.equals(luCode.getType())){
537                 alreadyHadPilotCourseCode = true;
538                 if(!course.isPilotCourse()){
539                     luCodeIterator.remove();
540                 }
541                 break;
542             }
543         }
544         if(!alreadyHadPilotCourseCode && course.isPilotCourse()){
545             LuCodeInfo luCode = new LuCodeInfo();
546             luCode.setType(CourseAssemblerConstants.COURSE_CODE_PILOT_COURSE);
547             luCode.setValue("true");
548             clu.getLuCodes().add(luCode);
549         }
550 
551         //FEES
552         if(clu.getFeeInfo() == null){
553             clu.setFeeInfo(new CluFeeInfo());
554         }
555         clu.getFeeInfo().setDescr(course.getFeeJustification());
556         clu.getFeeInfo().getCluFeeRecords().clear();
557         for(CourseRevenueInfo courseRevenue:course.getRevenues()){
558             CluFeeRecordInfo cluFeeRecord  = new CluFeeRecordInfo();
559             cluFeeRecord.setFeeType(CourseAssemblerConstants.COURSE_FINANCIALS_REVENUE_TYPE);
560             cluFeeRecord.setRateType(CourseAssemblerConstants.COURSE_FINANCIALS_REVENUE_TYPE);
561 
562             cluFeeRecord.setAttributes(courseRevenue.getAttributes());
563             cluFeeRecord.setAffiliatedOrgs(courseRevenue.getAffiliatedOrgs());
564             cluFeeRecord.setId(courseRevenue.getId());
565             cluFeeRecord.setMeta(courseRevenue.getMeta());
566             clu.getFeeInfo().getCluFeeRecords().add(cluFeeRecord);
567         }
568         for(CourseFeeInfo courseFee : course.getFees()){
569             CluFeeRecordInfo cluFeeRecord  = new CluFeeRecordInfo();
570             cluFeeRecord.setFeeType(courseFee.getFeeType());
571             cluFeeRecord.setRateType(courseFee.getRateType());
572             cluFeeRecord.setDescr(courseFee.getDescr());
573             cluFeeRecord.setMeta(courseFee.getMeta());
574             cluFeeRecord.setId(courseFee.getId());
575             cluFeeRecord.setFeeAmounts(courseFee.getFeeAmounts());
576 
577             cluFeeRecord.setAttributes(courseFee.getAttributes());
578             clu.getFeeInfo().getCluFeeRecords().add(cluFeeRecord);
579         }
580         if(clu.getAccountingInfo() == null || course.getExpenditure()== null){
581             clu.setAccountingInfo( new CluAccountingInfo());
582         }
583         if(course.getExpenditure() != null){
584             clu.getAccountingInfo().setAffiliatedOrgs(course.getExpenditure().getAffiliatedOrgs());
585 
586             clu.getAccountingInfo().setAttributes(course.getExpenditure().getAttributes());
587         }
588 
589         return result;
590     }
591 
592     /**
593      * Only checks for floats with format dddd.dddd where d's are optional (digits).
594      * The decimal point is also optional.
595      * Negative floats not checked.  Also, floats with other formats not checked.
596      * @param str
597      * @return true if it's a simple float (could be an integer)
598      */
599     private boolean _checkIfSimpleFloat(String str) {
600         String DIGITS = "0123456789";
601         String DIGITS_AND_DOT = DIGITS + ".";
602         boolean foundDot = false;
603         int numDigits = 0;
604         for (int i = 0; i < str.length(); i++) {
605             String ch = str.substring(i, i + 1);
606             if (DIGITS_AND_DOT.indexOf(ch) < 0) {
607                 return false; //
608             }
609             if (".".equals(ch)) {
610                 if (!foundDot) {
611                     foundDot = true;
612                 } else {
613                     // Found a second dot--not valid
614                     return false;
615                 }
616             }
617             if (DIGITS.indexOf(ch) >= 0) {
618                 numDigits++;
619             }
620         }
621         // Just checks if digits are correct.
622         return numDigits > 0;
623     }
624 
625     /**
626      * If resultValueKeys contains IDs, then look up its values and create a list of values
627      * @param resultValueKeys Potential list of IDs
628      * @return List of values
629      */
630     private List<String> _computeResultValues(List<String> resultValueKeys, ContextInfo contextInfo) throws InvalidParameterException, MissingParameterException, DoesNotExistException, PermissionDeniedException, OperationFailedException {
631         List<String> values = new ArrayList<String>();
632         if(resultValueKeys!=null && !resultValueKeys.isEmpty()){
633             String firstKey = resultValueKeys.get(0);
634 
635 
636             if (_checkIfSimpleFloat(firstKey)) {
637                 // No modification needed
638                 return resultValueKeys;
639             }
640             // Assume that resultValueKeys contains IDs
641             for (String key: resultValueKeys) {
642                 ResultValueInfo rv = lrcService.getResultValue(key, contextInfo);
643                 String value = rv.getValue();
644                 values.add(value);
645             }
646         }
647         return values;
648     }
649 
650     private List<BaseDTOAssemblyNode<?, ?>> disassembleCreditOutcomes(CourseInfo course, CluInfo clu, List<CluResultInfo> currentCluResults, NodeOperation operation, ContextInfo contextInfo) throws AssemblyException, NumberFormatException {
651 
652         List<BaseDTOAssemblyNode<?, ?>> results = new ArrayList<BaseDTOAssemblyNode<?, ?>>();
653 
654         String courseResultType = CourseAssemblerConstants.COURSE_RESULT_TYPE_CREDITS;
655 
656         //See if we need to create any new lrcs
657         if(NodeOperation.DELETE!=operation){
658             //Find all the existing LRCs for the following three types
659             Set<String> resultValueGroupIds = new HashSet<String>();
660 
661             try{
662                 try {
663                     resultValueGroupIds.addAll(lrcService.getResultValuesGroupKeysByType(CourseAssemblerConstants.COURSE_RESULT_COMP_TYPE_CREDIT_FIXED, contextInfo));
664                 } catch (DoesNotExistException e) {}
665                 try {
666                     resultValueGroupIds.addAll(lrcService.getResultValuesGroupKeysByType(CourseAssemblerConstants.COURSE_RESULT_COMP_TYPE_CREDIT_MULTIPLE, contextInfo));
667                 } catch (DoesNotExistException e) {}
668                 try {
669                     resultValueGroupIds.addAll(lrcService.getResultValuesGroupKeysByType(CourseAssemblerConstants.COURSE_RESULT_COMP_TYPE_CREDIT_VARIABLE, contextInfo));
670                 } catch (DoesNotExistException e) {}
671 
672                 //Create any LRCs that do not yet exist
673                 for(ResultValuesGroupInfo creditOption:course.getCreditOptions()){
674 
675                     String id = null;
676                     String type = null;
677                     List<String> resultValues = null;
678                     ResultValueRangeInfo resultValueRange = null;
679                     //Depending on the type, set the id, type and result values differently
680                     if(CourseAssemblerConstants.COURSE_RESULT_COMP_TYPE_CREDIT_FIXED.equals(creditOption.getTypeKey())){
681                         float fixedCreditValue = Float.parseFloat(creditOption.getResultValueRange().getMinValue());
682                         id = CourseAssemblerConstants.COURSE_RESULT_COMP_CREDIT_PREFIX + fixedCreditValue;
683                         type = CourseAssemblerConstants.COURSE_RESULT_COMP_TYPE_CREDIT_FIXED;
684                         resultValues = new ArrayList<String>();
685                         resultValues.add(String.valueOf(fixedCreditValue));
686                         resultValueRange = new ResultValueRangeInfo();
687                         resultValueRange.setMinValue(String.valueOf(fixedCreditValue));
688                         resultValueRange.setMaxValue(String.valueOf(fixedCreditValue));
689                     } else if (CourseAssemblerConstants.COURSE_RESULT_COMP_TYPE_CREDIT_MULTIPLE.equals(creditOption.getTypeKey())){
690                         List<String> resultVals = _computeResultValues(creditOption.getResultValueKeys(), contextInfo);
691                         Collections.sort(resultVals, new Comparator<String>() {
692                             public int compare(String o1, String o2) {
693                             return Float.compare(Float.parseFloat(o1),Float.parseFloat(o2));
694                             }
695                         });
696 
697                         StringBuilder sb = new StringBuilder(CourseAssemblerConstants.COURSE_RESULT_COMP_CREDIT_PREFIX);
698                         for (Iterator<String> iter = resultVals.iterator();
699                              iter.hasNext();){
700                             String str = iter.next();
701                             sb.append(str);
702                             if(iter.hasNext()){
703                                 sb.append(",");
704                             }
705                         }
706                         id = sb.toString();
707                         type = CourseAssemblerConstants.COURSE_RESULT_COMP_TYPE_CREDIT_MULTIPLE;
708                         resultValues = new ArrayList<String>();
709                         resultValues.addAll(resultVals);
710                     }else if(CourseAssemblerConstants.COURSE_RESULT_COMP_TYPE_CREDIT_VARIABLE.equals(creditOption.getTypeKey())){
711                         /*
712                                * For variable credits create a Result values that goes from min to max with the specified increment.
713                                * If no increment is specified, use 1.0 as the increment. The increment can be specified as a float.
714                                */
715 
716                         float minCredits = Float.parseFloat(creditOption.getResultValueRange().getMinValue());
717                         float maxCredits = Float.parseFloat(creditOption.getResultValueRange().getMaxValue());
718                         String creditValueIncr = creditOption.getResultValueRange().getIncrement();
719                         float increment = (null != creditValueIncr && creditValueIncr.length() > 0 ) ? Float.parseFloat(creditValueIncr) : defaultCreditIncrement ;
720 
721                         id = CourseAssemblerConstants.COURSE_RESULT_COMP_CREDIT_PREFIX + String.valueOf(minCredits) + "-" + String.valueOf(maxCredits);
722                         //Add in the increment to the key (logic is duplicated in LRC service)
723                         if (creditValueIncr!=null && !"1".equals(creditValueIncr) && !"1.0".equals(creditValueIncr)) {
724                             id += (" by " + creditValueIncr);
725                         }
726 
727                         type = CourseAssemblerConstants.COURSE_RESULT_COMP_TYPE_CREDIT_VARIABLE;
728                         resultValues = new ArrayList<String>();
729                         for(float i = minCredits; i <= maxCredits; i+=increment){
730                             resultValues.add(String.valueOf(i));
731                         }
732                         resultValueRange = new ResultValueRangeInfo();
733                         resultValueRange.setMinValue(String.valueOf(minCredits));
734                         resultValueRange.setMaxValue(String.valueOf(maxCredits));
735                         resultValueRange.setIncrement(String.valueOf(increment));
736                     }else{
737                         resultValues = Collections.emptyList();
738                     }
739 
740                     //Set the id
741                     creditOption.setKey(id);
742 
743                     //Ensure the resultValueKey has the proper prefix
744                     // TODO: Comment: Shouldn't muck around with altering IDs
745                     // Assumption is result values contains IDs, not string values like 2.0
746 
747 //                    String resultValueKeyPrefix = "kuali.result.value.credit.degree.";
748 //                    for(int i = 0; i < resultValues.size(); i++){
749 //                        if (!resultValues.get(i).contains("kuali.result.value")){ //only add the prefix if this is not a proper key
750 //                            String ithResultVal = resultValues.get(i);
751 //                            resultValues.set(i,resultValueKeyPrefix+Float.parseFloat(ithResultVal));
752 //                        }
753 //                    }
754 
755                     //Create a new result component
756                     if(id != null && !resultValueGroupIds.contains(id)){
757 
758                         //Build the new ResultValuesGroup
759                         ResultValuesGroupInfo resultValueGroup = new ResultValuesGroupInfo();
760                         resultValueGroup.setKey(id);
761                         resultValueGroup.setTypeKey(type);
762                         if (DtoConstants.STATE_DRAFT.equals(course.getStateKey())){
763                             resultValueGroup.setStateKey(LrcServiceConstants.RESULT_GROUPS_STATE_DRAFT);
764                         } else if (DtoConstants.STATE_APPROVED.equals(course.getStateKey())) {
765                             resultValueGroup.setStateKey(LrcServiceConstants.RESULT_GROUPS_STATE_APPROVED);
766                         } else {
767                             resultValueGroup.setStateKey(LrcServiceConstants.RESULT_GROUPS_STATE_RETIRED);
768                         }
769                         resultValueGroup.setResultScaleKey(LrcServiceConstants.RESULT_SCALE_KEY_CREDIT_DEGREE);
770                         resultValueGroup.setResultValueKeys(resultValues);
771                         resultValueGroup.setResultValueRange(resultValueRange);
772                         BaseDTOAssemblyNode<ResultValuesGroupInfo, ResultValuesGroupInfo> node = new BaseDTOAssemblyNode<ResultValuesGroupInfo, ResultValuesGroupInfo>(null);
773                         node.setOperation(NodeOperation.CREATE);
774                         node.setNodeData(resultValueGroup);
775                         node.setBusinessDTORef(creditOption);
776                         results.add(node);
777 
778                         resultValueGroupIds.add(id);
779                     }
780                 }
781             }catch (NumberFormatException e){
782                 throw new AssemblyException("Invalid Arguments for credit outcome values",e);
783             }catch (Exception e){
784                 throw new AssemblyException("Error Assembling", e);
785             }
786         }
787 
788         //Now do dissassembly for the actual clu-lrc relations and result options
789 
790         // Get the current options and put them in a map of option type id/cluResult
791         Map<String, List<CluResultInfo>> currentResults = new HashMap<String, List<CluResultInfo>>();
792 
793         //If this is not a create, lookup the results for this clu
794         if (!NodeOperation.CREATE.equals(operation)) {
795             for (CluResultInfo currentResult : currentCluResults) {
796                 if (courseResultType.equals(currentResult.getTypeKey())) {
797                     //There should only be one grading option per CluResult for credit outcomes
798                     if(currentResult.getResultOptions().size()==1){
799                         //Create a mapping to a list of cluresults with the same result componentId
800                         String resultComponentId = currentResult.getResultOptions().get(0).getResultComponentId();
801                         if(!currentResults.containsKey(resultComponentId)){
802                             currentResults.put(resultComponentId, new ArrayList<CluResultInfo>());
803                         }
804                         currentResults.get(resultComponentId).add(currentResult);
805                     }else{
806                         LOG.warn("Credit Results should have exactly one result option each");
807                     }
808                 }
809             }
810         }
811 
812         //Loop through options on the course, if they are new, create a new cluResult
813         for(ResultValuesGroupInfo creditOption : course.getCreditOptions()){
814             if (NodeOperation.CREATE == operation
815                     || (NodeOperation.UPDATE == operation && !currentResults.containsKey(creditOption.getKey()) )) {
816 
817                 ResultOptionInfo resultOption = new ResultOptionInfo();
818                 resultOption.setStateKey(course.getStateKey());
819                 resultOption.setResultComponentId(creditOption.getKey());
820 
821                 CluResultInfo cluResult = new CluResultInfo();
822                 cluResult.setCluId(clu.getId());
823                 cluResult.setStateKey(course.getStateKey());
824                 cluResult.setTypeKey(courseResultType);
825 
826                 cluResult.getResultOptions().add(resultOption);
827 
828                 BaseDTOAssemblyNode<ResultValuesGroupInfo, CluResultInfo> cluResultNode = new BaseDTOAssemblyNode<ResultValuesGroupInfo, CluResultInfo>(null);
829                 cluResultNode.setNodeData(cluResult);
830                 cluResultNode.setOperation(NodeOperation.CREATE);
831 
832                 results.add(cluResultNode);
833             } else if (NodeOperation.UPDATE == operation
834                     && currentResults.containsKey(creditOption.getKey())) {
835                 //Get the list from the map and remove an entry, if the list is empty then remove it from the map
836                 List<CluResultInfo> cluResults = currentResults.get(creditOption.getKey());
837                 cluResults.remove(cluResults.size()-1);
838                 if(cluResults.isEmpty()){
839                     currentResults.remove(creditOption.getKey());
840                 }
841             }
842         }
843 
844         //Delete the leftovers
845         for(Entry<String,List<CluResultInfo>> entry:currentResults.entrySet()){
846             for(CluResultInfo cluResult:entry.getValue()){
847                 BaseDTOAssemblyNode<ResultValuesGroupInfo, CluResultInfo> cluResultNode = new BaseDTOAssemblyNode<ResultValuesGroupInfo, CluResultInfo>(null);
848                 cluResultNode.setNodeData(cluResult);
849                 cluResultNode.setOperation(NodeOperation.DELETE);
850                 results.add(cluResultNode);
851             }
852         }
853 
854         return results;
855     }
856 
857     private List<String> assembleGradingOptions(List<CluResultInfo> cluResults){
858 
859         String courseResultType = CourseAssemblerConstants.COURSE_RESULT_TYPE_GRADE;
860 
861         List<String> results = new ArrayList<String>();
862         //Loop through all the CluResults to find the one with the matching type
863         for(CluResultInfo cluResult:cluResults){
864             if(courseResultType.equals(cluResult.getTypeKey())){
865                 //Loop through all options and add to the list of Strings
866                 for(ResultOptionInfo resultOption: cluResult.getResultOptions()){
867                     results.add(resultOption.getResultComponentId());
868                 }
869                 break;
870             }
871         }
872         return results;
873     }
874     //
875     private List<ResultValuesGroupInfo> assembleCreditOptions(
876             List<CluResultInfo> cluResults, ContextInfo contextInfo) throws AssemblyException {
877         String courseResultType = CourseAssemblerConstants.COURSE_RESULT_TYPE_CREDITS;
878         List<ResultValuesGroupInfo> results = new ArrayList<ResultValuesGroupInfo>();
879         //Loop through all the CluResults to find the one with the matching type
880         for(CluResultInfo cluResult:cluResults){
881             if(courseResultType.equals(cluResult.getTypeKey())){
882                 //Loop through all options and add to the list of Strings
883                 for(ResultOptionInfo resultOption: cluResult.getResultOptions()){
884                     try {
885                         if(resultOption.getResultComponentId()!=null){
886                             ResultValuesGroupInfo resultValuesGroup = lrcService.getResultValuesGroup(resultOption.getResultComponentId(), contextInfo);
887                             results.add(resultValuesGroup);
888                         }
889                     } catch (DoesNotExistException e) {
890                         LOG.warn("Course Credit option:"+resultOption.getId()+" refers to non-existant ResultValuesGroupInfo "+ resultOption.getResultComponentId());
891                     } catch (Exception e) {
892                         throw new AssemblyException("Error getting ResultValuesGroupInfo",e);
893                     }
894                 }
895             }
896         }
897         return results;
898     }
899 
900     // TODO Use CluAssemblerUtils
901     private List<BaseDTOAssemblyNode<?, ?>> disassembleLos(String cluId,  CourseInfo course, NodeOperation operation,ContextInfo contextInfo) throws AssemblyException {
902         // TODO Auto-generated method stub
903         List<BaseDTOAssemblyNode<?, ?>> results = new ArrayList<BaseDTOAssemblyNode<?, ?>>();
904 
905         // Get the current formats and put them in a map of format id/relation
906         // id
907         Map<String, CluLoRelationInfo> currentCluLoRelations = new HashMap<String, CluLoRelationInfo>();
908         try {
909             List<CluLoRelationInfo> cluLoRelations = cluService.getCluLoRelationsByClu(cluId,contextInfo);
910             for(CluLoRelationInfo cluLoRelation:cluLoRelations){
911                 if(CourseAssemblerConstants.COURSE_LO_COURSE_SPECIFIC_RELATION.equals(cluLoRelation.getTypeKey())){
912                     currentCluLoRelations.put(cluLoRelation.getLoId(), cluLoRelation);
913                 }
914             }
915         } catch (DoesNotExistException e) {
916         } catch (Exception e) {
917             throw new AssemblyException("Error finding related Los");
918         }
919 
920         // Loop through all the los in this clu
921         for(LoDisplayInfo loDisplay : course.getCourseSpecificLOs()){
922 
923             // If this is a clu create/new lo update then all los will be created
924             if (NodeOperation.CREATE == operation
925                     || (NodeOperation.UPDATE == operation &&  !currentCluLoRelations.containsKey(loDisplay.getLoInfo().getId()))) {
926 
927                 // the lo does not exist, so create
928                 // Assemble and add the lo
929                 loDisplay.getLoInfo().setId(null);
930                 loDisplay.getLoInfo().setStateKey(course.getStateKey());
931                 BaseDTOAssemblyNode<LoDisplayInfo, LoInfo> loNode = loAssembler
932                         .disassemble(loDisplay, NodeOperation.CREATE,contextInfo);
933                 results.add(loNode);
934 
935                 // Create the relationship and add it as well
936                 CluLoRelationInfo relation = new CluLoRelationInfo();
937                 relation.setCluId(cluId);
938                 relation.setLoId(loNode.getNodeData().getId());
939                 relation
940                         .setTypeKey(CourseAssemblerConstants.COURSE_LO_COURSE_SPECIFIC_RELATION);
941                 relation.setStateKey(course.getStateKey());
942 
943                 BaseDTOAssemblyNode<LoDisplayInfo, CluLoRelationInfo> relationNode = new BaseDTOAssemblyNode<LoDisplayInfo, CluLoRelationInfo>(
944                         null);
945                 relationNode.setNodeData(relation);
946                 relationNode.setOperation(NodeOperation.CREATE);
947 
948                 results.add(relationNode);
949             } else if (NodeOperation.UPDATE == operation
950                     && currentCluLoRelations.containsKey(loDisplay.getLoInfo().getId())) {
951                 loDisplay.getLoInfo().setStateKey(course.getStateKey());
952                 // If the clu already has this lo, then just update the lo
953                 BaseDTOAssemblyNode<LoDisplayInfo, LoInfo> loNode = loAssembler
954                         .disassemble(loDisplay, NodeOperation.UPDATE,contextInfo);
955                 results.add(loNode);
956 
957                 // remove this entry from the map so we can tell what needs to
958                 // be deleted at the end
959                 currentCluLoRelations.remove(loDisplay.getLoInfo().getId());
960             } else if (NodeOperation.DELETE == operation
961                     && currentCluLoRelations.containsKey(loDisplay.getLoInfo().getId())) {
962 
963                 // Delete the Format and its relation
964                 CluLoRelationInfo relationToDelete = currentCluLoRelations.get(loDisplay.getLoInfo().getId());
965                 BaseDTOAssemblyNode<LoDisplayInfo, CluLoRelationInfo> relationToDeleteNode = new BaseDTOAssemblyNode<LoDisplayInfo, CluLoRelationInfo>(
966                         null);
967                 relationToDeleteNode.setNodeData(relationToDelete);
968                 relationToDeleteNode.setOperation(NodeOperation.DELETE);
969                 results.add(relationToDeleteNode);
970 
971                 BaseDTOAssemblyNode<LoDisplayInfo, LoInfo> loNode = loAssembler
972                         .disassemble(loDisplay, NodeOperation.DELETE,contextInfo);
973                 results.add(loNode);
974 
975                 // remove this entry from the map so we can tell what needs to
976                 // be deleted at the end
977                 currentCluLoRelations.remove(loDisplay.getLoInfo().getId());
978             }
979         }
980 
981         // Now any leftover lo ids are no longer needed, so delete
982         // los and relations
983         for (Entry<String, CluLoRelationInfo> entry : currentCluLoRelations.entrySet()) {
984             // Create a new relation with the id of the relation we want to
985             // delete
986             CluLoRelationInfo relationToDelete = entry.getValue();
987             BaseDTOAssemblyNode<LoDisplayInfo, CluLoRelationInfo> relationToDeleteNode = new BaseDTOAssemblyNode<LoDisplayInfo, CluLoRelationInfo>(
988                     null);
989             relationToDeleteNode.setNodeData(relationToDelete);
990             relationToDeleteNode.setOperation(NodeOperation.DELETE);
991             results.add(relationToDeleteNode);
992 
993             try{
994                 LoInfo loToDelete = loService.getLo(entry.getKey(),contextInfo);
995 
996                 LoDisplayInfo loDisplayToDelete = loAssembler.assemble(loToDelete, null, false,contextInfo);
997                 BaseDTOAssemblyNode<LoDisplayInfo, LoInfo> loNode = loAssembler
998                         .disassemble(loDisplayToDelete, NodeOperation.DELETE,contextInfo);
999                 results.add(loNode);
1000             } catch (DoesNotExistException e){
1001                 LOG.warn("Trying to delete non exsistant LO:"+entry.getKey());
1002             } catch (Exception e) {
1003                 throw new AssemblyException("Error disassembling LOs",e);
1004             }
1005         }
1006 
1007         return results;
1008     }
1009 
1010     private BaseDTOAssemblyNode<?, ?> disassembleGradingOptions(String cluId,
1011                                                                 String courseState, List<String> options, List<CluResultInfo> currentCluResults, NodeOperation operation) throws AssemblyException {
1012         BaseDTOAssemblyNode<List<String>, CluResultInfo> cluResultNode = new BaseDTOAssemblyNode<List<String>, CluResultInfo>(null);
1013 
1014         String courseResultType=CourseAssemblerConstants.COURSE_RESULT_TYPE_GRADE;
1015         String resultsDescription="Grading options";
1016         String resultDescription="Grading option";
1017 
1018         // Get the current options and put them in a map of option type id/cluResult
1019         Map<String, ResultOptionInfo> currentResults = new HashMap<String, ResultOptionInfo>();
1020 
1021         CluResultInfo cluResult = null;
1022 
1023         //If this is not a create, lookup the results for this clu
1024         if (!NodeOperation.CREATE.equals(operation)) {
1025             for (CluResultInfo currentResult : currentCluResults) {
1026                 if (courseResultType.equals(currentResult.getTypeKey())) {
1027                     cluResult = currentResult;
1028                     if(NodeOperation.DELETE.equals(operation)){
1029                         //if this is a delete, then we only need the CluResultInfo
1030                         cluResultNode.setOperation(NodeOperation.DELETE);
1031                     }else{
1032                         //Find all the Result options and store in a map for easy access later
1033                         cluResultNode.setOperation(NodeOperation.UPDATE);
1034                         for(ResultOptionInfo resultOption:currentResult.getResultOptions()){
1035                             currentResults.put(resultOption.getResultComponentId(), resultOption);
1036                         }
1037                     }
1038                     break;
1039                 }
1040             }
1041         }
1042 
1043         //If this is a delete we don't need the result options, just the CluResultInfo
1044         if(!NodeOperation.DELETE.equals(operation)){
1045             if(cluResult == null){
1046                 //Create a new resultInfo of the given type if one does not exist and set operation to Create
1047                 cluResult = new CluResultInfo();
1048                 cluResult.setCluId(cluId);
1049                 cluResult.setStateKey(courseState);
1050                 cluResult.setTypeKey(courseResultType);
1051                 RichTextInfo desc = new RichTextInfo();
1052                 desc.setPlain(resultsDescription);
1053                 cluResult.setDescr(desc);
1054                 cluResult.setEffectiveDate(new Date());
1055                 cluResultNode.setOperation(NodeOperation.CREATE);
1056             }
1057 
1058             cluResult.setResultOptions(new ArrayList<ResultOptionInfo>());
1059 
1060             // Loop through all the credit options in this course
1061             for (String optionType : options) {
1062                 if(currentResults.containsKey(optionType)){
1063                     //If the option exists already copy it to the new list of result options
1064                     ResultOptionInfo resultOptionInfo = currentResults.get(optionType);
1065                     cluResult.getResultOptions().add(resultOptionInfo);
1066                 }else{
1067                     //Otherwise create a new result option
1068                     ResultOptionInfo resultOptionInfo = new ResultOptionInfo();
1069                     RichTextInfo desc = new RichTextInfo();
1070                     desc.setPlain(resultDescription);
1071                     resultOptionInfo.setDescr(desc);
1072                     resultOptionInfo.setResultComponentId(optionType);
1073                     resultOptionInfo.setStateKey(courseState);
1074 
1075                     cluResult.getResultOptions().add(resultOptionInfo);
1076                 }
1077             }
1078         }
1079 
1080         cluResultNode.setNodeData(cluResult);
1081         return cluResultNode;
1082     }
1083 
1084     // TODO This is pretty much a copy of the FormatAssembler's
1085     // disassembleActivities code... maybe can be made generic
1086     private List<BaseDTOAssemblyNode<?, ?>> disassembleFormats(String nodeId,
1087                                                                CourseInfo course, NodeOperation operation,ContextInfo contextInfo)
1088             throws AssemblyException, DoesNotExistException, InvalidParameterException, MissingParameterException, OperationFailedException, PermissionDeniedException {
1089 
1090         List<BaseDTOAssemblyNode<?, ?>> results = new ArrayList<BaseDTOAssemblyNode<?, ?>>();
1091 
1092         // Get the current formats and put them in a map of format id/relation
1093         // id
1094         Map<String, String> currentformatIds = new HashMap<String, String>();
1095 
1096         if (!NodeOperation.CREATE.equals(operation)) {
1097             try {
1098                 List<CluCluRelationInfo> formatRelationships = cluService
1099                         .getCluCluRelationsByClu(course.getId(),contextInfo);
1100 
1101                 formatRelationships = (null == formatRelationships) ? new ArrayList<CluCluRelationInfo>() : formatRelationships;
1102 
1103                 for (CluCluRelationInfo formatRelation : formatRelationships) {
1104                     if (CourseAssemblerConstants.COURSE_FORMAT_RELATION_TYPE
1105                             .equals(formatRelation.getTypeKey())) {
1106                         currentformatIds.put(formatRelation.getRelatedCluId(),
1107                                 formatRelation.getId());
1108                     }
1109                 }
1110             } catch (DoesNotExistException e) {
1111             } catch (InvalidParameterException e) {
1112                 throw new AssemblyException("Error getting related formats", e);
1113             } catch (MissingParameterException e) {
1114                 throw new AssemblyException("Error getting related formats", e);
1115             } catch (OperationFailedException e) {
1116                 throw new AssemblyException("Error getting related formats", e);
1117             }
1118         }
1119 
1120         // Loop through all the formats in this course
1121         for (FormatInfo format : course.getFormats()) {
1122 
1123             //  If this is a course create/new format update then all formats will be created
1124             if (NodeOperation.CREATE == operation
1125                     || (NodeOperation.UPDATE == operation && !currentformatIds.containsKey(format.getId()) )) {
1126                 // the format does not exist, so create
1127                 // Assemble and add the format
1128                 format.setStateKey(course.getStateKey());
1129                 BaseDTOAssemblyNode<FormatInfo, CluInfo> formatNode = formatAssembler
1130                         .disassemble(format, NodeOperation.CREATE,contextInfo);
1131                 results.add(formatNode);
1132 
1133 
1134                 // Create the relationship and add it as well
1135                 CluCluRelationInfo relation = new CluCluRelationInfo();
1136                 relation.setCluId(nodeId);
1137                 relation.setRelatedCluId(formatNode.getNodeData().getId());// this
1138                 // should
1139                 // already
1140                 // be set even if
1141                 // it's a create
1142                 relation
1143                         .setTypeKey(CourseAssemblerConstants.COURSE_FORMAT_RELATION_TYPE);
1144                 relation.setStateKey(course.getStateKey());
1145 
1146                 BaseDTOAssemblyNode<CourseInfo, CluCluRelationInfo> relationNode = new BaseDTOAssemblyNode<CourseInfo, CluCluRelationInfo>(
1147                         null);
1148                 relationNode.setNodeData(relation);
1149                 relationNode.setOperation(NodeOperation.CREATE);
1150 
1151                 results.add(relationNode);
1152             } else if (NodeOperation.UPDATE == operation
1153                     && currentformatIds.containsKey(format.getId())) {
1154                 // If the course already has this format, then just update the
1155                 // format
1156                 format.setStateKey(course.getStateKey());
1157                 BaseDTOAssemblyNode<FormatInfo, CluInfo> formatNode = formatAssembler
1158                         .disassemble(format, NodeOperation.UPDATE,contextInfo);
1159                 results.add(formatNode);
1160 
1161                 // remove this entry from the map so we can tell what needs to
1162                 // be deleted at the end
1163                 currentformatIds.remove(format.getId());
1164             } else if (NodeOperation.DELETE == operation
1165                     && currentformatIds.containsKey(format.getId()))  {
1166                 // Delete the Format and its relation
1167                 CluCluRelationInfo relationToDelete = new CluCluRelationInfo();
1168                 relationToDelete.setId( currentformatIds.get(format.getId()) );
1169                 BaseDTOAssemblyNode<CourseInfo, CluCluRelationInfo> relationToDeleteNode = new BaseDTOAssemblyNode<CourseInfo, CluCluRelationInfo>(
1170                         null);
1171                 relationToDeleteNode.setNodeData(relationToDelete);
1172                 relationToDeleteNode.setOperation(NodeOperation.DELETE);
1173                 results.add(relationToDeleteNode);
1174 
1175                 BaseDTOAssemblyNode<FormatInfo, CluInfo> formatNode = formatAssembler
1176                         .disassemble(format, NodeOperation.DELETE,contextInfo);
1177                 results.add(formatNode);
1178 
1179                 // remove this entry from the map so we can tell what needs to
1180                 // be deleted at the end
1181                 currentformatIds.remove(format.getId());
1182             }
1183         }
1184 
1185         // Now any leftover format ids are no longer needed, so delete
1186         // formats and relations. These formats have to be assembled first before they can be marked for deletion
1187         for (Entry<String, String> entry : currentformatIds.entrySet()) {
1188             // Create a new relation with the id of the relation we want to
1189             // delete
1190             CluCluRelationInfo relationToDelete = new CluCluRelationInfo();
1191             relationToDelete.setId( entry.getValue() );
1192             BaseDTOAssemblyNode<CourseInfo, CluCluRelationInfo> relationToDeleteNode = new BaseDTOAssemblyNode<CourseInfo, CluCluRelationInfo>(
1193                     null);
1194             relationToDeleteNode.setNodeData(relationToDelete);
1195             relationToDeleteNode.setOperation(NodeOperation.DELETE);
1196             results.add(relationToDeleteNode);
1197 
1198             CluInfo formatCluToDelete = cluService.getClu(entry.getKey() , new ContextInfo());
1199             FormatInfo formatToDelete = formatAssembler.assemble(formatCluToDelete, null, false,contextInfo);
1200             BaseDTOAssemblyNode<FormatInfo, CluInfo> formatNode = formatAssembler
1201                     .disassemble(formatToDelete, NodeOperation.DELETE,contextInfo);
1202             results.add(formatNode);
1203         }
1204 
1205         return results;
1206     }
1207 
1208     private List<CourseVariationInfo> assembleVariations(List<CluIdentifierInfo> cluIdents) {
1209         List<CourseVariationInfo> variations = new ArrayList<CourseVariationInfo>();
1210         if (cluIdents != null) {
1211             for (CluIdentifierInfo cluIdent : cluIdents) {
1212                 if (cluIdent.getTypeKey() != null &&
1213                         cluIdent.getTypeKey().equals(CourseAssemblerConstants.COURSE_VARIATION_IDENT_TYPE)) {
1214                     CourseVariationInfo variation = new CourseVariationInfo();
1215                     variation.setId(cluIdent.getId());
1216                     variation.setTypeKey(cluIdent.getTypeKey());
1217                     variation.setCourseNumberSuffix(cluIdent.getSuffixCode());
1218                     variation.setSubjectArea(cluIdent.getDivision());
1219                     variation.setVariationCode(cluIdent.getVariation());
1220                     variation.setVariationTitle(cluIdent.getLongName());
1221                     variations.add(variation);
1222                 }
1223             }
1224         }
1225         return variations;
1226     }
1227 
1228     private List<CourseCrossListingInfo> assembleCrossListings(List<CluIdentifierInfo> cluIdents)
1229             throws AssemblyException {
1230         List<CourseCrossListingInfo> crossListings = new ArrayList<CourseCrossListingInfo>();
1231         if (cluIdents != null) {
1232             for (CluIdentifierInfo cluIdent : cluIdents) {
1233                 if (cluIdent.getTypeKey() != null &&
1234                         cluIdent.getTypeKey().equals(CourseAssemblerConstants.COURSE_CROSSLISTING_IDENT_TYPE)) {
1235                     CourseCrossListingInfo crosslisting = new CourseCrossListingInfo();
1236 
1237                     boolean found = false;
1238                     for (AttributeInfo attr : cluIdent.getAttributes()){
1239                         if (attr.getKey().equals("courseId")){
1240                             found = true;
1241                             try {
1242                                 CluInfo cluInfo = cluService.getClu(attr.getValue(), new ContextInfo());
1243                                 crosslisting.setId(cluIdent.getId());
1244                                 crosslisting.setCode(cluInfo.getOfficialIdentifier().getCode());
1245                                 crosslisting.setAttributes(cluIdent.getAttributes());
1246                                 crosslisting.setTypeKey(cluInfo.getTypeKey());
1247                                 crosslisting.setCourseNumberSuffix(cluInfo.getOfficialIdentifier().getSuffixCode());
1248                                 crosslisting.setSubjectArea(cluInfo.getOfficialIdentifier().getDivision());
1249                                 crosslisting.setSubjectOrgId(cluIdent.getOrgId());
1250                             } catch (Exception e) {
1251                                 throw new AssemblyException("Error getting related clus", e);
1252                             }
1253                         }
1254                     }
1255                     if (!found) {
1256                         crosslisting.setId(cluIdent.getId());
1257                         crosslisting.setCode(cluIdent.getCode());
1258                         crosslisting.setAttributes(cluIdent.getAttributes());
1259                         crosslisting.setTypeKey(cluIdent.getTypeKey());
1260                         crosslisting.setCourseNumberSuffix(cluIdent.getSuffixCode());
1261                         crosslisting.setSubjectArea(cluIdent.getDivision());
1262                         crosslisting.setSubjectOrgId(cluIdent.getOrgId());
1263                     }
1264 
1265                     crossListings.add(crosslisting);
1266                 }
1267             }
1268         }
1269         return crossListings;
1270     }
1271 
1272     // TODO This is pretty much a copy of the disassembleJoints
1273     // code... maybe can be made generic
1274     private List<BaseDTOAssemblyNode<?, ?>> disassembleJoints(String nodeId,
1275                                                               CourseInfo course, NodeOperation operation,ContextInfo contextInfo)
1276             throws AssemblyException, PermissionDeniedException {
1277 
1278         List<BaseDTOAssemblyNode<?, ?>> results = new ArrayList<BaseDTOAssemblyNode<?, ?>>();
1279 
1280         // Get the current joints and put them in a map of joint id/relation
1281         // id
1282         Map<String, CluCluRelationInfo> currentJointIds = new HashMap<String, CluCluRelationInfo>();
1283 
1284         if (!NodeOperation.CREATE.equals(operation)) {
1285             try {
1286                 List<CluCluRelationInfo> jointRelationships = cluService.getCluCluRelationsByClu(course.getId(),contextInfo);
1287                 for (CluCluRelationInfo jointRelation : jointRelationships) {
1288                     if (CourseAssemblerConstants.JOINT_RELATION_TYPE.equals(jointRelation.getTypeKey())) {
1289                         if(jointRelation.getCluId().equals(course.getId())) {
1290                             cluService.getClu(jointRelation.getRelatedCluId(), contextInfo);
1291                             currentJointIds.put(jointRelation.getId(),jointRelation);
1292                         } else {
1293                             cluService.getClu(jointRelation.getCluId(), contextInfo);
1294                             currentJointIds.put(jointRelation.getId(),jointRelation);
1295                         }
1296                     }
1297                 }
1298             } catch (DoesNotExistException e) {
1299             } catch (InvalidParameterException e) {
1300                 throw new AssemblyException("Error getting related formats", e);
1301             } catch (MissingParameterException e) {
1302                 throw new AssemblyException("Error getting related formats", e);
1303             } catch (OperationFailedException e) {
1304                 throw new AssemblyException("Error getting related formats", e);
1305             }
1306         }
1307 
1308         // Loop through all the joints in this course
1309         for (CourseJointInfo joint : course.getJoints()) {
1310             // If this is a course create then all joints will be created
1311             if (NodeOperation.UPDATE.equals(operation) && joint.getRelationId() != null
1312                     && currentJointIds.containsKey(joint.getRelationId())) {
1313                 // remove this entry from the map so we can tell what needs to
1314                 // be deleted at the end
1315                 CluCluRelationInfo relation = currentJointIds.remove(joint.getRelationId());
1316                 relation.setRelatedCluId(joint.getCourseId());
1317                 relation.setStateKey(course.getStateKey());
1318                 BaseDTOAssemblyNode<CourseJointInfo, CluCluRelationInfo> jointNode = new BaseDTOAssemblyNode<CourseJointInfo, CluCluRelationInfo>(courseJointAssembler);
1319                 jointNode.setBusinessDTORef(joint);
1320                 jointNode.setNodeData(relation);
1321                 jointNode.setOperation(NodeOperation.UPDATE);
1322                 results.add(jointNode);
1323             } else if (!NodeOperation.DELETE.equals(operation)) {
1324                 // the joint does not exist, so create cluclurelation
1325                 BaseDTOAssemblyNode<CourseJointInfo, CluCluRelationInfo> jointNode = courseJointAssembler
1326                         .disassemble(joint, NodeOperation.CREATE,contextInfo);
1327                 jointNode.getNodeData().setCluId(nodeId);
1328                 jointNode.getNodeData().setStateKey(course.getStateKey());
1329                 results.add(jointNode);
1330             }
1331         }
1332 
1333         // Now any leftover joint ids are no longer needed, so delete
1334         // joint relations
1335         for (String id : currentJointIds.keySet()) {
1336             // Create a new relation with the id of the relation we want to
1337             // delete
1338             CluCluRelationInfo relationToDelete = new CluCluRelationInfo();
1339             relationToDelete.setId(id);
1340             BaseDTOAssemblyNode<CourseJointInfo, CluCluRelationInfo> relationToDeleteNode = new BaseDTOAssemblyNode<CourseJointInfo, CluCluRelationInfo>(
1341                     courseJointAssembler);
1342             relationToDeleteNode.setNodeData(relationToDelete);
1343             relationToDeleteNode.setOperation(NodeOperation.DELETE);
1344             results.add(relationToDeleteNode);
1345         }
1346 
1347         return results;
1348     }
1349 
1350     public void setCluService(CluService cluService) {
1351         this.cluService = cluService;
1352     }
1353 
1354     public void setFormatAssembler(FormatAssembler formatAssembler) {
1355         this.formatAssembler = formatAssembler;
1356     }
1357 
1358     public void setCourseJointAssembler(
1359             CourseJointAssembler courseJointAssembler) {
1360         this.courseJointAssembler = courseJointAssembler;
1361     }
1362 
1363     public void setLoAssembler(LoAssembler loAssembler) {
1364         this.loAssembler = loAssembler;
1365     }
1366 
1367     public void setLoService(LearningObjectiveService loService) {
1368         this.loService = loService;
1369     }
1370 
1371     public void setCluAssemblerUtils(CluAssemblerUtils cluAssemblerUtils) {
1372         this.cluAssemblerUtils = cluAssemblerUtils;
1373     }
1374 
1375     public void setLrcService(LRCService lrcService) {
1376         this.lrcService = lrcService;
1377     }
1378 
1379     public void setAtpService(AtpService atpService) {
1380         this.atpService = atpService;
1381     }
1382 
1383     public void setDefaultCreditIncrement(float defaultCreditIncrement) {
1384         this.defaultCreditIncrement = defaultCreditIncrement;
1385     }
1386 }