001    /*
002     * Copyright 2011 The Kuali Foundation.
003     * 
004     * Licensed under the Educational Community License, Version 2.0 (the "License");
005     * you may not use this file except in compliance with the License.
006     * You may obtain a copy of the License at
007     * 
008     * http://www.opensource.org/licenses/ecl2.php
009     * 
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS,
012     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013     * See the License for the specific language governing permissions and
014     * limitations under the License.
015     */
016    package org.kuali.kfs.sys.batch.service.impl;
017    
018    import java.util.ArrayList;
019    import java.util.Collection;
020    import java.util.HashMap;
021    import java.util.HashSet;
022    import java.util.List;
023    import java.util.Map;
024    import java.util.Set;
025    
026    import org.apache.commons.collections.CollectionUtils;
027    import org.apache.commons.lang.StringUtils;
028    import org.apache.log4j.Logger;
029    import org.kuali.kfs.sys.FinancialSystemModuleConfiguration;
030    import org.kuali.kfs.sys.KFSConstants;
031    import org.kuali.kfs.sys.batch.FiscalYearMakerStep;
032    import org.kuali.kfs.sys.batch.dataaccess.FiscalYearMaker;
033    import org.kuali.kfs.sys.batch.dataaccess.FiscalYearMakersDao;
034    import org.kuali.kfs.sys.batch.service.FiscalYearMakerService;
035    import org.kuali.rice.kns.bo.PersistableBusinessObject;
036    import org.kuali.rice.kns.service.KualiModuleService;
037    import org.kuali.rice.kns.service.ModuleService;
038    import org.kuali.rice.kns.service.ParameterService;
039    import org.springframework.transaction.annotation.Transactional;
040    
041    /**
042     * @see org.kuali.kfs.coa.batch.service.FiscalYearMakerService
043     */
044    @Transactional
045    public class FiscalYearMakerServiceImpl implements FiscalYearMakerService {
046        private static Logger LOG = org.apache.log4j.Logger.getLogger(FiscalYearMakerServiceImpl.class);
047    
048        private FiscalYearMakersDao fiscalYearMakersDao;
049        private ParameterService parameterService;
050        private KualiModuleService kualiModuleService;
051    
052        private List<FiscalYearMaker> fiscalYearMakers;
053    
054        /**
055         * @see org.kuali.kfs.coa.batch.service.FiscalYearMakerService#runProcess()
056         */
057        public void runProcess() {
058            String parmBaseYear = parameterService.getParameterValue(FiscalYearMakerStep.class, KFSConstants.ChartApcParms.FISCAL_YEAR_MAKER_SOURCE_FISCAL_YEAR);
059            if (StringUtils.isBlank(parmBaseYear)) {
060                throw new RuntimeException("Required fiscal year parameter " + KFSConstants.ChartApcParms.FISCAL_YEAR_MAKER_SOURCE_FISCAL_YEAR + " has not been set.");
061            }
062    
063            Integer baseYear = Integer.parseInt(parmBaseYear);
064            boolean replaceMode = parameterService.getIndicatorParameter(FiscalYearMakerStep.class, KFSConstants.ChartApcParms.FISCAL_YEAR_MAKER_REPLACE_MODE);
065    
066            if (fiscalYearMakers == null || fiscalYearMakers.isEmpty()) {
067                this.initialize();
068            }
069    
070            validateFiscalYearMakerConfiguration();
071    
072            // get correct order to do copy
073            List<FiscalYearMaker> copyList = getFiscalYearMakerHelpersInCopyOrder();
074    
075            // if configured to replace existing records first clear out any records in target year
076            if (replaceMode) {
077                List<FiscalYearMaker> deleteList = getFiscalYearMakerHelpersInDeleteOrder(copyList);
078                for (FiscalYearMaker fiscalYearMakerHelper : deleteList) {
079                    if (fiscalYearMakerHelper.isAllowOverrideTargetYear()) {
080                        fiscalYearMakersDao.deleteNewYearRows(baseYear, fiscalYearMakerHelper);
081                    }
082                }
083            }
084    
085            // Map to hold parent primary key values written to use for child RI checks
086            Map<Class<? extends PersistableBusinessObject>, Set<String>> parentKeysWritten = new HashMap<Class<? extends PersistableBusinessObject>, Set<String>>();
087    
088            // do copy process on each setup business object
089            for (FiscalYearMaker fiscalYearMaker : copyList) {
090                boolean isParent = isParentClass(fiscalYearMaker.getBusinessObjectClass());
091                if (!fiscalYearMaker.doCustomProcessingOnly()) {
092                    Collection<String> copyErrors = fiscalYearMakersDao.createNewYearRows(baseYear, fiscalYearMaker, replaceMode, parentKeysWritten, isParent);
093                    writeCopyFailureMessages(copyErrors);
094                }
095    
096                fiscalYearMaker.performCustomProcessing(baseYear, true);
097    
098                // if copy two years call copy procedure again to copy records from base year + 1 to base year + 2
099                if (fiscalYearMaker.isTwoYearCopy()) {
100                    if (!fiscalYearMaker.doCustomProcessingOnly()) {
101                        Collection<String> copyErrors = fiscalYearMakersDao.createNewYearRows(baseYear + 1, fiscalYearMaker, replaceMode, parentKeysWritten, isParent);
102                        writeCopyFailureMessages(copyErrors);
103                    }
104    
105                    fiscalYearMaker.performCustomProcessing(baseYear + 1, false);
106                }
107            }
108        }
109    
110        /**
111         * Returns List of <code>FiscalYearMaker</code> objects in the order they should be copied. Ordered by Parent classes first then
112         * children. This is necessary to ensure referential integrity is satisfied when the new record is inserted.
113         * 
114         * @return List<FiscalYearMaker> in copy order
115         */
116        protected List<FiscalYearMaker> getFiscalYearMakerHelpersInCopyOrder() {
117            List<Class<? extends PersistableBusinessObject>> classCopyOrder = new ArrayList<Class<? extends PersistableBusinessObject>>();
118    
119            // build map of parents and their children
120            Map<Class<? extends PersistableBusinessObject>, Set<Class<? extends PersistableBusinessObject>>> parentChildren = getParentChildrenMap();
121    
122            // figure out correct order among parents by picking off levels of hierarchy
123            while (!parentChildren.isEmpty()) {
124                Set<Class<? extends PersistableBusinessObject>> parents = parentChildren.keySet();
125                Set<Class<? extends PersistableBusinessObject>> children = getChildren(parentChildren);
126    
127                Set<Class<? extends PersistableBusinessObject>> rootParents = new HashSet<Class<? extends PersistableBusinessObject>>(CollectionUtils.subtract(parents, children));
128    
129                // if there are no root parents, then we must have a circular reference
130                if (rootParents.isEmpty()) {
131                    findCircularReferenceAndThrowException(parentChildren);
132                }
133    
134                for (Class<? extends PersistableBusinessObject> rootParent : rootParents) {
135                    classCopyOrder.add(rootParent);
136                    parentChildren.remove(rootParent);
137                }
138            }
139    
140            // now add remaining objects (those that are not parents)
141            for (FiscalYearMaker fiscalYearMakerHelper : this.fiscalYearMakers) {
142                if (!classCopyOrder.contains(fiscalYearMakerHelper.getBusinessObjectClass())) {
143                    classCopyOrder.add(fiscalYearMakerHelper.getBusinessObjectClass());
144                }
145            }
146    
147            // finally build list of FiscalYearMaker objects by the correct class order
148            List<FiscalYearMaker> fiscalYearMakerHelpersCopyOrder = new ArrayList<FiscalYearMaker>();
149    
150            Map<Class<? extends PersistableBusinessObject>, FiscalYearMaker> copyMap = getFiscalYearMakerMap();
151            for (Class<? extends PersistableBusinessObject> copyClass : classCopyOrder) {
152                fiscalYearMakerHelpersCopyOrder.add(copyMap.get(copyClass));
153            }
154    
155            return fiscalYearMakerHelpersCopyOrder;
156        }
157    
158        /**
159         * Returns List of <code>FiscalYearMaker</code> objects in the order they should be deleted. Ordered by Child classes first then
160         * Parents. This is necessary to ensure referential integrity is satisfied when the new record is deleted.
161         * 
162         * @param fiscalYearMakerHelpersCopyOrder list of fiscal year makers in copy order
163         * @return List<FiscalYearMaker> in delete order
164         */
165        protected List<FiscalYearMaker> getFiscalYearMakerHelpersInDeleteOrder(List<FiscalYearMaker> fiscalYearMakerHelpersCopyOrder) {
166            List<FiscalYearMaker> fiscalYearMakerHelpersDeleteOrder = new ArrayList<FiscalYearMaker>();
167            for (int i = fiscalYearMakerHelpersCopyOrder.size() - 1; i >= 0; i--) {
168                fiscalYearMakerHelpersDeleteOrder.add(fiscalYearMakerHelpersCopyOrder.get(i));
169            }
170    
171            return fiscalYearMakerHelpersDeleteOrder;
172        }
173    
174        /**
175         * Finds circular references (class which is a child to itself) and throws exception indicating the invalid parent-child
176         * configuration
177         * 
178         * @param parents Set of parent classes to check
179         * @param parentChildren Map with parent class as the key and its children classes as value
180         */
181        protected void findCircularReferenceAndThrowException(Map<Class<? extends PersistableBusinessObject>, Set<Class<? extends PersistableBusinessObject>>> parentChildren) {
182            Set<Class<? extends PersistableBusinessObject>> classesWithCircularReference = new HashSet<Class<? extends PersistableBusinessObject>>();
183    
184            // resolve children for each parent and verify the parent does not appear as a child to itself
185            for (Class<? extends PersistableBusinessObject> parent : parentChildren.keySet()) {
186                boolean circularReferenceFound = checkChildrenForParentReference(parent, parentChildren.get(parent), parentChildren, new HashSet<Class<? extends PersistableBusinessObject>>());
187                if (circularReferenceFound) {
188                    classesWithCircularReference.add(parent);
189                }
190            }
191    
192            if (!classesWithCircularReference.isEmpty()) {
193                String error = "Circular reference found for class(s): " + StringUtils.join(classesWithCircularReference, ", ");
194                LOG.error(error);
195                throw new RuntimeException(error);
196            }
197        }
198    
199        /**
200         * Recursively checks all children of children who are parents for reference to the given parent class
201         * 
202         * @param parent Class of parent to check for
203         * @param children Set of children classes to check
204         * @param parentChildren Map with parent class as the key and its children classes as value
205         * @param checkedParents Set of parent classes we have already checked (to prevent endless recursiveness)
206         * @return true if the parent class was found in one of the children list (meaning we have a circular reference), false
207         *         otherwise
208         */
209        protected boolean checkChildrenForParentReference(Class<? extends PersistableBusinessObject> parent, Set<Class<? extends PersistableBusinessObject>> children, Map<Class<? extends PersistableBusinessObject>, Set<Class<? extends PersistableBusinessObject>>> parentChildren, Set<Class<? extends PersistableBusinessObject>> checkedParents) {
210            // if parent is in child list then we have a circular reference
211            if (children.contains(parent)) {
212                return true;
213            }
214    
215            // iterate through children and check if the child is also a parent, if so then need to check its children
216            for (Class<? extends PersistableBusinessObject> child : children) {
217                if (parentChildren.containsKey(child) && !checkedParents.contains(child)) {
218                    checkedParents.add(child);
219                    Set<Class<? extends PersistableBusinessObject>> childChildren = parentChildren.get(child);
220    
221                    boolean foundParent = checkChildrenForParentReference(parent, childChildren, parentChildren, checkedParents);
222                    if (foundParent) {
223                        return true;
224                    }
225                }
226            }
227    
228            return false;
229        }
230    
231        /**
232         * Helper method to build a Map with Parent classes as the key and their Set of child classes as the value
233         * 
234         * @return Map<Class, Set<Class>> of parent to children classes
235         */
236        protected Map<Class<? extends PersistableBusinessObject>, Set<Class<? extends PersistableBusinessObject>>> getParentChildrenMap() {
237            Map<Class<? extends PersistableBusinessObject>, Set<Class<? extends PersistableBusinessObject>>> parentChildren = new HashMap<Class<? extends PersistableBusinessObject>, Set<Class<? extends PersistableBusinessObject>>>();
238    
239            for (FiscalYearMaker fiscalYearMakerHelper : fiscalYearMakers) {
240                for (Class<? extends PersistableBusinessObject> parentClass : fiscalYearMakerHelper.getParentClasses()) {
241                    Set<Class<? extends PersistableBusinessObject>> children = new HashSet<Class<? extends PersistableBusinessObject>>();
242                    if (parentChildren.containsKey(parentClass)) {
243                        children = parentChildren.get(parentClass);
244                    }
245                    children.add(fiscalYearMakerHelper.getBusinessObjectClass());
246    
247                    parentChildren.put(parentClass, children);
248                }
249            }
250    
251            return parentChildren;
252        }
253    
254        /**
255         * Checks if the given class is a parent (to at least one other class)
256         * 
257         * @param businessObjectClass class to check
258         * @return true if class is a parent, false otherwise
259         */
260        protected boolean isParentClass(Class<? extends PersistableBusinessObject> businessObjectClass) {
261            for (FiscalYearMaker fiscalYearMakerHelper : fiscalYearMakers) {
262                for (Class<? extends PersistableBusinessObject> parentClass : fiscalYearMakerHelper.getParentClasses()) {
263                    if (businessObjectClass.isAssignableFrom(parentClass)) {
264                        return true;
265                    }
266                }
267            }
268    
269            return false;
270        }
271    
272    
273        /**
274         * Gets all classes that are child of another class in the given Map
275         * 
276         * @param parentChildren Map with parent class as the key and its children classes as value
277         * @return Set of classes that are a child of another class
278         */
279        protected Set<Class<? extends PersistableBusinessObject>> getChildren(Map<Class<? extends PersistableBusinessObject>, Set<Class<? extends PersistableBusinessObject>>> parentChildren) {
280            Set<Class<? extends PersistableBusinessObject>> children = new HashSet<Class<? extends PersistableBusinessObject>>();
281    
282            for (Class<? extends PersistableBusinessObject> parentClass : parentChildren.keySet()) {
283                children.addAll(parentChildren.get(parentClass));
284            }
285    
286            return children;
287        }
288    
289        /**
290         * Helper method to build a Map with the copy class as the key and its corresponding <code>FiscalYearMaker</code> as the Map
291         * value
292         * 
293         * @return Map<Class, FiscalYearMaker> of copy classes to FiscalYearMaker objects
294         */
295        protected Map<Class<? extends PersistableBusinessObject>, FiscalYearMaker> getFiscalYearMakerMap() {
296            Map<Class<? extends PersistableBusinessObject>, FiscalYearMaker> fiscalYearMakerMap = new HashMap<Class<? extends PersistableBusinessObject>, FiscalYearMaker>();
297    
298            for (FiscalYearMaker fiscalYearMakerHelper : fiscalYearMakers) {
299                fiscalYearMakerMap.put(fiscalYearMakerHelper.getBusinessObjectClass(), fiscalYearMakerHelper);
300            }
301    
302            return fiscalYearMakerMap;
303        }
304    
305        /**
306         * Validates each configured fiscal year maker implementation
307         */
308        protected void validateFiscalYearMakerConfiguration() {
309            Set<Class<? extends PersistableBusinessObject>> businessObjectClasses = new HashSet<Class<? extends PersistableBusinessObject>>();
310    
311            for (FiscalYearMaker fiscalYearMaker : fiscalYearMakers) {
312                Class<? extends PersistableBusinessObject> businessObjectClass = fiscalYearMaker.getBusinessObjectClass();
313                if (businessObjectClass == null) {
314                    String error = "Business object class is null for fiscal year maker";
315                    LOG.error(error);
316                    throw new RuntimeException(error);
317                }
318    
319                if (!PersistableBusinessObject.class.isAssignableFrom(businessObjectClass)) {
320                    String error = String.format("Business object class %s does not implement %s", businessObjectClass.getName(), PersistableBusinessObject.class.getName());
321                    LOG.error(error);
322                    throw new RuntimeException(error);
323                }
324    
325                if (businessObjectClasses.contains(businessObjectClass)) {
326                    String error = String.format("Business object class %s has two fiscal year maker implementations defined", businessObjectClass.getName());
327                    LOG.error(error);
328                    throw new RuntimeException(error);
329                }
330    
331                businessObjectClasses.add(businessObjectClass);
332            }
333    
334            // validate parents are in copy list
335            Set<Class<? extends PersistableBusinessObject>> parentsNotInCopyList = new HashSet<Class<? extends PersistableBusinessObject>>();
336            for (FiscalYearMaker fiscalYearMaker : fiscalYearMakers) {
337                parentsNotInCopyList.addAll(CollectionUtils.subtract(fiscalYearMaker.getParentClasses(), businessObjectClasses));
338            }
339    
340            if (!parentsNotInCopyList.isEmpty()) {
341                String error = "Parent classes not in copy list: " + StringUtils.join(parentsNotInCopyList, ",");
342                LOG.error(error);
343                throw new RuntimeException(error);
344            }
345        }
346    
347        /**
348         * Write outs errors encountered while creating new records for an object to LOG.
349         * 
350         * @param copyErrors Collection of error messages to write
351         */
352        protected void writeCopyFailureMessages(Collection<String> copyErrors) {
353            if (!copyErrors.isEmpty()) {
354                LOG.warn("\n");
355                for (String copyError : copyErrors) {
356                    LOG.warn(String.format("\n%s", copyError));
357                }
358                LOG.warn("\n");
359            }
360        }
361    
362        /**
363         * Populates the fiscal year maker list from the installed modules
364         */
365        public void initialize() {
366            fiscalYearMakers = new ArrayList<FiscalYearMaker>();
367            for (ModuleService moduleService : kualiModuleService.getInstalledModuleServices()) {
368                if (moduleService.getModuleConfiguration() instanceof FinancialSystemModuleConfiguration) {
369                    fiscalYearMakers.addAll(((FinancialSystemModuleConfiguration) moduleService.getModuleConfiguration()).getFiscalYearMakers());
370                }
371            }
372        }
373    
374        /**
375         * Sets the fiscalYearMakers attribute value.
376         * 
377         * @param fiscalYearMakers The fiscalYearMakers to set.
378         */
379        protected void setFiscalYearMakers(List<FiscalYearMaker> fiscalYearMakers) {
380            this.fiscalYearMakers = fiscalYearMakers;
381        }
382    
383        /**
384         * Sets the parameterService attribute value.
385         * 
386         * @param parameterService The parameterService to set.
387         */
388        public void setParameterService(ParameterService parameterService) {
389            this.parameterService = parameterService;
390        }
391    
392        /**
393         * Sets the fiscalYearMakersDao attribute value.
394         * 
395         * @param fiscalYearMakersDao The fiscalYearMakersDao to set.
396         */
397        public void setFiscalYearMakersDao(FiscalYearMakersDao fiscalYearMakersDao) {
398            this.fiscalYearMakersDao = fiscalYearMakersDao;
399        }
400    
401        /**
402         * Sets the kualiModuleService attribute value.
403         * 
404         * @param kualiModuleService The kualiModuleService to set.
405         */
406        public void setKualiModuleService(KualiModuleService kualiModuleService) {
407            this.kualiModuleService = kualiModuleService;
408        }
409    
410        /**
411         * Gets the fiscalYearMakersDao attribute.
412         * 
413         * @return Returns the fiscalYearMakersDao.
414         */
415        protected FiscalYearMakersDao getFiscalYearMakersDao() {
416            return fiscalYearMakersDao;
417        }
418    
419        /**
420         * Gets the parameterService attribute.
421         * 
422         * @return Returns the parameterService.
423         */
424        protected ParameterService getParameterService() {
425            return parameterService;
426        }
427    
428        /**
429         * Gets the kualiModuleService attribute.
430         * 
431         * @return Returns the kualiModuleService.
432         */
433        protected KualiModuleService getKualiModuleService() {
434            return kualiModuleService;
435        }
436    
437        /**
438         * Gets the fiscalYearMakers attribute.
439         * 
440         * @return Returns the fiscalYearMakers.
441         */
442        protected List<FiscalYearMaker> getFiscalYearMakers() {
443            return fiscalYearMakers;
444        }
445    
446    }