org.xwiki.office.viewer.internal.DefaultOfficeResourceViewer.java Source code

Java tutorial

Introduction

Here is the source code for org.xwiki.office.viewer.internal.DefaultOfficeResourceViewer.java

Source

/*
 * See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.xwiki.office.viewer.internal;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.InputStream;
import java.net.URL;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.artofsolving.jodconverter.document.DocumentFamily;
import org.artofsolving.jodconverter.document.DocumentFormat;
import org.slf4j.Logger;
import org.xwiki.bridge.DocumentAccessBridge;
import org.xwiki.cache.Cache;
import org.xwiki.cache.CacheException;
import org.xwiki.cache.CacheManager;
import org.xwiki.cache.config.LRUCacheConfiguration;
import org.xwiki.component.annotation.Component;
import org.xwiki.component.phase.Initializable;
import org.xwiki.component.phase.InitializationException;
import org.xwiki.model.reference.AttachmentReference;
import org.xwiki.model.reference.AttachmentReferenceResolver;
import org.xwiki.model.reference.DocumentReference;
import org.xwiki.model.reference.EntityReferenceSerializer;
import org.xwiki.office.viewer.OfficeResourceViewer;
import org.xwiki.officeimporter.builder.PresentationBuilder;
import org.xwiki.officeimporter.builder.XDOMOfficeDocumentBuilder;
import org.xwiki.officeimporter.converter.OfficeConverter;
import org.xwiki.officeimporter.document.XDOMOfficeDocument;
import org.xwiki.officeimporter.server.OfficeServer;
import org.xwiki.properties.ConverterManager;
import org.xwiki.rendering.block.Block;
import org.xwiki.rendering.block.ExpandedMacroBlock;
import org.xwiki.rendering.block.ImageBlock;
import org.xwiki.rendering.block.MetaDataBlock;
import org.xwiki.rendering.block.XDOM;
import org.xwiki.rendering.block.match.ClassBlockMatcher;
import org.xwiki.rendering.listener.MetaData;
import org.xwiki.rendering.listener.reference.ResourceReference;
import org.xwiki.rendering.listener.reference.ResourceType;
import org.xwiki.rendering.renderer.reference.ResourceReferenceTypeSerializer;
import org.xwiki.rendering.syntax.Syntax;
import org.xwiki.resource.ResourceReferenceSerializer;
import org.xwiki.resource.temporary.TemporaryResourceReference;
import org.xwiki.resource.temporary.TemporaryResourceStore;
import org.xwiki.url.ExtendedURL;

/**
 * Default implementation of {@link org.xwiki.office.viewer.OfficeResourceViewer}.
 * 
 * @since 5.4.6
 * @since 6.2.2
 * @version $Id: fcef771ca9c1c12c9e13a7274ffd3e05809393f7 $
 */
@Component
@Singleton
public class DefaultOfficeResourceViewer implements OfficeResourceViewer, Initializable {
    /**
     * The module name used when creating temporary files. This is the module used by the temporary resource action to
     * retrieve the temporary file.
     */
    private static final String MODULE_NAME = "officeviewer";

    /**
     * Used to access attachment content.
     */
    @Inject
    private DocumentAccessBridge documentAccessBridge;

    @Inject
    private TemporaryResourceStore temporaryResourceStore;

    @Inject
    @Named("standard/tmp")
    private ResourceReferenceSerializer<TemporaryResourceReference, ExtendedURL> urlTemporaryResourceReferenceSerializer;

    /**
     * Used for serializing {@link AttachmentReference}s.
     */
    @Inject
    private EntityReferenceSerializer<String> serializer;

    @Inject
    private ResourceReferenceTypeSerializer resourceReferenceSerializer;

    @Inject
    @Named("current")
    private AttachmentReferenceResolver<String> attachmentResolver;

    /**
     * Used to initialize the view cache.
     */
    @Inject
    private CacheManager cacheManager;

    /**
     * Attachment based office document view cache.
     */
    private Cache<AttachmentOfficeDocumentView> attachmentCache;

    /**
     * External file based office document view cache.
     */
    private Cache<OfficeDocumentView> externalCache;

