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 static java.nio.charset.StandardCharsets.UTF_8; 025 026import java.io.File; 027import java.io.IOException; 028import java.io.Serializable; 029import java.util.Calendar; 030import java.util.HashMap; 031import java.util.Map; 032import java.util.concurrent.ConcurrentHashMap; 033 034import javax.servlet.http.HttpServletRequest; 035import javax.servlet.http.HttpServletResponse; 036 037import org.nuxeo.common.utils.FileUtils; 038import org.nuxeo.ecm.core.api.Blob; 039import org.nuxeo.ecm.core.api.DocumentModel; 040import org.nuxeo.ecm.core.api.NuxeoException; 041import org.nuxeo.ecm.core.io.download.DownloadService; 042import org.nuxeo.ecm.platform.pictures.tiles.api.PictureTiles; 043import org.nuxeo.ecm.platform.pictures.tiles.api.adapter.PictureTilesAdapter; 044import org.nuxeo.ecm.platform.pictures.tiles.serializer.JSONPictureTilesSerializer; 045import org.nuxeo.ecm.platform.pictures.tiles.serializer.PictureTilesSerializer; 046import org.nuxeo.ecm.platform.pictures.tiles.serializer.XMLPictureTilesSerializer; 047import org.nuxeo.ecm.platform.ui.web.restAPI.BaseStatelessNuxeoRestlet; 048import org.nuxeo.runtime.api.Framework; 049import org.restlet.data.CharacterSet; 050import org.restlet.data.Form; 051import org.restlet.data.MediaType; 052import org.restlet.data.Request; 053import org.restlet.data.Response; 054 055/** 056 * Restlet to provide a REST API on top of the PictureTilingService. 057 * 058 * @author tiry 059 */ 060public class PictureTilesRestlets extends BaseStatelessNuxeoRestlet { 061 062 // cache duration in seconds 063 protected static int MAX_CACHE_LIFE = 60 * 10; 064 065 protected static Map<String, PictureTilesCachedEntry> cachedAdapters = new ConcurrentHashMap<String, PictureTilesCachedEntry>(); 066 067 @Override 068 public void handle(Request req, Response res) { 069 HttpServletRequest request = getHttpRequest(req); 070 HttpServletResponse response = getHttpResponse(res); 071 072 String repo = (String) req.getAttributes().get("repoId"); 073 String docid = (String) req.getAttributes().get("docId"); 074 Integer tileWidth = Integer.decode((String) req.getAttributes().get("tileWidth")); 075 Integer tileHeight = Integer.decode((String) req.getAttributes().get("tileHeight")); 076 Integer maxTiles = Integer.decode((String) req.getAttributes().get("maxTiles")); 077 078 Form form = req.getResourceRef().getQueryAsForm(); 079 String xpath = form.getFirstValue("fieldPath"); 080 String x = form.getFirstValue("x"); 081 String y = form.getFirstValue("y"); 082 String format = form.getFirstValue("format"); 083 084 String test = form.getFirstValue("test"); 085 if (test != null) { 086 try { 087 handleSendTest(res, repo, docid, tileWidth, tileHeight, maxTiles); 088 return; 089 } catch (IOException e) { 090 handleError(res, e); 091 return; 092 } 093 } 094 095 if (repo == null || repo.equals("*")) { 096 handleError(res, "you must specify a repository"); 097 return; 098 } 099 if (docid == null || repo.equals("*")) { 100 handleError(res, "you must specify a documentId"); 101 return; 102 } 103 Boolean init = initRepositoryAndTargetDocument(res, repo, docid); 104 105 if (!init) { 106 handleError(res, "unable to init repository connection"); 107 return; 108 } 109 110 PictureTilesAdapter adapter; 111 try { 112 adapter = getFromCache(targetDocument, xpath); 113 if (adapter == null) { 114 adapter = targetDocument.getAdapter(PictureTilesAdapter.class); 115 if ((xpath != null) && (!"".equals(xpath))) { 116 adapter.setXPath(xpath); 117 } 118 updateCache(targetDocument, adapter, xpath); 119 } 120 } catch (NuxeoException e) { 121 handleError(res, e); 122 return; 123 } 124 125 if (adapter == null) { 126 handleNoTiles(res, null); 127 return; 128 } 129 130 PictureTiles tiles = null; 131 try { 132 tiles = adapter.getTiles(tileWidth, tileHeight, maxTiles); 133 } catch (NuxeoException e) { 134 handleError(res, e); 135 } 136 137 if ((x == null) || (y == null)) { 138 handleSendInfo(res, tiles, format); 139 return; 140 } 141 142 final Blob image; 143 try { 144 image = tiles.getTile(Integer.decode(x), Integer.decode(y)); 145 } catch (NuxeoException | IOException e) { 146 handleError(res, e); 147 return; 148 } 149 150 String reason = "tile"; 151 Boolean inline = Boolean.TRUE; 152 Map<String, Serializable> extendedInfos = new HashMap<>(); 153 extendedInfos.put("x", x); 154 extendedInfos.put("y", y); 155 DownloadService downloadService = Framework.getService(DownloadService.class); 156 try { 157 downloadService.downloadBlob(request, response, targetDocument, xpath, image, image.getFilename(), reason, 158 extendedInfos, inline, byteRange -> setEntityToBlobOutput(image, byteRange, res)); 159 } catch (IOException e) { 160 handleError(res, e); 161 } 162 } 163 164 protected void handleSendTest(Response res, String repoId, String docId, Integer tileWidth, Integer tileHeight, 165 Integer maxTiles) throws IOException { 166 MediaType mt; 167 mt = MediaType.TEXT_HTML; 168 169 File file = FileUtils.getResourceFileFromContext("testTiling.html"); 170 String html = org.apache.commons.io.FileUtils.readFileToString(file, UTF_8); 171 172 html = html.replace("$repoId$", repoId); 173 html = html.replace("$docId$", docId); 174 html = html.replace("$tileWidth$", tileWidth.toString()); 175 html = html.replace("$tileHeight$", tileHeight.toString()); 176 html = html.replace("$maxTiles$", maxTiles.toString()); 177 178 res.setEntity(html, mt); 179 } 180 181 protected void handleSendInfo(Response res, PictureTiles tiles, String format) { 182 if (format == null) { 183 format = "XML"; 184 } 185 MediaType mt; 186 PictureTilesSerializer serializer; 187 188 if (format.equalsIgnoreCase("json")) { 189 serializer = new JSONPictureTilesSerializer(); 190 mt = MediaType.APPLICATION_JSON; 191 } else { 192 serializer = new XMLPictureTilesSerializer(); 193 mt = MediaType.TEXT_XML; 194 } 195 196 res.setEntity(serializer.serialize(tiles), mt); 197 res.getEntity().setCharacterSet(CharacterSet.UTF_8); 198 199 HttpServletResponse response = getHttpResponse(res); 200 response.setHeader("Cache-Control", "no-cache"); 201 response.setHeader("Pragma", "no-cache"); 202 } 203 204 protected void handleNoTiles(Response res, Exception e) { 205 StringBuilder sb = new StringBuilder(); 206 207 sb.append("<html><body><center><h1>"); 208 if (e == null) { 209 sb.append("No Tiling is available for this document</h1>"); 210 } else { 211 sb.append("Picture Tiling can not be generated for this document</h1>"); 212 sb.append("<br/><pre>"); 213 sb.append(e.toString()); 214 sb.append("</pre>"); 215 } 216 217 sb.append("</center></body></html>"); 218 219 res.setEntity(sb.toString(), MediaType.TEXT_HTML); 220 HttpServletResponse response = getHttpResponse(res); 221 response.setHeader("Content-Disposition", "inline"); 222 } 223 224 protected void updateCache(DocumentModel doc, PictureTilesAdapter adapter, String xpath) { 225 226 Calendar modified = (Calendar) doc.getProperty("dublincore", "modified"); 227 PictureTilesCachedEntry entry = new PictureTilesCachedEntry(modified, adapter, xpath); 228 cachedAdapters.put(doc.getId(), entry); 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 cachedAdapters.remove(key); 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}