org.alfresco.repo.content.filestore.FileContentStore.java Source code

Java tutorial

Introduction

Here is the source code for org.alfresco.repo.content.filestore.FileContentStore.java

Source

/*
 * #%L
 * Alfresco Repository
 * %%
 * Copyright (C) 2005 - 2016 Alfresco Software Limited
 * %%
 * This file is part of the Alfresco software. 
 * If the software was purchased under a paid Alfresco license, the terms of 
 * the paid license agreement will prevail.  Otherwise, the software is 
 * provided under the following open source license terms:
 * 
 * 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/>.
 * #L%
 */
package org.alfresco.repo.content.filestore;

import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Map;

import org.alfresco.api.AlfrescoPublicApi;
import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.repo.content.AbstractContentStore;
import org.alfresco.repo.content.ContentContext;
import org.alfresco.repo.content.ContentStore;
import org.alfresco.repo.content.ContentStoreCreatedEvent;
import org.alfresco.repo.content.EmptyContentReader;
import org.alfresco.repo.content.UnsupportedContentUrlException;
import org.alfresco.service.cmr.repository.ContentIOException;
import org.alfresco.service.cmr.repository.ContentReader;
import org.alfresco.service.cmr.repository.ContentWriter;
import org.alfresco.util.Deleter;
import org.alfresco.util.GUID;
import org.alfresco.util.Pair;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;

/**
 * Provides a store of node content directly to the file system.  The writers
 * are generated using information from the {@link ContentContext simple content context}.
 * <p>
 * The file names obey, as they must, the URL naming convention
 * as specified in the {@link org.alfresco.repo.content.ContentStore ContentStore interface}.<br/>
 * The protocols handled are:
 * <ul>
 *   <li><b>{@link #STORE_PROTOCOL store}</b>: These URLs can be generated by this implementation and are file references within the root directory.</li>
 *   <li><b>{@link #SPOOF_PROTOCOL spoof}</b>: These URLs are never generated by the implementation but represent spoofed binary text stream data: TODO</li>
 * </ul>
 * 
 * @author Derek Hulley
 */
