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}