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