001/*
002 * (C) Copyright 2006-2014 Nuxeo SA (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl-2.1.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 */
015
016package org.nuxeo.ecm.platform.preview.restlet;
017
018import java.io.IOException;
019import java.io.InputStream;
020import java.io.OutputStream;
021import java.io.UnsupportedEncodingException;
022import java.net.URI;
023import java.net.URLDecoder;
024import java.util.ArrayList;
025import java.util.Collections;
026import java.util.List;
027import java.util.Locale;
028
029import javax.servlet.http.HttpServletResponse;
030
031import org.apache.commons.io.IOUtils;
032import org.apache.commons.logging.Log;
033import org.apache.commons.logging.LogFactory;
034import org.jboss.seam.ScopeType;
035import org.jboss.seam.annotations.In;
036import org.jboss.seam.annotations.Name;
037import org.jboss.seam.annotations.Scope;
038import org.jboss.seam.international.LocaleSelector;
039import org.nuxeo.ecm.core.api.Blob;
040import org.nuxeo.ecm.core.api.CoreSession;
041import org.nuxeo.ecm.core.api.DocumentModel;
042import org.nuxeo.ecm.core.api.DocumentNotFoundException;
043import org.nuxeo.ecm.core.api.IdRef;
044import org.nuxeo.ecm.core.api.blobholder.BlobHolder;
045import org.nuxeo.ecm.core.api.blobholder.DocumentBlobHolder;
046import org.nuxeo.ecm.core.blob.BlobManager;
047import org.nuxeo.ecm.core.blob.BlobManager.UsageHint;
048import org.nuxeo.ecm.platform.preview.api.HtmlPreviewAdapter;
049import org.nuxeo.ecm.platform.preview.api.NothingToPreviewException;
050import org.nuxeo.ecm.platform.preview.api.PreviewException;
051import org.nuxeo.ecm.platform.preview.helper.PreviewHelper;
052import org.nuxeo.ecm.platform.ui.web.api.NavigationContext;
053import org.nuxeo.ecm.platform.ui.web.restAPI.BaseNuxeoRestlet;
054import org.nuxeo.ecm.platform.util.RepositoryLocation;
055import org.nuxeo.ecm.platform.web.common.locale.LocaleProvider;
056import org.nuxeo.ecm.webapp.helpers.ResourcesAccessor;
057import org.nuxeo.runtime.api.Framework;
058import org.restlet.data.MediaType;
059import org.restlet.data.Request;
060import org.restlet.data.Response;
061import org.restlet.resource.OutputRepresentation;
062
063/**
064 * Provides a REST API to retrieve the preview of a document.
065 *
066 * @author tiry
067 */
068@Name("previewRestlet")
069@Scope(ScopeType.EVENT)
070public class PreviewRestlet extends BaseNuxeoRestlet {
071
072    private static final Log log = LogFactory.getLog(PreviewRestlet.class);
073
074    @In(create = true)
075    protected NavigationContext navigationContext;
076
077    protected CoreSession documentManager;
078
079    protected DocumentModel targetDocument;
080
081    @In(create = true)
082    protected transient LocaleSelector localeSelector;
083
084    @In(create = true)
085    protected transient ResourcesAccessor resourcesAccessor;
086
087    // cache duration in seconds
088    // protected static int MAX_CACHE_LIFE = 60 * 10;
089
090    // protected static final Map<String, PreviewCacheEntry> cachedAdapters =
091    // new ConcurrentHashMap<String, PreviewCacheEntry>();
092
093    protected static final List<String> previewInProcessing = Collections.synchronizedList(new ArrayList<String>());
094
095    @Override
096    public void handle(Request req, Response res) {
097
098        String repo = (String) req.getAttributes().get("repo");
099        String docid = (String) req.getAttributes().get("docid");
100        String xpath = (String) req.getAttributes().get("fieldPath");
101        xpath = xpath.replace("-", "/");
102        List<String> segments = req.getResourceRef().getSegments();
103        StringBuilder sb = new StringBuilder();
104        for (int i = 6; i < segments.size(); i++) {
105            sb.append(segments.get(i));
106            sb.append("/");
107        }
108        String subPath = sb.substring(0, sb.length() - 1);
109
110        try {
111            xpath = URLDecoder.decode(xpath, "UTF-8");
112            subPath = URLDecoder.decode(subPath, "UTF-8");
113        } catch (UnsupportedEncodingException e) {
114            log.error(e);
115        }
116
117        String blobPostProcessingParameter = getQueryParamValue(req, "blobPostProcessing", "false");
118        boolean blobPostProcessing = Boolean.parseBoolean(blobPostProcessingParameter);
119
120        if (repo == null || repo.equals("*")) {
121            handleError(res, "you must specify a repository");
122            return;
123        }
124        if (docid == null || repo.equals("*")) {
125            handleError(res, "you must specify a documentId");
126            return;
127        }
128        try {
129            navigationContext.setCurrentServerLocation(new RepositoryLocation(repo));
130            documentManager = navigationContext.getOrCreateDocumentManager();
131            targetDocument = documentManager.getDocument(new IdRef(docid));
132        } catch (DocumentNotFoundException e) {
133            handleError(res, e);
134            return;
135        }
136
137        // if it's a managed blob try to use the embed uri
138        Blob blobToPreview = getBlobToPreview(xpath);
139        BlobManager blobManager = Framework.getService(BlobManager.class);
140        try {
141            URI uri = blobManager.getURI(blobToPreview, UsageHint.EMBED, null);
142            if (uri != null) {
143                res.redirectSeeOther(uri.toString());
144                return;
145            }
146        } catch (IOException e) {
147            handleError(res, e);
148        }
149
150        localeSetup(req);
151
152        List<Blob> previewBlobs = initCachedBlob(res, xpath, blobPostProcessing);
153        if (previewBlobs == null || previewBlobs.isEmpty()) {
154            // response was already handled by initCachedBlob
155            return;
156        }
157        HttpServletResponse response = getHttpResponse(res);
158        response.setHeader("Cache-Control", "no-cache");
159        response.setHeader("Pragma", "no-cache");
160
161        try {
162            if (subPath == null || "".equals(subPath)) {
163                handlePreview(res, previewBlobs.get(0), "text/html");
164                return;
165            } else {
166                for (Blob blob : previewBlobs) {
167                    if (subPath.equals(blob.getFilename())) {
168                        handlePreview(res, blob, blob.getMimeType());
169                        return;
170                    }
171
172                }
173            }
174        } catch (IOException e) {
175            handleError(res, e);
176        }
177    }
178
179    /**
180     * @since 7.3
181     */
182    private Blob getBlobToPreview(String xpath) {
183        BlobHolder bh;
184        if ((xpath == null) || ("default".equals(xpath))) {
185            bh = targetDocument.getAdapter(BlobHolder.class);
186        } else {
187            bh = new DocumentBlobHolder(targetDocument, xpath);
188        }
189        return bh.getBlob();
190    }
191
192    /**
193     * @since 5.7
194     */
195    private void localeSetup(Request req) {
196        // Forward locale from HttpRequest to Seam context if not set into DM
197        Locale locale = Framework.getService(LocaleProvider.class).getLocale(documentManager);
198        if (locale == null) {
199            locale = getHttpRequest(req).getLocale();
200        }
201        localeSelector.setLocale(locale);
202    }
203
204    private List<Blob> initCachedBlob(Response res, String xpath, boolean blobPostProcessing) {
205
206        HtmlPreviewAdapter preview = null; // getFromCache(targetDocument,
207                                           // xpath);
208
209        // if (preview == null) {
210        preview = targetDocument.getAdapter(HtmlPreviewAdapter.class);
211        // }
212
213        if (preview == null) {
214            handleNoPreview(res, xpath, null);
215            return null;
216        }
217
218        List<Blob> previewBlobs = null;
219        try {
220            if (xpath.equals(PreviewHelper.PREVIEWURL_DEFAULTXPATH)) {
221                previewBlobs = preview.getFilePreviewBlobs(blobPostProcessing);
222            } else {
223                previewBlobs = preview.getFilePreviewBlobs(xpath, blobPostProcessing);
224            }
225            /*
226             * if (preview.cachable()) { updateCache(targetDocument, preview, xpath); }
227             */
228        } catch (PreviewException e) {
229            previewInProcessing.remove(targetDocument.getId());
230            handleNoPreview(res, xpath, e);
231            return null;
232        }
233
234        if (previewBlobs == null || previewBlobs.size() == 0) {
235            handleNoPreview(res, xpath, null);
236            return null;
237        }
238        return previewBlobs;
239    }
240
241    protected void handleNoPreview(Response res, String xpath, Exception e) {
242        StringBuilder sb = new StringBuilder();
243
244        sb.append("<html><body><center><h1>");
245        if (e == null) {
246            sb.append(resourcesAccessor.getMessages().get("label.not.available.preview") + "</h1>");
247        } else {
248            sb.append(resourcesAccessor.getMessages().get("label.cannot.generated.preview") + "</h1>");
249            sb.append("<pre>Technical issue:</pre>");
250            sb.append("<pre>Blob path: ");
251            sb.append(xpath);
252            sb.append("</pre>");
253            sb.append("<pre>");
254            sb.append(e.toString());
255            sb.append("</pre>");
256        }
257
258        sb.append("</center></body></html>");
259        if (e instanceof NothingToPreviewException) {
260            // Not an error, don't log
261        } else {
262            log.error("Could not build preview for missing blob at " + xpath, e);
263        }
264
265        res.setEntity(sb.toString(), MediaType.TEXT_HTML);
266        HttpServletResponse response = getHttpResponse(res);
267
268        response.setHeader("Content-Disposition", "inline");
269    }
270
271    protected void handlePreview(Response res, final Blob blob, String mimeType) throws IOException {
272        // blobs are always persistent, and temporary blobs are GCed only when not referenced anymore
273        res.setEntity(new OutputRepresentation(null) {
274            @Override
275            public void write(OutputStream outputStream) throws IOException {
276                try (InputStream stream = blob.getStream()) {
277                    IOUtils.copy(stream, outputStream);
278                }
279            }
280        });
281        HttpServletResponse response = getHttpResponse(res);
282
283        response.setHeader("Content-Disposition", "inline");
284        response.setContentType(mimeType);
285    }
286
287    /*
288     * protected void updateCache(DocumentModel doc, HtmlPreviewAdapter adapter, String xpath) {
289     * String docKey = doc.getId(); try { Calendar modified = (Calendar) doc.getProperty("dublincore", "modified");
290     * PreviewCacheEntry entry = new PreviewCacheEntry(modified, adapter, xpath); synchronized (cachedAdapters) {
291     * cachedAdapters.put(docKey, entry); } cacheGC(); } finally { previewInProcessing.remove(docKey); } } protected
292     * void removeFromCache(String key) { PreviewCacheEntry entry = cachedAdapters.get(key); if (entry != null) {
293     * entry.getAdapter().cleanup(); } synchronized (cachedAdapters) { cachedAdapters.remove(key); } } protected
294     * HtmlPreviewAdapter getFromCache(DocumentModel doc, String xpath) { String docKey =
295     * doc.getId(); while (previewInProcessing.contains(docKey)) { try { Thread.sleep(200); } catch
296     * (InterruptedException e) { log.error(e, e); } } if (cachedAdapters.containsKey(docKey)) { Calendar modified =
297     * (Calendar) doc.getProperty("dublincore", "modified"); PreviewCacheEntry entry = cachedAdapters.get(doc.getId());
298     * if (!entry.getModified().equals(modified) || !xpath.equals(entry.getXpath())) { removeFromCache(docKey); return
299     * null; } else { return entry.getAdapter(); } } else { return null; } } protected void cacheGC() { for (String key
300     * : cachedAdapters.keySet()) { long now = System.currentTimeMillis(); PreviewCacheEntry entry =
301     * cachedAdapters.get(key); if ((now - entry.getTimeStamp()) > MAX_CACHE_LIFE * 1000) { removeFromCache(key); } } }
302     */
303
304}