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 */ 015 016package org.nuxeo.ecm.platform.preview.restlet; 017 018import java.io.IOException; 019import java.io.Serializable; 020import java.io.UnsupportedEncodingException; 021import java.net.URI; 022import java.net.URLDecoder; 023import java.util.ArrayList; 024import java.util.Collections; 025import java.util.List; 026import java.util.Locale; 027import java.util.Map; 028 029import javax.servlet.http.HttpServletRequest; 030import javax.servlet.http.HttpServletResponse; 031 032import org.apache.commons.lang3.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.DocumentNotFoundException; 044import org.nuxeo.ecm.core.api.IdRef; 045import org.nuxeo.ecm.core.api.blobholder.BlobHolder; 046import org.nuxeo.ecm.core.api.blobholder.DocumentBlobHolder; 047import org.nuxeo.ecm.core.blob.BlobManager; 048import org.nuxeo.ecm.core.blob.BlobManager.UsageHint; 049import org.nuxeo.ecm.core.io.download.DownloadService; 050import org.nuxeo.ecm.platform.preview.api.HtmlPreviewAdapter; 051import org.nuxeo.ecm.platform.preview.api.NothingToPreviewException; 052import org.nuxeo.ecm.platform.preview.api.PreviewException; 053import org.nuxeo.ecm.platform.preview.helper.PreviewHelper; 054import org.nuxeo.ecm.platform.ui.web.api.NavigationContext; 055import org.nuxeo.ecm.platform.ui.web.restAPI.BaseNuxeoRestlet; 056import org.nuxeo.ecm.platform.util.RepositoryLocation; 057import org.nuxeo.ecm.platform.web.common.locale.LocaleProvider; 058import org.nuxeo.ecm.webapp.helpers.ResourcesAccessor; 059import org.nuxeo.runtime.api.Framework; 060import org.restlet.data.MediaType; 061import org.restlet.data.Request; 062import org.restlet.data.Response; 063import org.restlet.data.Status; 064 065/** 066 * Provides a REST API to retrieve the preview of a document. 067 * 068 * @author tiry 069 */ 070@Name("previewRestlet") 071@Scope(ScopeType.EVENT) 072public class PreviewRestlet extends BaseNuxeoRestlet { 073 074 private static final Log log = LogFactory.getLog(PreviewRestlet.class); 075 076 @In(create = true) 077 protected NavigationContext navigationContext; 078 079 protected CoreSession documentManager; 080 081 protected DocumentModel targetDocument; 082 083 @In(create = true) 084 protected transient LocaleSelector localeSelector; 085 086 @In(create = true) 087 protected transient ResourcesAccessor resourcesAccessor; 088 089 // cache duration in seconds 090 // protected static int MAX_CACHE_LIFE = 60 * 10; 091 092 // protected static final Map<String, PreviewCacheEntry> cachedAdapters = 093 // new ConcurrentHashMap<String, PreviewCacheEntry>(); 094 095 protected static final List<String> previewInProcessing = Collections.synchronizedList(new ArrayList<String>()); 096 097 @Override 098 public void handle(Request req, Response res) { 099 HttpServletRequest request = getHttpRequest(req); 100 HttpServletResponse response = getHttpResponse(res); 101 102 String repo = (String) req.getAttributes().get("repo"); 103 String docid = (String) req.getAttributes().get("docid"); 104 String xpath = (String) req.getAttributes().get("fieldPath"); 105 xpath = xpath.replace("-", "/"); 106 List<String> segments = req.getResourceRef().getSegments(); 107 StringBuilder sb = new StringBuilder(); 108 for (int i = 6; i < segments.size(); i++) { 109 sb.append(segments.get(i)); 110 sb.append("/"); 111 } 112 String subPath = sb.substring(0, sb.length() - 1); 113 114 try { 115 xpath = URLDecoder.decode(xpath, "UTF-8"); 116 subPath = URLDecoder.decode(subPath, "UTF-8"); 117 } catch (UnsupportedEncodingException e) { 118 log.error(e); 119 } 120 121 String blobPostProcessingParameter = getQueryParamValue(req, "blobPostProcessing", "false"); 122 boolean blobPostProcessing = Boolean.parseBoolean(blobPostProcessingParameter); 123 124 if (repo == null || repo.equals("*")) { 125 handleError(res, "you must specify a repository"); 126 return; 127 } 128 if (docid == null || repo.equals("*")) { 129 handleError(res, "you must specify a documentId"); 130 return; 131 } 132 try { 133 navigationContext.setCurrentServerLocation(new RepositoryLocation(repo)); 134 documentManager = navigationContext.getOrCreateDocumentManager(); 135 targetDocument = documentManager.getDocument(new IdRef(docid)); 136 } catch (DocumentNotFoundException e) { 137 handleError(res, e); 138 return; 139 } 140 141 // if it's a managed blob try to use the embed uri for preview 142 Blob blobToPreview = getBlobToPreview(xpath); 143 BlobManager blobManager = Framework.getService(BlobManager.class); 144 try { 145 URI uri = blobManager.getURI(blobToPreview, UsageHint.EMBED, null); 146 if (uri != null) { 147 res.redirectSeeOther(uri.toString()); 148 return; 149 } 150 } catch (IOException e) { 151 handleError(res, e); 152 return; 153 } 154 155 localeSetup(req); 156 157 List<Blob> previewBlobs = initCachedBlob(res, xpath, blobPostProcessing); 158 if (previewBlobs == null || previewBlobs.isEmpty()) { 159 // response was already handled by initCachedBlob 160 return; 161 } 162 163 // find blob 164 Blob blob = null; 165 if (StringUtils.isEmpty(subPath)) { 166 blob = previewBlobs.get(0); 167 blob.setMimeType("text/html"); 168 } else { 169 for (Blob b : previewBlobs) { 170 if (subPath.equals(b.getFilename())) { 171 blob = b; 172 break; 173 } 174 } 175 } 176 if (blob == null) { 177 res.setStatus(Status.CLIENT_ERROR_NOT_FOUND); 178 return; 179 } 180 181 response.setHeader("Cache-Control", "no-cache"); 182 response.setHeader("Pragma", "no-cache"); 183 184 String reason = "preview"; 185 final Blob fblob = blob; 186 if (xpath == null || "default".equals(xpath)) { 187 xpath = DownloadService.BLOBHOLDER_0; 188 } 189 Boolean inline = Boolean.TRUE; 190 Map<String, Serializable> extendedInfos = Collections.singletonMap("subPath", subPath); 191 DownloadService downloadService = Framework.getService(DownloadService.class); 192 try { 193 downloadService.downloadBlob(request, response, targetDocument, xpath, blob, blob.getFilename(), reason, 194 extendedInfos, inline, byteRange -> setEntityToBlobOutput(fblob, byteRange, res)); 195 } catch (IOException e) { 196 handleError(res, e); 197 } 198 } 199 200 /** 201 * @since 7.3 202 */ 203 private Blob getBlobToPreview(String xpath) { 204 BlobHolder bh; 205 if ((xpath == null) || ("default".equals(xpath))) { 206 bh = targetDocument.getAdapter(BlobHolder.class); 207 } else { 208 bh = new DocumentBlobHolder(targetDocument, xpath); 209 } 210 return bh.getBlob(); 211 } 212 213 /** 214 * @since 5.7 215 */ 216 private void localeSetup(Request req) { 217 // Forward locale from HttpRequest to Seam context if not set into DM 218 Locale locale = Framework.getService(LocaleProvider.class).getLocale(documentManager); 219 if (locale == null) { 220 locale = getHttpRequest(req).getLocale(); 221 } 222 localeSelector.setLocale(locale); 223 } 224 225 private List<Blob> initCachedBlob(Response res, String xpath, boolean blobPostProcessing) { 226 227 HtmlPreviewAdapter preview = null; // getFromCache(targetDocument, 228 // xpath); 229 230 // if (preview == null) { 231 preview = targetDocument.getAdapter(HtmlPreviewAdapter.class); 232 // } 233 234 if (preview == null) { 235 handleNoPreview(res, xpath, null); 236 return null; 237 } 238 239 List<Blob> previewBlobs = null; 240 try { 241 if (xpath.equals(PreviewHelper.PREVIEWURL_DEFAULTXPATH)) { 242 previewBlobs = preview.getFilePreviewBlobs(blobPostProcessing); 243 } else { 244 previewBlobs = preview.getFilePreviewBlobs(xpath, blobPostProcessing); 245 } 246 /* 247 * if (preview.cachable()) { updateCache(targetDocument, preview, xpath); } 248 */ 249 } catch (PreviewException e) { 250 previewInProcessing.remove(targetDocument.getId()); 251 handleNoPreview(res, xpath, e); 252 return null; 253 } 254 255 if (previewBlobs == null || previewBlobs.size() == 0) { 256 handleNoPreview(res, xpath, null); 257 return null; 258 } 259 return previewBlobs; 260 } 261 262 protected void handleNoPreview(Response res, String xpath, Exception e) { 263 StringBuilder sb = new StringBuilder(); 264 265 sb.append("<html><body><center><h1>"); 266 if (e == null) { 267 sb.append(resourcesAccessor.getMessages().get("label.not.available.preview") + "</h1>"); 268 } else { 269 sb.append(resourcesAccessor.getMessages().get("label.cannot.generated.preview") + "</h1>"); 270 sb.append("<pre>Technical issue:</pre>"); 271 sb.append("<pre>Blob path: "); 272 sb.append(xpath); 273 sb.append("</pre>"); 274 sb.append("<pre>"); 275 sb.append(e.toString()); 276 sb.append("</pre>"); 277 } 278 279 sb.append("</center></body></html>"); 280 if (e instanceof NothingToPreviewException) { 281 // Not an error, don't log 282 } else { 283 log.error("Could not build preview for missing blob at " + xpath, e); 284 } 285 286 res.setEntity(sb.toString(), MediaType.TEXT_HTML); 287 HttpServletResponse response = getHttpResponse(res); 288 289 response.setHeader("Content-Disposition", "inline"); 290 } 291 292}