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

File FilesystemAttachmentRecycleBinStore.java

 

Coverage histogram

../../../../../../img/srcFileCovDistChart0.png
83% of files have more coverage

Code metrics

24
98
16
2
531
318
33
0.34
6.12
8
2.06

Classes

Class Line # Actions
FilesystemAttachmentRecycleBinStore 71 97 0% 32 136
0.00%
FilesystemAttachmentRecycleBinStore.NewestFirstDateComparitor 518 1 0% 1 2
0.00%
 

Contributing tests

No tests hitting this source file were found.

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.store.legacy.store.internal;
21   
22    import java.io.File;
23    import java.io.FileInputStream;
24    import java.io.IOException;
25    import java.util.ArrayList;
26    import java.util.Collections;
27    import java.util.Comparator;
28    import java.util.Date;
29    import java.util.List;
30    import java.util.Map;
31    import java.util.concurrent.ConcurrentHashMap;
32    import java.util.concurrent.locks.ReadWriteLock;
33   
34    import javax.inject.Inject;
35    import javax.inject.Named;
36    import javax.inject.Singleton;
37   
38    import org.xwiki.component.annotation.Component;
39    import org.xwiki.component.phase.Initializable;
40    import org.xwiki.component.phase.InitializationException;
41    import org.xwiki.model.reference.DocumentReference;
42    import org.xwiki.model.reference.DocumentReferenceResolver;
43    import org.xwiki.store.FileDeleteTransactionRunnable;
44    import org.xwiki.store.FileSaveTransactionRunnable;
45    import org.xwiki.store.StartableTransactionRunnable;
46    import org.xwiki.store.filesystem.internal.DeletedAttachmentFileProvider;
47    import org.xwiki.store.filesystem.internal.FilesystemStoreTools;
48    import org.xwiki.store.legacy.doc.internal.DeletedFilesystemAttachment;
49    import org.xwiki.store.legacy.doc.internal.FilesystemAttachmentContent;
50    import org.xwiki.store.legacy.doc.internal.MutableDeletedFilesystemAttachment;
51    import org.xwiki.store.serialization.SerializationStreamProvider;
52    import org.xwiki.store.serialization.Serializer;
53   
54    import com.xpn.xwiki.XWikiContext;
55    import com.xpn.xwiki.XWikiException;
56    import com.xpn.xwiki.doc.DeletedAttachment;
57    import com.xpn.xwiki.doc.XWikiAttachment;
58    import com.xpn.xwiki.doc.XWikiDocument;
59    import com.xpn.xwiki.store.AttachmentRecycleBinStore;
60    import com.xpn.xwiki.store.AttachmentVersioningStore;
61   
62    /**
63    * Realization of {@link AttachmentRecycleBinStore} for filesystem storage.
64    *
65    * @version $Id: 2283f770740f5f36091e76b4bf9225b884ddda55 $
66    * @since 3.0M3
67    */
68    @Component
69    @Named("file")
70    @Singleton
 
