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.logging.log4j.LogManager;
027import org.apache.logging.log4j.Logger;
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.ImagePreviewer;
038import org.nuxeo.ecm.platform.preview.adapter.MarkdownPreviewer;
039import org.nuxeo.ecm.platform.preview.adapter.MimeTypePreviewer;
040import org.nuxeo.ecm.platform.preview.adapter.OfficePreviewer;
041import org.nuxeo.ecm.platform.preview.adapter.PdfPreviewer;
042import org.nuxeo.ecm.platform.preview.adapter.PlainImagePreviewer;
043import org.nuxeo.ecm.platform.preview.adapter.PreviewAdapterManager;
044import org.nuxeo.ecm.platform.preview.api.NothingToPreviewException;
045import org.nuxeo.ecm.platform.preview.api.PreviewException;
046import org.nuxeo.runtime.api.Framework;
047import org.nuxeo.runtime.services.config.ConfigurationService;
048
049/**
050 * Base class for preview based on "on the fly" HTML transformers
051 *
052 * @author tiry
053 */
054public class ConverterBasedHtmlPreviewAdapter extends AbstractHtmlPreviewAdapter {
055
056    private static final Logger log = LogManager.getLogger(ConverterBasedHtmlPreviewAdapter.class);
057
058    /**
059     * @since 10.3
060     * @deprecated since 10.3
061     */
062    public static final String OLD_PREVIEW_PROPERTY = "nuxeo.old.jsf.preview";
063
064    /**
065     * @since 10.3
066     * @deprecated since 10.3
067     */
068    public static final String TEXT_ANNOTATIONS_PROPERTY = "nuxeo.text.annotations";
069
070    protected String defaultFieldXPath;
071
072    protected MimetypeRegistry mimeTypeService;
073
074    /**
075     * @since 8.10
076     */
077    protected static final String ALLOW_ZIP_PREVIEW = "nuxeo.preview.zip.enabled";
078
079    public ConversionService getConversionService() {
080        return Framework.getService(ConversionService.class);
081    }
082
083    @Override
084    protected PreviewAdapterManager getPreviewManager() {
085        return Framework.getService(PreviewAdapterManager.class);
086    }
087
088    protected static String getMimeType(Blob blob) {
089        if (blob == null) {
090            return null;
091        }
092
093        String srcMT = blob.getMimeType();
094        if (srcMT == null || srcMT.startsWith("application/octet-stream")) {
095            // call MT Service
096            try {
097                MimetypeRegistry mtr = Framework.getService(MimetypeRegistry.class);
098                srcMT = mtr.getMimetypeFromFilenameAndBlobWithDefault(blob.getFilename(), blob,
099                        "application/octet-stream");
100                log.debug("mime type service returned " + srcMT);
101            } catch (MimetypeDetectionException e) {
102                log.warn("error while calling Mimetype service", e);
103            }
104        }
105        return srcMT;
106    }
107
108    protected String getMimeType(String xpath) {
109        BlobHolder blobHolder2preview = getBlobHolder2preview(xpath);
110        Blob blob = getBlob2preview(blobHolder2preview);
111        return getMimeType(blob);
112    }
113
114    protected String getDefaultPreviewFieldXPath() {
115        return defaultFieldXPath;
116    }
117
118    public void setDefaultPreviewFieldXPath(String xPath) {
119        defaultFieldXPath = xPath;
120    }
121
122    @Override
123    public List<Blob> getPreviewBlobs() throws PreviewException {
124        return getPreviewBlobs(getDefaultPreviewFieldXPath());
125    }
126
127    @Override
128    public boolean hasPreview(String xpath) {
129        String srcMT;
130        try {
131            srcMT = getMimeType(xpath);
132        } catch (NothingToPreviewException e) {
133            return false;
134        }
135        if ("application/zip".equals(srcMT)
136                && !Framework.getService(ConfigurationService.class).isBooleanPropertyTrue(ALLOW_ZIP_PREVIEW)) {
137            return false;
138        }
139        MimeTypePreviewer mtPreviewer = getPreviewManager().getPreviewer(srcMT);
140        return mtPreviewer != null || getConversionService().getConverterName(srcMT, "text/html") != null;
141    }
142
143    @Override
144    public List<Blob> getPreviewBlobs(String xpath) throws PreviewException {
145        BlobHolder blobHolder2preview = getBlobHolder2preview(xpath);
146        Blob blob2Preview;
147        try {
148            blob2Preview = getBlob2preview(blobHolder2preview);
149        } catch (NothingToPreviewException e) {
150            return Collections.emptyList();
151        }
152
153        String srcMT = getMimeType(xpath);
154        log.debug("Source type for HTML preview =" + srcMT);
155        MimeTypePreviewer mtPreviewer = getPreviewManager().getPreviewer(srcMT);
156        if (mtPreviewer != null) {
157            List<Blob> result = getPreviewFromMimeTypePreviewer(mtPreviewer, blob2Preview);
158            if (result != null) {
159                return result;
160            }
161        }
162
163        String converterName = getConversionService().getConverterName(srcMT, "text/html");
164        if (converterName == null) {
165            log.debug("No dedicated converter found, using generic");
166            converterName = "any2html";
167        }
168
169        BlobHolder result;
170        try {
171            result = getConversionService().convert(converterName, blobHolder2preview, null);
172            setMimeType(result);
173            setDigest(result);
174            return result.getBlobs();
175        } catch (ConversionException e) {
176            throw new PreviewException(e.getMessage(), e);
177        }
178    }
179
180    /**
181     * Backward compatibility method to trigger the right previewers if 'nuxeo.old.jsf.preview' is set.
182     * <p>
183     * This allows old HTML preview to be used, to make annotations available.
184     * <p>
185     * To be removed with JSF UI.
186     *
187     * @since 10.3
188     * @deprecated since 10.3
189     */
190    protected List<Blob> getPreviewFromMimeTypePreviewer(MimeTypePreviewer mtPreviewer, Blob blob2Preview) {
191        // this context data comes from the PreviewRestlet
192        boolean oldPreview = Boolean.TRUE.equals(adaptedDoc.getContextData(OLD_PREVIEW_PROPERTY));
193        if (!oldPreview) {
194            return mtPreviewer.getPreview(blob2Preview, adaptedDoc);
195        }
196
197        // when old preview is enabled
198        // - replace ImagePreviewer with PlainImagePreviewer
199        // - do nothing for "office" previewers if the text annotations are enabled to trigger the old preview behavior,
200        // otherwise keep the current preview behavior
201        if (mtPreviewer instanceof ImagePreviewer) {
202            return new PlainImagePreviewer().getPreview(blob2Preview, adaptedDoc);
203        }
204
205        ConfigurationService cs = Framework.getService(ConfigurationService.class);
206        if (cs.isBooleanPropertyTrue(TEXT_ANNOTATIONS_PROPERTY) && (mtPreviewer instanceof PdfPreviewer
207                || mtPreviewer instanceof MarkdownPreviewer || mtPreviewer instanceof OfficePreviewer)) {
208            return null;
209        }
210        return mtPreviewer.getPreview(blob2Preview, adaptedDoc);
211    }
212
213    /**
214     * @since 5.7.3
215     */
216    private Blob getBlob2preview(BlobHolder blobHolder2preview) throws PreviewException {
217        Blob blob2Preview;
218        try {
219            blob2Preview = blobHolder2preview.getBlob();
220        } catch (PropertyNotFoundException e) {
221            blob2Preview = null;
222        }
223        if (blob2Preview == null) {
224            throw new NothingToPreviewException("Can not preview a document without blob");
225        } else {
226            return blob2Preview;
227        }
228    }
229
230    /**
231     * Returns a blob holder suitable for a preview.
232     *
233     * @since 5.7.3
234     */
235    private BlobHolder getBlobHolder2preview(String xpath) {
236        if ((xpath == null) || ("default".equals(xpath))) {
237            return adaptedDoc.getAdapter(BlobHolder.class);
238        } else {
239            return new DocumentBlobHolder(adaptedDoc, xpath);
240        }
241    }
242
243    protected void setMimeType(BlobHolder result) {
244        for (Blob blob : result.getBlobs()) {
245            if ((blob.getMimeType() == null || blob.getMimeType().startsWith("application/octet-stream"))
246                    && blob.getFilename().endsWith("html")) {
247                String mimeTpye = getMimeType(blob);
248                blob.setMimeType(mimeTpye);
249            }
250        }
251    }
252
253    protected void setDigest(BlobHolder result) {
254        for (Blob blob : result.getBlobs()) {
255            if (blob.getDigest() == null) {
256                try (InputStream stream = blob.getStream()) {
257                    String digest = DigestUtils.md5Hex(stream);
258                    blob.setDigest(digest);
259                } catch (IOException e) {
260                    log.warn("Unable to compute digest of blob.", e);
261                }
262            }
263        }
264    }
265
266    public String getMimeType(File file) throws ConversionException {
267        try {
268            return getMimeTypeService().getMimetypeFromFile(file);
269        } catch (ConversionException e) {
270            throw new ConversionException("Could not get MimeTypeRegistry");
271        } catch (MimetypeNotFoundException | MimetypeDetectionException e) {
272            return "application/octet-stream";
273        }
274    }
275
276    public MimetypeRegistry getMimeTypeService() throws ConversionException {
277        if (mimeTypeService == null) {
278            mimeTypeService = Framework.getService(MimetypeRegistry.class);
279        }
280        return mimeTypeService;
281    }
282
283    @Override
284    public void cleanup() {
285
286    }
287
288    @Override
289    public boolean cachable() {
290        return true;
291    }
292
293    @Override
294    public boolean hasBlobToPreview() throws PreviewException {
295        String xpath = getDefaultPreviewFieldXPath();
296        Blob blob2Preview;
297        try {
298            blob2Preview = getBlob2preview(getBlobHolder2preview(xpath));
299        } catch (NothingToPreviewException e) {
300            return false;
301        }
302        String srcMT = getMimeType(xpath);
303        if ("application/zip".equals(srcMT)
304                && !Framework.getService(ConfigurationService.class).isBooleanPropertyTrue(ALLOW_ZIP_PREVIEW)) {
305            return false;
306        }
307        return blob2Preview != null;
308    }
309
310}