001/*
002 * (C) Copyright 2006-2015 Nuxeo SA (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-2.1.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 */
018package org.nuxeo.ecm.platform.pictures.tiles.service;
019
020import java.io.File;
021import java.io.IOException;
022import java.util.ArrayList;
023import java.util.Collections;
024import java.util.HashMap;
025import java.util.List;
026import java.util.Map;
027
028import org.apache.commons.io.FilenameUtils;
029import org.apache.commons.logging.Log;
030import org.apache.commons.logging.LogFactory;
031import org.nuxeo.common.Environment;
032import org.nuxeo.common.utils.ExceptionUtils;
033import org.nuxeo.ecm.core.api.Blob;
034import org.nuxeo.ecm.core.api.NuxeoException;
035import org.nuxeo.ecm.platform.commandline.executor.api.CommandException;
036import org.nuxeo.ecm.platform.commandline.executor.api.CommandNotAvailable;
037import org.nuxeo.ecm.platform.picture.api.ImageInfo;
038import org.nuxeo.ecm.platform.picture.magick.utils.ImageConverter;
039import org.nuxeo.ecm.platform.pictures.tiles.api.PictureTiles;
040import org.nuxeo.ecm.platform.pictures.tiles.api.PictureTilesImpl;
041import org.nuxeo.ecm.platform.pictures.tiles.api.PictureTilingService;
042import org.nuxeo.ecm.platform.pictures.tiles.api.imageresource.BlobResource;
043import org.nuxeo.ecm.platform.pictures.tiles.api.imageresource.ImageResource;
044import org.nuxeo.ecm.platform.pictures.tiles.gimp.tiler.GimpTiler;
045import org.nuxeo.ecm.platform.pictures.tiles.magick.tiler.MagickTiler;
046import org.nuxeo.ecm.platform.pictures.tiles.tilers.PictureTiler;
047import org.nuxeo.runtime.model.ComponentContext;
048import org.nuxeo.runtime.model.ComponentInstance;
049import org.nuxeo.runtime.model.DefaultComponent;
050
051/**
052 * Runtime component that expose the PictureTilingService interface. Also exposes the configuration Extension Point
053 *
054 * @author tiry
055 */
056public class PictureTilingComponent extends DefaultComponent implements PictureTilingService {
057
058    public static final String ENV_PARAMETERS_EP = "environment";
059
060    public static final String BLOB_PROPERTY_EP = "blobProperties";
061
062    public static final String IMAGES_TO_CONVERT_EP = "imagesToConvert";
063
064    protected static Map<String, PictureTilingCacheInfo> cache = new HashMap<>();
065
066    protected static List<String> inprocessTiles = Collections.synchronizedList(new ArrayList<String>());
067
068    protected static PictureTiler defaultTiler = new MagickTiler();
069
070    protected static List<PictureTiler> availableTilers = new ArrayList<>();
071
072    protected static Map<String, String> envParameters = new HashMap<>();
073
074    protected Map<String, String> blobProperties = new HashMap<>();
075
076    protected List<ImageToConvertDescriptor> imagesToConvert = new ArrayList<>();
077
078    protected static Thread gcThread;
079
080    private String workingDirPath = defaultWorkingDirPath();
081
082    private static final Log log = LogFactory.getLog(PictureTilingComponent.class);
083
084    @Override
085    public void activate(ComponentContext context) {
086        defaultTiler = new MagickTiler();
087        availableTilers.add(defaultTiler);
088        availableTilers.add(new GimpTiler());
089        startGC();
090    }
091
092    public static void startGC() {
093        if (!GCTask.GCEnabled) {
094            GCTask.GCEnabled = true;
095            log.debug("PictureTilingComponent activated starting GC thread");
096            gcThread = new Thread(new GCTask(), "Nuxeo-Tiling-GC");
097            gcThread.setDaemon(true);
098            gcThread.start();
099            log.debug("GC Thread started");
100        } else {
101            log.debug("GC Thread is already started");
102        }
103    }
104
105    public static void endGC() {
106        if (GCTask.GCEnabled) {
107            GCTask.GCEnabled = false;
108            log.debug("Stopping GC Thread");
109            gcThread.interrupt();
110        } else {
111            log.debug("GC Thread is already stopped");
112        }
113    }
114
115    @Override
116    public void deactivate(ComponentContext context) {
117        endGC();
118    }
119
120    public static Map<String, PictureTilingCacheInfo> getCache() {
121        return cache;
122    }
123
124    protected String getWorkingDirPath() {
125        return workingDirPath;
126    }
127
128    protected String defaultWorkingDirPath() {
129        String defaultPath = new File(Environment.getDefault().getData(), "nuxeo-tiling-cache").getAbsolutePath();
130        String path = getEnvValue("WorkingDirPath", defaultPath);
131        return normalizeWorkingDirPath(path);
132    }
133
134    protected String normalizeWorkingDirPath(String path) {
135        File dir = new File(path);
136        if (!dir.exists()) {
137            dir.mkdir();
138        }
139        path = dir.getAbsolutePath();
140        if (!path.endsWith("/")) {
141            path += "/";
142        }
143        return path;
144    }
145
146    @Override
147    public void setWorkingDirPath(String path) {
148        workingDirPath = normalizeWorkingDirPath(path);
149    }
150
151    protected String getWorkingDirPathForRessource(ImageResource resource) {
152        String pathForBlob = getWorkingDirPath();
153        String digest = resource.getHash();
154        pathForBlob = pathForBlob + digest + "/";
155        log.debug("WorkingDirPath for resource=" + pathForBlob);
156        File wdir = new File(pathForBlob);
157        if (!wdir.exists()) {
158            wdir.mkdir();
159        }
160        return pathForBlob;
161    }
162
163    @Override
164    @Deprecated
165    public PictureTiles getTilesFromBlob(Blob blob, int tileWidth, int tileHeight, int maxTiles) {
166        return getTilesFromBlob(blob, tileWidth, tileHeight, maxTiles, 0, 0, false);
167    }
168
169    @Override
170    public PictureTiles getTiles(ImageResource resource, int tileWidth, int tileHeight, int maxTiles) {
171        return getTiles(resource, tileWidth, tileHeight, maxTiles, 0, 0, false);
172    }
173
174    @Override
175    public PictureTiles completeTiles(PictureTiles existingTiles, int xCenter, int yCenter) {
176
177        String outputDirPath = existingTiles.getTilesPath();
178
179        long lastModificationTime = Long.parseLong(existingTiles.getInfo().get(
180                PictureTilesImpl.LAST_MODIFICATION_DATE_KEY));
181        return computeTiles(existingTiles.getSourceImageInfo(), outputDirPath, existingTiles.getTilesWidth(),
182                existingTiles.getTilesHeight(), existingTiles.getMaxTiles(), xCenter, yCenter, lastModificationTime,
183                false);
184    }
185
186    @Override
187    @Deprecated
188    public PictureTiles getTilesFromBlob(Blob blob, int tileWidth, int tileHeight, int maxTiles, int xCenter,
189            int yCenter, boolean fullGeneration) {
190
191        ImageResource resource = new BlobResource(blob);
192        return getTiles(resource, tileWidth, tileHeight, maxTiles, xCenter, yCenter, fullGeneration);
193    }
194
195    @Override
196    public PictureTiles getTiles(ImageResource resource, int tileWidth, int tileHeight, int maxTiles, int xCenter,
197            int yCenter, boolean fullGeneration) {
198
199        log.debug("enter getTiles");
200        String cacheKey = resource.getHash();
201
202        if (defaultTiler.needsSync()) {
203            // some tiler implementation may generate several tiles at once
204            // in order to be efficient this requires synchronization
205            while (inprocessTiles.contains(cacheKey)) {
206                try {
207                    log.debug("Waiting for tiler sync");
208                    Thread.sleep(200);
209                } catch (InterruptedException e) {
210                    ExceptionUtils.checkInterrupt(e);
211                }
212            }
213        }
214
215        PictureTiles tiles = getTilesWithSync(resource, tileWidth, tileHeight, maxTiles, xCenter, yCenter,
216                fullGeneration);
217        inprocessTiles.remove(cacheKey);
218
219        return tiles;
220    }
221
222    protected PictureTiles getTilesWithSync(ImageResource resource, int tileWidth, int tileHeight, int maxTiles,
223            int xCenter, int yCenter, boolean fullGeneration) {
224
225        String cacheKey = resource.getHash();
226        String inputFilePath;
227        PictureTilingCacheInfo cacheInfo;
228
229        if (cache.containsKey(cacheKey)) {
230            cacheInfo = cache.get(cacheKey);
231
232            PictureTiles pt = cacheInfo.getCachedPictureTiles(tileWidth, tileHeight, maxTiles);
233
234            if ((pt != null) && (pt.isTileComputed(xCenter, yCenter))) {
235                return pt;
236            }
237
238            inputFilePath = cacheInfo.getOriginalPicturePath();
239        } else {
240            String wdirPath = getWorkingDirPathForRessource(resource);
241            inputFilePath = wdirPath;
242            Blob blob = resource.getBlob();
243            inputFilePath += Integer.toString(blob.hashCode()) + ".";
244            if (blob.getFilename() != null) {
245                inputFilePath += FilenameUtils.getExtension(blob.getFilename());
246            } else {
247                inputFilePath += "img";
248            }
249
250            if (needToConvert(blob)) {
251                inputFilePath = inputFilePath.replaceFirst("\\..*", ".jpg");
252            }
253
254            File inputFile = new File(inputFilePath);
255
256            if (!inputFile.exists()) {
257                try {
258                    // create the empty file ASAP to avoid concurrent transfer
259                    // and conversions
260                    if (inputFile.createNewFile()) {
261                        transferBlob(blob, inputFile);
262                    }
263                } catch (IOException e) {
264                    String msg = String.format("Unable to transfer blob to file at '%s', "
265                            + "working directory path: '%s'", inputFilePath, wdirPath);
266                    log.error(msg, e);
267                    throw new NuxeoException(msg, e);
268                }
269                inputFile = new File(inputFilePath);
270            } else {
271                while (System.currentTimeMillis() - inputFile.lastModified() < 200) {
272                    try {
273                        log.debug("Waiting concurrent convert / dump");
274                        Thread.sleep(200);
275                    } catch (InterruptedException e) {
276                        ExceptionUtils.checkInterrupt(e);
277                    }
278                }
279
280            }
281            try {
282                cacheInfo = new PictureTilingCacheInfo(cacheKey, wdirPath, inputFilePath);
283                cache.put(cacheKey, cacheInfo);
284            } catch (CommandNotAvailable | CommandException e) {
285                throw new NuxeoException(e);
286            }
287
288        }
289
290        // compute output dir
291        String outDirPath = cacheInfo.getTilingDir(tileWidth, tileHeight, maxTiles);
292
293        // try to see if a shrinked image can be used
294        ImageInfo bestImageInfo = cacheInfo.getBestSourceImage(tileWidth, tileHeight, maxTiles);
295
296        inputFilePath = bestImageInfo.getFilePath();
297        log.debug("input source image path for tile computation=" + inputFilePath);
298
299        long lastModificationTime = resource.getModificationDate().getTimeInMillis();
300        PictureTiles tiles = computeTiles(bestImageInfo, outDirPath, tileWidth, tileHeight, maxTiles, xCenter, yCenter,
301                lastModificationTime, fullGeneration);
302
303        tiles.getInfo().put(PictureTilesImpl.MAX_TILES_KEY, Integer.toString(maxTiles));
304        tiles.getInfo().put(PictureTilesImpl.TILES_WIDTH_KEY, Integer.toString(tileWidth));
305        tiles.getInfo().put(PictureTilesImpl.TILES_HEIGHT_KEY, Integer.toString(tileHeight));
306        String lastModificationDate = Long.toString(lastModificationTime);
307        tiles.getInfo().put(PictureTilesImpl.LAST_MODIFICATION_DATE_KEY, lastModificationDate);
308        tiles.setCacheKey(cacheKey);
309        tiles.setSourceImageInfo(bestImageInfo);
310        tiles.setOriginalImageInfo(cacheInfo.getOriginalPictureInfos());
311
312        cacheInfo.addPictureTilesToCache(tiles);
313        return tiles;
314    }
315
316    protected void transferBlob(Blob blob, File file) throws IOException {
317        if (needToConvert(blob)) {
318            transferAndConvert(blob, file);
319        } else {
320            blob.transferTo(file);
321        }
322    }
323
324    protected boolean needToConvert(Blob blob) {
325        for (ImageToConvertDescriptor desc : imagesToConvert) {
326            String extension = getExtension(blob);
327            if (desc.getMimeType().equalsIgnoreCase(blob.getMimeType())
328                    || extension.equalsIgnoreCase(desc.getExtension())) {
329                return true;
330            }
331        }
332        return false;
333    }
334
335    protected String getExtension(Blob blob) {
336        String filename = blob.getFilename();
337        if (filename == null) {
338            return "";
339        }
340        int dotIndex = filename.lastIndexOf('.');
341        if (dotIndex == -1) {
342            return "";
343        }
344
345        return filename.substring(dotIndex + 1);
346    }
347
348    protected void transferAndConvert(Blob blob, File file) throws IOException {
349        File tmpFile = new File(file.getAbsolutePath() + ".tmp");
350        blob.transferTo(tmpFile);
351        try {
352            ImageConverter.convert(tmpFile.getAbsolutePath(), file.getAbsolutePath());
353        } catch (CommandNotAvailable | CommandException e) {
354            throw new IOException(e);
355        }
356
357        tmpFile.delete();
358    }
359
360    protected PictureTiles computeTiles(ImageInfo input, String outputDirPath, int tileWidth, int tileHeight,
361            int maxTiles, int xCenter, int yCenter, long lastModificationTime, boolean fullGeneration) {
362
363        PictureTiler pt = getDefaultTiler();
364        return pt.getTilesFromFile(input, outputDirPath, tileWidth, tileHeight, maxTiles, xCenter, yCenter,
365                lastModificationTime, fullGeneration);
366    }
367
368    protected PictureTiler getDefaultTiler() {
369        return defaultTiler;
370    }
371
372    // tests
373    public static void setDefaultTiler(PictureTiler tiler) {
374        defaultTiler = tiler;
375    }
376
377    // ****************************************
378    // Env setting management
379
380    public static Map<String, String> getEnv() {
381        return envParameters;
382    }
383
384    public static String getEnvValue(String paramName) {
385        if (envParameters == null) {
386            return null;
387        }
388        return envParameters.get(paramName);
389    }
390
391    public static String getEnvValue(String paramName, String defaultValue) {
392        String value = getEnvValue(paramName);
393        if (value == null) {
394            return defaultValue;
395        } else {
396            return value;
397        }
398    }
399
400    public static void setEnvValue(String paramName, String paramValue) {
401        envParameters.put(paramName, paramValue);
402    }
403
404    // Blob properties management
405    @Override
406    public Map<String, String> getBlobProperties() {
407        return blobProperties;
408    }
409
410    @Override
411    public String getBlobProperty(String docType) {
412        return blobProperties.get(docType);
413    }
414
415    @Override
416    public String getBlobProperty(String docType, String defaultValue) {
417        String property = blobProperties.get(docType);
418        if (property == null) {
419            return defaultValue;
420        }
421        return property;
422    }
423
424    // EP management
425
426    @Override
427    public void registerContribution(Object contribution, String extensionPoint, ComponentInstance contributor) {
428        if (ENV_PARAMETERS_EP.equals(extensionPoint)) {
429            TilingConfigurationDescriptor desc = (TilingConfigurationDescriptor) contribution;
430            envParameters.putAll(desc.getParameters());
431            workingDirPath = defaultWorkingDirPath();
432        } else if (BLOB_PROPERTY_EP.equals(extensionPoint)) {
433            TilingBlobPropertyDescriptor desc = (TilingBlobPropertyDescriptor) contribution;
434            blobProperties.putAll(desc.getBlobProperties());
435        } else if (IMAGES_TO_CONVERT_EP.equals(extensionPoint)) {
436            ImageToConvertDescriptor desc = (ImageToConvertDescriptor) contribution;
437            imagesToConvert.add(desc);
438        }
439    }
440
441    @Override
442    public void unregisterContribution(Object contribution, String extensionPoint, ComponentInstance contributor) {
443        // TODO
444    }
445
446    @Override
447    public void removeCacheEntry(ImageResource resource) {
448        if (cache.containsKey(resource.getHash())) {
449            PictureTilingCacheInfo cacheInfo = cache.remove(resource.getHash());
450            cacheInfo.cleanUp();
451        }
452    }
453
454}