PDFViewerRESTService.java

/*
 * Copyright (C) 2003-2008 eXo Platform SAS.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Affero General Public License
 * as published by the Free Software Foundation; either version 3
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see<http://www.gnu.org/licenses/>.
 */
package org.exoplatform.wcm.connector.viewer;

import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.imageio.ImageIO;
import javax.jcr.Node;
import javax.jcr.Session;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.core.Response;

import org.apache.commons.lang.StringUtils;
import org.artofsolving.jodconverter.office.OfficeException;
import org.exoplatform.services.cache.CacheService;
import org.exoplatform.services.cache.ExoCache;
import org.exoplatform.services.cms.impl.Utils;
import org.exoplatform.services.cms.jodconverter.JodConverterService;
import org.exoplatform.services.cms.mimetype.DMSMimeTypeResolver;
import org.exoplatform.services.jcr.RepositoryService;
import org.exoplatform.services.jcr.core.ManageableRepository;
import org.exoplatform.services.jcr.ext.app.SessionProviderService;
import org.exoplatform.services.jcr.ext.common.SessionProvider;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;
import org.exoplatform.services.pdfviewer.ObjectKey;
import org.exoplatform.services.pdfviewer.PDFViewerService;
import org.exoplatform.services.rest.resource.ResourceContainer;
import org.exoplatform.services.wcm.utils.WCMCoreUtils;
import org.icepdf.core.exceptions.PDFException;
import org.icepdf.core.exceptions.PDFSecurityException;
import org.icepdf.core.pobjects.Document;
import org.icepdf.core.pobjects.Page;
import org.icepdf.core.pobjects.Stream;
import org.icepdf.core.util.GraphicsRenderingHints;

/**
 * Returns a PDF content to be displayed on the web page.
 *
 * @LevelAPI Provisional
 *
 * @anchor PDFViewerRESTService
 */
@Path("/pdfviewer/{repoName}/")
public class PDFViewerRESTService implements ResourceContainer {

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

  public PDFViewerRESTService(RepositoryService repositoryService,
                              CacheService caService,
                              JodConverterService jodConverter) throws Exception {
    repositoryService_ = repositoryService;
    jodConverter_ = jodConverter;
    PDFViewerService pdfViewerService = WCMCoreUtils.getService(PDFViewerService.class);
    if(pdfViewerService != null){
      pdfCache = pdfViewerService.getCache();
    }else{
      pdfCache = caService.getCacheInstance(PDF_VIEWER_CACHE);
    }
  }

  /**
   * Returns a thumbnail image for a PDF document.
   *
   * @param repoName The repository name.
   * @param wsName The workspace name.
   * @param uuid The identifier of the document.
   * @param pageNumber The page number.
   * @param rotation The page rotation. The valid values are: 0.0f, 90.0f, 180.0f, 270.0f.
   * @param scale The Zoom factor which is applied to the rendered page.
   * @return Response inputstream.
   * @throws Exception The exception
   *
   * @anchor PDFViewerRESTService.getCoverImage
   */
  @GET
  @Path("/{workspaceName}/{pageNumber}/{rotation}/{scale}/{uuid}/")
  public Response getCoverImage(@PathParam("repoName") String repoName,
      @PathParam("workspaceName") String wsName,
      @PathParam("uuid") String uuid,
      @PathParam("pageNumber") String pageNumber,
      @PathParam("rotation") String rotation,
      @PathParam("scale") String scale) throws Exception {
    return getImageByPageNumber(repoName, wsName, uuid, pageNumber, rotation, scale);
  }
  
  /**
   * Returns a pdf file for a PDF document.
   *
   * @param repoName The repository name.
   * @param wsName The workspace name.
   * @param uuid The identifier of the document. 
   * @return Response inputstream.
   * @throws Exception The exception
   *
   * @anchor PDFViewerRESTService.getPDFFile
   */
  @GET
  @Path("/{workspaceName}/{uuid}/")
  public Response getPDFFile(@PathParam("repoName") String repoName,
      @PathParam("workspaceName") String wsName,
      @PathParam("uuid") String uuid) throws Exception {
    Session session = null;
    InputStream is = null;
    String fileName = null;
    try {
      ManageableRepository repository = repositoryService_.getCurrentRepository();
      session = getSystemProvider().getSession(wsName, repository);
      Node currentNode = session.getNodeByUUID(uuid);  
      fileName = Utils.getTitle(currentNode);
      File pdfFile = getPDFDocumentFile(currentNode, repoName);
      is = new FileInputStream(pdfFile);      
    } catch (Exception e) {
      if (LOG.isErrorEnabled()) {
        LOG.error(e);
      }
    }
    return Response.ok(is).header("Content-Disposition","attachment; filename=\"" + fileName+"\"").build();
  }

