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