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 }