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 Map<String, PictureTilingCacheInfo> cache = new HashMap<>();
064
065    protected List<String> inprocessTiles = Collections.synchronizedList(new ArrayList<>());
066
067    protected PictureTiler defaultTiler = new MagickTiler();
068
069    protected 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 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 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 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 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                    Thread.currentThread().interrupt();
191                    throw new NuxeoException(e);
192                }
193            }
194        }
195
196        PictureTiles tiles = getTilesWithSync(resource, tileWidth, tileHeight, maxTiles, xCenter, yCenter,
197                fullGeneration);
198        inprocessTiles.remove(cacheKey);
199
200        return tiles;
201    }
202
203    protected PictureTiles getTilesWithSync(ImageResource resource, int tileWidth, int tileHeight, int maxTiles,
204            int xCenter, int yCenter, boolean fullGeneration) {
205
206        String cacheKey = resource.getHash();
207        String inputFilePath;
208        PictureTilingCacheInfo cacheInfo;
209
210        if (cache.containsKey(cacheKey)) {
211            cacheInfo = cache.get(cacheKey);
212
213            PictureTiles pt = cacheInfo.getCachedPictureTiles(tileWidth, tileHeight, maxTiles);
214
215            if ((pt != null) && (pt.isTileComputed(xCenter, yCenter))) {
216                return pt;
217            }
218
219            inputFilePath = cacheInfo.getOriginalPicturePath();
220        } else {
221            String wdirPath = getWorkingDirPathForRessource(resource);
222            inputFilePath = wdirPath;
223            Blob blob = resource.getBlob();
224            inputFilePath += Integer.toString(blob.hashCode()) + ".";
225            if (blob.getFilename() != null) {
226                inputFilePath += FilenameUtils.getExtension(blob.getFilename());
227            } else {
228                inputFilePath += "img";
229            }
230
231            if (needToConvert(blob)) {
232                inputFilePath = FilenameUtils.removeExtension(inputFilePath) + ".jpg";
233            }
234
235            File inputFile = new File(inputFilePath);
236
237            if (!inputFile.exists()) {
238                try {
239                    // create the empty file ASAP to avoid concurrent transfer
240                    // and conversions
241                    if (inputFile.createNewFile()) {
242                        transferBlob(blob, inputFile);
243                    }
244                } catch (IOException e) {
245                    String msg = String.format(
246                            "Unable to transfer blob to file at '%s', " + "working directory path: '%s'", inputFilePath,
247                            wdirPath);
248                    log.error(msg, e);
249                    throw new NuxeoException(msg, e);
250                }
251                inputFile = new File(inputFilePath);
252            } else {
253                while (System.currentTimeMillis() - inputFile.lastModified() < 200) {
254                    try {
255                        log.debug("Waiting concurrent convert / dump");
256                        Thread.sleep(200);
257                    } catch (InterruptedException e) {
258                        Thread.currentThread().interrupt();
259                        throw new NuxeoException(e);
260                    }
261                }
262
263            }
264            try {
265                cacheInfo = new PictureTilingCacheInfo(cacheKey, wdirPath, inputFilePath);
266                cache.put(cacheKey, cacheInfo);
267            } catch (CommandNotAvailable | CommandException e) {
268                throw new NuxeoException(e);
269            }
270
271        }
272
273        // compute output dir
274        String outDirPath = cacheInfo.getTilingDir(tileWidth, tileHeight, maxTiles);
275
276        // try to see if a shrinked image can be used
277        ImageInfo bestImageInfo = cacheInfo.getBestSourceImage(tileWidth, tileHeight, maxTiles);
278
279        inputFilePath = bestImageInfo.getFilePath();
280        log.debug("input source image path for tile computation=" + inputFilePath);
281
282        long lastModificationTime = resource.getModificationDate().getTimeInMillis();
283        PictureTiles tiles = computeTiles(bestImageInfo, outDirPath, tileWidth, tileHeight, maxTiles, xCenter, yCenter,
284                lastModificationTime, fullGeneration);
285
286        tiles.getInfo().put(PictureTilesImpl.MAX_TILES_KEY, Integer.toString(maxTiles));
287        tiles.getInfo().put(PictureTilesImpl.TILES_WIDTH_KEY, Integer.toString(tileWidth));
288        tiles.getInfo().put(PictureTilesImpl.TILES_HEIGHT_KEY, Integer.toString(tileHeight));
289        String lastModificationDate = Long.toString(lastModificationTime);
290        tiles.getInfo().put(PictureTilesImpl.LAST_MODIFICATION_DATE_KEY, lastModificationDate);
291        tiles.setCacheKey(cacheKey);
292        tiles.setSourceImageInfo(bestImageInfo);
293        tiles.setOriginalImageInfo(cacheInfo.getOriginalPictureInfos());
294
295        cacheInfo.addPictureTilesToCache(tiles);
296        return tiles;
297    }
298
299    protected void transferBlob(Blob blob, File file) throws IOException {
300        if (needToConvert(blob)) {
301            transferAndConvert(blob, file);
302        } else {
303            blob.transferTo(file);
304        }
305    }
306
307    protected boolean needToConvert(Blob blob) {
308        for (ImageToConvertDescriptor desc : imagesToConvert) {
309            String extension = getExtension(blob);
310            if (desc.getMimeType().equalsIgnoreCase(blob.getMimeType())
311                    || extension.equalsIgnoreCase(desc.getExtension())) {
312                return true;
313            }
314        }
315        return false;
316    }
317
318    protected String getExtension(Blob blob) {
319        String filename = blob.getFilename();
320        if (filename == null) {
321            return "";
322        }
323        int dotIndex = filename.lastIndexOf('.');
324        if (dotIndex == -1) {
325            return "";
326        }
327
328        return filename.substring(dotIndex + 1);
329    }
330
331    protected void transferAndConvert(Blob blob, File file) throws IOException {
332        File tmpFile = new File(file.getAbsolutePath() + ".tmp");
333        blob.transferTo(tmpFile);
334        try {
335            ImageConverter.convert(tmpFile.getAbsolutePath(), file.getAbsolutePath());
336        } catch (CommandNotAvailable | CommandException e) {
337            throw new IOException(e);
338        }
339
340        tmpFile.delete();
341    }
342
343    protected PictureTiles computeTiles(ImageInfo input, String outputDirPath, int tileWidth, int tileHeight,
344            int maxTiles, int xCenter, int yCenter, long lastModificationTime, boolean fullGeneration) {
345
346        PictureTiler pt = getDefaultTiler();
347        return pt.getTilesFromFile(input, outputDirPath, tileWidth, tileHeight, maxTiles, xCenter, yCenter,
348                lastModificationTime, fullGeneration);
349    }
350
351    protected PictureTiler getDefaultTiler() {
352        return defaultTiler;
353    }
354
355    // tests
356    public void setDefaultTiler(PictureTiler tiler) {
357        defaultTiler = tiler;
358    }
359
360    // ****************************************
361    // Env setting management
362
363    public Map<String, String> getEnv() {
364        return envParameters;
365    }
366
367    public String getEnvValue(String paramName) {
368        if (envParameters == null) {
369            return null;
370        }
371        return envParameters.get(paramName);
372    }
373
374    public String getEnvValue(String paramName, String defaultValue) {
375        String value = getEnvValue(paramName);
376        if (value == null) {
377            return defaultValue;
378        } else {
379            return value;
380        }
381    }
382
383    public void setEnvValue(String paramName, String paramValue) {
384        envParameters.put(paramName, paramValue);
385    }
386
387    // Blob properties management
388    @Override
389    public Map<String, String> getBlobProperties() {
390        return blobProperties;
391    }
392
393    @Override
394    public String getBlobProperty(String docType) {
395        return blobProperties.get(docType);
396    }
397
398    @Override
399    public String getBlobProperty(String docType, String defaultValue) {
400        String property = blobProperties.get(docType);
401        if (property == null) {
402            return defaultValue;
403        }
404        return property;
405    }
406
407    // EP management
408
409    @Override
410    public void registerContribution(Object contribution, String extensionPoint, ComponentInstance contributor) {
411        if (ENV_PARAMETERS_EP.equals(extensionPoint)) {
412            TilingConfigurationDescriptor desc = (TilingConfigurationDescriptor) contribution;
413            envParameters.putAll(desc.getParameters());
414            workingDirPath = defaultWorkingDirPath();
415        } else if (BLOB_PROPERTY_EP.equals(extensionPoint)) {
416            TilingBlobPropertyDescriptor desc = (TilingBlobPropertyDescriptor) contribution;
417            blobProperties.putAll(desc.getBlobProperties());
418        } else if (IMAGES_TO_CONVERT_EP.equals(extensionPoint)) {
419            ImageToConvertDescriptor desc = (ImageToConvertDescriptor) contribution;
420            imagesToConvert.add(desc);
421        }
422    }
423
424    @Override
425    public void unregisterContribution(Object contribution, String extensionPoint, ComponentInstance contributor) {
426        // TODO
427    }
428
429    @Override
430    public void removeCacheEntry(ImageResource resource) {
431        if (cache.containsKey(resource.getHash())) {
432            PictureTilingCacheInfo cacheInfo = cache.remove(resource.getHash());
433            cacheInfo.cleanUp();
434        }
435    }
436
437}