1. Project Clover database Tue Dec 20 2016 21:24:09 CET
  2. Package org.xwiki.office.viewer.internal

File DefaultOfficeResourceViewer.java

 

Coverage histogram

../../../../../img/srcFileCovDistChart8.png
54% of files have more coverage

Code metrics

26
98
12
1
430
261
30
0.31
8.17
12
2.5

Classes

Class Line # Actions
DefaultOfficeResourceViewer 86 98 0% 30 29
0.786764778.7%
 

Contributing tests

This file is covered by 6 tests. .

Source view

1    /*
2    * See the NOTICE file distributed with this work for additional
3    * information regarding copyright ownership.
4    *
5    * This is free software; you can redistribute it and/or modify it
6    * under the terms of the GNU Lesser General Public License as
7    * published by the Free Software Foundation; either version 2.1 of
8    * the License, or (at your option) any later version.
9    *
10    * This software is distributed in the hope that it will be useful,
11    * but WITHOUT ANY WARRANTY; without even the implied warranty of
12    * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13    * Lesser General Public License for more details.
14    *
15    * You should have received a copy of the GNU Lesser General Public
16    * License along with this software; if not, write to the Free
17    * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
18    * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
19    */
20    package org.xwiki.office.viewer.internal;
21   
22    import java.io.ByteArrayInputStream;
23    import java.io.File;
24    import java.io.InputStream;
25    import java.net.URL;
26    import java.util.Arrays;
27    import java.util.Collections;
28    import java.util.HashSet;
29    import java.util.List;
30    import java.util.Map;
31    import java.util.Set;
32   
33    import javax.inject.Inject;
34    import javax.inject.Named;
35    import javax.inject.Singleton;
36   
37    import org.apache.commons.io.IOUtils;
38    import org.apache.commons.lang3.StringUtils;
39    import org.artofsolving.jodconverter.document.DocumentFamily;
40    import org.artofsolving.jodconverter.document.DocumentFormat;
41    import org.slf4j.Logger;
42    import org.xwiki.bridge.DocumentAccessBridge;
43    import org.xwiki.cache.Cache;
44    import org.xwiki.cache.CacheException;
45    import org.xwiki.cache.CacheManager;
46    import org.xwiki.cache.config.LRUCacheConfiguration;
47    import org.xwiki.component.annotation.Component;
48    import org.xwiki.component.phase.Initializable;
49    import org.xwiki.component.phase.InitializationException;
50    import org.xwiki.model.reference.AttachmentReference;
51    import org.xwiki.model.reference.AttachmentReferenceResolver;
52    import org.xwiki.model.reference.DocumentReference;
53    import org.xwiki.model.reference.EntityReferenceSerializer;
54    import org.xwiki.office.viewer.OfficeResourceViewer;
55    import org.xwiki.officeimporter.builder.PresentationBuilder;
56    import org.xwiki.officeimporter.builder.XDOMOfficeDocumentBuilder;
57    import org.xwiki.officeimporter.converter.OfficeConverter;
58    import org.xwiki.officeimporter.document.XDOMOfficeDocument;
59    import org.xwiki.officeimporter.server.OfficeServer;
60    import org.xwiki.properties.ConverterManager;
61    import org.xwiki.rendering.block.Block;
62    import org.xwiki.rendering.block.ExpandedMacroBlock;
63    import org.xwiki.rendering.block.ImageBlock;
64    import org.xwiki.rendering.block.MetaDataBlock;
65    import org.xwiki.rendering.block.XDOM;
66    import org.xwiki.rendering.block.match.ClassBlockMatcher;
67    import org.xwiki.rendering.listener.MetaData;
68    import org.xwiki.rendering.listener.reference.ResourceReference;
69    import org.xwiki.rendering.listener.reference.ResourceType;
70    import org.xwiki.rendering.renderer.reference.ResourceReferenceTypeSerializer;
71    import org.xwiki.rendering.syntax.Syntax;
72    import org.xwiki.resource.ResourceReferenceSerializer;
73    import org.xwiki.resource.temporary.TemporaryResourceReference;
74    import org.xwiki.resource.temporary.TemporaryResourceStore;
75    import org.xwiki.url.ExtendedURL;
76   
77    /**
78    * Default implementation of {@link org.xwiki.office.viewer.OfficeResourceViewer}.
79    *
80    * @since 5.4.6
81    * @since 6.2.2
82    * @version $Id: fcef771ca9c1c12c9e13a7274ffd3e05809393f7 $
83    */
84    @Component
85    @Singleton
 