    /**
     * Used to build XDOM documents from office documents.
     */
    @Inject
    private XDOMOfficeDocumentBuilder documentBuilder;

    /**
     * Used to build XDOM documents from office presentations.
     */
    @Inject
    private PresentationBuilder presentationBuilder;

    /**
     * Used to access the document converter.
     */
    @Inject
    private OfficeServer officeServer;

    @Inject
    private ConverterManager converter;

    /**
     * The logger to log.
     */
    @Inject
    private Logger logger;

    /**
     * Processes all the image blocks in the given XDOM and changes image URL to point to a temporary file for those
     * images that are view artifacts.
     * 
     * @param xdom the XDOM whose image blocks are to be processed
     * @param artifacts specify which of the image blocks should be processed; only the image blocks that were generated
     *            during the office import process should be processed
     * @param ownerDocumentReference specifies the document that owns the office file
     * @param resourceReference a reference to the office file that is being viewed; this reference is used to compute
     *            the path to the temporary directory holding the image artifacts
     * @param parameters the build parameters. Note that currently only {@code filterStyles} is supported and if "true"
     *            it means that styles will be filtered to the maximum and the focus will be put on importing only the
     * @return the set of temporary files corresponding to image artifacts
     */
    private Set<File> processImages(XDOM xdom, Map<String, byte[]> artifacts,
            DocumentReference ownerDocumentReference, String resourceReference, Map<String, ?> parameters) {
        // Process all image blocks.
        Set<File> temporaryFiles = new HashSet<File>();
        List<ImageBlock> imgBlocks = xdom.getBlocks(new ClassBlockMatcher(ImageBlock.class), Block.Axes.DESCENDANT);
        for (ImageBlock imgBlock : imgBlocks) {
            String imageReference = imgBlock.getReference().getReference();

            // Check whether there is a corresponding artifact.
            if (artifacts.containsKey(imageReference)) {
                try {
                    List<String> resourcePath = Arrays.asList(String.valueOf(parameters.hashCode()),
                            imageReference);
                    TemporaryResourceReference temporaryResourceReference = new TemporaryResourceReference(
                            MODULE_NAME, resourcePath, ownerDocumentReference);

                    // Write the image into a temporary file.
                    File tempFile = this.temporaryResourceStore.createTemporaryFile(temporaryResourceReference,
                            new ByteArrayInputStream(artifacts.get(imageReference)));

                    // Create a URL image reference which links to above temporary image file.
                    String temporaryResourceURL = this.urlTemporaryResourceReferenceSerializer
                            .serialize(temporaryResourceReference).serialize();
                    ResourceReference urlImageReference = new ResourceReference(temporaryResourceURL,
                            ResourceType.PATH);
                    urlImageReference.setTyped(true);

                    // Replace the old image block with a new one that uses the above URL image reference.
                    Block newImgBlock = new ImageBlock(urlImageReference, false, imgBlock.getParameters());
                    imgBlock.getParent().replaceChild(Arrays.asList(newImgBlock), imgBlock);

                    // Make sure the new image block is not inside an ExpandedMacroBlock whose's content syntax doesn't
                    // support relative path resource references (we use relative paths to refer the temporary files).
                    maybeFixExpandedMacroAncestor(newImgBlock);

                    // Collect the temporary file so that it can be cleaned up when the view is disposed from cache.
                    temporaryFiles.add(tempFile);
                } catch (Exception ex) {
                    String message = "Error while processing artifact image [%s].";
                    this.logger.error(String.format(message, imageReference), ex);
                }
            }
        }

        return temporaryFiles;
    }

    private void maybeFixExpandedMacroAncestor(Block block) {
        ExpandedMacroBlock expandedMacro = block.getFirstBlock(new ClassBlockMatcher(ExpandedMacroBlock.class),
                Block.Axes.ANCESTOR_OR_SELF);
        if (expandedMacro != null) {
            Block parent = expandedMacro.getParent();
            if (!(parent instanceof MetaDataBlock)
                    || !((MetaDataBlock) parent).getMetaData().contains(MODULE_NAME)) {
                MetaDataBlock metaData = new MetaDataBlock(Collections.<Block>emptyList());
                // Use a syntax that supports relative path resource references (we use relative paths to include the
                // temporary files).
                metaData.getMetaData().addMetaData(MetaData.SYNTAX, Syntax.XWIKI_2_1);
                metaData.getMetaData().addMetaData(MODULE_NAME, true);
                parent.replaceChild(metaData, expandedMacro);
                metaData.addChild(expandedMacro);
            }
        }
    }

