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