86    public class DefaultOfficeResourceViewer implements OfficeResourceViewer, Initializable
87    {
88    /**
89    * The module name used when creating temporary files. This is the module used by the temporary resource action to
90    * retrieve the temporary file.
91    */
92    private static final String MODULE_NAME = "officeviewer";
93   
94    /**
95    * Used to access attachment content.
96    */
97    @Inject
98    private DocumentAccessBridge documentAccessBridge;
99   
100    @Inject
101    private TemporaryResourceStore temporaryResourceStore;
102   
103    @Inject
104    @Named("standard/tmp")
105    private ResourceReferenceSerializer<TemporaryResourceReference, ExtendedURL> urlTemporaryResourceReferenceSerializer;
106   
107    /**
108    * Used for serializing {@link AttachmentReference}s.
109    */
110    @Inject
111    private EntityReferenceSerializer<String> serializer;
112   
113    @Inject
114    private ResourceReferenceTypeSerializer resourceReferenceSerializer;
115   
116    @Inject
117    @Named("current")
118    private AttachmentReferenceResolver<String> attachmentResolver;
119   
120    /**
121    * Used to initialize the view cache.
122    */
123    @Inject
124    private CacheManager cacheManager;
125   
126    /**
127    * Attachment based office document view cache.
128    */
129    private Cache<AttachmentOfficeDocumentView> attachmentCache;
130   
131    /**
132    * External file based office document view cache.
133    */
134    private Cache<OfficeDocumentView> externalCache;
135   
136    /**
137    * Used to build XDOM documents from office documents.
138    */
139    @Inject
140    private XDOMOfficeDocumentBuilder documentBuilder;
141   
142    /**
143    * Used to build XDOM documents from office presentations.
144    */
145    @Inject
146    private PresentationBuilder presentationBuilder;
147   
148    /**
149    * Used to access the document converter.
150    */
151    @Inject
152    private OfficeServer officeServer;
153   
154    @Inject
155    private ConverterManager converter;
156   
157    /**
158    * The logger to log.
159    */
160    @Inject
161    private Logger logger;
162   
163    /**
164    * Processes all the image blocks in the given XDOM and changes image URL to point to a temporary file for those
165    * images that are view artifacts.
166    *
167    * @param xdom the XDOM whose image blocks are to be processed
168    * @param artifacts specify which of the image blocks should be processed; only the image blocks that were generated
169    * during the office import process should be processed
170    * @param ownerDocumentReference specifies the document that owns the office file
171    * @param resourceReference a reference to the office file that is being viewed; this reference is used to compute
172    * the path to the temporary directory holding the image artifacts
173    * @param parameters the build parameters. Note that currently only {@code filterStyles} is supported and if "true"
174    * it means that styles will be filtered to the maximum and the focus will be put on importing only the
175    * @return the set of temporary files corresponding to image artifacts
176    */
 
177  3 toggle private Set<File> processImages(XDOM xdom, Map<String, byte[]> artifacts, DocumentReference ownerDocumentReference,
178    String resourceReference, Map<String, ?> parameters)
179    {
180    // Process all image blocks.
181  3 Set<File> temporaryFiles = new HashSet<File>();
182  3 List<ImageBlock> imgBlocks = xdom.getBlocks(new ClassBlockMatcher(ImageBlock.class), Block.Axes.DESCENDANT);
183  3 for (ImageBlock imgBlock : imgBlocks) {
184  1 String imageReference = imgBlock.getReference().getReference();
185   
186    // Check whether there is a corresponding artifact.
187  1 if (artifacts.containsKey(imageReference)) {
188  1 try {
189  1 List<String> resourcePath = Arrays.asList(String.valueOf(parameters.hashCode()), imageReference);
190  1 TemporaryResourceReference temporaryResourceReference =
191    new TemporaryResourceReference(MODULE_NAME, resourcePath, ownerDocumentReference);
192   
193    // Write the image into a temporary file.
194  1 File tempFile = this.temporaryResourceStore.createTemporaryFile(temporaryResourceReference,
195    new ByteArrayInputStream(artifacts.get(imageReference)));
196   
197    // Create a URL image reference which links to above temporary image file.
198  1 String temporaryResourceURL =
199    this.urlTemporaryResourceReferenceSerializer.serialize(temporaryResourceReference).serialize();
200  1 ResourceReference urlImageReference =
201    new ResourceReference(temporaryResourceURL, ResourceType.PATH);
202  1 urlImageReference.setTyped(true);
203   
204    // Replace the old image block with a new one that uses the above URL image reference.
205  1 Block newImgBlock = new ImageBlock(urlImageReference, false, imgBlock.getParameters());
206  1 imgBlock.getParent().replaceChild(Arrays.asList(newImgBlock), imgBlock);
207   
208    // Make sure the new image block is not inside an ExpandedMacroBlock whose's content syntax doesn't
209    // support relative path resource references (we use relative paths to refer the temporary files).
210  1 maybeFixExpandedMacroAncestor(newImgBlock);
211   
212    // Collect the temporary file so that it can be cleaned up when the view is disposed from cache.
213  1 temporaryFiles.add(tempFile);
214    } catch (Exception ex) {
215  0 String message = "Error while processing artifact image [%s].";
216  0 this.logger.error(String.format(message, imageReference), ex);
217    }
218    }
219    }
220   
221  3 return temporaryFiles;
222    }
223   
 
224  1 toggle private void maybeFixExpandedMacroAncestor(Block block)
225    {
226  1 ExpandedMacroBlock expandedMacro =
227    block.getFirstBlock(new ClassBlockMatcher(ExpandedMacroBlock.class), Block.Axes.ANCESTOR_OR_SELF);
228  1 if (expandedMacro != null) {
229  1 Block parent = expandedMacro.getParent();
230  1 if (!(parent instanceof MetaDataBlock) || !((MetaDataBlock) parent).getMetaData().contains(MODULE_NAME)) {
231  1 MetaDataBlock metaData = new MetaDataBlock(Collections.<Block>emptyList());
232    // Use a syntax that supports relative path resource references (we use relative paths to include the
233    // temporary files).
234  1 metaData.getMetaData().addMetaData(MetaData.SYNTAX, Syntax.XWIKI_2_1);
235  1 metaData.getMetaData().addMetaData(MODULE_NAME, true);
236  1 parent.replaceChild(metaData, expandedMacro);
237  1 metaData.addChild(expandedMacro);
238    }
239    }
240    }
241   
242    /**
243    * Creates a {@link XDOM} representation of the specified office attachment.
244    *
245    * @param attachmentReference a reference to the office file to be parsed into XDOM
246    * @param parameters the build parameters. Note that currently only {@code filterStyles} is supported and if "true"
247    * it means that styles will be filtered to the maximum and the focus will be put on importing only the
248    * content
249    * @return the {@link XDOMOfficeDocument} corresponding to the specified office file
250    * @throws Exception if building the XDOM fails
251    */
 
252  3 toggle private XDOMOfficeDocument createXDOM(AttachmentReference attachmentReference, Map<String, ?> parameters)
253    throws Exception
254    {
255  3 InputStream officeFileStream = this.documentAccessBridge.getAttachmentContent(attachmentReference);
256  3 String officeFileName = attachmentReference.getName();
257   
258  3 return createXDOM(attachmentReference.getDocumentReference(), officeFileStream, officeFileName, parameters);
259    }
260   
 
261  0 toggle private XDOMOfficeDocument createXDOM(DocumentReference ownerDocument, ResourceReference resourceReference,
262    Map<String, ?> parameters) throws Exception
263    {
264  0 InputStream officeFileStream;
265  0 String officeFileName;
266   
267  0 if (resourceReference.getType().equals(ResourceType.URL)) {
268  0 URL url = new URL(resourceReference.getReference());
269  0 officeFileStream = url.openStream();
270  0 officeFileName = StringUtils.substringAfterLast(url.getPath(), "/");
271    } else {
272  0 throw new Exception(String.format("Unsupported resource type [%s].", resourceReference.getType()));
273    }
274   
275  0 return createXDOM(ownerDocument, officeFileStream, officeFileName, parameters);
276    }
277   
 
278  3 toggle private XDOMOfficeDocument createXDOM(DocumentReference ownerDocument, InputStream officeFileStream,
279    String officeFileName, Map<String, ?> parameters) throws Exception
280    {
281  3 try {
282  3 if (isPresentation(officeFileName)) {
283  1 return this.presentationBuilder.build(officeFileStream, officeFileName, ownerDocument);
284    } else {
285  2 boolean filterStyles = this.converter.convert(boolean.class, parameters.get("filterStyles"));
286  2 return this.documentBuilder.build(officeFileStream, officeFileName, ownerDocument, filterStyles);
287    }
288    } finally {
289  3 IOUtils.closeQuietly(officeFileStream);
290    }
291    }
292   
293    /**
294    * Utility method for checking if a file name corresponds to an office presentation.
295    *
296    * @param fileName attachment file name
297    * @return {@code true} if the file extension represents an office presentation format, {@code false} otherwise
298    */
 
299  3 toggle private boolean isPresentation(String fileName)
300    {
301  3 String extension = fileName.substring(fileName.lastIndexOf('.') + 1);
302  3 OfficeConverter officeConverter = this.officeServer.getConverter();
303  3 if (officeConverter != null) {
304  3 DocumentFormat format = officeConverter.getFormatRegistry().getFormatByExtension(extension);
305  3 return format != null && format.getInputFamily() == DocumentFamily.PRESENTATION;
306    }
307   
308  0 return false;
309    }
310   
 
311  6 toggle @Override
312    public void initialize() throws InitializationException
313    {
314  6 try {
315  6 LRUCacheConfiguration attachmentConfig = new LRUCacheConfiguration(MODULE_NAME + ".attachment", 50);
316  6 this.attachmentCache = this.cacheManager.createNewCache(attachmentConfig);
317   
318    // We have no idea when to invalidate the cache so lets at least put a time to live
319  6 LRUCacheConfiguration exteralConfig = new LRUCacheConfiguration(MODULE_NAME + ".external", 50, 3600);
320  6 this.externalCache = this.cacheManager.createNewCache(exteralConfig);
321    } catch (CacheException e) {
322  0 throw new InitializationException("Failed to create caches.", e);
323    }
324    }
325   
 
326  5 toggle private OfficeDocumentView getView(ResourceReference reference, AttachmentReference attachmentReference,
327    Map<String, ?> parameters) throws Exception
328    {
329    // Search the cache.
330  5 String cacheKey =
331    getCacheKey(attachmentReference.getDocumentReference(), attachmentReference.getName(), parameters);
332  5 AttachmentOfficeDocumentView view = this.attachmentCache.get(cacheKey);
333   
334    // It's possible that the attachment has been deleted. We need to catch such events and cleanup the cache.
335  5 DocumentReference documentReference = attachmentReference.getDocumentReference();
336  5 if (!this.documentAccessBridge.getAttachmentReferences(documentReference).contains(attachmentReference)) {
337    // If a cached view exists, flush it.
338  1 if (view != null) {
339  0 this.attachmentCache.remove(cacheKey);
340    }
341  1 throw new Exception(String.format("Attachment [%s] does not exist.", attachmentReference));
342    }
343   
344    // Check if the view has expired.
345  4 String currentVersion = this.documentAccessBridge.getAttachmentVersion(attachmentReference);
346  4 if (view != null && !currentVersion.equals(view.getVersion())) {
347    // Flush the cached view.
348  1 this.attachmentCache.remove(cacheKey);
349  1 view = null;
350    }
351   
352    // If a view in not available, build one and cache it.
353  4 if (view == null) {
354  3 XDOMOfficeDocument xdomOfficeDocument = createXDOM(attachmentReference, parameters);
355  3 String attachmentVersion = this.documentAccessBridge.getAttachmentVersion(attachmentReference);
356  3 XDOM xdom = xdomOfficeDocument.getContentDocument();
357    // We use only the file name from the resource reference because the rest of the information is specified by
358    // the owner document reference. This way we ensure the path to the temporary files doesn't contain
359    // redundant information and so it remains as small as possible (considering that the path length is limited
360    // on some environments).
361  3 Set<File> temporaryFiles = processImages(xdom, xdomOfficeDocument.getArtifacts(),
362    attachmentReference.getDocumentReference(), attachmentReference.getName(), parameters);
363  3 view = new AttachmentOfficeDocumentView(reference, attachmentReference, attachmentVersion, xdom,
364    temporaryFiles);
365   
366  3 this.attachmentCache.set(cacheKey, view);
367    }
368   
369    // We have to clone the cached XDOM to protect it from the rendering transformations. For instance, macro
370    // transformations must be executed even when the XDOM is taken from the cache.
371  4 return view;
372    }
373   
 
374  1 toggle private OfficeDocumentView getView(ResourceReference resourceReference, Map<String, ?> parameters) throws Exception
375    {
376  1 DocumentReference ownerDocument = getOwnerDocument(parameters);
377  1 String serializedResourceReference = this.resourceReferenceSerializer.serialize(resourceReference);
378   
379    // Search the cache.
380  1 String cacheKey = getCacheKey(ownerDocument, serializedResourceReference, parameters);
381  1 OfficeDocumentView view = this.externalCache.get(cacheKey);
382   
383    // If a view in not available, build one and cache it.
384  1 if (view == null) {
385  0 XDOMOfficeDocument xdomOfficeDocument = createXDOM(ownerDocument, resourceReference, parameters);
386  0 XDOM xdom = xdomOfficeDocument.getContentDocument();
387  0 Set<File> temporaryFiles = processImages(xdom, xdomOfficeDocument.getArtifacts(), ownerDocument,
388    serializedResourceReference, parameters);
389  0 view = new OfficeDocumentView(resourceReference, xdom, temporaryFiles);
390   
391  0 this.externalCache.set(cacheKey, view);
392    }
393   
394  1 return view;
395    }
396   
 
397  6 toggle @Override
398    public XDOM createView(ResourceReference reference, Map<String, ?> parameters) throws Exception
399    {
400  6 OfficeDocumentView view;
401   
402  6 if (reference.getType().equals(ResourceType.ATTACHMENT) || reference.getType().equals(ResourceType.UNKNOWN)) {
403  5 AttachmentReference attachmentReference = this.attachmentResolver.resolve(reference.getReference());
404   
405  5 view = getView(reference, attachmentReference, parameters);
406    } else {
407  1 view = getView(reference, parameters);
408    }
409   
410    // We have to clone the cached XDOM to protect it from the rendering transformations. For instance, macro
411    // transformations must be executed even when the XDOM is taken from the cache.
412  5 return view.getXDOM().clone();
413    }
414   
 
415  6 toggle private String getCacheKey(DocumentReference ownerDocument, String resource, Map<String, ?> parameters)
416    {
417  6 return this.serializer.serialize(ownerDocument) + '/' + resource + '/' + parameters.hashCode();
418    }
419   
 
420  1 toggle private DocumentReference getOwnerDocument(Map<String, ?> parameters)
421    {
422  1 DocumentReference ownerDocument =
423    this.converter.convert(DocumentReference.class, parameters.get("ownerDocument"));
424  1 if (ownerDocument == null) {
425  0 this.documentAccessBridge.getCurrentDocumentReference();
426    }
427   
428  1 return ownerDocument;
429    }
430    }