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