1. Project Clover database Sat Feb 2 2019 06:45:20 CET
  2. Package com.xpn.xwiki.doc

File XWikiAttachment.java

 

Coverage histogram

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

Code metrics

158
360
95
2
1,437
900
200
0.56
3.79
47.5
2.11

Classes

Class Line # Actions
XWikiAttachment 76 360 0% 200 96
0.8433931584.3%
XWikiAttachment.AttachmentContainer 78 0 - 0 0
-1.0 -
 

Contributing tests

This file is covered by 98 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.IOException;
24    import java.io.InputStream;
25    import java.io.StringWriter;
26    import java.nio.charset.StandardCharsets;
27    import java.util.ArrayList;
28    import java.util.Date;
29    import java.util.List;
30    import java.util.Objects;
31   
32    import org.apache.commons.io.IOUtils;
33    import org.apache.commons.lang3.ObjectUtils;
34    import org.apache.commons.lang3.StringUtils;
35    import org.apache.commons.lang3.exception.ExceptionUtils;
36    import org.apache.tika.Tika;
37    import org.apache.tika.mime.MediaType;
38    import org.dom4j.Element;
39    import org.dom4j.io.DocumentResult;
40    import org.slf4j.Logger;
41    import org.slf4j.LoggerFactory;
42    import org.suigeneris.jrcs.rcs.Archive;
43    import org.suigeneris.jrcs.rcs.Version;
44    import org.xwiki.component.manager.ComponentLookupException;
45    import org.xwiki.filter.input.InputSource;
46    import org.xwiki.filter.input.StringInputSource;
47    import org.xwiki.filter.instance.input.DocumentInstanceInputProperties;
48    import org.xwiki.filter.output.DefaultWriterOutputTarget;
49    import org.xwiki.filter.output.OutputTarget;
50    import org.xwiki.filter.xar.output.XAROutputProperties;
51    import org.xwiki.filter.xml.output.DefaultResultOutputTarget;
52    import org.xwiki.model.EntityType;
53    import org.xwiki.model.reference.AttachmentReference;
54    import org.xwiki.model.reference.AttachmentReferenceResolver;
55    import org.xwiki.model.reference.DocumentReference;
56    import org.xwiki.model.reference.DocumentReferenceResolver;
57    import org.xwiki.model.reference.EntityReference;
58    import org.xwiki.model.reference.EntityReferenceResolver;
59    import org.xwiki.model.reference.EntityReferenceSerializer;
60    import org.xwiki.model.reference.WikiReference;
61    import org.xwiki.stability.Unstable;
62    import org.xwiki.tika.internal.TikaUtils;
63   
64    import com.xpn.xwiki.XWikiContext;
65    import com.xpn.xwiki.XWikiException;
66    import com.xpn.xwiki.doc.merge.MergeConfiguration;
67    import com.xpn.xwiki.doc.merge.MergeResult;
68    import com.xpn.xwiki.internal.filter.XWikiDocumentFilterUtils;
69    import com.xpn.xwiki.internal.xml.XMLWriter;
70    import com.xpn.xwiki.store.AttachmentVersioningStore;
71    import com.xpn.xwiki.store.XWikiAttachmentStoreInterface;
72    import com.xpn.xwiki.store.XWikiHibernateBaseStore;
73    import com.xpn.xwiki.user.api.XWikiRightService;
74    import com.xpn.xwiki.web.Utils;
75   
 
