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.InputStream;
025import java.io.OutputStream;
026import java.util.Calendar;
027import java.util.Map;
028import java.util.concurrent.ConcurrentHashMap;
029
030import javax.servlet.http.HttpServletResponse;
031
032import org.apache.commons.io.IOUtils;
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.platform.pictures.tiles.api.PictureTiles;
038import org.nuxeo.ecm.platform.pictures.tiles.api.adapter.PictureTilesAdapter;
039import org.nuxeo.ecm.platform.pictures.tiles.serializer.JSONPictureTilesSerializer;
040import org.nuxeo.ecm.platform.pictures.tiles.serializer.PictureTilesSerializer;
041import org.nuxeo.ecm.platform.pictures.tiles.serializer.XMLPictureTilesSerializer;
042import org.nuxeo.ecm.platform.ui.web.restAPI.BaseStatelessNuxeoRestlet;
043import org.restlet.data.CharacterSet;
044import org.restlet.data.Form;
045import org.restlet.data.MediaType;
046import org.restlet.data.Request;
047import org.restlet.data.Response;
048import org.restlet.resource.OutputRepresentation;
049
050/**
051 * Restlet to provide a REST API on top of the PictureTilingService.
052 *
053 * @author tiry
054 */
055public class PictureTilesRestlets extends BaseStatelessNuxeoRestlet {
056
057    // cache duration in seconds
058    protected static int MAX_CACHE_LIFE = 60 * 10;
059
060    protected static Map<String, PictureTilesCachedEntry> cachedAdapters = new ConcurrentHashMap<String, PictureTilesCachedEntry>();
061
062    @Override
063    public void handle(Request req, Response res) {
064
065        String repo = (String) req.getAttributes().get("repoId");
066        String docid = (String) req.getAttributes().get("docId");
067        Integer tileWidth = Integer.decode((String) req.getAttributes().get("tileWidth"));
068        Integer tileHeight = Integer.decode((String) req.getAttributes().get("tileHeight"));
069        Integer maxTiles = Integer.decode((String) req.getAttributes().get("maxTiles"));
070
071        Form form = req.getResourceRef().getQueryAsForm();
072        String xpath = (String) form.getFirstValue("fieldPath");
073        String x = form.getFirstValue("x");
074        String y = form.getFirstValue("y");
075        String format = form.getFirstValue("format");
076
077        String test = form.getFirstValue("test");
078        if (test != null) {
079            try {
080                handleSendTest(res, repo, docid, tileWidth, tileHeight, maxTiles);
081                return;
082            } catch (IOException e) {
083                handleError(res, e);
084                return;
085            }
086        }
087
088        if (repo == null || repo.equals("*")) {
089            handleError(res, "you must specify a repository");
090            return;
091        }
092        if (docid == null || repo.equals("*")) {
093            handleError(res, "you must specify a documentId");
094            return;
095        }
096        Boolean init = initRepositoryAndTargetDocument(res, repo, docid);
097
098        if (!init) {
099            handleError(res, "unable to init repository connection");
100            return;
101        }
102
103        PictureTilesAdapter adapter;
104        try {
105            adapter = getFromCache(targetDocument, xpath);
106            if (adapter == null) {
107                adapter = targetDocument.getAdapter(PictureTilesAdapter.class);
108                if ((xpath != null) && (!"".equals(xpath))) {
109                    adapter.setXPath(xpath);
110                }
111                updateCache(targetDocument, adapter, xpath);
112            }
113        } catch (NuxeoException e) {
114            handleError(res, e);
115            return;
116        }
117
118        if (adapter == null) {
119            handleNoTiles(res, null);
120            return;
121        }
122
123        PictureTiles tiles = null;
124        try {
125            tiles = adapter.getTiles(tileWidth, tileHeight, maxTiles);
126        } catch (NuxeoException e) {
127            handleError(res, e);
128        }
129
130        if ((x == null) || (y == null)) {
131            handleSendInfo(res, tiles, format);
132        } else {
133            handleSendImage(res, tiles, Integer.decode(x), Integer.decode(y));
134        }
135    }
136
137    protected void handleSendTest(Response res, String repoId, String docId, Integer tileWidth, Integer tileHeight,
138            Integer maxTiles) throws IOException {
139        MediaType mt = null;
140        mt = MediaType.TEXT_HTML;
141
142        File file = FileUtils.getResourceFileFromContext("testTiling.html");
143        String html = FileUtils.readFile(file);
144
145        html = html.replace("$repoId$", repoId);
146        html = html.replace("$docId$", docId);
147        html = html.replace("$tileWidth$", tileWidth.toString());
148        html = html.replace("$tileHeight$", tileHeight.toString());
149        html = html.replace("$maxTiles$", maxTiles.toString());
150
151        res.setEntity(html, mt);
152    }
153
154    protected void handleSendInfo(Response res, PictureTiles tiles, String format) {
155        if (format == null) {
156            format = "XML";
157        }
158        MediaType mt = null;
159        PictureTilesSerializer serializer = null;
160
161        if (format.equalsIgnoreCase("json")) {
162            serializer = new JSONPictureTilesSerializer();
163            mt = MediaType.APPLICATION_JSON;
164        } else {
165            serializer = new XMLPictureTilesSerializer();
166            mt = MediaType.TEXT_XML;
167        }
168
169        res.setEntity(serializer.serialize(tiles), mt);
170        res.getEntity().setCharacterSet(CharacterSet.UTF_8);
171
172        HttpServletResponse response = getHttpResponse(res);
173        response.setHeader("Cache-Control", "no-cache");
174        response.setHeader("Pragma", "no-cache");
175    }
176
177    protected void handleSendImage(Response res, PictureTiles tiles, Integer x, Integer y) {
178
179        final Blob image;
180        try {
181            image = tiles.getTile(x, y);
182        } catch (NuxeoException | IOException e) {
183            handleError(res, e);
184            return;
185        }
186
187        // blobs are always persistent, and temporary blobs are GCed only when not referenced anymore
188        res.setEntity(new OutputRepresentation(null) {
189            @Override
190            public void write(OutputStream outputStream) throws IOException {
191                try (InputStream stream = image.getStream()) {
192                    IOUtils.copy(stream, outputStream);
193                }
194            }
195        });
196    }
197
198    protected void handleNoTiles(Response res, Exception e) {
199        StringBuilder sb = new StringBuilder();
200
201        sb.append("<html><body><center><h1>");
202        if (e == null) {
203            sb.append("No Tiling is available for this document</h1>");
204        } else {
205            sb.append("Picture Tiling can not be generated for this document</h1>");
206            sb.append("<br/><pre>");
207            sb.append(e.toString());
208            sb.append("</pre>");
209        }
210
211        sb.append("</center></body></html>");
212
213        res.setEntity(sb.toString(), MediaType.TEXT_HTML);
214        HttpServletResponse response = getHttpResponse(res);
215        response.setHeader("Content-Disposition", "inline");
216    }
217
218    protected void updateCache(DocumentModel doc, PictureTilesAdapter adapter, String xpath) {
219
220        Calendar modified = (Calendar) doc.getProperty("dublincore", "modified");
221        PictureTilesCachedEntry entry = new PictureTilesCachedEntry(modified, adapter, xpath);
222        synchronized (cachedAdapters) {
223            cachedAdapters.put(doc.getId(), entry);
224        }
225        cacheGC();
226    }
227
228    protected void removeFromCache(String key) {
229        PictureTilesCachedEntry entry = cachedAdapters.get(key);
230        if (entry != null) {
231            entry.getAdapter().cleanup();
232        }
233        synchronized (cachedAdapters) {
234            cachedAdapters.remove(key);
235        }
236    }
237
238    protected boolean isSameDate(Calendar d1, Calendar d2) {
239
240        // because one of the date is stored in the repository
241        // the date may be 'rounded'
242        // so compare
243        long t1 = d1.getTimeInMillis() / 1000;
244        long t2 = d2.getTimeInMillis() / 1000;
245        return Math.abs(t1 - t2) <= 1;
246    }
247
248    protected PictureTilesAdapter getFromCache(DocumentModel doc, String xpath) {
249        if (cachedAdapters.containsKey(doc.getId())) {
250            if (xpath == null) {
251                xpath = "";
252            }
253            Calendar modified = (Calendar) doc.getProperty("dublincore", "modified");
254            PictureTilesCachedEntry entry = cachedAdapters.get(doc.getId());
255
256            if ((!isSameDate(entry.getModified(), modified)) || (!xpath.equals(entry.getXpath()))) {
257                removeFromCache(doc.getId());
258                return null;
259            } else {
260                return entry.getAdapter();
261            }
262        } else {
263            return null;
264        }
265    }
266
267    protected void cacheGC() {
268        for (String key : cachedAdapters.keySet()) {
269            long now = System.currentTimeMillis();
270            PictureTilesCachedEntry entry = cachedAdapters.get(key);
271            if ((now - entry.getTimeStamp()) > MAX_CACHE_LIFE * 1000) {
272            }
273        }
274    }
275
276}