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}