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}