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        // exist if not registered
182        ConverterCheckResult check = isConverterAvailable(converterName);
183        if (!check.isAvailable()) {
184            // exist is not installed / configured
185            throw new ConverterNotAvailable(converterName);
186        }
187
188        ConverterDescriptor desc = converterDescriptors.get(converterName);
189        if (desc == null) {
190            throw new ConversionException("Converter " + converterName + " can not be found");
191        }
192
193        String cacheKey = CacheKeyGenerator.computeKey(converterName, blobHolder, parameters);
194
195        BlobHolder result = ConversionCacheHolder.getFromCache(cacheKey);
196
197        if (result == null) {
198            Converter converter = desc.getConverterInstance();
199            result = converter.convert(blobHolder, parameters);
200
201            if (config.isCacheEnabled()) {
202                ConversionCacheHolder.addToCache(cacheKey, result);
203            }
204        }
205
206        if (result != null) {
207            updateResultBlobMimeType(result, desc);
208            updateResultBlobFileName(blobHolder, result);
209        }
210
211        return result;
212    }
213
214    protected void updateResultBlobMimeType(BlobHolder resultBh, ConverterDescriptor desc) {
215        Blob mainBlob = resultBh.getBlob();
216        if (mainBlob == null) {
217            return;
218        }
219        String mimeType = mainBlob.getMimeType();
220        if (StringUtils.isBlank(mimeType) || mimeType.equals("application/octet-stream")) {
221            mainBlob.setMimeType(desc.getDestinationMimeType());
222        }
223    }
224
225    protected void updateResultBlobFileName(BlobHolder srcBh, BlobHolder resultBh) {
226        Blob mainBlob = resultBh.getBlob();
227        if (mainBlob == null) {
228            return;
229        }
230        String filename = mainBlob.getFilename();
231        if (StringUtils.isBlank(filename) || filename.startsWith("nxblob-")) {
232            Blob srcBlob = srcBh.getBlob();
233            if (srcBlob != null && StringUtils.isNotBlank(srcBlob.getFilename())) {
234                String baseName = FilenameUtils.getBaseName(srcBlob.getFilename());
235
236                MimetypeRegistry mimetypeRegistry = Framework.getLocalService(MimetypeRegistry.class);
237                MimetypeEntry mimeTypeEntry = mimetypeRegistry.getMimetypeEntryByMimeType(mainBlob.getMimeType());
238                List<String> extensions = mimeTypeEntry.getExtensions();
239                String extension;
240                if (!extensions.isEmpty()) {
241                    extension = extensions.get(0);
242                } else {
243                    extension = FilenameUtils.getExtension(filename);
244                    if (extension == null) {
245                        extension = "bin";
246                    }
247                }
248                mainBlob.setFilename(baseName + "." + extension);
249            }
250
251        }
252    }
253
254    @Override
255    public BlobHolder convertToMimeType(String destinationMimeType, BlobHolder blobHolder,
256            Map<String, Serializable> parameters) throws ConversionException {
257        String srcMt = blobHolder.getBlob().getMimeType();
258        String converterName = translationHelper.getConverterName(srcMt, destinationMimeType);
259        if (converterName == null) {
260            throw new ConversionException("Cannot find converter from type " + srcMt + " to type "
261                    + destinationMimeType);
262        }
263        return convert(converterName, blobHolder, parameters);
264    }
265
266    @Override
267    public List<String> getConverterNames(String sourceMimeType, String destinationMimeType) {
268        return translationHelper.getConverterNames(sourceMimeType, destinationMimeType);
269    }
270
271    @Override
272    public String getConverterName(String sourceMimeType, String destinationMimeType) {
273        List<String> converterNames = getConverterNames(sourceMimeType, destinationMimeType);
274        if (!converterNames.isEmpty()) {
275            return converterNames.get(converterNames.size() - 1);
276        }
277        return null;
278    }
279
280    @Override
281    public ConverterCheckResult isConverterAvailable(String converterName) throws ConversionException {
282        return isConverterAvailable(converterName, false);
283    }
284
285    protected final Map<String, ConverterCheckResult> checkResultCache = new HashMap<>();
286
287    @Override
288    public ConverterCheckResult isConverterAvailable(String converterName, boolean refresh)
289            throws ConverterNotRegistered {
290
291        if (!refresh) {
292            if (checkResultCache.containsKey(converterName)) {
293                return checkResultCache.get(converterName);
294            }
295        }
296
297        ConverterDescriptor descriptor = converterDescriptors.get(converterName);
298        if (descriptor == null) {
299            throw new ConverterNotRegistered(converterName);
300        }
301
302        Converter converter = descriptor.getConverterInstance();
303
304        ConverterCheckResult result;
305        if (converter instanceof ExternalConverter) {
306            ExternalConverter exConverter = (ExternalConverter) converter;
307            result = exConverter.isConverterAvailable();
308        } else if (converter instanceof ChainedConverter) {
309            ChainedConverter chainedConverter = (ChainedConverter) converter;
310            result = new ConverterCheckResult();
311            if (chainedConverter.isSubConvertersBased()) {
312                for (String subConverterName : chainedConverter.getSubConverters()) {
313                    result = isConverterAvailable(subConverterName, refresh);
314                    if (!result.isAvailable()) {
315                        break;
316                    }
317                }
318            }
319        } else {
320            // return success since there is nothing to test
321            result = new ConverterCheckResult();
322        }
323
324        result.setSupportedInputMimeTypes(descriptor.getSourceMimeTypes());
325        checkResultCache.put(converterName, result);
326
327        return result;
328    }
329
330    @Override
331    public boolean isSourceMimeTypeSupported(String converterName, String sourceMimeType) {
332        return getConverterDescriptor(converterName).getSourceMimeTypes().contains(sourceMimeType);
333    }
334
335    @Override
336    public String scheduleConversion(String converterName, BlobHolder blobHolder, Map<String, Serializable> parameters) {
337        WorkManager workManager = Framework.getService(WorkManager.class);
338        ConversionWork work = new ConversionWork(converterName, null, blobHolder, parameters);
339        workManager.schedule(work);
340        return work.getId();
341    }
342
343    @Override
344    public String scheduleConversionToMimeType(String destinationMimeType, BlobHolder blobHolder,
345            Map<String, Serializable> parameters) {
346        WorkManager workManager = Framework.getService(WorkManager.class);
347        ConversionWork work = new ConversionWork(null, destinationMimeType, blobHolder, parameters);
348        workManager.schedule(work);
349        return work.getId();
350    }
351
352    @Override
353    public ConversionStatus getConversionStatus(String id) {
354        WorkManager workManager = Framework.getService(WorkManager.class);
355        Work.State workState = workManager.getWorkState(id);
356        if (workState == null) {
357            return null;
358        }
359
360        return new ConversionStatus(id, ConversionStatus.Status.valueOf(workState.name()));
361    }
362
363    @Override
364    public BlobHolder getConversionResult(String id, boolean cleanTransientStoreEntry) {
365        WorkManager workManager = Framework.getService(WorkManager.class);
366        String result = workManager.findResult(id);
367        if (result == null) {
368            return null;
369        }
370
371        BlobHolder bh = ConversionWork.getBlobHolder(result);
372        if (cleanTransientStoreEntry) {
373            ConversionWork.removeBlobHolder(result);
374        }
375        return bh;
376    }
377
378    @Override
379    public <T> T getAdapter(Class<T> adapter) {
380        if (adapter.isAssignableFrom(MimeTypeTranslationHelper.class)) {
381            return adapter.cast(translationHelper);
382        }
383        return super.getAdapter(adapter);
384    }
385
386    @Override
387    public void applicationStarted(ComponentContext context) {
388        startGC();
389    }
390
391    protected void startGC() {
392        log.debug("CasheCGTaskActivator activated starting GC thread");
393        gcThread = new Thread(new GCTask(), "Nuxeo-Convert-GC");
394        gcThread.setDaemon(true);
395        gcThread.start();
396        log.debug("GC Thread started");
397
398    }
399
400    public void endGC() {
401        log.debug("Stopping GC Thread");
402        gcThread.interrupt();
403        gcThread = null;
404    }
405
406}