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