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}