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