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