/*
* 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;
}
}