/* * 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.dataaccess.impl; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.beanutils.PropertyUtils; import org.apache.commons.lang.StringUtils; import org.apache.log4j.Logger; import org.apache.ojb.broker.query.QueryByCriteria; import org.apache.ojb.broker.query.ReportQueryByCriteria; import org.apache.ojb.broker.util.ObjectModification; import org.kuali.kfs.sys.batch.dataaccess.FiscalYearMaker; import org.kuali.kfs.sys.batch.dataaccess.FiscalYearMakersDao; import org.kuali.kfs.sys.businessobject.FiscalYearBasedBusinessObject; import org.kuali.rice.core.framework.persistence.ojb.dao.PlatformAwareDaoBaseOjb; import org.kuali.rice.krad.bo.PersistableBusinessObject; import org.kuali.rice.krad.util.ObjectUtils; /** * @see org.kuali.kfs.coa.batch.dataaccess.FiscalYearMakersDao */ public class FiscalYearMakersDaoOjb extends PlatformAwareDaoBaseOjb implements FiscalYearMakersDao { private static final Logger LOG = org.apache.log4j.Logger.getLogger(FiscalYearMakersDaoOjb.class); protected static final String KEY_STRING_DELIMITER = "|"; /** * @see org.kuali.kfs.coa.batch.dataaccess.FiscalYearMakersDao#deleteNewYearRows(java.lang.Integer, * org.kuali.kfs.coa.batch.dataaccess.FiscalYearMakerHelper) */ public void deleteNewYearRows(Integer baseYear, FiscalYearMaker objectFiscalYearMaker) { if ( LOG.isInfoEnabled() ) { LOG.info(String.format("\ndeleting %s for target year(s)", objectFiscalYearMaker.getBusinessObjectClass().getName())); } QueryByCriteria queryID = new QueryByCriteria(objectFiscalYearMaker.getBusinessObjectClass(), objectFiscalYearMaker.createDeleteCriteria(baseYear)); getPersistenceBrokerTemplate().deleteByQuery(queryID); getPersistenceBrokerTemplate().clearCache(); } /** * @see org.kuali.kfs.sys.batch.dataaccess.FiscalYearMakersDao#createNewYearRows(java.lang.Integer, * org.kuali.kfs.sys.batch.dataaccess.FiscalYearMaker, boolean, java.util.Map) */ public Collection<String> createNewYearRows(Integer baseYear, FiscalYearMaker fiscalYearMaker, boolean replaceMode, Map<Class<? extends FiscalYearBasedBusinessObject>, Set<String>> parentKeysWritten, boolean isParentClass) throws Exception { if ( LOG.isInfoEnabled() ) { LOG.info(String.format("\n copying %s from %d to %d", fiscalYearMaker.getBusinessObjectClass().getName(), baseYear, baseYear + 1)); } int rowsRead = 0; int rowsWritten = 0; int rowsFailingRI = 0; // list of copy error messages to be written out at end List<String> copyErrors = new ArrayList<String>(); // Set of primary key strings written Set<String> keysWritten = new HashSet<String>(); // retrieve the list of next-year PKs for the given object List<String> primaryKeyFields = fiscalYearMaker.getPrimaryKeyPropertyNames(); Set<String> nextYearPrimaryKeys = new HashSet<String>(2000); LOG.info( "Loading Next Year's PKs for comparison"); ReportQueryByCriteria nextYearKeyQuery = new ReportQueryByCriteria(fiscalYearMaker.getBusinessObjectClass(), primaryKeyFields.toArray(new String[0]), fiscalYearMaker.createNextYearSelectionCriteria(baseYear) ); Iterator<Object[]> nextYearRecords = getPersistenceBrokerTemplate().getReportQueryIteratorByQuery(nextYearKeyQuery); StringBuilder keyString = new StringBuilder(40); int numNextYearRecords = 0; while ( nextYearRecords.hasNext() ) { numNextYearRecords++; keyString.setLength(0); Object[] record = nextYearRecords.next(); for ( Object f : record ) { keyString.append( f ).append( KEY_STRING_DELIMITER ); } nextYearPrimaryKeys.add(keyString.toString()); if ( numNextYearRecords % 10000 == 0 ) { if ( LOG.isInfoEnabled() ) { LOG.info("Processing Record: " + numNextYearRecords); } } } if ( LOG.isInfoEnabled() ) { LOG.info( "Completed load of next year keys. " + numNextYearRecords + " keys loaded."); LOG.info( "Starting processing of existing FY rows" ); } // retrieve base year records to copy QueryByCriteria queryId = new QueryByCriteria(fiscalYearMaker.getBusinessObjectClass(), fiscalYearMaker.createSelectionCriteria(baseYear)); // BIG QUERY - GET ALL RECORDS for the current FY Iterator<FiscalYearBasedBusinessObject> recordsToCopy = getPersistenceBrokerTemplate().getIteratorByQuery(queryId); while ( recordsToCopy.hasNext() ) { FiscalYearBasedBusinessObject objectToCopy = recordsToCopy.next(); rowsRead++; if ( LOG.isInfoEnabled() ) { if ( rowsRead % 1000 == 0 ) { LOG.info( "*** Processing Record: " + rowsRead + " -- Written So Far: " + rowsWritten + " -- Failing RI: " + rowsFailingRI + " -- Keys Written: " + keysWritten.size() ); } } // remove reference/collection fields so they will not cause an issue with the insert removeNonPrimitiveFields(fiscalYearMaker, objectToCopy); // set record fields for new year fiscalYearMaker.changeForNewYear(baseYear, objectToCopy); // determine if new year record already exists and if so do not overwrite if ( nextYearPrimaryKeys.contains(getKeyString(fiscalYearMaker, primaryKeyFields, objectToCopy)) ) { if (isParentClass) { addToKeysWritten(fiscalYearMaker, primaryKeyFields, objectToCopy, keysWritten); } continue; } // check parent records exist so RI will be satisfied if (!validateParentRecordsExist(fiscalYearMaker, objectToCopy, parentKeysWritten, copyErrors)) { rowsFailingRI++; continue; } // store new record getPersistenceBroker(true).store(objectToCopy, ObjectModification.INSERT); rowsWritten++; if (isParentClass) { addToKeysWritten(fiscalYearMaker, primaryKeyFields, objectToCopy, keysWritten); } } if (isParentClass) { parentKeysWritten.put(fiscalYearMaker.getBusinessObjectClass(), keysWritten); } if ( LOG.isInfoEnabled() ) { LOG.info(String.format("\n%s:\n%d read = %d\n%d written = %d\nfailed RI = %d", fiscalYearMaker.getBusinessObjectClass(), baseYear, rowsRead, baseYear + 1, rowsWritten, rowsFailingRI)); } getPersistenceBrokerTemplate().clearCache(); return copyErrors; } /** * Sets all reference and collection fields defined in the persistence layer to null on the given object * * @param businessObject object to set properties for */ protected void removeNonPrimitiveFields( FiscalYearMaker fiscalYearMaker, FiscalYearBasedBusinessObject businessObject) { try { @SuppressWarnings("rawtypes") Map<String, Class> referenceFields = fiscalYearMaker.getReferenceObjectProperties(); for (String fieldName : referenceFields.keySet()) { if (!fieldName.equals("extension")) { PropertyUtils.setSimpleProperty(businessObject, fieldName, null); } } @SuppressWarnings("rawtypes") Map<String, Class> collectionFields = fiscalYearMaker.getCollectionProperties(); for (String fieldName : collectionFields.keySet()) { PropertyUtils.setSimpleProperty(businessObject, fieldName, null); } } catch (Exception e) { throw new RuntimeException("Unable to set non primitive fields to null: " + e.getMessage(), e); } } /** * Checks all parents for the object we are copying has a corresponding record for the child record * * @return true if all parent records exist, false otherwise */ protected boolean validateParentRecordsExist(FiscalYearMaker objectFiscalYearMaker, FiscalYearBasedBusinessObject childRecord, Map<Class<? extends FiscalYearBasedBusinessObject>, Set<String>> parentKeysWritten, List<String> copyErrors) throws Exception { // iterate through all parents, get attribute reference name and attempt to retrieve for (Class<? extends FiscalYearBasedBusinessObject> parentClass : objectFiscalYearMaker.getParentClasses()) { if ( !validateChildParentReferencesExist(objectFiscalYearMaker,childRecord, parentClass, parentKeysWritten.get(parentClass), copyErrors) ) { return false; } } return true; } /** * Validates the parent record(s) exists for the child record by retrieving the OJB reference (if found and foreign keys have * value) * * @param childRecord child record we are inserting * @param parentClass class for parent of child * @param parentKeys Set of parent key Strings that have been written * @param copyErrors Collection for adding error messages * @return true if the parent record(s) exist, false otherwise */ protected boolean validateChildParentReferencesExist(FiscalYearMaker objectFiscalYearMaker,FiscalYearBasedBusinessObject childRecord, Class<? extends FiscalYearBasedBusinessObject> parentClass, Set<String> parentKeys, List<String> copyErrors) throws Exception { boolean allChildParentReferencesExist = true; boolean foundParentReference = false; // get all references for child class @SuppressWarnings("rawtypes") Map<String, Class> referenceObjects = objectFiscalYearMaker.getReferenceObjectProperties(); // iterate through to find references with the parent class for (String referenceName : referenceObjects.keySet()) { Class<? extends PersistableBusinessObject> referenceClass = referenceObjects.get(referenceName); if (parentClass.isAssignableFrom(referenceClass)) { foundParentReference = true; String foreignKeyString = getForeignKeyStringForReference(objectFiscalYearMaker, childRecord, referenceName); if (StringUtils.isNotBlank(foreignKeyString) && !parentKeys.contains(foreignKeyString)) { // attempt to retrieve the parent reference in case it already existed getPersistenceBroker(true).retrieveReference(childRecord, referenceName); PersistableBusinessObject reference = (PersistableBusinessObject) PropertyUtils.getSimpleProperty(childRecord, referenceName); if (ObjectUtils.isNull(reference)) { allChildParentReferencesExist = false; writeMissingParentCopyError(childRecord, parentClass, foreignKeyString, copyErrors); LOG.warn( "Missing Parent Object: " + copyErrors.get(copyErrors.size()-1)); } else { parentKeys.add(foreignKeyString); } } } } if (!foundParentReference) { LOG.warn(String.format("\n!!! NO relationships between child %s and parent %s found in OJB descriptor\n", childRecord.getClass().getName(), parentClass.getName())); } return allChildParentReferencesExist; } /** * Builds a String containing foreign key values for the given reference of the business object * * @param businessObject business object instance with reference * @param referenceName name of reference * @return String of foreign key values or null if any of the foreign key values are null */ protected String getForeignKeyStringForReference( FiscalYearMaker fiscalYearMaker, FiscalYearBasedBusinessObject businessObject, String referenceName) throws Exception { Map<String, String> foreignKeyToPrimaryKeyMap = fiscalYearMaker.getForeignKeyMappings( referenceName ); StringBuilder foreignKeyString = new StringBuilder(80); for (String fkFieldName : foreignKeyToPrimaryKeyMap.keySet()) { Object fkFieldValue = PropertyUtils.getSimpleProperty(businessObject, fkFieldName); if (fkFieldValue != null) { foreignKeyString.append( fkFieldValue.toString() ).append( KEY_STRING_DELIMITER ); } else { foreignKeyString.setLength(0); break; } } return foreignKeyString.toString(); } /** * Builds an error message when a parent record was not found for the child * * @param childRecord child record we are inserting * @param parentClass class for parent of child * @param foreignKeyString string of foreign key values that was not found in parent * @param copyErrors Collection for adding error messages */ protected void writeMissingParentCopyError(FiscalYearBasedBusinessObject childRecord, Class<? extends FiscalYearBasedBusinessObject> parentClass, String foreignKeyString, Collection<String> copyErrors) { StringBuilder errorCopyFailedMessage = new StringBuilder(150); errorCopyFailedMessage.append(childRecord.getClass().getName()); errorCopyFailedMessage.append(" row for " + childRecord.toString()); errorCopyFailedMessage.append(" - " + foreignKeyString); errorCopyFailedMessage.append(" not in "); errorCopyFailedMessage.append(parentClass.getName()); copyErrors.add(errorCopyFailedMessage.toString()); } /** * Builds a string from the primary key values and adds to given set * * @param copiedObject object to grab key values for * @param keysWritten Set containing all pk strings */ protected void addToKeysWritten( FiscalYearMaker fiscalYearMaker, List<String> keyFieldNames, FiscalYearBasedBusinessObject copiedObject, Set<String> keysWritten) throws Exception { keysWritten.add(getKeyString(fiscalYearMaker, keyFieldNames, copiedObject)); } protected String getKeyString( FiscalYearMaker fiscalYearMaker, List<String> keyFieldNames, FiscalYearBasedBusinessObject businessObject ) throws Exception { StringBuilder keyString = new StringBuilder(40); for (String keyFieldName : keyFieldNames) { keyString.append( PropertyUtils.getSimpleProperty(businessObject, keyFieldName) ).append( KEY_STRING_DELIMITER ); } return keyString.toString(); } }