001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH (http://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software, please see the
018 * company website: http://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: http://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.pdftools;
029
030import org.opencms.file.CmsFile;
031import org.opencms.file.CmsObject;
032import org.opencms.file.CmsResource;
033import org.opencms.file.wrapper.CmsWrappedResource;
034import org.opencms.main.CmsLog;
035import org.opencms.main.CmsResourceInitException;
036import org.opencms.main.CmsRuntimeException;
037import org.opencms.main.I_CmsResourceInit;
038import org.opencms.main.Messages;
039import org.opencms.main.OpenCms;
040import org.opencms.security.CmsSecurityException;
041import org.opencms.util.CmsStringUtil;
042import org.opencms.workplace.CmsWorkplace;
043
044import java.io.ByteArrayInputStream;
045import java.util.Collections;
046import java.util.Locale;
047import java.util.Map;
048
049import javax.servlet.http.HttpServletRequest;
050import javax.servlet.http.HttpServletResponse;
051
052import org.apache.commons.logging.Log;
053
054/**
055 * This resource handler handles URLs of the form /pdflink/{locale}/{formatter-id}/{detailname} and format
056 * the content identified by detailname using the JSP identified by formatter-id to generate XHTML which is then
057 * converted to PDF and returned directly by this handler.<p>
058 *
059 * In Online mode, the generated PDFs are cached on the real file system, while in Offline mode, the PDF data is always
060 * generated on-the-fly.<p>
061 */
062public class CmsPdfResourceHandler implements I_CmsResourceInit {
063
064    /** Mime type data for different file extensions. */
065    public static final String IMAGE_MIMETYPECONFIG = "png:image/png|gif:image/gif|jpg:image/jpeg";
066
067    /** Map of mime types for different file extensions. */
068    public static final Map<String, String> IMAGE_MIMETYPES = Collections.unmodifiableMap(
069        CmsStringUtil.splitAsMap(IMAGE_MIMETYPECONFIG, "|", ":"));
070
071    /** The logger instance for this class. */
072    private static final Log LOG = CmsLog.getLog(CmsPdfResourceHandler.class);
073    /** The cache for the generated PDFs. */
074    private CmsPdfCache m_pdfCache;
075
076    /** The converter used to generate the PDFs. */
077    private CmsPdfConverter m_pdfConverter = new CmsPdfConverter();
078
079    /** Cache for thumbnails. */
080    private CmsPdfThumbnailCache m_thumbnailCache = new CmsPdfThumbnailCache();
081
082    /**
083     * Creates a new instance.<p>
084     */
085    public CmsPdfResourceHandler() {
086
087        m_pdfCache = new CmsPdfCache();
088    }
089
090    /**
091     * @see org.opencms.main.I_CmsResourceInit#initResource(org.opencms.file.CmsResource, org.opencms.file.CmsObject, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
092     */
093    public CmsResource initResource(
094        CmsResource resource,
095        CmsObject cms,
096        HttpServletRequest request,
097        HttpServletResponse response) throws CmsResourceInitException, CmsSecurityException {
098
099        // check if the resource was already found or the path starts with '/system/'
100        boolean abort = (resource != null) || cms.getRequestContext().getUri().startsWith(CmsWorkplace.VFS_PATH_SYSTEM);
101        if (abort) {
102            // skip in all cases above
103            return resource;
104        }
105        if (response != null) {
106            String uri = cms.getRequestContext().getUri();
107
108            try {
109                if (uri.contains(CmsPdfLink.PDF_LINK_PREFIX)) {
110                    handlePdfLink(cms, request, response, uri);
111                    return null; // this will not be reached because the previous call will throw an exception
112                } else if (uri.contains(CmsPdfThumbnailLink.MARKER)) {
113                    handleThumbnailLink(cms, request, response, uri);
114                    return null; // this will not be reached because the previous call will throw an exception
115                } else {
116                    return null;
117                }
118            } catch (CmsResourceInitException e) {
119                throw e;
120            } catch (CmsSecurityException e) {
121                LOG.warn(e.getLocalizedMessage(), e);
122                throw e;
123            } catch (CmsPdfLink.CmsPdfLinkParseException e) {
124                // not a valid PDF link, just continue with the resource init chain
125                LOG.warn(e.getLocalizedMessage(), e);
126                return null;
127            } catch (CmsPdfThumbnailLink.ParseException e) {
128                LOG.warn(e.getLocalizedMessage(), e);
129                return null;
130            } catch (Exception e) {
131                // don't just return null, because we want a useful error message to be displayed
132                LOG.error(e.getLocalizedMessage(), e);
133                throw new CmsRuntimeException(
134                    Messages.get().container(
135                        Messages.ERR_RESOURCE_INIT_ABORTED_1,
136                        CmsPdfResourceHandler.class.getName()),
137                    e);
138            }
139        } else {
140            return null;
141        }
142    }
143
144    /**
145     * Handles a link for generating a PDF.<p>
146     *
147     * @param cms the current CMS context
148     * @param request the servlet request
149     * @param response the servlet response
150     * @param uri the current uri
151     *
152     * @throws Exception if something goes wrong
153     * @throws CmsResourceInitException if the resource initialization is cancelled
154     */
155    protected void handlePdfLink(CmsObject cms, HttpServletRequest request, HttpServletResponse response, String uri)
156    throws Exception {
157
158        CmsPdfLink linkObj = new CmsPdfLink(cms, uri);
159        CmsResource formatter = linkObj.getFormatter();
160        CmsResource content = linkObj.getContent();
161        LOG.info("Trying to render " + content.getRootPath() + " using " + formatter.getRootPath());
162        Locale locale = linkObj.getLocale();
163        CmsObject cmsForJspExecution = OpenCms.initCmsObject(cms);
164        cmsForJspExecution.getRequestContext().setLocale(locale);
165        cmsForJspExecution.getRequestContext().setSiteRoot("");
166        byte[] result = null;
167        String cacheParams = formatter.getStructureId() + ";" + formatter.getDateLastModified() + ";" + locale;
168        String cacheName = m_pdfCache.getCacheName(content, cacheParams);
169        if (cms.getRequestContext().getCurrentProject().isOnlineProject()) {
170            result = m_pdfCache.getCacheContent(cacheName);
171        }
172        if (result == null) {
173            cmsForJspExecution.getRequestContext().setUri(content.getRootPath());
174            byte[] xhtmlData = CmsPdfFormatterUtils.executeJsp(
175                cmsForJspExecution,
176                request,
177                response,
178                formatter,
179                content);
180
181            LOG.info("Rendered XHTML from " + content.getRootPath() + " using " + formatter.getRootPath());
182            if (LOG.isDebugEnabled()) {
183                logXhtmlOutput(formatter, content, xhtmlData);
184            }
185            // Use the same CmsObject we used for executing the JSP, because the same site root is needed to resolve external resources like images
186            result = m_pdfConverter.convertXhtmlToPdf(cmsForJspExecution, xhtmlData, "opencms://" + uri);
187            LOG.info("Converted XHTML to PDF, size=" + result.length);
188            m_pdfCache.saveCacheFile(cacheName, result);
189        } else {
190            LOG.info(
191                "Retrieved PDF data from cache for content "
192                    + content.getRootPath()
193                    + " and formatter "
194                    + formatter.getRootPath());
195        }
196        response.setContentType("application/pdf");
197        response.getOutputStream().write(result);
198        CmsResourceInitException initEx = new CmsResourceInitException(CmsPdfResourceHandler.class);
199        initEx.setClearErrors(true);
200        throw initEx;
201    }
202
203    /**
204     * Logs the XHTML output.<p>
205     *
206     * @param formatter the formatter
207     * @param content the content resource
208     * @param xhtmlData the XHTML data
209     */
210    protected void logXhtmlOutput(CmsResource formatter, CmsResource content, byte[] xhtmlData) {
211
212        try {
213            String xhtmlString = new String(xhtmlData, "UTF-8");
214            LOG.debug(
215                "(PDF generation) The formatter "
216                    + formatter.getRootPath()
217                    + " generated the following XHTML source from "
218                    + content.getRootPath()
219                    + ":");
220            LOG.debug(xhtmlString);
221        } catch (Exception e) {
222            LOG.debug(e.getLocalizedMessage(), e);
223        }
224    }
225
226    /**
227     * Handles a request for a PDF thumbnail.<p>
228     *
229     * @param cms the current CMS context
230     * @param request the servlet request
231     * @param response the servlet response
232     * @param uri the current uri
233     *
234     *  @throws Exception if something goes wrong
235     */
236    private void handleThumbnailLink(
237        CmsObject cms,
238        HttpServletRequest request,
239        HttpServletResponse response,
240        String uri) throws Exception {
241
242        String options = request.getParameter(CmsPdfThumbnailLink.PARAM_OPTIONS);
243        if (CmsStringUtil.isEmptyOrWhitespaceOnly(options)) {
244            options = "w:64";
245        }
246        CmsPdfThumbnailLink linkObj = new CmsPdfThumbnailLink(cms, uri, options);
247        CmsResource pdf = linkObj.getPdfResource();
248        CmsFile pdfFile = cms.readFile(pdf);
249        CmsPdfThumbnailGenerator thumbnailGenerator = new CmsPdfThumbnailGenerator();
250        // use a wrapped resource because we want the cache to store files with the correct (image file) extensions
251        CmsWrappedResource wrapperWithImageExtension = new CmsWrappedResource(pdfFile);
252        wrapperWithImageExtension.setRootPath(pdfFile.getRootPath() + "." + linkObj.getFormat());
253        String cacheName = m_thumbnailCache.getCacheName(
254            wrapperWithImageExtension.getResource(),
255            options + ";" + linkObj.getFormat());
256        byte[] imageData = m_thumbnailCache.getCacheContent(cacheName);
257        if (imageData == null) {
258            imageData = thumbnailGenerator.generateThumbnail(
259                new ByteArrayInputStream(pdfFile.getContents()),
260                linkObj.getWidth(),
261                linkObj.getHeight(),
262                linkObj.getFormat(),
263                linkObj.getPage());
264            m_thumbnailCache.saveCacheFile(cacheName, imageData);
265        }
266        response.setContentType(IMAGE_MIMETYPES.get(linkObj.getFormat()));
267        response.getOutputStream().write(imageData);
268        CmsResourceInitException initEx = new CmsResourceInitException(CmsPdfResourceHandler.class);
269        initEx.setClearErrors(true);
270        throw initEx;
271
272    }
273
274}