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