View Javadoc
1   /*
2    * Copyright (C) 2003-2008 eXo Platform SAS.
3    *
4    * This program is free software; you can redistribute it and/or
5    * modify it under the terms of the GNU Affero General Public License
6    * as published by the Free Software Foundation; either version 3
7    * of the License, or (at your option) any later version.
8    *
9    * This program is distributed in the hope that it will be useful,
10   * but WITHOUT ANY WARRANTY; without even the implied warranty of
11   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12   * GNU General Public License for more details.
13   *
14   * You should have received a copy of the GNU General Public License
15   * along with this program; if not, see<http://www.gnu.org/licenses/>.
16   */
17  package org.exoplatform.wcm.connector.viewer;
18  
19  import java.awt.image.BufferedImage;
20  import java.awt.image.RenderedImage;
21  import java.io.BufferedInputStream;
22  import java.io.BufferedOutputStream;
23  import java.io.File;
24  import java.io.FileInputStream;
25  import java.io.FileNotFoundException;
26  import java.io.FileOutputStream;
27  import java.io.IOException;
28  import java.io.InputStream;
29  import java.io.OutputStream;
30  import java.io.Serializable;
31  import java.util.logging.Level;
32  import java.util.logging.Logger;
33  
34  import javax.imageio.ImageIO;
35  import javax.jcr.Node;
36  import javax.jcr.Session;
37  import javax.ws.rs.GET;
38  import javax.ws.rs.Path;
39  import javax.ws.rs.PathParam;
40  import javax.ws.rs.core.Response;
41  
42  import org.apache.commons.lang.StringUtils;
43  import org.artofsolving.jodconverter.office.OfficeException;
44  import org.exoplatform.services.cache.CacheService;
45  import org.exoplatform.services.cache.ExoCache;
46  import org.exoplatform.services.cms.impl.Utils;
47  import org.exoplatform.services.cms.jodconverter.JodConverterService;
48  import org.exoplatform.services.cms.mimetype.DMSMimeTypeResolver;
49  import org.exoplatform.services.jcr.RepositoryService;
50  import org.exoplatform.services.jcr.core.ManageableRepository;
51  import org.exoplatform.services.jcr.ext.app.SessionProviderService;
52  import org.exoplatform.services.jcr.ext.common.SessionProvider;
53  import org.exoplatform.services.log.ExoLogger;
54  import org.exoplatform.services.log.Log;
55  import org.exoplatform.services.pdfviewer.ObjectKey;
56  import org.exoplatform.services.pdfviewer.PDFViewerService;
57  import org.exoplatform.services.rest.resource.ResourceContainer;
58  import org.exoplatform.services.wcm.utils.WCMCoreUtils;
59  import org.icepdf.core.exceptions.PDFException;
60  import org.icepdf.core.exceptions.PDFSecurityException;
61  import org.icepdf.core.pobjects.Document;
62  import org.icepdf.core.pobjects.Page;
63  import org.icepdf.core.pobjects.Stream;
64  import org.icepdf.core.util.GraphicsRenderingHints;
65  
66  /**
67   * Returns a PDF content to be displayed on the web page.
68   *
69   * @LevelAPI Provisional
70   *
71   * @anchor PDFViewerRESTService
72   */
73  @Path("/pdfviewer/{repoName}/")
74  public class PDFViewerRESTService implements ResourceContainer {
75  
76    private static final int MAX_NAME_LENGTH= 150;
77    private static final String LASTMODIFIED = "Last-Modified";
78    private static final String PDF_VIEWER_CACHE = "ecms.PDFViewerRestService";
79    private RepositoryService repositoryService_;
80    private ExoCache<Serializable, Object> pdfCache;
81    private JodConverterService jodConverter_;
82    private static final Log LOG  = ExoLogger.getLogger(PDFViewerRESTService.class.getName());
83  
84    public PDFViewerRESTService(RepositoryService repositoryService,
85                                CacheService caService,
86                                JodConverterService jodConverter) throws Exception {
87      repositoryService_ = repositoryService;
88      jodConverter_ = jodConverter;
89      PDFViewerService pdfViewerService = WCMCoreUtils.getService(PDFViewerService.class);
90      if(pdfViewerService != null){
91        pdfCache = pdfViewerService.getCache();
92      }else{
93        pdfCache = caService.getCacheInstance(PDF_VIEWER_CACHE);
94      }
95    }
96  
97    /**
98     * Returns a thumbnail image for a PDF document.
99     *
100    * @param repoName The repository name.
101    * @param wsName The workspace name.
102    * @param uuid The identifier of the document.
103    * @param pageNumber The page number.
104    * @param rotation The page rotation. The valid values are: 0.0f, 90.0f, 180.0f, 270.0f.
105    * @param scale The Zoom factor which is applied to the rendered page.
106    * @return Response inputstream.
107    * @throws Exception The exception
108    *
109    * @anchor PDFViewerRESTService.getCoverImage
110    */
111   @GET
112   @Path("/{workspaceName}/{pageNumber}/{rotation}/{scale}/{uuid}/")
113   public Response getCoverImage(@PathParam("repoName") String repoName,
114       @PathParam("workspaceName") String wsName,
115       @PathParam("uuid") String uuid,
116       @PathParam("pageNumber") String pageNumber,
117       @PathParam("rotation") String rotation,
118       @PathParam("scale") String scale) throws Exception {
119     return getImageByPageNumber(repoName, wsName, uuid, pageNumber, rotation, scale);
120   }
121   
122   /**
123    * Returns a pdf file for a PDF document.
124    *
125    * @param repoName The repository name.
126    * @param wsName The workspace name.
127    * @param uuid The identifier of the document. 
128    * @return Response inputstream.
129    * @throws Exception The exception
130    *
131    * @anchor PDFViewerRESTService.getPDFFile
132    */
133   @GET
134   @Path("/{workspaceName}/{uuid}/")
135   public Response getPDFFile(@PathParam("repoName") String repoName,
136       @PathParam("workspaceName") String wsName,
137       @PathParam("uuid") String uuid) throws Exception {
138     Session session = null;
139     InputStream is = null;
140     String fileName = null;
141     try {
142       ManageableRepository repository = repositoryService_.getCurrentRepository();
143       session = getSystemProvider().getSession(wsName, repository);
144       Node currentNode = session.getNodeByUUID(uuid);  
145       fileName = Utils.getTitle(currentNode);
146       File pdfFile = getPDFDocumentFile(currentNode, repoName);
147       is = new FileInputStream(pdfFile);      
148     } catch (Exception e) {
149       if (LOG.isErrorEnabled()) {
150         LOG.error(e);
151       }
152     }
153     return Response.ok(is).header("Content-Disposition","attachment; filename=\"" + fileName+"\"").build();
154   }
155 
156   private Response getImageByPageNumber(String repoName, String wsName, String uuid,
157       String pageNumber, String strRotation, String strScale) throws Exception {
158     StringBuilder bd = new StringBuilder();
159     StringBuilder bd1 = new StringBuilder();
160     StringBuilder bd2 = new StringBuilder();
161     bd.append(repoName).append("/").append(wsName).append("/").append(uuid);
162     Session session = null;
163     try {
164       Object objCache = pdfCache.get(new ObjectKey(bd.toString()));
165       InputStream is = null;
166       ManageableRepository repository = repositoryService_.getCurrentRepository();
167       session = getSystemProvider().getSession(wsName, repository);
168       Node currentNode = session.getNodeByUUID(uuid);
169       String lastModified = (String) pdfCache.get(new ObjectKey(bd1.append(bd.toString())
170                                                                 .append("/jcr:lastModified").toString()));
171       String baseVersion = (String) pdfCache.get(new ObjectKey(bd2.append(bd.toString())
172               .append("/jcr:baseVersion").toString()));
173       if(objCache!=null) {
174         File content = new File((String) pdfCache.get(new ObjectKey(bd.toString())));
175         if (!content.exists()) {
176           initDocument(currentNode, repoName);
177         }
178         is = pushToCache(new File((String) pdfCache.get(new ObjectKey(bd.toString()))),
179                           repoName, wsName, uuid, pageNumber, strRotation, strScale, lastModified, baseVersion);
180       } else {
181         File file = getPDFDocumentFile(currentNode, repoName);
182         is = pushToCache(file, repoName, wsName, uuid, pageNumber, strRotation, strScale, lastModified, baseVersion);
183       }
184       return Response.ok(is, "image").header(LASTMODIFIED, lastModified).build();
185     } catch (Exception e) {
186       if (LOG.isErrorEnabled()) {
187         LOG.error(e);
188       }
189     }
190     return Response.ok().build();
191   }
192 
193   private SessionProvider getSystemProvider() {
194     SessionProviderService service = WCMCoreUtils.getService(SessionProviderService.class);
195     return service.getSystemSessionProvider(null) ;
196   }
197 
198   private InputStream pushToCache(File content, String repoName, String wsName, String uuid,
199       String pageNumber, String strRotation, String strScale, String lastModified,
200       String baseVersion) throws FileNotFoundException {
201     StringBuilder bd = new StringBuilder();
202     bd.append(repoName).append("/").append(wsName).append("/").append(uuid).append("/").append(
203         pageNumber).append("/").append(strRotation).append("/").append(strScale);
204     StringBuilder bd1 = new StringBuilder().append(bd).append("/jcr:lastModified");
205     StringBuilder bd2 = new StringBuilder().append(bd).append("/jcr:baseVersion");
206     String filePath = (String) pdfCache.get(new ObjectKey(bd.toString()));
207     String fileModifiedTime = (String) pdfCache.get(new ObjectKey(bd1.toString()));
208     String jcrBaseVersion = (String) pdfCache.get(new ObjectKey(bd2.toString()));
209     if (filePath == null || !(new File(filePath).exists()) || !StringUtils.equals(baseVersion, fileModifiedTime) ||
210     !StringUtils.equals(jcrBaseVersion, baseVersion)) {
211       File file = buildFileImage(content, uuid, pageNumber, strRotation, strScale);
212       filePath = file.getPath();
213       pdfCache.put(new ObjectKey(bd.toString()), filePath);
214       pdfCache.put(new ObjectKey(bd1.toString()), lastModified);
215       pdfCache.put(new ObjectKey(bd2.toString()), baseVersion);
216     }
217     return new BufferedInputStream(new FileInputStream(new File(filePath)));
218   }
219 
220   private Document buildDocumentImage(File input, String name) {
221      Document document = new Document();
222 
223      // Turn off Log of org.icepdf.core.pobjects.Document to avoid printing error stack trace in case viewing
224      // a PDF file which use new Public Key Security Handler.
225      // TODO: Remove this statement after IcePDF fix this
226      Logger.getLogger(Document.class.toString()).setLevel(Level.OFF);
227 
228     // Capture the page image to file
229     try {
230       // cut the file name if name is too long, because OS allows only file with name < 250 characters
231       name = reduceFileNameSize(name);
232       document.setInputStream(new BufferedInputStream(new FileInputStream(input)), name);
233     } catch (PDFException ex) {
234       if (LOG.isDebugEnabled()) {
235         LOG.error("Error parsing PDF document " + ex);
236       }
237     } catch (PDFSecurityException ex) {
238       if (LOG.isDebugEnabled()) {
239         LOG.error("Error encryption not supported " + ex);
240       }
241     } catch (FileNotFoundException ex) {
242       if (LOG.isDebugEnabled()) {
243         LOG.error("Error file not found " + ex);
244       }
245     } catch (IOException ex) {
246       if (LOG.isDebugEnabled()) {
247         LOG.debug("Error handling PDF document: {} {}", name, ex.toString());
248       }
249     }
250 
251     return document;
252   }
253 
254   private File buildFileImage(File input, String path, String pageNumber, String strRotation, String strScale) {
255      Document document = buildDocumentImage(input, path);
256 
257      // Turn off Log of org.icepdf.core.pobjects.Stream to not print error stack trace in case
258      // viewing a PDF file including CCITT (Fax format) images
259      // TODO: Remove these statement and comments after IcePDF fix ECMS-3765
260      Logger.getLogger(Stream.class.toString()).setLevel(Level.OFF);
261 
262      // save page capture to file.
263      float scale = 1.0f;
264      try {
265        scale = Float.parseFloat(strScale);
266        // maximum scale support is 300%
267        if (scale > 3.0f) {
268          scale = 3.0f;
269        }
270      } catch (NumberFormatException e) {
271        scale = 1.0f;
272      }
273      float rotation = 0.0f;
274      try {
275        rotation = Float.parseFloat(strRotation);
276      } catch (NumberFormatException e) {
277        rotation = 0.0f;
278      }
279      int maximumOfPage = document.getNumberOfPages();
280      int pageNum = 1;
281      try {
282        pageNum = Integer.parseInt(pageNumber);
283      } catch(NumberFormatException e) {
284        pageNum = 1;
285      }
286      if(pageNum >= maximumOfPage) pageNum = maximumOfPage;
287      else if(pageNum < 1) pageNum = 1;
288      // Paint each pages content to an image and write the image to file
289      BufferedImage image = (BufferedImage) document.getPageImage(pageNum - 1, GraphicsRenderingHints.SCREEN,
290          Page.BOUNDARY_CROPBOX, rotation, scale);
291      RenderedImage rendImage = image;
292      File file = null;
293      try {
294        file= File.createTempFile("imageCapture1_" + pageNum,".png");
295        /*
296        file.deleteOnExit();
297          PM Comment : I removed this line because each deleteOnExit creates a reference in the JVM for future removal
298          Each JVM reference takes 1KB of system memory and leads to a memleak
299        */
300        ImageIO.write(rendImage, "png", file);
301      } catch (IOException e) {
302        if (LOG.isErrorEnabled()) {
303          LOG.error(e);
304        }
305      } finally {
306        image.flush();
307        // clean up resources
308        document.dispose();
309      }
310      return file;
311   }
312 
313   /**
314    * Initializes the PDF document from InputStream in the _nt\:file_ node.
315    * @param currentNode The name of the current node.
316    * @param repoName  The repository name.
317    * @return
318    * @throws Exception
319    */
320   public Document initDocument(Node currentNode, String repoName) throws Exception {
321     return buildDocumentImage(getPDFDocumentFile(currentNode, repoName), currentNode.getName());
322   }
323 
324   /**
325    * Writes PDF data to file.
326    * @param currentNode The name of the current node.
327    * @param repoName The repository name.
328    * @return
329    * @throws Exception
330    */
331   public File getPDFDocumentFile(Node currentNode, String repoName) throws Exception {
332     String wsName = currentNode.getSession().getWorkspace().getName();
333     String uuid = currentNode.getUUID();
334     StringBuilder bd = new StringBuilder();
335     StringBuilder bd1 = new StringBuilder();
336     StringBuilder bd2 = new StringBuilder();
337     bd.append(repoName).append("/").append(wsName).append("/").append(uuid);
338     bd1.append(bd).append("/jcr:lastModified");
339     bd2.append(bd).append("/jcr:baseVersion");
340     String path = (String) pdfCache.get(new ObjectKey(bd.toString()));
341     String lastModifiedTime = (String)pdfCache.get(new ObjectKey(bd1.toString()));
342     String baseVersion = (String)pdfCache.get(new ObjectKey(bd2.toString()));
343     File content = null;
344     String name = currentNode.getName().replaceAll(":","_");
345     Node contentNode = currentNode.getNode("jcr:content");
346     String lastModified = getJcrLastModified(currentNode);
347     String jcrBaseVersion = getJcrBaseVersion(currentNode);
348 
349     if (path == null || !(content = new File(path)).exists() || !lastModified.equals(lastModifiedTime) ||
350             !StringUtils.equals(baseVersion, jcrBaseVersion)) {
351       String mimeType = contentNode.getProperty("jcr:mimeType").getString();
352       InputStream input = new BufferedInputStream(contentNode.getProperty("jcr:data").getStream());
353       // Create temp file to store converted data of nt:file node
354       if (name.indexOf(".") > 0) name = name.substring(0, name.lastIndexOf("."));
355       // cut the file name if name is too long, because OS allows only file with name < 250 characters
356       name = reduceFileNameSize(name);
357       content = File.createTempFile(name + "_tmp", ".pdf");
358       /*
359       file.deleteOnExit();
360         PM Comment : I removed this line because each deleteOnExit creates a reference in the JVM for future removal
361         Each JVM reference takes 1KB of system memory and leads to a memleak
362       */
363       // Convert to pdf if need
364       String extension = DMSMimeTypeResolver.getInstance().getExtension(mimeType);
365       if ("pdf".equals(extension)) {
366         read(input, new BufferedOutputStream(new FileOutputStream(content)));
367       } else {
368         // create temp file to store original data of nt:file node
369         File in = File.createTempFile(name + "_tmp", "." + extension);
370         read(input, new BufferedOutputStream(new FileOutputStream(in)));
371         try {
372           boolean success = jodConverter_.convert(in, content, "pdf");
373           // If the converting was failure then delete the content temporary file
374           if (!success) {
375             content.delete();
376           }
377         } catch (OfficeException connection) {
378           content.delete();
379           if (LOG.isErrorEnabled()) {
380             LOG.error("Exception when using Office Service");
381           }
382         } finally {
383           in.delete();
384         }
385       }
386       if (content.exists()) {
387         pdfCache.put(new ObjectKey(bd.toString()), content.getPath());
388         pdfCache.put(new ObjectKey(bd1.toString()), lastModified);
389         pdfCache.put(new ObjectKey(bd2.toString()), jcrBaseVersion);
390       }
391     }
392     return content;
393   }
394 
395   private String getJcrLastModified(Node node) throws Exception {
396     Node checkedNode = node;
397     if (node.isNodeType("nt:frozenNode")) {
398       checkedNode = node.getSession().getNodeByUUID(node.getProperty("jcr:frozenUuid").getString());
399     }
400     return Utils.getJcrContentLastModified(checkedNode);
401   }
402 
403   private String getJcrBaseVersion(Node node) throws Exception {
404     Node checkedNode = node;
405     if (node.isNodeType("nt:frozenNode")) {
406       checkedNode = node.getSession().getNodeByUUID(node.getProperty("jcr:frozenUuid").getString());
407     }
408     return checkedNode.hasProperty("jcr:baseVersion") ? checkedNode.getProperty("jcr:baseVersion").getString() : null;
409   }
410 
411   private void read(InputStream is, OutputStream os) throws Exception {
412     int bufferLength = 1024;
413     int readLength = 0;
414     while (readLength > -1) {
415       byte[] chunk = new byte[bufferLength];
416       readLength = is.read(chunk);
417       if (readLength > 0) {
418         os.write(chunk, 0, readLength);
419       }
420     }
421     os.flush();
422     os.close();
423   }
424   
425   /**
426    * reduces the file name size. If the length is > 150, return the first 150 characters, else, return the original value
427    * @param name the name
428    * @return the reduced name 
429    */
430   private String reduceFileNameSize(String name) {
431     return (name != null && name.length() > MAX_NAME_LENGTH) ? name.substring(0, MAX_NAME_LENGTH) : name;
432   }
433 
434 
435 }