001/*
002 * (C) Copyright 2006-2016 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 */
017package org.nuxeo.ecm.platform.preview.adapter.base;
018
019import java.io.File;
020import java.io.IOException;
021import java.io.InputStream;
022import java.util.Collections;
023import java.util.List;
024
025import org.apache.commons.codec.digest.DigestUtils;
026import org.apache.commons.logging.Log;
027import org.apache.commons.logging.LogFactory;
028import org.nuxeo.ecm.core.api.Blob;
029import org.nuxeo.ecm.core.api.blobholder.BlobHolder;
030import org.nuxeo.ecm.core.api.blobholder.DocumentBlobHolder;
031import org.nuxeo.ecm.core.api.model.PropertyNotFoundException;
032import org.nuxeo.ecm.core.convert.api.ConversionException;
033import org.nuxeo.ecm.core.convert.api.ConversionService;
034import org.nuxeo.ecm.platform.mimetype.MimetypeDetectionException;
035import org.nuxeo.ecm.platform.mimetype.MimetypeNotFoundException;
036import org.nuxeo.ecm.platform.mimetype.interfaces.MimetypeRegistry;
037import org.nuxeo.ecm.platform.preview.adapter.MimeTypePreviewer;
038import org.nuxeo.ecm.platform.preview.adapter.PreviewAdapterManager;
039import org.nuxeo.ecm.platform.preview.api.NothingToPreviewException;
040import org.nuxeo.ecm.platform.preview.api.PreviewException;
041import org.nuxeo.runtime.api.Framework;
042import org.nuxeo.runtime.services.config.ConfigurationService;
043
044/**
045 * Base class for preview based on "on the fly" HTML transformers
046 *
047 * @author tiry
048 */
049public class ConverterBasedHtmlPreviewAdapter extends AbstractHtmlPreviewAdapter {
050
051    private static final Log log = LogFactory.getLog(ConverterBasedHtmlPreviewAdapter.class);
052
053    protected String defaultFieldXPath;
054
055    protected MimetypeRegistry mimeTypeService;
056
057    /**
058     * @since 8.10
059     */
060    protected static final String ALLOW_ZIP_PREVIEW = "nuxeo.preview.zip.enabled";
061
062    public ConversionService getConversionService() {
063        return Framework.getService(ConversionService.class);
064    }
065
066    @Override
067    protected PreviewAdapterManager getPreviewManager() {
068        return Framework.getService(PreviewAdapterManager.class);
069    }
070
071    protected static String getMimeType(Blob blob) {
072        if (blob == null) {
073            return null;
074        }
075
076        String srcMT = blob.getMimeType();
077        if (srcMT == null || srcMT.startsWith("application/octet-stream")) {
078            // call MT Service
079            try {
080                MimetypeRegistry mtr = Framework.getService(MimetypeRegistry.class);
081                srcMT = mtr.getMimetypeFromFilenameAndBlobWithDefault(blob.getFilename(), blob,
082                        "application/octet-stream");
083                log.debug("mime type service returned " + srcMT);
084            } catch (MimetypeDetectionException e) {
085                log.warn("error while calling Mimetype service", e);
086            }
087        }
088        return srcMT;
089    }
090
091    protected String getMimeType(String xpath) {
092        BlobHolder blobHolder2preview = getBlobHolder2preview(xpath);
093        Blob blob = getBlob2preview(blobHolder2preview);
094        return getMimeType(blob);
095    }
096
097    protected String getDefaultPreviewFieldXPath() {
098        return defaultFieldXPath;
099    }
100
101    public void setDefaultPreviewFieldXPath(String xPath) {
102        defaultFieldXPath = xPath;
103    }
104
105    @Override
106    public List<Blob> getPreviewBlobs() throws PreviewException {
107        return getPreviewBlobs(getDefaultPreviewFieldXPath());
108    }
109
110    @Override
111    public boolean hasPreview(String xpath) {
112        String srcMT;
113        try {
114            srcMT = getMimeType(xpath);
115        } catch (NothingToPreviewException e) {
116            return false;
117        }
118        if ("application/zip".equals(srcMT)
119                && !Framework.getService(ConfigurationService.class).isBooleanPropertyTrue(ALLOW_ZIP_PREVIEW)) {
120            return false;
121        }
122        MimeTypePreviewer mtPreviewer = getPreviewManager().getPreviewer(srcMT);
123        return mtPreviewer != null || getConversionService().getConverterName(srcMT, "text/html") != null;
124    }
125
126    @Override
127    public List<Blob> getPreviewBlobs(String xpath) throws PreviewException {
128        BlobHolder blobHolder2preview = getBlobHolder2preview(xpath);
129        Blob blob2Preview;
130        try {
131            blob2Preview = getBlob2preview(blobHolder2preview);
132        } catch (NothingToPreviewException e) {
133            return Collections.emptyList();
134        }
135
136        String srcMT = getMimeType(xpath);
137        log.debug("Source type for HTML preview =" + srcMT);
138        MimeTypePreviewer mtPreviewer = getPreviewManager().getPreviewer(srcMT);
139        if (mtPreviewer != null) {
140            return mtPreviewer.getPreview(blob2Preview, adaptedDoc);
141        }
142
143        String converterName = getConversionService().getConverterName(srcMT, "text/html");
144        if (converterName == null) {
145            log.debug("No dedicated converter found, using generic");
146            converterName = "any2html";
147        }
148
149        BlobHolder result;
150        try {
151            result = getConversionService().convert(converterName, blobHolder2preview, null);
152            setMimeType(result);
153            setDigest(result);
154            return result.getBlobs();
155        } catch (ConversionException e) {
156            throw new PreviewException(e.getMessage(), e);
157        }
158    }
159
160    /**
161     * @since 5.7.3
162     */
163    private Blob getBlob2preview(BlobHolder blobHolder2preview) throws PreviewException {
164        Blob blob2Preview;
165        try {
166            blob2Preview = blobHolder2preview.getBlob();
167        } catch (PropertyNotFoundException e) {
168            blob2Preview = null;
169        }
170        if (blob2Preview == null) {
171            throw new NothingToPreviewException("Can not preview a document without blob");
172        } else {
173            return blob2Preview;
174        }
175    }
176
177    /**
178     * Returns a blob holder suitable for a preview.
179     *
180     * @since 5.7.3
181     */
182    private BlobHolder getBlobHolder2preview(String xpath) {
183        if ((xpath == null) || ("default".equals(xpath))) {
184            return adaptedDoc.getAdapter(BlobHolder.class);
185        } else {
186            return new DocumentBlobHolder(adaptedDoc, xpath);
187        }
188    }
189
190    protected void setMimeType(BlobHolder result) {
191        for (Blob blob : result.getBlobs()) {
192            if ((blob.getMimeType() == null || blob.getMimeType().startsWith("application/octet-stream"))
193                    && blob.getFilename().endsWith("html")) {
194                String mimeTpye = getMimeType(blob);
195                blob.setMimeType(mimeTpye);
196            }
197        }
198    }
199
200    protected void setDigest(BlobHolder result) {
201        for (Blob blob : result.getBlobs()) {
202            if (blob.getDigest() == null) {
203                try (InputStream stream = blob.getStream()) {
204                    String digest = DigestUtils.md5Hex(stream);
205                    blob.setDigest(digest);
206                } catch (IOException e) {
207                    log.warn("Unable to compute digest of blob.", e);
208                }
209            }
210        }
211    }
212
213    public String getMimeType(File file) throws ConversionException {
214        try {
215            return getMimeTypeService().getMimetypeFromFile(file);
216        } catch (ConversionException e) {
217            throw new ConversionException("Could not get MimeTypeRegistry");
218        } catch (MimetypeNotFoundException e) {
219            return "application/octet-stream";
220        } catch (MimetypeDetectionException e) {
221            return "application/octet-stream";
222        }
223    }
224
225    public MimetypeRegistry getMimeTypeService() throws ConversionException {
226        if (mimeTypeService == null) {
227            mimeTypeService = Framework.getService(MimetypeRegistry.class);
228        }
229        return mimeTypeService;
230    }
231
232    @Override
233    public void cleanup() {
234
235    }
236
237    @Override
238    public boolean cachable() {
239        return true;
240    }
241
242    @Override
243    public boolean hasBlobToPreview() throws PreviewException {
244        String xpath = getDefaultPreviewFieldXPath();
245        Blob blob2Preview;
246        try {
247            blob2Preview = getBlob2preview(getBlobHolder2preview(xpath));
248        } catch (NothingToPreviewException e) {
249            return false;
250        }
251        String srcMT = getMimeType(xpath);
252        if ("application/zip".equals(srcMT)
253                && !Framework.getService(ConfigurationService.class).isBooleanPropertyTrue(ALLOW_ZIP_PREVIEW)) {
254            return false;
255        }
256        return blob2Preview != null;
257    }
258
259}