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.Request; 050import org.restlet.Response; 051import org.restlet.data.CharacterSet; 052import org.restlet.data.Form; 053import org.restlet.data.MediaType; 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 doHandleStatelessRequest(Request req, Response res) { 069 logDeprecation(); 070 HttpServletRequest request = getHttpRequest(req); 071 HttpServletResponse response = getHttpResponse(res); 072 073 String repo = (String) req.getAttributes().get("repoId"); 074 String docid = (String) req.getAttributes().get("docId"); 075 Integer tileWidth = Integer.decode((String) req.getAttributes().get("tileWidth")); 076 Integer tileHeight = Integer.decode((String) req.getAttributes().get("tileHeight")); 077 Integer maxTiles = Integer.decode((String) req.getAttributes().get("maxTiles")); 078 079 Form form = req.getResourceRef().getQueryAsForm(); 080 String xpath = form.getFirstValue("fieldPath"); 081 String x = form.getFirstValue("x"); 082 String y = form.getFirstValue("y"); 083 String format = form.getFirstValue("format"); 084 085 String test = form.getFirstValue("test"); 086 if (test != null) { 087 try { 088 handleSendTest(res, repo, docid, tileWidth, tileHeight, maxTiles); 089 return; 090 } catch (IOException e) { 091 handleError(res, e); 092 return; 093 } 094 } 095 096 if (repo == null || repo.equals("*")) { 097 handleError(res, "you must specify a repository"); 098 return; 099 } 100 if (docid == null || repo.equals("*")) { 101 handleError(res, "you must specify a documentId"); 102 return; 103 } 104 Boolean init = initRepositoryAndTargetDocument(res, repo, docid); 105 106 if (!init) { 107 handleError(res, "unable to init repository connection"); 108 return; 109 } 110 111 PictureTilesAdapter adapter; 112 try { 113 adapter = getFromCache(targetDocument, xpath); 114 if (adapter == null) { 115 adapter = targetDocument.getAdapter(PictureTilesAdapter.class); 116 if ((xpath != null) && (!"".equals(xpath))) { 117 adapter.setXPath(xpath); 118 } 119 updateCache(targetDocument, adapter, xpath); 120 } 121 } catch (NuxeoException e) { 122 handleError(res, e); 123 return; 124 } 125 126 if (adapter == null) { 127 handleNoTiles(res, null); 128 return; 129 } 130 131 PictureTiles tiles = null; 132 try { 133 tiles = adapter.getTiles(tileWidth, tileHeight, maxTiles); 134 } catch (NuxeoException e) { 135 handleError(res, e); 136 return; 137 } 138 139 if ((x == null) || (y == null)) { 140 handleSendInfo(res, tiles, format); 141 return; 142 } 143 144 final Blob image; 145 try { 146 image = tiles.getTile(Integer.decode(x), Integer.decode(y)); 147 } catch (NuxeoException | IOException e) { 148 handleError(res, e); 149 return; 150 } 151 152 String reason = "tile"; 153 Boolean inline = Boolean.TRUE; 154 Map<String, Serializable> extendedInfos = new HashMap<>(); 155 extendedInfos.put("x", x); 156 extendedInfos.put("y", y); 157 DownloadService downloadService = Framework.getService(DownloadService.class); 158 try { 159 downloadService.downloadBlob(request, response, targetDocument, xpath, image, image.getFilename(), reason, 160 extendedInfos, inline, byteRange -> setEntityToBlobOutput(image, byteRange, res)); 161 } catch (IOException e) { 162 handleError(res, e); 163 } 164 } 165 166 protected void handleSendTest(Response res, String repoId, String docId, Integer tileWidth, Integer tileHeight, 167 Integer maxTiles) throws IOException { 168 MediaType mt; 169 mt = MediaType.TEXT_HTML; 170 171 File file = FileUtils.getResourceFileFromContext("testTiling.html"); 172 String html = org.apache.commons.io.FileUtils.readFileToString(file, UTF_8); 173 174 html = html.replace("$repoId$", repoId); 175 html = html.replace("$docId$", docId); 176 html = html.replace("$tileWidth$", tileWidth.toString()); 177 html = html.replace("$tileHeight$", tileHeight.toString()); 178 html = html.replace("$maxTiles$", maxTiles.toString()); 179 180 res.setEntity(html, mt); 181 } 182 183 protected void handleSendInfo(Response res, PictureTiles tiles, String format) { 184 if (format == null) { 185 format = "XML"; 186 } 187 MediaType mt; 188 PictureTilesSerializer serializer; 189 190 if (format.equalsIgnoreCase("json")) { 191 serializer = new JSONPictureTilesSerializer(); 192 mt = MediaType.APPLICATION_JSON; 193 } else { 194 serializer = new XMLPictureTilesSerializer(); 195 mt = MediaType.APPLICATION_XML; 196 } 197 198 res.setEntity(serializer.serialize(tiles), mt); 199 res.getEntity().setCharacterSet(CharacterSet.UTF_8); 200 201 HttpServletResponse response = getHttpResponse(res); 202 response.setHeader("Cache-Control", "no-cache"); 203 response.setHeader("Pragma", "no-cache"); 204 } 205 206 protected void handleNoTiles(Response res, Exception e) { 207 StringBuilder sb = new StringBuilder(); 208 209 sb.append("<html><body><center><h1>"); 210 if (e == null) { 211 sb.append("No Tiling is available for this document</h1>"); 212 } else { 213 sb.append("Picture Tiling can not be generated for this document</h1>"); 214 sb.append("<br/><pre>"); 215 sb.append(e.toString()); 216 sb.append("</pre>"); 217 } 218 219 sb.append("</center></body></html>"); 220 221 res.setEntity(sb.toString(), MediaType.TEXT_HTML); 222 HttpServletResponse response = getHttpResponse(res); 223 response.setHeader("Content-Disposition", "inline"); 224 } 225 226 protected void updateCache(DocumentModel doc, PictureTilesAdapter adapter, String xpath) { 227 228 Calendar modified = (Calendar) doc.getProperty("dublincore", "modified"); 229 PictureTilesCachedEntry entry = new PictureTilesCachedEntry(modified, adapter, xpath); 230 cachedAdapters.put(doc.getId(), entry); 231 cacheGC(); 232 } 233 234 protected void removeFromCache(String key) { 235 PictureTilesCachedEntry entry = cachedAdapters.get(key); 236 if (entry != null) { 237 entry.getAdapter().cleanup(); 238 } 239 cachedAdapters.remove(key); 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}