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