View Javadoc
1   /*
2    * Copyright 2008-2009 The Kuali Foundation
3    * 
4    * Licensed under the Educational Community License, Version 2.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/ecl2.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.ole.sys.batch.service.impl;
17  
18  import java.util.ArrayList;
19  import java.util.Collection;
20  import java.util.HashMap;
21  import java.util.HashSet;
22  import java.util.List;
23  import java.util.Map;
24  import java.util.Set;
25  
26  import org.apache.commons.collections.CollectionUtils;
27  import org.apache.commons.lang.StringUtils;
28  import org.apache.log4j.Logger;
29  import org.kuali.ole.sys.FinancialSystemModuleConfiguration;
30  import org.kuali.ole.sys.OLEConstants;
31  import org.kuali.ole.sys.batch.FiscalYearMakerStep;
32  import org.kuali.ole.sys.batch.dataaccess.FiscalYearMaker;
33  import org.kuali.ole.sys.batch.dataaccess.FiscalYearMakersDao;
34  import org.kuali.ole.sys.batch.service.FiscalYearMakerService;
35  import org.kuali.ole.sys.businessobject.FiscalYearBasedBusinessObject;
36  import org.kuali.rice.coreservice.framework.parameter.ParameterService;
37  import org.kuali.rice.krad.bo.PersistableBusinessObject;
38  import org.kuali.rice.krad.service.KualiModuleService;
39  import org.kuali.rice.krad.service.ModuleService;
40  import org.springframework.transaction.annotation.Transactional;
41  
42  /**
43   * @see org.kuali.ole.coa.batch.service.FiscalYearMakerService
44   */
45  @Transactional
46  public class FiscalYearMakerServiceImpl implements FiscalYearMakerService {
47      private static final Logger LOG = org.apache.log4j.Logger.getLogger(FiscalYearMakerServiceImpl.class);
48  
49      protected FiscalYearMakersDao fiscalYearMakersDao;
50      protected ParameterService parameterService;
51      protected KualiModuleService kualiModuleService;
52  
53      protected List<FiscalYearMaker> fiscalYearMakers;
54  
55      /**
56       * @see org.kuali.ole.coa.batch.service.FiscalYearMakerService#runProcess()
57       */
58      public void runProcess() {
59          String parmBaseYear = parameterService.getParameterValueAsString(FiscalYearMakerStep.class, OLEConstants.ChartApcParms.FISCAL_YEAR_MAKER_SOURCE_FISCAL_YEAR);
60          if (StringUtils.isBlank(parmBaseYear)) {
61              throw new RuntimeException("Required fiscal year parameter " + OLEConstants.ChartApcParms.FISCAL_YEAR_MAKER_SOURCE_FISCAL_YEAR + " has not been set.");
62          }
63  
64          Integer baseYear = Integer.parseInt(parmBaseYear);
65          boolean replaceMode = parameterService.getParameterValueAsBoolean(FiscalYearMakerStep.class, OLEConstants.ChartApcParms.FISCAL_YEAR_MAKER_REPLACE_MODE);
66  
67          if (fiscalYearMakers == null || fiscalYearMakers.isEmpty()) {
68              this.initialize();
69          }
70  
71          validateFiscalYearMakerConfiguration();
72  
73          // get correct order to do copy
74          List<FiscalYearMaker> copyList = getFiscalYearMakerHelpersInCopyOrder();
75  
76          // if configured to replace existing records first clear out any records in target year
77          if (replaceMode) {
78              List<FiscalYearMaker> deleteList = getFiscalYearMakerHelpersInDeleteOrder(copyList);
79              for (FiscalYearMaker fiscalYearMakerHelper : deleteList) {
80                  if (fiscalYearMakerHelper.isAllowOverrideTargetYear()) {
81                      fiscalYearMakersDao.deleteNewYearRows(baseYear, fiscalYearMakerHelper);
82                  }
83              }
84          }
85  
86          // Map to hold parent primary key values written to use for child RI checks
87          Map<Class<? extends FiscalYearBasedBusinessObject>, Set<String>> parentKeysWritten = new HashMap<Class<? extends FiscalYearBasedBusinessObject>, Set<String>>();
88  
89          // do copy process on each setup business object
90          for (FiscalYearMaker fiscalYearMaker : copyList) {
91              try {
92                  boolean isParent = isParentClass(fiscalYearMaker.getBusinessObjectClass());
93                  if (!fiscalYearMaker.doCustomProcessingOnly()) {
94                      Collection<String> copyErrors = fiscalYearMakersDao.createNewYearRows(baseYear, fiscalYearMaker, replaceMode, parentKeysWritten, isParent);
95                      writeCopyFailureMessages(copyErrors);
96                  }
97      
98                  fiscalYearMaker.performCustomProcessing(baseYear, true);
99      
100                 // if copy two years call copy procedure again to copy records from base year + 1 to base year + 2
101                 if (fiscalYearMaker.isTwoYearCopy()) {
102                     if (!fiscalYearMaker.doCustomProcessingOnly()) {
103                         Collection<String> copyErrors = fiscalYearMakersDao.createNewYearRows(baseYear + 1, fiscalYearMaker, replaceMode, parentKeysWritten, isParent);
104                         writeCopyFailureMessages(copyErrors);
105                     }
106     
107                     fiscalYearMaker.performCustomProcessing(baseYear + 1, false);
108                 }
109             } catch ( Exception ex ) {
110                 throw new RuntimeException( "Internal exception while processing fiscal year for " + fiscalYearMaker.getBusinessObjectClass(), ex );
111             }
112         }
113     }
114 
115     /**
116      * Returns List of <code>FiscalYearMaker</code> objects in the order they should be copied. Ordered by Parent classes first then
117      * children. This is necessary to ensure referential integrity is satisfied when the new record is inserted.
118      * 
119      * @return List<FiscalYearMaker> in copy order
120      */
121     protected List<FiscalYearMaker> getFiscalYearMakerHelpersInCopyOrder() {
122         List<Class<? extends FiscalYearBasedBusinessObject>> classCopyOrder = new ArrayList<Class<? extends FiscalYearBasedBusinessObject>>();
123 
124         // build map of parents and their children
125         Map<Class<? extends FiscalYearBasedBusinessObject>, Set<Class<? extends FiscalYearBasedBusinessObject>>> parentChildren = getParentChildrenMap();
126 
127         // figure out correct order among parents by picking off levels of hierarchy
128         while (!parentChildren.isEmpty()) {
129             Set<Class<? extends FiscalYearBasedBusinessObject>> parents = parentChildren.keySet();
130             Set<Class<? extends FiscalYearBasedBusinessObject>> children = getChildren(parentChildren);
131 
132             Set<Class<? extends FiscalYearBasedBusinessObject>> rootParents = new HashSet<Class<? extends FiscalYearBasedBusinessObject>>(CollectionUtils.subtract(parents, children));
133 
134             // if there are no root parents, then we must have a circular reference
135             if (rootParents.isEmpty()) {
136                 findCircularReferenceAndThrowException(parentChildren);
137             }
138 
139             for (Class<? extends FiscalYearBasedBusinessObject> rootParent : rootParents) {
140                 classCopyOrder.add(rootParent);
141                 parentChildren.remove(rootParent);
142             }
143         }
144 
145         // now add remaining objects (those that are not parents)
146         for (FiscalYearMaker fiscalYearMakerHelper : this.fiscalYearMakers) {
147             if (!classCopyOrder.contains(fiscalYearMakerHelper.getBusinessObjectClass())) {
148                 classCopyOrder.add(fiscalYearMakerHelper.getBusinessObjectClass());
149             }
150         }
151 
152         // finally build list of FiscalYearMaker objects by the correct class order
153         List<FiscalYearMaker> fiscalYearMakerHelpersCopyOrder = new ArrayList<FiscalYearMaker>();
154 
155         Map<Class<? extends FiscalYearBasedBusinessObject>, FiscalYearMaker> copyMap = getFiscalYearMakerMap();
156         for (Class<? extends FiscalYearBasedBusinessObject> copyClass : classCopyOrder) {
157             fiscalYearMakerHelpersCopyOrder.add(copyMap.get(copyClass));
158         }
159 
160         return fiscalYearMakerHelpersCopyOrder;
161     }
162 
163     /**
164      * Returns List of <code>FiscalYearMaker</code> objects in the order they should be deleted. Ordered by Child classes first then
165      * Parents. This is necessary to ensure referential integrity is satisfied when the new record is deleted.
166      * 
167      * @param fiscalYearMakerHelpersCopyOrder list of fiscal year makers in copy order
168      * @return List<FiscalYearMaker> in delete order
169      */
170     protected List<FiscalYearMaker> getFiscalYearMakerHelpersInDeleteOrder(List<FiscalYearMaker> fiscalYearMakerHelpersCopyOrder) {
171         List<FiscalYearMaker> fiscalYearMakerHelpersDeleteOrder = new ArrayList<FiscalYearMaker>();
172         for (int i = fiscalYearMakerHelpersCopyOrder.size() - 1; i >= 0; i--) {
173             fiscalYearMakerHelpersDeleteOrder.add(fiscalYearMakerHelpersCopyOrder.get(i));
174         }
175 
176         return fiscalYearMakerHelpersDeleteOrder;
177     }
178 
179     /**
180      * Finds circular references (class which is a child to itself) and throws exception indicating the invalid parent-child
181      * configuration
182      * 
183      * @param parents Set of parent classes to check
184      * @param parentChildren Map with parent class as the key and its children classes as value
185      */
186     protected void findCircularReferenceAndThrowException(Map<Class<? extends FiscalYearBasedBusinessObject>, Set<Class<? extends FiscalYearBasedBusinessObject>>> parentChildren) {
187         Set<Class<? extends FiscalYearBasedBusinessObject>> classesWithCircularReference = new HashSet<Class<? extends FiscalYearBasedBusinessObject>>();
188 
189         // resolve children for each parent and verify the parent does not appear as a child to itself
190         for (Class<? extends FiscalYearBasedBusinessObject> parent : parentChildren.keySet()) {
191             boolean circularReferenceFound = checkChildrenForParentReference(parent, parentChildren.get(parent), parentChildren, new HashSet<Class<? extends FiscalYearBasedBusinessObject>>());
192             if (circularReferenceFound) {
193                 classesWithCircularReference.add(parent);
194             }
195         }
196 
197         if (!classesWithCircularReference.isEmpty()) {
198             String error = "Circular reference found for class(s): " + StringUtils.join(classesWithCircularReference, ", ");
199             LOG.error(error);
200             throw new RuntimeException(error);
201         }
202     }
203 
204     /**
205      * Recursively checks all children of children who are parents for reference to the given parent class
206      * 
207      * @param parent Class of parent to check for
208      * @param children Set of children classes to check
209      * @param parentChildren Map with parent class as the key and its children classes as value
210      * @param checkedParents Set of parent classes we have already checked (to prevent endless recursiveness)
211      * @return true if the parent class was found in one of the children list (meaning we have a circular reference), false
212      *         otherwise
213      */
214     protected boolean checkChildrenForParentReference(Class<? extends FiscalYearBasedBusinessObject> parent, Set<Class<? extends FiscalYearBasedBusinessObject>> children, Map<Class<? extends FiscalYearBasedBusinessObject>, Set<Class<? extends FiscalYearBasedBusinessObject>>> parentChildren, Set<Class<? extends FiscalYearBasedBusinessObject>> checkedParents) {
215         // if parent is in child list then we have a circular reference
216         if (children.contains(parent)) {
217             return true;
218         }
219 
220         // iterate through children and check if the child is also a parent, if so then need to check its children
221         for (Class<? extends FiscalYearBasedBusinessObject> child : children) {
222             if (parentChildren.containsKey(child) && !checkedParents.contains(child)) {
223                 checkedParents.add(child);
224                 Set<Class<? extends FiscalYearBasedBusinessObject>> childChildren = parentChildren.get(child);
225 
226                 boolean foundParent = checkChildrenForParentReference(parent, childChildren, parentChildren, checkedParents);
227                 if (foundParent) {
228                     return true;
229                 }
230             }
231         }
232 
233         return false;
234     }
235 
236     /**
237      * Helper method to build a Map with Parent classes as the key and their Set of child classes as the value
238      * 
239      * @return Map<Class, Set<Class>> of parent to children classes
240      */
241     protected Map<Class<? extends FiscalYearBasedBusinessObject>, Set<Class<? extends FiscalYearBasedBusinessObject>>> getParentChildrenMap() {
242         Map<Class<? extends FiscalYearBasedBusinessObject>, Set<Class<? extends FiscalYearBasedBusinessObject>>> parentChildren = new HashMap<Class<? extends FiscalYearBasedBusinessObject>, Set<Class<? extends FiscalYearBasedBusinessObject>>>();
243 
244         for (FiscalYearMaker fiscalYearMakerHelper : fiscalYearMakers) {
245             for (Class<? extends FiscalYearBasedBusinessObject> parentClass : fiscalYearMakerHelper.getParentClasses()) {
246                 Set<Class<? extends FiscalYearBasedBusinessObject>> children = new HashSet<Class<? extends FiscalYearBasedBusinessObject>>();
247                 if (parentChildren.containsKey(parentClass)) {
248                     children = parentChildren.get(parentClass);
249                 }
250                 children.add(fiscalYearMakerHelper.getBusinessObjectClass());
251 
252                 parentChildren.put(parentClass, children);
253             }
254         }
255 
256         return parentChildren;
257     }
258 
259     /**
260      * Checks if the given class is a parent (to at least one other class)
261      * 
262      * @param businessObjectClass class to check
263      * @return true if class is a parent, false otherwise
264      */
265     protected boolean isParentClass(Class<? extends FiscalYearBasedBusinessObject> businessObjectClass) {
266         for (FiscalYearMaker fiscalYearMakerHelper : fiscalYearMakers) {
267             for (Class<? extends FiscalYearBasedBusinessObject> parentClass : fiscalYearMakerHelper.getParentClasses()) {
268                 if (businessObjectClass.isAssignableFrom(parentClass)) {
269                     return true;
270                 }
271             }
272         }
273 
274         return false;
275     }
276 
277 
278     /**
279      * Gets all classes that are child of another class in the given Map
280      * 
281      * @param parentChildren Map with parent class as the key and its children classes as value
282      * @return Set of classes that are a child of another class
283      */
284     protected Set<Class<? extends FiscalYearBasedBusinessObject>> getChildren(Map<Class<? extends FiscalYearBasedBusinessObject>, Set<Class<? extends FiscalYearBasedBusinessObject>>> parentChildren) {
285         Set<Class<? extends FiscalYearBasedBusinessObject>> children = new HashSet<Class<? extends FiscalYearBasedBusinessObject>>();
286 
287         for (Class<? extends FiscalYearBasedBusinessObject> parentClass : parentChildren.keySet()) {
288             children.addAll(parentChildren.get(parentClass));
289         }
290 
291         return children;
292     }
293 
294     /**
295      * Helper method to build a Map with the copy class as the key and its corresponding <code>FiscalYearMaker</code> as the Map
296      * value
297      * 
298      * @return Map<Class, FiscalYearMaker> of copy classes to FiscalYearMaker objects
299      */
300     protected Map<Class<? extends FiscalYearBasedBusinessObject>, FiscalYearMaker> getFiscalYearMakerMap() {
301         Map<Class<? extends FiscalYearBasedBusinessObject>, FiscalYearMaker> fiscalYearMakerMap = new HashMap<Class<? extends FiscalYearBasedBusinessObject>, FiscalYearMaker>();
302 
303         for (FiscalYearMaker fiscalYearMakerHelper : fiscalYearMakers) {
304             fiscalYearMakerMap.put(fiscalYearMakerHelper.getBusinessObjectClass(), fiscalYearMakerHelper);
305         }
306 
307         return fiscalYearMakerMap;
308     }
309 
310     /**
311      * Validates each configured fiscal year maker implementation
312      */
313     protected void validateFiscalYearMakerConfiguration() {
314         Set<Class<? extends FiscalYearBasedBusinessObject>> businessObjectClasses = new HashSet<Class<? extends FiscalYearBasedBusinessObject>>();
315 
316         for (FiscalYearMaker fiscalYearMaker : fiscalYearMakers) {
317             Class<? extends FiscalYearBasedBusinessObject> businessObjectClass = fiscalYearMaker.getBusinessObjectClass();
318             if (businessObjectClass == null) {
319                 String error = "Business object class is null for fiscal year maker";
320                 LOG.error(error);
321                 throw new RuntimeException(error);
322             }
323 
324             if (!FiscalYearBasedBusinessObject.class.isAssignableFrom(businessObjectClass)) {
325                 String error = String.format("Business object class %s does not implement %s", businessObjectClass.getName(), FiscalYearBasedBusinessObject.class.getName());
326                 LOG.error(error);
327                 throw new RuntimeException(error);
328             }
329 
330             if (businessObjectClasses.contains(businessObjectClass)) {
331                 String error = String.format("Business object class %s has two fiscal year maker implementations defined", businessObjectClass.getName());
332                 LOG.error(error);
333                 throw new RuntimeException(error);
334             }
335 
336             businessObjectClasses.add(businessObjectClass);
337         }
338 
339         // validate parents are in copy list
340         Set<Class<? extends PersistableBusinessObject>> parentsNotInCopyList = new HashSet<Class<? extends PersistableBusinessObject>>();
341         for (FiscalYearMaker fiscalYearMaker : fiscalYearMakers) {
342             parentsNotInCopyList.addAll(CollectionUtils.subtract(fiscalYearMaker.getParentClasses(), businessObjectClasses));
343         }
344 
345         if (!parentsNotInCopyList.isEmpty()) {
346             String error = "Parent classes not in copy list: " + StringUtils.join(parentsNotInCopyList, ",");
347             LOG.error(error);
348             throw new RuntimeException(error);
349         }
350     }
351 
352     /**
353      * Write outs errors encountered while creating new records for an object to LOG.
354      * 
355      * @param copyErrors Collection of error messages to write
356      */
357     protected void writeCopyFailureMessages(Collection<String> copyErrors) {
358         if (!copyErrors.isEmpty()) {
359             LOG.warn("\n");
360             for (String copyError : copyErrors) {
361                 LOG.warn(String.format("\n%s", copyError));
362             }
363             LOG.warn("\n");
364         }
365     }
366 
367     /**
368      * Populates the fiscal year maker list from the installed modules
369      */
370     public void initialize() {
371         fiscalYearMakers = new ArrayList<FiscalYearMaker>();
372         for (ModuleService moduleService : kualiModuleService.getInstalledModuleServices()) {
373             if (moduleService.getModuleConfiguration() instanceof FinancialSystemModuleConfiguration) {
374                 fiscalYearMakers.addAll(((FinancialSystemModuleConfiguration) moduleService.getModuleConfiguration()).getFiscalYearMakers());
375             }
376         }
377     }
378 
379     /**
380      * Sets the fiscalYearMakers attribute value.
381      * 
382      * @param fiscalYearMakers The fiscalYearMakers to set.
383      */
384     protected void setFiscalYearMakers(List<FiscalYearMaker> fiscalYearMakers) {
385         this.fiscalYearMakers = fiscalYearMakers;
386     }
387 
388     /**
389      * Sets the parameterService attribute value.
390      * 
391      * @param parameterService The parameterService to set.
392      */
393     public void setParameterService(ParameterService parameterService) {
394         this.parameterService = parameterService;
395     }
396 
397     /**
398      * Sets the fiscalYearMakersDao attribute value.
399      * 
400      * @param fiscalYearMakersDao The fiscalYearMakersDao to set.
401      */
402     public void setFiscalYearMakersDao(FiscalYearMakersDao fiscalYearMakersDao) {
403         this.fiscalYearMakersDao = fiscalYearMakersDao;
404     }
405 
406     /**
407      * Sets the kualiModuleService attribute value.
408      * 
409      * @param kualiModuleService The kualiModuleService to set.
410      */
411     public void setKualiModuleService(KualiModuleService kualiModuleService) {
412         this.kualiModuleService = kualiModuleService;
413     }
414 
415     /**
416      * Gets the fiscalYearMakersDao attribute.
417      * 
418      * @return Returns the fiscalYearMakersDao.
419      */
420     protected FiscalYearMakersDao getFiscalYearMakersDao() {
421         return fiscalYearMakersDao;
422     }
423 
424     /**
425      * Gets the parameterService attribute.
426      * 
427      * @return Returns the parameterService.
428      */
429     protected ParameterService getParameterService() {
430         return parameterService;
431     }
432 
433     /**
434      * Gets the kualiModuleService attribute.
435      * 
436      * @return Returns the kualiModuleService.
437      */
438     protected KualiModuleService getKualiModuleService() {
439         return kualiModuleService;
440     }
441 
442     /**
443      * Gets the fiscalYearMakers attribute.
444      * 
445      * @return Returns the fiscalYearMakers.
446      */
447     protected List<FiscalYearMaker> getFiscalYearMakers() {
448         return fiscalYearMakers;
449     }
450 
451 }