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