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