@AlfrescoPublicApi
public class FileContentStore extends AbstractContentStore
        implements ApplicationContextAware, ApplicationListener<ApplicationEvent> {
    /**
     * <b>store</b> is the default prefix for file content URLs
     * @see ContentStore#PROTOCOL_DELIMITER
     */
    public static final String STORE_PROTOCOL = "store";
    public static final String SPOOF_PROTOCOL = "spoof";

    private static final Log logger = LogFactory.getLog(FileContentStore.class);

    private File rootDirectory;
    private String rootAbsolutePath;
    private boolean allowRandomAccess;
    private boolean readOnly;
    private ApplicationContext applicationContext;
    private boolean deleteEmptyDirs = true;

    /**
     * Private: for Spring-constructed instances only.
     * 
     * @param rootDirectoryStr
     *            the root under which files will be stored. The directory will be created if it does not exist.
     * @see FileContentStore#FileContentStore(File)
     */
    /*package*/ FileContentStore(String rootDirectoryStr) {
        this(new File(rootDirectoryStr));
    }

    /**
     * Private: for Spring-constructed instances only.
     * 
     * @param rootDirectory
     *            the root under which files will be stored. The directory will be created if it does not exist.
     */
    private FileContentStore(File rootDirectory) {
        if (!rootDirectory.exists()) {
            if (!rootDirectory.mkdirs()) {
                throw new ContentIOException("Failed to create store root: " + rootDirectory, null);
            }
        }
        this.rootDirectory = rootDirectory.getAbsoluteFile();
        rootAbsolutePath = rootDirectory.getAbsolutePath();
        allowRandomAccess = true;
        readOnly = false;
    }

    /**
     * Public constructor for programmatic use.
     * 
     * @param context
     *            application context through which events can be published
     * @param rootDirectoryStr
     *            the root under which files will be stored. The directory will be created if it does not exist.
     * @see FileContentStore#FileContentStore(File)
     */
    public FileContentStore(ApplicationContext context, String rootDirectoryStr) {
        this(rootDirectoryStr);
        setApplicationContext(context);
        publishEvent(context, Collections.<String, Serializable>emptyMap());
    }

    /**
     * Public constructor for programmatic use.
     * 
     * @param context
     *            application context through which events can be published
     * @param rootDirectory
     *            the root under which files will be stored. The directory will be created if it does not exist.
     */
    public FileContentStore(ApplicationContext context, File rootDirectory) {
        this(rootDirectory);
        setApplicationContext(context);
        publishEvent(context, Collections.<String, Serializable>emptyMap());
    }

    public FileContentStore(ApplicationContext context, File rootDirectory,
            Map<String, Serializable> extendedEventParams) {
        this(rootDirectory);
        setApplicationContext(context);
        publishEvent(context, extendedEventParams);
    }

    public String toString() {
        StringBuilder sb = new StringBuilder(36);
        sb.append("FileContentStore").append("[ root=").append(rootDirectory).append(", allowRandomAccess=")
                .append(allowRandomAccess).append(", readOnly=").append(readOnly).append("]");
        return sb.toString();
    }

    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    /**
     * Stores may optionally produce readers and writers that support random access.
     * Switch this off for this store by setting this to <tt>false</tt>.
     * <p>
     * This switch is primarily used during testing to ensure that the system has the
     * ability to spoof random access in cases where the store is unable to produce
     * readers and writers that allow random access.  Typically, stream-based access
     * would be an example.
     * 
     * @param allowRandomAccess true to allow random access, false to have it faked
     */
    public void setAllowRandomAccess(boolean allowRandomAccess) {
        this.allowRandomAccess = allowRandomAccess;
    }

    /**
     * File stores may optionally be declared read-only.  This is useful when configuring
     * a store, possibly temporarily, to act as a source of data but to preserve it against
     * any writes.
     * 
     * @param readOnly      <tt>true</tt> to force the store to only allow reads.
     */
    public void setReadOnly(boolean readOnly) {
        this.readOnly = readOnly;
    }

    /**
     * Generates a new URL and file appropriate to it.
     * 
     * @return Returns a new and unique file
     * @throws IOException if the file or parent directories couldn't be created
     */
    /*package*/ File createNewFile() throws IOException {
        String contentUrl = FileContentStore.createNewFileStoreUrl();
        return createNewFile(contentUrl);
    }

    /**
     * Creates a file for the specifically provided content URL.  The URL may
     * not already be in use.
     * <p>
     * The store prefix is stripped off the URL and the rest of the URL
     * used directly to create a file.
     * 
     * @param newContentUrl the specific URL to use, which may not be in use
     * @return Returns a new and unique file
     * @throws IOException
     *             if the file or parent directories couldn't be created or if the URL is already in use.
     * @throws UnsupportedOperationException
     *             if the store is read-only
     * 
     * @see #setReadOnly(boolean)
     */
    /*package*/ File createNewFile(String newContentUrl) throws IOException {
        if (readOnly) {
            throw new UnsupportedOperationException("This store is currently read-only: " + this);
        }

        File file = makeFile(newContentUrl);

        // create the directory, if it doesn't exist
        File dir = file.getParentFile();
        if (!dir.exists()) {
            makeDirectory(dir);
        }

        // create a new, empty file
        boolean created = file.createNewFile();
        if (!created) {
            throw new ContentIOException(
                    "When specifying a URL for new content, the URL may not be in use already. \n" + "   store: "
                            + this + "\n" + "   new URL: " + newContentUrl);
        }

        // done
        return file;
    }

    /**
     * Synchronized and retrying directory creation.  Repeated attempts will be made to create the
     * directory, subject to a limit on the number of retries.
     * 
     * @param dir               the directory to create
     * @throws IOException      if an IO error occurs
     */
    private synchronized void makeDirectory(File dir) throws IOException {
        /*
         * Once in this method, the only contention will be from other file stores or processes.
         * This is OK as we have retrying to sort it out.
         */
        if (dir.exists()) {
            // Beaten to it during synchronization
            return;
        }
        // 20 attempts with 20 ms wait each time
        for (int i = 0; i < 20; i++) {
            boolean created = dir.mkdirs();
            if (created) {
                // Successfully created
                return;
            }
            // Wait
            try {
                this.wait(20L);
            } catch (InterruptedException e) {
            }
            // Did it get created in the meantime
            if (dir.exists()) {
                // Beaten to it while asleep
                return;
            }
        }
        // It still didn't succeed
        throw new ContentIOException("Failed to create directory for file storage: " + dir);
    }

    /**
     * Takes the file absolute path, strips off the root path of the store
     * and appends the store URL prefix.
     * 
     * @param file the file from which to create the URL
     * @return Returns the equivalent content URL
     */
    /*package*/ String makeContentUrl(File file) {
        String path = file.getAbsolutePath();
        // check if it belongs to this store
        if (!path.startsWith(rootAbsolutePath)) {
            throw new AlfrescoRuntimeException("File does not fall below the store's root: \n" + "   file: " + file
                    + "\n" + "   store: " + this);
        }
        // strip off the file separator char, if present
        int index = rootAbsolutePath.length();
        if (path.charAt(index) == File.separatorChar) {
            index++;
        }
        // strip off the root path and adds the protocol prefix
        String url = FileContentStore.STORE_PROTOCOL + ContentStore.PROTOCOL_DELIMITER + path.substring(index);
        // replace '\' with '/' so that URLs are consistent across all filesystems
        url = url.replace('\\', '/');
        // done
        return url;
    }

    /**
     * Creates a file from the given relative URL.
     * 
     * @param contentUrl    the content URL including the protocol prefix
     * @return              Returns a file representing the URL - the file may or may not
     *                      exist
     * @throws UnsupportedContentUrlException
     *                      if the URL is invalid and doesn't support the
     *                      {@link FileContentStore#STORE_PROTOCOL correct protocol}
     */
    /*package*/ File makeFile(String contentUrl) {
        // take just the part after the protocol
        Pair<String, String> urlParts = super.getContentUrlParts(contentUrl);
        String protocol = urlParts.getFirst();
        String relativePath = urlParts.getSecond();
        // Check the protocol
        if (!protocol.equals(FileContentStore.STORE_PROTOCOL)) {
            throw new UnsupportedContentUrlException(this, protocol + PROTOCOL_DELIMITER + relativePath);
        }
        // get the file
        File file = new File(rootDirectory, relativePath);

        ensureFileInContentStore(file);

        // done
        return file;
    }

    /**
     * @return Returns <tt>true</tt> always
     */
    public boolean isWriteSupported() {
        return !readOnly;
    }

    /**
     * Performs a direct check against the file for its existence.
     * For {@link #SPOOF_PROTOCOL spoofed} URLs, the URL always exists.
     */
    @Override
    public boolean exists(String contentUrl) {
        if (contentUrl.startsWith(SPOOF_PROTOCOL)) {
            return true;
        } else {
            File file = makeFile(contentUrl);
            return file.exists();
        }
    }

    /**
     * Get the filesystem's free space.
     * 
     * @return          Returns the root directory partition's {@link File#getFreeSpace() free space}
     */
    @Override
    public long getSpaceFree() {
        return rootDirectory.getFreeSpace();
    }

    /**
     * Get the filesystem's total space.
     * 
     * @return          Returns the root directory partition's {@link File#getTotalSpace() total space}
     */
    @Override
    public long getSpaceTotal() {
        return rootDirectory.getTotalSpace();
    }

    /**
     * @return          Returns the canonical path to the root directory
     */
    @Override
    public String getRootLocation() {
        try {
            return rootDirectory.getCanonicalPath();
        } catch (Throwable e) {
            logger.warn("Unabled to return root location", e);
            return super.getRootLocation();
        }
    }

    /**
     * This implementation requires that the URL start with
     * {@link FileContentStore#STORE_PROTOCOL } or {@link FileContentStore#SPOOF_PROTOCOL }
     */
    public ContentReader getReader(String contentUrl) {
        // Handle the spoofed URL
        if (contentUrl.startsWith(SPOOF_PROTOCOL)) {
            return new SpoofedTextContentReader(contentUrl);
        }
        // else, it's a real file we are after
        try {
            File file = makeFile(contentUrl);
            ContentReader reader = null;
            if (file.exists()) {
                FileContentReader fileContentReader = new FileContentReader(file, contentUrl);
                fileContentReader.setAllowRandomAccess(allowRandomAccess);
                reader = fileContentReader;
            } else {
                reader = new EmptyContentReader(contentUrl);
            }

            // done
            if (logger.isDebugEnabled()) {
                logger.debug("Created content reader: \n" + "   url: " + contentUrl + "\n" + "   file: " + file
                        + "\n" + "   reader: " + reader);
            }
            return reader;
        } catch (UnsupportedContentUrlException e) {
            // This can go out directly
            throw e;
        } catch (Throwable e) {
            throw new ContentIOException("Failed to get reader for URL: " + contentUrl, e);
        }
    }

    /**
     * Returns a writer onto a location based on the date.
     * 
     * @param existingContentReader
     *            the existing content reader
     * @param newContentUrl
     *            the new content url
     * @return Returns a writer onto a location based on the date
     */
    public ContentWriter getWriterInternal(ContentReader existingContentReader, String newContentUrl) {
        try {
            File file = null;
            String contentUrl = null;
            if (newContentUrl == null) // a specific URL was not supplied
            {
                // get a new file with a new URL
                file = createNewFile();
                // make a URL
                contentUrl = makeContentUrl(file);
            } else // the URL has been given
            {
                file = createNewFile(newContentUrl);
                contentUrl = newContentUrl;
            }
            // create the writer
            FileContentWriter writer = new FileContentWriter(file, contentUrl, existingContentReader);

            if (contentLimitProvider != null) {
                writer.setContentLimitProvider(contentLimitProvider);
            }
            writer.setAllowRandomAccess(allowRandomAccess);

            // done
            if (logger.isDebugEnabled()) {
                logger.debug("Created content writer: \n" + "   writer: " + writer);
            }
            return writer;
        } catch (IOException e) {
            throw new ContentIOException("Failed to get writer", e);
        }
    }

    /**
     * Gets the urls.
     * 
     * @param createdAfter
     *            the created after date
     * @param createdBefore
     *            the created before dat6e
     * @param handler
     *            the handler
     */
    @SuppressWarnings("deprecation")
    public void getUrls(Date createdAfter, Date createdBefore, ContentUrlHandler handler) {
        // recursively get all files within the root
        getUrls(rootDirectory, handler, createdAfter, createdBefore);
        // done
        if (logger.isDebugEnabled()) {
            logger.debug("Listed all content URLS: \n" + "   store: " + this);
        }
    }

    /**
     * Returns a list of all files within the given directory and all subdirectories.
     * @param directory the current directory to get the files from
     * @param handler the callback to use for each URL
     * @param createdAfter only get URLs for content create after this date
     * @param createdBefore only get URLs for content created before this date
     */
    @SuppressWarnings("deprecation")
    private void getUrls(File directory, ContentUrlHandler handler, Date createdAfter, Date createdBefore) {
        File[] files = directory.listFiles();
        if (files == null) {
            // the directory has disappeared
            throw new ContentIOException("Failed list files in folder: " + directory);
        }
        for (File file : files) {
            if (file.isDirectory()) {
                // we have a subdirectory - recurse
                getUrls(file, handler, createdAfter, createdBefore);
            } else {
                // check the created date of the file
                long lastModified = file.lastModified();
                if (createdAfter != null && lastModified < createdAfter.getTime()) {
                    // file is too old
                    continue;
                } else if (createdBefore != null && lastModified > createdBefore.getTime()) {
                    // file is too young
                    continue;
                }
                // found a file - create the URL
                String contentUrl = makeContentUrl(file);
                // Callback
                handler.handle(contentUrl);
            }
        }
    }

    /**
     * Attempts to delete the content.  The actual deletion is optional on the interface
     * so it just returns the success or failure of the underlying delete.
     * 
     * @throws UnsupportedOperationException        if the store is read-only
     * 
     * @see #setReadOnly(boolean)
     */
    public boolean delete(String contentUrl) {
        if (readOnly) {
            throw new UnsupportedOperationException("This store is currently read-only: " + this);
        }
        if (contentUrl.startsWith(SPOOF_PROTOCOL)) {
            // This is not a failure but the content can never actually be deleted
            return false;
        }
        // Handle regular files based on the real files
        File file = makeFile(contentUrl);
        boolean deleted = false;
        if (!file.exists()) {
            // File does not exist
            deleted = true;
        } else {
            deleted = file.delete();
        }

        // Delete empty parents regardless of whether the file was ignore above.
        if (deleteEmptyDirs && deleted) {
            Deleter.deleteEmptyParents(file, getRootLocation());
        }

        // done
        if (logger.isDebugEnabled()) {
            logger.debug("Delete content directly: \n" + "   store: " + this + "\n" + "   url: " + contentUrl);
        }
        return deleted;
    }

    /**
     * Creates a new content URL.  This must be supported by all
     * stores that are compatible with Alfresco.
     * 
     * @return Returns a new and unique content URL
     */
    public static String createNewFileStoreUrl() {
        Calendar calendar = new GregorianCalendar();
        int year = calendar.get(Calendar.YEAR);
        int month = calendar.get(Calendar.MONTH) + 1; // 0-based
        int day = calendar.get(Calendar.DAY_OF_MONTH);
        int hour = calendar.get(Calendar.HOUR_OF_DAY);
        int minute = calendar.get(Calendar.MINUTE);
        // create the URL
        StringBuilder sb = new StringBuilder(20);
        sb.append(FileContentStore.STORE_PROTOCOL).append(ContentStore.PROTOCOL_DELIMITER).append(year).append('/')
                .append(month).append('/').append(day).append('/').append(hour).append('/').append(minute)
                .append('/').append(GUID.generate()).append(".bin");
        String newContentUrl = sb.toString();
        // done
        return newContentUrl;
    }

    /**
     * Publishes an event to the application context that will notify any interested parties of the existence of this
     * content store.
     * 
     * @param context
     *            the application context
     * @param extendedEventParams 
     */
    private void publishEvent(ApplicationContext context, Map<String, Serializable> extendedEventParams) {
        context.publishEvent(new ContentStoreCreatedEvent(this, extendedEventParams));
    }

    public void onApplicationEvent(ApplicationEvent event) {
        // Once the context has been refreshed, we tell other interested beans about the existence of this content store
        // (e.g. for monitoring purposes)
        if (event instanceof ContextRefreshedEvent && event.getSource() == this.applicationContext) {
            publishEvent(((ContextRefreshedEvent) event).getApplicationContext(),
                    Collections.<String, Serializable>emptyMap());
        }
    }

    /**
     * Configure the FileContentStore to delete empty parent directories upon deleting a content URL.
     * 
     * @param deleteEmptyDirs the deleteEmptyDirs to set
     */
    public void setDeleteEmptyDirs(boolean deleteEmptyDirs) {
        this.deleteEmptyDirs = deleteEmptyDirs;
    }

    /*
     * Added as fix for MNT-12301, we should ensure that content store accesses content only inside of store root
     */
    private void ensureFileInContentStore(File file) {
        String fileNormalizedAbsoultePath = FilenameUtils.normalize(file.getAbsolutePath());
        String rootNormalizedAbsolutePath = FilenameUtils.normalize(rootAbsolutePath);

        if (!fileNormalizedAbsoultePath.startsWith(rootNormalizedAbsolutePath)) {
            throw new ContentIOException("Access to files outside of content store root is not allowed: " + file);
        }
    }
}