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.module.ar.batch.service.impl;
017    
018    import java.awt.Color;
019    import java.io.BufferedOutputStream;
020    import java.io.File;
021    import java.io.FileInputStream;
022    import java.io.FileNotFoundException;
023    import java.io.FileOutputStream;
024    import java.io.IOException;
025    import java.io.InputStream;
026    import java.text.MessageFormat;
027    import java.text.SimpleDateFormat;
028    import java.util.ArrayList;
029    import java.util.List;
030    import java.util.Set;
031    
032    import org.apache.commons.beanutils.BeanUtils;
033    import org.apache.commons.beanutils.PropertyUtils;
034    import org.apache.commons.io.IOUtils;
035    import org.apache.commons.lang.StringUtils;
036    import org.kuali.kfs.coa.service.OrganizationService;
037    import org.kuali.kfs.module.ar.ArConstants;
038    import org.kuali.kfs.module.ar.ArKeyConstants;
039    import org.kuali.kfs.module.ar.batch.CustomerLoadStep;
040    import org.kuali.kfs.module.ar.batch.report.CustomerLoadBatchErrors;
041    import org.kuali.kfs.module.ar.batch.report.CustomerLoadFileResult;
042    import org.kuali.kfs.module.ar.batch.report.CustomerLoadResult;
043    import org.kuali.kfs.module.ar.batch.report.CustomerLoadResult.ResultCode;
044    import org.kuali.kfs.module.ar.batch.service.CustomerLoadService;
045    import org.kuali.kfs.module.ar.batch.vo.CustomerDigesterAdapter;
046    import org.kuali.kfs.module.ar.batch.vo.CustomerDigesterVO;
047    import org.kuali.kfs.module.ar.businessobject.Customer;
048    import org.kuali.kfs.module.ar.businessobject.CustomerAddress;
049    import org.kuali.kfs.module.ar.document.service.CustomerService;
050    import org.kuali.kfs.module.ar.document.service.SystemInformationService;
051    import org.kuali.kfs.module.ar.document.validation.impl.CustomerRule;
052    import org.kuali.kfs.sys.KFSConstants;
053    import org.kuali.kfs.sys.KFSKeyConstants;
054    import org.kuali.kfs.sys.batch.BatchInputFileType;
055    import org.kuali.kfs.sys.batch.service.BatchInputFileService;
056    import org.kuali.kfs.sys.exception.ParseException;
057    import org.kuali.rice.kew.exception.WorkflowException;
058    import org.kuali.rice.kns.document.MaintenanceDocument;
059    import org.kuali.rice.kns.document.MaintenanceDocumentBase;
060    import org.kuali.rice.kns.service.BusinessObjectService;
061    import org.kuali.rice.kns.service.DateTimeService;
062    import org.kuali.rice.kns.service.DocumentService;
063    import org.kuali.rice.kns.service.KualiConfigurationService;
064    import org.kuali.rice.kns.service.ParameterService;
065    import org.kuali.rice.kns.util.ErrorMessage;
066    import org.kuali.rice.kns.util.GlobalVariables;
067    import org.kuali.rice.kns.util.KNSConstants;
068    import org.kuali.rice.kns.util.MessageMap;
069    
070    import com.lowagie.text.Chunk;
071    import com.lowagie.text.Document;
072    import com.lowagie.text.DocumentException;
073    import com.lowagie.text.Element;
074    import com.lowagie.text.Font;
075    import com.lowagie.text.FontFactory;
076    import com.lowagie.text.PageSize;
077    import com.lowagie.text.Paragraph;
078    import com.lowagie.text.pdf.PdfWriter;
079    
080    public class CustomerLoadServiceImpl implements CustomerLoadService {
081        private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(CustomerLoadServiceImpl.class);
082    
083        private static final String MAX_RECORDS_PARM_NAME = "MAX_NUMBER_OF_RECORDS_PER_DOCUMENT";
084        private static final String NA = "-- N/A --";
085        private static final String WORKFLOW_DOC_ID_PREFIX = " - WITH WORKFLOW DOCID: ";
086        
087        private BatchInputFileService batchInputFileService;
088        private CustomerService customerService;
089        private KualiConfigurationService configService;
090        private DocumentService docService;
091        private ParameterService parameterService;
092        private OrganizationService orgService;
093        private SystemInformationService sysInfoService;
094        private BusinessObjectService boService;
095        private DateTimeService dateTimeService;
096        
097        private BatchInputFileType batchInputFileType;
098        private CustomerDigesterAdapter adapter;
099        private String reportsDirectory;
100        
101        public CustomerLoadServiceImpl() {
102        }
103        
104        public boolean loadFiles() {
105            
106            LOG.info("Beginning processing of all available files for AR Customer Batch Upload.");
107            
108            boolean result = true;
109            List<CustomerLoadFileResult> fileResults = new ArrayList<CustomerLoadFileResult>();
110            CustomerLoadFileResult reporter = null;
111            
112            //  create a list of the files to process
113            List<String> fileNamesToLoad = getListOfFilesToProcess();
114            LOG.info("Found " + fileNamesToLoad.size() + " file(s) to process.");
115            
116            //  process each file in turn
117            List<String> processedFiles = new ArrayList<String>();
118            for (String inputFileName : fileNamesToLoad) {
119                
120                LOG.info("Beginning processing of filename: " + inputFileName + ".");
121                
122                //  setup the results reporting
123                reporter = new CustomerLoadFileResult(inputFileName);
124                fileResults.add(reporter);
125                
126                if (loadFile(inputFileName, reporter)) {
127                    result &= true;
128                    reporter.addFileInfoMessage("File successfully completed processing.");
129                    processedFiles.add(inputFileName);
130                }
131                else {
132                    reporter.addFileErrorMessage("File failed to process successfully.");
133                    result &= false;
134                }
135            }
136    
137            //  remove done files
138            removeDoneFiles(processedFiles);
139            
140            //  write report PDF
141            writeReportPDF(fileResults);
142            
143            return result;
144        }
145        
146        protected List<String> getListOfFilesToProcess() {
147            
148            //  create a list of the files to process
149            List<String> fileNamesToLoad = batchInputFileService.listInputFileNamesWithDoneFile(batchInputFileType);
150            
151            if (fileNamesToLoad == null) {
152                LOG.error("BatchInputFileService.listInputFileNamesWithDoneFile(" + 
153                        batchInputFileType.getFileTypeIdentifer() + ") returned NULL which should never happen.");
154                throw new RuntimeException("BatchInputFileService.listInputFileNamesWithDoneFile(" + 
155                        batchInputFileType.getFileTypeIdentifer() + ") returned NULL which should never happen.");
156            }
157            
158            //  filenames returned should never be blank/empty/null
159            for (String inputFileName : fileNamesToLoad) {
160                if (StringUtils.isBlank(inputFileName)) {
161                    LOG.error("One of the file names returned as ready to process [" + inputFileName + 
162                            "] was blank.  This should not happen, so throwing an error to investigate.");
163                    throw new RuntimeException("One of the file names returned as ready to process [" + inputFileName + 
164                            "] was blank.  This should not happen, so throwing an error to investigate.");
165                }
166            }
167            
168            return fileNamesToLoad;
169        }
170        
171        /**
172         * Clears out associated .done files for the processed data files.
173         * 
174         * @param dataFileNames
175         */
176        protected void removeDoneFiles(List<String> dataFileNames) {
177            for (String dataFileName : dataFileNames) {
178                File doneFile = new File(StringUtils.substringBeforeLast(dataFileName, ".") + ".done");
179                if (doneFile.exists()) {
180                    doneFile.delete();
181                }
182            }
183        }
184    
185        /**
186         * 
187         * @see org.kuali.kfs.module.ar.batch.service.CustomerLoadService#loadFile(java.lang.String)
188         */
189        public boolean loadFile(String fileName) {
190            return loadFile(fileName, new CustomerLoadFileResult(fileName));
191        }
192        
193        public boolean loadFile(String fileName, CustomerLoadFileResult reporter) {
194            
195            boolean result = true;
196            
197            //TODO move up to the loadFiles() method
198            List<String> routedDocumentNumbers = new ArrayList<String>();
199            List<String> failedDocumentNumbers = new ArrayList<String>();
200            
201            //  load up the file into a byte array 
202            byte[] fileByteContent = safelyLoadFileBytes(fileName);
203    
204            //  parse the file against the XSD schema and load it into an object
205            LOG.info("Attempting to parse the file using Apache Digester.");
206            Object parsedObject = null;
207            try {
208                parsedObject = batchInputFileService.parse(batchInputFileType, fileByteContent);
209            }
210            catch (ParseException e) {
211                LOG.error("Error parsing batch file: " + e.getMessage());
212                reporter.addFileErrorMessage("Error parsing batch file: " + e.getMessage());
213                throw new ParseException(e.getMessage());
214            }
215            
216            //  make sure we got the type we expected, then cast it
217            if (!(parsedObject instanceof List)) {
218                LOG.error("Parsed file was not of the expected type.  Expected [" + List.class + "] but got [" + parsedObject.getClass() + "].");
219                reporter.addFileErrorMessage("Parsed file was not of the expected type.  Expected [" + List.class + "] but got [" + parsedObject.getClass() + "].");
220                throw new RuntimeException("Parsed file was not of the expected type.  Expected [" + List.class + "] but got [" + parsedObject.getClass() + "].");
221            }
222            
223            //  prepare a list for the regular validate() method
224            List<CustomerDigesterVO> customerVOs = (List<CustomerDigesterVO>) parsedObject;
225            
226            List<MaintenanceDocument> readyTransientDocs = new ArrayList<MaintenanceDocument>();
227            LOG.info("Beginning validation and preparation of batch file.");
228            result = validateCustomers(customerVOs, readyTransientDocs, reporter, false);
229            
230            //  send the readyDocs into workflow
231            result &= sendDocumentsIntoWorkflow(readyTransientDocs, routedDocumentNumbers, failedDocumentNumbers, reporter);
232            
233            return result;
234        }
235    
236        protected boolean sendDocumentsIntoWorkflow(List<MaintenanceDocument> readyTransientDocs, List<String> routedDocumentNumbers, 
237                List<String> failedDocumentNumbers, CustomerLoadFileResult reporter) {
238            boolean result = true;
239            for (MaintenanceDocument readyTransientDoc : readyTransientDocs) {
240                result &= sendDocumentIntoWorkflow(readyTransientDoc, routedDocumentNumbers, failedDocumentNumbers, reporter);
241            }
242            return result;
243        }
244        
245        protected boolean sendDocumentIntoWorkflow(MaintenanceDocument readyTransientDoc, List<String> routedDocumentNumbers, 
246                List<String> failedDocumentNumbers, CustomerLoadFileResult reporter) {
247            boolean result = true;
248            
249            String customerName = ((Customer) readyTransientDoc.getNewMaintainableObject().getBusinessObject()).getCustomerName();
250            
251            //  create a real workflow document
252            MaintenanceDocument realMaintDoc;
253            try {
254                realMaintDoc = (MaintenanceDocument) docService.getNewDocument(getCustomerMaintenanceDocumentTypeName());
255            }
256            catch (WorkflowException e) {
257                LOG.error("WorkflowException occurred while trying to create a new MaintenanceDocument.", e);
258                throw new RuntimeException("WorkflowException occurred while trying to create a new MaintenanceDocument.", e);
259            }
260            
261            realMaintDoc.getNewMaintainableObject().setBusinessObject(readyTransientDoc.getNewMaintainableObject().getBusinessObject());
262            realMaintDoc.getOldMaintainableObject().setBusinessObject(readyTransientDoc.getOldMaintainableObject().getBusinessObject());
263            realMaintDoc.getNewMaintainableObject().setMaintenanceAction(readyTransientDoc.getNewMaintainableObject().getMaintenanceAction());
264            realMaintDoc.getDocumentHeader().setDocumentDescription(readyTransientDoc.getDocumentHeader().getDocumentDescription());
265            
266            Customer customer = (Customer) realMaintDoc.getNewMaintainableObject().getBusinessObject();
267            LOG.info("Routing Customer Maintenance document for [" + customer.getCustomerNumber() + "] " + customer.getCustomerName());
268            
269            try {
270                docService.routeDocument(realMaintDoc, "Routed Edit/Update Customer Maintenance from CustomerLoad Batch Process", null);
271            }
272            catch (WorkflowException e) {
273                LOG.error("WorkflowException occurred while trying to route a new MaintenanceDocument.", e);
274                reporter.addCustomerErrorMessage(customerName, "WorkflowException occurred while trying to route a new MaintenanceDocument: " + e.getMessage());
275                result = false;
276            }
277            
278            if (result == true) {
279                reporter.setCustomerSuccessResult(customerName);
280                reporter.setCustomerWorkflowDocId(customerName, realMaintDoc.getDocumentNumber());
281                routedDocumentNumbers.add(realMaintDoc.getDocumentNumber());
282            }
283            else {
284                reporter.setCustomerFailureResult(customerName);
285                failedDocumentNumbers.add(realMaintDoc.getDocumentNumber());
286            }
287            return result;
288        }
289        
290        protected String getCustomerMaintenanceDocumentTypeName() {
291            return "CUS";
292        }
293        
294        protected void addError(CustomerLoadBatchErrors batchErrors, String customerName, String propertyName, Class<?> propertyClass, String origValue, String description) {
295            batchErrors.addError(customerName, propertyName, propertyClass, origValue, description);
296        }
297        
298        protected void addBatchErrorsToGlobalVariables(CustomerLoadBatchErrors batchErrors) {
299            Set<String> errorMessages = batchErrors.getErrorStrings();
300            for (String errorMessage : errorMessages) {
301                GlobalVariables.getMessageMap().putError(KFSConstants.GLOBAL_ERRORS, 
302                        KFSKeyConstants.ERROR_BATCH_UPLOAD_SAVE, errorMessage);
303            }
304        }
305        
306        protected void addBatchErrorstoCustomerLoadResult(CustomerLoadBatchErrors batchErrors, CustomerLoadResult result) {
307            Set<String> errorMessages = batchErrors.getErrorStrings();
308            for (String errorMessage : errorMessages) {
309                result.addErrorMessage(errorMessage);
310            }
311        }
312        
313        /**
314         * 
315         * Accepts a file name and returns a byte-array of the file name contents, if possible.
316         * 
317         * Throws RuntimeExceptions if FileNotFound or IOExceptions occur.
318         * 
319         * @param fileName String containing valid path & filename (relative or absolute) of file to load.
320         * @return A Byte Array of the contents of the file.
321         */
322        protected byte[] safelyLoadFileBytes(String fileName) {
323            
324            InputStream fileContents;
325            byte[] fileByteContent;
326            try {
327                fileContents = new FileInputStream(fileName);
328            }
329            catch (FileNotFoundException e1) {
330                LOG.error("Batch file not found [" + fileName + "]. " + e1.getMessage());
331                throw new RuntimeException("Batch File not found [" + fileName + "]. " + e1.getMessage());
332            }
333            try {
334                fileByteContent = IOUtils.toByteArray(fileContents);
335            }
336            catch (IOException e1) {
337                LOG.error("IO Exception loading: [" + fileName + "]. " + e1.getMessage());
338                throw new RuntimeException("IO Exception loading: [" + fileName + "]. " + e1.getMessage());
339            }
340            return fileByteContent;
341        }
342        
343        /**
344         * The results of this method follow the same rules as the batch step result rules:
345         * 
346         * The execution of this method may have 3 possible outcomes:
347         * 
348         * 1. returns true, meaning that everything has succeeded, and dependent steps can continue running. No 
349         * errors should be added to GlobalVariables.getMessageMap().
350         * 
351         * 2. returns false, meaning that some (but not necessarily all) steps have succeeded, and dependent 
352         * steps can continue running.  Details can be found in the GlobalVariables.getMessageMap().
353         * 
354         * 3. throws an exception, meaning that the step has failed, that the rest of the steps in a job should 
355         * not be run, and that the job has failed.  There may be errors in the GlobalVariables.getMessageMap().
356         * 
357         * @see org.kuali.kfs.module.ar.batch.service.CustomerLoadService#validate(java.util.List)
358         */    
359        public boolean validate(List<CustomerDigesterVO> customerUploads) {
360            return validateAndPrepare(customerUploads, new ArrayList<MaintenanceDocument>(), true);
361        }
362        
363        /**
364         * @see org.kuali.kfs.module.ar.batch.service.CustomerLoadService#validateAndPrepare(java.util.List, java.util.List, boolean)
365         */
366        public boolean validateAndPrepare(List<CustomerDigesterVO> customerUploads, List<MaintenanceDocument> customerMaintDocs, boolean useGlobalErrorMap) {
367            return validateCustomers(customerUploads, customerMaintDocs, new CustomerLoadFileResult(), useGlobalErrorMap);
368        }
369        
370        /**
371         * 
372         * Validate the customers lists
373         * 
374         * @param customerUploads
375         * @param customerMaintDocs
376         * @param reporter
377         * @param useGlobalErrorMap
378         * @return
379         */
380        protected boolean validateCustomers(List<CustomerDigesterVO> customerUploads, List<MaintenanceDocument> customerMaintDocs, CustomerLoadFileResult reporter, boolean useGlobalErrorMap) {
381            
382            //  fail if empty or null list
383            if (customerUploads == null) {
384                LOG.error("Null list of Customer upload objects.  This should never happen.");
385                throw new IllegalArgumentException("Null list of Customer upload objects.  This should never happen.");
386            }
387            if (customerUploads.isEmpty()) {
388                reporter.addFileErrorMessage("An empty list of Customer uploads was passed in for validation.  As a result, no validation can be done.");
389                if (useGlobalErrorMap) {
390                    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." });
391                }
392                return false;
393            }
394    
395            boolean groupSucceeded = true;
396            boolean docSucceeded = true;
397            
398            //  check to make sure the input file doesnt have more docs than we allow in one batch file
399            String maxRecordsString = parameterService.getParameterValue(CustomerLoadStep.class, MAX_RECORDS_PARM_NAME);
400            if (StringUtils.isBlank(maxRecordsString) || !StringUtils.isNumeric(maxRecordsString)) {
401                LOG.error("Expected 'Max Records Per Document' System Parameter is not available.");
402                throw new RuntimeException("Expected 'Max Records Per Document' System Parameter is not available.");
403            }
404            Integer maxRecords = new Integer(maxRecordsString);
405            if (customerUploads.size() > maxRecords.intValue()) {
406                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.");
407                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.");
408                if (useGlobalErrorMap) {
409                    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." });
410                }
411                return false;
412            }
413            
414            //  we have to create one real maint doc for the whole thing to pass the maintainable.checkAuthorizationRestrictions 
415            MaintenanceDocument oneRealMaintDoc = null;
416            
417            Customer customer = null;
418            CustomerLoadBatchErrors fileBatchErrors = new CustomerLoadBatchErrors();
419            CustomerLoadBatchErrors customerBatchErrors;
420            String customerName;
421            if (adapter == null) adapter = new CustomerDigesterAdapter();
422            for (CustomerDigesterVO customerDigesterVO : customerUploads) {
423                
424                docSucceeded = true;
425                customerName = customerDigesterVO.getCustomerName();
426                
427                //  setup logging and reporting
428                LOG.info("Beginning conversion and validation for [" + customerName + "].");
429                reporter.addCustomerInfoMessage(customerName, "Beginning conversion and validation.");
430                CustomerLoadResult result = reporter.getCustomer(customerName);
431                customerBatchErrors = new CustomerLoadBatchErrors();
432                
433                //  convert the VO to a BO
434                LOG.info("Beginning conversion from VO to BO.");
435                customer = adapter.convert(customerDigesterVO, customerBatchErrors);
436                
437                //  if any errors were generated, add them to the GlobalVariables, and return false
438                if (!customerBatchErrors.isEmpty()) {
439                    LOG.info("The customer [" + customerName + "] was not processed due to errors in uploading and conversion.");
440                    customerBatchErrors.addError(customerName, "Global", Object.class, "", "This document was not processed due to errors in uploading and conversion.");
441                    addBatchErrorstoCustomerLoadResult(customerBatchErrors, result);
442                    reporter.setCustomerFailureResult(customerName);
443                    docSucceeded = false;
444                    groupSucceeded &= false;
445                    continue;
446                }
447    
448                //  determine whether this is an Update or a New
449                Customer existingCustomer = customerAlreadyExists(customer);
450                boolean isNew = (existingCustomer == null);
451                boolean isUpdate = !isNew;
452                
453                //  do some housekeeping
454                processBeforeValidating(customer, existingCustomer, isUpdate);
455                
456                //  create the transient maint doc
457                MaintenanceDocument transientMaintDoc = createTransientMaintDoc();
458                
459                //  make sure we have the one real maint doc (to steal its document id)
460                oneRealMaintDoc = createRealMaintDoc(oneRealMaintDoc);
461                
462                //  steal the doc id from the real doc
463                transientMaintDoc.setDocumentNumber(oneRealMaintDoc.getDocumentNumber());
464                transientMaintDoc.setDocumentHeader(oneRealMaintDoc.getDocumentHeader());
465                transientMaintDoc.getDocumentHeader().setDocumentDescription("AR Customer Load Batch Transient");
466                
467                //  set the old and new
468                transientMaintDoc.getNewMaintainableObject().setBusinessObject(customer);
469                transientMaintDoc.getOldMaintainableObject().setBusinessObject((existingCustomer == null ? new Customer() : existingCustomer ));
470    
471                //  set the maintainable actions, so isNew and isEdit on the maint doc return correct values
472                if (isNew) {
473                    transientMaintDoc.getNewMaintainableObject().setMaintenanceAction(KNSConstants.MAINTENANCE_NEW_ACTION);
474                }
475                else {
476                    transientMaintDoc.getNewMaintainableObject().setMaintenanceAction(KNSConstants.MAINTENANCE_EDIT_ACTION);
477                }
478    
479                //  report whether the customer is an Add or an Edit
480                if (isNew) {
481                    reporter.addCustomerInfoMessage(customerName, "Customer record batched is a New Customer.");
482                }
483                else {
484                    reporter.addCustomerInfoMessage(customerName, "Customer record batched is an Update to an existing Customer.");
485                }
486                
487                //  validate the batched customer
488                if (!validateSingle(transientMaintDoc, customerBatchErrors, customerName)) {
489                    groupSucceeded &= false;
490                    docSucceeded = false;
491                    reporter.setCustomerFailureResult(customerName);
492                }
493                addBatchErrorstoCustomerLoadResult(customerBatchErrors, result);
494                
495                //  if the doc succeeded then add it to the list to be routed, and report it as successful
496                if (docSucceeded) {
497                    customerMaintDocs.add(transientMaintDoc);
498                    Customer customer2 = (Customer) transientMaintDoc.getNewMaintainableObject().getBusinessObject();
499                    reporter.addCustomerInfoMessage(customerName, "Customer Number is: " + customer2.getCustomerNumber());
500                    reporter.addCustomerInfoMessage(customerName, "Customer Name is:   " + customer2.getCustomerName());
501                    reporter.setCustomerSuccessResult(customerName);
502                }
503                
504                fileBatchErrors.addAll(customerBatchErrors);
505            }
506            
507            //  put any errors back in global vars
508            if (useGlobalErrorMap) {
509                addBatchErrorsToGlobalVariables(fileBatchErrors);
510            }
511    
512            return groupSucceeded;
513        }
514    
515        /**
516         * pre-processing for existing and new customer 
517         * 
518         * @param customer
519         * @param existingCustomer
520         * @param isUpdate
521         */
522        protected void processBeforeValidating(Customer customer, Customer existingCustomer, boolean isUpdate) {
523    
524            //update specifics processing
525            if (isUpdate) {
526                //  if its has no customerNumber, then set it from existing record
527                if (StringUtils.isBlank(customer.getCustomerNumber())) {
528                    customer.setCustomerNumber(existingCustomer.getCustomerNumber());
529                }
530                
531                //  carry forward the version number
532                customer.setVersionNumber(existingCustomer.getVersionNumber());
533            
534                //  don't let the batch zero out certain key fields on an update
535                dontBlankOutFieldsOnUpdate(customer, existingCustomer, "customerTypeCode");
536                dontBlankOutFieldsOnUpdate(customer, existingCustomer, "customerTaxTypeCode");
537                dontBlankOutFieldsOnUpdate(customer, existingCustomer, "customerTaxNbr");
538                dontBlankOutFieldsOnUpdate(customer, existingCustomer, "customerCreditLimitAmount");
539                dontBlankOutFieldsOnUpdate(customer, existingCustomer, "customerCreditApprovedByName");
540                dontBlankOutFieldsOnUpdate(customer, existingCustomer, "customerParentCompanyNumber");
541                dontBlankOutFieldsOnUpdate(customer, existingCustomer, "customerPhoneNumber");
542                dontBlankOutFieldsOnUpdate(customer, existingCustomer, "customer800PhoneNumber");
543                dontBlankOutFieldsOnUpdate(customer, existingCustomer, "customerContactName");
544                dontBlankOutFieldsOnUpdate(customer, existingCustomer, "customerContactPhoneNumber");
545                dontBlankOutFieldsOnUpdate(customer, existingCustomer, "customerFaxNumber");
546                dontBlankOutFieldsOnUpdate(customer, existingCustomer, "customerBirthDate");
547            }
548            
549            //  upper case important fields
550            upperCaseKeyFields(customer);
551            
552            //NOTE: What's the reason for determining primary address?? address isn't used afterward
553            //  determine whether the batch has a primary address, and which one it is
554            boolean batchHasPrimaryAddress = false;
555            CustomerAddress batchPrimaryAddress = null;
556            for (CustomerAddress address : customer.getCustomerAddresses()) {
557                if (ArKeyConstants.CustomerConstants.CUSTOMER_ADDRESS_TYPE_CODE_PRIMARY.equalsIgnoreCase(address.getCustomerAddressTypeCode())) {
558                    batchHasPrimaryAddress = true;
559                    batchPrimaryAddress = address;
560                }
561            }
562    
563            //  if its an update, merge the address records (ie, only add or update, dont remove all addresses not imported).
564            if (isUpdate) {
565                boolean addressInBatchCustomer = false;
566                List<CustomerAddress> newCusomterAddresses = customer.getCustomerAddresses();
567                
568                // populate a stub address list (with empty addresses) base on the new customer address list size 
569                List<CustomerAddress> stubAddresses = new ArrayList<CustomerAddress>();
570                for (CustomerAddress batchAddress : newCusomterAddresses) {
571                    stubAddresses.add(new CustomerAddress());
572                }
573                
574                for (CustomerAddress existingAddress : existingCustomer.getCustomerAddresses()) {
575                    addressInBatchCustomer = false;
576                    for (CustomerAddress batchAddress : newCusomterAddresses) {
577                        if (!addressInBatchCustomer && existingAddress.compareTo(batchAddress) == 0) {
578                            addressInBatchCustomer = true;
579                        }
580                    }
581                    
582                    if (!addressInBatchCustomer) {
583                        
584                        //clone the address to avoid changing the existingAddress's type code
585                        CustomerAddress clonedExistingAddress = cloneCustomerAddress(existingAddress);
586                        //  make sure we don't add a second Primary address, if the batch specifies a primary address, it wins
587                        if (batchHasPrimaryAddress && ArKeyConstants.CustomerConstants.CUSTOMER_ADDRESS_TYPE_CODE_PRIMARY.equalsIgnoreCase(clonedExistingAddress.getCustomerAddressTypeCode())) {
588                            clonedExistingAddress.setCustomerAddressTypeCode(ArKeyConstants.CustomerConstants.CUSTOMER_ADDRESS_TYPE_CODE_ALTERNATE);
589                        }
590                        customer.getCustomerAddresses().add(clonedExistingAddress);
591                    }else{
592                        //found a address already in batch, remove one stub address from the list
593                        stubAddresses.remove(0);
594                    }
595                }
596                
597                //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
598                stubAddresses.addAll(existingCustomer.getCustomerAddresses());
599                // reset existing customer's address to the stub address list
600                existingCustomer.setCustomerAddresses(stubAddresses);
601            }
602            
603            //  set parent customer number to null if blank (otherwise foreign key rule fails)
604            if (StringUtils.isBlank(customer.getCustomerParentCompanyNumber())) {
605                customer.setCustomerParentCompanyNumber(null);
606            }
607            
608        }
609        
610        /**
611         * Clone the address object
612         * 
613         * @param address
614         * @return
615         */
616        private CustomerAddress cloneCustomerAddress(CustomerAddress address) {
617            CustomerAddress clonedAddress = null;
618            try {
619                clonedAddress = (CustomerAddress) BeanUtils.cloneBean(address);
620            }
621            catch (Exception ex) {
622                LOG.error("Unable to clone address [" + address + "]", ex);
623            }
624            return clonedAddress;
625        }
626    
627        protected void upperCaseKeyFields(Customer customer) {
628            
629            //  customer name
630            if (StringUtils.isNotBlank(customer.getCustomerName())) { 
631                customer.setCustomerName(customer.getCustomerName().toUpperCase());
632            }
633            
634            //  customer number
635            if (StringUtils.isNotBlank(customer.getCustomerNumber())) { 
636                customer.setCustomerNumber(customer.getCustomerNumber().toUpperCase());
637            }
638            
639            //  parent company number
640            if (StringUtils.isNotBlank(customer.getCustomerParentCompanyNumber())) { 
641                customer.setCustomerParentCompanyNumber(customer.getCustomerParentCompanyNumber().toUpperCase());
642            }
643            
644            //  customer tax type code
645            if (StringUtils.isNotBlank(customer.getCustomerTaxTypeCode())) { 
646                customer.setCustomerTaxTypeCode(customer.getCustomerTaxTypeCode().toUpperCase());
647            }
648            
649            //  customer tax number
650            if (StringUtils.isNotBlank(customer.getCustomerTaxNbr())) { 
651                customer.setCustomerTaxNbr(customer.getCustomerTaxNbr().toUpperCase());
652            }
653            
654            //  customer contact name
655            if (StringUtils.isNotBlank(customer.getCustomerContactName())) { 
656                customer.setCustomerContactName(customer.getCustomerContactName().toUpperCase());
657            }
658            
659            //  customer credit approved by name
660            if (StringUtils.isNotBlank(customer.getCustomerCreditApprovedByName())) { 
661                customer.setCustomerCreditApprovedByName(customer.getCustomerCreditApprovedByName().toUpperCase());
662            }
663            
664            //  customer email address
665            if (StringUtils.isNotBlank(customer.getCustomerEmailAddress())) { 
666                customer.setCustomerEmailAddress(customer.getCustomerEmailAddress().toUpperCase());
667            }
668            
669            for (CustomerAddress address : customer.getCustomerAddresses()) {
670                
671                if (address == null) continue;
672                
673                //  customer number
674                if (StringUtils.isNotBlank(address.getCustomerNumber())) {
675                    address.setCustomerNumber(address.getCustomerNumber().toUpperCase());
676                }
677                
678                //  customer address name
679                if (StringUtils.isNotBlank(address.getCustomerAddressName())) {
680                    address.setCustomerAddressName(address.getCustomerAddressName().toUpperCase());
681                }
682                
683                //  customerLine1StreetAddress
684                if (StringUtils.isNotBlank(address.getCustomerLine1StreetAddress())) {
685                    address.setCustomerLine1StreetAddress(address.getCustomerLine1StreetAddress().toUpperCase());
686                }
687                
688                //  customerLine2StreetAddress
689                if (StringUtils.isNotBlank(address.getCustomerLine2StreetAddress())) {
690                    address.setCustomerLine2StreetAddress(address.getCustomerLine2StreetAddress().toUpperCase());
691                }
692                
693                //  customerCityName
694                if (StringUtils.isNotBlank(address.getCustomerCityName())) {
695                    address.setCustomerCityName(address.getCustomerCityName().toUpperCase());
696                }
697                
698                //  customerStateCode
699                if (StringUtils.isNotBlank(address.getCustomerStateCode())) {
700                    address.setCustomerStateCode(address.getCustomerStateCode().toUpperCase());
701                }
702                
703                //  customerZipCode
704                if (StringUtils.isNotBlank(address.getCustomerZipCode())) {
705                    address.setCustomerZipCode(address.getCustomerZipCode().toUpperCase());
706                }
707                
708                //  customerCountryCode
709                if (StringUtils.isNotBlank(address.getCustomerNumber())) {
710                    address.setCustomerNumber(address.getCustomerNumber().toUpperCase());
711                }
712                
713                //  customerAddressInternationalProvinceName
714                if (StringUtils.isNotBlank(address.getCustomerAddressInternationalProvinceName())) {
715                    address.setCustomerAddressInternationalProvinceName(address.getCustomerAddressInternationalProvinceName().toUpperCase());
716                }
717                
718                //  customerInternationalMailCode
719                if (StringUtils.isNotBlank(address.getCustomerInternationalMailCode())) {
720                    address.setCustomerInternationalMailCode(address.getCustomerInternationalMailCode().toUpperCase());
721                }
722                
723                //  customerEmailAddress
724                if (StringUtils.isNotBlank(address.getCustomerEmailAddress())) {
725                    address.setCustomerEmailAddress(address.getCustomerEmailAddress().toUpperCase());
726                }
727                
728                //  customerAddressTypeCode
729                if (StringUtils.isNotBlank(address.getCustomerAddressTypeCode())) {
730                    address.setCustomerAddressTypeCode(address.getCustomerAddressTypeCode().toUpperCase());
731                }
732                
733            }
734        }
735        
736        /**
737         * 
738         * This messy thing attempts to compare a property on the batch customer (new) and existing customer, and if 
739         * the new is blank, but the old is there, to overwrite the new-value with the old-value, thus preventing 
740         * batch uploads from blanking out certain fields.
741         * 
742         * @param batchCustomer
743         * @param existingCustomer
744         * @param propertyName
745         */
746        protected void dontBlankOutFieldsOnUpdate(Customer batchCustomer, Customer existingCustomer, String propertyName) {
747            String batchValue;
748            String existingValue;
749            Class<?> propertyClass = null;
750            
751            //  try to retrieve the property type to see if it exists at all
752            try {
753                propertyClass = PropertyUtils.getPropertyType(batchCustomer, propertyName);
754            }
755            catch (Exception e) {
756                throw new RuntimeException("Could not access properties on the Customer object.", e);
757            }
758            
759            //  if the property doesnt exist, then throw an exception
760            if (propertyClass == null) {
761                throw new IllegalArgumentException("The propertyName specified [" + propertyName + "] doesnt exist on the Customer object.");
762            }
763            
764            //  get the String values of both batch and existing, to compare
765            try {
766                batchValue = BeanUtils.getSimpleProperty(batchCustomer, propertyName);
767                existingValue = BeanUtils.getSimpleProperty(existingCustomer, propertyName);
768            }
769            catch (Exception e) {
770                throw new RuntimeException("Could not access properties on the Customer object.", e);
771            }
772            
773            //  if the existing is non-blank, and the new is blank, then over-write the new with the existing value
774            if (StringUtils.isBlank(batchValue) && StringUtils.isNotBlank(existingValue)) {
775    
776                //  get the real typed value, and then try to set the property value 
777                try {
778                    Object typedValue = PropertyUtils.getProperty(existingCustomer, propertyName);
779                    BeanUtils.setProperty(batchCustomer, propertyName, typedValue);
780                }
781                catch (Exception e) {
782                    throw new RuntimeException("Could not set properties on the Customer object.", e);
783                }
784            }
785        }
786        
787        protected boolean validateSingle(MaintenanceDocument maintDoc, CustomerLoadBatchErrors batchErrors, String customerName) {
788            boolean result = true;
789            
790            //  get an instance of the business rule 
791            CustomerRule rule = new CustomerRule();
792            
793            //  run the business rules
794            result &= rule.processRouteDocument(maintDoc);
795            
796            extractGlobalVariableErrors(batchErrors, customerName);
797            
798            return result;
799        }
800        
801        protected boolean extractGlobalVariableErrors(CustomerLoadBatchErrors batchErrors, String customerName) {
802            boolean result = true;
803            
804            MessageMap errorMap = GlobalVariables.getMessageMap();
805    
806            Set<String> errorKeys = errorMap.keySet();
807            List<ErrorMessage> errorMessages = null;
808            Object[] messageParams;
809            String errorKeyString;
810            String errorString;
811            
812            for (String errorProperty : errorKeys) {
813                errorMessages = (List<ErrorMessage>) errorMap.get(errorProperty);
814                for (ErrorMessage errorMessage : errorMessages) {
815                    errorKeyString = configService.getPropertyString(errorMessage.getErrorKey()); 
816                    messageParams = errorMessage.getMessageParameters();
817                    
818                    // MessageFormat.format only seems to replace one 
819                    // per pass, so I just keep beating on it until all are gone.
820                    if (StringUtils.isBlank(errorKeyString)) {
821                        errorString = errorMessage.getErrorKey();
822                    }
823                    else {
824                        errorString = errorKeyString;
825                    }
826                    while (errorString.matches("^.*\\{\\d\\}.*$")) {
827                        errorString = MessageFormat.format(errorString, messageParams);
828                    }
829                    batchErrors.addError(customerName, errorProperty, Object.class, "", errorString);
830                    result = false;
831                }
832            }
833            
834            //  clear the stuff out of globalvars, as we need to reformat it and put it back
835            GlobalVariables.getMessageMap().clear();
836            return result;
837        }
838        
839        protected MaintenanceDocument createTransientMaintDoc() {
840            MaintenanceDocument maintDoc = new MaintenanceDocumentBase(getCustomerMaintenanceDocumentTypeName());
841            return maintDoc;
842        }
843        
844        protected MaintenanceDocument createRealMaintDoc(MaintenanceDocument document) {
845            if (document == null) {
846                try {
847                    document = (MaintenanceDocument) docService.getNewDocument(getCustomerMaintenanceDocumentTypeName());
848                }
849                catch (WorkflowException e) {
850                    throw new RuntimeException("WorkflowException thrown when trying to create new MaintenanceDocument.", e);
851                }
852            }
853            return document;
854        }
855        
856        /**
857         */
858        protected Customer customerAlreadyExists(Customer customer) {
859            
860            Customer existingCustomer = null;
861            
862            //  test existence by customerNumber, if one is passed in
863            if (StringUtils.isNotBlank(customer.getCustomerNumber())) {
864                existingCustomer = customerService.getByPrimaryKey(customer.getCustomerNumber());
865                if (existingCustomer != null) {
866                    return existingCustomer;
867                }
868            }
869            
870            //  test existence by TaxNumber, if one is passed in
871            if (StringUtils.isNotBlank(customer.getCustomerTaxNbr())) {
872                existingCustomer = customerService.getByTaxNumber(customer.getCustomerTaxNbr());
873                if (existingCustomer != null) {
874                    return existingCustomer;
875                }
876            }
877            
878            //  test existence by Customer Name.  this is looking for an exact match, so isnt terribly effective
879            if (StringUtils.isNotBlank(customer.getCustomerName())) {
880                existingCustomer = customerService.getCustomerByName(customer.getCustomerName());
881                if (existingCustomer != null) {
882                    return existingCustomer;
883                }
884            }
885            
886            //  return a null Customer if no matches were found
887            return existingCustomer;
888        }
889        
890        protected void writeReportPDF(List<CustomerLoadFileResult> fileResults) {
891            
892            if (fileResults.isEmpty()) {
893                return;
894            }
895            
896            //  setup the PDF business
897            Document pdfDoc = new Document(PageSize.LETTER, 54, 54, 72, 72);
898            getPdfWriter(pdfDoc);
899            pdfDoc.open();
900            
901            if (fileResults.isEmpty()) {
902                writeFileNameSectionTitle(pdfDoc, "NO DOCUMENTS FOUND TO PROCESS");
903                return;
904            }
905            
906            CustomerLoadResult result;
907            String customerResultLine;
908            for (CustomerLoadFileResult fileResult : fileResults) { 
909                
910                //  file name title
911                String fileNameOnly = fileResult.getFilename().toUpperCase();
912                fileNameOnly = fileNameOnly.substring(fileNameOnly.lastIndexOf("\\") + 1);
913                writeFileNameSectionTitle(pdfDoc, fileNameOnly);
914                
915                //  write any file-general messages
916                writeMessageEntryLines(pdfDoc, fileResult.getMessages());
917                
918                //  walk through each customer included in this file
919                for (String customerName : fileResult.getCustomerNames()) {
920                    result = fileResult.getCustomer(customerName);
921                    
922                    //  write the customer title
923                    writeCustomerSectionTitle(pdfDoc, customerName.toUpperCase());
924                    
925                    //  write a success/failure results line for this customer
926                    customerResultLine = result.getResultString() + (ResultCode.SUCCESS.equals(result.getResult()) ? WORKFLOW_DOC_ID_PREFIX + result.getWorkflowDocId() : "");
927                    writeCustomerSectionResult(pdfDoc, customerResultLine);
928                    
929                    //  write any customer messages 
930                    writeMessageEntryLines(pdfDoc, result.getMessages());
931                }
932            }
933            
934            pdfDoc.close();
935        }
936        
937        protected void writeFileNameSectionTitle(Document pdfDoc, String filenameLine) {
938            Font font = FontFactory.getFont(FontFactory.COURIER, 10, Font.BOLD);
939            
940            Paragraph paragraph = new Paragraph();
941            paragraph.setAlignment(Element.ALIGN_LEFT);
942            Chunk chunk = new Chunk(filenameLine, font);
943            chunk.setBackground(Color.LIGHT_GRAY, 5, 5, 5, 5);
944            paragraph.add(chunk);
945            
946            //  blank line
947            paragraph.add(new Chunk("", font));
948            
949            try {
950                pdfDoc.add(paragraph);
951            }
952            catch (DocumentException e) {
953                LOG.error("iText DocumentException thrown when trying to write content.", e);
954                throw new RuntimeException("iText DocumentException thrown when trying to write content.", e);
955            }
956        }
957        
958        protected void writeCustomerSectionTitle(Document pdfDoc, String customerNameLine) {
959            Font font = FontFactory.getFont(FontFactory.COURIER, 8, Font.BOLD + Font.UNDERLINE);
960            
961            Paragraph paragraph = new Paragraph();
962            paragraph.setAlignment(Element.ALIGN_LEFT);
963            paragraph.add(new Chunk(customerNameLine, font));
964    
965            //  blank line
966            paragraph.add(new Chunk("", font));
967            
968            try {
969                pdfDoc.add(paragraph);
970            }
971            catch (DocumentException e) {
972                LOG.error("iText DocumentException thrown when trying to write content.", e);
973                throw new RuntimeException("iText DocumentException thrown when trying to write content.", e);
974            }
975        }
976        
977        protected void writeCustomerSectionResult(Document pdfDoc, String resultLine) {
978            Font font = FontFactory.getFont(FontFactory.COURIER, 8, Font.BOLD);
979            
980            Paragraph paragraph = new Paragraph();
981            paragraph.setAlignment(Element.ALIGN_LEFT);
982            paragraph.add(new Chunk(resultLine, font));
983    
984            //  blank line
985            paragraph.add(new Chunk("", font));
986            
987            try {
988                pdfDoc.add(paragraph);
989            }
990            catch (DocumentException e) {
991                LOG.error("iText DocumentException thrown when trying to write content.", e);
992                throw new RuntimeException("iText DocumentException thrown when trying to write content.", e);
993            }
994        }
995        
996        protected void writeMessageEntryLines(Document pdfDoc, List<String[]> messageLines) {
997            Font font = FontFactory.getFont(FontFactory.COURIER, 8, Font.NORMAL);
998            
999            Paragraph paragraph;
1000            String messageEntry;
1001            for (String[] messageLine : messageLines) {
1002                paragraph = new Paragraph();
1003                paragraph.setAlignment(Element.ALIGN_LEFT);
1004                messageEntry = StringUtils.rightPad(messageLine[0], (12 - messageLine[0].length()), " ") + " - " + messageLine[1].toUpperCase();
1005                paragraph.add(new Chunk(messageEntry, font));
1006    
1007                //  blank line
1008                paragraph.add(new Chunk("", font));
1009                
1010                try {
1011                    pdfDoc.add(paragraph);
1012                }
1013                catch (DocumentException e) {
1014                    LOG.error("iText DocumentException thrown when trying to write content.", e);
1015                    throw new RuntimeException("iText DocumentException thrown when trying to write content.", e);
1016                }
1017            }
1018        }
1019        
1020        protected void getPdfWriter(Document pdfDoc) {
1021            
1022            String reportDropFolder = reportsDirectory + "/" + ArConstants.CustomerLoad.CUSTOMER_LOAD_REPORT_SUBFOLDER + "/";
1023            String fileName = ArConstants.CustomerLoad.BATCH_REPORT_BASENAME + "_" +  
1024                new SimpleDateFormat("yyyyMMdd_HHmmssSSS").format(dateTimeService.getCurrentDate()) + ".pdf";
1025           
1026            //  setup the writer
1027            File reportFile = new File(reportDropFolder + fileName);
1028            FileOutputStream fileOutStream;
1029            try {
1030                fileOutStream = new FileOutputStream(reportFile);
1031            }
1032            catch (IOException e) {
1033                LOG.error("IOException thrown when trying to open the FileOutputStream.", e);
1034                throw new RuntimeException("IOException thrown when trying to open the FileOutputStream.", e);
1035            }
1036            BufferedOutputStream buffOutStream = new BufferedOutputStream(fileOutStream);
1037            
1038            try {
1039                PdfWriter.getInstance(pdfDoc, buffOutStream);
1040            }
1041            catch (DocumentException e) {
1042                LOG.error("iText DocumentException thrown when trying to start a new instance of the PdfWriter.", e);
1043                throw new RuntimeException("iText DocumentException thrown when trying to start a new instance of the PdfWriter.", e);
1044            }
1045            
1046        }
1047    
1048        public void setBatchInputFileService(BatchInputFileService batchInputFileService) {
1049            this.batchInputFileService = batchInputFileService;
1050        }
1051    
1052        public void setCustomerService(CustomerService customerService) {
1053            this.customerService = customerService;
1054        }
1055    
1056        public void setConfigService(KualiConfigurationService configService) {
1057            this.configService = configService;
1058        }
1059    
1060        public void setDocService(DocumentService docService) {
1061            this.docService = docService;
1062        }
1063        
1064        public void setBatchInputFileType(BatchInputFileType batchInputFileType) {
1065            this.batchInputFileType = batchInputFileType;
1066        }
1067    
1068        public void setParameterService(ParameterService parameterService) {
1069            this.parameterService = parameterService;
1070        }
1071    
1072        public void setOrgService(OrganizationService orgService) {
1073            this.orgService = orgService;
1074        }
1075    
1076        public void setSysInfoService(SystemInformationService sysInfoService) {
1077            this.sysInfoService = sysInfoService;
1078        }
1079    
1080        public void setBoService(BusinessObjectService boService) {
1081            this.boService = boService;
1082        }
1083    
1084        public void setDateTimeService(DateTimeService dateTimeService) {
1085            this.dateTimeService = dateTimeService;
1086        }
1087    
1088        public void setReportsDirectory(String reportsDirectory) {
1089            this.reportsDirectory = reportsDirectory;
1090        }
1091        
1092    }
1093