/*
* 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 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(PDFViewerRESTService.class.getName());
}
}
/**
* 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();
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()));
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);
} else {
File file = getPDFDocumentFile(currentNode, repoName);
is = pushToCache(file, repoName, wsName, uuid, pageNumber, strRotation, strScale, lastModified);
}
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) 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");
String filePath = (String) pdfCache.get(new ObjectKey(bd.toString()));
String fileModifiedTime = (String) pdfCache.get(new ObjectKey(bd1.toString()));
if (filePath == null || !(new File(filePath).exists()) || !StringUtils.equals(lastModified, fileModifiedTime)) {
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);
}
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();
bd.append(repoName).append("/").append(wsName).append("/").append(uuid);
bd1.append(bd).append("/jcr:lastModified");
String path = (String) pdfCache.get(new ObjectKey(bd.toString()));
String lastModifiedTime = (String)pdfCache.get(new ObjectKey(bd1.toString()));
File content = null;
String name = currentNode.getName().replaceAll(":","_");
Node contentNode = currentNode.getNode("jcr:content");
String lastModified = getJcrLastModified(currentNode);
if (path == null || !(content = new File(path)).exists() || !lastModified.equals(lastModifiedTime)) {
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);
}
}
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 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;
}
}