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}