/*
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.xwiki.store.legacy.store.internal;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReadWriteLock;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import org.xwiki.component.annotation.Component;
import org.xwiki.component.phase.Initializable;
import org.xwiki.component.phase.InitializationException;
import org.xwiki.model.reference.DocumentReference;
import org.xwiki.model.reference.DocumentReferenceResolver;
import org.xwiki.store.FileDeleteTransactionRunnable;
import org.xwiki.store.FileSaveTransactionRunnable;
import org.xwiki.store.StartableTransactionRunnable;
import org.xwiki.store.filesystem.internal.DeletedAttachmentFileProvider;
import org.xwiki.store.filesystem.internal.FilesystemStoreTools;
import org.xwiki.store.internal.FileSystemStoreUtils;
import org.xwiki.store.legacy.doc.internal.DeletedFilesystemAttachment;
import org.xwiki.store.legacy.doc.internal.FilesystemAttachmentContent;
import org.xwiki.store.legacy.doc.internal.MutableDeletedFilesystemAttachment;
import org.xwiki.store.serialization.SerializationStreamProvider;
import org.xwiki.store.serialization.Serializer;
import com.xpn.xwiki.XWikiContext;
import com.xpn.xwiki.XWikiException;
import com.xpn.xwiki.doc.DeletedAttachment;
import com.xpn.xwiki.doc.XWikiAttachment;
import com.xpn.xwiki.doc.XWikiDocument;
import com.xpn.xwiki.store.AttachmentRecycleBinStore;
import com.xpn.xwiki.store.AttachmentVersioningStore;
/**
* Realization of {@link AttachmentRecycleBinStore} for filesystem storage.
*
* @version $Id: c1d3993563503e99dc3555a353551615647172dc $
* @since 3.0M3
*/
@Component
@Named(FileSystemStoreUtils.HINT)
@Singleton
public class FilesystemAttachmentRecycleBinStore implements AttachmentRecycleBinStore, Initializable
{
/**
* Some utilities for getting attachment files, locks, and backup files.
*/
@Inject
private FilesystemStoreTools fileTools;
/**
* A serializer for the archive metadata.
*/
@Inject
@Named("attachment-list-meta/1.0")
private Serializer<List<XWikiAttachment>, List<XWikiAttachment>> versionSerializer;
/**
* Used to parse and serialize deleted attachment metadata when loading and storing.
*/
@Inject
@Named("deleted-attachment-meta/1.0")
private Serializer<DeletedAttachment, MutableDeletedFilesystemAttachment> deletedAttachmentSerializer;
/**
* This is needed in order to be able to map the database ids given by the user to meaningful paths to deleted
* attachments.
*/
@Inject
@Named("deleted-attachment-id-mappings/1.0")
private Serializer<Map<Long, String>, Map<Long, String>> attachmentIdMappingSerializer;
/**
* Used to store the versions of the deleted attachment.
*/
@Inject
@Named(FileSystemStoreUtils.HINT)
private AttachmentVersioningStore attachmentVersionStore;
/**
* Used to extract the {@link DocumentReference} from the path to the deleted attachment.
*/
@Inject
@Named("path")
private DocumentReferenceResolver<String> pathDocumentReferenceResolver;
/**
* This is required because deleted attachments may be looked up by a database id number So we are forced to
* simulate the database id numbering scheme even though they are and should be stored with their documents which
* are stored by name.
*/
private final Map<Long, String> pathById = new ConcurrentHashMap<Long, String>();
/**
* The location to persist the pathById map.
*/
private File pathByIdStore;
/**
* Load the pathById mappings so that attachments can be loaded by database id.
*
* @throws InitializationException if the mapping file cannot be parsed.
*/
@Override
public void initialize() throws InitializationException
{
// make sure we have a FilesystemAttachmentVersioningStore.
if (!(attachmentVersionStore instanceof FilesystemAttachmentVersioningStore)) {
throw new InitializationException("Wrong attachment versioning store registered under hint "
+ "'file', expecting a FilesystemAttachmentVersioningStore");
}
this.pathByIdStore = this.fileTools.getGlobalFile("DELETED_ATTACHMENT_ID_MAPPINGS.xml");
if (pathByIdStore.exists()) {
try {
this.pathById.putAll(attachmentIdMappingSerializer.parse(new FileInputStream(this.pathByIdStore)));
} catch (IOException e) {
throw new InitializationException("Failed to parse deleted attachment id mappings.", e);
}
}
}
@Override
public void saveToRecycleBin(final XWikiAttachment attachment, final String deleter, final Date deleteDate,
final XWikiContext context, final boolean bTransaction) throws XWikiException
{
final DeletedFilesystemAttachment dfa = new DeletedFilesystemAttachment(attachment, deleter, deleteDate);
final StartableTransactionRunnable tr = this.getSaveTrashAttachmentRunnable(dfa, context);
// Need to add the ID to the map and persist the map
// otherwise the attachment will not be able to loaded by the ID.
// TODO standardize a deleted attachment entity reference and deprecate the use of a long integer
// as a key to load a deleted attachment with.
final String absolutePath = this.fileTools.getDeletedAttachmentFileProvider(attachment, deleteDate)
.getAttachmentContentFile().getParentFile().getAbsolutePath();
final String path = absolutePath.substring(absolutePath.indexOf(this.fileTools.getStorageLocationPath()));
final Long id = Long.valueOf(dfa.getId());
(new StartableTransactionRunnable()
{
public void onRun()
{
pathById.put(id, path);
}
public void onRollback()
{
pathById.remove(id);
}
}).runIn(tr);
// Need to save the updated map right away in case the power goes out or something.
new FileSaveTransactionRunnable(this.pathByIdStore, this.fileTools.getTempFile(this.pathByIdStore),
this.fileTools.getBackupFile(this.pathByIdStore), this.fileTools.getLockForFile(this.pathByIdStore),
new SerializationStreamProvider<Map<Long, String>>(this.attachmentIdMappingSerializer, this.pathById))
.runIn(tr);
try {
tr.start();
} catch (Exception e) {
throw new XWikiException(XWikiException.MODULE_XWIKI_STORE, XWikiException.MODULE_XWIKI,
"Failed to store deleted attachment " + attachment.getFilename() + " for document: "
+ attachment.getDoc().getDocumentReference(),
e);
}
}
/**
* Get a StartableTransactionRunnable to save an attachment in the recycle-bin.
*
* @param deleted the FilesystemDeletedAttachment to save.
* @param context the legacy XWikiContext which might be needed to get the content from the attachment, or to load
* the attachment versioning store.
* @return a TransactionRunnable for storing the deleted attachment.
* @throws XWikiException if one is thrown trying to get data from the attachment or loading the attachment archive
* in order to save it in the deleted section.
*/
public StartableTransactionRunnable getSaveTrashAttachmentRunnable(final DeletedFilesystemAttachment deleted,
final XWikiContext context) throws XWikiException
{
final DeletedAttachmentFileProvider provider =
this.fileTools.getDeletedAttachmentFileProvider(deleted.getAttachment(), deleted.getDate());
return new SaveTrashAttachmentRunnable(deleted, provider, this.fileTools, this.deletedAttachmentSerializer,
this.versionSerializer, context);
}
/**
* {@inheritDoc}
* <p>
* bTransaction is ignored by this implementation.
* </p>
*
* @see AttachmentRecycleBinStore#restoreFromRecycleBin(XWikiAttachment, long, XWikiContext, boolean)
*/
@Override
public XWikiAttachment restoreFromRecycleBin(final XWikiAttachment attachment, final long index,
final XWikiContext context, boolean bTransaction) throws XWikiException
{
final DeletedAttachment delAttach = getDeletedAttachment(index, context, false);
return delAttach != null ? delAttach.restoreAttachment(attachment, context) : null;
}
/**
* {@inheritDoc}
* <p>
* bTransaction is ignored by this implementation. context is unused and may safely be null.
* </p>
*
* @see AttachmentRecycleBinStore#getDeletedAttachment(long, XWikiContext, boolean)
*/
@Override
public DeletedAttachment getDeletedAttachment(final long index, final XWikiContext context,
final boolean bTransaction) throws XWikiException
{
final String path = this.pathById.get(Long.valueOf(index));
if (path == null) {
return null;
}
try {
return this.deletedAttachmentFromProvider(this.fileTools.getDeletedAttachmentFileProvider(path), context);
} catch (IOException e) {
throw new XWikiException(XWikiException.MODULE_XWIKI_STORE, XWikiException.MODULE_XWIKI,
"Failed to get deleted attachment at index " + index + " with filesystem path " + path, e);
}
}
/**
* {@inheritDoc}
* <p>
* bTransaction is ignored by this implementation.
* </p>
*
* @see AttachmentRecycleBinStore#getAllDeletedAttachments(XWikiAttachment, XWikiContext, boolean)
*/
@Override
public List<DeletedAttachment> getAllDeletedAttachments(final XWikiAttachment attachment,
final XWikiContext context, final boolean bTransaction) throws XWikiException
{
if (attachment.getDoc() == null) {
throw new XWikiException(XWikiException.MODULE_XWIKI_STORE, XWikiException.MODULE_XWIKI,
"Cannot load deleted attachments because the given attachment " + attachment.getFilename()
+ " is not attached to any document.");
}
// I don't know that there is no way to upload an attachment named ""
// so I don't want to use isEmpty here.
if (attachment.getFilename() == null) {
return this.getAllDeletedAttachments(attachment.getDoc(), context, false);
}
final Map<Date, DeletedAttachmentFileProvider> attachMap = this.fileTools
.deletedAttachmentsForDocument(attachment.getDoc().getDocumentReference()).get(attachment.getFilename());
// There may not be any deleted versions matching the requested attachment filename.
if (attachMap == null) {
return Collections.<DeletedAttachment>emptyList();
}
final List<Date> deleteDatesList = new ArrayList<Date>(attachMap.keySet());
Collections.sort(deleteDatesList, NewestFirstDateComparitor.INSTANCE);
final List<DeletedAttachment> out = new ArrayList<DeletedAttachment>(deleteDatesList.size());
try {
for (Date date : deleteDatesList) {
out.add(this.deletedAttachmentFromProvider(attachMap.get(date), context));
}
} catch (IOException e) {
throw new XWikiException(XWikiException.MODULE_XWIKI_STORE, XWikiException.MODULE_XWIKI,
"Failed to get deleted attachment " + attachment.getFilename() + " attached to the document: "
+ attachment.getDoc().getDocumentReference(),
e);
}
return out;
}
/**
* {@inheritDoc}
* <p>
* bTransaction is ignored by this implementation. context is unused and may safely be null.
* </p>
*
* @see AttachmentRecycleBinStore#getAllDeletedAttachments(XWikiDocument, XWikiContext, boolean)
*/
@Override
public List<DeletedAttachment> getAllDeletedAttachments(final XWikiDocument doc, final XWikiContext context,
final boolean bTransaction) throws XWikiException
{
final Map<String, Map<Date, DeletedAttachmentFileProvider>> attachMap =
this.fileTools.deletedAttachmentsForDocument(doc.getDocumentReference());
// Get a list of dates ordered by newest first.
final List<Date> allDates = new ArrayList<Date>();
for (Map<Date, ?> dateMap : attachMap.values()) {
allDates.addAll(dateMap.keySet());
}
Collections.sort(allDates, NewestFirstDateComparitor.INSTANCE);
// Populate the output list by the order of the date.
// Everything cannot be placed into an ordered map because it is conceivable that 2 attachments
// would be deleted in the same millisecond and that would cause them to be merged.
try {
final List<DeletedAttachment> out = new ArrayList<DeletedAttachment>(allDates.size());
for (Date date : allDates) {
for (Map<Date, DeletedAttachmentFileProvider> map : attachMap.values()) {
if (map.get(date) != null) {
out.add(this.deletedAttachmentFromProvider(map.get(date), context));
map.remove(date);
break;
}
}
}
return out;
} catch (IOException e) {
throw new XWikiException(XWikiException.MODULE_XWIKI_STORE, XWikiException.MODULE_XWIKI,
"Failed to get deleted attachments for document: " + doc.getDocumentReference(), e);
}
}
/**
* {@inheritDoc}
* <p>
* bTransaction is ignored because the filesystem cannot synchronize with the database commit. TODO: make
* getDeletedAttachmentPurgeRunnable public so that a transaction safe method is available. context is unused and
* may safely be null.
* </p>
*
* @see AttachmentRecycleBinStore#deleteFromRecycleBin(long, XWikiContext, boolean)
*/
@Override
public void deleteFromRecycleBin(final long index, final XWikiContext context, final boolean bTransaction)
throws XWikiException
{
final String path = this.pathById.get(Long.valueOf(index));
if (path != null) {
this.getDeletedAttachmentPurgeRunnable(this.fileTools.getDeletedAttachmentFileProvider(path));
}
}
/**
* Get a TransactionRunnable for removing a deleted attachment from the filesystem entirely. TODO: Standardize an
* EntityReference for deleted attachments and make that the parameter.
*
* @param provider the file provider for the deleted attachment to purge from the recycle bin.
* @return a StartableTransactionRunnable for removing the attachment.
*/
private StartableTransactionRunnable getDeletedAttachmentPurgeRunnable(final DeletedAttachmentFileProvider provider)
{
final StartableTransactionRunnable out = new StartableTransactionRunnable();
final File deletedAttachDir = provider.getDeletedAttachmentMetaFile().getParentFile();
if (!deletedAttachDir.exists()) {
// No such dir, return a do-nothing runnable.
return out;
}
// Easy thing to do is just delete everything in the deleted-attachment directory.
for (File toDelete : deletedAttachDir.listFiles()) {
new FileDeleteTransactionRunnable(toDelete, this.fileTools.getBackupFile(toDelete),
this.fileTools.getLockForFile(toDelete)).runIn(out);
}
// Remove the entry from the pathById map so that it doesn't cause a memory leak.
final String path = deletedAttachDir.getAbsolutePath();
for (final Long id : pathById.keySet()) {
if (pathById.get(id).endsWith(path)) {
(new StartableTransactionRunnable()
{
public void onRun()
{
pathById.remove(id);
}
public void onRollback()
{
pathById.put(id, path);
}
}).runIn(out);
break;
}
}
return out;
}
/**
* Get a deleted attachment by it's filesystem location. This returns a DeletedAttachment which is not attached to
* any document! It is the job of the caller to get the attachment and any version of it and attach them to a
* document.
*
* @param provider a means to get the files which store the deleted attachment content and metadata.
* @param context the XWiki context
* @return the deleted attachment for that directory.
* @throws IOException if deserialization fails or there is a problem loading the archive.
* @throws XWikiException if we fail to load the document associated with the deleted attachment (the document that
* was holding the attachment before it was deleted)
*/
private DeletedAttachment deletedAttachmentFromProvider(final DeletedAttachmentFileProvider provider,
final XWikiContext context) throws IOException, XWikiException
{
final File deletedMeta = provider.getDeletedAttachmentMetaFile();
// No metadata, no deleted attachment.
if (!deletedMeta.exists()) {
return null;
}
final MutableDeletedFilesystemAttachment delAttach;
ReadWriteLock lock = this.fileTools.getLockForFile(deletedMeta);
lock.readLock().lock();
try {
delAttach = this.deletedAttachmentSerializer.parse(new FileInputStream(deletedMeta));
} finally {
lock.readLock().unlock();
}
// Bind the deleted attachment to the associated document in order to be able to restore it.
DocumentReference documentReference = getDocumentReference(provider);
delAttach.getAttachment().setDoc(context.getWiki().getDocument(documentReference, context));
final File contentFile = provider.getAttachmentContentFile();
final XWikiAttachment attachment = delAttach.getAttachment();
attachment.setAttachment_content(new FilesystemAttachmentContent(contentFile, attachment));
attachment.setAttachment_archive(
((FilesystemAttachmentVersioningStore) this.attachmentVersionStore).loadArchive(attachment, provider));
return delAttach.getImmutable();
}
/**
* FIXME: This method works with the default implementation of {@link FilesystemStoreTools}. It should probably be
* moved there but then we need to add a new method to the {@link FilesystemStoreTools} interface, thus breaking
* backward compatibility.
*
* @param provider a means to get the files which store the deleted attachment content and metadata
* @return the reference to the document that was holding the deleted attachment
*/
private DocumentReference getDocumentReference(DeletedAttachmentFileProvider provider)
{
String absolutePath = provider.getDeletedAttachmentMetaFile().getAbsolutePath();
int documentPathStart = this.fileTools.getStorageLocationPath().length() + 1;
// See DefaultFilesystemStoreTools#DOCUMENT_DIR_NAME
int documentPathEnd = absolutePath.indexOf("/~this/");
String documentPath = absolutePath.substring(documentPathStart, documentPathEnd);
return pathDocumentReferenceResolver.resolve(documentPath);
}
/* ---------------------------- Nested Classes. ---------------------------- */
/**
* A date comparator which compares dates in reverse chronological order.
*/
private static class NewestFirstDateComparitor implements Comparator<Date>
{
/**
* A static reference to a singleton instance of the comparator.
*/
public static final Comparator<Date> INSTANCE = new NewestFirstDateComparitor();
@Override
public int compare(final Date d1, final Date d2)
{
return d2.compareTo(d1);
}
}
}