76    public class XWikiAttachment implements Cloneable
77    {
 
78    public static interface AttachmentContainer
79    {
80    void onAttachmentNameModified(String previousAttachmentName, XWikiAttachment attachment);
81    }
82   
83    private static final Logger LOGGER = LoggerFactory.getLogger(XWikiAttachment.class);
84   
85    /**
86    * Used to convert a Document Reference to string (compact form without the wiki part if it matches the current
87    * wiki).
88    */
 
89  743 toggle private static EntityReferenceSerializer<String> getCompactWikiEntityReferenceSerializer()
90    {
91  743 return Utils.getComponent(EntityReferenceSerializer.TYPE_STRING, "compactwiki");
92    }
93   
 
94  641 toggle private static EntityReferenceResolver<String> getXClassEntityReferenceResolver()
95    {
96  641 return Utils.getComponent(EntityReferenceResolver.TYPE_STRING, "xclass");
97    }
98   
99    /**
100    * Used to normalize references.
101    */
 
102  641 toggle private DocumentReferenceResolver<EntityReference> getExplicitReferenceDocumentReferenceResolver()
103    {
104  641 return Utils.getComponent(DocumentReferenceResolver.TYPE_REFERENCE, "explicit");
105    }
106   
 
107  8 toggle private AttachmentReferenceResolver<String> getCurentAttachmentReferenceResolver()
108    {
109  8 return Utils.getComponent(AttachmentReferenceResolver.TYPE_STRING, "current");
110    }
111   
112    private XWikiDocument doc;
113   
114    private Long size;
115   
116    private String mimeType;
117   
118    private String charset;
119   
120    private String filename;
121   
122    private String author;
123   
124    private DocumentReference authorReference;
125   
126    private Version version;
127   
128    private String comment;
129   
130    private Date date;
131   
132    private String contentStore;
133   
134    private boolean contentStoreSet;
135   
136    private XWikiAttachmentStoreInterface contentStoreInstance;
137   
138    private String archiveStore;
139   
140    private boolean archiveStoreSet;
141   
142    private AttachmentVersioningStore archiveStoreInstance;
143   
144    private XWikiAttachmentContent content;
145   
146    private XWikiAttachmentArchive attachment_archive;
147   
148    private boolean isMetaDataDirty = false;
149   
150    private AttachmentReference reference;
151   
152    private boolean forceSetFilesize;
153   
154    private AttachmentContainer container;
155   
 
156  144 toggle public XWikiAttachment(XWikiDocument doc, String filename)
157    {
158  144 this();
159   
160  144 setDoc(doc);
161  144 setFilename(filename);
162   
163    // We know it's not Hibernate
164  144 this.forceSetFilesize = true;
165    }
166   
 
167  4769 toggle public XWikiAttachment()
168    {
169  4769 this.filename = "";
170  4769 this.comment = "";
171  4769 this.date = new Date();
172   
173    // It might be Hibernate
174  4769 this.forceSetFilesize = false;
175    }
176   
 
177  7921 toggle public AttachmentReference getReference()
178    {
179  7926 if (this.reference == null) {
180  3106 if (this.doc != null) {
181  3099 this.reference = new AttachmentReference(this.filename, this.doc.getDocumentReference());
182    } else {
183    // Try with current
184  8 return getCurentAttachmentReferenceResolver().resolve(this.filename);
185    }
186    }
187   
188  7917 return this.reference;
189    }
190   
 
191  3432 toggle public long getId()
192    {
193  3432 if (this.doc == null) {
194  2 return this.filename.hashCode();
195    } else {
196  3430 return (this.doc.getFullName() + "/" + this.filename).hashCode();
197    }
198    }
199   
 
200  3482 toggle public void setDocId(long id)
201    {
202    }
203   
 
204  1827 toggle public long getDocId()
205    {
206  1827 return this.doc.getId();
207    }
208   
 
209  4133 toggle public void setId(long id)
210    {
211    }
212   
 
213  221495 toggle @Override
214    public XWikiAttachment clone()
215    {
216  221495 XWikiAttachment attachment = null;
217   
218  221495 try {
219  221496 attachment = (XWikiAttachment) super.clone();
220   
221    // The new attachment is not associated to any document yet
222  221497 attachment.setDoc(null, false);
223   
224  221497 attachment.setComment(getComment());
225  221496 attachment.setDate(getDate());
226  221494 attachment.setFilename(getFilename());
227  221494 attachment.setMimeType(getMimeType());
228  221496 attachment.setLongSize(getLongSize());
229  221496 attachment.setRCSVersion(getRCSVersion());
230  221496 attachment.setMetaDataDirty(isMetaDataDirty());
231  221496 if (getAttachment_content() != null) {
232  109281 attachment.setAttachment_content((XWikiAttachmentContent) getAttachment_content().clone());
233  109282 attachment.getAttachment_content().setAttachment(attachment);
234    }
235  221493 if (getAttachment_archive() != null) {
236  140 attachment.setAttachment_archive((XWikiAttachmentArchive) getAttachment_archive().clone());
237  140 attachment.getAttachment_archive().setAttachment(attachment);
238    }
239   
240    // Reset container since this new instance is not yet stored anywhere
241  221496 attachment.container = null;
242    } catch (CloneNotSupportedException e) {
243    // This should not happen
244  0 LOGGER.error("exception while attach.clone", e);
245    }
246   
247  221495 return attachment;
248    }
249   
250    /**
251    * @return the number of bytes in this attachment content
252    * @deprecated since 9.0RC1, use {@link #getLongSize()} instead
253    */
 
254  1816 toggle @Deprecated
255    public int getFilesize()
256    {
257  1816 long longSize = getLongSize();
258   
259  1816 return longSize > (long) Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) longSize;
260    }
261   
262    /**
263    * Set cached filesize of the attachment that will be stored as metadata.
264    *
265    * @param filesize the number of bytes in this attachment content
266    * @deprecated since 9.0RC1, use {@link #setLongSize(long)} instead
267    */
 
268  3482 toggle @Deprecated
269    public void setFilesize(int filesize)
270    {
271    // There is no way to tell Hibernate to not call #setFilesize and we don't want to break the size if it's bigger
272    // than an int (#setFilesize is usually called after setLongSize by Hibernate)
273  3482 if (filesize >= 0 || filesize < Integer.MAX_VALUE || this.forceSetFilesize) {
274  3482 setLongSize(filesize);
275    }
276    }
277   
278    /**
279    * @return the real size of the attachment extracted from the content, or the size metadata if the content is not
280    * loaded yet. -1 if the size is unknown.
281    * @since 9.0RC1
282    */
 
283  230117 toggle public long getLongSize()
284    {
285    // Give priority to the real attachment size if the content is loaded (bulletproofing for any mistake in the
286    // metadata)
287  230117 XWikiAttachmentContent attachmentContent = getAttachment_content();
288  230118 if (attachmentContent != null) {
289  116566 this.size = attachmentContent.getLongSize();
290    }
291   
292  230117 return this.size == null ? -1 : this.size;
293    }
294   
295    /**
296    * The size is automatically calculated from the attachment content so this method is mostly internal API that
297    * should not be used.
298    *
299    * @param size the metadata holding the number of bytes in this attachment content
300    * @since 9.0RC1
301    */
 
302  231077 toggle public void setLongSize(long size)
303    {
304  231077 if (this.size == null || size != this.size) {
305  4868 setMetaDataDirty(true);
306    }
307   
308  231078 this.size = size;
309    }
310   
311    /**
312    * @param context current XWikiContext
313    * @return the real filesize in byte of the attachment. We cannot trust the metadata that may be publicly changed.
314    * @throws XWikiException
315    * @since 2.3M2
316    * @deprecated since 9.0RC1, use {@link #getContentLongSize(XWikiContext)} instead
317    */
 
318  611 toggle @Deprecated
319    public int getContentSize(XWikiContext context) throws XWikiException
320    {
321  613 long longSize = getContentLongSize(context);
322   
323  609 return longSize > (long) Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) longSize;
324    }
325   
326    /**
327    * @param context current XWikiContext
328    * @return the real filesize in byte of the attachment. We cannot trust the metadata that may be publicly changed.
329    * @throws XWikiException
330    * @since 9.0RC1
331    */
 
332  1616 toggle public long getContentLongSize(XWikiContext context) throws XWikiException
333    {
334  1620 if (this.content == null && context != null) {
335  367 loadAttachmentContent(context);
336    }
337   
338  1618 return this.content != null ? this.content.getLongSize() : -1;
339    }
340   
 
341  956157 toggle public String getFilename()
342    {
343  956159 return this.filename;
344    }
345   
 
346  226262 toggle public void setFilename(String filename)
347    {
348  226261 if (ObjectUtils.notEqual(getFilename(), filename)) {
349  4765 setMetaDataDirty(true);
350   
351  4765 String previousFileName = this.getFilename();
352   
353  4765 this.filename = filename;
354   
355  4765 notificateNameModifed(previousFileName, this);
356    }
357   
358  226262 this.reference = null;
359    }
360   
361    /**
362    * @since 6.4M1
363    */
 
