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