View Javadoc
1   /*
2    * Copyright 2005-2014 The Kuali Foundation
3    * 
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * 
8    * http://www.opensource.org/licenses/ecl1.php
9    * 
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.coeus.s2sgen.impl.print;
17  
18  import com.lowagie.text.*;
19  import com.lowagie.text.Font;
20  import com.lowagie.text.pdf.*;
21  import org.apache.commons.lang3.StringUtils;
22  import org.apache.fop.apps.*;
23  import org.kuali.coeus.propdev.api.s2s.S2SConfigurationService;
24  import org.kuali.coeus.s2sgen.api.core.S2SException;
25  import org.kuali.coeus.s2sgen.api.core.ConfigurationConstants;
26  import org.slf4j.Logger;
27  import org.slf4j.LoggerFactory;
28  import org.springframework.beans.factory.annotation.Autowired;
29  import org.springframework.beans.factory.annotation.Qualifier;
30  import org.springframework.stereotype.Component;
31  
32  import javax.xml.transform.*;
33  import javax.xml.transform.sax.SAXResult;
34  import javax.xml.transform.stream.StreamSource;
35  import java.awt.*;
36  import java.io.*;
37  import java.sql.Timestamp;
38  import java.text.DateFormat;
39  import java.text.SimpleDateFormat;
40  import java.util.*;
41  import java.util.List;
42  
43  /**
44   * This class provides the functionality for printing any {@link S2SPrintable} object. It uses the methods available in Printable
45   * object to generate XML, fetch XSL style-sheets, then transforms the XML to a PDF after applying the style sheet.
46   *
47   */
48  @Component("s2SPrintingService")
49  public class S2SPrintingServiceImpl implements S2SPrintingService {
50  
51      private static final Logger LOG = LoggerFactory.getLogger(S2SPrintingServiceImpl.class);
52      public static final String PDF_REPORT_CONTENT_TYPE = "application/pdf";
53      public static final String PDF_FILE_EXTENSION = ".pdf";
54      public char SPACE_SEPARATOR = 32;
55      public int WHITESPACE_LENGTH_76 = 76;
56      public int WHITESPACE_LENGTH_60 = 60;
57  
58      @Autowired
59      @Qualifier("s2SConfigurationService")
60      private S2SConfigurationService s2SConfigurationService;
61  
62      /**
63       * This method receives a {@link S2SPrintable} object, generates XML for it, transforms into PDF after applying style-sheet and
64       * returns the PDF bytes
65       *
66       * @param printableArtifact to be printed
67       * @return PDF bytes
68       */
69      protected Map<String, byte[]> getPrintBytes(S2SPrintable printableArtifact) {
70          try {
71              Map<String, byte[]> streamMap = printableArtifact.renderXML();
72              try{
73                  String loggingEnable = s2SConfigurationService.getValueAsString(ConfigurationConstants.PRINT_LOGGING_ENABLE);
74                  if (loggingEnable != null && Boolean.parseBoolean(loggingEnable))
75                      logPrintDetails(streamMap);
76              }catch(Exception ex){
77                  LOG.error(ex.getMessage());
78              }
79  
80              Map<String, byte[]> pdfByteMap = new LinkedHashMap<String, byte[]>();
81  
82              FopFactory fopFactory = FopFactory.newInstance();
83  
84              int xslCount = 0;
85              // Apply all the style sheets to the xml document and generate the
86              // PDF bytes
87              if (printableArtifact.getXSLTemplates() != null) {
88                  for (Source source : printableArtifact.getXSLTemplates()) {
89                      xslCount++;
90                      StreamSource xslt = (StreamSource) source;
91                      if(xslt.getInputStream()==null || xslt.getInputStream().available()<=0){
92                          LOG.error("Stylesheet is not available");
93                      }else{
94                          createPdfWithFOP(streamMap, pdfByteMap, fopFactory, xslCount, xslt, printableArtifact);
95                      }
96                  }
97              }
98              else if (printableArtifact.getXSLTemplateWithBookmarks() != null) {
99                  Map<String, Source> templatesWithBookmarks = printableArtifact.getXSLTemplateWithBookmarks();
100                 for (Map.Entry<String, Source> templatesWithBookmark : templatesWithBookmarks.entrySet()) {
101                     StreamSource xslt = (StreamSource) templatesWithBookmark.getValue();
102                     createPdfWithFOP(streamMap, pdfByteMap, fopFactory, xslCount, xslt, templatesWithBookmark.getKey(),
103                             printableArtifact);
104                 }
105 
106             }
107 
108             // Add all the attachments.
109             if (printableArtifact.getAttachments() != null) {
110                 pdfByteMap.putAll(printableArtifact.getAttachments());
111             }
112             return pdfByteMap;
113         }
114         catch (FOPException|TransformerException|IOException e) {
115             throw new S2SException(e.getMessage(), e);
116         }
117     }
118 
119     protected void createPdfWithFOP(Map<String, byte[]> streamMap, Map<String, byte[]> pdfByteMap, FopFactory fopFactory,
120             int xslCount, StreamSource xslt, S2SPrintable printableArtifact) throws FOPException, TransformerException {
121         createPdfWithFOP(streamMap, pdfByteMap, fopFactory, xslCount, xslt, null, printableArtifact);
122     }
123 
124     protected void createPdfWithFOP(Map<String, byte[]> streamMap, Map<String, byte[]> pdfByteMap, FopFactory fopFactory,
125             int xslCount, StreamSource xslt, String bookmark, S2SPrintable printableArtifact) throws FOPException,
126             TransformerException {
127         TransformerFactory factory = TransformerFactory.newInstance();
128         Transformer transformer = factory.newTransformer(xslt);
129         String applicationUrl = s2SConfigurationService.getValueAsString(ConfigurationConstants.APPLICATION_URL_KEY);
130         FOUserAgent foUserAgent = fopFactory.newFOUserAgent();
131         foUserAgent.setBaseURL(applicationUrl);
132         for (Map.Entry<String, byte[]> xmlData : streamMap.entrySet()) {
133             ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
134             ByteArrayInputStream inputStream = new ByteArrayInputStream(xmlData.getValue());
135             Source src = new StreamSource(inputStream);
136             Fop fop = fopFactory.newFop(MimeConstants.MIME_PDF, foUserAgent, outputStream);
137             Result res = new SAXResult(fop.getDefaultHandler());
138             transformer.transform(src, res);
139             byte[] pdfBytes = outputStream.toByteArray();
140             if (pdfBytes != null && pdfBytes.length > 0) {
141                 String pdfMapKey = bookmark == null ? createBookMark(xslCount, xmlData.getKey()) : bookmark;
142                 pdfByteMap.put(pdfMapKey, pdfBytes);
143             }
144         }
145     }
146 
147 
148     protected String createBookMark(int xslCount, String bookmarkKey) {
149         String pdfMapKey = bookmarkKey + (xslCount == 1 ? "" : " " + xslCount);
150         return pdfMapKey;
151     }
152 
153     /**
154      * This method receives a {@link S2SPrintable} object, generates XML for it, transforms into PDF after applying style-sheet and
155      * returns the PDF bytes as {@link S2SFile}
156      *
157      * @param printableArtifacts to be printed
158      * @return PDF bytes
159      */
160     public S2SFile print(S2SPrintable printableArtifacts) {
161         List<S2SPrintable> printables = new ArrayList<S2SPrintable>();
162         printables.add(printableArtifacts);
163         return print(printables);
164     }
165 
166     /**
167      * This method receives a {@link java.util.List} of {@link S2SPrintable} object, generates XML for it, transforms into PDF after applying
168      * style-sheet and returns the PDF bytes as {@link S2SFile}
169      *
170      * @param printableArtifactList List of printableArtifact to be printed
171      * @return {@link S2SFile} PDF bytes
172      */
173     public S2SFile print(List<S2SPrintable> printableArtifactList) {
174         return print(printableArtifactList, false);
175     }
176 
177     public S2SFile print(List<S2SPrintable> printableArtifactList, boolean headerFooterRequired) {
178         S2SFile printablePdf = null;
179         List<String> bookmarksList = new ArrayList<String>();
180         List<byte[]> pdfBaosList = new ArrayList<byte[]>();
181         for (S2SPrintable printableArtifact : printableArtifactList) {
182             Map<String, byte[]> printBytes = getPrintBytes(printableArtifact);
183             for (String bookmark : printBytes.keySet()) {
184                 byte[] pdfBytes = printBytes.get(bookmark);
185                 if (isPdfGoodToMerge(pdfBytes)) {
186                     bookmarksList.add(bookmark);
187                     pdfBaosList.add(pdfBytes);
188                 }
189             }
190         }
191 
192         printablePdf = new S2SFile();
193         byte[] mergedPdfBytes = mergePdfBytes(pdfBaosList, bookmarksList, headerFooterRequired);
194 
195         // If there is a stylesheet issue, the pdf bytes will be null. To avoid an exception
196         // initialize to an empty array before sending the content back
197         if (mergedPdfBytes == null) {
198             mergedPdfBytes = new byte[0];
199         }
200 
201         printablePdf.setData(mergedPdfBytes);
202         StringBuilder fileName = new StringBuilder();
203         fileName.append(getReportName());
204         fileName.append(PDF_FILE_EXTENSION);
205         printablePdf.setName(fileName.toString());
206         printablePdf.setType(PDF_REPORT_CONTENT_TYPE);
207         return printablePdf;
208     }
209 
210     protected boolean isPdfGoodToMerge(byte[] pdfBytes) {
211         try {
212             new PdfReader(pdfBytes);
213             return true;
214         }
215         catch (IOException e) {
216             return false;
217         }
218     }
219 
220     protected String getReportName() {
221         String dateString = new Date().toString();
222         return StringUtils.deleteWhitespace(dateString);
223     }
224 
225     /**
226      * @param pdfBytesList List containing the PDF data bytes
227      * @param bookmarksList List of bookmarks corresponding to the PDF bytes.
228      */
229     protected byte[] mergePdfBytes(List<byte[]> pdfBytesList, List<String> bookmarksList, boolean headerFooterRequired) {
230         Document document = null;
231         PdfWriter writer = null;
232         ByteArrayOutputStream mergedPdfReport = new ByteArrayOutputStream();
233         int totalNumOfPages = 0;
234         PdfReader[] pdfReaderArr = new PdfReader[pdfBytesList.size()];
235         int pdfReaderCount = 0;
236         for (byte[] fileBytes : pdfBytesList) {
237             LOG.debug("File Size " + fileBytes.length + " For " + bookmarksList.get(pdfReaderCount));
238             PdfReader reader = null;
239             try {
240                 reader = new PdfReader(fileBytes);
241                 pdfReaderArr[pdfReaderCount] = reader;
242                 pdfReaderCount = pdfReaderCount + 1;
243                 totalNumOfPages += reader.getNumberOfPages();
244             }
245             catch (IOException e) {
246                 LOG.error(e.getMessage(), e);
247             }
248         }
249         HeaderFooter footer = null;
250         if (headerFooterRequired) {
251             Calendar calendar = Calendar.getInstance();
252             String dateString = formateCalendar(calendar);
253             StringBuilder footerPhStr = new StringBuilder();
254             footerPhStr.append(" of ");
255             footerPhStr.append(totalNumOfPages);
256             footerPhStr.append(getWhitespaceString(WHITESPACE_LENGTH_76));
257             footerPhStr.append(getWhitespaceString(WHITESPACE_LENGTH_76));
258             footerPhStr.append(getWhitespaceString(WHITESPACE_LENGTH_60));
259             footerPhStr.append(dateString);
260             Font font = FontFactory.getFont(FontFactory.TIMES, 8, Font.NORMAL, Color.BLACK);
261             Phrase beforePhrase = new Phrase("Page ", font);
262             Phrase afterPhrase = new Phrase(footerPhStr.toString(), font);
263             footer = new HeaderFooter(beforePhrase, afterPhrase);
264             footer.setAlignment(Element.ALIGN_BASELINE);
265             footer.setBorderWidth(0f);
266         }
267         for (int count = 0; count < pdfReaderArr.length; count++) {
268             PdfReader reader = pdfReaderArr[count];
269             int nop;
270             if (reader == null) {
271                 LOG.debug("Empty PDF byetes found for " + bookmarksList.get(count));
272                 continue;
273             }
274             else {
275                 nop = reader.getNumberOfPages();
276             }
277 
278             if (count == 0) {
279                 document = nop > 0 ? new Document(reader.getPageSizeWithRotation(1))
280                         : new Document();
281                 try {
282                     writer = PdfWriter.getInstance(document, mergedPdfReport);
283                 }
284                 catch (DocumentException e) {
285                     LOG.error(e.getMessage(), e);
286                     throw new S2SException(e.getMessage(), e);
287                 }
288                 if (footer != null) {
289                     document.setFooter(footer);
290                 }
291                 document.open();
292             }
293 
294             PdfContentByte cb = writer.getDirectContent();
295             int pageCount = 0;
296             while (pageCount < nop) {
297                 document.setPageSize(reader.getPageSize(++pageCount));
298                 document.newPage();
299                 if (footer != null) {
300                     document.setFooter(footer);
301                 }
302                 PdfImportedPage page = writer.getImportedPage(reader, pageCount);
303 
304                 cb.addTemplate(page, 1, 0, 0, 1, 0, 0);
305 
306 
307                 PdfOutline root = cb.getRootOutline();
308                 if (pageCount == 1) {
309                     String pageName = bookmarksList.get(count);
310                     cb.addOutline(new PdfOutline(root, new PdfDestination(PdfDestination.FITH), pageName), pageName);
311                 }
312             }
313         }
314         if (document != null) {
315             try {
316                 document.close();
317                 return mergedPdfReport.toByteArray();
318             }
319             catch (Exception e) {
320                 LOG.error("Exception occured because the generated PDF document has no pages", e);
321             }
322         }
323         return null;
324     }
325 
326 
327     protected String formateCalendar(Calendar calendar) {
328         DateFormat dateFormat = new SimpleDateFormat("M/d/yy h:mm a");
329         return dateFormat.format(calendar.getTime());
330     }
331 
332     protected String getWhitespaceString(int length) {
333         StringBuilder sb = new StringBuilder();
334         char[] whiteSpace = new char[length];
335         Arrays.fill(whiteSpace, SPACE_SEPARATOR);
336         sb.append(whiteSpace);
337         return sb.toString();
338     }
339 
340     protected void logPrintDetails(Map<String, byte[]> xmlStreamMap) {
341         String loggingDirectory = s2SConfigurationService.getValueAsString(ConfigurationConstants.PRINT_LOGGING_DIRECTORY);
342         if (loggingDirectory != null) {
343 
344             for (String key : xmlStreamMap.keySet()) {
345                 byte[] xmlBytes = xmlStreamMap.get(key);
346                 String xmlString = new String(xmlBytes);
347                 String dateString = new Timestamp(new Date().getTime()).toString();
348                 String reportName = StringUtils.deleteWhitespace(key);
349                 String createdTime = StringUtils.replaceChars(StringUtils.deleteWhitespace(dateString), ":", "_");
350                 File dir = new File(loggingDirectory);
351                 if(!dir.exists() || !dir.isDirectory()){
352                     dir.mkdirs();
353                 }
354                 File file = new File(dir , reportName + createdTime + ".xml");
355                 try(BufferedWriter out = new BufferedWriter(new FileWriter(file))) {
356                     out.write(xmlString);
357                 } catch (IOException e) {
358                         LOG.error(e.getMessage(), e);
359                 }
360             }
361         }
362     }
363 
364     public S2SConfigurationService getS2SConfigurationService() {
365         return s2SConfigurationService;
366     }
367 
368     public void setS2SConfigurationService(S2SConfigurationService s2SConfigurationService) {
369         this.s2SConfigurationService = s2SConfigurationService;
370     }
371 }