364  8752 toggle public DocumentReference getAuthorReference()
365    {
366  8752 if (this.authorReference == null) {
367  5346 if (this.doc != null) {
368  750 this.authorReference = userStringToReference(this.author);
369    } else {
370    // Don't store the reference when generated based on context (it might become wrong when actually
371    // setting the document)
372  4596 return userStringToReference(this.author);
373    }
374    }
375   
376  4156 return this.authorReference;
377    }
378   
379    /**
380    * @since 6.4M1
381    */
 
382  1383 toggle public void setAuthorReference(DocumentReference authorReference)
383    {
384  1383 if (ObjectUtils.notEqual(authorReference, getAuthorReference())) {
385  1305 setMetaDataDirty(true);
386    }
387   
388  1383 this.authorReference = authorReference;
389  1383 this.author = null;
390   
391    // Log this since it's probably a mistake so that we find who is doing bad things
392  1383 if (this.authorReference != null && this.authorReference.getName().equals(XWikiRightService.GUEST_USER)) {
393  0 LOGGER.warn("A reference to XWikiGuest user has been set instead of null. This is probably a mistake.",
394    new Exception("See stack trace"));
395    }
396    }
397   
398    /**
399    * Note that this method cannot be removed for now since it's used by Hibernate for saving a XWikiDocument.
400    *
401    * @deprecated since 6.4M1 use {@link #getAuthorReference()} instead
402    */
 
403  9534 toggle @Deprecated
404    public String getAuthor()
405    {
406  9537 if (this.author == null) {
407  4338 this.author = userReferenceToString(getAuthorReference());
408    }
409   
410  9539 return this.author != null ? this.author : "";
411    }
412   
413    /**
414    * Note that this method cannot be removed for now since it's used by Hibernate for loading a XWikiDocument.
415    *
416    * @deprecated since 6.4M1 use {@link #setAuthorReference} instead
417    */
 
418  3564 toggle @Deprecated
419    public void setAuthor(String author)
420    {
421  3564 if (!Objects.equals(getAuthor(), author)) {
422  3554 this.author = author;
423  3554 this.authorReference = null;
424   
425  3554 setMetaDataDirty(true);
426    }
427    }
428   
 
429  7819 toggle public String getVersion()
430    {
431  7819 return getRCSVersion().toString();
432    }
433   
 
434  3980 toggle public void setVersion(String version)
435    {
436  3980 this.version = new Version(version);
437    }
438   
 
439  19 toggle public String getNextVersion()
440    {
441  19 if (this.version == null) {
442  14 return "1.1";
443    } else {
444  5 return ((Version) this.version.clone()).next().toString();
445    }
446    }
447   
 
448  230271 toggle public Version getRCSVersion()
449    {
450  230270 if (this.version == null) {
451  276 return new Version("1.1");
452    }
453   
454  229995 return (Version) this.version.clone();
455    }
456   
 
457  221495 toggle public void setRCSVersion(Version version)
458    {
459  221497 this.version = version;
460    }
461   
 
462  451832 toggle public String getComment()
463    {
464  451832 return this.comment != null ? this.comment : "";
465    }
466   
 
467  225470 toggle public void setComment(String comment)
468    {
469  225467 if (ObjectUtils.notEqual(getComment(), comment)) {
470  13 setMetaDataDirty(true);
471  13 this.comment = comment;
472    }
473    }
474   
 
475  2676 toggle public XWikiDocument getDoc()
476    {
477  2676 return this.doc;
478    }
479   
 
480  224760 toggle public void setDoc(XWikiDocument doc)
481    {
482  224761 setDoc(doc, true);
483    }
484   
485    /**
486    * @param doc the document to associate to the attachment
487    * @param updateDirty false if the document metadata dirty flag should not be modified
488    * @since 9.10RC1
489    */
 
490  446962 toggle public void setDoc(XWikiDocument doc, boolean updateDirty)
491    {
492  446964 if (this.doc != doc) {
493  446075 this.doc = doc;
494  446073 this.reference = null;
495   
496  446075 if (updateDirty) {
497  224619 if (isMetaDataDirty() && doc != null) {
498  222805 doc.setMetaDataDirty(true);
499    }
500  224618 if (getAttachment_content() != null) {
501  108797 getAttachment_content().setOwnerDocument(doc);
502    }
503    }
504    }
505    }
506   
 
507  228461 toggle public Date getDate()
508    {
509  228465 return this.date;
510    }
511   
 
512  226150 toggle public void setDate(Date date)
513    {
514    // Make sure we drop milliseconds for consistency with the database
515  226151 if (date != null) {
516  226148 date.setTime((date.getTime() / 1000) * 1000);
517    }
518   
519  226150 this.date = date;
520    }
521   
 
522  3621 toggle public boolean isContentDirty()
523    {
524  3621 if (this.content == null) {
525  213 return false;
526    } else {
527  3408 return this.content.isContentDirty();
528    }
529    }
530   
 
531  662 toggle public void incrementVersion()
532    {
533  662 if (this.version == null) {
534  275 this.version = new Version("1.1");
535    } else {
536  387 this.version = this.version.next();
537    }
538    }
539   
 
540  446116 toggle public boolean isMetaDataDirty()
541    {
542  446117 return this.isMetaDataDirty;
543    }
544   
 
545  237248 toggle public void setMetaDataDirty(boolean metaDataDirty)
546    {
547  237249 this.isMetaDataDirty = metaDataDirty;
548  237249 if (metaDataDirty && this.doc != null) {
549  546 this.doc.setMetaDataDirty(true);
550    }
551    }
552   
553    /**
554    * Retrieve an attachment as an XML string. You should prefer
555    * {@link #toXML(OutputTarget, boolean, boolean, boolean, XWikiContext)} to avoid memory loads when appropriate.
556    *
557    * @param bWithAttachmentContent if true, binary content of the attachment is included (base64 encoded)
558    * @param bWithVersions if true, all archived versions are also included
559    * @param context current XWikiContext
560    * @return a string containing an XML representation of the attachment
561    * @throws XWikiException when an error occurs during wiki operations
562    */
 
