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}