Dave Ward 051508c21d Merged PATCHES/V3.2.r to HEAD
19546: (RECORD ONLY) Merged V3.2 to PATCHES/V3.2.r
      19432: Merged V3.1 to V3.2
         19427: Merged V3.0 to V3.1
            19423: Merged V2.2 to V3.0
               19391: Fix for ALF-2076: AUTO does not work if a document has been added and deleted since the index backup
               19419: V2.2 Build Fix
               19421: Fix for ALF-2076: AUTO does not work if a document has been added and deleted since the index backup
      19463: Merged V3.1 to V3.2
         19459: Merged V3.0 to V3.1
            19457: Merged V2.2 to V3.0
               19449: Addition Fix for ALF-2076: AUTO does not work if a document has been added and deleted since the index backup
      19493 Merged V3.1 to V3.2
         19471: Build fix after changes for ALF-2076 were merged forward. Index checker correctly understands INDETERMINATE state of indexed transactions
   19547: (RECORD ONLY) Incremented version label
   19555: (RECORD ONLY) Merged V3.2 to PATCHES/V3.2.r
      19552: Merged V3.1 to V3.2
         19551: Further fix after changes for ALF-2076 were merged forward. Final fix to check for InIndex.No
   19566: (RECORD ONLY) Merged V3.2 to PATCHES/V3.2.r
      19539: Merged HEAD to V3.2
         19538: ALF-2076: Build fix - fix build speed
   19802: (RECORD ONLY) ALF-2382, ALF-2383: Merged V3.2 to PATCHES/V3.2.r
      19647: ALF-2231: Merged DEV/BELARUS/V2.2-2009_12_01 to V3.2 
         17704: ENH-681: alfresco webdav does not respect webdav locks 
      19624: ALF-2231: Merged DEV/BELARUS/V2.2-2009_12_01 to V3.2
         17704: ENH-681: alfresco webdav does not respect webdav locks
      19623: ALF-1890: Correction to previous checkin to allow defaulting of request body charset
      19617: ALF-1890: Improvements to make ALL WebDAV methods retryable
         - Solution from PutMethod promoted to request wrapper that will handle ALL calls to getInputStream and getReader
      19614: ALF-1890: Merged V2.2 to V3.2
         17709: Merged DEV_TEMPORARY to V2.2
            17700: ETWOTWO-1393: concurrent writes to webdav lead to data loss (0kb resulting file)
         19613: Merged DEV/BELARUS/V2.2-2010_02_03 to V2.2
            19157: ALF-1890: concurrent writes to webdav lead to data loss (0kb resulting file)
   19803: ALF-558: File servers (CIFS / FTP / NFS) can now handle concurrent write operations on Alfresco repository
      - ContentDiskDriver / AVMDiskDriver now use retrying transactions for write operations
      - Disable EagerContentStoreCleaner on ContentDiskDriver / AVMDiskDriver closeFile() operations so that they may be retried after rollback (Sony zero byte problem)
      - Allow manual association of AVM ContentData with nodes so that closeFile() may be retried
      - Propagation of new argument through AVM interfaces
   19804: (RECORD ONLY) Merged PATCHES/V3.2.0 to PATCHES/V3.2.r
      Merged HEAD to V3.2.0
         19786: Refactor of previous test fix. I have pushed down the OOo-specific parts of the change from AbstractContentTransformerTest to OpenOfficeContentTransformerTest leaving an extension point in the base class should other transformations need to be excluded in the future.
         19785: Fix for failing test OpenOfficeContentTransformerTest.testAllConversions.
            Various OOo-related transformations are returned as available but fail on our test server with OOo on it.
            Pending further work on these failings, I am disabling those transformations in test code whilst leaving them available in the product code. This is because in the wild a different OOo version may succeed with these transformations.
            I had previously explicitly disabled 3 transformations in the product and I am moving that restriction from product to test code for the same reason.
         19707: Return value from isTransformationBlocked was inverted. Fixed now.
         19705: Refinement of previous check-in re OOo transformations.
            I have pulled up the code that handles blocked transformations into a superclass so that the JodConverter-based transformer worker can inherit the same list of blocked transformations. To reiterate, blocked transformations are those that the OOo integration code believes should work but which are broken in practice. These are blocked by the transformers and will always be unavailable regardless of the OOo connection state.
         19702: Fix for HEAD builds running on panda build server.
            OOo was recently installed on panda which has activated various OOo-related transformations/extractions in the test code.
            It appears that OOo does not support some transformations from Office 97 to Office 2007. Specifically doc to docx and xls to xlsx. These transformations have now been marked as unavailable.


