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