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.Serializable;
020import java.io.UnsupportedEncodingException;
021import java.net.URI;
022import java.net.URLDecoder;
023import java.util.ArrayList;
024import java.util.Collections;
025import java.util.List;
026import java.util.Locale;
027import java.util.Map;
028
029import javax.servlet.http.HttpServletRequest;
030import javax.servlet.http.HttpServletResponse;
031
032import org.apache.commons.lang3.StringUtils;
033import org.apache.commons.logging.Log;
034import org.apache.commons.logging.LogFactory;
035import org.jboss.seam.ScopeType;
036import org.jboss.seam.annotations.In;
037import org.jboss.seam.annotations.Name;
038import org.jboss.seam.annotations.Scope;
039import org.jboss.seam.international.LocaleSelector;
040import org.nuxeo.ecm.core.api.Blob;
041import org.nuxeo.ecm.core.api.CoreSession;
042import org.nuxeo.ecm.core.api.DocumentModel;
043import org.nuxeo.ecm.core.api.DocumentNotFoundException;
044import org.nuxeo.ecm.core.api.IdRef;
045import org.nuxeo.ecm.core.api.blobholder.BlobHolder;
046import org.nuxeo.ecm.core.api.blobholder.DocumentBlobHolder;
047import org.nuxeo.ecm.core.blob.BlobManager;
048import org.nuxeo.ecm.core.blob.BlobManager.UsageHint;
049import org.nuxeo.ecm.core.io.download.DownloadService;
050import org.nuxeo.ecm.platform.preview.api.HtmlPreviewAdapter;
051import org.nuxeo.ecm.platform.preview.api.NothingToPreviewException;
052import org.nuxeo.ecm.platform.preview.api.PreviewException;
053import org.nuxeo.ecm.platform.preview.helper.PreviewHelper;
054import org.nuxeo.ecm.platform.ui.web.api.NavigationContext;
055import org.nuxeo.ecm.platform.ui.web.restAPI.BaseNuxeoRestlet;
056import org.nuxeo.ecm.platform.util.RepositoryLocation;
057import org.nuxeo.ecm.platform.web.common.locale.LocaleProvider;
058import org.nuxeo.ecm.webapp.helpers.ResourcesAccessor;
059import org.nuxeo.runtime.api.Framework;
060import org.restlet.data.MediaType;
061import org.restlet.data.Request;
062import org.restlet.data.Response;
063import org.restlet.data.Status;
064
065/**
066 * Provides a REST API to retrieve the preview of a document.
067 *
068 * @author tiry
069 */
070@Name("previewRestlet")
071@Scope(ScopeType.EVENT)
072public class PreviewRestlet extends BaseNuxeoRestlet {
073
074    private static final Log log = LogFactory.getLog(PreviewRestlet.class);
075
076    @In(create = true)
077    protected NavigationContext navigationContext;
078
079    protected CoreSession documentManager;
080
081    protected DocumentModel targetDocument;
082
083    @In(create = true)
084    protected transient LocaleSelector localeSelector;
085
086    @In(create = true)
087    protected transient ResourcesAccessor resourcesAccessor;
088
089    // cache duration in seconds
090    // protected static int MAX_CACHE_LIFE = 60 * 10;
091
092    // protected static final Map<String, PreviewCacheEntry> cachedAdapters =
093    // new ConcurrentHashMap<String, PreviewCacheEntry>();
094
095    protected static final List<String> previewInProcessing = Collections.synchronizedList(new ArrayList<String>());
096
097    @Override
098    public void handle(Request req, Response res) {
099        HttpServletRequest request = getHttpRequest(req);
100        HttpServletResponse response = getHttpResponse(res);
101
102        String repo = (String) req.getAttributes().get("repo");
103        String docid = (String) req.getAttributes().get("docid");
104        String xpath = (String) req.getAttributes().get("fieldPath");
105        xpath = xpath.replace("-", "/");
106        List<String> segments = req.getResourceRef().getSegments();
107        StringBuilder sb = new StringBuilder();
108        for (int i = 6; i < segments.size(); i++) {
109            sb.append(segments.get(i));
110            sb.append("/");
111        }
112        String subPath = sb.substring(0, sb.length() - 1);
113
114        try {
115            xpath = URLDecoder.decode(xpath, "UTF-8");
116            subPath = URLDecoder.decode(subPath, "UTF-8");
117        } catch (UnsupportedEncodingException e) {
118            log.error(e);
119        }
120
121        String blobPostProcessingParameter = getQueryParamValue(req, "blobPostProcessing", "false");
122        boolean blobPostProcessing = Boolean.parseBoolean(blobPostProcessingParameter);
123
124        if (repo == null || repo.equals("*")) {
125            handleError(res, "you must specify a repository");
126            return;
127        }
128        if (docid == null || repo.equals("*")) {
129            handleError(res, "you must specify a documentId");
130            return;
131        }
132        try {
133            navigationContext.setCurrentServerLocation(new RepositoryLocation(repo));
134            documentManager = navigationContext.getOrCreateDocumentManager();
135            targetDocument = documentManager.getDocument(new IdRef(docid));
136        } catch (DocumentNotFoundException e) {
137            handleError(res, e);
138            return;
139        }
140
141        // if it's a managed blob try to use the embed uri for preview
142        Blob blobToPreview = getBlobToPreview(xpath);
143        BlobManager blobManager = Framework.getService(BlobManager.class);
144        try {
145            URI uri = blobManager.getURI(blobToPreview, UsageHint.EMBED, null);
146            if (uri != null) {
147                res.redirectSeeOther(uri.toString());
148                return;
149            }
150        } catch (IOException e) {
151            handleError(res, e);
152            return;
153        }
154
155        localeSetup(req);
156
157        List<Blob> previewBlobs = initCachedBlob(res, xpath, blobPostProcessing);
158        if (previewBlobs == null || previewBlobs.isEmpty()) {
159            // response was already handled by initCachedBlob
160            return;
161        }
162
163        // find blob
164        Blob blob = null;
165        if (StringUtils.isEmpty(subPath)) {
166            blob = previewBlobs.get(0);
167            blob.setMimeType("text/html");
168        } else {
169            for (Blob b : previewBlobs) {
170                if (subPath.equals(b.getFilename())) {
171                    blob = b;
172                    break;
173                }
174            }
175        }
176        if (blob == null) {
177            res.setStatus(Status.CLIENT_ERROR_NOT_FOUND);
178            return;
179        }
180
181        response.setHeader("Cache-Control", "no-cache");
182        response.setHeader("Pragma", "no-cache");
183
184        String reason = "preview";
185        final Blob fblob = blob;
186        if (xpath == null || "default".equals(xpath)) {
187            xpath = DownloadService.BLOBHOLDER_0;
188        }
189        Boolean inline = Boolean.TRUE;
190        Map<String, Serializable> extendedInfos = Collections.singletonMap("subPath", subPath);
191        DownloadService downloadService = Framework.getService(DownloadService.class);
192        try {
193            downloadService.downloadBlob(request, response, targetDocument, xpath, blob, blob.getFilename(), reason,
194                    extendedInfos, inline, byteRange -> setEntityToBlobOutput(fblob, byteRange, res));
195        } catch (IOException e) {
196            handleError(res, e);
197        }
198    }
199
200    /**
201     * @since 7.3
202     */
203    private Blob getBlobToPreview(String xpath) {
204        BlobHolder bh;
205        if ((xpath == null) || ("default".equals(xpath))) {
206            bh = targetDocument.getAdapter(BlobHolder.class);
207        } else {
208            bh = new DocumentBlobHolder(targetDocument, xpath);
209        }
210        return bh.getBlob();
211    }
212
213    /**
214     * @since 5.7
215     */
216    private void localeSetup(Request req) {
217        // Forward locale from HttpRequest to Seam context if not set into DM
218        Locale locale = Framework.getService(LocaleProvider.class).getLocale(documentManager);
219        if (locale == null) {
220            locale = getHttpRequest(req).getLocale();
221        }
222        localeSelector.setLocale(locale);
223    }
224
225    private List<Blob> initCachedBlob(Response res, String xpath, boolean blobPostProcessing) {
226
227        HtmlPreviewAdapter preview = null; // getFromCache(targetDocument,
228                                           // xpath);
229
230        // if (preview == null) {
231        preview = targetDocument.getAdapter(HtmlPreviewAdapter.class);
232        // }
233
234        if (preview == null) {
235            handleNoPreview(res, xpath, null);
236            return null;
237        }
238
239        List<Blob> previewBlobs = null;
240        try {
241            if (xpath.equals(PreviewHelper.PREVIEWURL_DEFAULTXPATH)) {
242                previewBlobs = preview.getFilePreviewBlobs(blobPostProcessing);
243            } else {
244                previewBlobs = preview.getFilePreviewBlobs(xpath, blobPostProcessing);
245            }
246            /*
247             * if (preview.cachable()) { updateCache(targetDocument, preview, xpath); }
248             */
249        } catch (PreviewException e) {
250            previewInProcessing.remove(targetDocument.getId());
251            handleNoPreview(res, xpath, e);
252            return null;
253        }
254
255        if (previewBlobs == null || previewBlobs.size() == 0) {
256            handleNoPreview(res, xpath, null);
257            return null;
258        }
259        return previewBlobs;
260    }
261
262    protected void handleNoPreview(Response res, String xpath, Exception e) {
263        StringBuilder sb = new StringBuilder();
264
265        sb.append("<html><body><center><h1>");
266        if (e == null) {
267            sb.append(resourcesAccessor.getMessages().get("label.not.available.preview") + "</h1>");
268        } else {
269            sb.append(resourcesAccessor.getMessages().get("label.cannot.generated.preview") + "</h1>");
270            sb.append("<pre>Technical issue:</pre>");
271            sb.append("<pre>Blob path: ");
272            sb.append(xpath);
273            sb.append("</pre>");
274            sb.append("<pre>");
275            sb.append(e.toString());
276            sb.append("</pre>");
277        }
278
279        sb.append("</center></body></html>");
280        if (e instanceof NothingToPreviewException) {
281            // Not an error, don't log
282        } else {
283            log.error("Could not build preview for missing blob at " + xpath, e);
284        }
285
286        res.setEntity(sb.toString(), MediaType.TEXT_HTML);
287        HttpServletResponse response = getHttpResponse(res);
288
289        response.setHeader("Content-Disposition", "inline");
290    }
291
292}