git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@20004 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261
2010-04-27 10:57:47 +00:00

525 lines
19 KiB
Java

/*
* Copyright (C) 2005-2010 Alfresco Software Limited.
*
* This file is part of Alfresco
*
* 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
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Alfresco 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 Alfresco. If not, see <http://www.gnu.org/licenses/>.
*/
package org.alfresco.repo.domain.contentdata;
import java.io.Serializable;
import java.util.Locale;
import java.util.Set;
import org.alfresco.repo.cache.SimpleCache;
import org.alfresco.repo.cache.lookup.EntityLookupCache;
import org.alfresco.repo.cache.lookup.EntityLookupCache.EntityLookupCallbackDAOAdaptor;
import org.alfresco.repo.content.cleanup.EagerContentStoreCleaner;
import org.alfresco.repo.domain.LocaleDAO;
import org.alfresco.repo.domain.encoding.EncodingDAO;
import org.alfresco.repo.domain.mimetype.MimetypeDAO;
import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
import org.alfresco.repo.transaction.TransactionListenerAdapter;
import org.alfresco.repo.transaction.TransactionalResourceHelper;
import org.alfresco.service.cmr.repository.ContentData;
import org.alfresco.util.EqualsHelper;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.dao.ConcurrencyFailureException;
import org.springframework.dao.DataIntegrityViolationException;
import org.alfresco.util.Pair;
/**
* Abstract implementation for ContentData DAO.
* <p>
* This provides basic services such as caching, but defers to the underlying implementation
* for CRUD operations.
* <p>
* The DAO deals in {@link ContentData} instances. The cache is primarily present to decode
* IDs into <code>ContentData</code> instances.
*
* @author Derek Hulley
* @since 3.2
*/
public abstract class AbstractContentDataDAOImpl implements ContentDataDAO
{
private static final String CACHE_REGION_CONTENT_DATA = "ContentData";
/**
* Content URL IDs to delete before final commit.
*/
private static final String KEY_PRE_COMMIT_CONTENT_URL_DELETIONS = "AbstractContentDataDAOImpl.PreCommitContentUrlDeletions";
private static Log logger = LogFactory.getLog(AbstractContentDataDAOImpl.class);
private final ContentDataCallbackDAO contentDataCallbackDAO;
private MimetypeDAO mimetypeDAO;
private EncodingDAO encodingDAO;
private LocaleDAO localeDAO;
private EagerContentStoreCleaner contentStoreCleaner;
/**
* Cache for the ContentData class:<br/>
* KEY: ID<br/>
* VALUE: ContentData object<br/>
* VALUE KEY: NONE<br/>
*/
private EntityLookupCache<Long, ContentData, Serializable> contentDataCache;
/**
* Default constructor
*/
public AbstractContentDataDAOImpl()
{
this.contentDataCallbackDAO = new ContentDataCallbackDAO();
this.contentDataCache = new EntityLookupCache<Long, ContentData, Serializable>(contentDataCallbackDAO);
}
public void setMimetypeDAO(MimetypeDAO mimetypeDAO)
{
this.mimetypeDAO = mimetypeDAO;
}
public void setEncodingDAO(EncodingDAO encodingDAO)
{
this.encodingDAO = encodingDAO;
}
public void setLocaleDAO(LocaleDAO localeDAO)
{
this.localeDAO = localeDAO;
}
/**
* Set this property to enable eager cleanup of orphaned content.
*
* @param contentStoreCleaner an eager cleaner (may be <tt>null</tt>)
*/
public void setContentStoreCleaner(EagerContentStoreCleaner contentStoreCleaner)
{
this.contentStoreCleaner = contentStoreCleaner;
}
/**
* @param contentDataCache the cache of IDs to ContentData and vice versa
*/
public void setContentDataCache(SimpleCache<Long, ContentData> contentDataCache)
{
this.contentDataCache = new EntityLookupCache<Long, ContentData, Serializable>(
contentDataCache,
CACHE_REGION_CONTENT_DATA,
contentDataCallbackDAO);
}
/**
* Register new content for post-rollback handling
*/
protected void registerNewContentUrl(String contentUrl)
{
contentStoreCleaner.registerNewContentUrl(contentUrl);
}
/**
* A <b>content_url</b> entity was dereferenced. This makes no assumptions about the
* current references - dereference deletion is handled in the commit phase.
*/
protected void registerDereferencedContentUrl(String contentUrl)
{
Set<String> contentUrls = TransactionalResourceHelper.getSet(KEY_PRE_COMMIT_CONTENT_URL_DELETIONS);
if (contentUrls.size() == 0)
{
ContentUrlDeleteTransactionListener listener = new ContentUrlDeleteTransactionListener();
AlfrescoTransactionSupport.bindListener(listener);
}
contentUrls.add(contentUrl);
}
/**
* {@inheritDoc}
*/
public Pair<Long, ContentData> createContentData(ContentData contentData)
{
if (contentData == null)
{
throw new IllegalArgumentException("ContentData values cannot be null");
}
Pair<Long, ContentData> entityPair = contentDataCache.getOrCreateByValue(contentData);
return entityPair;
}
/**
* {@inheritDoc}
*/
public Pair<Long, ContentData> getContentData(Long id)
{
if (id == null)
{
throw new IllegalArgumentException("Cannot look up ContentData by null ID.");
}
Pair<Long, ContentData> entityPair = contentDataCache.getByKey(id);
if (entityPair == null)
{
throw new DataIntegrityViolationException("No ContentData value exists for ID " + id);
}
return entityPair;
}
/**
* {@inheritDoc}
*/
public void updateContentData(Long id, ContentData contentData)
{
if (id == null)
{
throw new IllegalArgumentException("Cannot look up ContentData by null ID.");
}
if (contentData == null)
{
throw new IllegalArgumentException("Cannot update ContentData with a null.");
}
int updated = contentDataCache.updateValue(id, contentData);
if (updated < 1)
{
throw new ConcurrencyFailureException("ContentData with ID " + id + " not updated");
}
}
/**
* {@inheritDoc}
*/
public void deleteContentData(Long id)
{
if (id == null)
{
throw new IllegalArgumentException("Cannot delete ContentData by null ID.");
}
int deleted = contentDataCache.deleteByKey(id);
if (deleted < 1)
{
throw new ConcurrencyFailureException("ContentData with ID " + id + " no longer exists");
}
return;
}
/**
* Callback for <b>alf_content_data</b> DAO.
*/
private class ContentDataCallbackDAO extends EntityLookupCallbackDAOAdaptor<Long, ContentData, Serializable>
{
public Pair<Long, ContentData> createValue(ContentData value)
{
ContentDataEntity contentDataEntity = createContentDataEntity(value);
// Done
return new Pair<Long, ContentData>(contentDataEntity.getId(), value);
}
public Pair<Long, ContentData> findByKey(Long key)
{
ContentDataEntity contentDataEntity = getContentDataEntity(key);
if (contentDataEntity == null)
{
return null;
}
ContentData contentData = makeContentData(contentDataEntity);
// Done
return new Pair<Long, ContentData>(key, contentData);
}
@Override
public int updateValue(Long key, ContentData value)
{
ContentDataEntity contentDataEntity = getContentDataEntity(key);
if (contentDataEntity == null)
{
return 0; // The client (outer-level code) will decide if this is an error
}
return updateContentDataEntity(contentDataEntity, value);
}
@Override
public int deleteByKey(Long key)
{
return deleteContentDataEntity(key);
}
}
/**
* Translates this instance into an externally-usable <code>ContentData</code> instance.
*/
private ContentData makeContentData(ContentDataEntity contentDataEntity)
{
// Decode content URL
String contentUrl = contentDataEntity.getContentUrl();
long size = contentDataEntity.getSize() == null ? 0L : contentDataEntity.getSize().longValue();
// Decode mimetype
Long mimetypeId = contentDataEntity.getMimetypeId();
String mimetype = null;
if (mimetypeId != null)
{
mimetype = mimetypeDAO.getMimetype(mimetypeId).getSecond();
}
// Decode encoding
Long encodingId = contentDataEntity.getEncodingId();
String encoding = null;
if (encodingId != null)
{
encoding = encodingDAO.getEncoding(encodingId).getSecond();
}
// Decode locale
Long localeId = contentDataEntity.getLocaleId();
Locale locale = null;
if (localeId != null)
{
locale = localeDAO.getLocalePair(localeId).getSecond();
}
// Build the ContentData
ContentData contentData = new ContentData(contentUrl, mimetype, size, encoding, locale);
// Done
return contentData;
}
/**
* Translates the {@link ContentData} into persistable values using the helper DAOs
*/
private ContentDataEntity createContentDataEntity(ContentData contentData)
{
// Resolve the content URL
Long contentUrlId = null;
String contentUrl = contentData.getContentUrl();
long size = contentData.getSize();
if (contentUrl != null)
{
// We must find or create the ContentUrlEntity
contentUrlId = getOrCreateContentUrlEntity(contentUrl, size, contentData.isReferenced()).getId();
}
// Resolve the mimetype
Long mimetypeId = null;
String mimetype = contentData.getMimetype();
if (mimetype != null)
{
mimetypeId = mimetypeDAO.getOrCreateMimetype(mimetype).getFirst();
}
// Resolve the encoding
Long encodingId = null;
String encoding = contentData.getEncoding();
if (encoding != null)
{
encodingId = encodingDAO.getOrCreateEncoding(encoding).getFirst();
}
// Resolve the locale
Long localeId = null;
Locale locale = contentData.getLocale();
if (locale != null)
{
localeId = localeDAO.getOrCreateLocalePair(locale).getFirst();
}
// Create ContentDataEntity
ContentDataEntity contentDataEntity = createContentDataEntity(contentUrlId, mimetypeId, encodingId, localeId);
// Done
return contentDataEntity;
}
/**
* Translates the {@link ContentData} into persistable values using the helper DAOs
*/
private int updateContentDataEntity(ContentDataEntity contentDataEntity, ContentData contentData)
{
// Resolve the content URL
String oldContentUrl = contentDataEntity.getContentUrl();
String newContentUrl = contentData.getContentUrl();
if (!EqualsHelper.nullSafeEquals(oldContentUrl, newContentUrl))
{
if (oldContentUrl != null)
{
// We have a changed value. The old content URL has been dereferenced.
registerDereferencedContentUrl(oldContentUrl);
}
if (newContentUrl != null)
{
Long contentUrlId = getOrCreateContentUrlEntity(newContentUrl, contentData.getSize(), contentData.isReferenced()).getId();
contentDataEntity.setContentUrlId(contentUrlId);
contentDataEntity.setContentUrl(newContentUrl);
}
else
{
contentDataEntity.setContentUrlId(null);
contentDataEntity.setContentUrl(null);
}
}
// Resolve the mimetype
Long mimetypeId = null;
String mimetype = contentData.getMimetype();
if (mimetype != null)
{
mimetypeId = mimetypeDAO.getOrCreateMimetype(mimetype).getFirst();
}
// Resolve the encoding
Long encodingId = null;
String encoding = contentData.getEncoding();
if (encoding != null)
{
encodingId = encodingDAO.getOrCreateEncoding(encoding).getFirst();
}
// Resolve the locale
Long localeId = null;
Locale locale = contentData.getLocale();
if (locale != null)
{
localeId = localeDAO.getOrCreateLocalePair(locale).getFirst();
}
contentDataEntity.setMimetypeId(mimetypeId);
contentDataEntity.setEncodingId(encodingId);
contentDataEntity.setLocaleId(localeId);
return updateContentDataEntity(contentDataEntity);
}
/**
* Method to create (or get an existing) content URL. The URL will be unorphaned
* whether it has been created or is being re-used.
* @param isReferenced if <code>true</code> we won't worry about eagerly deleting the content on transaction rollback
*/
private ContentUrlEntity getOrCreateContentUrlEntity(String contentUrl, long size, boolean isReferenced)
{
// Create the content URL entity
ContentUrlEntity contentUrlEntity = getContentUrlEntity(contentUrl);
// If it exists, then we can just re-use it, but check that the size is consistent
if (contentUrlEntity != null)
{
// Reuse it
long existingSize = contentUrlEntity.getSize();
if (size != existingSize)
{
logger.warn(
"Re-using Content URL, but size is mismatched: \n" +
" Inbound: " + contentUrl + "\n" +
" Existing: " + contentUrlEntity);
}
// Check orphan state
if (contentUrlEntity.getOrphanTime() != null)
{
Long id = contentUrlEntity.getId();
updateContentUrlOrphanTime(id, null);
}
}
else
{
// Create it
contentUrlEntity = createContentUrlEntity(contentUrl, size, isReferenced);
}
// Done
return contentUrlEntity;
}
/**
* @param contentUrl the content URL to create or search for
* @param isReferenced if <code>true</code> we won't worry about eagerly deleting the content on transaction rollback
*/
protected abstract ContentUrlEntity createContentUrlEntity(String contentUrl, long size, boolean isReferenced);
/**
* @param id the ID of the <b>content url</b> entity
* @return Return the entity or <tt>null</tt> if it doesn't exist
*/
protected abstract ContentUrlEntity getContentUrlEntity(Long id);
/**
* @param contentUrl the URL of the <b>content url</b> entity
* @return Return the entity or <tt>null</tt> if it doesn't exist
*/
protected abstract ContentUrlEntity getContentUrlEntity(String contentUrl);
/**
* @param contentUrl the URL of the <b>content url</b> entity
* @return Return the entity or <tt>null</tt> if it doesn't exist or is still
* referenced by a <b>content_data</b> entity
*/
protected abstract ContentUrlEntity getContentUrlEntityUnreferenced(String contentUrl);
/**
* Update a content URL with the given orphan time
*
* @param id the unique ID of the entity
* @param orphanTime the time (ms since epoch) that the entity was orphaned
* @return Returns the number of rows updated
*/
protected abstract int updateContentUrlOrphanTime(Long id, Long orphanTime);
/**
* Create the row for the <b>alf_content_data<b>
*/
protected abstract ContentDataEntity createContentDataEntity(
Long contentUrlId,
Long mimetypeId,
Long encodingId,
Long localeId);
/**
* @param id the entity ID
* @return Returns the entity or <tt>null</tt> if it doesn't exist
*/
protected abstract ContentDataEntity getContentDataEntity(Long id);
/**
* Update an existing <b>alf_content_data</b> entity
*
* @param entity the existing entity that will be updated
* @return Returns the number of rows updated (should be 1)
*/
protected abstract int updateContentDataEntity(ContentDataEntity entity);
/**
* Delete the entity with the given ID
*
* @return Returns the number of rows deleted
*/
protected abstract int deleteContentDataEntity(Long id);
/**
* Transactional listener that deletes unreferenced <b>content_url</b> entities.
*
* @author Derek Hulley
*/
public class ContentUrlDeleteTransactionListener extends TransactionListenerAdapter
{
@Override
public void beforeCommit(boolean readOnly)
{
// Ignore read-only
if (readOnly)
{
return;
}
Set<String> contentUrls = TransactionalResourceHelper.getSet(KEY_PRE_COMMIT_CONTENT_URL_DELETIONS);
long orphanTime = System.currentTimeMillis();
for (String contentUrl : contentUrls)
{
ContentUrlEntity contentUrlEntity = getContentUrlEntityUnreferenced(contentUrl);
if (contentUrlEntity == null)
{
// It is still referenced, so ignore it
continue;
}
// We mark the URL as orphaned.
Long contentUrlId = contentUrlEntity.getId();
updateContentUrlOrphanTime(contentUrlId, orphanTime);
// Pop this in the queue for deletion from the content store
contentStoreCleaner.registerOrphanedContentUrl(contentUrl);
}
contentUrls.clear();
}
}
}