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