/* * 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.module.ar.batch.service.impl; import java.awt.Color; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.InvocationTargetException; import java.text.MessageFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.beanutils.BeanUtils; import org.apache.commons.beanutils.PropertyUtils; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.kuali.kfs.coa.service.OrganizationService; import org.kuali.kfs.module.ar.ArConstants; import org.kuali.kfs.module.ar.ArKeyConstants; import org.kuali.kfs.module.ar.batch.CustomerLoadStep; import org.kuali.kfs.module.ar.batch.report.CustomerLoadBatchErrors; import org.kuali.kfs.module.ar.batch.report.CustomerLoadFileResult; import org.kuali.kfs.module.ar.batch.report.CustomerLoadResult; import org.kuali.kfs.module.ar.batch.report.CustomerLoadResult.ResultCode; import org.kuali.kfs.module.ar.batch.service.CustomerLoadService; import org.kuali.kfs.module.ar.batch.vo.CustomerDigesterAdapter; import org.kuali.kfs.module.ar.batch.vo.CustomerDigesterVO; import org.kuali.kfs.module.ar.businessobject.Customer; import org.kuali.kfs.module.ar.businessobject.CustomerAddress; import org.kuali.kfs.module.ar.document.service.CustomerService; import org.kuali.kfs.module.ar.document.service.SystemInformationService; import org.kuali.kfs.module.ar.document.validation.impl.CustomerRule; import org.kuali.kfs.sys.KFSConstants; import org.kuali.kfs.sys.KFSKeyConstants; import org.kuali.kfs.sys.batch.BatchInputFileType; import org.kuali.kfs.sys.batch.InitiateDirectoryBase; import org.kuali.kfs.sys.batch.service.BatchInputFileService; import org.kuali.kfs.sys.exception.ParseException; import org.kuali.rice.core.api.config.property.ConfigurationService; import org.kuali.rice.core.api.datetime.DateTimeService; import org.kuali.rice.coreservice.framework.parameter.ParameterService; import org.kuali.rice.kew.api.exception.WorkflowException; import org.kuali.rice.kns.document.MaintenanceDocument; import org.kuali.rice.kns.document.MaintenanceDocumentBase; import org.kuali.rice.krad.service.BusinessObjectService; import org.kuali.rice.krad.service.DocumentService; import org.kuali.rice.krad.util.ErrorMessage; import org.kuali.rice.krad.util.GlobalVariables; import org.kuali.rice.krad.util.KRADConstants; import org.kuali.rice.krad.util.MessageMap; import org.kuali.rice.krad.util.ObjectUtils; import org.springframework.transaction.annotation.Transactional; import com.lowagie.text.Chunk; import com.lowagie.text.Document; import com.lowagie.text.DocumentException; import com.lowagie.text.Element; import com.lowagie.text.Font; import com.lowagie.text.FontFactory; import com.lowagie.text.PageSize; import com.lowagie.text.Paragraph; import com.lowagie.text.pdf.PdfWriter; @Transactional public class CustomerLoadServiceImpl extends InitiateDirectoryBase implements CustomerLoadService { private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(CustomerLoadServiceImpl.class); private static final String MAX_RECORDS_PARM_NAME = "MAX_NUMBER_OF_RECORDS_PER_DOCUMENT"; private static final String NA = "-- N/A --"; private static final String WORKFLOW_DOC_ID_PREFIX = " - WITH WORKFLOW DOCID: "; private BatchInputFileService batchInputFileService; private CustomerService customerService; private ConfigurationService configService; private DocumentService docService; private ParameterService parameterService; private OrganizationService orgService; private SystemInformationService sysInfoService; private BusinessObjectService boService; private DateTimeService dateTimeService; private List<BatchInputFileType> batchInputFileTypes; private CustomerDigesterAdapter adapter; private String reportsDirectory; /** * @see org.kuali.kfs.module.ar.batch.service.CustomerLoadService#loadFiles() */ @Override public boolean loadFiles() { LOG.info("Beginning processing of all available files for AR Customer Batch Upload."); boolean result = true; List<CustomerLoadFileResult> fileResults = new ArrayList<CustomerLoadFileResult>(); CustomerLoadFileResult reporter = null; // moved these two lists from loadFile() as comment indicated from svn-17753 which can possibly be used for report/log output List<String> routedDocumentNumbers = new ArrayList<String>(); List<String> failedDocumentNumbers = new ArrayList<String>(); // create a list of the files to process Map<String, BatchInputFileType> fileNamesToLoad = getListOfFilesToProcess(); LOG.info("Found " + fileNamesToLoad.size() + " file(s) to process."); // process each file in turn List<String> processedFiles = new ArrayList<String>(); for (String inputFileName : fileNamesToLoad.keySet()) { LOG.info("Beginning processing of filename: " + inputFileName + "."); // setup the results reporting reporter = new CustomerLoadFileResult(inputFileName); fileResults.add(reporter); if (loadFile(inputFileName, reporter, fileNamesToLoad.get(inputFileName), routedDocumentNumbers, failedDocumentNumbers)) { result &= true; reporter.addFileInfoMessage("File successfully completed processing."); processedFiles.add(inputFileName); } else { reporter.addFileErrorMessage("File failed to process successfully."); result &= false; } } // remove done files removeDoneFiles(processedFiles); // write report PDF writeReportPDF(fileResults); return result; } /** * Create a collection of the files to process with the mapped value of the BatchInputFileType * * @return */ protected Map<String, BatchInputFileType> getListOfFilesToProcess() { Map<String, BatchInputFileType> inputFileTypeMap = new LinkedHashMap<String, BatchInputFileType>(); for (BatchInputFileType batchInputFileType : batchInputFileTypes) { List<String> inputFileNames = batchInputFileService.listInputFileNamesWithDoneFile(batchInputFileType); if (inputFileNames == null) { criticalError("BatchInputFileService.listInputFileNamesWithDoneFile(" + batchInputFileType.getFileTypeIdentifer() + ") returned NULL which should never happen."); } else { // update the file name mapping for (String inputFileName : inputFileNames) { // filenames returned should never be blank/empty/null if (StringUtils.isBlank(inputFileName)) { criticalError("One of the file names returned as ready to process [" + inputFileName + "] was blank. This should not happen, so throwing an error to investigate."); } inputFileTypeMap.put(inputFileName, batchInputFileType); } } } return inputFileTypeMap; } /** * Clears out associated .done files for the processed data files. * * @param dataFileNames */ protected void removeDoneFiles(List<String> dataFileNames) { for (String dataFileName : dataFileNames) { File doneFile = new File(StringUtils.substringBeforeLast(dataFileName, ".") + ".done"); if (doneFile.exists()) { doneFile.delete(); } } } /** * @see org.kuali.kfs.module.ar.batch.service.CustomerLoadService#loadFile(java.lang.String, org.kuali.kfs.module.ar.batch.report.CustomerLoadFileResult, org.kuali.kfs.sys.batch.BatchInputFileType, java.util.List, java.util.List) */ @Override public boolean loadFile(String fileName, CustomerLoadFileResult reporter, BatchInputFileType batchInputFileType, List<String> routedDocumentNumbers, List<String> failedDocumentNumbers) { boolean result = true; // load up the file into a byte array byte[] fileByteContent = safelyLoadFileBytes(fileName); // parse the file against the XSD schema and load it into an object LOG.info("Attempting to parse the file using Apache Digester."); Object parsedObject = null; try { parsedObject = batchInputFileService.parse(batchInputFileType, fileByteContent); } catch (ParseException e) { String errorMessage ="Error parsing batch file: " + e.getMessage(); reporter.addFileErrorMessage(errorMessage); LOG.error(errorMessage, e); throw new RuntimeException(errorMessage); } // make sure we got the type we expected, then cast it if (!(parsedObject instanceof List)) { String errorMessage = "Parsed file was not of the expected type. Expected [" + List.class + "] but got [" + parsedObject.getClass() + "]."; reporter.addFileErrorMessage(errorMessage); criticalError(errorMessage); } // prepare a list for the regular validate() method List<CustomerDigesterVO> customerVOs = (List<CustomerDigesterVO>) parsedObject; List<MaintenanceDocument> readyTransientDocs = new ArrayList<MaintenanceDocument>(); LOG.info("Beginning validation and preparation of batch file."); result = validateCustomers(customerVOs, readyTransientDocs, reporter, false); // send the readyDocs into workflow result &= sendDocumentsIntoWorkflow(readyTransientDocs, routedDocumentNumbers, failedDocumentNumbers, reporter); return result; } protected boolean sendDocumentsIntoWorkflow(List<MaintenanceDocument> readyTransientDocs, List<String> routedDocumentNumbers, List<String> failedDocumentNumbers, CustomerLoadFileResult reporter) { boolean result = true; for (MaintenanceDocument readyTransientDoc : readyTransientDocs) { result &= sendDocumentIntoWorkflow(readyTransientDoc, routedDocumentNumbers, failedDocumentNumbers, reporter); } return result; } protected boolean sendDocumentIntoWorkflow(MaintenanceDocument readyTransientDoc, List<String> routedDocumentNumbers, List<String> failedDocumentNumbers, CustomerLoadFileResult reporter) { boolean result = true; String customerName = ((Customer) readyTransientDoc.getNewMaintainableObject().getBusinessObject()).getCustomerName(); // create a real workflow document MaintenanceDocument realMaintDoc; try { realMaintDoc = (MaintenanceDocument) docService.getNewDocument(getCustomerMaintenanceDocumentTypeName()); } catch (WorkflowException e) { LOG.error("WorkflowException occurred while trying to create a new MaintenanceDocument.", e); throw new RuntimeException("WorkflowException occurred while trying to create a new MaintenanceDocument.", e); } realMaintDoc.getNewMaintainableObject().setBusinessObject(readyTransientDoc.getNewMaintainableObject().getBusinessObject()); realMaintDoc.getOldMaintainableObject().setBusinessObject(readyTransientDoc.getOldMaintainableObject().getBusinessObject()); realMaintDoc.getNewMaintainableObject().setMaintenanceAction(readyTransientDoc.getNewMaintainableObject().getMaintenanceAction()); realMaintDoc.getDocumentHeader().setDocumentDescription(readyTransientDoc.getDocumentHeader().getDocumentDescription()); Customer customer = (Customer) realMaintDoc.getNewMaintainableObject().getBusinessObject(); LOG.info("Routing Customer Maintenance document for [" + customer.getCustomerNumber() + "] " + customer.getCustomerName()); try { docService.routeDocument(realMaintDoc, "Routed Edit/Update Customer Maintenance from CustomerLoad Batch Process", null); } catch (WorkflowException e) { LOG.error("WorkflowException occurred while trying to route a new MaintenanceDocument.", e); reporter.addCustomerErrorMessage(customerName, "WorkflowException occurred while trying to route a new MaintenanceDocument: " + e.getMessage()); result = false; } if (result == true) { reporter.setCustomerSuccessResult(customerName); reporter.setCustomerWorkflowDocId(customerName, realMaintDoc.getDocumentNumber()); routedDocumentNumbers.add(realMaintDoc.getDocumentNumber()); } else { reporter.setCustomerFailureResult(customerName); failedDocumentNumbers.add(realMaintDoc.getDocumentNumber()); } return result; } protected String getCustomerMaintenanceDocumentTypeName() { return "CUS"; } protected void addError(CustomerLoadBatchErrors batchErrors, String customerName, String propertyName, Class<?> propertyClass, String origValue, String description) { batchErrors.addError(customerName, propertyName, propertyClass, origValue, description); } protected void addBatchErrorsToGlobalVariables(CustomerLoadBatchErrors batchErrors) { Set<String> errorMessages = batchErrors.getErrorStrings(); for (String errorMessage : errorMessages) { GlobalVariables.getMessageMap().putError(KFSConstants.GLOBAL_ERRORS, KFSKeyConstants.ERROR_BATCH_UPLOAD_SAVE, errorMessage); } } protected void addBatchErrorstoCustomerLoadResult(CustomerLoadBatchErrors batchErrors, CustomerLoadResult result) { Set<String> errorMessages = batchErrors.getErrorStrings(); for (String errorMessage : errorMessages) { result.addErrorMessage(errorMessage); } } /** * * Accepts a file name and returns a byte-array of the file name contents, if possible. * * Throws RuntimeExceptions if FileNotFound or IOExceptions occur. * * @param fileName String containing valid path & filename (relative or absolute) of file to load. * @return A Byte Array of the contents of the file. */ protected byte[] safelyLoadFileBytes(String fileName) { InputStream fileContents; byte[] fileByteContent; try { fileContents = new FileInputStream(fileName); fileByteContent = IOUtils.toByteArray(fileContents); } catch (FileNotFoundException fnfe) { LOG.error("Batch file not found [" + fileName + "]. " + fnfe.getMessage()); throw new RuntimeException("Batch File not found [" + fileName + "]. " + fnfe.getMessage()); } catch (IOException ioe) { LOG.error("IO Exception loading: [" + fileName + "]. " + ioe.getMessage()); throw new RuntimeException("IO Exception loading: [" + fileName + "]. " + ioe.getMessage()); } return fileByteContent; } /** * The results of this method follow the same rules as the batch step result rules: * * The execution of this method may have 3 possible outcomes: * * 1. returns true, meaning that everything has succeeded, and dependent steps can continue running. No * errors should be added to GlobalVariables.getMessageMap(). * * 2. returns false, meaning that some (but not necessarily all) steps have succeeded, and dependent * steps can continue running. Details can be found in the GlobalVariables.getMessageMap(). * * 3. throws an exception, meaning that the step has failed, that the rest of the steps in a job should * not be run, and that the job has failed. There may be errors in the GlobalVariables.getMessageMap(). * * @see org.kuali.kfs.module.ar.batch.service.CustomerLoadService#validate(java.util.List) */ @Override public boolean validate(List<CustomerDigesterVO> customerUploads) { return validateAndPrepare(customerUploads, new ArrayList<MaintenanceDocument>(), true); } /** * @see org.kuali.kfs.module.ar.batch.service.CustomerLoadService#validateAndPrepare(java.util.List, java.util.List, boolean) */ @Override public boolean validateAndPrepare(List<CustomerDigesterVO> customerUploads, List<MaintenanceDocument> customerMaintDocs, boolean useGlobalMessageMap) { return validateCustomers(customerUploads, customerMaintDocs, new CustomerLoadFileResult(), useGlobalMessageMap); } /** * * Validate the customers lists * * @param customerUploads * @param customerMaintDocs * @param reporter * @param useGlobalMessageMap * @return */ protected boolean validateCustomers(List<CustomerDigesterVO> customerUploads, List<MaintenanceDocument> customerMaintDocs, CustomerLoadFileResult reporter, boolean useGlobalMessageMap) { // fail if empty or null list if (customerUploads == null) { LOG.error("Null list of Customer upload objects. This should never happen."); throw new IllegalArgumentException("Null list of Customer upload objects. This should never happen."); } if (customerUploads.isEmpty()) { reporter.addFileErrorMessage("An empty list of Customer uploads was passed in for validation. As a result, no validation can be done."); if (useGlobalMessageMap) { GlobalVariables.getMessageMap().putError(KFSConstants.GLOBAL_ERRORS, KFSKeyConstants.ERROR_BATCH_UPLOAD_SAVE, new String[] { "An empty list of Customer uploads was passed in for validation. As a result, no validation was done." }); } return false; } boolean groupSucceeded = true; boolean docSucceeded = true; // check to make sure the input file doesnt have more docs than we allow in one batch file String maxRecordsString = parameterService.getParameterValueAsString(CustomerLoadStep.class, MAX_RECORDS_PARM_NAME); if (StringUtils.isBlank(maxRecordsString) || !StringUtils.isNumeric(maxRecordsString)) { criticalError("Expected 'Max Records Per Document' System Parameter is not available."); } Integer maxRecords = new Integer(maxRecordsString); if (customerUploads.size() > maxRecords.intValue()) { LOG.error("Too many records passed in for this file. " + customerUploads.size() + " were passed in, and the limit is " + maxRecords + ". As a result, no validation was done."); reporter.addFileErrorMessage("Too many records passed in for this file. " + customerUploads.size() + " were passed in, and the limit is " + maxRecords + ". As a result, no validation was done."); if (useGlobalMessageMap) { GlobalVariables.getMessageMap().putError(KFSConstants.GLOBAL_ERRORS, KFSKeyConstants.ERROR_BATCH_UPLOAD_SAVE, new String[] { "Too many records passed in for this file. " + customerUploads.size() + " were passed in, and the limit is " + maxRecords + ". As a result, no validation was done." }); } return false; } // we have to create one real maint doc for the whole thing to pass the maintainable.checkAuthorizationRestrictions MaintenanceDocument oneRealMaintDoc = null; Customer customer = null; CustomerLoadBatchErrors fileBatchErrors = new CustomerLoadBatchErrors(); CustomerLoadBatchErrors customerBatchErrors; String customerName; if (adapter == null) { adapter = new CustomerDigesterAdapter(); } for (CustomerDigesterVO customerDigesterVO : customerUploads) { docSucceeded = true; customerName = customerDigesterVO.getCustomerName(); // setup logging and reporting LOG.info("Beginning conversion and validation for [" + customerName + "]."); reporter.addCustomerInfoMessage(customerName, "Beginning conversion and validation."); CustomerLoadResult result = reporter.getCustomer(customerName); customerBatchErrors = new CustomerLoadBatchErrors(); // convert the VO to a BO LOG.info("Beginning conversion from VO to BO."); customer = adapter.convert(customerDigesterVO, customerBatchErrors); // if any errors were generated, add them to the GlobalVariables, and return false if (!customerBatchErrors.isEmpty()) { LOG.info("The customer [" + customerName + "] was not processed due to errors in uploading and conversion."); customerBatchErrors.addError(customerName, "Global", Object.class, "", "This document was not processed due to errors in uploading and conversion."); addBatchErrorstoCustomerLoadResult(customerBatchErrors, result); reporter.setCustomerFailureResult(customerName); docSucceeded = false; groupSucceeded &= false; continue; } // determine whether this is an Update or a New Customer existingCustomer = customerAlreadyExists(customer); boolean isNew = (existingCustomer == null); boolean isUpdate = !isNew; // do some housekeeping processBeforeValidating(customer, existingCustomer, isUpdate); // create the transient maint doc MaintenanceDocument transientMaintDoc = createTransientMaintDoc(); // make sure we have the one real maint doc (to steal its document id) oneRealMaintDoc = createRealMaintDoc(oneRealMaintDoc); // steal the doc id from the real doc transientMaintDoc.setDocumentNumber(oneRealMaintDoc.getDocumentNumber()); transientMaintDoc.setDocumentHeader(oneRealMaintDoc.getDocumentHeader()); transientMaintDoc.getDocumentHeader().setDocumentDescription("AR Customer Load Batch Transient"); // set the old and new transientMaintDoc.getNewMaintainableObject().setBusinessObject(customer); transientMaintDoc.getOldMaintainableObject().setBusinessObject((existingCustomer == null ? new Customer() : existingCustomer )); // set the maintainable actions, so isNew and isEdit on the maint doc return correct values if (isNew) { transientMaintDoc.getNewMaintainableObject().setMaintenanceAction(KRADConstants.MAINTENANCE_NEW_ACTION); } else { transientMaintDoc.getNewMaintainableObject().setMaintenanceAction(KRADConstants.MAINTENANCE_EDIT_ACTION); } // report whether the customer is an Add or an Edit if (isNew) { reporter.addCustomerInfoMessage(customerName, "Customer record batched is a New Customer."); } else { reporter.addCustomerInfoMessage(customerName, "Customer record batched is an Update to an existing Customer."); } // validate the batched customer if (!validateSingle(transientMaintDoc, customerBatchErrors, customerName)) { groupSucceeded &= false; docSucceeded = false; reporter.setCustomerFailureResult(customerName); } addBatchErrorstoCustomerLoadResult(customerBatchErrors, result); // if the doc succeeded then add it to the list to be routed, and report it as successful if (docSucceeded) { customerMaintDocs.add(transientMaintDoc); Customer customer2 = (Customer) transientMaintDoc.getNewMaintainableObject().getBusinessObject(); reporter.addCustomerInfoMessage(customerName, "Customer Number is: " + customer2.getCustomerNumber()); reporter.addCustomerInfoMessage(customerName, "Customer Name is: " + customer2.getCustomerName()); reporter.setCustomerSuccessResult(customerName); } fileBatchErrors.addAll(customerBatchErrors); } // put any errors back in global vars if (useGlobalMessageMap) { addBatchErrorsToGlobalVariables(fileBatchErrors); } return groupSucceeded; } /** * pre-processing for existing and new customer * * @param customer * @param existingCustomer * @param isUpdate */ protected void processBeforeValidating(Customer customer, Customer existingCustomer, boolean isUpdate) { //update specifics processing if (isUpdate) { // if its has no customerNumber, then set it from existing record if (StringUtils.isBlank(customer.getCustomerNumber())) { customer.setCustomerNumber(existingCustomer.getCustomerNumber()); } // carry forward the version number customer.setVersionNumber(existingCustomer.getVersionNumber()); // don't let the batch zero out certain key fields on an update dontBlankOutFieldsOnUpdate(customer, existingCustomer, "customerTypeCode"); dontBlankOutFieldsOnUpdate(customer, existingCustomer, "customerTaxTypeCode"); dontBlankOutFieldsOnUpdate(customer, existingCustomer, "customerTaxNbr"); dontBlankOutFieldsOnUpdate(customer, existingCustomer, "customerCreditLimitAmount"); dontBlankOutFieldsOnUpdate(customer, existingCustomer, "customerCreditApprovedByName"); dontBlankOutFieldsOnUpdate(customer, existingCustomer, "customerParentCompanyNumber"); dontBlankOutFieldsOnUpdate(customer, existingCustomer, "customerPhoneNumber"); dontBlankOutFieldsOnUpdate(customer, existingCustomer, "customer800PhoneNumber"); dontBlankOutFieldsOnUpdate(customer, existingCustomer, "customerContactName"); dontBlankOutFieldsOnUpdate(customer, existingCustomer, "customerContactPhoneNumber"); dontBlankOutFieldsOnUpdate(customer, existingCustomer, "customerFaxNumber"); dontBlankOutFieldsOnUpdate(customer, existingCustomer, "customerBirthDate"); } // upper case important fields upperCaseKeyFields(customer); //NOTE: What's the reason for determining primary address?? address isn't used afterward // determine whether the batch has a primary address, and which one it is boolean batchHasPrimaryAddress = false; CustomerAddress batchPrimaryAddress = null; for (CustomerAddress address : customer.getCustomerAddresses()) { if (ArKeyConstants.CustomerConstants.CUSTOMER_ADDRESS_TYPE_CODE_PRIMARY.equalsIgnoreCase(address.getCustomerAddressTypeCode())) { batchHasPrimaryAddress = true; batchPrimaryAddress = address; } } // if its an update, merge the address records (ie, only add or update, dont remove all addresses not imported). if (isUpdate) { boolean addressInBatchCustomer = false; List<CustomerAddress> newCusomterAddresses = customer.getCustomerAddresses(); // populate a stub address list (with empty addresses) base on the new customer address list size List<CustomerAddress> stubAddresses = new ArrayList<CustomerAddress>(); for (CustomerAddress batchAddress : newCusomterAddresses) { stubAddresses.add(new CustomerAddress()); } for (CustomerAddress existingAddress : existingCustomer.getCustomerAddresses()) { addressInBatchCustomer = false; for (CustomerAddress batchAddress : newCusomterAddresses) { if (!addressInBatchCustomer && existingAddress.compareTo(batchAddress) == 0) { addressInBatchCustomer = true; } } if (!addressInBatchCustomer) { //clone the address to avoid changing the existingAddress's type code CustomerAddress clonedExistingAddress = cloneCustomerAddress(existingAddress); // make sure we don't add a second Primary address, if the batch specifies a primary address, it wins if (batchHasPrimaryAddress && ArKeyConstants.CustomerConstants.CUSTOMER_ADDRESS_TYPE_CODE_PRIMARY.equalsIgnoreCase(clonedExistingAddress.getCustomerAddressTypeCode())) { clonedExistingAddress.setCustomerAddressTypeCode(ArKeyConstants.CustomerConstants.CUSTOMER_ADDRESS_TYPE_CODE_ALTERNATE); } customer.getCustomerAddresses().add(clonedExistingAddress); }else{ //found a address already in batch, remove one stub address from the list stubAddresses.remove(0); } } //append existing list to the stub list in order to have matching number of address for display, so the merged address from existing list is matched up stubAddresses.addAll(existingCustomer.getCustomerAddresses()); // reset existing customer's address to the stub address list existingCustomer.setCustomerAddresses(stubAddresses); } // set parent customer number to null if blank (otherwise foreign key rule fails) if (StringUtils.isBlank(customer.getCustomerParentCompanyNumber())) { customer.setCustomerParentCompanyNumber(null); } } /** * Clone the address object * * @param address * @return */ private CustomerAddress cloneCustomerAddress(CustomerAddress address) { CustomerAddress clonedAddress = null; try { clonedAddress = (CustomerAddress) BeanUtils.cloneBean(address); } catch (IllegalAccessException | InstantiationException | InvocationTargetException | NoSuchMethodException ex) { LOG.error("Unable to clone address [" + address + "]", ex); throw new RuntimeException("Unable to clone address [" + address + "]", ex); } return clonedAddress; } protected void upperCaseKeyFields(Customer customer) { // customer name if (StringUtils.isNotBlank(customer.getCustomerName())) { customer.setCustomerName(customer.getCustomerName().toUpperCase()); } // customer number if (StringUtils.isNotBlank(customer.getCustomerNumber())) { customer.setCustomerNumber(customer.getCustomerNumber().toUpperCase()); } // parent company number if (StringUtils.isNotBlank(customer.getCustomerParentCompanyNumber())) { customer.setCustomerParentCompanyNumber(customer.getCustomerParentCompanyNumber().toUpperCase()); } // customer tax type code if (StringUtils.isNotBlank(customer.getCustomerTaxTypeCode())) { customer.setCustomerTaxTypeCode(customer.getCustomerTaxTypeCode().toUpperCase()); } // customer tax number if (StringUtils.isNotBlank(customer.getCustomerTaxNbr())) { customer.setCustomerTaxNbr(customer.getCustomerTaxNbr().toUpperCase()); } // customer contact name if (StringUtils.isNotBlank(customer.getCustomerContactName())) { customer.setCustomerContactName(customer.getCustomerContactName().toUpperCase()); } // customer credit approved by name if (StringUtils.isNotBlank(customer.getCustomerCreditApprovedByName())) { customer.setCustomerCreditApprovedByName(customer.getCustomerCreditApprovedByName().toUpperCase()); } // customer email address if (StringUtils.isNotBlank(customer.getCustomerEmailAddress())) { customer.setCustomerEmailAddress(customer.getCustomerEmailAddress().toUpperCase()); } for (CustomerAddress address : customer.getCustomerAddresses()) { if (address == null) { continue; } // customer number if (StringUtils.isNotBlank(address.getCustomerNumber())) { address.setCustomerNumber(address.getCustomerNumber().toUpperCase()); } // customer address name if (StringUtils.isNotBlank(address.getCustomerAddressName())) { address.setCustomerAddressName(address.getCustomerAddressName().toUpperCase()); } // customerLine1StreetAddress if (StringUtils.isNotBlank(address.getCustomerLine1StreetAddress())) { address.setCustomerLine1StreetAddress(address.getCustomerLine1StreetAddress().toUpperCase()); } // customerLine2StreetAddress if (StringUtils.isNotBlank(address.getCustomerLine2StreetAddress())) { address.setCustomerLine2StreetAddress(address.getCustomerLine2StreetAddress().toUpperCase()); } // customerCityName if (StringUtils.isNotBlank(address.getCustomerCityName())) { address.setCustomerCityName(address.getCustomerCityName().toUpperCase()); } // customerStateCode if (StringUtils.isNotBlank(address.getCustomerStateCode())) { address.setCustomerStateCode(address.getCustomerStateCode().toUpperCase()); } // customerZipCode if (StringUtils.isNotBlank(address.getCustomerZipCode())) { address.setCustomerZipCode(address.getCustomerZipCode().toUpperCase()); } // customerCountryCode if (StringUtils.isNotBlank(address.getCustomerNumber())) { address.setCustomerNumber(address.getCustomerNumber().toUpperCase()); } // customerAddressInternationalProvinceName if (StringUtils.isNotBlank(address.getCustomerAddressInternationalProvinceName())) { address.setCustomerAddressInternationalProvinceName(address.getCustomerAddressInternationalProvinceName().toUpperCase()); } // customerInternationalMailCode if (StringUtils.isNotBlank(address.getCustomerInternationalMailCode())) { address.setCustomerInternationalMailCode(address.getCustomerInternationalMailCode().toUpperCase()); } // customerEmailAddress if (StringUtils.isNotBlank(address.getCustomerEmailAddress())) { address.setCustomerEmailAddress(address.getCustomerEmailAddress().toUpperCase()); } // customerAddressTypeCode if (StringUtils.isNotBlank(address.getCustomerAddressTypeCode())) { address.setCustomerAddressTypeCode(address.getCustomerAddressTypeCode().toUpperCase()); } } } /** * * This messy thing attempts to compare a property on the batch customer (new) and existing customer, and if * the new is blank, but the old is there, to overwrite the new-value with the old-value, thus preventing * batch uploads from blanking out certain fields. * * @param batchCustomer * @param existingCustomer * @param propertyName */ protected void dontBlankOutFieldsOnUpdate(Customer batchCustomer, Customer existingCustomer, String propertyName) { String batchValue; String existingValue; Class<?> propertyClass = null; // try to retrieve the property type to see if it exists at all try { propertyClass = PropertyUtils.getPropertyType(batchCustomer, propertyName); // if the property doesnt exist, then throw an exception if (propertyClass == null) { throw new IllegalArgumentException("The propertyName specified [" + propertyName + "] doesnt exist on the Customer object."); } // get the String values of both batch and existing, to compare batchValue = BeanUtils.getSimpleProperty(batchCustomer, propertyName); existingValue = BeanUtils.getSimpleProperty(existingCustomer, propertyName); // if the existing is non-blank, and the new is blank, then over-write the new with the existing value if (StringUtils.isBlank(batchValue) && StringUtils.isNotBlank(existingValue)) { // get the real typed value, and then try to set the property value Object typedValue = PropertyUtils.getProperty(existingCustomer, propertyName); BeanUtils.setProperty(batchCustomer, propertyName, typedValue); } } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException ex) { throw new RuntimeException("Could not set properties on the Customer object", ex); } } protected boolean validateSingle(MaintenanceDocument maintDoc, CustomerLoadBatchErrors batchErrors, String customerName) { boolean result = true; // get an instance of the business rule CustomerRule rule = new CustomerRule(); // run the business rules result &= rule.processRouteDocument(maintDoc); extractGlobalVariableErrors(batchErrors, customerName); return result; } protected boolean extractGlobalVariableErrors(CustomerLoadBatchErrors batchErrors, String customerName) { boolean result = true; MessageMap messageMap = GlobalVariables.getMessageMap(); Set<String> errorKeys = messageMap.getAllPropertiesWithErrors(); List<ErrorMessage> errorMessages = null; Object[] messageParams; String errorKeyString; String errorString; for (String errorProperty : errorKeys) { errorMessages = messageMap.getErrorMessagesForProperty(errorProperty); for (ErrorMessage errorMessage : errorMessages) { errorKeyString = configService.getPropertyValueAsString(errorMessage.getErrorKey()); messageParams = errorMessage.getMessageParameters(); // MessageFormat.format only seems to replace one // per pass, so I just keep beating on it until all are gone. if (StringUtils.isBlank(errorKeyString)) { errorString = errorMessage.getErrorKey(); } else { errorString = errorKeyString; } while (errorString.matches("^.*\\{\\d\\}.*$")) { errorString = MessageFormat.format(errorString, messageParams); } batchErrors.addError(customerName, errorProperty, Object.class, "", errorString); result = false; } } // clear the stuff out of globalvars, as we need to reformat it and put it back GlobalVariables.getMessageMap().clearErrorMessages(); return result; } protected MaintenanceDocument createTransientMaintDoc() { MaintenanceDocument maintDoc = new MaintenanceDocumentBase(getCustomerMaintenanceDocumentTypeName()); return maintDoc; } protected MaintenanceDocument createRealMaintDoc(MaintenanceDocument document) { if (document == null) { try { document = (MaintenanceDocument) docService.getNewDocument(getCustomerMaintenanceDocumentTypeName()); } catch (WorkflowException e) { throw new RuntimeException("WorkflowException thrown when trying to create new MaintenanceDocument.", e); } } return document; } /** */ protected Customer customerAlreadyExists(Customer customer) { Customer existingCustomer = null; // test existence by customerNumber, if one is passed in if (StringUtils.isNotBlank(customer.getCustomerNumber())) { existingCustomer = customerService.getByPrimaryKey(customer.getCustomerNumber()); if (existingCustomer != null) { return existingCustomer; } } // test existence by TaxNumber, if one is passed in if (StringUtils.isNotBlank(customer.getCustomerTaxNbr())) { existingCustomer = customerService.getByTaxNumber(customer.getCustomerTaxNbr()); if (existingCustomer != null) { return existingCustomer; } } // test existence by Customer Name. this is looking for an exact match, so isnt terribly effective if (StringUtils.isNotBlank(customer.getCustomerName())) { existingCustomer = customerService.getCustomerByName(customer.getCustomerName()); if (existingCustomer != null) { return existingCustomer; } } // return a null Customer if no matches were found return existingCustomer; } protected void writeReportPDF(List<CustomerLoadFileResult> fileResults) { if (fileResults.isEmpty()) { return; } // setup the PDF business Document pdfDoc = new Document(PageSize.LETTER, 54, 54, 72, 72); try { getPdfWriter(pdfDoc); try { pdfDoc.open(); if (fileResults.isEmpty()) { writeFileNameSectionTitle(pdfDoc, "NO DOCUMENTS FOUND TO PROCESS"); return; } CustomerLoadResult result; String customerResultLine; for (CustomerLoadFileResult fileResult : fileResults) { // file name title String fileNameOnly = fileResult.getFilename().toUpperCase(); fileNameOnly = fileNameOnly.substring(fileNameOnly.lastIndexOf("\\") + 1); writeFileNameSectionTitle(pdfDoc, fileNameOnly); // write any file-general messages writeMessageEntryLines(pdfDoc, fileResult.getMessages()); // walk through each customer included in this file for (String customerName : fileResult.getCustomerNames()) { result = fileResult.getCustomer(customerName); // write the customer title writeCustomerSectionTitle(pdfDoc, customerName.toUpperCase()); // write a success/failure results line for this customer customerResultLine = result.getResultString() + (ResultCode.SUCCESS.equals(result.getResult()) ? WORKFLOW_DOC_ID_PREFIX + result.getWorkflowDocId() : ""); writeCustomerSectionResult(pdfDoc, customerResultLine); // write any customer messages writeMessageEntryLines(pdfDoc, result.getMessages()); } } } finally { if (pdfDoc != null) { pdfDoc.close(); } } } catch (IOException | DocumentException ex) { throw new RuntimeException("Could not open file for results report",ex); } } protected void writeFileNameSectionTitle(Document pdfDoc, String filenameLine) { Font font = FontFactory.getFont(FontFactory.COURIER, 10, Font.BOLD); Paragraph paragraph = new Paragraph(); paragraph.setAlignment(Element.ALIGN_LEFT); Chunk chunk = new Chunk(filenameLine, font); chunk.setBackground(Color.LIGHT_GRAY, 5, 5, 5, 5); paragraph.add(chunk); // blank line paragraph.add(new Chunk("", font)); try { pdfDoc.add(paragraph); } catch (DocumentException e) { LOG.error("iText DocumentException thrown when trying to write content.", e); throw new RuntimeException("iText DocumentException thrown when trying to write content.", e); } } protected void writeCustomerSectionTitle(Document pdfDoc, String customerNameLine) { Font font = FontFactory.getFont(FontFactory.COURIER, 8, Font.BOLD + Font.UNDERLINE); Paragraph paragraph = new Paragraph(); paragraph.setAlignment(Element.ALIGN_LEFT); paragraph.add(new Chunk(customerNameLine, font)); // blank line paragraph.add(new Chunk("", font)); try { pdfDoc.add(paragraph); } catch (DocumentException e) { LOG.error("iText DocumentException thrown when trying to write content.", e); throw new RuntimeException("iText DocumentException thrown when trying to write content.", e); } } protected void writeCustomerSectionResult(Document pdfDoc, String resultLine) { Font font = FontFactory.getFont(FontFactory.COURIER, 8, Font.BOLD); Paragraph paragraph = new Paragraph(); paragraph.setAlignment(Element.ALIGN_LEFT); paragraph.add(new Chunk(resultLine, font)); // blank line paragraph.add(new Chunk("", font)); try { pdfDoc.add(paragraph); } catch (DocumentException e) { LOG.error("iText DocumentException thrown when trying to write content.", e); throw new RuntimeException("iText DocumentException thrown when trying to write content.", e); } } protected void writeMessageEntryLines(Document pdfDoc, List<String[]> messageLines) { Font font = FontFactory.getFont(FontFactory.COURIER, 8, Font.NORMAL); Paragraph paragraph; String messageEntry; for (String[] messageLine : messageLines) { paragraph = new Paragraph(); paragraph.setAlignment(Element.ALIGN_LEFT); messageEntry = StringUtils.rightPad(messageLine[0], (12 - messageLine[0].length()), " ") + " - " + messageLine[1].toUpperCase(); paragraph.add(new Chunk(messageEntry, font)); // blank line paragraph.add(new Chunk("", font)); try { pdfDoc.add(paragraph); } catch (DocumentException e) { LOG.error("iText DocumentException thrown when trying to write content.", e); throw new RuntimeException("iText DocumentException thrown when trying to write content.", e); } } } protected void getPdfWriter(Document pdfDoc) throws IOException, DocumentException { String reportDropFolder = reportsDirectory + "/" + ArConstants.CustomerLoad.CUSTOMER_LOAD_REPORT_SUBFOLDER + "/"; String fileName = ArConstants.CustomerLoad.BATCH_REPORT_BASENAME + "_" + new SimpleDateFormat("yyyyMMdd_HHmmssSSS").format(dateTimeService.getCurrentDate()) + ".pdf"; // setup the writer File reportFile = new File(reportDropFolder + fileName); FileOutputStream fileOutStream; fileOutStream = new FileOutputStream(reportFile); BufferedOutputStream buffOutStream = new BufferedOutputStream(fileOutStream); PdfWriter.getInstance(pdfDoc, buffOutStream); } public void setBatchInputFileService(BatchInputFileService batchInputFileService) { this.batchInputFileService = batchInputFileService; } public void setCustomerService(CustomerService customerService) { this.customerService = customerService; } public void setConfigService(ConfigurationService configService) { this.configService = configService; } public void setDocService(DocumentService docService) { this.docService = docService; } public void setBatchInputFileTypes(List<BatchInputFileType> batchInputFileType) { this.batchInputFileTypes = batchInputFileType; } public void setParameterService(ParameterService parameterService) { this.parameterService = parameterService; } public void setOrgService(OrganizationService orgService) { this.orgService = orgService; } public void setSysInfoService(SystemInformationService sysInfoService) { this.sysInfoService = sysInfoService; } public void setBoService(BusinessObjectService boService) { this.boService = boService; } public void setDateTimeService(DateTimeService dateTimeService) { this.dateTimeService = dateTimeService; } public void setReportsDirectory(String reportsDirectory) { this.reportsDirectory = reportsDirectory; } /** * @see org.kuali.kfs.module.ar.batch.service.CustomerLoadService#getFileName() * * this is abstracted from the CustomerLoadInputFileType */ @Override public String getFileName(String principalName, String fileUserIdentifer, String prefix, String delim) { // start with the batch-job-prefix StringBuilder fileName = new StringBuilder(delim); // add the logged-in user name if there is one, otherwise use a sensible default fileName.append(delim + principalName); // if the user specified an identifying lable, then use it if (StringUtils.isNotBlank(fileUserIdentifer)) { fileName.append(delim + fileUserIdentifer); } // stick a timestamp on the end fileName.append(delim + dateTimeService.toString(dateTimeService.getCurrentTimestamp(), "yyyyMMdd_HHmmss")); // stupid spaces, begone! return StringUtils.remove(fileName.toString(), " "); } /** * LOG error and throw RunTimeException * * @param errorMessage */ private void criticalError(String errorMessage){ LOG.error(errorMessage); throw new RuntimeException(errorMessage); } /** * @see org.kuali.kfs.sys.batch.InitiateDirectoryBase#getRequiredDirectoryNames() */ @Override public List<String> getRequiredDirectoryNames() { List<String> directoryNames = new ArrayList<String>(); if(ObjectUtils.isNotNull(batchInputFileTypes) && !CollectionUtils.isEmpty(batchInputFileTypes)) { for (BatchInputFileType batchInputFileType : batchInputFileTypes){ directoryNames.add(batchInputFileType.getDirectoryPath()); } } return directoryNames; } }