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