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.SimpleDateFormat; 027 import java.util.ArrayList; 028 import java.util.List; 029 030 import org.apache.commons.io.IOUtils; 031 import org.apache.commons.lang.StringUtils; 032 import org.apache.xerces.dom.DocumentImpl; 033 import org.apache.xml.serialize.OutputFormat; 034 import org.apache.xml.serialize.XMLSerializer; 035 import org.kuali.kfs.module.ar.ArConstants; 036 import org.kuali.kfs.module.ar.batch.service.CustomerInvoiceWriteoffBatchService; 037 import org.kuali.kfs.module.ar.batch.vo.CustomerInvoiceWriteoffBatchVO; 038 import org.kuali.kfs.module.ar.businessobject.Customer; 039 import org.kuali.kfs.module.ar.document.service.CustomerInvoiceDocumentService; 040 import org.kuali.kfs.module.ar.document.service.CustomerInvoiceWriteoffDocumentService; 041 import org.kuali.kfs.module.ar.document.service.CustomerService; 042 import org.kuali.kfs.sys.batch.BatchInputFileType; 043 import org.kuali.kfs.sys.batch.service.BatchInputFileService; 044 import org.kuali.kfs.sys.context.SpringContext; 045 import org.kuali.kfs.sys.exception.ParseException; 046 import org.kuali.rice.kew.exception.WorkflowException; 047 import org.kuali.rice.kim.bo.Person; 048 import org.kuali.rice.kim.service.PersonService; 049 import org.kuali.rice.kns.service.DateTimeService; 050 import org.w3c.dom.Document; 051 import org.w3c.dom.Element; 052 import org.w3c.dom.Node; 053 054 import com.lowagie.text.Chunk; 055 import com.lowagie.text.DocumentException; 056 import com.lowagie.text.Font; 057 import com.lowagie.text.FontFactory; 058 import com.lowagie.text.PageSize; 059 import com.lowagie.text.Paragraph; 060 import com.lowagie.text.pdf.PdfWriter; 061 062 public class CustomerInvoiceWriteoffBatchServiceImpl implements CustomerInvoiceWriteoffBatchService { 063 private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(CustomerInvoiceWriteoffBatchServiceImpl.class); 064 065 private static final String XML_ROOT_ELEMENT_NAME = "invoiceWriteoffBatch"; 066 private static final String XML_BATCH_NAMESPACE = "http://www.kuali.org/kfs/ar/customerInvoiceWriteoffBatch"; 067 private static final String BATCH_FILE_KEY = "BATCH-FILE"; 068 private static final String WORKFLOW_DOC_ID_PREFIX = " - WITH WORKFLOW DOCID: "; 069 070 private PersonService<Person> personService; 071 private CustomerService customerService; 072 private CustomerInvoiceDocumentService invoiceDocumentService; 073 private DateTimeService dateTimeService; 074 private BatchInputFileService batchInputFileService; 075 private BatchInputFileType batchInputFileType; 076 private String reportsDirectory; 077 078 public CustomerInvoiceWriteoffBatchServiceImpl() {} 079 080 public boolean loadFiles() { 081 LOG.info("Beginning processing of all available files for AR Customer Invoice Writeoff Batch Documents."); 082 083 boolean result = true; 084 085 // create a list of the files to process 086 List<String> fileNamesToLoad = getListOfFilesToProcess(); 087 LOG.info("Found " + fileNamesToLoad.size() + " file(s) to process."); 088 boolean anyFilesFound = (fileNamesToLoad.size() > 0); 089 090 // create the pdf doc 091 com.lowagie.text.Document pdfdoc = null; 092 if (anyFilesFound) { 093 pdfdoc = getPdfDoc(); 094 } 095 096 // process each file in turn 097 List<String> processedFiles = new ArrayList<String>(); 098 for (String inputFileName : fileNamesToLoad) { 099 100 LOG.info("Beginning processing of filename: " + inputFileName + "."); 101 102 // setup the results reporting 103 writeFileNameSectionTitle(pdfdoc, inputFileName); 104 105 // load the file 106 boolean success = false; 107 try { 108 success = loadFile(inputFileName, pdfdoc); 109 } 110 catch (Exception e) { 111 LOG.error("An unhandled error occurred. " + e.getMessage()); 112 writeInvoiceSectionMessage(pdfdoc, "ERROR - Unhandled exception caught."); 113 writeInvoiceSectionMessage(pdfdoc, e.getMessage()); 114 } 115 result &= success; 116 117 // handle result 118 if (success) { 119 result &= true; 120 writeInvoiceSectionMessage(pdfdoc, "File successfully completed processing."); 121 processedFiles.add(inputFileName); 122 } 123 else { 124 writeInvoiceSectionMessage(pdfdoc, "File failed to process successfully."); 125 result &= false; 126 } 127 } 128 129 // if we've written anything, then spool it out to the file 130 if (pdfdoc != null) { 131 pdfdoc.close(); 132 } 133 134 // remove done files 135 removeDoneFiles(processedFiles); 136 137 return result; 138 } 139 140 /** 141 * Clears out associated .done files for the processed data files. 142 */ 143 protected void removeDoneFiles(List<String> dataFileNames) { 144 for (String dataFileName : dataFileNames) { 145 String doneFileName = doneFileName(dataFileName); 146 File doneFile = new File(doneFileName); 147 if (doneFile.exists()) { 148 doneFile.delete(); 149 } 150 } 151 } 152 153 public boolean loadFile(String fileName, com.lowagie.text.Document pdfdoc) { 154 155 boolean result = true; 156 157 // load up the file into a byte array 158 byte[] fileByteContent = safelyLoadFileBytes(fileName); 159 160 // parse the file against the XSD schema and load it into an object 161 LOG.info("Attempting to parse the file using Apache Digester."); 162 Object parsedObject = null; 163 try { 164 parsedObject = batchInputFileService.parse(batchInputFileType, fileByteContent); 165 } 166 catch (ParseException e) { 167 LOG.error("Error parsing batch file: " + e.getMessage()); 168 writeInvoiceSectionMessage(pdfdoc, "Error parsing batch file: " + e.getMessage()); 169 throw new ParseException(e.getMessage()); 170 } 171 172 // make sure we got the type we expected, then cast it 173 if (!(parsedObject instanceof CustomerInvoiceWriteoffBatchVO)) { 174 LOG.error("Parsed file was not of the expected type. Expected [" + CustomerInvoiceWriteoffBatchVO.class + "] but got [" + parsedObject.getClass() + "]."); 175 writeInvoiceSectionMessage(pdfdoc, "Parsed file was not of the expected type. Expected [" + CustomerInvoiceWriteoffBatchVO.class + "] but got [" + parsedObject.getClass() + "]."); 176 throw new RuntimeException("Parsed file was not of the expected type. Expected [" + CustomerInvoiceWriteoffBatchVO.class + "] but got [" + parsedObject.getClass() + "]."); 177 } 178 179 // convert to the real object type 180 CustomerInvoiceWriteoffBatchVO batchVO = (CustomerInvoiceWriteoffBatchVO) parsedObject; 181 182 LOG.info("Beginning validation and preparation of batch file."); 183 createCustomerInvoiceWriteoffDocumentsFromBatchVO(batchVO, pdfdoc); 184 185 return result; 186 } 187 188 /** 189 * 190 * @see org.kuali.kfs.module.ar.document.service.CustomerInvoiceWriteoffDocumentService#createCustomerInvoiceWriteoffDocumentsFromBatchVO(org.kuali.kfs.module.ar.batch.vo.CustomerInvoiceWriteoffBatchVO) 191 */ 192 protected void createCustomerInvoiceWriteoffDocumentsFromBatchVO(CustomerInvoiceWriteoffBatchVO batchVO, com.lowagie.text.Document pdfdoc) { 193 194 // retrieve the Person from the batch 195 Person person = getPersonService().getPersonByPrincipalName(batchVO.getSubmittedByPrincipalName()); 196 if (person == null) { 197 throw new RuntimeException("The Person who initiated this batch could not be retrieved."); 198 } 199 200 String createdOn = batchVO.getSubmittedOn(); 201 202 // retrieve the user note 203 String note = batchVO.getNote(); 204 205 // add submittedOn and submittedBy to the pdf 206 writeInvoiceSectionMessage(pdfdoc, "Batch Submitted By: " + person.getPrincipalName()); 207 writeInvoiceSectionMessage(pdfdoc, "Batch Submitted On: " + batchVO.getSubmittedOn()); 208 if (StringUtils.isNotBlank(note)) { 209 writeInvoiceSectionMessage(pdfdoc, "NOTE: " + note); 210 } 211 212 // create a new Invoice Writeoff document for each invoice number in the batch file 213 boolean succeeded = true; 214 boolean customerNoteIsSet = false; 215 String writeoffDocNumber = null; 216 for (String invoiceNumber : batchVO.getInvoiceNumbers()) { 217 218 // set the customer note 219 if (!customerNoteIsSet) { 220 Customer customer = invoiceDocumentService.getCustomerByInvoiceDocumentNumber(invoiceNumber); 221 if (customer != null) { 222 customerService.createCustomerNote(customer.getCustomerNumber(), note); 223 customerNoteIsSet = true; 224 } 225 } 226 227 // write the doc # we're trying to write off 228 writeInvoiceSectionTitle(pdfdoc, "INVOICE DOC#: " + invoiceNumber); 229 230 // attempt to create the writeoff document 231 succeeded = true; 232 writeoffDocNumber = null; 233 try { 234 writeoffDocNumber = getInvoiceWriteoffDocumentService().createCustomerInvoiceWriteoffDocument(person, invoiceNumber, note); 235 } 236 catch (WorkflowException e) { 237 succeeded = false; 238 writeInvoiceSectionMessage(pdfdoc, "ERROR - Failed to create and route the Invoice Writeoff Document."); 239 writeInvoiceSectionMessage(pdfdoc, "EXCEPTION DETAILS: " + e.getMessage()); 240 } 241 242 // write the successful information if we got it 243 if (succeeded) { 244 if (StringUtils.isNotBlank(writeoffDocNumber)) { 245 writeInvoiceSectionMessage(pdfdoc, "SUCCESS - Created new Invoice Writeoff Document #" + writeoffDocNumber); 246 } 247 else { 248 writeInvoiceSectionMessage(pdfdoc, "FAILURE - No error occurred, but a new Invoice Writeoff Document number was not created. Check the logs."); 249 } 250 } 251 } 252 } 253 254 /** 255 * 256 * Accepts a file name and returns a byte-array of the file name contents, if possible. 257 * 258 * Throws RuntimeExceptions if FileNotFound or IOExceptions occur. 259 * 260 * @param fileName String containing valid path & filename (relative or absolute) of file to load. 261 * @return A Byte Array of the contents of the file. 262 */ 263 protected byte[] safelyLoadFileBytes(String fileName) { 264 265 InputStream fileContents; 266 byte[] fileByteContent; 267 try { 268 fileContents = new FileInputStream(fileName); 269 } 270 catch (FileNotFoundException e1) { 271 LOG.error("Batch file not found [" + fileName + "]. " + e1.getMessage()); 272 throw new RuntimeException("Batch File not found [" + fileName + "]. " + e1.getMessage()); 273 } 274 try { 275 fileByteContent = IOUtils.toByteArray(fileContents); 276 } 277 catch (IOException e1) { 278 LOG.error("IO Exception loading: [" + fileName + "]. " + e1.getMessage()); 279 throw new RuntimeException("IO Exception loading: [" + fileName + "]. " + e1.getMessage()); 280 } 281 return fileByteContent; 282 } 283 284 protected List<String> getListOfFilesToProcess() { 285 286 // create a list of the files to process 287 List<String> fileNamesToLoad = batchInputFileService.listInputFileNamesWithDoneFile(batchInputFileType); 288 289 if (fileNamesToLoad == null) { 290 LOG.error("BatchInputFileService.listInputFileNamesWithDoneFile(" + 291 batchInputFileType.getFileTypeIdentifer() + ") returned NULL which should never happen."); 292 throw new RuntimeException("BatchInputFileService.listInputFileNamesWithDoneFile(" + 293 batchInputFileType.getFileTypeIdentifer() + ") returned NULL which should never happen."); 294 } 295 296 // filenames returned should never be blank/empty/null 297 for (String inputFileName : fileNamesToLoad) { 298 if (StringUtils.isBlank(inputFileName)) { 299 LOG.error("One of the file names returned as ready to process [" + inputFileName + 300 "] was blank. This should not happen, so throwing an error to investigate."); 301 throw new RuntimeException("One of the file names returned as ready to process [" + inputFileName + 302 "] was blank. This should not happen, so throwing an error to investigate."); 303 } 304 } 305 306 return fileNamesToLoad; 307 } 308 309 protected com.lowagie.text.Document getPdfDoc() { 310 311 String reportDropFolder = reportsDirectory + "/" + ArConstants.CustomerInvoiceWriteoff.CUSTOMER_INVOICE_WRITEOFF_REPORT_SUBFOLDER + "/"; 312 String fileName = ArConstants.CustomerInvoiceWriteoff.BATCH_REPORT_BASENAME + "_" + 313 new SimpleDateFormat("yyyyMMdd_HHmmssSSS").format(dateTimeService.getCurrentDate()) + ".pdf"; 314 315 // setup the writer 316 File reportFile = new File(reportDropFolder + fileName); 317 FileOutputStream fileOutStream; 318 try { 319 fileOutStream = new FileOutputStream(reportFile); 320 } 321 catch (IOException e) { 322 LOG.error("IOException thrown when trying to open the FileOutputStream.", e); 323 throw new RuntimeException("IOException thrown when trying to open the FileOutputStream.", e); 324 } 325 BufferedOutputStream buffOutStream = new BufferedOutputStream(fileOutStream); 326 327 com.lowagie.text.Document pdfdoc = new com.lowagie.text.Document(PageSize.LETTER, 54, 54, 72, 72); 328 try { 329 PdfWriter.getInstance(pdfdoc, buffOutStream); 330 } 331 catch (DocumentException e) { 332 LOG.error("iText DocumentException thrown when trying to start a new instance of the PdfWriter.", e); 333 throw new RuntimeException("iText DocumentException thrown when trying to start a new instance of the PdfWriter.", e); 334 } 335 336 pdfdoc.open(); 337 338 return pdfdoc; 339 } 340 341 protected void writeFileNameSectionTitle(com.lowagie.text.Document pdfDoc, String filenameLine) { 342 Font font = FontFactory.getFont(FontFactory.COURIER, 10, Font.BOLD); 343 344 // file name title, get title only, on windows & unix platforms 345 String fileNameOnly = filenameLine.toUpperCase(); 346 int indexOfSlashes = fileNameOnly.lastIndexOf("\\"); 347 if (indexOfSlashes < fileNameOnly.length()) fileNameOnly = fileNameOnly.substring(indexOfSlashes + 1); 348 indexOfSlashes = fileNameOnly.lastIndexOf("/"); 349 if (indexOfSlashes < fileNameOnly.length()) fileNameOnly = fileNameOnly.substring(indexOfSlashes + 1); 350 351 Paragraph paragraph = new Paragraph(); 352 paragraph.setAlignment(com.lowagie.text.Element.ALIGN_LEFT); 353 Chunk chunk = new Chunk(fileNameOnly, font); 354 chunk.setBackground(Color.LIGHT_GRAY, 5, 5, 5, 5); 355 paragraph.add(chunk); 356 357 // blank line 358 paragraph.add(new Chunk("", font)); 359 360 try { 361 pdfDoc.add(paragraph); 362 } 363 catch (DocumentException e) { 364 LOG.error("iText DocumentException thrown when trying to write content.", e); 365 throw new RuntimeException("iText DocumentException thrown when trying to write content.", e); 366 } 367 } 368 369 protected void writeInvoiceSectionTitle(com.lowagie.text.Document pdfDoc, String customerNameLine) { 370 Font font = FontFactory.getFont(FontFactory.COURIER, 8, Font.BOLD + Font.UNDERLINE); 371 372 Paragraph paragraph = new Paragraph(); 373 paragraph.setAlignment(com.lowagie.text.Element.ALIGN_LEFT); 374 paragraph.add(new Chunk(customerNameLine, font)); 375 376 // blank line 377 paragraph.add(new Chunk("", font)); 378 379 try { 380 pdfDoc.add(paragraph); 381 } 382 catch (DocumentException e) { 383 LOG.error("iText DocumentException thrown when trying to write content.", e); 384 throw new RuntimeException("iText DocumentException thrown when trying to write content.", e); 385 } 386 } 387 388 protected void writeInvoiceSectionMessage(com.lowagie.text.Document pdfDoc, String resultLine) { 389 Font font = FontFactory.getFont(FontFactory.COURIER, 8, Font.NORMAL); 390 391 Paragraph paragraph = new Paragraph(); 392 paragraph.setAlignment(com.lowagie.text.Element.ALIGN_LEFT); 393 paragraph.add(new Chunk(resultLine, font)); 394 395 // blank line 396 paragraph.add(new Chunk("", font)); 397 398 try { 399 pdfDoc.add(paragraph); 400 } 401 catch (DocumentException e) { 402 LOG.error("iText DocumentException thrown when trying to write content.", e); 403 throw new RuntimeException("iText DocumentException thrown when trying to write content.", e); 404 } 405 } 406 407 /** 408 * 409 * @see org.kuali.kfs.module.ar.batch.service.CustomerInvoiceWriteoffBatchService#createBatchDrop(org.kuali.kfs.module.ar.batch.vo.CustomerInvoiceWriteoffBatchVO) 410 */ 411 public String createBatchDrop(Person person, CustomerInvoiceWriteoffBatchVO writeoffBatchVO) { 412 413 org.w3c.dom.Document xmldoc = transformVOtoXml(writeoffBatchVO); 414 415 String batchXmlFileName = dropXmlFile(person, xmldoc); 416 417 createDoneFile(batchXmlFileName); 418 419 return batchXmlFileName; 420 } 421 422 protected String getBatchXMLNamespace() { 423 return XML_BATCH_NAMESPACE; 424 } 425 426 protected String doneFileName(String filename) { 427 String fileNoExtension = filename.substring(0, filename.lastIndexOf(".")); 428 return fileNoExtension + ".done"; 429 } 430 431 protected void createDoneFile(String filename) { 432 String fileNoExtension = doneFileName(filename); 433 File doneFile = new File(fileNoExtension); 434 try { 435 doneFile.createNewFile(); 436 } 437 catch (IOException e) { 438 throw new RuntimeException("Exception while trying to create .done file.", e); 439 } 440 } 441 442 protected String getBatchFilePathAndName(Person person) { 443 444 String filename = batchInputFileType.getFileName(person.getPrincipalId(), "", ""); 445 446 String filepath = batchInputFileType.getDirectoryPath(); 447 if (!filepath.endsWith("/")) filepath = filepath + "/"; 448 449 String extension = batchInputFileType.getFileExtension(); 450 451 return filepath + filename + "." + extension; 452 } 453 454 protected String dropXmlFile(Person person, org.w3c.dom.Document xmldoc) { 455 456 // determine file paths and names 457 String filename = getBatchFilePathAndName(person); 458 459 // setup the file stream 460 FileOutputStream fos = null; 461 try { 462 fos = new FileOutputStream(filename); 463 } 464 catch (FileNotFoundException e) { 465 throw new RuntimeException("Could not find/create output file at: '" + filename + "'.", e); 466 } 467 468 // setup the output format 469 OutputFormat of = new OutputFormat("XML", "UTF-8", true); 470 of.setIndent(1); 471 of.setIndenting(true); 472 473 // setup the xml serializer and do the serialization 474 Element docElement = xmldoc.getDocumentElement(); 475 XMLSerializer serializer = new XMLSerializer(fos, of); 476 try { 477 serializer.asDOMSerializer(); 478 serializer.serialize(docElement); 479 } 480 catch (IOException e) { 481 throw new RuntimeException("Exception while serializing the DOM Document.", e); 482 } 483 484 // close the output stream 485 try { 486 fos.close(); 487 } 488 catch (IOException e) { 489 throw new RuntimeException("Exception while closing the FileOutputStream.", e); 490 } 491 492 return filename; 493 } 494 495 protected Document transformVOtoXml(CustomerInvoiceWriteoffBatchVO writeoffBatchVO) { 496 497 Document xmldoc = new DocumentImpl(); 498 Element e = null; 499 Element invoicesElement = null; 500 Node n = null; 501 502 Element root = xmldoc.createElementNS("http://www.kuali.org/kfs/ar/customer", XML_ROOT_ELEMENT_NAME); 503 root.setAttribute("xmlns", getBatchXMLNamespace()); 504 root.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance"); 505 506 // create submittedBy element 507 e = xmldoc.createElement("submittedByPrincipalId"); 508 n = xmldoc.createCDATASection(writeoffBatchVO.getSubmittedByPrincipalName()); 509 e.appendChild(n); 510 root.appendChild(e); 511 512 // create submittedOn element 513 e = xmldoc.createElement("submittedOn"); 514 n = xmldoc.createCDATASection(writeoffBatchVO.getSubmittedOn()); 515 e.appendChild(n); 516 root.appendChild(e); 517 518 // create note element 519 e = xmldoc.createElement("note"); 520 n = xmldoc.createCDATASection(writeoffBatchVO.getNote()); 521 e.appendChild(n); 522 root.appendChild(e); 523 524 // create invoices element and list of invoice child elements 525 invoicesElement = xmldoc.createElement("invoiceNumbers"); 526 for (String invoiceNumber : writeoffBatchVO.getInvoiceNumbers()) { 527 e = xmldoc.createElement("invoiceNumber"); 528 n = xmldoc.createCDATASection(invoiceNumber); 529 e.appendChild(n); 530 invoicesElement.appendChild(e); 531 } 532 root.appendChild(invoicesElement); 533 534 xmldoc.appendChild(root); 535 536 return xmldoc; 537 } 538 539 // this strange construct (rather than using setter injection) is here to eliminate a 540 // circular reference problem with Spring's eager init. 541 protected CustomerInvoiceWriteoffDocumentService getInvoiceWriteoffDocumentService() { 542 return SpringContext.getBean(CustomerInvoiceWriteoffDocumentService.class); 543 } 544 545 public void setDateTimeService(DateTimeService dateTimeService) { 546 this.dateTimeService = dateTimeService; 547 } 548 549 public void setBatchInputFileService(BatchInputFileService batchInputFileService) { 550 this.batchInputFileService = batchInputFileService; 551 } 552 553 public void setBatchInputFileType(BatchInputFileType batchInputFileType) { 554 this.batchInputFileType = batchInputFileType; 555 } 556 557 public void setReportsDirectory(String reportsDirectory) { 558 this.reportsDirectory = reportsDirectory; 559 } 560 561 /** 562 * @return Returns the personService. 563 */ 564 protected PersonService<Person> getPersonService() { 565 if(personService==null) 566 personService = SpringContext.getBean(PersonService.class); 567 return personService; 568 } 569 570 public void setCustomerService(CustomerService customerService) { 571 this.customerService = customerService; 572 } 573 574 public void setInvoiceDocumentService(CustomerInvoiceDocumentService invoiceDocumentService) { 575 this.invoiceDocumentService = invoiceDocumentService; 576 } 577 578 }