/*
 * 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 
* This provides basic services such as caching, but defers to the underlying implementation * for CRUD operations. *
 * The DAO deals in {@link ContentData} instances.  The cache is primarily present to decode
 * IDs into ContentData 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 ControlDAO controlDAO;
    private MimetypeDAO mimetypeDAO;
    private EncodingDAO encodingDAO;
    private LocaleDAO localeDAO;
    private EagerContentStoreCleaner contentStoreCleaner;
    /**
     * Cache for the ContentData class:
     * KEY: ID
     * VALUE: ContentData object
     * VALUE KEY: NONE
     */
    private EntityLookupCacheContentData 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).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()).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 true we won't worry about eagerly deleting the content on transaction rollback
     */
    private ContentUrlEntity getOrCreateContentUrlEntity(String contentUrl, long size)
    {
        // Try to insert the content first.  Usually, the insert will not clash with anything
        // as content URL re-use is far less frequent than new content creation.
        ContentUrlEntity contentUrlEntity = null;
        Savepoint savepoint = controlDAO.createSavepoint("getOrCreateContentUrlEntity");
        try
        {
            contentUrlEntity = createContentUrlEntity(contentUrl, size);
            controlDAO.releaseSavepoint(savepoint);
        }
        catch (RuntimeException e)
        {
            controlDAO.rollbackToSavepoint(savepoint);
            // See if this was caused by an existing URL
            contentUrlEntity = getContentUrlEntity(contentUrl);
            // If it exists, then we can just re-use it, but check that the size is consistent
            if (contentUrlEntity == null)
            {
                // The error was caused by something else.  Perhaps another, as-yet-unseen
                // row clashes with this.  Just propagate the exception and let retrying
                // happen as required.
                throw e;
            }
            // 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
            Long oldOrphanTime = contentUrlEntity.getOrphanTime();
            if (oldOrphanTime != null)
            {
                Long id = contentUrlEntity.getId();
                int updated = updateContentUrlOrphanTime(id, null, oldOrphanTime);
                if (updated == 0)
                {
                    throw new ConcurrencyFailureException("Failed to remove orphan time: " + contentUrlEntity);
                }
            }
        }
        // Done
        return contentUrlEntity;
    }
    /**
     * @param contentUrl    the content URL to create or search for
     */
    protected abstract ContentUrlEntity createContentUrlEntity(String contentUrl, long size);
    
    /**
     * @param id            the ID of the content url entity
     * @return              Return the entity or null if it doesn't exist
     */
    protected abstract ContentUrlEntity getContentUrlEntity(Long id);
    
    /**
     * @param contentUrl    the URL of the content url entity
     * @return              Return the entity or null if it doesn't exist
     */
    protected abstract ContentUrlEntity getContentUrlEntity(String contentUrl);
    
    /**
     * @param contentUrl    the URL of the content url entity
     * @return              Return the entity or null if it doesn't exist or is still
     *                      referenced by a content_data 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
     * @param oldOrphanTime the orphan time we expect to update for optimistic locking (may be null)
     * @return              Returns the number of rows updated
     */
    protected abstract int updateContentUrlOrphanTime(Long id, Long orphanTime, Long oldOrphanTime);
    
    /**
     * Create the row for the alf_content_data
     */
    protected abstract ContentDataEntity createContentDataEntity(
            Long contentUrlId,
            Long mimetypeId,
            Long encodingId,
            Long localeId);
    
    /**
     * @param id            the entity ID
     * @return              Returns the entity or null if it doesn't exist
     */
    protected abstract ContentDataEntity getContentDataEntity(Long id);
    
    /**
     * Update an existing alf_content_data 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 content_url entities.
     * 
     * @author Derek Hulley
     */
    public class ContentUrlDeleteTransactionListener extends TransactionListenerAdapter
    {
        @Override
        public void beforeCommit(boolean readOnly)
        {
            // Ignore read-only
            if (readOnly)
            {
                return;
            }
            Set