MNT-24753 Restore from Archive when we have multiple content stores (#3066)

* Pre-commit changes
* Method requestRestoreContentFromArchive needs to use injected ContentStore
This commit is contained in:
Eva Vasques
2024-12-02 14:22:51 +00:00
committed by GitHub
parent 26d8c7dcbe
commit 37231e50a2

View File

@@ -1,493 +1,484 @@
/* /*
* #%L * #%L
* Alfresco Repository * Alfresco Repository
* %% * %%
* Copyright (C) 2005 - 2021 Alfresco Software Limited * Copyright (C) 2005 - 2021 Alfresco Software Limited
* %% * %%
* This file is part of the Alfresco software. * This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of * If the software was purchased under a paid Alfresco license, the terms of
* the paid license agreement will prevail. Otherwise, the software is * the paid license agreement will prevail. Otherwise, the software is
* provided under the following open source license terms: * provided under the following open source license terms:
* *
* Alfresco is free software: you can redistribute it and/or modify * Alfresco is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by * it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or * the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version. * (at your option) any later version.
* *
* Alfresco is distributed in the hope that it will be useful, * Alfresco is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details. * GNU Lesser General Public License for more details.
* *
* You should have received a copy of the GNU Lesser General Public License * You should have received a copy of the GNU Lesser General Public License
* along with Alfresco. If not, see <http://www.gnu.org/licenses/>. * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
* #L% * #L%
*/ */
package org.alfresco.repo.content; package org.alfresco.repo.content;
import java.io.Serializable; import java.io.Serializable;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock; import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;
import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;
import org.alfresco.error.AlfrescoRuntimeException; import org.apache.commons.logging.Log;
import org.alfresco.repo.cache.SimpleCache; import org.apache.commons.logging.LogFactory;
import org.alfresco.service.cmr.repository.ContentIOException;
import org.alfresco.service.cmr.repository.ContentReader; import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.service.cmr.repository.ContentWriter; import org.alfresco.repo.cache.SimpleCache;
import org.alfresco.util.GUID; import org.alfresco.service.cmr.repository.ContentIOException;
import org.alfresco.util.Pair; import org.alfresco.service.cmr.repository.ContentReader;
import org.apache.commons.logging.Log; import org.alfresco.service.cmr.repository.ContentWriter;
import org.apache.commons.logging.LogFactory; import org.alfresco.util.GUID;
import org.alfresco.util.Pair;
/**
* A store providing support for content store implementations that provide /**
* routing of content read and write requests based on context. * A store providing support for content store implementations that provide routing of content read and write requests based on context.
* *
* @see ContentContext * @see ContentContext
* *
* @since 2.1 * @since 2.1
* @author Derek Hulley * @author Derek Hulley
*/ */
public abstract class AbstractRoutingContentStore implements ContentStore public abstract class AbstractRoutingContentStore implements ContentStore
{ {
private static Log logger = LogFactory.getLog(AbstractRoutingContentStore.class); private static Log logger = LogFactory.getLog(AbstractRoutingContentStore.class);
private String instanceKey = GUID.generate(); private String instanceKey = GUID.generate();
private SimpleCache<Pair<String, String>, ContentStore> storesByContentUrl; private SimpleCache<Pair<String, String>, ContentStore> storesByContentUrl;
private ReadLock storesCacheReadLock; private ReadLock storesCacheReadLock;
private WriteLock storesCacheWriteLock; private WriteLock storesCacheWriteLock;
protected AbstractRoutingContentStore() protected AbstractRoutingContentStore()
{ {
ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
storesCacheReadLock = lock.readLock(); storesCacheReadLock = lock.readLock();
storesCacheWriteLock = lock.writeLock(); storesCacheWriteLock = lock.writeLock();
} }
/** /**
* @param storesCache cache of stores used to access URLs * @param storesCache
*/ * cache of stores used to access URLs
public void setStoresCache(SimpleCache<Pair<String, String>, ContentStore> storesCache) */
{ public void setStoresCache(SimpleCache<Pair<String, String>, ContentStore> storesCache)
this.storesByContentUrl = storesCache; {
} this.storesByContentUrl = storesCache;
}
/**
* @return Returns a list of all possible stores available for reading or writing /**
*/ * @return Returns a list of all possible stores available for reading or writing
protected abstract List<ContentStore> getAllStores(); */
protected abstract List<ContentStore> getAllStores();
/**
* Get a content store based on the context provided. The applicability of the /**
* context and even the types of context allowed are up to the implementation, but * Get a content store based on the context provided. The applicability of the context and even the types of context allowed are up to the implementation, but normally there should be a fallback case for when the parameters are not adequate to make a decision.
* normally there should be a fallback case for when the parameters are not adequate *
* to make a decision. * @param ctx
* * the context to use to make the choice
* @param ctx the context to use to make the choice * @return Returns the store most appropriate for the given context and <b>never <tt>null</tt></b>
* @return Returns the store most appropriate for the given context and */
* <b>never <tt>null</tt></b> protected abstract ContentStore selectWriteStore(ContentContext ctx);
*/
protected abstract ContentStore selectWriteStore(ContentContext ctx); /**
* Checks the cache for the store and ensures that the URL is in the store.
/** *
* Checks the cache for the store and ensures that the URL is in the store. * @param contentUrl
* * the content URL to search for
* @param contentUrl the content URL to search for * @return Returns the store matching the content URL
* @return Returns the store matching the content URL */
*/ private ContentStore selectReadStore(String contentUrl)
private ContentStore selectReadStore(String contentUrl) {
{ Pair<String, String> cacheKey = new Pair<String, String>(instanceKey, contentUrl);
Pair<String, String> cacheKey = new Pair<String, String>(instanceKey, contentUrl); storesCacheReadLock.lock();
storesCacheReadLock.lock(); try
try {
{ // Check if the store is in the cache
// Check if the store is in the cache ContentStore store = storesByContentUrl.get(cacheKey);
ContentStore store = storesByContentUrl.get(cacheKey); if (store != null)
if (store != null) {
{ // We found a store that was previously used
// We found a store that was previously used try
try {
{ // It is possible for content to be removed from a store and
// It is possible for content to be removed from a store and // it might have moved into another store.
// it might have moved into another store. if (store.exists(contentUrl))
if (store.exists(contentUrl)) {
{ // We found a store and can use it
// We found a store and can use it return store;
return store; }
} }
} catch (UnsupportedContentUrlException e)
catch (UnsupportedContentUrlException e) {
{ // This is odd. The store that previously supported the content URL
// This is odd. The store that previously supported the content URL // no longer does so. I can't think of a reason why that would be.
// no longer does so. I can't think of a reason why that would be. throw new AlfrescoRuntimeException(
throw new AlfrescoRuntimeException( "Found a content store that previously supported a URL, but no longer does: \n" +
"Found a content store that previously supported a URL, but no longer does: \n" + " Store: " + store + "\n" +
" Store: " + store + "\n" + " Content URL: " + contentUrl);
" Content URL: " + contentUrl); }
} }
} }
} finally
finally {
{ storesCacheReadLock.unlock();
storesCacheReadLock.unlock(); }
} // Get the write lock and double check
// Get the write lock and double check storesCacheWriteLock.lock();
storesCacheWriteLock.lock(); try
try {
{ // Double check
// Double check ContentStore store = storesByContentUrl.get(cacheKey);
ContentStore store = storesByContentUrl.get(cacheKey); if (store != null && store.exists(contentUrl))
if (store != null && store.exists(contentUrl)) {
{ // We found a store and can use it
// We found a store and can use it if (logger.isDebugEnabled())
if (logger.isDebugEnabled()) {
{ logger.debug(
logger.debug( "Found mapped store for content URL: \n" +
"Found mapped store for content URL: \n" + " Content URL: " + contentUrl + "\n" +
" Content URL: " + contentUrl + "\n" + " Store: " + store);
" Store: " + store); }
} return store;
return store; }
} else
else {
{ store = null;
store = null; }
} // It isn't, so search all the stores
// It isn't, so search all the stores List<ContentStore> stores = getAllStores();
List<ContentStore> stores = getAllStores(); // Keep track of the unsupported state of the content URL - it might be a rubbish URL
// Keep track of the unsupported state of the content URL - it might be a rubbish URL boolean contentUrlSupported = false;
boolean contentUrlSupported = false; for (ContentStore storeInList : stores)
for (ContentStore storeInList : stores) {
{ boolean exists = false;
boolean exists = false; try
try {
{ exists = storeInList.exists(contentUrl);
exists = storeInList.exists(contentUrl); // At least the content URL was supported
// At least the content URL was supported contentUrlSupported = true;
contentUrlSupported = true; }
} catch (UnsupportedContentUrlException e)
catch (UnsupportedContentUrlException e) {
{ // The store can't handle the content URL
// The store can't handle the content URL }
} if (!exists)
if (!exists) {
{ // It is not in the store
// It is not in the store continue;
continue; }
} // We found one
// We found one store = storeInList;
store = storeInList; // Put the value in the cache
// Put the value in the cache storesByContentUrl.put(cacheKey, store);
storesByContentUrl.put(cacheKey, store); break;
break; }
} // Check if the content URL was supported
// Check if the content URL was supported if (!contentUrlSupported)
if (!contentUrlSupported) {
{ throw new UnsupportedContentUrlException(this, contentUrl);
throw new UnsupportedContentUrlException(this, contentUrl); }
} // Done
// Done if (logger.isDebugEnabled())
if (logger.isDebugEnabled()) {
{ logger.debug(
logger.debug( "Mapped content URL to store for reading: \n" +
"Mapped content URL to store for reading: \n" + " Content URL: " + contentUrl + "\n" +
" Content URL: " + contentUrl + "\n" + " Store: " + store);
" Store: " + store); }
} return store;
return store; }
} finally
finally {
{ storesCacheWriteLock.unlock();
storesCacheWriteLock.unlock(); }
} }
}
/**
/** * @return Returns <tt>true</tt> if the URL is supported by any of the stores.
* @return Returns <tt>true</tt> if the URL is supported by any of the stores. */
*/ public boolean isContentUrlSupported(String contentUrl)
public boolean isContentUrlSupported(String contentUrl) {
{ List<ContentStore> stores = getAllStores();
List<ContentStore> stores = getAllStores(); boolean supported = false;
boolean supported = false; for (ContentStore store : stores)
for (ContentStore store : stores) {
{ if (store.isContentUrlSupported(contentUrl))
if (store.isContentUrlSupported(contentUrl)) {
{ supported = true;
supported = true; break;
break; }
} }
} // Done
// Done if (logger.isDebugEnabled())
if (logger.isDebugEnabled()) {
{ logger.debug("The url " + (supported ? "is" : "is not") + " supported by at least one store.");
logger.debug("The url " + (supported ? "is" : "is not") + " supported by at least one store."); }
} return supported;
return supported; }
}
/**
/** * @return Returns <tt>true</tt> if write is supported by any of the stores.
* @return Returns <tt>true</tt> if write is supported by any of the stores. */
*/ public boolean isWriteSupported()
public boolean isWriteSupported() {
{ List<ContentStore> stores = getAllStores();
List<ContentStore> stores = getAllStores(); boolean supported = false;
boolean supported = false; for (ContentStore store : stores)
for (ContentStore store : stores) {
{ if (store.isWriteSupported())
if (store.isWriteSupported()) {
{ supported = true;
supported = true; break;
break; }
} }
} // Done
// Done if (logger.isDebugEnabled())
if (logger.isDebugEnabled()) {
{ logger.debug("Writing " + (supported ? "is" : "is not") + " supported by at least one store.");
logger.debug("Writing " + (supported ? "is" : "is not") + " supported by at least one store."); }
} return supported;
return supported; }
}
/**
/** * @return Returns <b>.</b> always
* @return Returns <b>.</b> always */
*/ public String getRootLocation()
public String getRootLocation() {
{ return ".";
return "."; }
}
/**
/** * @return Returns <tt>-1</tt> always
* @return Returns <tt>-1</tt> always */
*/ @Override
@Override public long getSpaceFree()
public long getSpaceFree() {
{ return -1L;
return -1L; }
}
/**
/** * @return Returns <tt>-1</tt> always
* @return Returns <tt>-1</tt> always */
*/ @Override
@Override public long getSpaceTotal()
public long getSpaceTotal() {
{ return -1L;
return -1L; }
}
/**
/** * @see #selectReadStore(String)
* @see #selectReadStore(String) */
*/ public boolean exists(String contentUrl) throws ContentIOException
public boolean exists(String contentUrl) throws ContentIOException {
{ ContentStore store = selectReadStore(contentUrl);
ContentStore store = selectReadStore(contentUrl); return (store != null);
return (store != null); }
}
/**
/** * @return Returns a valid reader from one of the stores otherwise a {@link EmptyContentReader} is returned.
* @return Returns a valid reader from one of the stores otherwise */
* a {@link EmptyContentReader} is returned. public ContentReader getReader(String contentUrl) throws ContentIOException
*/ {
public ContentReader getReader(String contentUrl) throws ContentIOException ContentStore store = selectReadStore(contentUrl);
{ if (store != null)
ContentStore store = selectReadStore(contentUrl); {
if (store != null) if (logger.isDebugEnabled())
{ {
if (logger.isDebugEnabled()) logger.debug("Getting reader from store: \n" +
{ " Content URL: " + contentUrl + "\n" +
logger.debug("Getting reader from store: \n" + " Store: " + store);
" Content URL: " + contentUrl + "\n" + }
" Store: " + store); return store.getReader(contentUrl);
} }
return store.getReader(contentUrl); else
} {
else if (logger.isDebugEnabled())
{ {
if (logger.isDebugEnabled()) logger.debug("Getting empty reader for content URL: " + contentUrl);
{ }
logger.debug("Getting empty reader for content URL: " + contentUrl); return new EmptyContentReader(contentUrl);
} }
return new EmptyContentReader(contentUrl); }
}
} /**
* Selects a store for the given context and caches store that was used.
/** *
* Selects a store for the given context and caches store that was used. * @see #selectWriteStore(ContentContext)
* */
* @see #selectWriteStore(ContentContext) public ContentWriter getWriter(ContentContext context) throws ContentIOException
*/ {
public ContentWriter getWriter(ContentContext context) throws ContentIOException String contentUrl = context.getContentUrl();
{ Pair<String, String> cacheKey = new Pair<String, String>(instanceKey, contentUrl);
String contentUrl = context.getContentUrl(); if (contentUrl != null)
Pair<String, String> cacheKey = new Pair<String, String>(instanceKey, contentUrl); {
if (contentUrl != null) // Check to see if it is in the cache
{ storesCacheReadLock.lock();
// Check to see if it is in the cache try
storesCacheReadLock.lock(); {
try // Check if the store is in the cache
{ ContentStore store = storesByContentUrl.get(cacheKey);
// Check if the store is in the cache if (store != null)
ContentStore store = storesByContentUrl.get(cacheKey); {
if (store != null) throw new ContentExistsException(this, contentUrl);
{ }
throw new ContentExistsException(this, contentUrl); /* We could go further and check each store for the existence of the URL, but that would be overkill. The main problem we need to prevent is the simultaneous access of the same store. The router represents a single store and therefore if the URL is present in any of the stores, it is effectively present in all of them. */
} }
/* finally
* We could go further and check each store for the existence of the URL, {
* but that would be overkill. The main problem we need to prevent is storesCacheReadLock.unlock();
* the simultaneous access of the same store. The router represents }
* a single store and therefore if the URL is present in any of the stores, }
* it is effectively present in all of them. // Select the store for writing
*/ ContentStore store = selectWriteStore(context);
} // Check that we were given a valid store
finally if (store == null)
{ {
storesCacheReadLock.unlock(); throw new NullPointerException(
} "Unable to find a writer. 'selectWriteStore' may not return null: \n" +
} " Router: " + this + "\n" +
// Select the store for writing " Chose: " + store);
ContentStore store = selectWriteStore(context); }
// Check that we were given a valid store else if (!store.isWriteSupported())
if (store == null) {
{ throw new AlfrescoRuntimeException(
throw new NullPointerException( "A write store was chosen that doesn't support writes: \n" +
"Unable to find a writer. 'selectWriteStore' may not return null: \n" + " Router: " + this + "\n" +
" Router: " + this + "\n" + " Chose: " + store);
" Chose: " + store); }
} ContentWriter writer = store.getWriter(context);
else if (!store.isWriteSupported()) String newContentUrl = writer.getContentUrl();
{ Pair<String, String> newCacheKey = new Pair<String, String>(instanceKey, newContentUrl);
throw new AlfrescoRuntimeException( // Cache the store against the URL
"A write store was chosen that doesn't support writes: \n" + storesCacheWriteLock.lock();
" Router: " + this + "\n" + try
" Chose: " + store); {
} storesByContentUrl.put(newCacheKey, store);
ContentWriter writer = store.getWriter(context); }
String newContentUrl = writer.getContentUrl(); finally
Pair<String, String> newCacheKey = new Pair<String, String>(instanceKey, newContentUrl); {
// Cache the store against the URL storesCacheWriteLock.unlock();
storesCacheWriteLock.lock(); }
try // Done
{ if (logger.isDebugEnabled())
storesByContentUrl.put(newCacheKey, store); {
} logger.debug(
finally "Got writer and cache URL from store: \n" +
{ " Context: " + context + "\n" +
storesCacheWriteLock.unlock(); " Writer: " + writer + "\n" +
} " Store: " + store);
// Done }
if (logger.isDebugEnabled()) return writer;
{ }
logger.debug(
"Got writer and cache URL from store: \n" + public ContentWriter getWriter(ContentReader existingContentReader, String newContentUrl) throws ContentIOException
" Context: " + context + "\n" + {
" Writer: " + writer + "\n" + return getWriter(new ContentContext(existingContentReader, newContentUrl));
" Store: " + store); }
}
return writer; /**
} * This operation has to be performed on all the stores in order to maintain the {@link ContentStore#exists(String)} contract.
*/
public ContentWriter getWriter(ContentReader existingContentReader, String newContentUrl) throws ContentIOException public boolean delete(String contentUrl) throws ContentIOException
{ {
return getWriter(new ContentContext(existingContentReader, newContentUrl)); boolean deleted = true;
} List<ContentStore> stores = getAllStores();
for (ContentStore store : stores)
/** {
* This operation has to be performed on all the stores in order to maintain the if (store.isWriteSupported())
* {@link ContentStore#exists(String)} contract. {
*/ deleted &= store.delete(contentUrl);
public boolean delete(String contentUrl) throws ContentIOException }
{ }
boolean deleted = true; // Done
List<ContentStore> stores = getAllStores(); if (logger.isDebugEnabled())
for (ContentStore store : stores) {
{ logger.debug("Deleted content URL from stores: \n" +
if (store.isWriteSupported()) " Stores: " + stores.size() + "\n" +
{ " Deleted: " + deleted);
deleted &= store.delete(contentUrl); }
} return deleted;
} }
// Done
if (logger.isDebugEnabled()) /**
{ * {@inheritDoc}
logger.debug("Deleted content URL from stores: \n" + */
" Stores: " + stores.size() + "\n" + @Override
" Deleted: " + deleted); public Map<String, String> getStorageProperties(String contentUrl)
} {
return deleted; ContentStore contentStore = selectReadStore(contentUrl);
}
if (contentStore == null)
/** {
* {@inheritDoc} logNoContentStore(contentUrl);
*/ return Collections.emptyMap();
@Override }
public Map<String, String> getStorageProperties(String contentUrl) final String message = "Getting storage properties from store: ";
{ logExecution(contentUrl, contentStore, message);
ContentStore contentStore = selectReadStore(contentUrl);
return contentStore.getStorageProperties(contentUrl);
if (contentStore == null) }
{
logNoContentStore(contentUrl); /**
return Collections.emptyMap(); * {@inheritDoc}
} */
final String message = "Getting storage properties from store: "; @Override
logExecution(contentUrl, contentStore, message); public boolean requestSendContentToArchive(String contentUrl, Map<String, Serializable> archiveParams)
{
return contentStore.getStorageProperties(contentUrl); final ContentStore contentStore = selectReadStore(contentUrl);
} if (contentStore == null)
{
/** logNoContentStore(contentUrl);
* {@inheritDoc} return ContentStore.super.requestSendContentToArchive(contentUrl, archiveParams);
*/ }
@Override final String message = "Sending content to archive: ";
public boolean requestSendContentToArchive(String contentUrl, Map<String, Serializable> archiveParams) logExecution(contentUrl, contentStore, message);
{ return contentStore.requestSendContentToArchive(contentUrl, archiveParams);
final ContentStore contentStore = selectReadStore(contentUrl); }
if (contentStore == null)
{ /**
logNoContentStore(contentUrl); * {@inheritDoc}
return ContentStore.super.requestSendContentToArchive(contentUrl, archiveParams); */
} @Override
final String message = "Sending content to archive: "; public boolean requestRestoreContentFromArchive(String contentUrl, Map<String, Serializable> restoreParams)
logExecution(contentUrl, contentStore, message); {
return contentStore.requestSendContentToArchive(contentUrl, archiveParams); final ContentStore contentStore = selectReadStore(contentUrl);
} if (contentStore == null)
{
/** logNoContentStore(contentUrl);
* {@inheritDoc} return ContentStore.super.requestRestoreContentFromArchive(contentUrl, restoreParams);
*/ }
@Override final String message = "Restoring content from archive: ";
public boolean requestRestoreContentFromArchive(String contentUrl, Map<String, Serializable> restoreParams) logExecution(contentUrl, contentStore, message);
{ return contentStore.requestRestoreContentFromArchive(contentUrl, restoreParams);
final ContentStore contentStore = selectReadStore(contentUrl); }
if (contentStore == null)
{ private void logExecution(final String contentUrl, final ContentStore contentStore, final String message)
logNoContentStore(contentUrl); {
return ContentStore.super.requestRestoreContentFromArchive(contentUrl, restoreParams); if (logger.isTraceEnabled())
} {
final String message = "Restoring content from archive: "; logger.trace(message + "\n" +
logExecution(contentUrl, contentStore, message); " Content URL: " + contentUrl + "\n" +
return ContentStore.super.requestRestoreContentFromArchive(contentUrl, restoreParams); " Store: " + contentStore);
} }
}
private void logExecution(final String contentUrl, final ContentStore contentStore, final String message)
{ private void logNoContentStore(String contentUrl)
if (logger.isTraceEnabled()) {
{ if (logger.isTraceEnabled())
logger.trace(message + "\n" + {
" Content URL: " + contentUrl + "\n" + logger.trace("Content Store not found for content URL: " + contentUrl);
" Store: " + contentStore); }
} }
} }
private void logNoContentStore(String contentUrl)
{
if (logger.isTraceEnabled())
{
logger.trace("Content Store not found for content URL: " + contentUrl);
}
}
}