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