  private Response getImageByPageNumber(String repoName, String wsName, String uuid,
      String pageNumber, String strRotation, String strScale) throws Exception {
    StringBuilder bd = new StringBuilder();
    StringBuilder bd1 = new StringBuilder();
    StringBuilder bd2 = new StringBuilder();
    bd.append(repoName).append("/").append(wsName).append("/").append(uuid);
    Session session = null;
    try {
      Object objCache = pdfCache.get(new ObjectKey(bd.toString()));
      InputStream is = null;
      ManageableRepository repository = repositoryService_.getCurrentRepository();
      session = getSystemProvider().getSession(wsName, repository);
      Node currentNode = session.getNodeByUUID(uuid);
      String lastModified = (String) pdfCache.get(new ObjectKey(bd1.append(bd.toString())
                                                                .append("/jcr:lastModified").toString()));
      String baseVersion = (String) pdfCache.get(new ObjectKey(bd2.append(bd.toString())
              .append("/jcr:baseVersion").toString()));
      if(objCache!=null) {
        File content = new File((String) pdfCache.get(new ObjectKey(bd.toString())));
        if (!content.exists()) {
          initDocument(currentNode, repoName);
        }
        is = pushToCache(new File((String) pdfCache.get(new ObjectKey(bd.toString()))),
                          repoName, wsName, uuid, pageNumber, strRotation, strScale, lastModified, baseVersion);
      } else {
        File file = getPDFDocumentFile(currentNode, repoName);
        is = pushToCache(file, repoName, wsName, uuid, pageNumber, strRotation, strScale, lastModified, baseVersion);
      }
      return Response.ok(is, "image").header(LASTMODIFIED, lastModified).build();
    } catch (Exception e) {
      if (LOG.isErrorEnabled()) {
        LOG.error(e);
      }
    }
    return Response.ok().build();
  }

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

  private InputStream pushToCache(File content, String repoName, String wsName, String uuid,
      String pageNumber, String strRotation, String strScale, String lastModified,
      String baseVersion) throws FileNotFoundException {
    StringBuilder bd = new StringBuilder();
    bd.append(repoName).append("/").append(wsName).append("/").append(uuid).append("/").append(
        pageNumber).append("/").append(strRotation).append("/").append(strScale);
    StringBuilder bd1 = new StringBuilder().append(bd).append("/jcr:lastModified");
    StringBuilder bd2 = new StringBuilder().append(bd).append("/jcr:baseVersion");
    String filePath = (String) pdfCache.get(new ObjectKey(bd.toString()));
    String fileModifiedTime = (String) pdfCache.get(new ObjectKey(bd1.toString()));
    String jcrBaseVersion = (String) pdfCache.get(new ObjectKey(bd2.toString()));
    if (filePath == null || !(new File(filePath).exists()) || !StringUtils.equals(baseVersion, fileModifiedTime) ||
    !StringUtils.equals(jcrBaseVersion, baseVersion)) {
      File file = buildFileImage(content, uuid, pageNumber, strRotation, strScale);
      filePath = file.getPath();
      pdfCache.put(new ObjectKey(bd.toString()), filePath);
      pdfCache.put(new ObjectKey(bd1.toString()), lastModified);
      pdfCache.put(new ObjectKey(bd2.toString()), baseVersion);
    }
    return new BufferedInputStream(new FileInputStream(new File(filePath)));
  }

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

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

    // Capture the page image to file
    try {
      // cut the file name if name is too long, because OS allows only file with name < 250 characters
      name = reduceFileNameSize(name);
      document.setInputStream(new BufferedInputStream(new FileInputStream(input)), name);
    } catch (PDFException ex) {
      if (LOG.isDebugEnabled()) {
        LOG.error("Error parsing PDF document " + ex);
      }
    } catch (PDFSecurityException ex) {
      if (LOG.isDebugEnabled()) {
        LOG.error("Error encryption not supported " + ex);
      }
    } catch (FileNotFoundException ex) {
      if (LOG.isDebugEnabled()) {
        LOG.error("Error file not found " + ex);
      }
    } catch (IOException ex) {
      if (LOG.isDebugEnabled()) {
        LOG.debug("Error handling PDF document: {} {}", name, ex.toString());
      }
    }