71    public class FilesystemAttachmentRecycleBinStore implements AttachmentRecycleBinStore, Initializable
72    {
73    /**
74    * Some utilities for getting attachment files, locks, and backup files.
75    */
76    @Inject
77    private FilesystemStoreTools fileTools;
78   
79    /**
80    * A serializer for the archive metadata.
81    */
82    @Inject
83    @Named("attachment-list-meta/1.0")
84    private Serializer<List<XWikiAttachment>, List<XWikiAttachment>> versionSerializer;
85   
86    /**
87    * Used to parse and serialize deleted attachment metadata when loading and storing.
88    */
89    @Inject
90    @Named("deleted-attachment-meta/1.0")
91    private Serializer<DeletedAttachment, MutableDeletedFilesystemAttachment> deletedAttachmentSerializer;
92   
93    /**
94    * This is needed in order to be able to map the database ids given by the
95    * user to meaningful paths to deleted attachments.
96    */
97    @Inject
98    @Named("deleted-attachment-id-mappings/1.0")
99    private Serializer<Map<Long, String>, Map<Long, String>> attachmentIdMappingSerializer;
100   
101    /**
102    * Used to store the versions of the deleted attachment.
103    */
104    @Inject
105    @Named("file")
106    private AttachmentVersioningStore attachmentVersionStore;
107   
108    /**
109    * Used to extract the {@link DocumentReference} from the path to the deleted attachment.
110    */
111    @Inject
112    @Named("path")
113    private DocumentReferenceResolver<String> pathDocumentReferenceResolver;
114   
115    /**
116    * This is required because deleted attachments may be looked up by a database id number
117    * So we are forced to simulate the database id numbering scheme even though they
118    * are and should be stored with their documents which are stored by name.
119    */
120    private final Map<Long, String> pathById = new ConcurrentHashMap<Long, String>();
121   
122    /**
123    * The location to persist the pathById map.
124    */
125    private File pathByIdStore;
126   
127    /**
128    * Load the pathById mappings so that attachments can be loaded by database id.
129    *
130    * @throws InitializationException if the mapping file cannot be parsed.
131    */
 
132  0 toggle @Override
133    public void initialize() throws InitializationException
134    {
135    // make sure we have a FilesystemAttachmentVersioningStore.
136  0 if (!(attachmentVersionStore instanceof FilesystemAttachmentVersioningStore)) {
137  0 throw new InitializationException("Wrong attachment versioning store registered under hint "
138    + "'file', expecting a FilesystemAttachmentVersioningStore");
139    }
140   
141  0 this.pathByIdStore = this.fileTools.getGlobalFile("DELETED_ATTACHMENT_ID_MAPPINGS.xml");
142  0 if (pathByIdStore.exists()) {
143  0 try {
144  0 this.pathById.putAll(
145    attachmentIdMappingSerializer.parse(new FileInputStream(this.pathByIdStore)));
146    } catch (IOException e) {
147  0 throw new InitializationException("Failed to parse deleted attachment id mappings.", e);
148    }
149    }
150    }
151   
 
152  0 toggle @Override
153    public void saveToRecycleBin(final XWikiAttachment attachment,
154    final String deleter,
155    final Date deleteDate,
156    final XWikiContext context,
157    final boolean bTransaction) throws XWikiException
158    {
159  0 final DeletedFilesystemAttachment dfa =
160    new DeletedFilesystemAttachment(attachment, deleter, deleteDate);
161  0 final StartableTransactionRunnable tr = this.getSaveTrashAttachmentRunnable(dfa, context);
162   
163    // Need to add the ID to the map and persist the map
164    // otherwise the attachment will not be able to loaded by the ID.
165    // TODO standardize a deleted attachment entity reference and deprecate the use of a long integer
166    // as a key to load a deleted attachment with.
167  0 final String absolutePath =
168    this.fileTools.getDeletedAttachmentFileProvider(attachment, deleteDate)
169    .getAttachmentContentFile().getParentFile().getAbsolutePath();
170  0 final String path =
171    absolutePath.substring(absolutePath.indexOf(this.fileTools.getStorageLocationPath()));
172  0 final Long id = Long.valueOf(dfa.getId());
173  0 (new StartableTransactionRunnable()
174    {
 
175  0 toggle public void onRun()
176    {
177  0 pathById.put(id, path);
178    }
179   
 
180  0 toggle public void onRollback()
181    {
182  0 pathById.remove(id);
183    }
184    }).runIn(tr);
185   
186    // Need to save the updated map right away in case the power goes out or something.
187  0 new FileSaveTransactionRunnable(
188    this.pathByIdStore,
189    this.fileTools.getTempFile(this.pathByIdStore),
190    this.fileTools.getBackupFile(this.pathByIdStore),
191    this.fileTools.getLockForFile(this.pathByIdStore),
192    new SerializationStreamProvider<Map<Long, String>>(
193    this.attachmentIdMappingSerializer,
194    this.pathById)).runIn(tr);
195   
196  0 try {
197  0 tr.start();
198    } catch (Exception e) {
199  0 throw new XWikiException(XWikiException.MODULE_XWIKI_STORE,
200    XWikiException.MODULE_XWIKI,
201    "Failed to store deleted attachment " + attachment.getFilename()
202    + " for document: " + attachment.getDoc().getDocumentReference(), e);
203    }
204    }
205   
206    /**
207    * Get a StartableTransactionRunnable to save an attachment in the recycle-bin.
208    *
209    * @param deleted the FilesystemDeletedAttachment to save.
210    * @param context the legacy XWikiContext which might be needed to get the content
211    * from the attachment, or to load the attachment versioning store.
212    * @return a TransactionRunnable for storing the deleted attachment.
213    * @throws XWikiException if one is thrown trying to get data from the attachment
214    * or loading the attachment archive in order to save it in the deleted section.
215    */
 
216  0 toggle public StartableTransactionRunnable
217    getSaveTrashAttachmentRunnable(final DeletedFilesystemAttachment deleted,
218    final XWikiContext context)
219    throws XWikiException
220    {
221  0 final DeletedAttachmentFileProvider provider =
222    this.fileTools.getDeletedAttachmentFileProvider(deleted.getAttachment(), deleted.getDate());
223   
224  0 return new SaveTrashAttachmentRunnable(deleted,
225    provider,
226    this.fileTools,
227    this.deletedAttachmentSerializer,
228    this.versionSerializer,
229    context);
230    }
231   
232    /**
233    * {@inheritDoc}
234    * <p>
235    * bTransaction is ignored by this implementation.
236    * </p>
237    *
238    * @see AttachmentRecycleBinStore#restoreFromRecycleBin(XWikiAttachment, long, XWikiContext, boolean)
239    */
 
240  0 toggle @Override
241    public XWikiAttachment restoreFromRecycleBin(final XWikiAttachment attachment,
242    final long index,
243    final XWikiContext context,
244    boolean bTransaction) throws XWikiException
245    {
246  0 final DeletedAttachment delAttach = getDeletedAttachment(index, context, false);
247  0 return delAttach != null ? delAttach.restoreAttachment(attachment, context) : null;
248    }
249   
250    /**
251    * {@inheritDoc}
252    * <p>
253    * bTransaction is ignored by this implementation.
254    * context is unused and may safely be null.
255    * </p>
256    *
257    * @see AttachmentRecycleBinStore#getDeletedAttachment(long, XWikiContext, boolean)
258    */
 
259  0 toggle @Override
260    public DeletedAttachment getDeletedAttachment(final long index,
261    final XWikiContext context,
262    final boolean bTransaction) throws XWikiException
263    {
264  0 final String path = this.pathById.get(Long.valueOf(index));
265  0 if (path == null) {
266  0 return null;
267    }
268   
269  0 try {
270  0 return this.deletedAttachmentFromProvider(this.fileTools.getDeletedAttachmentFileProvider(path), context);
271    } catch (IOException e) {
272  0 throw new XWikiException(XWikiException.MODULE_XWIKI_STORE,
273    XWikiException.MODULE_XWIKI,
274    "Failed to get deleted attachment at index " + index
275    + " with filesystem path " + path, e);
276    }
277    }
278   
279    /**
280    * {@inheritDoc}
281    * <p>
282    * bTransaction is ignored by this implementation.
283    * </p>
284    *
285    * @see AttachmentRecycleBinStore#getAllDeletedAttachments(XWikiAttachment, XWikiContext, boolean)
286    */
 
287  0 toggle @Override
288    public List<DeletedAttachment> getAllDeletedAttachments(final XWikiAttachment attachment,
289    final XWikiContext context,
290    final boolean bTransaction)
291    throws XWikiException
292    {
293  0 if (attachment.getDoc() == null) {
294  0 throw new XWikiException(XWikiException.MODULE_XWIKI_STORE,
295    XWikiException.MODULE_XWIKI,
296    "Cannot load deleted attachments because the given attachment "
297    + attachment.getFilename() + " is not attached to any document.");
298    }
299   
300    // I don't know that there is no way to upload an attachment named ""
301    // so I don't want to use isEmpty here.
302  0 if (attachment.getFilename() == null) {
303  0 return this.getAllDeletedAttachments(attachment.getDoc(), context, false);
304    }
305   
306  0 final Map<Date, DeletedAttachmentFileProvider> attachMap =
307    this.fileTools.deletedAttachmentsForDocument(attachment.getDoc().getDocumentReference())
308    .get(attachment.getFilename());
309   
310    // There may not be any deleted versions matching the requested attachment filename.
311  0 if (attachMap == null) {
312  0 return Collections.<DeletedAttachment>emptyList();
313    }
314   
315  0 final List<Date> deleteDatesList = new ArrayList<Date>(attachMap.keySet());
316  0 Collections.sort(deleteDatesList, NewestFirstDateComparitor.INSTANCE);
317   
318  0 final List<DeletedAttachment> out = new ArrayList<DeletedAttachment>(deleteDatesList.size());
319  0 try {
320  0 for (Date date : deleteDatesList) {
321  0 out.add(this.deletedAttachmentFromProvider(attachMap.get(date), context));
322    }
323    } catch (IOException e) {
324  0 throw new XWikiException(XWikiException.MODULE_XWIKI_STORE,
325    XWikiException.MODULE_XWIKI,
326    "Failed to get deleted attachment " + attachment.getFilename()
327    + " attached to the document: "
328    + attachment.getDoc().getDocumentReference(), e);
329    }
330  0 return out;
331    }
332   
333    /**
334    * {@inheritDoc}
335    * <p>
336    * bTransaction is ignored by this implementation.
337    * context is unused and may safely be null.
338    * </p>
339    *
340    * @see AttachmentRecycleBinStore#getAllDeletedAttachments(XWikiDocument, XWikiContext, boolean)
341    */
 
342  0 toggle @Override
343    public List<DeletedAttachment> getAllDeletedAttachments(final XWikiDocument doc,
344    final XWikiContext context,
345    final boolean bTransaction)
346    throws XWikiException
347    {
348  0 final Map<String, Map<Date, DeletedAttachmentFileProvider>> attachMap =
349    this.fileTools.deletedAttachmentsForDocument(doc.getDocumentReference());
350   
351    // Get a list of dates ordered by newest first.
352  0 final List<Date> allDates = new ArrayList<Date>();
353  0 for (Map<Date, ?> dateMap : attachMap.values()) {
354  0 allDates.addAll(dateMap.keySet());
355    }
356  0 Collections.sort(allDates, NewestFirstDateComparitor.INSTANCE);
357   
358    // Populate the output list by the order of the date.
359    // Everything cannot be placed into an ordered map because it is conceivable that 2 attachments
360    // would be deleted in the same millisecond and that would cause them to be merged.
361  0 try {
362  0 final List<DeletedAttachment> out = new ArrayList<DeletedAttachment>(allDates.size());
363  0 for (Date date : allDates) {
364  0 for (Map<Date, DeletedAttachmentFileProvider> map : attachMap.values()) {
365  0 if (map.get(date) != null) {
366  0 out.add(this.deletedAttachmentFromProvider(map.get(date), context));
367  0 map.remove(date);
368  0 break;
369    }
370    }
371    }
372  0 return out;
373    } catch (IOException e) {
374  0 throw new XWikiException(XWikiException.MODULE_XWIKI_STORE,
375    XWikiException.MODULE_XWIKI,
376    "Failed to get deleted attachments for document: "
377    + doc.getDocumentReference(), e);
378    }
379    }
380   
381    /**
382    * {@inheritDoc}
383    * <p>
384    * bTransaction is ignored because the filesystem cannot synchronize with the database commit.
385    * TODO: make getDeletedAttachmentPurgeRunnable public so that a transaction safe method is available.
386    * context is unused and may safely be null.
387    * </p>
388    *
389    * @see AttachmentRecycleBinStore#deleteFromRecycleBin(long, XWikiContext, boolean)
390    */
 
391  0 toggle @Override
392    public void deleteFromRecycleBin(final long index,
393    final XWikiContext context,
394    final boolean bTransaction)
395    throws XWikiException
396    {
397  0 final String path = this.pathById.get(Long.valueOf(index));
398  0 if (path != null) {
399  0 this.getDeletedAttachmentPurgeRunnable(this.fileTools.getDeletedAttachmentFileProvider(path));
400    }
401    }
402   
403    /**
404    * Get a TransactionRunnable for removing a deleted attachment from the filesystem entirely.
405    * TODO: Standardize an EntityReference for deleted attachments and make that the parameter.
406    *
407    * @param provider the file provider for the deleted attachment to purge from the recycle bin.
408    * @return a StartableTransactionRunnable for removing the attachment.
409    */
 
410  0 toggle private StartableTransactionRunnable getDeletedAttachmentPurgeRunnable(
411    final DeletedAttachmentFileProvider provider)
412    {
413  0 final StartableTransactionRunnable out = new StartableTransactionRunnable();
414  0 final File deletedAttachDir = provider.getDeletedAttachmentMetaFile().getParentFile();
415  0 if (!deletedAttachDir.exists()) {
416    // No such dir, return a do-nothing runnable.
417  0 return out;
418    }
419    // Easy thing to do is just delete everything in the deleted-attachment directory.
420  0 for (File toDelete : deletedAttachDir.listFiles()) {
421  0 new FileDeleteTransactionRunnable(toDelete,
422    this.fileTools.getBackupFile(toDelete),
423    this.fileTools.getLockForFile(toDelete)).runIn(out);
424    }
425   
426    // Remove the entry from the pathById map so that it doesn't cause a memory leak.
427  0 final String path = deletedAttachDir.getAbsolutePath();
428  0 for (final Long id : pathById.keySet()) {
429  0 if (pathById.get(id).endsWith(path)) {
430  0 (new StartableTransactionRunnable()
431    {
 
432  0 toggle public void onRun()
433    {
434  0 pathById.remove(id);
435    }
436   
 
437  0 toggle public void onRollback()
438    {
439  0 pathById.put(id, path);
440    }
441    }).runIn(out);
442  0 break;
443    }
444    }
445   
446  0 return out;
447    }
448   
449    /**
450    * Get a deleted attachment by it's filesystem location. This returns a DeletedAttachment which is not attached to
451    * any document! It is the job of the caller to get the attachment and any version of it and attach them to a
452    * document.
453    *
454    * @param provider a means to get the files which store the deleted attachment content and metadata.
455    * @param context the XWiki context
456    * @return the deleted attachment for that directory.
457    * @throws IOException if deserialization fails or there is a problem loading the archive.
458    * @throws XWikiException if we fail to load the document associated with the deleted attachment (the document that
459    * was holding the attachment before it was deleted)
460    */
 
461  0 toggle private DeletedAttachment deletedAttachmentFromProvider(final DeletedAttachmentFileProvider provider,
462    final XWikiContext context) throws IOException, XWikiException
463    {
464  0 final File deletedMeta = provider.getDeletedAttachmentMetaFile();
465   
466    // No metadata, no deleted attachment.
467  0 if (!deletedMeta.exists()) {
468  0 return null;
469    }
470   
471  0 final MutableDeletedFilesystemAttachment delAttach;
472  0 ReadWriteLock lock = this.fileTools.getLockForFile(deletedMeta);
473  0 lock.readLock().lock();
474  0 try {
475  0 delAttach = this.deletedAttachmentSerializer.parse(new FileInputStream(deletedMeta));
476    } finally {
477  0 lock.readLock().unlock();
478    }
479   
480    // Bind the deleted attachment to the associated document in order to be able to restore it.
481  0 DocumentReference documentReference = getDocumentReference(provider);
482  0 delAttach.getAttachment().setDoc(context.getWiki().getDocument(documentReference, context));
483   
484  0 final File contentFile = provider.getAttachmentContentFile();
485  0 final XWikiAttachment attachment = delAttach.getAttachment();
486  0 attachment.setAttachment_content(new FilesystemAttachmentContent(contentFile, attachment));
487   
488  0 attachment.setAttachment_archive(
489    ((FilesystemAttachmentVersioningStore) this.attachmentVersionStore)
490    .loadArchive(attachment, provider));
491   
492  0 return delAttach.getImmutable();
493    }
494   
495    /**
496    * FIXME: This method works with the default implementation of {@link FilesystemStoreTools}. It should probably be
497    * moved there but then we need to add a new method to the {@link FilesystemStoreTools} interface, thus breaking
498    * backward compatibility.
499    *
500    * @param provider a means to get the files which store the deleted attachment content and metadata
501    * @return the reference to the document that was holding the deleted attachment
502    */
 
503  0 toggle private DocumentReference getDocumentReference(DeletedAttachmentFileProvider provider)
504    {
505  0 String absolutePath = provider.getDeletedAttachmentMetaFile().getAbsolutePath();
506  0 int documentPathStart = this.fileTools.getStorageLocationPath().length() + 1;
507    // See DefaultFilesystemStoreTools#DOCUMENT_DIR_NAME
508  0 int documentPathEnd = absolutePath.indexOf("/~this/");
509  0 String documentPath = absolutePath.substring(documentPathStart, documentPathEnd);
510  0 return pathDocumentReferenceResolver.resolve(documentPath);
511    }
512   
513    /* ---------------------------- Nested Classes. ---------------------------- */
514   
515    /**
516    * A date comparator which compares dates in reverse chronological order.
517    */
 
518    private static class NewestFirstDateComparitor implements Comparator<Date>
519    {
520    /**
521    * A static reference to a singleton instance of the comparator.
522    */
523    public static final Comparator<Date> INSTANCE = new NewestFirstDateComparitor();
524   
 
525  0 toggle @Override
526    public int compare(final Date d1, final Date d2)
527    {
528  0 return d2.compareTo(d1);
529    }
530    }
531    }