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}