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.sys.service.impl;
017    
018    import java.io.File;
019    import java.io.FileNotFoundException;
020    import java.io.PrintStream;
021    import java.util.ArrayList;
022    import java.util.HashMap;
023    import java.util.IllegalFormatException;
024    import java.util.Iterator;
025    import java.util.List;
026    import java.util.Map;
027    
028    import org.apache.commons.lang.StringUtils;
029    import org.kuali.kfs.sys.KFSConstants;
030    import org.kuali.kfs.sys.Message;
031    import org.kuali.kfs.sys.batch.service.WrappingBatchService;
032    import org.kuali.kfs.sys.context.SpringContext;
033    import org.kuali.kfs.sys.report.BusinessObjectReportHelper;
034    import org.kuali.kfs.sys.service.ReportWriterService;
035    import org.kuali.rice.kns.bo.BusinessObject;
036    import org.kuali.rice.kns.service.DateTimeService;
037    import org.kuali.rice.kns.util.ObjectUtils;
038    
039    /**
040     * Text output implementation of <code>ReportWriterService</code> interface. If you are a developer attempting to add a new business
041     * object for error report writing, take a look at the Spring definitions for BusinessObjectReportHelper.<br>
042     * This class CANNOT be used by 2 processes simultaneously. It is for very specific batch processes that should not run at the same
043     * time, and initialize and destroy must be called and the beginning and end of each process that uses it.
044     * 
045     * @see org.kuali.kfs.sys.report.BusinessObjectReportHelper
046     */
047    public class ReportWriterTextServiceImpl implements ReportWriterService, WrappingBatchService {
048        private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(ReportWriterTextServiceImpl.class);
049    
050        // Changing the initial line number would only affect that a page break occurs early. It does not actually print in the
051        // middle of the page. Hence changing this has little use.
052        protected static final int INITIAL_LINE_NUMBER = 0;
053    
054        protected String filePath;
055        protected String fileNamePrefix;
056        protected String fileNameSuffix;
057        protected String title;
058        protected int pageWidth;
059        protected int pageLength;
060        protected int initialPageNumber;
061        protected String errorSubTitle;
062        protected String statisticsLabel;
063        protected String statisticsLeftPadding;
064        private String parametersLabel;
065        private String parametersLeftPadding;
066        protected String pageLabel;
067        protected String newLineCharacter;
068        protected DateTimeService dateTimeService;
069        protected boolean aggregationModeOn;
070        
071        /**
072         * A map of BO classes to {@link BusinessObjectReportHelper} bean names, to configure which BO's will be rendered by which
073         * BusinessObjectReportHelper. This property should be configured via the spring bean definition
074         */
075        protected Map<Class<? extends BusinessObject>, String> classToBusinessObjectReportHelperBeanNames;
076    
077        // Local caching field to speed up the selection of formatting BusinessObjectReportHelper to use per configuration in Spring
078        protected Map<Class<? extends BusinessObject>, BusinessObjectReportHelper> businessObjectReportHelpers;
079    
080        protected PrintStream printStream;
081        protected int page;
082        protected int line = INITIAL_LINE_NUMBER;
083        protected String errorFormat;
084    
085        // Ensures that the statistics header isn't written multiple times. Does not check that a user doesn't write other stuff into
086        // the statistics
087        // section. A developer is responsible for ensuring that themselves
088        protected boolean modeStatistics = false;
089        
090        // Ensures that the parameters header isn't written multiple times. Does not check that a user doesn't write other stuff into
091        // the parameters
092        // section. A developer is responsible for ensuring that themselves
093        protected boolean modeParameters = false;
094    
095        // So that writeError knows when to writeErrorHeader
096        protected boolean newPage = true;
097    
098        // For printing new headers when the BO is changed
099        protected Class<? extends BusinessObject> businessObjectClass;
100    
101        /**
102         * @see org.kuali.kfs.sys.batch.service.WrappingBatchService#initialize()
103         */
104        public void initialize() {
105            try {
106                printStream = new PrintStream(generateFullFilePath());
107            }
108            catch (FileNotFoundException e) {
109                throw new RuntimeException(e);
110            }
111    
112            page = initialPageNumber;
113            initializeBusinessObjectReportHelpers();
114            // Initial header
115            this.writeHeader(title);
116        }
117    
118        protected void initializeBusinessObjectReportHelpers() {
119            businessObjectReportHelpers = new HashMap<Class<? extends BusinessObject>, BusinessObjectReportHelper>();
120            if (classToBusinessObjectReportHelperBeanNames != null) {
121                for (Class<? extends BusinessObject> clazz : classToBusinessObjectReportHelperBeanNames.keySet()) {
122                    String businessObjectReportHelperBeanName = classToBusinessObjectReportHelperBeanNames.get(clazz);
123                    BusinessObjectReportHelper reportHelper = (BusinessObjectReportHelper) SpringContext.getService(businessObjectReportHelperBeanName);
124                    if (ObjectUtils.isNull(reportHelper)) {
125                        LOG.error("Cannot find BusinessObjectReportHelper implementation for class: " + clazz.getName() + " bean name: " + businessObjectReportHelperBeanName);
126                        throw new RuntimeException("Cannot find BusinessObjectReportHelper implementation for class: " + clazz.getName() + " bean name: " + businessObjectReportHelperBeanName);
127                    }
128                    businessObjectReportHelpers.put(clazz, reportHelper);
129                }
130            }
131        }
132    
133        protected String generateFullFilePath() {
134            if (aggregationModeOn) {
135                return filePath + File.separator + this.fileNamePrefix + fileNameSuffix;
136            }
137            else {
138                return filePath + File.separator + this.fileNamePrefix + dateTimeService.toDateTimeStringForFilename(dateTimeService.getCurrentDate()) + fileNameSuffix;
139            }
140        }
141    
142        /**
143         * @see org.kuali.kfs.sys.batch.service.WrappingBatchService#destroy()
144         */
145        public void destroy() {
146            if(printStream != null) {        
147                printStream.close();
148                printStream = null;
149            }
150    
151            // reset variables that track state
152            page = initialPageNumber;
153            line = INITIAL_LINE_NUMBER;
154            modeStatistics = false;
155            modeParameters = false;
156            newPage = true;
157            businessObjectClass = null;
158        }
159    
160        /**
161         * @see org.kuali.kfs.sys.service.ReportWriterService#writeSubTitle(java.lang.String)
162         */
163        public void writeSubTitle(String message) {
164            if (message.length() > pageWidth) {
165                LOG.warn("sub title to be written exceeds pageWidth. Printing anyway.");
166                this.writeFormattedMessageLine(message);
167            }
168            else {
169                int padding = (pageWidth - message.length()) / 2;
170                this.writeFormattedMessageLine("%" + (padding + message.length()) + "s", message);
171            }
172        }
173        
174        /**
175         * @see org.kuali.kfs.sys.service.ReportWriterService#writeError(java.lang.Class, org.kuali.kfs.sys.Message)
176         */
177        public void writeError(BusinessObject businessObject, Message message) {
178            this.writeError(businessObject, message, true);
179        }
180    
181        /**
182         * @param printBusinessObjectValues indicates whether the bo values should be printed before the message
183         * @see org.kuali.kfs.sys.service.ReportWriterService#writeError(java.lang.Class, org.kuali.kfs.sys.Message)
184         */
185        public void writeError(BusinessObject businessObject, Message message, boolean printBusinessObjectValues) {
186            // Check if we need to write a new table header. We do this if it hasn't been written before or if the businessObject
187            // changed
188            if (newPage || businessObjectClass == null || !businessObjectClass.getName().equals(businessObject.getClass().getName())) {
189                if (businessObjectClass == null) {
190                    // If we didn't write the header before, write it with a subTitle
191                    this.writeSubTitle(errorSubTitle);
192                }
193                else if (!businessObjectClass.getName().equals(businessObject.getClass().getName())) {
194                    // If it changed push a newline in for neater formatting
195                    this.writeNewLines(1);
196                }
197    
198                this.writeErrorHeader(businessObject);
199                newPage = false;
200                businessObjectClass = businessObject.getClass();
201            }
202    
203            // Get business object formatter that will be used
204            BusinessObjectReportHelper businessObjectReportHelper = getBusinessObjectReportHelper(businessObject);
205    
206            // Print the values of the businessObject per formatting determined by writeErrorHeader
207            List<Object> formatterArgs = new ArrayList<Object>();
208            if (printBusinessObjectValues) {
209                formatterArgs.addAll(businessObjectReportHelper.getValues(businessObject));
210            }
211            else {
212                formatterArgs.addAll(businessObjectReportHelper.getBlankValues(businessObject));
213            }
214    
215            // write rest of message on new line(s) if it was cut off
216            int maxMessageLength = Integer.parseInt(StringUtils.substringBefore(StringUtils.substringAfterLast(errorFormat, "%-"), "s"));
217            String messageToPrint = message.getMessage();
218    
219            boolean firstMessageLine = true;
220            while (messageToPrint.length() > 0 && StringUtils.isNotBlank(messageToPrint)) {
221                if (!firstMessageLine) {
222                    formatterArgs = new ArrayList<Object>();
223                    formatterArgs.addAll(businessObjectReportHelper.getBlankValues(businessObject));
224                }
225                else {
226                    firstMessageLine = false;
227                }
228    
229                messageToPrint =  StringUtils.trim(messageToPrint);
230                String messageLine = messageToPrint;
231                if (messageLine.length() > maxMessageLength) {
232                    messageLine = StringUtils.substring(messageLine, 0, maxMessageLength);
233                    if (StringUtils.contains(messageLine, " ")) {
234                        messageLine = StringUtils.substringBeforeLast(messageLine, " ");
235                    }
236                }
237                
238                formatterArgs.add(new Message(messageLine, message.getType()));
239                this.writeFormattedMessageLine(errorFormat, formatterArgs.toArray());
240       
241                messageToPrint = StringUtils.removeStart(messageToPrint, messageLine);
242            }
243        }
244    
245        /**
246         * @see org.kuali.kfs.sys.service.ReportWriterService#writeError(java.lang.Class, java.util.List)
247         */
248        public void writeError(BusinessObject businessObject, List<Message> messages) {
249            int i = 0;
250            for (Iterator<Message> messagesIter = messages.iterator(); messagesIter.hasNext();) {
251                Message message = messagesIter.next();
252    
253                if (i == 0) {
254                    // First one has its values written
255                    this.writeError(businessObject, message, true);
256                }
257                else {
258                    // Any consecutive one only has message written
259                    this.writeError(businessObject, message, false);
260                }
261    
262                i++;
263            }
264        }
265    
266        /**
267         * @see org.kuali.kfs.sys.service.ReportWriterService#writeNewLines(int)
268         */
269        public void writeNewLines(int lines) {
270            for (int i = 0; i < lines; i++) {
271                this.writeFormattedMessageLine("");
272            }
273        }
274    
275        /**
276         * @see org.kuali.kfs.sys.service.ReportWriterService#writeStatisticLine(java.lang.String, java.lang.Object[])
277         */
278        public void writeStatisticLine(String message, Object... args) {
279            // Statistics header is only written if it hasn't been written before
280            if (!modeStatistics) {
281                this.modeStatistics = true;
282    
283                // If nothing has been written to the report we don't want to page break
284                if (!(page == initialPageNumber && line == INITIAL_LINE_NUMBER + 2)) {
285                    this.pageBreak();
286                }
287    
288                this.writeFormattedMessageLine("*********************************************************************************************************************************");
289                this.writeFormattedMessageLine("*********************************************************************************************************************************");
290                this.writeFormattedMessageLine("*******************" + statisticsLabel + "*******************");
291                this.writeFormattedMessageLine("*********************************************************************************************************************************");
292                this.writeFormattedMessageLine("*********************************************************************************************************************************");
293            }
294    
295            this.writeFormattedMessageLine(statisticsLeftPadding + message, args);
296        }
297        
298        /**
299         * @see org.kuali.kfs.sys.service.ReportWriterService#writeParameterLine(java.lang.String, java.lang.Object[])
300         */
301        public void writeParameterLine(String message, Object... args) {
302            // Statistics header is only written if it hasn't been written before
303            if (!modeParameters) {
304                this.modeParameters = true;
305    
306                // If nothing has been written to the report we don't want to page break
307                if (!(page == initialPageNumber && line == INITIAL_LINE_NUMBER + 2)) {
308                    this.pageBreak();
309                }
310    
311                this.writeFormattedMessageLine("*********************************************************************************************************************************");
312                this.writeFormattedMessageLine("*********************************************************************************************************************************");
313                this.writeFormattedMessageLine("*******************" + getParametersLabel() + "*******************");
314                this.writeFormattedMessageLine("*********************************************************************************************************************************");
315                this.writeFormattedMessageLine("*********************************************************************************************************************************");
316            }
317    
318            this.writeFormattedMessageLine(getParametersLeftPadding() + message, args);
319        }
320    
321        /**
322         * @see org.kuali.kfs.sys.service.ReportWriterService#writeFormattedMessageLine(java.lang.String)
323         */
324        public void writeFormattedMessageLine(String format) {
325            this.writeFormattedMessageLine(format, new Object());
326        }
327    
328        /**
329         * @see org.kuali.kfs.sys.service.ReportWriterService#writeFormattedMessageLine(java.lang.String, java.lang.Object[])
330         */
331        public void writeFormattedMessageLine(String format, Object... args) {
332            if (format.indexOf("% s") > -1) {
333                LOG.warn("Cannot properly format: "+format);
334            } 
335            else {
336                Object[] escapedArgs = escapeArguments(args);
337                if (LOG.isDebugEnabled()) {
338                    LOG.debug("writeFormattedMessageLine, format: "+format);
339                }
340                
341                String message = null;
342        
343                if (escapedArgs.length > 0) {
344                    message = String.format(format + newLineCharacter, escapedArgs);
345                } else {
346                    message = format+newLineCharacter;
347                }
348        
349                // Log we are writing out of bounds. Would be nice to show message here but not so sure if it's wise to dump that data into
350                // logs
351                if (message.length() > pageWidth) {
352                    if (LOG.isDebugEnabled()) {
353                        LOG.debug("message is out of bounds writing anyway");
354                    }
355                }
356        
357                printStream.print(message);
358                printStream.flush();
359        
360                line++;
361                if (line >= pageLength) {
362                    this.pageBreak();
363                }
364            }
365        }
366        
367        /**
368         * Determines if all formatting on the given String is escaped - ie, that it has no formatting
369         * @param format the format to test
370         * @return true if the String is without formatting, false otherwise
371         */
372        protected boolean allFormattingEscaped(String format) {
373            int currPoint = 0;
374            int currIndex = format.indexOf('%', currPoint);
375            while (currIndex > -1) {
376                char nextChar = format.charAt(currIndex+1);
377                if (nextChar != '%') {
378                    return false;
379                }
380                currPoint = currIndex + 2;
381            }
382            return true;
383        }
384    
385        /**
386         * @see org.kuali.kfs.sys.service.ReportWriterService#pageBreak()
387         */
388        public void pageBreak() {
389            // Intentionally not using writeFormattedMessageLine here since it would loop trying to page break ;)
390            // 12 represents the ASCII Form Feed character
391            printStream.printf("%c" + newLineCharacter, 12);
392            page++;
393            line = INITIAL_LINE_NUMBER;
394            newPage = true;
395    
396            this.writeHeader(title);
397        }
398    
399        /**
400         * Helper method to write a header for placement at top of new page
401         * 
402         * @param title that should be printed on this header
403         */
404        protected void writeHeader(String title) {
405            String headerText = String.format("%1$tY-%1$tm-%1$td %1$tH:%1$tM", dateTimeService.getCurrentDate());
406            int reportTitlePadding = pageWidth / 2 - headerText.length() - title.length() / 2;
407            headerText = String.format("%s%" + (reportTitlePadding + title.length()) + "s%" + reportTitlePadding + "s", headerText, title, "");
408    
409            if (aggregationModeOn) {
410                this.writeFormattedMessageLine("%s%s%s", headerText, pageLabel, KFSConstants.REPORT_WRITER_SERVICE_PAGE_NUMBER_PLACEHOLDER);
411            }
412            else {
413                this.writeFormattedMessageLine("%s%s%,9d", headerText, pageLabel, page);
414            }
415            this.writeNewLines(1);
416        }
417    
418        /**
419         * Helper method to write the error header
420         * 
421         * @param businessObject to print header for
422         */
423        protected void writeErrorHeader(BusinessObject businessObject) {
424            BusinessObjectReportHelper businessObjectReportHelper = getBusinessObjectReportHelper(businessObject);
425            List<String> errorHeader = businessObjectReportHelper.getTableHeader(pageWidth);
426    
427            // If we are at end of page and don't have space for table header, go ahead and page break
428            if (errorHeader.size() + line >= pageLength) {
429                this.pageBreak();
430            }
431    
432            // Print the header one by one. Note the last element is the formatter. So capture that seperately
433            for (Iterator<String> headers = errorHeader.iterator(); headers.hasNext();) {
434                String header = headers.next();
435    
436                if (headers.hasNext()) {
437                    this.writeFormattedMessageLine("%s", header);
438                }
439                else {
440                    errorFormat = header;
441                }
442            }
443        }
444    
445        /**
446         * @see org.kuali.kfs.sys.service.ReportWriterService#writeTableHeader(org.kuali.rice.kns.bo.BusinessObject)
447         */
448        public void writeTableHeader(BusinessObject businessObject) {
449            BusinessObjectReportHelper businessObjectReportHelper = getBusinessObjectReportHelper(businessObject);
450    
451            Map<String, String> tableDefinition = businessObjectReportHelper.getTableDefinition();
452            String tableHeaderFormat = tableDefinition.get(KFSConstants.ReportConstants.TABLE_HEADER_LINE_KEY);
453    
454            String[] headerLines = this.getMultipleFormattedMessageLines(tableHeaderFormat, new Object());
455            this.writeMultipleFormattedMessageLines(headerLines);
456        }
457        
458        /**
459         * Writes out the table header, based on a business object class
460         * @param businessObjectClass the class to write the header out for
461         */
462        public void writeTableHeader(Class<? extends BusinessObject> businessObjectClass) {
463            BusinessObjectReportHelper businessObjectReportHelper = getBusinessObjectReportHelper(businessObjectClass);
464    
465            Map<String, String> tableDefinition = businessObjectReportHelper.getTableDefinition();
466            String tableHeaderFormat = tableDefinition.get(KFSConstants.ReportConstants.TABLE_HEADER_LINE_KEY);
467    
468            String[] headerLines = this.getMultipleFormattedMessageLines(tableHeaderFormat, new Object());
469            this.writeMultipleFormattedMessageLines(headerLines);
470        }
471        
472        /**
473         * @see org.kuali.kfs.sys.service.ReportWriterService#writeTableRow(org.kuali.rice.kns.bo.BusinessObject)
474         */
475        public void writeTableRowSeparationLine(BusinessObject businessObject) {
476            BusinessObjectReportHelper businessObjectReportHelper = getBusinessObjectReportHelper(businessObject);
477            Map<String, String> tableDefinition = businessObjectReportHelper.getTableDefinition();
478    
479            String separationLine = tableDefinition.get(KFSConstants.ReportConstants.SEPARATOR_LINE_KEY);
480            this.writeFormattedMessageLine(separationLine);
481        }
482    
483        /**
484         * @see org.kuali.kfs.sys.service.ReportWriterService#writeTableRow(org.kuali.rice.kns.bo.BusinessObject)
485         */
486        public void writeTableRow(BusinessObject businessObject) {
487            BusinessObjectReportHelper businessObjectReportHelper = getBusinessObjectReportHelper(businessObject);
488            Map<String, String> tableDefinition = businessObjectReportHelper.getTableDefinition();
489    
490            String tableCellFormat = tableDefinition.get(KFSConstants.ReportConstants.TABLE_CELL_FORMAT_KEY);
491            List<String> tableCellValues = businessObjectReportHelper.getTableCellValuesPaddingWithEmptyCell(businessObject, false);
492    
493            String[] rowMessageLines = this.getMultipleFormattedMessageLines(tableCellFormat, tableCellValues.toArray());
494            this.writeMultipleFormattedMessageLines(rowMessageLines);
495        }
496        
497        /**
498         * @see org.kuali.kfs.sys.service.ReportWriterService#writeTableRowWithColspan(org.kuali.rice.kns.bo.BusinessObject)
499         */
500        public void writeTableRowWithColspan(BusinessObject businessObject) {
501            BusinessObjectReportHelper businessObjectReportHelper = getBusinessObjectReportHelper(businessObject);
502            Map<String, String> tableDefinition = businessObjectReportHelper.getTableDefinition();
503    
504            String tableCellFormat = businessObjectReportHelper.getTableCellFormat(true, true, StringUtils.EMPTY);
505            List<String> tableCellValues = businessObjectReportHelper.getTableCellValuesPaddingWithEmptyCell(businessObject, true);
506    
507            String[] rowMessageLines = this.getMultipleFormattedMessageLines(tableCellFormat, tableCellValues.toArray());
508            this.writeMultipleFormattedMessageLines(rowMessageLines);
509        }
510    
511        /**
512         * @see org.kuali.kfs.sys.service.ReportWriterService#writeTable(java.util.List, boolean, boolean)
513         */
514        public void writeTable(List<? extends BusinessObject> businessObjects, boolean isHeaderRepeatedInNewPage, boolean isRowBreakAcrossPageAllowed) {
515            if (ObjectUtils.isNull(businessObjects) || businessObjects.isEmpty()) {
516                return;
517            }
518    
519            BusinessObject firstBusinessObject = businessObjects.get(0);
520            this.writeTableHeader(firstBusinessObject);
521    
522            BusinessObjectReportHelper businessObjectReportHelper = getBusinessObjectReportHelper(businessObjects.get(0));
523            Map<String, String> tableDefinition = businessObjectReportHelper.getTableDefinition();
524            String tableHeaderFormat = tableDefinition.get(KFSConstants.ReportConstants.TABLE_HEADER_LINE_KEY);
525            String[] headerLines = this.getMultipleFormattedMessageLines(tableHeaderFormat, new Object());
526    
527            String tableCellFormat = tableDefinition.get(KFSConstants.ReportConstants.TABLE_CELL_FORMAT_KEY);
528    
529            for (BusinessObject businessObject : businessObjects) {
530    
531                List<String> tableCellValues = businessObjectReportHelper.getTableCellValuesPaddingWithEmptyCell(businessObject, false);
532                String[] messageLines = this.getMultipleFormattedMessageLines(tableCellFormat, tableCellValues.toArray());
533    
534                boolean hasEnoughLinesInPage = messageLines.length <= (this.pageLength - this.line);
535                if (!hasEnoughLinesInPage && !isRowBreakAcrossPageAllowed) {
536                    this.pageBreak();
537    
538                    if (isHeaderRepeatedInNewPage) {
539                        this.writeTableHeader(firstBusinessObject);
540                    }
541                }
542    
543                this.writeMultipleFormattedMessageLines(messageLines, headerLines, isRowBreakAcrossPageAllowed);
544            }
545    
546        }
547    
548        /**
549         * get the business report helper for the given business object
550         * 
551         * @param businessObject the given business object
552         * @return the business report helper for the given business object
553         */
554        public BusinessObjectReportHelper getBusinessObjectReportHelper(BusinessObject businessObject) {
555            if (LOG.isDebugEnabled()) {
556                if (businessObject == null) {
557                    LOG.debug("reporting "+filePath+" but can't because null business object sent in");
558                } else if (businessObjectReportHelpers == null) {
559                    LOG.debug("Logging "+businessObject+" in report "+filePath+" but businessObjectReportHelpers are null");
560                }
561            }
562            BusinessObjectReportHelper businessObjectReportHelper = this.businessObjectReportHelpers.get(businessObject.getClass());
563            if (ObjectUtils.isNull(businessObjectReportHelper)) {
564                throw new RuntimeException(businessObject.getClass().toString() + " is not handled");
565            }
566    
567            return businessObjectReportHelper;
568        }
569        
570        /**
571         * get the business report helper for the given business object
572         * 
573         * @param businessObject the given business object
574         * @return the business report helper for the given business object
575         */
576        public BusinessObjectReportHelper getBusinessObjectReportHelper(Class<? extends BusinessObject> businessObjectClass) {
577            BusinessObjectReportHelper businessObjectReportHelper = this.businessObjectReportHelpers.get(businessObjectClass);
578            if (ObjectUtils.isNull(businessObjectReportHelper)) {
579                throw new RuntimeException(businessObjectClass.getName() + " is not handled");
580            }
581    
582            return businessObjectReportHelper;
583        }
584    
585        /**
586         * write the given information as multiple lines if it contains more than one line breaks
587         * 
588         * @param format the given text format definition
589         * @param messageLines the given information being written out
590         * @param headerLinesInNewPage the given header lines being written in the begin of a new page
591         */
592        protected void writeMultipleFormattedMessageLines(String[] messageLines, String[] headerLinesInNewPage, boolean isRowBreakAcrossPageAllowed) {
593            int currentPageNumber = this.page;
594    
595            for (String line : messageLines) {
596                boolean hasEnoughLinesInPage = messageLines.length <= (this.pageLength - this.line);
597                if (!hasEnoughLinesInPage && !isRowBreakAcrossPageAllowed) {
598                    this.pageBreak();
599                }
600    
601                if (currentPageNumber < this.page && ObjectUtils.isNotNull(headerLinesInNewPage)) {
602                    currentPageNumber = this.page;
603    
604                    for (String headerLine : headerLinesInNewPage) {
605                        this.writeFormattedMessageLine(headerLine);
606                    }
607                }
608    
609                this.writeFormattedMessageLine(line);
610            }
611        }
612    
613        /**
614         * write the given information as multiple lines if it contains more than one line breaks
615         * 
616         * @param format the given text format definition
617         * @param args the given information being written out
618         */
619        public void writeMultipleFormattedMessageLines(String[] messageLines) {
620            this.writeMultipleFormattedMessageLines(messageLines, null, false);
621        }
622    
623        public void writeMultipleFormattedMessageLines(String format, Object... args) {
624            Object[] escapedArgs = escapeArguments(args);
625            String[] messageLines = getMultipleFormattedMessageLines(format, escapedArgs);
626            writeMultipleFormattedMessageLines(messageLines);
627        }
628            
629        /**
630         * This method...
631         * 
632         * @param format
633         * @param args
634         * @return
635         */
636        public String[] getMultipleFormattedMessageLines(String format, Object... args) {
637            Object[] escapedArgs = escapeArguments(args);
638            String message = String.format(format, escapedArgs);
639            return StringUtils.split(message, newLineCharacter);
640        }
641        
642        /**
643         * Iterates through array and escapes special formatting characters 
644         * 
645         * @param args Object array to process      
646         * @return Object array with String members escaped
647         */
648        protected Object[] escapeArguments(Object... args) {
649            Object[] escapedArgs = new Object[args.length];
650            for (int i = 0; i < args.length; i++) {
651                Object arg = args[i];
652                if (arg == null) {
653                    args[i] = "";
654                } else if (arg != null && arg instanceof String) {
655                    String escapedArg = escapeFormatCharacters((String)arg);
656                    escapedArgs[i] = escapedArg;
657                }
658                else {
659                    escapedArgs[i] = arg;
660                }
661            }
662            
663            return escapedArgs;
664        }
665    
666        /**
667         * Escapes characters in a string that have special meaning for formatting
668         * 
669         * @param replacementString string to escape
670         * @return string with format characters escaped
671         * @see KFSConstants.ReportConstants.FORMAT_ESCAPE_CHARACTERS
672         */
673        protected String escapeFormatCharacters(String replacementString) {
674            String escapedString = replacementString;
675            for (int i = 0; i < KFSConstants.ReportConstants.FORMAT_ESCAPE_CHARACTERS.length; i++) {
676                String characterToEscape = KFSConstants.ReportConstants.FORMAT_ESCAPE_CHARACTERS[i];
677                escapedString = StringUtils.replace(escapedString, characterToEscape, characterToEscape + characterToEscape);
678            }
679    
680            return escapedString;
681        }
682    
683        /**
684         * Sets the filePath
685         * 
686         * @param filePath The filePath to set.
687         */
688        public void setFilePath(String filePath) {
689            this.filePath = filePath;
690        }
691    
692        /**
693         * Sets the fileNamePrefix
694         * 
695         * @param fileNamePrefix The fileNamePrefix to set.
696         */
697        public void setFileNamePrefix(String fileNamePrefix) {
698            this.fileNamePrefix = fileNamePrefix;
699        }
700    
701        /**
702         * Sets the fileNameSuffix
703         * 
704         * @param fileNameSuffix The fileNameSuffix to set.
705         */
706        public void setFileNameSuffix(String fileNameSuffix) {
707            this.fileNameSuffix = fileNameSuffix;
708        }
709    
710        /**
711         * Sets the title
712         * 
713         * @param title The title to set.
714         */
715        public void setTitle(String title) {
716            this.title = title;
717        }
718    
719        /**
720         * Sets the pageWidth
721         * 
722         * @param pageWidth The pageWidth to set.
723         */
724        public void setPageWidth(int pageWidth) {
725            this.pageWidth = pageWidth;
726        }
727    
728        /**
729         * Sets the pageLength
730         * 
731         * @param pageLength The pageLength to set.
732         */
733        public void setPageLength(int pageLength) {
734            this.pageLength = pageLength;
735        }
736    
737        /**
738         * Sets the initialPageNumber
739         * 
740         * @param initialPageNumber The initialPageNumber to set.
741         */
742        public void setInitialPageNumber(int initialPageNumber) {
743            this.initialPageNumber = initialPageNumber;
744        }
745    
746        /**
747         * Sets the errorSubTitle
748         * 
749         * @param errorSubTitle The errorSubTitle to set.
750         */
751        public void setErrorSubTitle(String errorSubTitle) {
752            this.errorSubTitle = errorSubTitle;
753        }
754    
755        /**
756         * Sets the statisticsLabel
757         * 
758         * @param statisticsLabel The statisticsLabel to set.
759         */
760        public void setStatisticsLabel(String statisticsLabel) {
761            this.statisticsLabel = statisticsLabel;
762        }
763    
764        /**
765         * Sets the statisticsLeftPadding
766         * 
767         * @param statisticsLeftPadding The statisticsLeftPadding to set.
768         */
769        public void setStatisticsLeftPadding(String statisticsLeftPadding) {
770            this.statisticsLeftPadding = statisticsLeftPadding;
771        }
772    
773        /**
774         * Sets the pageLabel
775         * 
776         * @param pageLabel The pageLabel to set.
777         */
778        public void setPageLabel(String pageLabel) {
779            this.pageLabel = pageLabel;
780        }
781        
782        /**
783         * Sets the newLineCharacter
784         * 
785         * @param newLineCharacter The newLineCharacter to set.
786         */
787        public void setNewLineCharacter(String newLineCharacter) {
788            this.newLineCharacter = newLineCharacter;
789        }
790    
791        /**
792         * Sets the DateTimeService
793         * 
794         * @param dateTimeService The DateTimeService to set.
795         */
796        public void setDateTimeService(DateTimeService dateTimeService) {
797            this.dateTimeService = dateTimeService;
798        }
799    
800        /**
801         * Sets a map of BO classes to {@link BusinessObjectReportHelper} bean names, to configure which BO's will be rendered by which
802         * BusinessObjectReportHelper. This property should be configured via the spring bean definition
803         * 
804         * @param classToBusinessObjectReportHelperBeanNames The classToBusinessObjectReportHelperBeanNames to set.
805         */
806        public void setClassToBusinessObjectReportHelperBeanNames(Map<Class<? extends BusinessObject>, String> classToBusinessObjectReportHelperBeanNames) {
807            this.classToBusinessObjectReportHelperBeanNames = classToBusinessObjectReportHelperBeanNames;
808        }
809    
810        /**
811         * Gets the parametersLabel attribute. 
812         * @return Returns the parametersLabel.
813         */
814        public String getParametersLabel() {
815            return parametersLabel;
816        }
817    
818        /**
819         * Sets the parametersLabel attribute value.
820         * @param parametersLabel The parametersLabel to set.
821         */
822        public void setParametersLabel(String parametersLabel) {
823            this.parametersLabel = parametersLabel;
824        }
825    
826        /**
827         * Gets the parametersLeftPadding attribute. 
828         * @return Returns the parametersLeftPadding.
829         */
830        public String getParametersLeftPadding() {
831            return parametersLeftPadding;
832        }
833    
834        /**
835         * Sets the parametersLeftPadding attribute value.
836         * @param parametersLeftPadding The parametersLeftPadding to set.
837         */
838        public void setParametersLeftPadding(String parametersLeftPadding) {
839            this.parametersLeftPadding = parametersLeftPadding;
840        }
841    
842        /**
843         * Gets the aggregationModeOn attribute. 
844         * @return Returns the aggregationModeOn.
845         */
846        public boolean isAggregationModeOn() {
847            return aggregationModeOn;
848        }
849    
850        /**
851         * Sets the aggregationModeOn attribute value.
852         * @param aggregationModeOn The aggregationModeOn to set.
853         */
854        public void setAggregationModeOn(boolean aggregationModeOn) {
855            this.aggregationModeOn = aggregationModeOn;
856        }
857        
858        
859    }