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}