563  1 toggle public String toStringXML(boolean bWithAttachmentContent, boolean bWithVersions, XWikiContext context)
564    throws XWikiException
565    {
566  1 try {
567  1 StringWriter writer = new StringWriter();
568  1 toXML(new DefaultWriterOutputTarget(writer), bWithAttachmentContent, bWithVersions, true, context);
569   
570  1 return writer.toString();
571    } catch (IOException e) {
572  0 LOGGER.error("Failed to write attachment XML", e);
573   
574  0 return "";
575    }
576    }
577   
578    /**
579    * Retrieve an attachment as an XML string. You should prefer
580    * {@link #toXML(OutputTarget, boolean, boolean, boolean, String)} to avoid memory loads when appropriate.
581    *
582    * @return a string containing an XML representation of the attachment
583    * @throws XWikiException when an error occurs during wiki operations
584    * @since 9.10RC1
585    */
 
586  0 toggle public String toXML() throws XWikiException
587    {
588  0 StringWriter writer = new StringWriter();
589  0 toXML(new DefaultWriterOutputTarget(writer), true, true, true, StandardCharsets.UTF_8.name());
590   
591  0 return writer.toString();
592    }
593   
594    /**
595    * Retrieve XML representation of attachment's metadata into an {@link Element}.
596    *
597    * @return a {@link Element} containing an XML representation of the attachment without content
598    * @throws XWikiException when an error occurs during wiki operations
599    */
 
600  0 toggle public Element toXML(XWikiContext context) throws XWikiException
601    {
602  0 return toXML(false, false, context);
603    }
604   
605    /**
606    * Write an XML representation of the attachment into an {@link com.xpn.xwiki.internal.xml.XMLWriter}
607    *
608    * @param wr the XMLWriter to write to
609    * @param bWithAttachmentContent if true, binary content of the attachment is included (base64 encoded)
610    * @param bWithVersions if true, all archive version is also included
611    * @param context current XWikiContext
612    * @throws IOException when an error occurs during streaming operation
613    * @throws XWikiException when an error occurs during xwiki operation
614    * @since 2.3M2
615    */
 
616  0 toggle public void toXML(XMLWriter wr, boolean bWithAttachmentContent, boolean bWithVersions, XWikiContext context)
617    throws IOException, XWikiException
618    {
619    // IMPORTANT: we don't use directly XMLWriter's SAX apis here because it's not really working well
620  0 DocumentResult domResult = new DocumentResult();
621   
622  0 toXML(new DefaultResultOutputTarget(domResult), bWithAttachmentContent, bWithVersions, true, context);
623   
624  0 wr.write(domResult.getDocument().getRootElement());
625    }
626   
627    /**
628    * Write an XML representation of the attachment into an {@link com.xpn.xwiki.internal.xml.XMLWriter}
629    *
630    * @param out the output where to write the XML
631    * @param bWithAttachmentContent if true, binary content of the attachment is included (base64 encoded)
632    * @param bWithVersions if true, all archive version is also included
633    * @param format true if the XML should be formated
634    * @param context current XWikiContext
635    * @throws IOException when an error occurs during streaming operation
636    * @throws XWikiException when an error occurs during xwiki operation
637    * @since 9.0RC1
638    */
 
639  1 toggle public void toXML(OutputTarget out, boolean bWithAttachmentContent, boolean bWithVersions, boolean format,
640    XWikiContext context) throws IOException, XWikiException
641    {
642  1 toXML(out, bWithAttachmentContent, bWithVersions, format, context.getWiki().getEncoding());
643    }
644   
645    /**
646    * Write an XML representation of the attachment into an {@link com.xpn.xwiki.internal.xml.XMLWriter}
647    *
648    * @param out the output where to write the XML
649    * @param bWithAttachmentContent if true, binary content of the attachment is included (base64 encoded)
650    * @param bWithVersions if true, all archive version is also included
651    * @param format true if the XML should be formated
652    * @param encoding the encoding to use when serializing XML
653    * @throws XWikiException when an error occurs during xwiki operation
654    * @since 9.10RC1
655    */
 
656  1 toggle public void toXML(OutputTarget out, boolean bWithAttachmentContent, boolean bWithVersions, boolean format,
657    String encoding) throws XWikiException
658    {
659    // Input
660  1 DocumentInstanceInputProperties documentProperties = new DocumentInstanceInputProperties();
661  1 documentProperties.setWithWikiAttachmentsContent(bWithAttachmentContent);
662  1 documentProperties.setWithJRCSRevisions(bWithVersions);
663  1 documentProperties.setWithRevisions(false);
664   
665    // Output
666  1 XAROutputProperties xarProperties = new XAROutputProperties();
667  1 xarProperties.setPreserveVersion(bWithVersions);
668  1 xarProperties.setEncoding(encoding);
669  1 xarProperties.setFormat(format);
670   
671  1 try {
672  1 Utils.getComponent(XWikiDocumentFilterUtils.class).exportEntity(this, out, xarProperties,
673    documentProperties);
674    } catch (Exception e) {
675  0 throw new XWikiException(XWikiException.MODULE_XWIKI_DOC, XWikiException.ERROR_DOC_XML_PARSING,
676    "Error parsing xml", e, null);
677    }
678    }
679   
680    /**
681    * Retrieve XML representation of attachment's metadata into an {@link Element}. You should prefer
682    * {@link #toXML(com.xpn.xwiki.internal.xml.XMLWriter, boolean, boolean, com.xpn.xwiki.XWikiContext)} to avoid
683    * memory loads when appropriate.
684    *
685    * @param bWithAttachmentContent if true, binary content of the attachment is included (base64 encoded)
686    * @param bWithVersions if true, all archived versions are also included
687    * @param context current XWikiContext
688    * @return an {@link Element} containing an XML representation of the attachment
689    * @throws XWikiException when an error occurs during wiki operations
690    * @since 2.3M2
691    */
 
