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