/* * The Kuali Financial System, a comprehensive financial management system for higher education. * * Copyright 2005-2014 The Kuali Foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.kuali.kfs.sys.batch.service.impl; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang.StringUtils; import org.apache.log4j.Logger; import org.kuali.kfs.sys.FinancialSystemModuleConfiguration; import org.kuali.kfs.sys.KFSConstants; import org.kuali.kfs.sys.batch.FiscalYearMakerStep; import org.kuali.kfs.sys.batch.dataaccess.FiscalYearMaker; import org.kuali.kfs.sys.batch.dataaccess.FiscalYearMakersDao; import org.kuali.kfs.sys.batch.service.FiscalYearMakerService; import org.kuali.kfs.sys.businessobject.FiscalYearBasedBusinessObject; import org.kuali.rice.coreservice.framework.parameter.ParameterService; import org.kuali.rice.krad.bo.PersistableBusinessObject; import org.kuali.rice.krad.service.KualiModuleService; import org.kuali.rice.krad.service.ModuleService; import org.springframework.transaction.annotation.Transactional; /** * @see org.kuali.kfs.coa.batch.service.FiscalYearMakerService */ @Transactional public class FiscalYearMakerServiceImpl implements FiscalYearMakerService { private static final Logger LOG = org.apache.log4j.Logger.getLogger(FiscalYearMakerServiceImpl.class); protected FiscalYearMakersDao fiscalYearMakersDao; protected ParameterService parameterService; protected KualiModuleService kualiModuleService; protected List<FiscalYearMaker> fiscalYearMakers; /** * @see org.kuali.kfs.coa.batch.service.FiscalYearMakerService#runProcess() */ public void runProcess() { String parmBaseYear = parameterService.getParameterValueAsString(FiscalYearMakerStep.class, KFSConstants.ChartApcParms.FISCAL_YEAR_MAKER_SOURCE_FISCAL_YEAR); if (StringUtils.isBlank(parmBaseYear)) { throw new RuntimeException("Required fiscal year parameter " + KFSConstants.ChartApcParms.FISCAL_YEAR_MAKER_SOURCE_FISCAL_YEAR + " has not been set."); } Integer baseYear = Integer.parseInt(parmBaseYear); boolean replaceMode = parameterService.getParameterValueAsBoolean(FiscalYearMakerStep.class, KFSConstants.ChartApcParms.FISCAL_YEAR_MAKER_REPLACE_MODE); if (fiscalYearMakers == null || fiscalYearMakers.isEmpty()) { this.initialize(); } validateFiscalYearMakerConfiguration(); // get correct order to do copy List<FiscalYearMaker> copyList = getFiscalYearMakerHelpersInCopyOrder(); // if configured to replace existing records first clear out any records in target year if (replaceMode) { List<FiscalYearMaker> deleteList = getFiscalYearMakerHelpersInDeleteOrder(copyList); for (FiscalYearMaker fiscalYearMakerHelper : deleteList) { if (fiscalYearMakerHelper.isAllowOverrideTargetYear()) { fiscalYearMakersDao.deleteNewYearRows(baseYear, fiscalYearMakerHelper); } } } // Map to hold parent primary key values written to use for child RI checks Map<Class<? extends FiscalYearBasedBusinessObject>, Set<String>> parentKeysWritten = new HashMap<Class<? extends FiscalYearBasedBusinessObject>, Set<String>>(); // do copy process on each setup business object for (FiscalYearMaker fiscalYearMaker : copyList) { try { boolean isParent = isParentClass(fiscalYearMaker.getBusinessObjectClass()); if (!fiscalYearMaker.doCustomProcessingOnly()) { Collection<String> copyErrors = fiscalYearMakersDao.createNewYearRows(baseYear, fiscalYearMaker, replaceMode, parentKeysWritten, isParent); writeCopyFailureMessages(copyErrors); } fiscalYearMaker.performCustomProcessing(baseYear, true); // if copy two years call copy procedure again to copy records from base year + 1 to base year + 2 if (fiscalYearMaker.isTwoYearCopy()) { if (!fiscalYearMaker.doCustomProcessingOnly()) { Collection<String> copyErrors = fiscalYearMakersDao.createNewYearRows(baseYear + 1, fiscalYearMaker, replaceMode, parentKeysWritten, isParent); writeCopyFailureMessages(copyErrors); } fiscalYearMaker.performCustomProcessing(baseYear + 1, false); } } catch ( Exception ex ) { throw new RuntimeException( "Internal exception while processing fiscal year for " + fiscalYearMaker.getBusinessObjectClass(), ex ); } } } /** * Returns List of <code>FiscalYearMaker</code> objects in the order they should be copied. Ordered by Parent classes first then * children. This is necessary to ensure referential integrity is satisfied when the new record is inserted. * * @return List<FiscalYearMaker> in copy order */ protected List<FiscalYearMaker> getFiscalYearMakerHelpersInCopyOrder() { List<Class<? extends FiscalYearBasedBusinessObject>> classCopyOrder = new ArrayList<Class<? extends FiscalYearBasedBusinessObject>>(); // build map of parents and their children Map<Class<? extends FiscalYearBasedBusinessObject>, Set<Class<? extends FiscalYearBasedBusinessObject>>> parentChildren = getParentChildrenMap(); // figure out correct order among parents by picking off levels of hierarchy while (!parentChildren.isEmpty()) { Set<Class<? extends FiscalYearBasedBusinessObject>> parents = parentChildren.keySet(); Set<Class<? extends FiscalYearBasedBusinessObject>> children = getChildren(parentChildren); Set<Class<? extends FiscalYearBasedBusinessObject>> rootParents = new HashSet<Class<? extends FiscalYearBasedBusinessObject>>(CollectionUtils.subtract(parents, children)); // if there are no root parents, then we must have a circular reference if (rootParents.isEmpty()) { findCircularReferenceAndThrowException(parentChildren); } for (Class<? extends FiscalYearBasedBusinessObject> rootParent : rootParents) { classCopyOrder.add(rootParent); parentChildren.remove(rootParent); } } // now add remaining objects (those that are not parents) for (FiscalYearMaker fiscalYearMakerHelper : this.fiscalYearMakers) { if (!classCopyOrder.contains(fiscalYearMakerHelper.getBusinessObjectClass())) { classCopyOrder.add(fiscalYearMakerHelper.getBusinessObjectClass()); } } // finally build list of FiscalYearMaker objects by the correct class order List<FiscalYearMaker> fiscalYearMakerHelpersCopyOrder = new ArrayList<FiscalYearMaker>(); Map<Class<? extends FiscalYearBasedBusinessObject>, FiscalYearMaker> copyMap = getFiscalYearMakerMap(); for (Class<? extends FiscalYearBasedBusinessObject> copyClass : classCopyOrder) { fiscalYearMakerHelpersCopyOrder.add(copyMap.get(copyClass)); } return fiscalYearMakerHelpersCopyOrder; } /** * Returns List of <code>FiscalYearMaker</code> objects in the order they should be deleted. Ordered by Child classes first then * Parents. This is necessary to ensure referential integrity is satisfied when the new record is deleted. * * @param fiscalYearMakerHelpersCopyOrder list of fiscal year makers in copy order * @return List<FiscalYearMaker> in delete order */ protected List<FiscalYearMaker> getFiscalYearMakerHelpersInDeleteOrder(List<FiscalYearMaker> fiscalYearMakerHelpersCopyOrder) { List<FiscalYearMaker> fiscalYearMakerHelpersDeleteOrder = new ArrayList<FiscalYearMaker>(); for (int i = fiscalYearMakerHelpersCopyOrder.size() - 1; i >= 0; i--) { fiscalYearMakerHelpersDeleteOrder.add(fiscalYearMakerHelpersCopyOrder.get(i)); } return fiscalYearMakerHelpersDeleteOrder; } /** * Finds circular references (class which is a child to itself) and throws exception indicating the invalid parent-child * configuration * * @param parents Set of parent classes to check * @param parentChildren Map with parent class as the key and its children classes as value */ protected void findCircularReferenceAndThrowException(Map<Class<? extends FiscalYearBasedBusinessObject>, Set<Class<? extends FiscalYearBasedBusinessObject>>> parentChildren) { Set<Class<? extends FiscalYearBasedBusinessObject>> classesWithCircularReference = new HashSet<Class<? extends FiscalYearBasedBusinessObject>>(); // resolve children for each parent and verify the parent does not appear as a child to itself for (Class<? extends FiscalYearBasedBusinessObject> parent : parentChildren.keySet()) { boolean circularReferenceFound = checkChildrenForParentReference(parent, parentChildren.get(parent), parentChildren, new HashSet<Class<? extends FiscalYearBasedBusinessObject>>()); if (circularReferenceFound) { classesWithCircularReference.add(parent); } } if (!classesWithCircularReference.isEmpty()) { String error = "Circular reference found for class(s): " + StringUtils.join(classesWithCircularReference, ", "); LOG.error(error); throw new RuntimeException(error); } } /** * Recursively checks all children of children who are parents for reference to the given parent class * * @param parent Class of parent to check for * @param children Set of children classes to check * @param parentChildren Map with parent class as the key and its children classes as value * @param checkedParents Set of parent classes we have already checked (to prevent endless recursiveness) * @return true if the parent class was found in one of the children list (meaning we have a circular reference), false * otherwise */ 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) { // if parent is in child list then we have a circular reference if (children.contains(parent)) { return true; } // iterate through children and check if the child is also a parent, if so then need to check its children for (Class<? extends FiscalYearBasedBusinessObject> child : children) { if (parentChildren.containsKey(child) && !checkedParents.contains(child)) { checkedParents.add(child); Set<Class<? extends FiscalYearBasedBusinessObject>> childChildren = parentChildren.get(child); boolean foundParent = checkChildrenForParentReference(parent, childChildren, parentChildren, checkedParents); if (foundParent) { return true; } } } return false; } /** * Helper method to build a Map with Parent classes as the key and their Set of child classes as the value * * @return Map<Class, Set<Class>> of parent to children classes */ protected Map<Class<? extends FiscalYearBasedBusinessObject>, Set<Class<? extends FiscalYearBasedBusinessObject>>> getParentChildrenMap() { Map<Class<? extends FiscalYearBasedBusinessObject>, Set<Class<? extends FiscalYearBasedBusinessObject>>> parentChildren = new HashMap<Class<? extends FiscalYearBasedBusinessObject>, Set<Class<? extends FiscalYearBasedBusinessObject>>>(); for (FiscalYearMaker fiscalYearMakerHelper : fiscalYearMakers) { for (Class<? extends FiscalYearBasedBusinessObject> parentClass : fiscalYearMakerHelper.getParentClasses()) { Set<Class<? extends FiscalYearBasedBusinessObject>> children = new HashSet<Class<? extends FiscalYearBasedBusinessObject>>(); if (parentChildren.containsKey(parentClass)) { children = parentChildren.get(parentClass); } children.add(fiscalYearMakerHelper.getBusinessObjectClass()); parentChildren.put(parentClass, children); } } return parentChildren; } /** * Checks if the given class is a parent (to at least one other class) * * @param businessObjectClass class to check * @return true if class is a parent, false otherwise */ protected boolean isParentClass(Class<? extends FiscalYearBasedBusinessObject> businessObjectClass) { for (FiscalYearMaker fiscalYearMakerHelper : fiscalYearMakers) { for (Class<? extends FiscalYearBasedBusinessObject> parentClass : fiscalYearMakerHelper.getParentClasses()) { if (businessObjectClass.isAssignableFrom(parentClass)) { return true; } } } return false; } /** * Gets all classes that are child of another class in the given Map * * @param parentChildren Map with parent class as the key and its children classes as value * @return Set of classes that are a child of another class */ protected Set<Class<? extends FiscalYearBasedBusinessObject>> getChildren(Map<Class<? extends FiscalYearBasedBusinessObject>, Set<Class<? extends FiscalYearBasedBusinessObject>>> parentChildren) { Set<Class<? extends FiscalYearBasedBusinessObject>> children = new HashSet<Class<? extends FiscalYearBasedBusinessObject>>(); for (Class<? extends FiscalYearBasedBusinessObject> parentClass : parentChildren.keySet()) { children.addAll(parentChildren.get(parentClass)); } return children; } /** * Helper method to build a Map with the copy class as the key and its corresponding <code>FiscalYearMaker</code> as the Map * value * * @return Map<Class, FiscalYearMaker> of copy classes to FiscalYearMaker objects */ protected Map<Class<? extends FiscalYearBasedBusinessObject>, FiscalYearMaker> getFiscalYearMakerMap() { Map<Class<? extends FiscalYearBasedBusinessObject>, FiscalYearMaker> fiscalYearMakerMap = new HashMap<Class<? extends FiscalYearBasedBusinessObject>, FiscalYearMaker>(); for (FiscalYearMaker fiscalYearMakerHelper : fiscalYearMakers) { fiscalYearMakerMap.put(fiscalYearMakerHelper.getBusinessObjectClass(), fiscalYearMakerHelper); } return fiscalYearMakerMap; } /** * Validates each configured fiscal year maker implementation */ protected void validateFiscalYearMakerConfiguration() { Set<Class<? extends FiscalYearBasedBusinessObject>> businessObjectClasses = new HashSet<Class<? extends FiscalYearBasedBusinessObject>>(); for (FiscalYearMaker fiscalYearMaker : fiscalYearMakers) { Class<? extends FiscalYearBasedBusinessObject> businessObjectClass = fiscalYearMaker.getBusinessObjectClass(); if (businessObjectClass == null) { String error = "Business object class is null for fiscal year maker"; LOG.error(error); throw new RuntimeException(error); } if (!FiscalYearBasedBusinessObject.class.isAssignableFrom(businessObjectClass)) { String error = String.format("Business object class %s does not implement %s", businessObjectClass.getName(), FiscalYearBasedBusinessObject.class.getName()); LOG.error(error); throw new RuntimeException(error); } if (businessObjectClasses.contains(businessObjectClass)) { String error = String.format("Business object class %s has two fiscal year maker implementations defined", businessObjectClass.getName()); LOG.error(error); throw new RuntimeException(error); } businessObjectClasses.add(businessObjectClass); } // validate parents are in copy list Set<Class<? extends PersistableBusinessObject>> parentsNotInCopyList = new HashSet<Class<? extends PersistableBusinessObject>>(); for (FiscalYearMaker fiscalYearMaker : fiscalYearMakers) { parentsNotInCopyList.addAll(CollectionUtils.subtract(fiscalYearMaker.getParentClasses(), businessObjectClasses)); } if (!parentsNotInCopyList.isEmpty()) { String error = "Parent classes not in copy list: " + StringUtils.join(parentsNotInCopyList, ","); LOG.error(error); throw new RuntimeException(error); } } /** * Write outs errors encountered while creating new records for an object to LOG. * * @param copyErrors Collection of error messages to write */ protected void writeCopyFailureMessages(Collection<String> copyErrors) { if (!copyErrors.isEmpty()) { LOG.warn("\n"); for (String copyError : copyErrors) { LOG.warn(String.format("\n%s", copyError)); } LOG.warn("\n"); } } /** * Populates the fiscal year maker list from the installed modules */ public void initialize() { fiscalYearMakers = new ArrayList<FiscalYearMaker>(); for (ModuleService moduleService : kualiModuleService.getInstalledModuleServices()) { if (moduleService.getModuleConfiguration() instanceof FinancialSystemModuleConfiguration) { fiscalYearMakers.addAll(((FinancialSystemModuleConfiguration) moduleService.getModuleConfiguration()).getFiscalYearMakers()); } } } /** * Sets the fiscalYearMakers attribute value. * * @param fiscalYearMakers The fiscalYearMakers to set. */ protected void setFiscalYearMakers(List<FiscalYearMaker> fiscalYearMakers) { this.fiscalYearMakers = fiscalYearMakers; } /** * Sets the parameterService attribute value. * * @param parameterService The parameterService to set. */ public void setParameterService(ParameterService parameterService) { this.parameterService = parameterService; } /** * Sets the fiscalYearMakersDao attribute value. * * @param fiscalYearMakersDao The fiscalYearMakersDao to set. */ public void setFiscalYearMakersDao(FiscalYearMakersDao fiscalYearMakersDao) { this.fiscalYearMakersDao = fiscalYearMakersDao; } /** * Sets the kualiModuleService attribute value. * * @param kualiModuleService The kualiModuleService to set. */ public void setKualiModuleService(KualiModuleService kualiModuleService) { this.kualiModuleService = kualiModuleService; } /** * Gets the fiscalYearMakersDao attribute. * * @return Returns the fiscalYearMakersDao. */ protected FiscalYearMakersDao getFiscalYearMakersDao() { return fiscalYearMakersDao; } /** * Gets the parameterService attribute. * * @return Returns the parameterService. */ protected ParameterService getParameterService() { return parameterService; } /** * Gets the kualiModuleService attribute. * * @return Returns the kualiModuleService. */ protected KualiModuleService getKualiModuleService() { return kualiModuleService; } /** * Gets the fiscalYearMakers attribute. * * @return Returns the fiscalYearMakers. */ protected List<FiscalYearMaker> getFiscalYearMakers() { return fiscalYearMakers; } }