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