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