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}