    return document;
  }

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

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

     // save page capture to file.
     float scale = 1.0f;
     try {
       scale = Float.parseFloat(strScale);
       // maximum scale support is 300%
       if (scale > 3.0f) {
         scale = 3.0f;
       }
     } catch (NumberFormatException e) {
       scale = 1.0f;
     }
     float rotation = 0.0f;
     try {
       rotation = Float.parseFloat(strRotation);
     } catch (NumberFormatException e) {
       rotation = 0.0f;
     }
     int maximumOfPage = document.getNumberOfPages();
     int pageNum = 1;
     try {
       pageNum = Integer.parseInt(pageNumber);
     } catch(NumberFormatException e) {
       pageNum = 1;
     }
     if(pageNum >= maximumOfPage) pageNum = maximumOfPage;
     else if(pageNum < 1) pageNum = 1;
     // Paint each pages content to an image and write the image to file
     BufferedImage image = (BufferedImage) document.getPageImage(pageNum - 1, GraphicsRenderingHints.SCREEN,
         Page.BOUNDARY_CROPBOX, rotation, scale);
     RenderedImage rendImage = image;
     File file = null;
     try {
       file= File.createTempFile("imageCapture1_" + pageNum,".png");
       /*
       file.deleteOnExit();
         PM Comment : I removed this line because each deleteOnExit creates a reference in the JVM for future removal
         Each JVM reference takes 1KB of system memory and leads to a memleak
       */
       ImageIO.write(rendImage, "png", file);
     } catch (IOException e) {
       if (LOG.isErrorEnabled()) {
         LOG.error(e);
       }
     } finally {
       image.flush();
       // clean up resources
       document.dispose();
     }
     return file;
  }

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

  /**
   * Writes PDF data to file.
   * @param currentNode The name of the current node.
   * @param repoName The repository name.
   * @return
   * @throws Exception
   */
  public File getPDFDocumentFile(Node currentNode, String repoName) throws Exception {
    String wsName = currentNode.getSession().getWorkspace().getName();
    String uuid = currentNode.getUUID();
    StringBuilder bd = new StringBuilder();
    StringBuilder bd1 = new StringBuilder();
    StringBuilder bd2 = new StringBuilder();
    bd.append(repoName).append("/").append(wsName).append("/").append(uuid);
    bd1.append(bd).append("/jcr:lastModified");
    bd2.append(bd).append("/jcr:baseVersion");
    String path = (String) pdfCache.get(new ObjectKey(bd.toString()));
    String lastModifiedTime = (String)pdfCache.get(new ObjectKey(bd1.toString()));
    String baseVersion = (String)pdfCache.get(new ObjectKey(bd2.toString()));
    File content = null;
    String name = currentNode.getName().replaceAll(":","_");
    Node contentNode = currentNode.getNode("jcr:content");
    String lastModified = getJcrLastModified(currentNode);
    String jcrBaseVersion = getJcrBaseVersion(currentNode);

    if (path == null || !(content = new File(path)).exists() || !lastModified.equals(lastModifiedTime) ||
            !StringUtils.equals(baseVersion, jcrBaseVersion)) {
      String mimeType = contentNode.getProperty("jcr:mimeType").getString();
      InputStream input = new BufferedInputStream(contentNode.getProperty("jcr:data").getStream());
      // Create temp file to store converted data of nt:file node
      if (name.indexOf(".") > 0) name = name.substring(0, name.lastIndexOf("."));
      // cut the file name if name is too long, because OS allows only file with name < 250 characters
      name = reduceFileNameSize(name);
      content = File.createTempFile(name + "_tmp", ".pdf");
      /*
      file.deleteOnExit();
        PM Comment : I removed this line because each deleteOnExit creates a reference in the JVM for future removal
        Each JVM reference takes 1KB of system memory and leads to a memleak
      */
      // Convert to pdf if need
      String extension = DMSMimeTypeResolver.getInstance().getExtension(mimeType);
      if ("pdf".equals(extension)) {
        read(input, new BufferedOutputStream(new FileOutputStream(content)));
      } else {
        // create temp file to store original data of nt:file node
        File in = File.createTempFile(name + "_tmp", "." + extension);
        read(input, new BufferedOutputStream(new FileOutputStream(in)));
        try {
          boolean success = jodConverter_.convert(in, content, "pdf");
          // If the converting was failure then delete the content temporary file
          if (!success) {
            content.delete();
          }
        } catch (OfficeException connection) {
          content.delete();
          if (LOG.isErrorEnabled()) {
            LOG.error("Exception when using Office Service");
          }
        } finally {
          in.delete();
        }
      }
      if (content.exists()) {
        pdfCache.put(new ObjectKey(bd.toString()), content.getPath());
        pdfCache.put(new ObjectKey(bd1.toString()), lastModified);
        pdfCache.put(new ObjectKey(bd2.toString()), jcrBaseVersion);
      }
    }
    return content;
  }

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

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

  private void read(InputStream is, OutputStream os) throws Exception {
    int bufferLength = 1024;
    int readLength = 0;
    while (readLength > -1) {
      byte[] chunk = new byte[bufferLength];
      readLength = is.read(chunk);
      if (readLength > 0) {
        os.write(chunk, 0, readLength);
      }
    }
    os.flush();
    os.close();
  }
  
  /**
   * reduces the file name size. If the length is > 150, return the first 150 characters, else, return the original value
   * @param name the name
   * @return the reduced name 
   */
  private String reduceFileNameSize(String name) {
    return (name != null && name.length() > MAX_NAME_LENGTH) ? name.substring(0, MAX_NAME_LENGTH) : name;
  }


}