/*
* (C) Copyright 2006-2016 Nuxeo SA (http://nuxeo.com/) and others.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Contributors:
* Nuxeo - initial API and implementation
*/
package org.nuxeo.ecm.platform.pictures.tiles.service;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.common.Environment;
import org.nuxeo.common.utils.ExceptionUtils;
import org.nuxeo.ecm.core.api.Blob;
import org.nuxeo.ecm.core.api.NuxeoException;
import org.nuxeo.ecm.platform.commandline.executor.api.CommandException;
import org.nuxeo.ecm.platform.commandline.executor.api.CommandNotAvailable;
import org.nuxeo.ecm.platform.picture.api.ImageInfo;
import org.nuxeo.ecm.platform.picture.magick.utils.ImageConverter;
import org.nuxeo.ecm.platform.pictures.tiles.api.PictureTiles;
import org.nuxeo.ecm.platform.pictures.tiles.api.PictureTilesImpl;
import org.nuxeo.ecm.platform.pictures.tiles.api.PictureTilingService;
import org.nuxeo.ecm.platform.pictures.tiles.api.imageresource.ImageResource;
import org.nuxeo.ecm.platform.pictures.tiles.magick.tiler.MagickTiler;
import org.nuxeo.ecm.platform.pictures.tiles.tilers.PictureTiler;
import org.nuxeo.runtime.model.ComponentContext;
import org.nuxeo.runtime.model.ComponentInstance;
import org.nuxeo.runtime.model.DefaultComponent;
/**
* Runtime component that expose the PictureTilingService interface. Also exposes the configuration Extension Point
*
* @author tiry
*/
public class PictureTilingComponent extends DefaultComponent implements PictureTilingService {
public static final String ENV_PARAMETERS_EP = "environment";
public static final String BLOB_PROPERTY_EP = "blobProperties";
public static final String IMAGES_TO_CONVERT_EP = "imagesToConvert";
protected static Map<String, PictureTilingCacheInfo> cache = new HashMap<>();
protected static List<String> inprocessTiles = Collections.synchronizedList(new ArrayList<>());
protected static PictureTiler defaultTiler = new MagickTiler();
protected static Map<String, String> envParameters = new HashMap<>();
protected Map<String, String> blobProperties = new HashMap<>();
protected List<ImageToConvertDescriptor> imagesToConvert = new ArrayList<>();
protected static Thread gcThread;
private String workingDirPath = defaultWorkingDirPath();
private static final Log log = LogFactory.getLog(PictureTilingComponent.class);
@Override
public void activate(ComponentContext context) {
defaultTiler = new MagickTiler();
startGC();
}
public static void startGC() {
if (!GCTask.GCEnabled) {
GCTask.GCEnabled = true;
log.debug("PictureTilingComponent activated starting GC thread");
gcThread = new Thread(new GCTask(), "Nuxeo-Tiling-GC");
gcThread.setDaemon(true);
gcThread.start();
log.debug("GC Thread started");
} else {
log.debug("GC Thread is already started");
}
}
public static void endGC() {
if (GCTask.GCEnabled) {
GCTask.GCEnabled = false;
log.debug("Stopping GC Thread");
gcThread.interrupt();
} else {
log.debug("GC Thread is already stopped");
}
}
@Override
public void deactivate(ComponentContext context) {
endGC();
}
public static Map<String, PictureTilingCacheInfo> getCache() {
return cache;
}
protected String getWorkingDirPath() {
return workingDirPath;
}
protected String defaultWorkingDirPath() {
String defaultPath = new File(Environment.getDefault().getData(), "nuxeo-tiling-cache").getAbsolutePath();
String path = getEnvValue("WorkingDirPath", defaultPath);
return normalizeWorkingDirPath(path);
}
protected String normalizeWorkingDirPath(String path) {
File dir = new File(path);
if (!dir.exists()) {
dir.mkdir();
}
path = dir.getAbsolutePath();
if (!path.endsWith(File.separator)) {
path += File.separator;
}
return path;
}
@Override
public void setWorkingDirPath(String path) {
workingDirPath = normalizeWorkingDirPath(path);
}
protected String getWorkingDirPathForRessource(ImageResource resource) {
String pathForBlob = getWorkingDirPath();
String digest = resource.getHash();
pathForBlob = pathForBlob + digest + File.separator;
log.debug("WorkingDirPath for resource=" + pathForBlob);
File wdir = new File(pathForBlob);
if (!wdir.exists()) {
wdir.mkdir();
}
return pathForBlob;
}
@Override
public PictureTiles getTiles(ImageResource resource, int tileWidth, int tileHeight, int maxTiles) {
return getTiles(resource, tileWidth, tileHeight, maxTiles, 0, 0, false);
}
@Override
public PictureTiles completeTiles(PictureTiles existingTiles, int xCenter, int yCenter) {
String outputDirPath = existingTiles.getTilesPath();
long lastModificationTime = Long.parseLong(
existingTiles.getInfo().get(PictureTilesImpl.LAST_MODIFICATION_DATE_KEY));
return computeTiles(existingTiles.getSourceImageInfo(), outputDirPath, existingTiles.getTilesWidth(),
existingTiles.getTilesHeight(), existingTiles.getMaxTiles(), xCenter, yCenter, lastModificationTime,
false);
}
@Override
public PictureTiles getTiles(ImageResource resource, int tileWidth, int tileHeight, int maxTiles, int xCenter,
int yCenter, boolean fullGeneration) {
log.debug("enter getTiles");
String cacheKey = resource.getHash();
if (defaultTiler.needsSync()) {
// some tiler implementation may generate several tiles at once
// in order to be efficient this requires synchronization
while (inprocessTiles.contains(cacheKey)) {
try {
log.debug("Waiting for tiler sync");
Thread.sleep(200);
} catch (InterruptedException e) {
ExceptionUtils.checkInterrupt(e);
}
}
}
PictureTiles tiles = getTilesWithSync(resource, tileWidth, tileHeight, maxTiles, xCenter, yCenter,
fullGeneration);
inprocessTiles.remove(cacheKey);
return tiles;
}
protected PictureTiles getTilesWithSync(ImageResource resource, int tileWidth, int tileHeight, int maxTiles,
int xCenter, int yCenter, boolean fullGeneration) {
String cacheKey = resource.getHash();
String inputFilePath;
PictureTilingCacheInfo cacheInfo;
if (cache.containsKey(cacheKey)) {
cacheInfo = cache.get(cacheKey);
PictureTiles pt = cacheInfo.getCachedPictureTiles(tileWidth, tileHeight, maxTiles);
if ((pt != null) && (pt.isTileComputed(xCenter, yCenter))) {
return pt;
}
inputFilePath = cacheInfo.getOriginalPicturePath();
} else {
String wdirPath = getWorkingDirPathForRessource(resource);
inputFilePath = wdirPath;
Blob blob = resource.getBlob();
inputFilePath += Integer.toString(blob.hashCode()) + ".";
if (blob.getFilename() != null) {
inputFilePath += FilenameUtils.getExtension(blob.getFilename());
} else {
inputFilePath += "img";
}
if (needToConvert(blob)) {
inputFilePath = FilenameUtils.removeExtension(inputFilePath) + ".jpg";
}
File inputFile = new File(inputFilePath);
if (!inputFile.exists()) {
try {
// create the empty file ASAP to avoid concurrent transfer
// and conversions
if (inputFile.createNewFile()) {
transferBlob(blob, inputFile);
}
} catch (IOException e) {
String msg = String.format(
"Unable to transfer blob to file at '%s', " + "working directory path: '%s'", inputFilePath,
wdirPath);
log.error(msg, e);
throw new NuxeoException(msg, e);
}
inputFile = new File(inputFilePath);
} else {
while (System.currentTimeMillis() - inputFile.lastModified() < 200) {
try {
log.debug("Waiting concurrent convert / dump");
Thread.sleep(200);
} catch (InterruptedException e) {
ExceptionUtils.checkInterrupt(e);
}
}
}
try {
cacheInfo = new PictureTilingCacheInfo(cacheKey, wdirPath, inputFilePath);
cache.put(cacheKey, cacheInfo);
} catch (CommandNotAvailable | CommandException e) {
throw new NuxeoException(e);
}
}
// compute output dir
String outDirPath = cacheInfo.getTilingDir(tileWidth, tileHeight, maxTiles);
// try to see if a shrinked image can be used
ImageInfo bestImageInfo = cacheInfo.getBestSourceImage(tileWidth, tileHeight, maxTiles);
inputFilePath = bestImageInfo.getFilePath();
log.debug("input source image path for tile computation=" + inputFilePath);
long lastModificationTime = resource.getModificationDate().getTimeInMillis();
PictureTiles tiles = computeTiles(bestImageInfo, outDirPath, tileWidth, tileHeight, maxTiles, xCenter, yCenter,
lastModificationTime, fullGeneration);
tiles.getInfo().put(PictureTilesImpl.MAX_TILES_KEY, Integer.toString(maxTiles));
tiles.getInfo().put(PictureTilesImpl.TILES_WIDTH_KEY, Integer.toString(tileWidth));
tiles.getInfo().put(PictureTilesImpl.TILES_HEIGHT_KEY, Integer.toString(tileHeight));
String lastModificationDate = Long.toString(lastModificationTime);
tiles.getInfo().put(PictureTilesImpl.LAST_MODIFICATION_DATE_KEY, lastModificationDate);
tiles.setCacheKey(cacheKey);
tiles.setSourceImageInfo(bestImageInfo);
tiles.setOriginalImageInfo(cacheInfo.getOriginalPictureInfos());
cacheInfo.addPictureTilesToCache(tiles);
return tiles;
}
protected void transferBlob(Blob blob, File file) throws IOException {
if (needToConvert(blob)) {
transferAndConvert(blob, file);
} else {
blob.transferTo(file);
}
}
protected boolean needToConvert(Blob blob) {
for (ImageToConvertDescriptor desc : imagesToConvert) {
String extension = getExtension(blob);
if (desc.getMimeType().equalsIgnoreCase(blob.getMimeType())
|| extension.equalsIgnoreCase(desc.getExtension())) {
return true;
}
}
return false;
}
protected String getExtension(Blob blob) {
String filename = blob.getFilename();
if (filename == null) {
return "";
}
int dotIndex = filename.lastIndexOf('.');
if (dotIndex == -1) {
return "";
}
return filename.substring(dotIndex + 1);
}
protected void transferAndConvert(Blob blob, File file) throws IOException {
File tmpFile = new File(file.getAbsolutePath() + ".tmp");
blob.transferTo(tmpFile);
try {
ImageConverter.convert(tmpFile.getAbsolutePath(), file.getAbsolutePath());
} catch (CommandNotAvailable | CommandException e) {
throw new IOException(e);
}
tmpFile.delete();
}
protected PictureTiles computeTiles(ImageInfo input, String outputDirPath, int tileWidth, int tileHeight,
int maxTiles, int xCenter, int yCenter, long lastModificationTime, boolean fullGeneration) {
PictureTiler pt = getDefaultTiler();
return pt.getTilesFromFile(input, outputDirPath, tileWidth, tileHeight, maxTiles, xCenter, yCenter,
lastModificationTime, fullGeneration);
}
protected PictureTiler getDefaultTiler() {
return defaultTiler;
}
// tests
public static void setDefaultTiler(PictureTiler tiler) {
defaultTiler = tiler;
}
// ****************************************
// Env setting management
public static Map<String, String> getEnv() {
return envParameters;
}
public static String getEnvValue(String paramName) {
if (envParameters == null) {
return null;
}
return envParameters.get(paramName);
}
public static String getEnvValue(String paramName, String defaultValue) {
String value = getEnvValue(paramName);
if (value == null) {
return defaultValue;
} else {
return value;
}
}
public static void setEnvValue(String paramName, String paramValue) {
envParameters.put(paramName, paramValue);
}
// Blob properties management
@Override
public Map<String, String> getBlobProperties() {
return blobProperties;
}
@Override
public String getBlobProperty(String docType) {
return blobProperties.get(docType);
}
@Override
public String getBlobProperty(String docType, String defaultValue) {
String property = blobProperties.get(docType);
if (property == null) {
return defaultValue;
}
return property;
}
// EP management
@Override
public void registerContribution(Object contribution, String extensionPoint, ComponentInstance contributor) {
if (ENV_PARAMETERS_EP.equals(extensionPoint)) {
TilingConfigurationDescriptor desc = (TilingConfigurationDescriptor) contribution;
envParameters.putAll(desc.getParameters());
workingDirPath = defaultWorkingDirPath();
} else if (BLOB_PROPERTY_EP.equals(extensionPoint)) {
TilingBlobPropertyDescriptor desc = (TilingBlobPropertyDescriptor) contribution;
blobProperties.putAll(desc.getBlobProperties());
} else if (IMAGES_TO_CONVERT_EP.equals(extensionPoint)) {
ImageToConvertDescriptor desc = (ImageToConvertDescriptor) contribution;
imagesToConvert.add(desc);
}
}
@Override
public void unregisterContribution(Object contribution, String extensionPoint, ComponentInstance contributor) {
// TODO
}
@Override
public void removeCacheEntry(ImageResource resource) {
if (cache.containsKey(resource.getHash())) {
PictureTilingCacheInfo cacheInfo = cache.remove(resource.getHash());
cacheInfo.cleanUp();
}
}
}