001/*
002 * (C) Copyright 2006-2016 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 */
019package org.nuxeo.ecm.platform.pictures.tiles.service;
020
021import java.io.File;
022import java.io.IOException;
023import java.util.ArrayList;
024import java.util.Collections;
025import java.util.HashMap;
026import java.util.List;
027import java.util.Map;
028
029import org.apache.commons.io.FilenameUtils;
030import org.apache.commons.logging.Log;
031import org.apache.commons.logging.LogFactory;
032import org.nuxeo.common.Environment;
033import org.nuxeo.common.utils.ExceptionUtils;
034import org.nuxeo.ecm.core.api.Blob;
035import org.nuxeo.ecm.core.api.NuxeoException;
036import org.nuxeo.ecm.platform.commandline.executor.api.CommandException;
037import org.nuxeo.ecm.platform.commandline.executor.api.CommandNotAvailable;
038import org.nuxeo.ecm.platform.picture.api.ImageInfo;
039import org.nuxeo.ecm.platform.picture.magick.utils.ImageConverter;
040import org.nuxeo.ecm.platform.pictures.tiles.api.PictureTiles;
041import org.nuxeo.ecm.platform.pictures.tiles.api.PictureTilesImpl;
042import org.nuxeo.ecm.platform.pictures.tiles.api.PictureTilingService;
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<>());
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(File.separator)) {
136            path += File.separator;
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 + File.separator;
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    public PictureTiles getTiles(ImageResource resource, int tileWidth, int tileHeight, int maxTiles) {
160        return getTiles(resource, tileWidth, tileHeight, maxTiles, 0, 0, false);
161    }
162
163    @Override
164    public PictureTiles completeTiles(PictureTiles existingTiles, int xCenter, int yCenter) {
165
166        String outputDirPath = existingTiles.getTilesPath();
167
168        long lastModificationTime = Long.parseLong(
169                existingTiles.getInfo().get(PictureTilesImpl.LAST_MODIFICATION_DATE_KEY));
170        return computeTiles(existingTiles.getSourceImageInfo(), outputDirPath, existingTiles.getTilesWidth(),
171                existingTiles.getTilesHeight(), existingTiles.getMaxTiles(), xCenter, yCenter, lastModificationTime,
172                false);
173    }
174
175    @Override
176    public PictureTiles getTiles(ImageResource resource, int tileWidth, int tileHeight, int maxTiles, int xCenter,
177            int yCenter, boolean fullGeneration) {
178
179        log.debug("enter getTiles");
180        String cacheKey = resource.getHash();
181
182        if (defaultTiler.needsSync()) {
183            // some tiler implementation may generate several tiles at once
184            // in order to be efficient this requires synchronization
185            while (inprocessTiles.contains(cacheKey)) {
186                try {
187                    log.debug("Waiting for tiler sync");
188                    Thread.sleep(200);
189                } catch (InterruptedException e) {
190                    ExceptionUtils.checkInterrupt(e);
191                }
192            }
193        }
194
195        PictureTiles tiles = getTilesWithSync(resource, tileWidth, tileHeight, maxTiles, xCenter, yCenter,
196                fullGeneration);
197        inprocessTiles.remove(cacheKey);
198
199        return tiles;
200    }
201
202    protected PictureTiles getTilesWithSync(ImageResource resource, int tileWidth, int tileHeight, int maxTiles,
203            int xCenter, int yCenter, boolean fullGeneration) {
204
205        String cacheKey = resource.getHash();
206        String inputFilePath;
207        PictureTilingCacheInfo cacheInfo;
208
209        if (cache.containsKey(cacheKey)) {
210            cacheInfo = cache.get(cacheKey);
211
212            PictureTiles pt = cacheInfo.getCachedPictureTiles(tileWidth, tileHeight, maxTiles);
213
214            if ((pt != null) && (pt.isTileComputed(xCenter, yCenter))) {
215                return pt;
216            }
217
218            inputFilePath = cacheInfo.getOriginalPicturePath();
219        } else {
220            String wdirPath = getWorkingDirPathForRessource(resource);
221            inputFilePath = wdirPath;
222            Blob blob = resource.getBlob();
223            inputFilePath += Integer.toString(blob.hashCode()) + ".";
224            if (blob.getFilename() != null) {
225                inputFilePath += FilenameUtils.getExtension(blob.getFilename());
226            } else {
227                inputFilePath += "img";
228            }
229
230            if (needToConvert(blob)) {
231                inputFilePath = FilenameUtils.removeExtension(inputFilePath) + ".jpg";
232            }
233
234            File inputFile = new File(inputFilePath);
235
236            if (!inputFile.exists()) {
237                try {
238                    // create the empty file ASAP to avoid concurrent transfer
239                    // and conversions
240                    if (inputFile.createNewFile()) {
241                        transferBlob(blob, inputFile);
242                    }
243                } catch (IOException e) {
244                    String msg = String.format(
245                            "Unable to transfer blob to file at '%s', " + "working directory path: '%s'", inputFilePath,
246                            wdirPath);
247                    log.error(msg, e);
248                    throw new NuxeoException(msg, e);
249                }
250                inputFile = new File(inputFilePath);
251            } else {
252                while (System.currentTimeMillis() - inputFile.lastModified() < 200) {
253                    try {
254                        log.debug("Waiting concurrent convert / dump");
255                        Thread.sleep(200);
256                    } catch (InterruptedException e) {
257                        ExceptionUtils.checkInterrupt(e);
258                    }
259                }
260
261            }
262            try {
263                cacheInfo = new PictureTilingCacheInfo(cacheKey, wdirPath, inputFilePath);
264                cache.put(cacheKey, cacheInfo);
265            } catch (CommandNotAvailable | CommandException e) {
266                throw new NuxeoException(e);
267            }
268
269        }
270
271        // compute output dir
272        String outDirPath = cacheInfo.getTilingDir(tileWidth, tileHeight, maxTiles);
273
274        // try to see if a shrinked image can be used
275        ImageInfo bestImageInfo = cacheInfo.getBestSourceImage(tileWidth, tileHeight, maxTiles);
276
277        inputFilePath = bestImageInfo.getFilePath();
278        log.debug("input source image path for tile computation=" + inputFilePath);
279
280        long lastModificationTime = resource.getModificationDate().getTimeInMillis();
281        PictureTiles tiles = computeTiles(bestImageInfo, outDirPath, tileWidth, tileHeight, maxTiles, xCenter, yCenter,
282                lastModificationTime, fullGeneration);
283
284        tiles.getInfo().put(PictureTilesImpl.MAX_TILES_KEY, Integer.toString(maxTiles));
285        tiles.getInfo().put(PictureTilesImpl.TILES_WIDTH_KEY, Integer.toString(tileWidth));
286        tiles.getInfo().put(PictureTilesImpl.TILES_HEIGHT_KEY, Integer.toString(tileHeight));
287        String lastModificationDate = Long.toString(lastModificationTime);
288        tiles.getInfo().put(PictureTilesImpl.LAST_MODIFICATION_DATE_KEY, lastModificationDate);
289        tiles.setCacheKey(cacheKey);
290        tiles.setSourceImageInfo(bestImageInfo);
291        tiles.setOriginalImageInfo(cacheInfo.getOriginalPictureInfos());
292
293        cacheInfo.addPictureTilesToCache(tiles);
294        return tiles;
295    }
296
297    protected void transferBlob(Blob blob, File file) throws IOException {
298        if (needToConvert(blob)) {
299            transferAndConvert(blob, file);
300        } else {
301            blob.transferTo(file);
302        }
303    }
304
305    protected boolean needToConvert(Blob blob) {
306        for (ImageToConvertDescriptor desc : imagesToConvert) {
307            String extension = getExtension(blob);
308            if (desc.getMimeType().equalsIgnoreCase(blob.getMimeType())
309                    || extension.equalsIgnoreCase(desc.getExtension())) {
310                return true;
311            }
312        }
313        return false;
314    }
315
316    protected String getExtension(Blob blob) {
317        String filename = blob.getFilename();
318        if (filename == null) {
319            return "";
320        }
321        int dotIndex = filename.lastIndexOf('.');
322        if (dotIndex == -1) {
323            return "";
324        }
325
326        return filename.substring(dotIndex + 1);
327    }
328
329    protected void transferAndConvert(Blob blob, File file) throws IOException {
330        File tmpFile = new File(file.getAbsolutePath() + ".tmp");
331        blob.transferTo(tmpFile);
332        try {
333            ImageConverter.convert(tmpFile.getAbsolutePath(), file.getAbsolutePath());
334        } catch (CommandNotAvailable | CommandException e) {
335            throw new IOException(e);
336        }
337
338        tmpFile.delete();
339    }
340
341    protected PictureTiles computeTiles(ImageInfo input, String outputDirPath, int tileWidth, int tileHeight,
342            int maxTiles, int xCenter, int yCenter, long lastModificationTime, boolean fullGeneration) {
343
344        PictureTiler pt = getDefaultTiler();
345        return pt.getTilesFromFile(input, outputDirPath, tileWidth, tileHeight, maxTiles, xCenter, yCenter,
346                lastModificationTime, fullGeneration);
347    }
348
349    protected PictureTiler getDefaultTiler() {
350        return defaultTiler;
351    }
352
353    // tests
354    public static void setDefaultTiler(PictureTiler tiler) {
355        defaultTiler = tiler;
356    }
357
358    // ****************************************
359    // Env setting management
360
361    public static Map<String, String> getEnv() {
362        return envParameters;
363    }
364
365    public static String getEnvValue(String paramName) {
366        if (envParameters == null) {
367            return null;
368        }
369        return envParameters.get(paramName);
370    }
371
372    public static String getEnvValue(String paramName, String defaultValue) {
373        String value = getEnvValue(paramName);
374        if (value == null) {
375            return defaultValue;
376        } else {
377            return value;
378        }
379    }
380
381    public static void setEnvValue(String paramName, String paramValue) {
382        envParameters.put(paramName, paramValue);
383    }
384
385    // Blob properties management
386    @Override
387    public Map<String, String> getBlobProperties() {
388        return blobProperties;
389    }
390
391    @Override
392    public String getBlobProperty(String docType) {
393        return blobProperties.get(docType);
394    }
395
396    @Override
397    public String getBlobProperty(String docType, String defaultValue) {
398        String property = blobProperties.get(docType);
399        if (property == null) {
400            return defaultValue;
401        }
402        return property;
403    }
404
405    // EP management
406
407    @Override
408    public void registerContribution(Object contribution, String extensionPoint, ComponentInstance contributor) {
409        if (ENV_PARAMETERS_EP.equals(extensionPoint)) {
410            TilingConfigurationDescriptor desc = (TilingConfigurationDescriptor) contribution;
411            envParameters.putAll(desc.getParameters());
412            workingDirPath = defaultWorkingDirPath();
413        } else if (BLOB_PROPERTY_EP.equals(extensionPoint)) {
414            TilingBlobPropertyDescriptor desc = (TilingBlobPropertyDescriptor) contribution;
415            blobProperties.putAll(desc.getBlobProperties());
416        } else if (IMAGES_TO_CONVERT_EP.equals(extensionPoint)) {
417            ImageToConvertDescriptor desc = (ImageToConvertDescriptor) contribution;
418            imagesToConvert.add(desc);
419        }
420    }
421
422    @Override
423    public void unregisterContribution(Object contribution, String extensionPoint, ComponentInstance contributor) {
424        // TODO
425    }
426
427    @Override
428    public void removeCacheEntry(ImageResource resource) {
429        if (cache.containsKey(resource.getHash())) {
430            PictureTilingCacheInfo cacheInfo = cache.remove(resource.getHash());
431            cacheInfo.cleanUp();
432        }
433    }
434
435}