1. Project Clover database Tue Dec 20 2016 21:24:09 CET
  2. Package com.xpn.xwiki.doc

File XWikiAttachment.java

 

Coverage histogram

../../../../img/srcFileCovDistChart9.png
38% of files have more coverage

Code metrics

134
330
74
1
1,140
750
165
0.5
4.46
74
2.23

Classes

Class Line # Actions
XWikiAttachment 71 330 0% 165 76
0.8587360485.9%
 

Contributing tests

This file is covered by 93 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 com.xpn.xwiki.doc;
21   
22    import java.io.BufferedInputStream;
23    import java.io.ByteArrayOutputStream;
24    import java.io.IOException;
25    import java.io.InputStream;
26    import java.io.StringReader;
27    import java.util.ArrayList;
28    import java.util.Date;
29    import java.util.List;
30    import java.util.Objects;
31   
32    import javax.inject.Provider;
33   
34    import org.apache.commons.codec.binary.Base64InputStream;
35    import org.apache.commons.io.IOUtils;
36    import org.apache.commons.io.input.ReaderInputStream;
37    import org.apache.commons.lang3.ObjectUtils;
38    import org.apache.commons.lang3.StringUtils;
39    import org.apache.commons.lang3.exception.ExceptionUtils;
40    import org.apache.tika.Tika;
41    import org.apache.tika.mime.MediaType;
42    import org.dom4j.Document;
43    import org.dom4j.DocumentException;
44    import org.dom4j.Element;
45    import org.dom4j.dom.DOMDocument;
46    import org.dom4j.dom.DOMElement;
47    import org.dom4j.io.OutputFormat;
48    import org.dom4j.io.SAXReader;
49    import org.slf4j.Logger;
50    import org.slf4j.LoggerFactory;
51    import org.suigeneris.jrcs.rcs.Archive;
52    import org.suigeneris.jrcs.rcs.Version;
53    import org.xwiki.model.EntityType;
54    import org.xwiki.model.reference.AttachmentReference;
55    import org.xwiki.model.reference.AttachmentReferenceResolver;
56    import org.xwiki.model.reference.DocumentReference;
57    import org.xwiki.model.reference.DocumentReferenceResolver;
58    import org.xwiki.model.reference.EntityReference;
59    import org.xwiki.model.reference.EntityReferenceResolver;
60    import org.xwiki.model.reference.EntityReferenceSerializer;
61   
62    import com.xpn.xwiki.XWikiContext;
63    import com.xpn.xwiki.XWikiException;
64    import com.xpn.xwiki.doc.merge.MergeConfiguration;
65    import com.xpn.xwiki.doc.merge.MergeResult;
66    import com.xpn.xwiki.internal.xml.DOMXMLWriter;
67    import com.xpn.xwiki.internal.xml.XMLWriter;
68    import com.xpn.xwiki.user.api.XWikiRightService;
69    import com.xpn.xwiki.web.Utils;
70   
 
