PDFViewerRESTService.java

  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. import java.awt.image.BufferedImage;
  19. import java.awt.image.RenderedImage;
  20. import java.io.BufferedInputStream;
  21. import java.io.BufferedOutputStream;
  22. import java.io.File;
  23. import java.io.FileInputStream;
  24. import java.io.FileNotFoundException;
  25. import java.io.FileOutputStream;
  26. import java.io.IOException;
  27. import java.io.InputStream;
  28. import java.io.OutputStream;
  29. import java.io.Serializable;
  30. import java.util.logging.Level;
  31. import java.util.logging.Logger;

  32. import javax.imageio.ImageIO;
  33. import javax.jcr.Node;
  34. import javax.jcr.Session;
  35. import javax.ws.rs.GET;
  36. import javax.ws.rs.Path;
  37. import javax.ws.rs.PathParam;
  38. import javax.ws.rs.core.Response;

  39. import org.apache.commons.lang.StringUtils;
  40. import org.artofsolving.jodconverter.office.OfficeException;
  41. import org.exoplatform.services.cache.CacheService;
  42. import org.exoplatform.services.cache.ExoCache;
  43. import org.exoplatform.services.cms.impl.Utils;
  44. import org.exoplatform.services.cms.jodconverter.JodConverterService;
  45. import org.exoplatform.services.cms.mimetype.DMSMimeTypeResolver;
  46. import org.exoplatform.services.jcr.RepositoryService;
  47. import org.exoplatform.services.jcr.core.ManageableRepository;
  48. import org.exoplatform.services.jcr.ext.app.SessionProviderService;
  49. import org.exoplatform.services.jcr.ext.common.SessionProvider;
  50. import org.exoplatform.services.log.ExoLogger;
  51. import org.exoplatform.services.log.Log;
  52. import org.exoplatform.services.pdfviewer.ObjectKey;
  53. import org.exoplatform.services.pdfviewer.PDFViewerService;
  54. import org.exoplatform.services.rest.resource.ResourceContainer;
  55. import org.exoplatform.services.wcm.utils.WCMCoreUtils;
  56. import org.icepdf.core.exceptions.PDFException;
  57. import org.icepdf.core.exceptions.PDFSecurityException;
  58. import org.icepdf.core.pobjects.Document;
  59. import org.icepdf.core.pobjects.Page;
  60. import org.icepdf.core.pobjects.Stream;
  61. import org.icepdf.core.util.GraphicsRenderingHints;

  62. /**
  63.  * Returns a PDF content to be displayed on the web page.
  64.  *
  65.  * @LevelAPI Provisional
  66.  *
  67.  * @anchor PDFViewerRESTService
  68.  */
  69. @Path("/pdfviewer/{repoName}/")
  70. public class PDFViewerRESTService implements ResourceContainer {

  71.   private static final int MAX_NAME_LENGTH= 150;
  72.   private static final String LASTMODIFIED = "Last-Modified";
  73.   private static final String PDF_VIEWER_CACHE = "ecms.PDFViewerRestService";
  74.   private RepositoryService repositoryService_;
  75.   private ExoCache<Serializable, Object> pdfCache;
  76.   private JodConverterService jodConverter_;
  77.   private static final Log LOG  = ExoLogger.getLogger(PDFViewerRESTService.class.getName());

  78.   public PDFViewerRESTService(RepositoryService repositoryService,
  79.                               CacheService caService,
  80.                               JodConverterService jodConverter) throws Exception {
  81.     repositoryService_ = repositoryService;
  82.     jodConverter_ = jodConverter;
  83.     PDFViewerService pdfViewerService = WCMCoreUtils.getService(PDFViewerService.class);
  84.     if(pdfViewerService != null){
  85.       pdfCache = pdfViewerService.getCache();
  86.     }else{
  87.       pdfCache = caService.getCacheInstance(PDF_VIEWER_CACHE);
  88.     }
  89.   }

  90.   /**
  91.    * Returns a thumbnail image for a PDF document.
  92.    *
  93.    * @param repoName The repository name.
  94.    * @param wsName The workspace name.
  95.    * @param uuid The identifier of the document.
  96.    * @param pageNumber The page number.
  97.    * @param rotation The page rotation. The valid values are: 0.0f, 90.0f, 180.0f, 270.0f.
  98.    * @param scale The Zoom factor which is applied to the rendered page.
  99.    * @return Response inputstream.
  100.    * @throws Exception The exception
  101.    *
  102.    * @anchor PDFViewerRESTService.getCoverImage
  103.    */
  104.   @GET
  105.   @Path("/{workspaceName}/{pageNumber}/{rotation}/{scale}/{uuid}/")
  106.   public Response getCoverImage(@PathParam("repoName") String repoName,
  107.       @PathParam("workspaceName") String wsName,
  108.       @PathParam("uuid") String uuid,
  109.       @PathParam("pageNumber") String pageNumber,
  110.       @PathParam("rotation") String rotation,
  111.       @PathParam("scale") String scale) throws Exception {
  112.     return getImageByPageNumber(repoName, wsName, uuid, pageNumber, rotation, scale);
  113.   }
  114.  
  115.   /**
  116.    * Returns a pdf file for a PDF document.
  117.    *
  118.    * @param repoName The repository name.
  119.    * @param wsName The workspace name.
  120.    * @param uuid The identifier of the document.
  121.    * @return Response inputstream.
  122.    * @throws Exception The exception
  123.    *
  124.    * @anchor PDFViewerRESTService.getPDFFile
  125.    */
  126.   @GET
  127.   @Path("/{workspaceName}/{uuid}/")
  128.   public Response getPDFFile(@PathParam("repoName") String repoName,
  129.       @PathParam("workspaceName") String wsName,
  130.       @PathParam("uuid") String uuid) throws Exception {
  131.     Session session = null;
  132.     InputStream is = null;
  133.     String fileName = null;
  134.     try {
  135.       ManageableRepository repository = repositoryService_.getCurrentRepository();
  136.       session = getSystemProvider().getSession(wsName, repository);
  137.       Node currentNode = session.getNodeByUUID(uuid);  
  138.       fileName = Utils.getTitle(currentNode);
  139.       File pdfFile = getPDFDocumentFile(currentNode, repoName);
  140.       is = new FileInputStream(pdfFile);      
  141.     } catch (Exception e) {
  142.       if (LOG.isErrorEnabled()) {
  143.         LOG.error(e);
  144.       }
  145.     }
  146.     return Response.ok(is).header("Content-Disposition","attachment; filename=\"" + fileName+"\"").build();
  147.   }

  148.   private Response getImageByPageNumber(String repoName, String wsName, String uuid,
  149.       String pageNumber, String strRotation, String strScale) throws Exception {
  150.     StringBuilder bd = new StringBuilder();
  151.     StringBuilder bd1 = new StringBuilder();
  152.     StringBuilder bd2 = new StringBuilder();
  153.     bd.append(repoName).append("/").append(wsName).append("/").append(uuid);
  154.     Session session = null;
  155.     try {
  156.       Object objCache = pdfCache.get(new ObjectKey(bd.toString()));
  157.       InputStream is = null;
  158.       ManageableRepository repository = repositoryService_.getCurrentRepository();
  159.       session = getSystemProvider().getSession(wsName, repository);
  160.       Node currentNode = session.getNodeByUUID(uuid);
  161.       String lastModified = (String) pdfCache.get(new ObjectKey(bd1.append(bd.toString())
  162.                                                                 .append("/jcr:lastModified").toString()));
  163.       String baseVersion = (String) pdfCache.get(new ObjectKey(bd2.append(bd.toString())
  164.               .append("/jcr:baseVersion").toString()));
  165.       if(objCache!=null) {
  166.         File content = new File((String) pdfCache.get(new ObjectKey(bd.toString())));
  167.         if (!content.exists()) {
  168.           initDocument(currentNode, repoName);
  169.         }
  170.         is = pushToCache(new File((String) pdfCache.get(new ObjectKey(bd.toString()))),
  171.                           repoName, wsName, uuid, pageNumber, strRotation, strScale, lastModified, baseVersion);
  172.       } else {
  173.         File file = getPDFDocumentFile(currentNode, repoName);
  174.         is = pushToCache(file, repoName, wsName, uuid, pageNumber, strRotation, strScale, lastModified, baseVersion);
  175.       }
  176.       return Response.ok(is, "image").header(LASTMODIFIED, lastModified).build();
  177.     } catch (Exception e) {
  178.       if (LOG.isErrorEnabled()) {
  179.         LOG.error(e);
  180.       }
  181.     }
  182.     return Response.ok().build();
  183.   }

  184.   private SessionProvider getSystemProvider() {
  185.     SessionProviderService service = WCMCoreUtils.getService(SessionProviderService.class);
  186.     return service.getSystemSessionProvider(null) ;
  187.   }

  188.   private InputStream pushToCache(File content, String repoName, String wsName, String uuid,
  189.       String pageNumber, String strRotation, String strScale, String lastModified,
  190.       String baseVersion) throws FileNotFoundException {
  191.     StringBuilder bd = new StringBuilder();
  192.     bd.append(repoName).append("/").append(wsName).append("/").append(uuid).append("/").append(
  193.         pageNumber).append("/").append(strRotation).append("/").append(strScale);
  194.     StringBuilder bd1 = new StringBuilder().append(bd).append("/jcr:lastModified");
  195.     StringBuilder bd2 = new StringBuilder().append(bd).append("/jcr:baseVersion");
  196.     String filePath = (String) pdfCache.get(new ObjectKey(bd.toString()));
  197.     String fileModifiedTime = (String) pdfCache.get(new ObjectKey(bd1.toString()));
  198.     String jcrBaseVersion = (String) pdfCache.get(new ObjectKey(bd2.toString()));
  199.     if (filePath == null || !(new File(filePath).exists()) || !StringUtils.equals(baseVersion, fileModifiedTime) ||
  200.     !StringUtils.equals(jcrBaseVersion, baseVersion)) {
  201.       File file = buildFileImage(content, uuid, pageNumber, strRotation, strScale);
  202.       filePath = file.getPath();
  203.       pdfCache.put(new ObjectKey(bd.toString()), filePath);
  204.       pdfCache.put(new ObjectKey(bd1.toString()), lastModified);
  205.       pdfCache.put(new ObjectKey(bd2.toString()), baseVersion);
  206.     }
  207.     return new BufferedInputStream(new FileInputStream(new File(filePath)));
  208.   }

  209.   private Document buildDocumentImage(File input, String name) {
  210.      Document document = new Document();

  211.      // Turn off Log of org.icepdf.core.pobjects.Document to avoid printing error stack trace in case viewing
  212.      // a PDF file which use new Public Key Security Handler.
  213.      // TODO: Remove this statement after IcePDF fix this
  214.      Logger.getLogger(Document.class.toString()).setLevel(Level.OFF);

  215.     // Capture the page image to file
  216.     try {
  217.       // cut the file name if name is too long, because OS allows only file with name < 250 characters
  218.       name = reduceFileNameSize(name);
  219.       document.setInputStream(new BufferedInputStream(new FileInputStream(input)), name);
  220.     } catch (PDFException ex) {
  221.       if (LOG.isDebugEnabled()) {
  222.         LOG.error("Error parsing PDF document " + ex);
  223.       }
  224.     } catch (PDFSecurityException ex) {
  225.       if (LOG.isDebugEnabled()) {
  226.         LOG.error("Error encryption not supported " + ex);
  227.       }
  228.     } catch (FileNotFoundException ex) {
  229.       if (LOG.isDebugEnabled()) {
  230.         LOG.error("Error file not found " + ex);
  231.       }
  232.     } catch (IOException ex) {
  233.       if (LOG.isDebugEnabled()) {
  234.         LOG.debug("Error handling PDF document: {} {}", name, ex.toString());
  235.       }
  236.     }

  237.     return document;
  238.   }

  239.   private File buildFileImage(File input, String path, String pageNumber, String strRotation, String strScale) {
  240.      Document document = buildDocumentImage(input, path);

  241.      // Turn off Log of org.icepdf.core.pobjects.Stream to not print error stack trace in case
  242.      // viewing a PDF file including CCITT (Fax format) images
  243.      // TODO: Remove these statement and comments after IcePDF fix ECMS-3765
  244.      Logger.getLogger(Stream.class.toString()).setLevel(Level.OFF);

  245.      // save page capture to file.
  246.      float scale = 1.0f;
  247.      try {
  248.        scale = Float.parseFloat(strScale);
  249.        // maximum scale support is 300%
  250.        if (scale > 3.0f) {
  251.          scale = 3.0f;
  252.        }
  253.      } catch (NumberFormatException e) {
  254.        scale = 1.0f;
  255.      }
  256.      float rotation = 0.0f;
  257.      try {
  258.        rotation = Float.parseFloat(strRotation);
  259.      } catch (NumberFormatException e) {
  260.        rotation = 0.0f;
  261.      }
  262.      int maximumOfPage = document.getNumberOfPages();
  263.      int pageNum = 1;
  264.      try {
  265.        pageNum = Integer.parseInt(pageNumber);
  266.      } catch(NumberFormatException e) {
  267.        pageNum = 1;
  268.      }
  269.      if(pageNum >= maximumOfPage) pageNum = maximumOfPage;
  270.      else if(pageNum < 1) pageNum = 1;
  271.      // Paint each pages content to an image and write the image to file
  272.      BufferedImage image = (BufferedImage) document.getPageImage(pageNum - 1, GraphicsRenderingHints.SCREEN,
  273.          Page.BOUNDARY_CROPBOX, rotation, scale);
  274.      RenderedImage rendImage = image;
  275.      File file = null;
  276.      try {
  277.        file= File.createTempFile("imageCapture1_" + pageNum,".png");
  278.        /*
  279.        file.deleteOnExit();
  280.          PM Comment : I removed this line because each deleteOnExit creates a reference in the JVM for future removal
  281.          Each JVM reference takes 1KB of system memory and leads to a memleak
  282.        */
  283.        ImageIO.write(rendImage, "png", file);
  284.      } catch (IOException e) {
  285.        if (LOG.isErrorEnabled()) {
  286.          LOG.error(e);
  287.        }
  288.      } finally {
  289.        image.flush();
  290.        // clean up resources
  291.        document.dispose();
  292.      }
  293.      return file;
  294.   }

  295.   /**
  296.    * Initializes the PDF document from InputStream in the _nt\:file_ node.
  297.    * @param currentNode The name of the current node.
  298.    * @param repoName  The repository name.
  299.    * @return
  300.    * @throws Exception
  301.    */
  302.   public Document initDocument(Node currentNode, String repoName) throws Exception {
  303.     return buildDocumentImage(getPDFDocumentFile(currentNode, repoName), currentNode.getName());
  304.   }

  305.   /**
  306.    * Writes PDF data to file.
  307.    * @param currentNode The name of the current node.
  308.    * @param repoName The repository name.
  309.    * @return
  310.    * @throws Exception
  311.    */
  312.   public File getPDFDocumentFile(Node currentNode, String repoName) throws Exception {
  313.     String wsName = currentNode.getSession().getWorkspace().getName();
  314.     String uuid = currentNode.getUUID();
  315.     StringBuilder bd = new StringBuilder();
  316.     StringBuilder bd1 = new StringBuilder();
  317.     StringBuilder bd2 = new StringBuilder();
  318.     bd.append(repoName).append("/").append(wsName).append("/").append(uuid);
  319.     bd1.append(bd).append("/jcr:lastModified");
  320.     bd2.append(bd).append("/jcr:baseVersion");
  321.     String path = (String) pdfCache.get(new ObjectKey(bd.toString()));
  322.     String lastModifiedTime = (String)pdfCache.get(new ObjectKey(bd1.toString()));
  323.     String baseVersion = (String)pdfCache.get(new ObjectKey(bd2.toString()));
  324.     File content = null;
  325.     String name = currentNode.getName().replaceAll(":","_");
  326.     Node contentNode = currentNode.getNode("jcr:content");
  327.     String lastModified = getJcrLastModified(currentNode);
  328.     String jcrBaseVersion = getJcrBaseVersion(currentNode);

  329.     if (path == null || !(content = new File(path)).exists() || !lastModified.equals(lastModifiedTime) ||
  330.             !StringUtils.equals(baseVersion, jcrBaseVersion)) {
  331.       String mimeType = contentNode.getProperty("jcr:mimeType").getString();
  332.       InputStream input = new BufferedInputStream(contentNode.getProperty("jcr:data").getStream());
  333.       // Create temp file to store converted data of nt:file node
  334.       if (name.indexOf(".") > 0) name = name.substring(0, name.lastIndexOf("."));
  335.       // cut the file name if name is too long, because OS allows only file with name < 250 characters
  336.       name = reduceFileNameSize(name);
  337.       content = File.createTempFile(name + "_tmp", ".pdf");
  338.       /*
  339.       file.deleteOnExit();
  340.         PM Comment : I removed this line because each deleteOnExit creates a reference in the JVM for future removal
  341.         Each JVM reference takes 1KB of system memory and leads to a memleak
  342.       */
  343.       // Convert to pdf if need
  344.       String extension = DMSMimeTypeResolver.getInstance().getExtension(mimeType);
  345.       if ("pdf".equals(extension)) {
  346.         read(input, new BufferedOutputStream(new FileOutputStream(content)));
  347.       } else {
  348.         // create temp file to store original data of nt:file node
  349.         File in = File.createTempFile(name + "_tmp", "." + extension);
  350.         read(input, new BufferedOutputStream(new FileOutputStream(in)));
  351.         try {
  352.           boolean success = jodConverter_.convert(in, content, "pdf");
  353.           // If the converting was failure then delete the content temporary file
  354.           if (!success) {
  355.             content.delete();
  356.           }
  357.         } catch (OfficeException connection) {
  358.           content.delete();
  359.           if (LOG.isErrorEnabled()) {
  360.             LOG.error("Exception when using Office Service");
  361.           }
  362.         } finally {
  363.           in.delete();
  364.         }
  365.       }
  366.       if (content.exists()) {
  367.         pdfCache.put(new ObjectKey(bd.toString()), content.getPath());
  368.         pdfCache.put(new ObjectKey(bd1.toString()), lastModified);
  369.         pdfCache.put(new ObjectKey(bd2.toString()), jcrBaseVersion);
  370.       }
  371.     }
  372.     return content;
  373.   }

  374.   private String getJcrLastModified(Node node) throws Exception {
  375.     Node checkedNode = node;
  376.     if (node.isNodeType("nt:frozenNode")) {
  377.       checkedNode = node.getSession().getNodeByUUID(node.getProperty("jcr:frozenUuid").getString());
  378.     }
  379.     return Utils.getJcrContentLastModified(checkedNode);
  380.   }

  381.   private String getJcrBaseVersion(Node node) throws Exception {
  382.     Node checkedNode = node;
  383.     if (node.isNodeType("nt:frozenNode")) {
  384.       checkedNode = node.getSession().getNodeByUUID(node.getProperty("jcr:frozenUuid").getString());
  385.     }
  386.     return checkedNode.hasProperty("jcr:baseVersion") ? checkedNode.getProperty("jcr:baseVersion").getString() : null;
  387.   }

  388.   private void read(InputStream is, OutputStream os) throws Exception {
  389.     int bufferLength = 1024;
  390.     int readLength = 0;
  391.     while (readLength > -1) {
  392.       byte[] chunk = new byte[bufferLength];
  393.       readLength = is.read(chunk);
  394.       if (readLength > 0) {
  395.         os.write(chunk, 0, readLength);
  396.       }
  397.     }
  398.     os.flush();
  399.     os.close();
  400.   }
  401.  
  402.   /**
  403.    * reduces the file name size. If the length is > 150, return the first 150 characters, else, return the original value
  404.    * @param name the name
  405.    * @return the reduced name
  406.    */
  407.   private String reduceFileNameSize(String name) {
  408.     return (name != null && name.length() > MAX_NAME_LENGTH) ? name.substring(0, MAX_NAME_LENGTH) : name;
  409.   }


  410. }