692  0 toggle public Element toXML(boolean bWithAttachmentContent, boolean bWithVersions, XWikiContext context)
693    throws XWikiException
694    {
695  0 DocumentResult domResult = new DocumentResult();
696   
697  0 try {
698  0 toXML(new DefaultResultOutputTarget(domResult), bWithAttachmentContent, bWithVersions, true, context);
699    } catch (IOException e) {
700  0 throw new RuntimeException(e);
701    }
702   
703  0 return domResult.getDocument().getRootElement();
704    }
705   
 
706  1 toggle public void fromXML(String source) throws XWikiException
707    {
708  1 if (!source.isEmpty()) {
709  1 fromXML(new StringInputSource(source));
710    }
711    }
712   
713    /**
714    * @param source the XML source to parse
715    * @throws XWikiException when failing to parse the XML
716    * @since 9.0RC1
717    */
 
718  1 toggle public void fromXML(InputSource source) throws XWikiException
719    {
720  1 try {
721  1 Utils.getComponent(XWikiDocumentFilterUtils.class).importEntity(this, source);
722    } catch (Exception e) {
723  0 throw new XWikiException(XWikiException.MODULE_XWIKI_DOC, XWikiException.ERROR_DOC_XML_PARSING,
724    "Error parsing xml", e, null);
725    }
726   
727    // The setters we're calling above will set the metadata dirty flag to true since they're changing the
728    // attachment's identity. However since this method is about loading the attachment from XML it shouldn't be
729    // considered as dirty.
730  1 setMetaDataDirty(false);
731    }
732   
 
733  0 toggle public void fromXML(Element docel) throws XWikiException
734    {
735    // Serialize the Document (could not find a way to convert a dom4j Element into a usable StAX source)
736  0 StringWriter writer = new StringWriter();
737  0 try {
738  0 org.dom4j.io.XMLWriter domWriter = new org.dom4j.io.XMLWriter(writer);
739  0 domWriter.write(docel);
740  0 domWriter.flush();
741    } catch (IOException e) {
742  0 throw new XWikiException(XWikiException.MODULE_XWIKI_DOC, XWikiException.ERROR_DOC_XML_PARSING,
743    "Error parsing xml", e, null);
744    }
745   
746    // Actually parse the XML
747  0 fromXML(writer.toString());
748    }
749   
750    /**
751    * @return the type of the store used for the content
752    * @since 9.10RC1
753    */
 
754  4734 toggle public String getContentStore()
755    {
756  4736 return this.contentStore;
757    }
758   
759    /**
760    * @return true if the content store of this attachment has been explicitly set
761    * @since 9.10RC1
762    */
 
763  0 toggle public boolean isContentStoreSet()
764    {
765  0 return this.contentStoreSet;
766    }
767   
768    /**
769    * @param contentStore the type of the store used for the content
770    * @since 9.10RC1
771    */
 
772  5745 toggle public void setContentStore(String contentStore)
773    {
774  5748 this.contentStore = contentStore;
775  5748 this.contentStoreSet = true;
776    }
777   
 
778  1004932 toggle public XWikiAttachmentContent getAttachment_content()
779    {
780  1004936 return this.content;
781    }
782   
 
783  110903 toggle public void setAttachment_content(XWikiAttachmentContent attachment_content)
784    {
785  110905 this.content = attachment_content;
786  110900 if (attachment_content != null) {
787  110900 attachment_content.setOwnerDocument(this.doc);
788    }
789    }
790   
791    /**
792    * @return the type of the store used for the archive
793    * @since 9.10RC1
794    */
 
795  3221 toggle public String getArchiveStore()
796    {
797  3221 return this.archiveStore;
798    }
799   
800    /**
801    * @return true if the archive store of this attachment has been explicitly set
802    * @since 9.10RC1
803    */
 
804  671 toggle public boolean isArchiveStoreSet()
805    {
806  671 return this.archiveStoreSet;
807    }
808   
809    /**
810    * @param archiveStore the type of the store used for the archive
811    * @since 9.10RC1
812    */
 
813  4144 toggle public void setArchiveStore(String archiveStore)
814    {
815  4144 this.archiveStore = archiveStore;
816  4144 this.archiveStoreSet = true;
817    }
818   
 
819  224065 toggle public XWikiAttachmentArchive getAttachment_archive()
820    {
821  224065 return this.attachment_archive;
822    }
823   
 
824  5091 toggle public void setAttachment_archive(XWikiAttachmentArchive attachment_archive)
825    {
826  5091 this.attachment_archive = attachment_archive;
827    }
828   
829    /**
830    * Retrieve the content of this attachment as a byte array.
831    *
832    * @param xcontext current XWikiContext
833    * @return a byte array containing the binary data content of the attachment
834    * @throws XWikiException when an error occurs during wiki operation
835    * @deprecated use {@link #getContentInputStream(XWikiContext)} instead
836    */
 
837  23 toggle @Deprecated
838    public byte[] getContent(XWikiContext xcontext) throws XWikiException
839    {
840  23 InputStream inputStream = getContentInputStream(xcontext);
841   
842  23 if (inputStream != null) {
843  23 try {
844  23 return IOUtils.toByteArray(inputStream);
845    } catch (IOException e) {
846  0 throw new XWikiException("Failed to read attachment content", e);
847    } finally {
848  23 try {
849  23 inputStream.close();
850    } catch (IOException e) {
851  0 LOGGER.warn("Failed to close attachment content input stream: {}",
852    ExceptionUtils.getRootCauseMessage(e));
853    }
854    }
855    }
856   
857  0 return null;
858    }
859   
860    /**
861    * Retrieve the content of this attachment as an input stream.
862    *
863    * @param xcontext current XWikiContext
864    * @return an InputStream to consume for receiving the content of this attachment
865    * @throws XWikiException when an error occurs during wiki operation
866    * @since 2.3M2
867    */
 
868  4730 toggle public InputStream getContentInputStream(XWikiContext xcontext) throws XWikiException
869    {
870  4732 if (this.content == null && xcontext != null) {
871  1016 reloadAttachmentContent(xcontext);
872    } else {
873  3714 try {
874  3714 return getContentInputStream();
875    } catch (Exception e) {
876    // Bulletproofing: if the content object is corrupted for some reason try to reload it
877  0 reloadAttachmentContent(xcontext);
878    }
879    }
880   
881  1014 return getContentInputStream();
882    }
883   
 
