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}