001/*
002 * (C) Copyright 2006-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 *     Antoine Taillefer
018 */
019
020package org.nuxeo.ecm.diff.content.restlet;
021
022import static org.apache.commons.lang3.StringUtils.isBlank;
023
024import java.io.IOException;
025import java.io.Serializable;
026import java.io.UnsupportedEncodingException;
027import java.net.URLDecoder;
028import java.util.HashMap;
029import java.util.List;
030import java.util.Locale;
031import java.util.Map;
032
033import javax.servlet.http.HttpServletRequest;
034import javax.servlet.http.HttpServletResponse;
035
036import org.apache.commons.collections.CollectionUtils;
037import org.apache.commons.lang3.LocaleUtils;
038import org.apache.commons.lang3.StringUtils;
039import org.apache.commons.logging.Log;
040import org.apache.commons.logging.LogFactory;
041import org.nuxeo.ecm.core.api.Blob;
042import org.nuxeo.ecm.core.api.CloseableCoreSession;
043import org.nuxeo.ecm.core.api.CoreInstance;
044import org.nuxeo.ecm.core.api.DocumentModel;
045import org.nuxeo.ecm.core.api.IdRef;
046import org.nuxeo.ecm.core.api.NuxeoException;
047import org.nuxeo.ecm.core.convert.api.ConverterNotRegistered;
048import org.nuxeo.ecm.core.io.download.DownloadService;
049import org.nuxeo.ecm.diff.content.ContentDiffAdapter;
050import org.nuxeo.ecm.diff.content.ContentDiffHelper;
051import org.nuxeo.ecm.diff.content.adapter.base.ContentDiffConversionType;
052import org.nuxeo.ecm.platform.ui.web.restAPI.BaseNuxeoRestlet;
053import org.nuxeo.runtime.api.Framework;
054import org.restlet.Request;
055import org.restlet.Response;
056import org.restlet.data.MediaType;
057import org.restlet.data.Status;
058
059/**
060 * Restlet to retrieve the content diff of a given property between two documents.
061 *
062 * @author Antoine Taillefer
063 * @since 5.6
064 */
065public class ContentDiffRestlet extends BaseNuxeoRestlet {
066
067    private static final Log log = LogFactory.getLog(ContentDiffRestlet.class);
068
069    protected Locale locale;
070
071    protected DocumentModel leftDoc;
072
073    protected DocumentModel rightDoc;
074
075    @Override
076    public void handle(Request req, Response res) {
077        logDeprecation();
078        HttpServletResponse response = getHttpResponse(res);
079        HttpServletRequest request = getHttpRequest(req);
080
081        String repo = (String) req.getAttributes().get("repo");
082        String leftDocId = (String) req.getAttributes().get("leftDocId");
083        String rightDocId = (String) req.getAttributes().get("rightDocId");
084        String xpath = (String) req.getAttributes().get("fieldXPath");
085        xpath = xpath.replace("--", "/");
086
087        // Get subPath for other content diff blobs, such as images
088        List<String> segments = req.getResourceRef().getSegments();
089        StringBuilder sb = new StringBuilder();
090        int pos = segments.indexOf("restAPI") + 6;
091        for (int i = pos; i < segments.size(); i++) {
092            sb.append(segments.get(i));
093            sb.append("/");
094        }
095        String subPath = sb.substring(0, sb.length() - 1);
096
097        // Check conversion type param, default is html.
098        String conversionTypeParam = getQueryParamValue(req, ContentDiffHelper.CONVERSION_TYPE_URL_PARAM_NAME,
099                ContentDiffConversionType.html.name());
100        ContentDiffConversionType conversionType = ContentDiffConversionType.valueOf(conversionTypeParam);
101
102        // Check locale
103        String localeParam = getQueryParamValue(req, ContentDiffHelper.LOCALE_URL_PARAM_NAME, null);
104        locale = isBlank(localeParam) ? Locale.getDefault() : LocaleUtils.toLocale(localeParam);
105
106        try {
107            xpath = URLDecoder.decode(xpath, "UTF-8");
108            subPath = URLDecoder.decode(subPath, "UTF-8");
109        } catch (UnsupportedEncodingException e) {
110            log.error(e);
111        }
112
113        if (repo == null || repo.equals("*")) {
114            handleError(res, "You must specify a repository.");
115            return;
116        }
117        if (leftDocId == null || leftDocId.equals("*")) {
118            handleError(res, "You must specify a left document id.");
119            return;
120        }
121        if (rightDocId == null || rightDocId.equals("*")) {
122            handleError(res, "You must specify a right document id.");
123            return;
124        }
125        try (CloseableCoreSession documentManager = CoreInstance.openCoreSession(repo)) {
126            leftDoc = documentManager.getDocument(new IdRef(leftDocId));
127            rightDoc = documentManager.getDocument(new IdRef(rightDocId));
128
129        List<Blob> contentDiffBlobs = initCachedContentDiffBlobs(res, xpath, conversionType);
130        if (CollectionUtils.isEmpty(contentDiffBlobs)) {
131            // Response was already handled by initCachedContentDiffBlobs
132            return;
133        }
134
135        // find blob
136        Blob blob = null;
137        if (StringUtils.isEmpty(subPath)) {
138            blob = contentDiffBlobs.get(0);
139            blob.setMimeType("text/html");
140        } else {
141            for (Blob b : contentDiffBlobs) {
142                if (subPath.equals(b.getFilename())) {
143                    blob = b;
144                    break;
145                }
146            }
147        }
148        if (blob == null) {
149            res.setStatus(Status.CLIENT_ERROR_NOT_FOUND);
150            return;
151        }
152
153        response.setHeader("Cache-Control", "no-cache");
154        response.setHeader("Pragma", "no-cache");
155
156        String reason = "contentDiff";
157        final Blob fblob = blob;
158        Boolean inline = Boolean.TRUE;
159        Map<String, Serializable> extendedInfos = new HashMap<>();
160        extendedInfos.put("subPath", subPath);
161        extendedInfos.put("leftDocId", leftDocId);
162        extendedInfos.put("rightDocId", rightDocId);
163        // check permission on right doc (downloadBlob will check on the left one)
164        DownloadService downloadService = Framework.getService(DownloadService.class);
165        if (!downloadService.checkPermission(rightDoc, xpath, blob, reason, extendedInfos)) {
166            res.setStatus(Status.CLIENT_ERROR_FORBIDDEN);
167            return;
168        }
169
170            downloadService.downloadBlob(request, response, leftDoc, xpath, blob, blob.getFilename(), reason,
171                    extendedInfos, inline, byteRange -> setEntityToBlobOutput(fblob, byteRange, res));
172        } catch (NuxeoException | IOException e) {
173            handleError(res, e);
174        }
175    }
176
177    private List<Blob> initCachedContentDiffBlobs(Response res, String xpath,
178            ContentDiffConversionType conversionType) {
179
180        ContentDiffAdapter contentDiffAdapter = leftDoc.getAdapter(ContentDiffAdapter.class);
181
182        if (contentDiffAdapter == null) {
183            handleNoContentDiff(res, xpath, null);
184            return null;
185        }
186
187        List<Blob> contentDiffBlobs;
188        try {
189            if (xpath.equals(ContentDiffHelper.DEFAULT_XPATH)) {
190                contentDiffBlobs = contentDiffAdapter.getFileContentDiffBlobs(rightDoc, conversionType, locale);
191            } else {
192                contentDiffBlobs = contentDiffAdapter.getFileContentDiffBlobs(rightDoc, xpath, conversionType, locale);
193            }
194        } catch (NuxeoException ce) {
195            handleNoContentDiff(res, xpath, ce);
196            return null;
197        }
198
199        if (CollectionUtils.isEmpty(contentDiffBlobs)) {
200            handleNoContentDiff(res, xpath, null);
201            return null;
202        }
203        return contentDiffBlobs;
204    }
205
206    protected void handleNoContentDiff(Response res, String xpath, NuxeoException e) {
207        StringBuilder sb = new StringBuilder();
208
209        sb.append("<html><body><center><h1>");
210        if (e == null) {
211            sb.append("No content diff is available for these documents</h1>");
212        } else {
213            sb.append("Content diff can not be generated for these documents</h1>");
214            sb.append("<pre>Blob path: ");
215            sb.append(xpath);
216            sb.append("</pre>");
217            sb.append("<pre>");
218            if (e instanceof ConverterNotRegistered) {
219                sb.append(e.getMessage());
220            } else {
221                sb.append(e.toString());
222            }
223            sb.append("</pre>");
224        }
225
226        sb.append("</center></body></html>");
227        log.error("Could not build content diff for missing blob at " + xpath, e);
228
229        res.setEntity(sb.toString(), MediaType.TEXT_HTML);
230        HttpServletResponse response = getHttpResponse(res);
231
232        response.setHeader("Content-Disposition", "inline");
233    }
234
235}