884  4726 toggle private InputStream getContentInputStream()
885    {
886  4732 return this.content != null ? this.content.getContentInputStream() : null;
887    }
888   
 
889  1016 toggle private void reloadAttachmentContent(XWikiContext xcontext) throws XWikiException
890    {
891  1015 if (Objects.equals(getVersion(), getLatestStoredVersion(xcontext))) {
892    // Load the attachment content from the xwikiattachment_content table.
893  1013 loadAttachmentContent(xcontext);
894    } else {
895    // Load the attachment content from the xwikiattachment_archive table.
896    // We don't use #getAttachmentRevision() because it checks if the requested version equals the version
897    // of the target attachment (XWIKI-1938).
898  2 XWikiAttachment archivedVersion = loadArchive(xcontext).getRevision(this, getVersion(), xcontext);
899  2 XWikiAttachmentContent archivedContent =
900  2 archivedVersion != null ? archivedVersion.getAttachment_content() : null;
901  2 if (archivedContent != null) {
902  2 setAttachment_content(archivedContent);
903    } else {
904    // Fall back on the version of the content stored in the xwikiattachment_content table.
905  0 loadAttachmentContent(xcontext);
906    }
907    }
908    }
909   
910    /**
911    * The {@code xwikiattachment_content} table stores only the latest version of an attachment (which is identified by
912    * the attachment file name and the owner document reference). The rest of the attachment versions are stored in
913    * {@code xwikiattachment_archive} table. This method can be used to determine from where to load the attachment
914    * content.
915    *
916    * @param context the XWiki context
917    * @return the latest stored version of this attachment
918    */
 
919  1016 toggle private String getLatestStoredVersion(XWikiContext context)
920    {
921  1016 try {
922  1016 XWikiDocument ownerDocumentLastestVersion =
923    context.getWiki().getDocument(this.doc.getDocumentReference(), context);
924  1015 XWikiAttachment latestStoredVersion = ownerDocumentLastestVersion.getAttachment(this.filename);
925   
926  1015 return latestStoredVersion != null ? latestStoredVersion.getVersion() : null;
927    } catch (XWikiException e) {
928  0 LOGGER.warn(ExceptionUtils.getRootCauseMessage(e));
929  0 return null;
930    }
931    }
932   
933    /**
934    * @deprecated since 2.6M1 please do not use this, it is bound to a jrcs based implementation.
935    */
 
936  0 toggle @Deprecated
937    public Archive getArchive()
938    {
939  0 if (this.attachment_archive == null) {
940  0 return null;
941    } else {
942  0 return this.attachment_archive.getRCSArchive();
943    }
944    }
945   
946    /**
947    * @deprecated since 2.6M1 please do not use this, it is bound to a jrcs based implementation.
948    */
 
949  0 toggle @Deprecated
950    public void setArchive(Archive archive)
951    {
952  0 if (this.attachment_archive == null) {
953  0 this.attachment_archive = new XWikiAttachmentArchive();
954  0 this.attachment_archive.setAttachment(this);
955    }
956   
957  0 this.attachment_archive.setRCSArchive(archive);
958    }
959   
 
960  2 toggle public void setArchive(String data) throws XWikiException
961    {
962  2 if (this.attachment_archive == null) {
963  2 this.attachment_archive = new XWikiAttachmentArchive();
964  2 this.attachment_archive.setAttachment(this);
965    }
966   
967  2 this.attachment_archive.setArchive(data);
968    }
969   
 
970  4 toggle public synchronized Version[] getVersions()
971    {
972  4 try {
973  4 return getAttachment_archive().getVersions();
974    } catch (Exception ex) {
975  0 LOGGER.warn("Cannot retrieve versions of attachment [{}@{}]: {}",
976    new Object[] { getFilename(), getDoc().getDocumentReference(), ex.getMessage() });
977  0 return new Version[] { new Version(this.getVersion()) };
978    }
979    }
980   
981    /**
982    * Get the list of all versions up to the current. We assume versions go from 1.1 to the current one This allows not
983    * to read the full archive file.
984    *
985    * @return a list of Version from 1.1 to the current version.
986    * @throws XWikiException never happens.
987    */
 
988  3 toggle public List<Version> getVersionList() throws XWikiException
989    {
990  3 final List<Version> list = new ArrayList<Version>();
991  3 final String currentVersion = this.version.toString();
992  3 Version v = new Version("1.1");
993  3 for (;;) {
994  6 list.add(v);
995  6 if (v.toString().equals(currentVersion)) {
996  3 break;
997    }
998  3 v = v.next();
999    }
1000   
1001  3 return list;
1002    }
1003   
1004    /**
1005    * Set the content of an attachment from a byte array.
1006    *
1007    * @param data a byte array with the binary content of the attachment
1008    * @deprecated use {@link #setContent(java.io.InputStream, int)} instead
1009    */
 
1010  15 toggle @Deprecated
1011    public void setContent(byte[] data)
1012    {
1013  15 if (this.content == null) {
1014  11 this.content = new XWikiAttachmentContent();
1015  11 this.content.setAttachment(this);
1016    }
1017   
1018  15 this.content.setContent(data);
1019  15 this.size = this.content.getLongSize();
1020    }
1021   
1022    /**
1023    * Set the content of an attachment from an InputStream.
1024    *
1025    * @param is the input stream that will be read
1026    * @param length the length in byte to read
1027    * @throws IOException when an error occurs during streaming operation
1028    * @since 2.3M2
1029    */
 
1030  1 toggle public void setContent(InputStream is, int length) throws IOException
1031    {
1032  1 if (this.content == null) {
1033  1 this.content = new XWikiAttachmentContent();
1034  1 this.content.setAttachment(this);
1035    }
1036   
1037  1 this.content.setContent(is, length);
1038  1 this.size = this.content.getLongSize();
1039    }
1040   
1041    /**
1042    * Set the content of the attachment from an InputStream.
1043    *
1044    * @param is the input stream that will be read
1045    * @throws IOException when an error occurs during streaming operation
1046    * @since 2.6M1
1047    */
 
