/* * The Kuali Financial System, a comprehensive financial management system for higher education. * * Copyright 2005-2014 The Kuali Foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.kuali.kfs.module.ar.batch.service.impl; import java.awt.Color; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.List; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.apache.xerces.dom.DocumentImpl; import org.apache.xml.serialize.OutputFormat; import org.apache.xml.serialize.XMLSerializer; import org.kuali.kfs.module.ar.ArConstants; import org.kuali.kfs.module.ar.ArPropertyConstants; import org.kuali.kfs.module.ar.batch.service.CustomerInvoiceWriteoffBatchService; import org.kuali.kfs.module.ar.batch.vo.CustomerInvoiceWriteoffBatchVO; import org.kuali.kfs.module.ar.businessobject.Customer; import org.kuali.kfs.module.ar.document.service.CustomerInvoiceDocumentService; import org.kuali.kfs.module.ar.document.service.CustomerInvoiceWriteoffDocumentService; import org.kuali.kfs.module.ar.document.service.CustomerService; import org.kuali.kfs.sys.batch.BatchInputFileType; import org.kuali.kfs.sys.batch.service.BatchInputFileService; import org.kuali.kfs.sys.context.SpringContext; import org.kuali.kfs.sys.exception.ParseException; import org.kuali.rice.core.api.datetime.DateTimeService; import org.kuali.rice.kew.api.exception.WorkflowException; import org.kuali.rice.kim.api.identity.Person; import org.kuali.rice.kim.api.identity.entity.Entity; import org.kuali.rice.kim.api.services.KimApiServiceLocator; import org.springframework.transaction.annotation.Transactional; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import com.lowagie.text.Chunk; import com.lowagie.text.DocumentException; import com.lowagie.text.Font; import com.lowagie.text.FontFactory; import com.lowagie.text.PageSize; import com.lowagie.text.Paragraph; import com.lowagie.text.pdf.PdfWriter; @Transactional public class CustomerInvoiceWriteoffBatchServiceImpl implements CustomerInvoiceWriteoffBatchService { private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(CustomerInvoiceWriteoffBatchServiceImpl.class); private static final String XML_ROOT_ELEMENT_NAME = "invoiceWriteoffBatch"; private static final String XML_BATCH_NAMESPACE = "http://www.kuali.org/kfs/ar/customerInvoiceWriteoffBatch"; private static final String BATCH_FILE_KEY = "BATCH-FILE"; private static final String WORKFLOW_DOC_ID_PREFIX = " - WITH WORKFLOW DOCID: "; private CustomerService customerService; private CustomerInvoiceDocumentService invoiceDocumentService; private DateTimeService dateTimeService; private BatchInputFileService batchInputFileService; private BatchInputFileType batchInputFileType; private String reportsDirectory; public CustomerInvoiceWriteoffBatchServiceImpl() {} @Override public boolean loadFiles() { LOG.info("Beginning processing of all available files for AR Customer Invoice Writeoff Batch Documents."); boolean result = true; // create a list of the files to process List<String> fileNamesToLoad = getListOfFilesToProcess(); LOG.info("Found " + fileNamesToLoad.size() + " file(s) to process."); boolean anyFilesFound = (fileNamesToLoad.size() > 0); // create the pdf doc com.lowagie.text.Document pdfdoc = null; // process each file in turn List<String> processedFiles; try { if (anyFilesFound) { pdfdoc = getPdfDoc(); } try { processedFiles = new ArrayList<String>(); for (String inputFileName : fileNamesToLoad) { LOG.info("Beginning processing of filename: " + inputFileName + "."); // setup the results reporting writeFileNameSectionTitle(pdfdoc, inputFileName); // load the file boolean success = false; try { success = loadFile(inputFileName, pdfdoc); } catch (Exception e) { LOG.error("An unhandled error occurred. " + e.getMessage()); writeInvoiceSectionMessage(pdfdoc, "ERROR - Unhandled exception caught."); writeInvoiceSectionMessage(pdfdoc, e.getMessage()); } result &= success; // handle result if (success) { result &= true; writeInvoiceSectionMessage(pdfdoc, "File successfully completed processing."); processedFiles.add(inputFileName); } else { writeInvoiceSectionMessage(pdfdoc, "File failed to process successfully."); result &= false; } } } finally { // if we've written anything, then spool it out to the file if (pdfdoc != null) { pdfdoc.close(); } } // remove done files removeDoneFiles(processedFiles); } catch (IOException | DocumentException ex) { throw new RuntimeException("Could not load customer invoice writeoff files", ex); } return result; } /** * Clears out associated .done files for the processed data files. */ protected void removeDoneFiles(List<String> dataFileNames) { for (String dataFileName : dataFileNames) { String doneFileName = doneFileName(dataFileName); File doneFile = new File(doneFileName); if (doneFile.exists()) { doneFile.delete(); } } } public boolean loadFile(String fileName, com.lowagie.text.Document pdfdoc) { boolean result = true; // load up the file into a byte array byte[] fileByteContent = safelyLoadFileBytes(fileName); // parse the file against the XSD schema and load it into an object LOG.info("Attempting to parse the file using Apache Digester."); Object parsedObject = null; try { parsedObject = batchInputFileService.parse(batchInputFileType, fileByteContent); } catch (ParseException e) { LOG.error("Error parsing batch file: " + e.getMessage()); writeInvoiceSectionMessage(pdfdoc, "Error parsing batch file: " + e.getMessage()); throw new ParseException(e.getMessage()); } // make sure we got the type we expected, then cast it if (!(parsedObject instanceof CustomerInvoiceWriteoffBatchVO)) { LOG.error("Parsed file was not of the expected type. Expected [" + CustomerInvoiceWriteoffBatchVO.class + "] but got [" + parsedObject.getClass() + "]."); writeInvoiceSectionMessage(pdfdoc, "Parsed file was not of the expected type. Expected [" + CustomerInvoiceWriteoffBatchVO.class + "] but got [" + parsedObject.getClass() + "]."); throw new RuntimeException("Parsed file was not of the expected type. Expected [" + CustomerInvoiceWriteoffBatchVO.class + "] but got [" + parsedObject.getClass() + "]."); } // convert to the real object type CustomerInvoiceWriteoffBatchVO batchVO = (CustomerInvoiceWriteoffBatchVO) parsedObject; LOG.info("Beginning validation and preparation of batch file."); createCustomerInvoiceWriteoffDocumentsFromBatchVO(batchVO, pdfdoc); return result; } /** * * @see org.kuali.kfs.module.ar.document.service.CustomerInvoiceWriteoffDocumentService#createCustomerInvoiceWriteoffDocumentsFromBatchVO(org.kuali.kfs.module.ar.batch.vo.CustomerInvoiceWriteoffBatchVO) */ protected void createCustomerInvoiceWriteoffDocumentsFromBatchVO(CustomerInvoiceWriteoffBatchVO batchVO, com.lowagie.text.Document pdfdoc) { // retrieve the Person from the batch Entity entity = KimApiServiceLocator.getIdentityService().getEntityByPrincipalName(batchVO.getSubmittedByPrincipalName()); if (entity == null) { throw new RuntimeException("The Person who initiated this batch could not be retrieved."); } String createdOn = batchVO.getSubmittedOn(); // retrieve the user note String note = batchVO.getNote(); // add submittedOn and submittedBy to the pdf writeInvoiceSectionMessage(pdfdoc, "Batch Submitted By: " + batchVO.getSubmittedByPrincipalName()); writeInvoiceSectionMessage(pdfdoc, "Batch Submitted On: " + batchVO.getSubmittedOn()); if (StringUtils.isNotBlank(note)) { writeInvoiceSectionMessage(pdfdoc, "NOTE: " + note); } // create a new Invoice Writeoff document for each invoice number in the batch file boolean succeeded = true; boolean customerNoteIsSet = false; String writeoffDocNumber = null; for (String invoiceNumber : batchVO.getInvoiceNumbers()) { // set the customer note if (!customerNoteIsSet) { Customer customer = invoiceDocumentService.getCustomerByInvoiceDocumentNumber(invoiceNumber); if (customer != null) { customerService.createCustomerNote(customer.getCustomerNumber(), note); customerNoteIsSet = true; } } // write the doc # we're trying to write off writeInvoiceSectionTitle(pdfdoc, "INVOICE DOC#: " + invoiceNumber); // attempt to create the writeoff document succeeded = true; writeoffDocNumber = null; try { writeoffDocNumber = getInvoiceWriteoffDocumentService().createCustomerInvoiceWriteoffDocument(invoiceNumber, note); } catch (WorkflowException e) { succeeded = false; writeInvoiceSectionMessage(pdfdoc, "ERROR - Failed to create and route the Invoice Writeoff Document."); writeInvoiceSectionMessage(pdfdoc, "EXCEPTION DETAILS: " + e.getMessage()); } // write the successful information if we got it if (succeeded) { if (StringUtils.isNotBlank(writeoffDocNumber)) { writeInvoiceSectionMessage(pdfdoc, "SUCCESS - Created new Invoice Writeoff Document #" + writeoffDocNumber); } else { writeInvoiceSectionMessage(pdfdoc, "FAILURE - No error occurred, but a new Invoice Writeoff Document number was not created. Check the logs."); } } } } /** * * Accepts a file name and returns a byte-array of the file name contents, if possible. * * Throws RuntimeExceptions if FileNotFound or IOExceptions occur. * * @param fileName String containing valid path & filename (relative or absolute) of file to load. * @return A Byte Array of the contents of the file. */ protected byte[] safelyLoadFileBytes(String fileName) { InputStream fileContents; byte[] fileByteContent; try { fileContents = new FileInputStream(fileName); } catch (FileNotFoundException e1) { LOG.error("Batch file not found [" + fileName + "]. " + e1.getMessage()); throw new RuntimeException("Batch File not found [" + fileName + "]. " + e1.getMessage()); } try { fileByteContent = IOUtils.toByteArray(fileContents); } catch (IOException e1) { LOG.error("IO Exception loading: [" + fileName + "]. " + e1.getMessage()); throw new RuntimeException("IO Exception loading: [" + fileName + "]. " + e1.getMessage()); } return fileByteContent; } protected List<String> getListOfFilesToProcess() { // create a list of the files to process List<String> fileNamesToLoad = batchInputFileService.listInputFileNamesWithDoneFile(batchInputFileType); if (fileNamesToLoad == null) { LOG.error("BatchInputFileService.listInputFileNamesWithDoneFile(" + batchInputFileType.getFileTypeIdentifer() + ") returned NULL which should never happen."); throw new RuntimeException("BatchInputFileService.listInputFileNamesWithDoneFile(" + batchInputFileType.getFileTypeIdentifer() + ") returned NULL which should never happen."); } // filenames returned should never be blank/empty/null for (String inputFileName : fileNamesToLoad) { if (StringUtils.isBlank(inputFileName)) { LOG.error("One of the file names returned as ready to process [" + inputFileName + "] was blank. This should not happen, so throwing an error to investigate."); throw new RuntimeException("One of the file names returned as ready to process [" + inputFileName + "] was blank. This should not happen, so throwing an error to investigate."); } } return fileNamesToLoad; } protected com.lowagie.text.Document getPdfDoc() throws IOException, DocumentException { String reportDropFolder = reportsDirectory + "/" + ArConstants.CustomerInvoiceWriteoff.CUSTOMER_INVOICE_WRITEOFF_REPORT_SUBFOLDER + "/"; String fileName = ArConstants.CustomerInvoiceWriteoff.BATCH_REPORT_BASENAME + "_" + new SimpleDateFormat("yyyyMMdd_HHmmssSSS").format(dateTimeService.getCurrentDate()) + ".pdf"; // setup the writer File reportFile = new File(reportDropFolder + fileName); FileOutputStream fileOutStream; fileOutStream = new FileOutputStream(reportFile); BufferedOutputStream buffOutStream = new BufferedOutputStream(fileOutStream); com.lowagie.text.Document pdfdoc = new com.lowagie.text.Document(PageSize.LETTER, 54, 54, 72, 72); PdfWriter.getInstance(pdfdoc, buffOutStream); pdfdoc.open(); return pdfdoc; } protected void writeFileNameSectionTitle(com.lowagie.text.Document pdfDoc, String filenameLine) { Font font = FontFactory.getFont(FontFactory.COURIER, 10, Font.BOLD); // file name title, get title only, on windows & unix platforms String fileNameOnly = filenameLine.toUpperCase(); int indexOfSlashes = fileNameOnly.lastIndexOf("\\"); if (indexOfSlashes < fileNameOnly.length()) { fileNameOnly = fileNameOnly.substring(indexOfSlashes + 1); } indexOfSlashes = fileNameOnly.lastIndexOf("/"); if (indexOfSlashes < fileNameOnly.length()) { fileNameOnly = fileNameOnly.substring(indexOfSlashes + 1); } Paragraph paragraph = new Paragraph(); paragraph.setAlignment(com.lowagie.text.Element.ALIGN_LEFT); Chunk chunk = new Chunk(fileNameOnly, font); chunk.setBackground(Color.LIGHT_GRAY, 5, 5, 5, 5); paragraph.add(chunk); // blank line paragraph.add(new Chunk("", font)); try { pdfDoc.add(paragraph); } catch (DocumentException e) { LOG.error("iText DocumentException thrown when trying to write content.", e); throw new RuntimeException("iText DocumentException thrown when trying to write content.", e); } } protected void writeInvoiceSectionTitle(com.lowagie.text.Document pdfDoc, String customerNameLine) { Font font = FontFactory.getFont(FontFactory.COURIER, 8, Font.BOLD + Font.UNDERLINE); Paragraph paragraph = new Paragraph(); paragraph.setAlignment(com.lowagie.text.Element.ALIGN_LEFT); paragraph.add(new Chunk(customerNameLine, font)); // blank line paragraph.add(new Chunk("", font)); try { pdfDoc.add(paragraph); } catch (DocumentException e) { LOG.error("iText DocumentException thrown when trying to write content.", e); throw new RuntimeException("iText DocumentException thrown when trying to write content.", e); } } protected void writeInvoiceSectionMessage(com.lowagie.text.Document pdfDoc, String resultLine) { Font font = FontFactory.getFont(FontFactory.COURIER, 8, Font.NORMAL); Paragraph paragraph = new Paragraph(); paragraph.setAlignment(com.lowagie.text.Element.ALIGN_LEFT); paragraph.add(new Chunk(resultLine, font)); // blank line paragraph.add(new Chunk("", font)); try { pdfDoc.add(paragraph); } catch (DocumentException e) { LOG.error("iText DocumentException thrown when trying to write content.", e); throw new RuntimeException("iText DocumentException thrown when trying to write content.", e); } } /** * * @see org.kuali.kfs.module.ar.batch.service.CustomerInvoiceWriteoffBatchService#createBatchDrop(org.kuali.kfs.module.ar.batch.vo.CustomerInvoiceWriteoffBatchVO) */ @Override public String createBatchDrop(Person person, CustomerInvoiceWriteoffBatchVO writeoffBatchVO) { org.w3c.dom.Document xmldoc = transformVOtoXml(writeoffBatchVO); String batchXmlFileName = dropXmlFile(person, xmldoc); createDoneFile(batchXmlFileName); return batchXmlFileName; } protected String getBatchXMLNamespace() { return XML_BATCH_NAMESPACE; } protected String doneFileName(String filename) { String fileNoExtension = filename.substring(0, filename.lastIndexOf(".")); return fileNoExtension + ".done"; } protected void createDoneFile(String filename) { String fileNoExtension = doneFileName(filename); File doneFile = new File(fileNoExtension); try { doneFile.createNewFile(); } catch (IOException e) { throw new RuntimeException("Exception while trying to create .done file.", e); } } protected String getBatchFilePathAndName(Person person) { String filename = batchInputFileType.getFileName(person.getPrincipalId(), "", ""); String filepath = batchInputFileType.getDirectoryPath(); if (!filepath.endsWith("/")) { filepath = filepath + "/"; } String extension = batchInputFileType.getFileExtension(); return filepath + filename + "." + extension; } protected String dropXmlFile(Person person, org.w3c.dom.Document xmldoc) { // determine file paths and names String filename = getBatchFilePathAndName(person); // setup the file stream FileOutputStream fos = null; try { fos = new FileOutputStream(filename); try { // setup the output format OutputFormat of = new OutputFormat("XML", "UTF-8", true); of.setIndent(1); of.setIndenting(true); // setup the xml serializer and do the serialization Element docElement = xmldoc.getDocumentElement(); XMLSerializer serializer = new XMLSerializer(fos, of); serializer.asDOMSerializer(); serializer.serialize(docElement); } finally { // close the output stream if (fos != null) { fos.close(); } } } catch (IOException e) { throw new RuntimeException("Exception while writing customer invoice writeoff xml file.", e); } return filename; } protected Document transformVOtoXml(CustomerInvoiceWriteoffBatchVO writeoffBatchVO) { Document xmldoc = new DocumentImpl(); Element e = null; Element invoicesElement = null; Node n = null; Element root = xmldoc.createElementNS("http://www.kuali.org/kfs/ar/customer", XML_ROOT_ELEMENT_NAME); root.setAttribute("xmlns", getBatchXMLNamespace()); root.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance"); // create submittedBy element e = xmldoc.createElement(ArPropertyConstants.SUBMITTED_BY_PRINCIPAL_ID); n = xmldoc.createCDATASection(writeoffBatchVO.getSubmittedByPrincipalName()); e.appendChild(n); root.appendChild(e); // create submittedOn element e = xmldoc.createElement(ArPropertyConstants.SUBMITTED_ON); n = xmldoc.createCDATASection(writeoffBatchVO.getSubmittedOn()); e.appendChild(n); root.appendChild(e); // create note element e = xmldoc.createElement(ArPropertyConstants.NOTE); n = xmldoc.createCDATASection(writeoffBatchVO.getNote()); e.appendChild(n); root.appendChild(e); // create invoices element and list of invoice child elements invoicesElement = xmldoc.createElement(ArConstants.LOOKUP_INVOICE_NUMBERS); for (String invoiceNumber : writeoffBatchVO.getInvoiceNumbers()) { e = xmldoc.createElement(ArConstants.LOOKUP_INVOICE_NUMBER); n = xmldoc.createCDATASection(invoiceNumber); e.appendChild(n); invoicesElement.appendChild(e); } root.appendChild(invoicesElement); xmldoc.appendChild(root); return xmldoc; } // this strange construct (rather than using setter injection) is here to eliminate a // circular reference problem with Spring's eager init. protected CustomerInvoiceWriteoffDocumentService getInvoiceWriteoffDocumentService() { return SpringContext.getBean(CustomerInvoiceWriteoffDocumentService.class); } public void setDateTimeService(DateTimeService dateTimeService) { this.dateTimeService = dateTimeService; } public void setBatchInputFileService(BatchInputFileService batchInputFileService) { this.batchInputFileService = batchInputFileService; } public void setBatchInputFileType(BatchInputFileType batchInputFileType) { this.batchInputFileType = batchInputFileType; } public void setReportsDirectory(String reportsDirectory) { this.reportsDirectory = reportsDirectory; } public void setCustomerService(CustomerService customerService) { this.customerService = customerService; } public void setInvoiceDocumentService(CustomerInvoiceDocumentService invoiceDocumentService) { this.invoiceDocumentService = invoiceDocumentService; } }