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 }