1048  1152 toggle public void setContent(InputStream is) throws IOException
1049    {
1050  1152 if (this.content == null) {
1051  1148 this.content = new XWikiAttachmentContent(this);
1052    }
1053   
1054  1152 this.content.setContent(is);
1055  1152 this.size = this.content.getLongSize();
1056    }
1057   
 
1058  1694 toggle public void loadAttachmentContent(XWikiContext xcontext) throws XWikiException
1059    {
1060  1695 if (this.content == null) {
1061  1541 WikiReference currentWiki = xcontext.getWikiReference();
1062   
1063  1540 try {
1064    // Make sure we work on the attachment's wiki
1065  1541 WikiReference attachmentWiki = getReference().getDocumentReference().getWikiReference();
1066  1540 if (attachmentWiki != null) {
1067  1539 xcontext.setWikiReference(attachmentWiki);
1068    }
1069   
1070  1541 try {
1071  1540 XWikiAttachmentStoreInterface store = getAttachmentContentStore(xcontext);
1072   
1073  1539 store.loadAttachmentContent(this, xcontext, true);
1074    } catch (ComponentLookupException e) {
1075  0 throw new XWikiException("Failed to find store for attachment [" + getReference() + "]", e);
1076    }
1077    } finally {
1078  1539 if (currentWiki != null) {
1079  1538 xcontext.setWikiReference(currentWiki);
1080    }
1081    }
1082    }
1083    }
1084   
1085    /**
1086    * @deprecated since 9.11RC1, use {@link #loadAttachmentContent(XWikiContext)} instead
1087    */
 
1088  0 toggle @Deprecated
1089    public void loadContent(XWikiContext xcontext)
1090    {
1091  0 try {
1092  0 loadAttachmentContent(xcontext);
1093    } catch (Exception e) {
1094  0 LOGGER.error(
1095    "Failed to load content for attachment [{}@{}]. "
1096    + "This attachment is broken, please consider re-uploading it",
1097  0 this.doc != null ? this.doc.getDocumentReference() : "<unknown>", getFilename(), e);
1098    }
1099    }
1100   
 
1101  177 toggle public XWikiAttachmentArchive loadArchive(XWikiContext xcontext)
1102    {
1103  177 if (this.attachment_archive == null) {
1104  141 WikiReference currentWiki = xcontext.getWikiReference();
1105   
1106  141 try {
1107    // Make sure we work on the attachment's wiki
1108  141 WikiReference attachmentWiki = getReference().getDocumentReference().getWikiReference();
1109  141 if (attachmentWiki != null) {
1110  141 xcontext.setWikiReference(attachmentWiki);
1111    }
1112   
1113  141 try {
1114  141 AttachmentVersioningStore store = getAttachmentVersioningStore(xcontext);
1115   
1116  141 this.attachment_archive = store.loadArchive(this, xcontext, true);
1117    } catch (Exception e) {
1118  0 LOGGER.warn(
1119    "Failed to load archive for attachment [{}@{}]. "
1120    + "This attachment is broken, please consider re-uploading it",
1121  0 this.doc != null ? this.doc.getDocumentReference() : "<unknown>", getFilename(), e);
1122    }
1123    } finally {
1124  141 if (currentWiki != null) {
1125  141 xcontext.setWikiReference(currentWiki);
1126    }
1127    }
1128    }
1129   
1130  177 return this.attachment_archive;
1131    }
1132   
 
1133  78 toggle public void updateContentArchive(XWikiContext context) throws XWikiException
1134    {
1135  78 if (this.content == null) {
1136  0 return;
1137    }
1138   
1139  78 loadArchive(context).updateArchive(context);
1140    }
1141   
1142    /**
1143    * Return the stored media type. If none is stored try to detects the media type of this attachment's content using
1144    * {@link Tika}. We first try to determine the media type based on the file name extension and if the extension is
1145    * unknown we try to determine the media type by reading the first bytes of the attachment content.
1146    *
1147    * @param xcontext the XWiki context
1148    * @return the media type of this attachment's content
1149    */
 
1150  4971 toggle public String getMimeType(XWikiContext xcontext)
1151    {
1152  4974 String type = getMimeType();
1153   
1154  4976 if (StringUtils.isEmpty(type)) {
1155  921 type = extractMimeType(xcontext);
1156    }
1157   
1158  4973 return type;
1159    }
1160   
1161    /**
1162    * Return the stored media type.
1163    *
1164    * @return the media type of this attachment's content
1165    * @since 7.1M1
1166    */
 
1167  231066 toggle public String getMimeType()
1168    {
1169  231076 return this.mimeType;
1170    }
1171   
1172    /**
1173    * @param mimeType the explicit mime type of the file
1174    * @since 7.1M1
1175    */
 
1176  226062 toggle public void setMimeType(String mimeType)
1177    {
1178  226063 this.mimeType = mimeType;
1179    }
1180   
1181    /**
1182    * Extract the mime type from the file name and content and remember it to be stored.
1183    *
1184    * @param xcontext the {@link XWikiContext} use to load the content if it's not already loaded
1185    * @since 7.1M1
1186    */
 
1187  20 toggle public void resetMimeType(XWikiContext xcontext)
1188    {
1189  20 this.mimeType = extractMimeType(xcontext);
1190    }
1191   
 
1192  941 toggle private String extractMimeType(XWikiContext xcontext)
1193    {
1194    // We try name-based detection and then fall back on content-based detection. We don't do this in a single step,
1195    // by passing both the content and the file name to Tika, because the default detector looks at the content
1196    // first which can be an issue for large attachments. Our approach is less accurate but has better performance.
1197  941 String mediaType = getFilename() != null ? TikaUtils.detect(getFilename()) : MediaType.OCTET_STREAM.toString();
1198  941 if (MediaType.OCTET_STREAM.toString().equals(mediaType)) {
1199  20 try {
1200    // Content-based detection is more accurate but it may require loading the attachment content in memory
1201    // (from the database) if it hasn't been cached as a temporary file yet. This can be an issue for large
1202    // attachments when database storage is used. Only the first bytes are normally read but still this
1203    // process is slower than name-based detection.
1204    //
1205    // We wrap the content input stream in a BufferedInputStream to make sure that all the detectors can
1206    // read the content even if the input stream is configured to auto close when it reaches the end. This
1207    // can happen for small files if AutoCloseInputStream is used, which supports the mark and reset methods
1208    // so Tika uses it directly. In this case, the input stream is automatically closed after the first
1209    // detector reads it so the next detector fails to read it.
1210  20 mediaType = TikaUtils.detect(new BufferedInputStream(getContentInputStream(xcontext)));
1211    } catch (Exception e) {
1212  5 LOGGER.warn("Failed to read the content of [{}] in order to detect its mime type.", getReference());
1213    }
1214    }
1215   
1216  941 return mediaType;
1217    }
1218   
1219    /**
1220    * @return charset the character encoding associated with the attachment content
1221    * @since 10.11RC1
1222    */
 