    /**
     * Creates a {@link XDOM} representation of the specified office attachment.
     * 
     * @param attachmentReference a reference to the office file to be parsed into XDOM
     * @param parameters the build parameters. Note that currently only {@code filterStyles} is supported and if "true"
     *            it means that styles will be filtered to the maximum and the focus will be put on importing only the
     *            content
     * @return the {@link XDOMOfficeDocument} corresponding to the specified office file
     * @throws Exception if building the XDOM fails
     */
    private XDOMOfficeDocument createXDOM(AttachmentReference attachmentReference, Map<String, ?> parameters)
            throws Exception {
        InputStream officeFileStream = this.documentAccessBridge.getAttachmentContent(attachmentReference);
        String officeFileName = attachmentReference.getName();

        return createXDOM(attachmentReference.getDocumentReference(), officeFileStream, officeFileName, parameters);
    }

    private XDOMOfficeDocument createXDOM(DocumentReference ownerDocument, ResourceReference resourceReference,
            Map<String, ?> parameters) throws Exception {
        InputStream officeFileStream;
        String officeFileName;

        if (resourceReference.getType().equals(ResourceType.URL)) {
            URL url = new URL(resourceReference.getReference());
            officeFileStream = url.openStream();
            officeFileName = StringUtils.substringAfterLast(url.getPath(), "/");
        } else {
            throw new Exception(String.format("Unsupported resource type [%s].", resourceReference.getType()));
        }

        return createXDOM(ownerDocument, officeFileStream, officeFileName, parameters);
    }

    private XDOMOfficeDocument createXDOM(DocumentReference ownerDocument, InputStream officeFileStream,
            String officeFileName, Map<String, ?> parameters) throws Exception {
        try {
            if (isPresentation(officeFileName)) {
                return this.presentationBuilder.build(officeFileStream, officeFileName, ownerDocument);
            } else {
                boolean filterStyles = this.converter.convert(boolean.class, parameters.get("filterStyles"));
                return this.documentBuilder.build(officeFileStream, officeFileName, ownerDocument, filterStyles);
            }
        } finally {
            IOUtils.closeQuietly(officeFileStream);
        }
    }

    /**
     * Utility method for checking if a file name corresponds to an office presentation.
     * 
     * @param fileName attachment file name
     * @return {@code true} if the file extension represents an office presentation format, {@code false} otherwise
     */
    private boolean isPresentation(String fileName) {
        String extension = fileName.substring(fileName.lastIndexOf('.') + 1);
        OfficeConverter officeConverter = this.officeServer.getConverter();
        if (officeConverter != null) {
            DocumentFormat format = officeConverter.getFormatRegistry().getFormatByExtension(extension);
            return format != null && format.getInputFamily() == DocumentFamily.PRESENTATION;
        }

        return false;
    }

    @Override
    public void initialize() throws InitializationException {
        try {
            LRUCacheConfiguration attachmentConfig = new LRUCacheConfiguration(MODULE_NAME + ".attachment", 50);
            this.attachmentCache = this.cacheManager.createNewCache(attachmentConfig);

            // We have no idea when to invalidate the cache so lets at least put a time to live
            LRUCacheConfiguration exteralConfig = new LRUCacheConfiguration(MODULE_NAME + ".external", 50, 3600);
            this.externalCache = this.cacheManager.createNewCache(exteralConfig);
        } catch (CacheException e) {
            throw new InitializationException("Failed to create caches.", e);
        }
    }

