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