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}