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