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    }