71    public class XWikiAttachment implements Cloneable
72    {
73    private static final Logger LOGGER = LoggerFactory.getLogger(XWikiAttachment.class);
74   
75    /**
76    * {@link Tika} is thread safe and the configuration initialization is quite expensive so we keep it (and it's
77    * blocking all others detections when initializing despite the fact that they will still all redo the complete
78    * initialization).
79    */
80    // TODO: move that in a singleton component to be shared between everything that needs it
81    private static final Tika TIKA = new Tika();
82   
83    /**
84    * Used to convert a Document Reference to string (compact form without the wiki part if it matches the current
85    * wiki).
86    */
 
87  429 toggle private static EntityReferenceSerializer<String> getCompactWikiEntityReferenceSerializer()
88    {
89  429 return Utils.getComponent(EntityReferenceSerializer.TYPE_STRING, "compactwiki");
90    }
91   
 
92  134 toggle private static EntityReferenceResolver<String> getXClassEntityReferenceResolver()
93    {
94  134 return Utils.getComponent(EntityReferenceResolver.TYPE_STRING, "xclass");
95    }
96   
97    /**
98    * Used to normalize references.
99    */
 
100  134 toggle private DocumentReferenceResolver<EntityReference> getExplicitReferenceDocumentReferenceResolver()
101    {
102  134 return Utils.getComponent(DocumentReferenceResolver.TYPE_REFERENCE, "explicit");
103    }
104   
 
105  7 toggle private AttachmentReferenceResolver<String> getCurentAttachmentReferenceResolver()
106    {
107  7 return Utils.getComponent(AttachmentReferenceResolver.TYPE_STRING, "current");
108    }
109   
110    private XWikiDocument doc;
111   
112    private int filesize;
113   
114    private String mimeType;
115   
116    private String filename;
117   
118    private String author;
119   
120    private DocumentReference authorReference;
121   
122    private Version version;
123   
124    private String comment;
125   
126    private Date date;
127   
128    private XWikiAttachmentContent attachment_content;
129   
130    private XWikiAttachmentArchive attachment_archive;
131   
132    private boolean isMetaDataDirty = false;
133   
134    private AttachmentReference reference;
135   
 
136  59 toggle public XWikiAttachment(XWikiDocument doc, String filename)
137    {
138  59 this();
139   
140  59 setDoc(doc);
141  59 setFilename(filename);
142    }
143   
 
144  57453 toggle public XWikiAttachment()
145    {
146  57453 this.filesize = 0;
147  57453 this.filename = "";
148  57453 this.comment = "";
149  57453 this.date = new Date();
150    }
151   
 
152  2169 toggle public AttachmentReference getReference()
153    {
154  2170 if (this.reference == null) {
155  785 if (this.doc != null) {
156  779 this.reference = new AttachmentReference(this.filename, this.doc.getDocumentReference());
157    } else {
158    // Try with current
159  7 return getCurentAttachmentReferenceResolver().resolve(this.filename);
160    }
161    }
162   
163  2163 return this.reference;
164    }
165   
 
166  5341 toggle public long getId()
167    {
168  5341 if (this.doc == null) {
169  2 return this.filename.hashCode();
170    } else {
171  5338 return (this.doc.getFullName() + "/" + this.filename).hashCode();
172    }
173    }
174   
 
175  396 toggle public void setDocId(long id)
176    {
177    }
178   
 
179  1017 toggle public long getDocId()
180    {
181  1017 return this.doc.getId();
182    }
183   
 
184  812 toggle public void setId(long id)
185    {
186    }
187   
 
188  56438 toggle @Override
189    public Object clone()
190    {
191  56438 XWikiAttachment attachment = null;
192  56438 try {
193  56438 attachment = getClass().newInstance();
194    } catch (Exception e) {
195    // This should not happen
196  0 LOGGER.error("exception while attach.clone", e);
197    }
198   
199  56438 attachment.author = this.author;
200  56438 attachment.authorReference = this.authorReference;
201   
202  56438 attachment.setComment(getComment());
203  56438 attachment.setDate(getDate());
204  56438 attachment.setFilename(getFilename());
205  56438 attachment.setMimeType(getMimeType());
206  56438 attachment.setFilesize(getFilesize());
207  56438 attachment.setRCSVersion(getRCSVersion());
208  56438 attachment.setMetaDataDirty(isMetaDataDirty());
209  56438 if (getAttachment_content() != null) {
210  38553 attachment.setAttachment_content((XWikiAttachmentContent) getAttachment_content().clone());
211  38553 attachment.getAttachment_content().setAttachment(attachment);
212    }
213  56438 if (getAttachment_archive() != null) {
214  441 attachment.setAttachment_archive((XWikiAttachmentArchive) getAttachment_archive().clone());
215  441 attachment.getAttachment_archive().setAttachment(attachment);
216    }
217   
218  56438 attachment.setDoc(getDoc());
219   
220  56438 return attachment;
221    }
222   
223    /**
224    * @return the cached filesize in byte of the attachment, stored as metadata
225    */
 
226  58191 toggle public int getFilesize()
227    {
228  58191 return this.filesize;
229    }
230   
231    /**
232    * Set cached filesize of the attachment that will be stored as metadata
233    *
234    * @param filesize in byte
235    */
 
236  57746 toggle public void setFilesize(int filesize)
237    {
238  57746 if (filesize != this.filesize) {
239  57412 setMetaDataDirty(true);
240    }
241   
242  57746 this.filesize = filesize;
243    }
244   
245    /**
246    * @param context current XWikiContext
247    * @return the real filesize in byte of the attachment. We cannot trust the metadata that may be publicly changed.
248    * @throws XWikiException
249    * @since 2.3M2
250    */
 
251  308 toggle public int getContentSize(XWikiContext context) throws XWikiException
252    {
253  304 if (this.attachment_content == null && context != null) {
254  32 this.doc.loadAttachmentContent(this, context);
255    }
256   
257  306 return this.attachment_content.getSize();
258    }
259   
 
260  149305 toggle public String getFilename()
261    {
262  149307 return this.filename;
263    }
264   
 
265  57445 toggle public void setFilename(String filename)
266    {
267  57445 if (ObjectUtils.notEqual(getFilename(), filename)) {
268  57441 setMetaDataDirty(true);
269  57441 this.filename = filename;
270    }
271  57445 this.reference = null;
272    }
273   
274    /**
275    * @since 6.4M1
276    */
 
277  1957 toggle public DocumentReference getAuthorReference()
278    {
279  1957 if (this.authorReference == null) {
280  1101 if (this.doc != null) {
281  180 this.authorReference = userStringToReference(this.author);
282    } else {
283    // Don't store the reference when generated based on context (it might become wrong when actually
284    // setting the document)
285  921 return userStringToReference(this.author);
286    }
287    }
288   
289  1036 return this.authorReference;
290    }
291   
292    /**
293    * @since 6.4M1
294    */
 
295  467 toggle public void setAuthorReference(DocumentReference authorReference)
296    {
297  467 if (ObjectUtils.notEqual(authorReference, getAuthorReference())) {
298  456 setMetaDataDirty(true);
299    }
300   
301  467 this.authorReference = authorReference;
302  467 this.author = null;
303   
304    // Log this since it's probably a mistake so that we find who is doing bad things
305  467 if (this.authorReference != null && this.authorReference.getName().equals(XWikiRightService.GUEST_USER)) {
306  0 LOGGER.warn("A reference to XWikiGuest user has been set instead of null. This is probably a mistake.",
307    new Exception("See stack trace"));
308    }
309    }
310   
311    /**
312    * Note that this method cannot be removed for now since it's used by Hibernate for saving a XWikiDocument.
313    *
314    * @deprecated since 6.4M1 use {@link #getAuthorReference()} instead
315    */
 
316  1832 toggle @Deprecated
317    public String getAuthor()
318    {
319  1832 if (this.author == null) {
320  932 this.author = userReferenceToString(getAuthorReference());
321    }
322   
323  1832 return this.author != null ? this.author : "";
324    }
325   
326    /**
327    * Note that this method cannot be removed for now since it's used by Hibernate for loading a XWikiDocument.
328    *
329    * @deprecated since 6.4M1 use {@link #setAuthorReference} instead
330    */
 
331  523 toggle @Deprecated
332    public void setAuthor(String author)
333    {
334  523 if (!Objects.equals(getAuthor(), author)) {
335  514 this.author = author;
336  514 this.authorReference = null;
337   
338  514 setMetaDataDirty(true);
339    }
340    }
341   
 
342  1990 toggle public String getVersion()
343    {
344  1990 return getRCSVersion().toString();
345    }
346   
 
347  458 toggle public void setVersion(String version)
348    {
349  458 this.version = new Version(version);
350    }
351   
 
352  10 toggle public String getNextVersion()
353    {
354  10 if (this.version == null) {
355  6 return "1.1";
356    } else {
357  4 return ((Version) this.version.clone()).next().toString();
358    }
359    }
360   
 
361  58474 toggle public Version getRCSVersion()
362    {
363  58474 if (this.version == null) {
364  434 return new Version("1.1");
365    }
366   
367  58040 return (Version) this.version.clone();
368    }
369   
 
370  56438 toggle public void setRCSVersion(Version version)
371    {
372  56438 this.version = version;
373    }
374   
 
375  114899 toggle public String getComment()
376    {
377  114899 return this.comment != null ? this.comment : "";
378    }
379   
 
380  56869 toggle public void setComment(String comment)
381    {
382  56869 if (ObjectUtils.notEqual(getComment(), comment)) {
383  23 setMetaDataDirty(true);
384  23 this.comment = comment;
385    }
386    }
387   
 
388  57417 toggle public XWikiDocument getDoc()
389    {
390  57417 return this.doc;
391    }
392   
 
393  113852 toggle public void setDoc(XWikiDocument doc)
394    {
395  113852 if (this.doc != doc) {
396  113834 this.doc = doc;
397  113834 this.reference = null;
398   
399  113834 if (isMetaDataDirty() && doc != null) {
400  1867 doc.setMetaDataDirty(true);
401    }
402  113834 if (getAttachment_content() != null) {
403  77620 getAttachment_content().setOwnerDocument(doc);
404    }
405    }
406    }
407   
 
408  58292 toggle public Date getDate()
409    {
410  58291 return this.date;
411    }
412   
 
413  56967 toggle public void setDate(Date date)
414    {
415    // Make sure we drop milliseconds for consistency with the database
416  56967 if (date != null) {
417  56967 date.setTime((date.getTime() / 1000) * 1000);
418    }
419   
420  56967 this.date = date;
421    }
422   
 
423  1490 toggle public boolean isContentDirty()
424    {
425  1490 if (this.attachment_content == null) {
426  156 return false;
427    } else {
428  1334 return this.attachment_content.isContentDirty();
429    }
430    }
431   
 
432  60 toggle public void incrementVersion()
433    {
434  60 if (this.version == null) {
435  52 this.version = new Version("1.1");
436    } else {
437  8 this.version = this.version.next();
438    }
439    }
440   
 
441  170273 toggle public boolean isMetaDataDirty()
442    {
443  170273 return this.isMetaDataDirty;
444    }
445   
 
446  173157 toggle public void setMetaDataDirty(boolean metaDataDirty)
447    {
448  173157 this.isMetaDataDirty = metaDataDirty;
449  173157 if (metaDataDirty && this.doc != null) {
450  172 this.doc.setMetaDataDirty(true);
451    }
452    }
453   
454    /**
455    * Retrieve an attachment as an XML string. You should prefer
456    * {@link #toXML(com.xpn.xwiki.internal.xml.XMLWriter, boolean, boolean, com.xpn.xwiki.XWikiContext)} to avoid
457    * memory loads when appropriate.
458    *
459    * @param bWithAttachmentContent if true, binary content of the attachment is included (base64 encoded)
460    * @param bWithVersions if true, all archived versions are also included
461    * @param context current XWikiContext
462    * @return a string containing an XML representation of the attachment
463    * @throws XWikiException when an error occurs during wiki operations
464    */
 
465  62 toggle public String toStringXML(boolean bWithAttachmentContent, boolean bWithVersions, XWikiContext context)
466    throws XWikiException
467    {
468    // This is very bad. baos holds the entire attachment on the heap, then it makes a copy when toByteArray
469    // is called, then String forces us to make a copy when we construct a new String.
470    // Unfortunately this can't be fixed because jrcs demands the content as a String.
471  62 ByteArrayOutputStream baos = new ByteArrayOutputStream();
472  62 try {
473  62 XMLWriter wr = new XMLWriter(baos, new OutputFormat("", true, context.getWiki().getEncoding()));
474  62 Document doc = new DOMDocument();
475  62 wr.writeDocumentStart(doc);
476  62 toXML(wr, bWithAttachmentContent, bWithVersions, context);
477  62 wr.writeDocumentEnd(doc);
478  62 return baos.toString();
479    } catch (IOException e) {
480  0 e.printStackTrace();
481  0 return "";
482    }
483    }
484   
485    /**
486    * Retrieve XML representation of attachment's metadata into an {@link Element}.
487    *
488    * @return a {@link Element} containing an XML representation of the attachment without content
489    * @throws XWikiException when an error occurs during wiki operations
490    */
 
491  0 toggle public Element toXML(XWikiContext context) throws XWikiException
492    {
493  0 return toXML(false, false, context);
494    }
495   
496    /**
497    * Write an XML representation of the attachment into an {@link com.xpn.xwiki.internal.xml.XMLWriter}
498    *
499    * @param wr the XMLWriter to write to
500    * @param bWithAttachmentContent if true, binary content of the attachment is included (base64 encoded)
501    * @param bWithVersions if true, all archive version is also included
502    * @param context current XWikiContext
503    * @throws IOException when an error occurs during streaming operation
504    * @throws XWikiException when an error occurs during xwiki operation
505    * @since 2.3M2
506    */
 
507  62 toggle public void toXML(XMLWriter wr, boolean bWithAttachmentContent, boolean bWithVersions, XWikiContext context)
508    throws IOException, XWikiException
509    {
510    // IMPORTANT: we don't use SAX apis here because the specified XMLWriter could be a DOMXMLWriter for retro
511    // compatibility reasons
512   
513  62 Element docel = new DOMElement("attachment");
514  62 wr.writeOpen(docel);
515   
516  62 Element el = new DOMElement("filename");
517  62 el.addText(getFilename());
518  62 wr.write(el);
519   
520  62 el = new DOMElement("filesize");
521  62 el.addText(String.valueOf(getFilesize()));
522  62 wr.write(el);
523   
524  62 if (StringUtils.isNotEmpty(getMimeType())) {
525  13 el = new DOMElement("mimetype");
526  13 el.addText(getMimeType());
527  13 wr.write(el);
528    }
529   
530  62 el = new DOMElement("author");
531  62 el.addText(getAuthor());
532  62 wr.write(el);
533   
534  62 long d = getDate().getTime();
535  62 el = new DOMElement("date");
536  62 el.addText(String.valueOf(d));
537  62 wr.write(el);
538   
539  62 el = new DOMElement("version");
540  62 el.addText(getVersion());
541  62 wr.write(el);
542   
543  62 el = new DOMElement("comment");
544  62 el.addText(getComment());
545  62 wr.write(el);
546   
547  62 if (bWithAttachmentContent) {
548  62 el = new DOMElement("content");
549    // We need to make sure content is loaded
550  62 loadContent(context);
551  62 XWikiAttachmentContent acontent = getAttachment_content();
552  62 if (acontent != null) {
553  62 try (InputStream stream = acontent.getContentInputStream()) {
554  62 wr.writeBase64(el, stream);
555    }
556    } else {
557  0 el.addText("");
558  0 wr.write(el);
559    }
560    }
561   
562  62 if (bWithVersions) {
563    // We need to make sure content is loaded
564  5 XWikiAttachmentArchive aarchive = loadArchive(context);
565  5 if (aarchive != null) {
566  5 el = new DOMElement("versions");
567  5 try {
568  5 el.addText(aarchive.getArchiveAsString());
569  5 wr.write(el);
570    } catch (XWikiException e) {
571    }
572    }
573    }
574   
575  62 wr.writeClose(docel);
576    }
577   
578    /**
579    * Retrieve XML representation of attachment's metadata into an {@link Element}. You should prefer
580    * {@link #toXML(com.xpn.xwiki.internal.xml.XMLWriter, boolean, boolean, com.xpn.xwiki.XWikiContext)} to avoid
581    * memory loads when appropriate.
582    *
583    * @param bWithAttachmentContent if true, binary content of the attachment is included (base64 encoded)
584    * @param bWithVersions if true, all archived versions are also included
585    * @param context current XWikiContext
586    * @return an {@link Element} containing an XML representation of the attachment
587    * @throws XWikiException when an error occurs during wiki operations
588    * @since 2.3M2
589    */
 
590  0 toggle public Element toXML(boolean bWithAttachmentContent, boolean bWithVersions, XWikiContext context)
591    throws XWikiException
592    {
593  0 Document doc = new DOMDocument();
594  0 DOMXMLWriter wr = new DOMXMLWriter(doc, new OutputFormat("", true, context.getWiki().getEncoding()));
595   
596  0 try {
597  0 toXML(wr, bWithAttachmentContent, bWithVersions, context);
598    } catch (IOException e) {
599  0 throw new RuntimeException(e);
600    }
601  0 return doc.getRootElement();
602    }
603   
 
604  14 toggle public void fromXML(String data) throws XWikiException
605    {
606  14 SAXReader reader = new SAXReader();
607  14 Document domdoc;
608  14 try {
609  14 StringReader in = new StringReader(data);
610  14 domdoc = reader.read(in);
611    } catch (DocumentException e) {
612  0 throw new XWikiException(XWikiException.MODULE_XWIKI_DOC, XWikiException.ERROR_DOC_XML_PARSING,
613    "Error parsing xml", e, null);
614    }
615  14 Element docel = domdoc.getRootElement();
616  14 fromXML(docel);
617    }
618   
 
619  14 toggle public void fromXML(Element docel) throws XWikiException
620    {
621  14 setFilename(getElementText(docel, "filename"));
622  14 setFilesize(Integer.parseInt(getElementText(docel, "filesize")));
623  14 setMimeType(getElementText(docel, "mimetype"));
624  14 setAuthor(getElementText(docel, "author"));
625  14 setVersion(getElementText(docel, "version"));
626  14 setComment(getElementText(docel, "comment"));
627   
628  14 String sdate = getElementText(docel, "date");
629  14 setDate(new Date(Long.parseLong(sdate)));
630   
631  14 String base64content = getElementText(docel, "content");
632  14 if (base64content != null) {
633  14 try (Base64InputStream decoded =
634    new Base64InputStream(new ReaderInputStream(new StringReader(base64content)))) {
635  14 setContent(decoded);
636    } catch (IOException e) {
637  0 throw new XWikiException("Failed to read base 64 encoded atachment content", e);
638    }
639    }
640   
641  14 setArchive(getElementText(docel, "versions"));
642   
643    // The setters we're calling above will set the metadata dirty flag to true since they're changing the
644    // attachment's identity. However since this method is about loading the attachment from XML it shouldn't be
645    // considered as dirty.
646  14 setMetaDataDirty(false);
647    }
648   
 
649  126 toggle private String getElementText(Element element, String name)
650    {
651  126 Element child = element.element(name);
652   
653  126 return child != null ? child.getText() : null;
654    }
655   
 
656  325926 toggle public XWikiAttachmentContent getAttachment_content()
657    {
658  325926 return this.attachment_content;
659    }
660   
 
661  38860 toggle public void setAttachment_content(XWikiAttachmentContent attachment_content)
662    {
663  38860 this.attachment_content = attachment_content;
664  38859 if (attachment_content != null) {
665  38860 attachment_content.setOwnerDocument(this.doc);
666    }
667    }
668   
 
669  58186 toggle public XWikiAttachmentArchive getAttachment_archive()
670    {
671  58186 return this.attachment_archive;
672    }
673   
 
674  951 toggle public void setAttachment_archive(XWikiAttachmentArchive attachment_archive)
675    {
676  951 this.attachment_archive = attachment_archive;
677    }
678   
679    /**
680    * Retrive the content of this attachment as a byte array.
681    *
682    * @param context current XWikiContext
683    * @return a byte array containing the binary data content of the attachment
684    * @throws XWikiException when an error occurs during wiki operation
685    * @deprecated use {@link #getContentInputStream(XWikiContext)} instead
686    */
 
687  24 toggle @Deprecated
688    public byte[] getContent(XWikiContext context) throws XWikiException
689    {
690  24 if (this.attachment_content == null && context != null) {
691  4 this.doc.loadAttachmentContent(this, context);
692    }
693   
694  24 return this.attachment_content.getContent();
695    }
696   
697    /**
698    * Retrieve the content of this attachment as an input stream.
699    *
700    * @param context current XWikiContext
701    * @return an InputStream to consume for receiving the content of this attachment
702    * @throws XWikiException when an error occurs during wiki operation
703    * @since 2.3M2
704    */
 
705  631 toggle public InputStream getContentInputStream(XWikiContext context) throws XWikiException
706    {
707  631 if (this.attachment_content == null && context != null) {
708  252 if (Objects.equals(this.getVersion(), this.getLatestStoredVersion(context))) {
709    // Load the attachment content from the xwikiattachment_content table.
710  244 this.doc.loadAttachmentContent(this, context);
711    } else {
712    // Load the attachment content from the xwikiattachment_archive table.
713    // We don't use #getAttachmentRevision() because it checks if the requested version equals the version
714    // of the target attachment (XWIKI-1938).
715  8 XWikiAttachment archivedVersion =
716    this.loadArchive(context).getRevision(this, this.getVersion(), context);
717  8 XWikiAttachmentContent content =
718  8 archivedVersion != null ? archivedVersion.getAttachment_content() : null;
719  8 if (content != null) {
720  8 this.setAttachment_content(content);
721    } else {
722    // Fall back on the version of the content stored in the xwikiattachment_content table.
723  0 this.doc.loadAttachmentContent(this, context);
724    }
725    }
726    }
727   
728  630 return this.attachment_content.getContentInputStream();
729    }
730   
731    /**
732    * The {@code xwikiattachment_content} table stores only the latest version of an attachment (which is identified by
733    * the attachment file name and the owner document reference). The rest of the attachment versions are stored in
734    * {@code xwikiattachment_archive} table. This method can be used to determine from where to load the attachment
735    * content.
736    *
737    * @param context the XWiki context
738    * @return the latest stored version of this attachment
739    */
 
740  253 toggle private String getLatestStoredVersion(XWikiContext context)
741    {
742  253 try {
743  253 XWikiDocument ownerDocumentLastestVersion =
744    context.getWiki().getDocument(this.doc.getDocumentReference(), context);
745  252 XWikiAttachment latestStoredVersion = ownerDocumentLastestVersion.getAttachment(this.filename);
746  252 return latestStoredVersion != null ? latestStoredVersion.getVersion() : null;
747    } catch (XWikiException e) {
748  0 LOGGER.warn(ExceptionUtils.getRootCauseMessage(e));
749  0 return null;
750    }
751    }
752   
753    /**
754    * @deprecated since 2.6M1 please do not use this, it is bound to a jrcs based implementation.
755    */
 
756  0 toggle @Deprecated
757    public Archive getArchive()
758    {
759  0 if (this.attachment_archive == null) {
760  0 return null;
761    } else {
762  0 return this.attachment_archive.getRCSArchive();
763    }
764    }
765   
766    /**
767    * @deprecated since 2.6M1 please do not use this, it is bound to a jrcs based implementation.
768    */
 
769  0 toggle @Deprecated
770    public void setArchive(Archive archive)
771    {
772  0 if (this.attachment_archive == null) {
773  0 this.attachment_archive = new XWikiAttachmentArchive();
774  0 this.attachment_archive.setAttachment(this);
775    }
776   
777  0 this.attachment_archive.setRCSArchive(archive);
778    }
779   
 
780  15 toggle public void setArchive(String data) throws XWikiException
781    {
782  15 if (this.attachment_archive == null) {
783  15 this.attachment_archive = new XWikiAttachmentArchive();
784  15 this.attachment_archive.setAttachment(this);
785    }
786   
787  15 this.attachment_archive.setArchive(data);
788    }
789   
 
790  3 toggle public synchronized Version[] getVersions()
791    {
792  3 try {
793  3 return getAttachment_archive().getVersions();
794    } catch (Exception ex) {
795  0 LOGGER.warn("Cannot retrieve versions of attachment [{}@{}]: {}",
796    new Object[] { getFilename(), getDoc().getDocumentReference(), ex.getMessage() });
797  0 return new Version[] { new Version(this.getVersion()) };
798    }
799    }
800   
801    /**
802    * Get the list of all versions up to the current. We assume versions go from 1.1 to the current one This allows not
803    * to read the full archive file.
804    *
805    * @return a list of Version from 1.1 to the current version.
806    * @throws XWikiException never happens.
807    */
 
808  3 toggle public List<Version> getVersionList() throws XWikiException
809    {
810  3 final List<Version> list = new ArrayList<Version>();
811  3 final String currentVersion = this.version.toString();
812  3 Version v = new Version("1.1");
813  3 for (;;) {
814  6 list.add(v);
815  6 if (v.toString().equals(currentVersion)) {
816  3 break;
817    }
818  3 v = v.next();
819    }
820   
821  3 return list;
822    }
823   
824    /**
825    * Set the content of an attachment from a byte array.
826    *
827    * @param data a byte array with the binary content of the attachment
828    * @deprecated use {@link #setContent(java.io.InputStream, int)} instead
829    */
 
830  16 toggle @Deprecated
831    public void setContent(byte[] data)
832    {
833  16 if (this.attachment_content == null) {
834  12 this.attachment_content = new XWikiAttachmentContent();
835  12 this.attachment_content.setAttachment(this);
836    }
837   
838  16 this.attachment_content.setContent(data);
839    }
840   
841    /**
842    * Set the content of an attachment from an InputStream.
843    *
844    * @param is the input stream that will be read
845    * @param length the length in byte to read
846    * @throws IOException when an error occurs during streaming operation
847    * @since 2.3M2
848    */
 
849  1 toggle public void setContent(InputStream is, int length) throws IOException
850    {
851  1 if (this.attachment_content == null) {
852  1 this.attachment_content = new XWikiAttachmentContent();
853  1 this.attachment_content.setAttachment(this);
854    }
855   
856  1 this.attachment_content.setContent(is, length);
857    }
858   
859    /**
860    * Set the content of the attachment from an InputStream.
861    *
862    * @param is the input stream that will be read
863    * @throws IOException when an error occurs during streaming operation
864    * @since 2.6M1
865    */
 
866  573 toggle public void setContent(InputStream is) throws IOException
867    {
868  573 if (this.attachment_content == null) {
869  571 this.attachment_content = new XWikiAttachmentContent(this);
870    }
871   
872  573 this.attachment_content.setContent(is);
873    }
874   
 
875  84 toggle public void loadContent(XWikiContext context) throws XWikiException
876    {
877  84 if (this.attachment_content == null) {
878  1 try {
879  1 context.getWiki().getAttachmentStore().loadAttachmentContent(this, context, true);
880    } catch (Exception ex) {
881  0 LOGGER.warn(
882    "Failed to load content for attachment [{}@{}]. "
883    + "This attachment is broken, please consider re-uploading it. Internal error: {}",
884  0 new Object[] { getFilename(), (this.doc != null) ? this.doc.getDocumentReference() : "<unknown>",
885    ex.getMessage() });
886    }
887    }
888    }
889   
 
890  464 toggle public XWikiAttachmentArchive loadArchive(XWikiContext context) throws XWikiException
891    {
892  464 if (this.attachment_archive == null) {
893  458 try {
894  458 this.attachment_archive =
895    context.getWiki().getAttachmentVersioningStore().loadArchive(this, context, true);
896    } catch (Exception ex) {
897  0 LOGGER.warn(
898    "Failed to load archive for attachment [{}@{}]. "
899    + "This attachment is broken, please consider re-uploading it. Internal error: {}",
900  0 new Object[] { getFilename(), (this.doc != null) ? this.doc.getDocumentReference() : "<unknown>",
901    ex.getMessage() });
902    }
903    }
904   
905  464 return this.attachment_archive;
906    }
907   
 
908  60 toggle public void updateContentArchive(XWikiContext context) throws XWikiException
909    {
910  60 if (this.attachment_content == null) {
911  0 return;
912    }
913   
914  60 loadArchive(context).updateArchive(context);
915    }
916   
917    /**
918    * Return the stored media type. If none is stored try to detects the media type of this attachment's content using
919    * {@link Tika}. We first try to determine the media type based on the file name extension and if the extension is
920    * unknown we try to determine the media type by reading the first bytes of the attachment content.
921    *
922    * @param xcontext the XWiki context
923    * @return the media type of this attachment's content
924    */
 
925  831 toggle public String getMimeType(XWikiContext xcontext)
926    {
927  831 String type = getMimeType();
928   
929  831 if (StringUtils.isEmpty(type)) {
930  793 type = extractMimeType(xcontext);
931    }
932   
933  831 return type;
934    }
935   
936    /**
937    * Return the stored media type.
938    *
939    * @return the media type of this attachment's content
940    * @since 7.1M1
941    */
 
942  58387 toggle public String getMimeType()
943    {
944  58387 return this.mimeType;
945    }
946   
947    /**
948    * @param mimeType the explicit mime type of the file
949    * @since 7.1M1
950    */
 
951  56857 toggle public void setMimeType(String mimeType)
952    {
953  56857 this.mimeType = mimeType;
954    }
955   
956    /**
957    * Extract the mime type from the file name and content and remember it to be stored.
958    *
959    * @param xcontext the {@link XWikiContext} use to load the content if it's not already loaded
960    * @since 7.1M1
961    */
 
962  11 toggle public void resetMimeType(XWikiContext xcontext)
963    {
964  11 this.mimeType = extractMimeType(xcontext);
965    }
966   
 
967  804 toggle private String extractMimeType(XWikiContext xcontext)
968    {
969    // We try name-based detection and then fall back on content-based detection. We don't do this in a single step,
970    // by passing both the content and the file name to Tika, because the default detector looks at the content
971    // first which can be an issue for large attachments. Our approach is less accurate but has better performance.
972  804 String mediaType = getFilename() != null ? TIKA.detect(getFilename()) : MediaType.OCTET_STREAM.toString();
973  804 if (MediaType.OCTET_STREAM.toString().equals(mediaType)) {
974  20 try {
975    // Content-based detection is more accurate but it may require loading the attachment content in memory
976    // (from the database) if it hasn't been cached as a temporary file yet. This can be an issue for large
977    // attachments when database storage is used. Only the first bytes are normally read but still this
978    // process is slower than name-based detection.
979    //
980    // We wrap the content input stream in a BufferedInputStream to make sure that all the detectors can
981    // read the content even if the input stream is configured to auto close when it reaches the end. This
982    // can happen for small files if AutoCloseInputStream is used, which supports the mark and reset methods
983    // so Tika uses it directly. In this case, the input stream is automatically closed after the first
984    // detector reads it so the next detector fails to read it.
985  20 mediaType = TIKA.detect(new BufferedInputStream(getContentInputStream(xcontext)));
986    } catch (Exception e) {
987  5 LOGGER.warn("Failed to read the content of [{}] in order to detect its mime type.", getReference());
988    }
989    }
990   
991  804 return mediaType;
992    }
993   
 
994  311 toggle public boolean isImage(XWikiContext context)
995    {
996  311 String contenttype = getMimeType(context);
997  311 if (contenttype.startsWith("image/")) {
998  299 return true;
999    } else {
1000  12 return false;
1001    }
1002    }
1003   
 
1004  12 toggle public XWikiAttachment getAttachmentRevision(String rev, XWikiContext context) throws XWikiException
1005    {
1006  12 if (StringUtils.equals(rev, this.getVersion())) {
1007  4 return this;
1008    }
1009   
1010  8 return loadArchive(context).getRevision(this, rev, context);
1011    }
1012   
1013    /**
1014    * Apply the provided attachment so that the current one contains the same informations and indicate if it was
1015    * necessary to modify it in any way.
1016    *
1017    * @param attachment the attachment to apply
1018    * @return true if the attachment has been modified
1019    * @since 5.3M2
1020    */
 
1021  1 toggle public boolean apply(XWikiAttachment attachment)
1022    {
1023  1 boolean modified = false;
1024   
1025  1 if (getFilesize() != attachment.getFilesize()) {
1026  1 setFilesize(attachment.getFilesize());
1027  1 modified = true;
1028    }
1029   
1030  1 if (StringUtils.equals(getMimeType(), attachment.getMimeType())) {
1031  1 setMimeType(attachment.getMimeType());
1032  1 modified = true;
1033    }
1034   
1035  1 try {
1036  1 if (!IOUtils.contentEquals(getContentInputStream(null), attachment.getContentInputStream(null))) {
1037  1 setContent(attachment.getContentInputStream(null));
1038  1 modified = true;
1039    }
1040    } catch (Exception e) {
1041  0 LOGGER.error("Failed to compare content of attachments", e);
1042    }
1043   
1044  1 return modified;
1045    }
1046   
 
1047  80 toggle public boolean equalsData(XWikiAttachment otherAttachment, XWikiContext xcontext) throws XWikiException
1048    {
1049  80 try {
1050  80 if (getFilesize() == otherAttachment.getFilesize()) {
1051  77 InputStream is = getContentInputStream(xcontext);
1052   
1053  77 try {
1054  77 InputStream otherIS = otherAttachment.getContentInputStream(xcontext);
1055   
1056  77 try {
1057  77 return IOUtils.contentEquals(is, otherIS);
1058    } finally {
1059  77 otherIS.close();
1060    }
1061    } finally {
1062  77 is.close();
1063    }
1064    }
1065    } catch (Exception e) {
1066  0 throw new XWikiException(XWikiException.MODULE_XWIKI_DOC, XWikiException.ERROR_XWIKI_UNKNOWN,
1067    "Failed to compare attachments", e);
1068    }
1069   
1070  3 return false;
1071    }
1072   
 
1073  1 toggle public void merge(XWikiAttachment previousAttachment, XWikiAttachment nextAttachment,
1074    MergeConfiguration configuration, XWikiContext xcontext, MergeResult mergeResult)
1075    {
1076  1 try {
1077  1 if (equalsData(previousAttachment, xcontext)) {
1078  1 this.apply(nextAttachment);
1079  1 mergeResult.setModified(true);
1080    } else {
1081  0 if (this.equals(nextAttachment)) {
1082    // Already modified as expected in the DB, lets assume the user is prescient
1083  0 mergeResult.getLog().warn("Attachment [{}] already modified", getReference());
1084    }
1085    }
1086    } catch (Exception e) {
1087  0 mergeResult.getLog().error("Failed to merge attachment [{}]", this, e);
1088    }
1089    }
1090   
1091    /**
1092    * @param userReference the user {@link DocumentReference} to convert to {@link String}
1093    * @return the user as String
1094    */
 
1095  932 toggle private String userReferenceToString(DocumentReference userReference)
1096    {
1097  932 String userString;
1098   
1099  932 if (userReference != null) {
1100  429 userString = getCompactWikiEntityReferenceSerializer().serialize(userReference, getReference());
1101    } else {
1102  503 userString = XWikiRightService.GUEST_USER_FULLNAME;
1103    }
1104   
1105  932 return userString;
1106    }
1107   
1108    /**
1109    * @param userString the user {@link String} to convert to {@link DocumentReference}
1110    * @return the user as {@link DocumentReference}
1111    */
 
1112  1101 toggle private DocumentReference userStringToReference(String userString)
1113    {
1114  1101 DocumentReference userReference;
1115   
1116  1101 if (StringUtils.isEmpty(userString)) {
1117  967 userReference = null;
1118    } else {
1119  134 userReference = getExplicitReferenceDocumentReferenceResolver()
1120    .resolve(getXClassEntityReferenceResolver().resolve(userString, EntityType.DOCUMENT), getReference());
1121   
1122  134 if (userReference.getName().equals(XWikiRightService.GUEST_USER)) {
1123  1 userReference = null;
1124    }
1125    }
1126   
1127  1101 return userReference;
1128    }
1129   
 
1130  0 toggle private XWikiContext getXWikiContext()
1131    {
1132  0 Provider<XWikiContext> xcontextProvider = Utils.getComponent(XWikiContext.TYPE_PROVIDER);
1133   
1134  0 if (xcontextProvider != null) {
1135  0 return xcontextProvider.get();
1136    }
1137   
1138  0 return null;
1139    }
1140    }