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