1223  3504 toggle @Unstable
1224    public String getCharset()
1225    {
1226  3509 return this.charset;
1227    }
1228   
1229    /**
1230    * @param charset the character encoding associated with the attachment content
1231    * @since 10.11RC1
1232    */
 
1233  4534 toggle @Unstable
1234    public void setCharset(String charset)
1235    {
1236  4534 this.charset = charset;
1237    }
1238   
 
1239  2581 toggle public boolean isImage(XWikiContext context)
1240    {
1241  2581 String contenttype = getMimeType(context);
1242  2581 if (contenttype.startsWith("image/")) {
1243  2500 return true;
1244    } else {
1245  81 return false;
1246    }
1247    }
1248   
 
1249  18 toggle public XWikiAttachment getAttachmentRevision(String rev, XWikiContext context) throws XWikiException
1250    {
1251  18 if (StringUtils.equals(rev, this.getVersion())) {
1252  8 return this;
1253    }
1254   
1255  10 return loadArchive(context).getRevision(this, rev, context);
1256    }
1257   
1258    /**
1259    * Apply the provided attachment so that the current one contains the same informations and indicate if it was
1260    * necessary to modify it in any way.
1261    *
1262    * @param attachment the attachment to apply
1263    * @return true if the attachment has been modified
1264    * @since 5.3M2
1265    */
 
1266  110 toggle public boolean apply(XWikiAttachment attachment)
1267    {
1268  110 boolean modified = false;
1269   
1270  110 if (getLongSize() != attachment.getLongSize()) {
1271  2 setLongSize(attachment.getLongSize());
1272  2 modified = true;
1273    }
1274   
1275  110 if (StringUtils.equals(getMimeType(), attachment.getMimeType())) {
1276  49 setMimeType(attachment.getMimeType());
1277  49 modified = true;
1278    }
1279   
1280  110 try {
1281  110 if (!IOUtils.contentEquals(getContentInputStream(null), attachment.getContentInputStream(null))) {
1282  2 setContent(attachment.getContentInputStream(null));
1283  2 modified = true;
1284    }
1285    } catch (Exception e) {
1286  0 LOGGER.error("Failed to compare content of attachments", e);
1287    }
1288   
1289  110 return modified;
1290    }
1291   
 
1292  487 toggle public boolean equalsData(XWikiAttachment otherAttachment, XWikiContext xcontext) throws XWikiException
1293    {
1294  487 try {
1295  487 if (getContentLongSize(xcontext) == otherAttachment.getContentLongSize(xcontext)) {
1296  484 InputStream is = getContentInputStream(xcontext);
1297   
1298  484 try {
1299  484 InputStream otherIS = otherAttachment.getContentInputStream(xcontext);
1300   
1301  484 try {
1302  484 return IOUtils.contentEquals(is, otherIS);
1303    } finally {
1304  484 otherIS.close();
1305    }
1306    } finally {
1307  484 is.close();
1308    }
1309    }
1310    } catch (Exception e) {
1311  0 throw new XWikiException(XWikiException.MODULE_XWIKI_DOC, XWikiException.ERROR_XWIKI_UNKNOWN,
1312    "Failed to compare attachments", e);
1313    }
1314   
1315  3 return false;
1316    }
1317   
 
1318  1 toggle public void merge(XWikiAttachment previousAttachment, XWikiAttachment nextAttachment,
1319    MergeConfiguration configuration, XWikiContext xcontext, MergeResult mergeResult)
1320    {
1321  1 try {
1322  1 if (equalsData(previousAttachment, xcontext)) {
1323  1 this.apply(nextAttachment);
1324  1 mergeResult.setModified(true);
1325    } else {
1326  0 if (this.equals(nextAttachment)) {
1327    // Already modified as expected in the DB, lets assume the user is prescient
1328  0 mergeResult.getLog().warn("Attachment [{}] already modified", getReference());
1329    }
1330    }
1331    } catch (Exception e) {
1332  0 mergeResult.getLog().error("Failed to merge attachment [{}]", this, e);
1333    }
1334    }
1335   
1336    /**
1337    * @param userReference the user {@link DocumentReference} to convert to {@link String}
1338    * @return the user as String
1339    */
 
1340  4338 toggle private String userReferenceToString(DocumentReference userReference)
1341    {
1342  4338 String userString;
1343   
1344  4338 if (userReference != null) {
1345  743 userString = getCompactWikiEntityReferenceSerializer().serialize(userReference, getReference());
1346    } else {
1347  3595 userString = XWikiRightService.GUEST_USER_FULLNAME;
1348    }
1349   
1350  4338 return userString;
1351    }
1352   
1353    /**
1354    * @param userString the user {@link String} to convert to {@link DocumentReference}
1355    * @return the user as {@link DocumentReference}
1356    */
 
1357  5346 toggle private DocumentReference userStringToReference(String userString)
1358    {
1359  5346 DocumentReference userReference;
1360   
1361  5346 if (StringUtils.isEmpty(userString)) {
1362  4705 userReference = null;
1363    } else {
1364  641 userReference = getExplicitReferenceDocumentReferenceResolver()
1365    .resolve(getXClassEntityReferenceResolver().resolve(userString, EntityType.DOCUMENT), getReference());
1366   
1367  641 if (userReference.getName().equals(XWikiRightService.GUEST_USER)) {
1368  2 userReference = null;
1369    }
1370    }
1371