    private OfficeDocumentView getView(ResourceReference reference, AttachmentReference attachmentReference,
            Map<String, ?> parameters) throws Exception {
        // Search the cache.
        String cacheKey = getCacheKey(attachmentReference.getDocumentReference(), attachmentReference.getName(),
                parameters);
        AttachmentOfficeDocumentView view = this.attachmentCache.get(cacheKey);

        // It's possible that the attachment has been deleted. We need to catch such events and cleanup the cache.
        DocumentReference documentReference = attachmentReference.getDocumentReference();
        if (!this.documentAccessBridge.getAttachmentReferences(documentReference).contains(attachmentReference)) {
            // If a cached view exists, flush it.
            if (view != null) {
                this.attachmentCache.remove(cacheKey);
            }
            throw new Exception(String.format("Attachment [%s] does not exist.", attachmentReference));
        }

        // Check if the view has expired.
        String currentVersion = this.documentAccessBridge.getAttachmentVersion(attachmentReference);
        if (view != null && !currentVersion.equals(view.getVersion())) {
            // Flush the cached view.
            this.attachmentCache.remove(cacheKey);
            view = null;
        }

        // If a view in not available, build one and cache it.
        if (view == null) {
            XDOMOfficeDocument xdomOfficeDocument = createXDOM(attachmentReference, parameters);
            String attachmentVersion = this.documentAccessBridge.getAttachmentVersion(attachmentReference);
            XDOM xdom = xdomOfficeDocument.getContentDocument();
            // We use only the file name from the resource reference because the rest of the information is specified by
            // the owner document reference. This way we ensure the path to the temporary files doesn't contain
            // redundant information and so it remains as small as possible (considering that the path length is limited
            // on some environments).
            Set<File> temporaryFiles = processImages(xdom, xdomOfficeDocument.getArtifacts(),
                    attachmentReference.getDocumentReference(), attachmentReference.getName(), parameters);
            view = new AttachmentOfficeDocumentView(reference, attachmentReference, attachmentVersion, xdom,
                    temporaryFiles);

            this.attachmentCache.set(cacheKey, view);
        }

        // We have to clone the cached XDOM to protect it from the rendering transformations. For instance, macro
        // transformations must be executed even when the XDOM is taken from the cache.
        return view;
    }

    private OfficeDocumentView getView(ResourceReference resourceReference, Map<String, ?> parameters)
            throws Exception {
        DocumentReference ownerDocument = getOwnerDocument(parameters);
        String serializedResourceReference = this.resourceReferenceSerializer.serialize(resourceReference);

        // Search the cache.
        String cacheKey = getCacheKey(ownerDocument, serializedResourceReference, parameters);
        OfficeDocumentView view = this.externalCache.get(cacheKey);

        // If a view in not available, build one and cache it.
        if (view == null) {
            XDOMOfficeDocument xdomOfficeDocument = createXDOM(ownerDocument, resourceReference, parameters);
            XDOM xdom = xdomOfficeDocument.getContentDocument();
            Set<File> temporaryFiles = processImages(xdom, xdomOfficeDocument.getArtifacts(), ownerDocument,
                    serializedResourceReference, parameters);
            view = new OfficeDocumentView(resourceReference, xdom, temporaryFiles);

            this.externalCache.set(cacheKey, view);
        }

        return view;
    }

    @Override
    public XDOM createView(ResourceReference reference, Map<String, ?> parameters) throws Exception {
        OfficeDocumentView view;

        if (reference.getType().equals(ResourceType.ATTACHMENT)
                || reference.getType().equals(ResourceType.UNKNOWN)) {
            AttachmentReference attachmentReference = this.attachmentResolver.resolve(reference.getReference());

            view = getView(reference, attachmentReference, parameters);
        } else {
            view = getView(reference, parameters);
        }

        // We have to clone the cached XDOM to protect it from the rendering transformations. For instance, macro
        // transformations must be executed even when the XDOM is taken from the cache.
        return view.getXDOM().clone();
    }

    private String getCacheKey(DocumentReference ownerDocument, String resource, Map<String, ?> parameters) {
        return this.serializer.serialize(ownerDocument) + '/' + resource + '/' + parameters.hashCode();
    }

    private DocumentReference getOwnerDocument(Map<String, ?> parameters) {
        DocumentReference ownerDocument = this.converter.convert(DocumentReference.class,
                parameters.get("ownerDocument"));
        if (ownerDocument == null) {
            this.documentAccessBridge.getCurrentDocumentReference();
        }

        return ownerDocument;
    }
}