001/* 002 * (C) Copyright 2006-2008 Nuxeo SAS (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.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 * Nuxeo - initial API and implementation 016 * 017 * $Id$ 018 * 019 */ 020package org.nuxeo.ecm.platform.pictures.tiles.restlets; 021 022import java.io.File; 023import java.io.IOException; 024import java.io.Serializable; 025import java.util.Calendar; 026import java.util.HashMap; 027import java.util.Map; 028import java.util.concurrent.ConcurrentHashMap; 029 030import javax.servlet.http.HttpServletRequest; 031import javax.servlet.http.HttpServletResponse; 032 033import org.nuxeo.common.utils.FileUtils; 034import org.nuxeo.ecm.core.api.Blob; 035import org.nuxeo.ecm.core.api.DocumentModel; 036import org.nuxeo.ecm.core.api.NuxeoException; 037import org.nuxeo.ecm.core.io.download.DownloadService; 038import org.nuxeo.ecm.platform.pictures.tiles.api.PictureTiles; 039import org.nuxeo.ecm.platform.pictures.tiles.api.adapter.PictureTilesAdapter; 040import org.nuxeo.ecm.platform.pictures.tiles.serializer.JSONPictureTilesSerializer; 041import org.nuxeo.ecm.platform.pictures.tiles.serializer.PictureTilesSerializer; 042import org.nuxeo.ecm.platform.pictures.tiles.serializer.XMLPictureTilesSerializer; 043import org.nuxeo.ecm.platform.ui.web.restAPI.BaseStatelessNuxeoRestlet; 044import org.nuxeo.runtime.api.Framework; 045import org.restlet.data.CharacterSet; 046import org.restlet.data.Form; 047import org.restlet.data.MediaType; 048import org.restlet.data.Request; 049import org.restlet.data.Response; 050 051/** 052 * Restlet to provide a REST API on top of the PictureTilingService. 053 * 054 * @author tiry 055 */ 056public class PictureTilesRestlets extends BaseStatelessNuxeoRestlet { 057 058 // cache duration in seconds 059 protected static int MAX_CACHE_LIFE = 60 * 10; 060 061 protected static Map<String, PictureTilesCachedEntry> cachedAdapters = new ConcurrentHashMap<String, PictureTilesCachedEntry>(); 062 063 @Override 064 public void handle(Request req, Response res) { 065 HttpServletRequest request = getHttpRequest(req); 066 HttpServletResponse response = getHttpResponse(res); 067 068 String repo = (String) req.getAttributes().get("repoId"); 069 String docid = (String) req.getAttributes().get("docId"); 070 Integer tileWidth = Integer.decode((String) req.getAttributes().get("tileWidth")); 071 Integer tileHeight = Integer.decode((String) req.getAttributes().get("tileHeight")); 072 Integer maxTiles = Integer.decode((String) req.getAttributes().get("maxTiles")); 073 074 Form form = req.getResourceRef().getQueryAsForm(); 075 String xpath = (String) form.getFirstValue("fieldPath"); 076 String x = form.getFirstValue("x"); 077 String y = form.getFirstValue("y"); 078 String format = form.getFirstValue("format"); 079 080 String test = form.getFirstValue("test"); 081 if (test != null) { 082 try { 083 handleSendTest(res, repo, docid, tileWidth, tileHeight, maxTiles); 084 return; 085 } catch (IOException e) { 086 handleError(res, e); 087 return; 088 } 089 } 090 091 if (repo == null || repo.equals("*")) { 092 handleError(res, "you must specify a repository"); 093 return; 094 } 095 if (docid == null || repo.equals("*")) { 096 handleError(res, "you must specify a documentId"); 097 return; 098 } 099 Boolean init = initRepositoryAndTargetDocument(res, repo, docid); 100 101 if (!init) { 102 handleError(res, "unable to init repository connection"); 103 return; 104 } 105 106 PictureTilesAdapter adapter; 107 try { 108 adapter = getFromCache(targetDocument, xpath); 109 if (adapter == null) { 110 adapter = targetDocument.getAdapter(PictureTilesAdapter.class); 111 if ((xpath != null) && (!"".equals(xpath))) { 112 adapter.setXPath(xpath); 113 } 114 updateCache(targetDocument, adapter, xpath); 115 } 116 } catch (NuxeoException e) { 117 handleError(res, e); 118 return; 119 } 120 121 if (adapter == null) { 122 handleNoTiles(res, null); 123 return; 124 } 125 126 PictureTiles tiles = null; 127 try { 128 tiles = adapter.getTiles(tileWidth, tileHeight, maxTiles); 129 } catch (NuxeoException e) { 130 handleError(res, e); 131 } 132 133 if ((x == null) || (y == null)) { 134 handleSendInfo(res, tiles, format); 135 return; 136 } 137 138 final Blob image; 139 try { 140 image = tiles.getTile(Integer.decode(x), Integer.decode(y)); 141 } catch (NuxeoException | IOException e) { 142 handleError(res, e); 143 return; 144 } 145 146 String reason = "tile"; 147 Boolean inline = Boolean.TRUE; 148 Map<String, Serializable> extendedInfos = new HashMap<>(); 149 extendedInfos.put("x", x); 150 extendedInfos.put("y", y); 151 DownloadService downloadService = Framework.getService(DownloadService.class); 152 try { 153 downloadService.downloadBlob(request, response, targetDocument, xpath, image, image.getFilename(), reason, 154 extendedInfos, inline, byteRange -> setEntityToBlobOutput(image, byteRange, res)); 155 } catch (IOException e) { 156 handleError(res, e); 157 } 158 } 159 160 protected void handleSendTest(Response res, String repoId, String docId, Integer tileWidth, Integer tileHeight, 161 Integer maxTiles) throws IOException { 162 MediaType mt = null; 163 mt = MediaType.TEXT_HTML; 164 165 File file = FileUtils.getResourceFileFromContext("testTiling.html"); 166 String html = FileUtils.readFile(file); 167 168 html = html.replace("$repoId$", repoId); 169 html = html.replace("$docId$", docId); 170 html = html.replace("$tileWidth$", tileWidth.toString()); 171 html = html.replace("$tileHeight$", tileHeight.toString()); 172 html = html.replace("$maxTiles$", maxTiles.toString()); 173 174 res.setEntity(html, mt); 175 } 176 177 protected void handleSendInfo(Response res, PictureTiles tiles, String format) { 178 if (format == null) { 179 format = "XML"; 180 } 181 MediaType mt = null; 182 PictureTilesSerializer serializer = null; 183 184 if (format.equalsIgnoreCase("json")) { 185 serializer = new JSONPictureTilesSerializer(); 186 mt = MediaType.APPLICATION_JSON; 187 } else { 188 serializer = new XMLPictureTilesSerializer(); 189 mt = MediaType.TEXT_XML; 190 } 191 192 res.setEntity(serializer.serialize(tiles), mt); 193 res.getEntity().setCharacterSet(CharacterSet.UTF_8); 194 195 HttpServletResponse response = getHttpResponse(res); 196 response.setHeader("Cache-Control", "no-cache"); 197 response.setHeader("Pragma", "no-cache"); 198 } 199 200 protected void handleNoTiles(Response res, Exception e) { 201 StringBuilder sb = new StringBuilder(); 202 203 sb.append("<html><body><center><h1>"); 204 if (e == null) { 205 sb.append("No Tiling is available for this document</h1>"); 206 } else { 207 sb.append("Picture Tiling can not be generated for this document</h1>"); 208 sb.append("<br/><pre>"); 209 sb.append(e.toString()); 210 sb.append("</pre>"); 211 } 212 213 sb.append("</center></body></html>"); 214 215 res.setEntity(sb.toString(), MediaType.TEXT_HTML); 216 HttpServletResponse response = getHttpResponse(res); 217 response.setHeader("Content-Disposition", "inline"); 218 } 219 220 protected void updateCache(DocumentModel doc, PictureTilesAdapter adapter, String xpath) { 221 222 Calendar modified = (Calendar) doc.getProperty("dublincore", "modified"); 223 PictureTilesCachedEntry entry = new PictureTilesCachedEntry(modified, adapter, xpath); 224 synchronized (cachedAdapters) { 225 cachedAdapters.put(doc.getId(), entry); 226 } 227 cacheGC(); 228 } 229 230 protected void removeFromCache(String key) { 231 PictureTilesCachedEntry entry = cachedAdapters.get(key); 232 if (entry != null) { 233 entry.getAdapter().cleanup(); 234 } 235 synchronized (cachedAdapters) { 236 cachedAdapters.remove(key); 237 } 238 } 239 240 protected boolean isSameDate(Calendar d1, Calendar d2) { 241 242 // because one of the date is stored in the repository 243 // the date may be 'rounded' 244 // so compare 245 long t1 = d1.getTimeInMillis() / 1000; 246 long t2 = d2.getTimeInMillis() / 1000; 247 return Math.abs(t1 - t2) <= 1; 248 } 249 250 protected PictureTilesAdapter getFromCache(DocumentModel doc, String xpath) { 251 if (cachedAdapters.containsKey(doc.getId())) { 252 if (xpath == null) { 253 xpath = ""; 254 } 255 Calendar modified = (Calendar) doc.getProperty("dublincore", "modified"); 256 PictureTilesCachedEntry entry = cachedAdapters.get(doc.getId()); 257 258 if ((!isSameDate(entry.getModified(), modified)) || (!xpath.equals(entry.getXpath()))) { 259 removeFromCache(doc.getId()); 260 return null; 261 } else { 262 return entry.getAdapter(); 263 } 264 } else { 265 return null; 266 } 267 } 268 269 protected void cacheGC() { 270 for (String key : cachedAdapters.keySet()) { 271 long now = System.currentTimeMillis(); 272 PictureTilesCachedEntry entry = cachedAdapters.get(key); 273 if ((now - entry.getTimeStamp()) > MAX_CACHE_LIFE * 1000) { 274 } 275 } 276 } 277 278}