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