001/* 002 * Copyright (c) 2006-2014 Nuxeo SA (http://nuxeo.com/) and others. 003 * 004 * All rights reserved. This program and the accompanying materials 005 * are made available under the terms of the Eclipse Public License v1.0 006 * which accompanies this distribution, and is available at 007 * http://www.eclipse.org/legal/epl-v10.html 008 * 009 * Contributors: 010 * Florent Guillaume 011 */ 012 013package org.nuxeo.ecm.core.convert.service; 014 015import java.io.Serializable; 016import java.util.ArrayList; 017import java.util.HashMap; 018import java.util.List; 019import java.util.Map; 020 021import org.apache.commons.io.FilenameUtils; 022import org.apache.commons.lang.StringUtils; 023import org.apache.commons.logging.Log; 024import org.apache.commons.logging.LogFactory; 025import org.nuxeo.ecm.core.api.Blob; 026import org.nuxeo.ecm.core.api.blobholder.BlobHolder; 027import org.nuxeo.ecm.core.api.blobholder.SimpleBlobHolderWithProperties; 028import org.nuxeo.ecm.core.convert.api.ConversionException; 029import org.nuxeo.ecm.core.convert.api.ConversionService; 030import org.nuxeo.ecm.core.convert.api.ConversionStatus; 031import org.nuxeo.ecm.core.convert.api.ConverterCheckResult; 032import org.nuxeo.ecm.core.convert.api.ConverterNotAvailable; 033import org.nuxeo.ecm.core.convert.api.ConverterNotRegistered; 034import org.nuxeo.ecm.core.convert.cache.CacheKeyGenerator; 035import org.nuxeo.ecm.core.convert.cache.ConversionCacheHolder; 036import org.nuxeo.ecm.core.convert.cache.GCTask; 037import org.nuxeo.ecm.core.convert.extension.ChainedConverter; 038import org.nuxeo.ecm.core.convert.extension.Converter; 039import org.nuxeo.ecm.core.convert.extension.ConverterDescriptor; 040import org.nuxeo.ecm.core.convert.extension.ExternalConverter; 041import org.nuxeo.ecm.core.convert.extension.GlobalConfigDescriptor; 042import org.nuxeo.ecm.core.transientstore.api.StorageEntry; 043import org.nuxeo.ecm.core.work.api.Work; 044import org.nuxeo.ecm.core.work.api.WorkManager; 045import org.nuxeo.ecm.platform.mimetype.interfaces.MimetypeEntry; 046import org.nuxeo.ecm.platform.mimetype.interfaces.MimetypeRegistry; 047import org.nuxeo.runtime.api.Framework; 048import org.nuxeo.runtime.model.ComponentContext; 049import org.nuxeo.runtime.model.ComponentInstance; 050import org.nuxeo.runtime.model.DefaultComponent; 051 052/** 053 * Runtime Component that also provides the POJO implementation of the {@link ConversionService}. 054 * 055 * @author tiry 056 */ 057public class ConversionServiceImpl extends DefaultComponent implements ConversionService { 058 059 protected static final Log log = LogFactory.getLog(ConversionServiceImpl.class); 060 061 public static final String CONVERTER_EP = "converter"; 062 063 public static final String CONFIG_EP = "configuration"; 064 065 protected final Map<String, ConverterDescriptor> converterDescriptors = new HashMap<>(); 066 067 protected final MimeTypeTranslationHelper translationHelper = new MimeTypeTranslationHelper(); 068 069 protected final GlobalConfigDescriptor config = new GlobalConfigDescriptor(); 070 071 protected static ConversionServiceImpl self; 072 073 protected Thread gcThread; 074 075 @Override 076 public void activate(ComponentContext context) { 077 converterDescriptors.clear(); 078 translationHelper.clear(); 079 self = this; 080 } 081 082 @Override 083 public void deactivate(ComponentContext context) { 084 if (config.isCacheEnabled()) { 085 ConversionCacheHolder.deleteCache(); 086 } 087 self = null; 088 converterDescriptors.clear(); 089 translationHelper.clear(); 090 } 091 092 /** 093 * Component implementation. 094 */ 095 @Override 096 public void registerContribution(Object contribution, String extensionPoint, ComponentInstance contributor) { 097 098 if (CONVERTER_EP.equals(extensionPoint)) { 099 ConverterDescriptor desc = (ConverterDescriptor) contribution; 100 registerConverter(desc); 101 } else if (CONFIG_EP.equals(extensionPoint)) { 102 GlobalConfigDescriptor desc = (GlobalConfigDescriptor) contribution; 103 config.update(desc); 104 } else { 105 log.error("Unable to handle unknown extensionPoint " + extensionPoint); 106 } 107 } 108 109 @Override 110 public void unregisterContribution(Object contribution, String extensionPoint, ComponentInstance contributor) { 111 } 112 113 /* Component API */ 114 115 public static Converter getConverter(String converterName) { 116 ConverterDescriptor desc = self.converterDescriptors.get(converterName); 117 if (desc == null) { 118 return null; 119 } 120 return desc.getConverterInstance(); 121 } 122 123 public static ConverterDescriptor getConverterDescriptor(String converterName) { 124 return self.converterDescriptors.get(converterName); 125 } 126 127 public static long getGCIntervalInMinutes() { 128 return self.config.getGCInterval(); 129 } 130 131 public static void setGCIntervalInMinutes(long interval) { 132 self.config.setGCInterval(interval); 133 } 134 135 public static void registerConverter(ConverterDescriptor desc) { 136 137 if (self.converterDescriptors.containsKey(desc.getConverterName())) { 138 139 ConverterDescriptor existing = self.converterDescriptors.get(desc.getConverterName()); 140 desc = existing.merge(desc); 141 } 142 desc.initConverter(); 143 self.translationHelper.addConverter(desc); 144 self.converterDescriptors.put(desc.getConverterName(), desc); 145 } 146 147 public static int getMaxCacheSizeInKB() { 148 return self.config.getDiskCacheSize(); 149 } 150 151 public static void setMaxCacheSizeInKB(int size) { 152 self.config.setDiskCacheSize(size); 153 } 154 155 public static boolean isCacheEnabled() { 156 return self.config.isCacheEnabled(); 157 } 158 159 public static String getCacheBasePath() { 160 return self.config.getCachingDirectory(); 161 } 162 163 /* Service API */ 164 165 @Override 166 public List<String> getRegistredConverters() { 167 List<String> converterNames = new ArrayList<>(); 168 converterNames.addAll(converterDescriptors.keySet()); 169 return converterNames; 170 } 171 172 @Override 173 public BlobHolder convert(String converterName, BlobHolder blobHolder, Map<String, Serializable> parameters) 174 throws ConversionException { 175 176 // exist if not registered 177 ConverterCheckResult check = isConverterAvailable(converterName); 178 if (!check.isAvailable()) { 179 // exist is not installed / configured 180 throw new ConverterNotAvailable(converterName); 181 } 182 183 ConverterDescriptor desc = converterDescriptors.get(converterName); 184 if (desc == null) { 185 throw new ConversionException("Converter " + converterName + " can not be found"); 186 } 187 188 String cacheKey = CacheKeyGenerator.computeKey(converterName, blobHolder, parameters); 189 190 BlobHolder result = ConversionCacheHolder.getFromCache(cacheKey); 191 192 if (result == null) { 193 Converter converter = desc.getConverterInstance(); 194 result = converter.convert(blobHolder, parameters); 195 196 if (config.isCacheEnabled()) { 197 ConversionCacheHolder.addToCache(cacheKey, result); 198 } 199 } 200 201 if (result != null) { 202 updateResultBlobMimeType(result, desc); 203 updateResultBlobFileName(blobHolder, result); 204 } 205 206 return result; 207 } 208 209 protected void updateResultBlobMimeType(BlobHolder resultBh, ConverterDescriptor desc) { 210 Blob mainBlob = resultBh.getBlob(); 211 if (mainBlob != null) { 212 String mimeType = mainBlob.getMimeType(); 213 if (StringUtils.isBlank(mimeType) || mimeType.equals("application/octet-stream")) { 214 mainBlob.setMimeType(desc.getDestinationMimeType()); 215 } 216 } 217 } 218 219 protected void updateResultBlobFileName(BlobHolder srcBh, BlobHolder resultBh) { 220 Blob mainBlob = resultBh.getBlob(); 221 String filename = mainBlob.getFilename(); 222 if (StringUtils.isBlank(filename) || filename.startsWith("nxblob-")) { 223 Blob srcBlob = srcBh.getBlob(); 224 if (srcBlob != null && StringUtils.isNotBlank(srcBlob.getFilename())) { 225 String baseName = FilenameUtils.getBaseName(srcBlob.getFilename()); 226 227 MimetypeRegistry mimetypeRegistry = Framework.getLocalService(MimetypeRegistry.class); 228 MimetypeEntry mimeTypeEntry = mimetypeRegistry.getMimetypeEntryByMimeType(mainBlob.getMimeType()); 229 List<String> extensions = mimeTypeEntry.getExtensions(); 230 String extension; 231 if (!extensions.isEmpty()) { 232 extension = extensions.get(0); 233 } else { 234 extension = FilenameUtils.getExtension(filename); 235 if (extension == null) { 236 extension = "bin"; 237 } 238 } 239 mainBlob.setFilename(baseName + "." + extension); 240 } 241 242 } 243 } 244 245 @Override 246 public BlobHolder convertToMimeType(String destinationMimeType, BlobHolder blobHolder, 247 Map<String, Serializable> parameters) throws ConversionException { 248 String srcMt = blobHolder.getBlob().getMimeType(); 249 String converterName = translationHelper.getConverterName(srcMt, destinationMimeType); 250 if (converterName == null) { 251 throw new ConversionException("Cannot find converter from type " + srcMt + " to type " 252 + destinationMimeType); 253 } 254 return convert(converterName, blobHolder, parameters); 255 } 256 257 @Override 258 public List<String> getConverterNames(String sourceMimeType, String destinationMimeType) { 259 return translationHelper.getConverterNames(sourceMimeType, destinationMimeType); 260 } 261 262 @Override 263 public String getConverterName(String sourceMimeType, String destinationMimeType) { 264 List<String> converterNames = getConverterNames(sourceMimeType, destinationMimeType); 265 if (!converterNames.isEmpty()) { 266 return converterNames.get(converterNames.size() - 1); 267 } 268 return null; 269 } 270 271 @Override 272 public ConverterCheckResult isConverterAvailable(String converterName) throws ConversionException { 273 return isConverterAvailable(converterName, false); 274 } 275 276 protected final Map<String, ConverterCheckResult> checkResultCache = new HashMap<>(); 277 278 @Override 279 public ConverterCheckResult isConverterAvailable(String converterName, boolean refresh) 280 throws ConverterNotRegistered { 281 282 if (!refresh) { 283 if (checkResultCache.containsKey(converterName)) { 284 return checkResultCache.get(converterName); 285 } 286 } 287 288 ConverterDescriptor descriptor = converterDescriptors.get(converterName); 289 if (descriptor == null) { 290 throw new ConverterNotRegistered(converterName); 291 } 292 293 Converter converter = descriptor.getConverterInstance(); 294 295 ConverterCheckResult result; 296 if (converter instanceof ExternalConverter) { 297 ExternalConverter exConverter = (ExternalConverter) converter; 298 result = exConverter.isConverterAvailable(); 299 } else if (converter instanceof ChainedConverter) { 300 ChainedConverter chainedConverter = (ChainedConverter) converter; 301 result = new ConverterCheckResult(); 302 if (chainedConverter.isSubConvertersBased()) { 303 for (String subConverterName : chainedConverter.getSubConverters()) { 304 result = isConverterAvailable(subConverterName, refresh); 305 if (!result.isAvailable()) { 306 break; 307 } 308 } 309 } 310 } else { 311 // return success since there is nothing to test 312 result = new ConverterCheckResult(); 313 } 314 315 result.setSupportedInputMimeTypes(descriptor.getSourceMimeTypes()); 316 checkResultCache.put(converterName, result); 317 318 return result; 319 } 320 321 @Override 322 public boolean isSourceMimeTypeSupported(String converterName, String sourceMimeType) { 323 return getConverterDescriptor(converterName).getSourceMimeTypes().contains(sourceMimeType); 324 } 325 326 @Override 327 public String scheduleConversion(String converterName, BlobHolder blobHolder, Map<String, Serializable> parameters) { 328 WorkManager workManager = Framework.getService(WorkManager.class); 329 ConversionWork work = new ConversionWork(converterName, blobHolder, parameters); 330 workManager.schedule(work); 331 return work.getId(); 332 } 333 334 @Override 335 public ConversionStatus getConversionStatus(String id) { 336 WorkManager workManager = Framework.getService(WorkManager.class); 337 Work.State workState = workManager.getWorkState(id); 338 if (workState == null) { 339 return null; 340 } 341 342 return new ConversionStatus(id, ConversionStatus.Status.valueOf(workState.name())); 343 } 344 345 @Override 346 public BlobHolder getConversionResult(String id, boolean cleanTransientStoreEntry) { 347 WorkManager workManager = Framework.getService(WorkManager.class); 348 String result = workManager.findResult(id); 349 if (result == null) { 350 return null; 351 } 352 353 StorageEntry storageEntry = ConversionWork.getStorageEntry(result); 354 if (storageEntry == null) { 355 return null; 356 } 357 358 if (cleanTransientStoreEntry) { 359 ConversionWork.removeStorageEntry(result); 360 } 361 return new SimpleBlobHolderWithProperties(storageEntry.getBlobs(), storageEntry.getParameters()); 362 } 363 364 @Override 365 public <T> T getAdapter(Class<T> adapter) { 366 if (adapter.isAssignableFrom(MimeTypeTranslationHelper.class)) { 367 return adapter.cast(translationHelper); 368 } 369 return super.getAdapter(adapter); 370 } 371 372 @Override 373 public void applicationStarted(ComponentContext context) { 374 startGC(); 375 } 376 377 protected void startGC() { 378 log.debug("CasheCGTaskActivator activated starting GC thread"); 379 gcThread = new Thread(new GCTask(), "Nuxeo-Convert-GC"); 380 gcThread.setDaemon(true); 381 gcThread.start(); 382 log.debug("GC Thread started"); 383 384 } 385 386 public void endGC() { 387 log.debug("Stopping GC Thread"); 388 gcThread.interrupt(); 389 gcThread = null; 390 } 391 392}