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