org.alfresco.bm.cm.FileFolderService.java Source code

Java tutorial

Introduction

Here is the source code for org.alfresco.bm.cm.FileFolderService.java

Source

/*
 * Copyright (C) 2005-2014 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.bm.cm;

import java.util.ArrayList;
import java.util.List;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;

import com.mongodb.BasicDBObjectBuilder;
import com.mongodb.DB;
import com.mongodb.DBCollection;
import com.mongodb.DBCursor;
import com.mongodb.DBObject;
import com.mongodb.DuplicateKeyException;
import com.mongodb.WriteConcern;
import com.mongodb.WriteResult;

/**
 * Service to keep track of files and folders.
 *
 * @author Derek Hulley
 * @since 4.0.4
 */
public class FileFolderService implements InitializingBean {
    public static final String FIELD_ID = "_id";
    public static final String FIELD_CONTEXT = "context";
    public static final String FIELD_PATH = "path";
    public static final String FIELD_LEVEL = "level";
    public static final String FIELD_PARENT_PATH = "parentPath";
    public static final String FIELD_NAME = "name";
    public static final String FIELD_FOLDER_COUNT = "folderCount";
    public static final String FIELD_FILE_COUNT = "fileCount";

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

    /** The collection of users, which can be reused by derived extensions. */
    protected final DBCollection collection;

    public FileFolderService(DB db, String collection) {
        this.collection = db.getCollection(collection);
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        checkIndexes();
    }

    /**
     * Ensure that the MongoDB collection has the required indexes associated with
     * this user bean.
     */
    private void checkIndexes() {
        collection.setWriteConcern(WriteConcern.SAFE);

        DBObject uidxCtxPath = BasicDBObjectBuilder.start().add(FIELD_CONTEXT, 1).add(FIELD_PATH, 1).get();
        DBObject optCtxPath = BasicDBObjectBuilder.start().add("name", "uidxCtxPath").add("unique", Boolean.TRUE)
                .get();
        collection.createIndex(uidxCtxPath, optCtxPath);

        DBObject uidxCtxParentName = BasicDBObjectBuilder.start().add(FIELD_CONTEXT, 1).add(FIELD_PARENT_PATH, 1)
                .add(FIELD_NAME, 1).get();
        DBObject optCtxParentName = BasicDBObjectBuilder.start().add("name", "uidxCtxParentName")
                .add("unique", Boolean.FALSE).get();
        collection.createIndex(uidxCtxParentName, optCtxParentName);

        DBObject idxCtxLevel = BasicDBObjectBuilder.start().add(FIELD_CONTEXT, 1).add(FIELD_LEVEL, 1).get();
        DBObject optCtxLevel = BasicDBObjectBuilder.start().add("name", "idxCtxLevel").add("unique", Boolean.FALSE)
                .get();
        collection.createIndex(idxCtxLevel, optCtxLevel);

        DBObject idxCtxFileCount = BasicDBObjectBuilder.start().add(FIELD_CONTEXT, 1).add(FIELD_FILE_COUNT, 1)
                .add(FIELD_LEVEL, 1).get();
        DBObject optCtxFileCount = BasicDBObjectBuilder.start().add("name", "idxCtxFileCount")
                .add("unique", Boolean.FALSE).get();
        collection.createIndex(idxCtxFileCount, optCtxFileCount);

        DBObject idxCtxFolderCount = BasicDBObjectBuilder.start().add(FIELD_CONTEXT, 1).add(FIELD_FOLDER_COUNT, 1)
                .add(FIELD_LEVEL, 1).get();
        DBObject optCtxFolderCount = BasicDBObjectBuilder.start().add("name", "idxCtxFolderCount")
                .add("unique", Boolean.FALSE).get();
        collection.createIndex(idxCtxFolderCount, optCtxFolderCount);
    }

    /**
     * Helper to convert a Mongo DBObject into the API consumable object
     * <p/>
     * Note that <tt>null</tt> is handled as a <tt>null</tt> return.
     */
    protected FolderData fromDBObject(DBObject folderDataObj) {
        if (folderDataObj == null) {
            return null;
        }

        String id = (String) folderDataObj.get(FIELD_ID);
        String context = (String) folderDataObj.get(FIELD_CONTEXT);
        String path = (String) folderDataObj.get(FIELD_PATH);
        Long folderCount = (Long) folderDataObj.get(FIELD_FOLDER_COUNT);
        Long fileCount = (Long) folderDataObj.get(FIELD_FILE_COUNT);
        FolderData folderData = new FolderData(id, context, path, folderCount, fileCount);
        // Done
        return folderData;
    }

    /**
     * Turn a cursor into an array of API-friendly objects
     */
    protected List<FolderData> fromDBCursor(DBCursor cursor) {
        int count = cursor.count();
        try {
            List<FolderData> folderDatas = new ArrayList<FolderData>(count);
            while (cursor.hasNext()) {
                DBObject folderDataObj = cursor.next();
                FolderData folderData = fromDBObject(folderDataObj);
                folderDatas.add(folderData);
            }
            // Done
            return folderDatas;
        } finally {
            cursor.close();
        }
    }

    /**
     * Create a new folder entry with the given data.
     * <p/>
     * The file count is assumed to be zero.
     */
    public void createNewFolder(String id, String context, String path) {
        FolderData folder = new FolderData(id, context, path, 0L, 0L);
        createNewFolder(folder);
    }

    /**
     * Create a new folder entry with the given data
     */
    public void createNewFolder(FolderData data) {
        BasicDBObjectBuilder insertObjBuilder = BasicDBObjectBuilder.start().add(FIELD_ID, data.getId())
                .add(FIELD_CONTEXT, data.getContext()).add(FIELD_PATH, data.getPath())
                .add(FIELD_LEVEL, data.getLevel()).add(FIELD_PARENT_PATH, data.getParentPath())
                .add(FIELD_NAME, data.getName()).add(FIELD_FOLDER_COUNT, data.getFolderCount())
                .add(FIELD_FILE_COUNT, data.getFileCount());
        DBObject insertObj = insertObjBuilder.get();

        try {
            collection.insert(insertObj);
        } catch (DuplicateKeyException e) {
            // We just rethrow as per the API
            throw e;
        }
    }

    /**
     * Delete a folder entry based on the folder ID
     * 
     * @param id                the folder ID
     */
    public int deleteFolder(String context, String path, boolean cascade) {
        int deleted = 0;
        // Delete primary
        {
            DBObject queryObj = BasicDBObjectBuilder.start().add(FIELD_CONTEXT, context).add(FIELD_PATH, path)
                    .get();
            WriteResult wr = collection.remove(queryObj);
            deleted += wr.getN();
        }
        // Cascade
        if (cascade) {
            DBObject queryObj = BasicDBObjectBuilder.start().add(FIELD_CONTEXT, context).push(FIELD_PATH)
                    .add("$regex", "^" + path + "/").pop().get();
            WriteResult wr = collection.remove(queryObj);
            deleted += wr.getN();
        }
        // Done
        if (logger.isDebugEnabled()) {
            logger.debug("Deleted folder: \n" + "   Context:        " + context + "\n" + "   Path:           "
                    + path + "\n" + "   Cascade:        " + cascade + "\n" + "   Deleted:        " + deleted);
        }
        return deleted;
    }

    /**
     * Retrieve a folder by the ID
     * 
     * @param id                the folder id
     * @return                  the folder data or <tt>null</tt> if it does not exist
     */
    public FolderData getFolder(String id) {
        DBObject queryObj = BasicDBObjectBuilder.start().add(FIELD_ID, id).get();
        DBObject folderDataObj = collection.findOne(queryObj);
        FolderData folderData = fromDBObject(folderDataObj);
        return folderData;
    }

    /**
     * Retrieve a folder by the path
     * 
     * @param context           the context in which the folder path is valid (mandatory)
     * @param path              the folder path relative to the given context
     * @return                  the folder data or <tt>null</tt> if it does not exist
     */
    public FolderData getFolder(String context, String path) {
        DBObject queryObj = BasicDBObjectBuilder.start().add(FIELD_CONTEXT, context).add(FIELD_PATH, path).get();
        DBObject folderDataObj = collection.findOne(queryObj);
        FolderData folderData = fromDBObject(folderDataObj);
        return folderData;
    }

    /**
     * Increment the count of the subfolders in a folder.
     * 
     * @param context           the context in which the folder path is valid (mandatory)
     * @param path              the folder path relative to the given context
     * @param fileCountInc      the file count increment (can be negative)
     */
    public void incrementFolderCount(String context, String path, long folderCountInc) {
        DBObject queryObj = BasicDBObjectBuilder.start().add(FIELD_CONTEXT, context).add(FIELD_PATH, path).get();
        DBObject updateObj = BasicDBObjectBuilder.start().push("$inc").add(FIELD_FOLDER_COUNT, folderCountInc).pop()
                .get();
        WriteResult result = collection.update(queryObj, updateObj);
        if (result.getN() != 1) {
            throw new RuntimeException("Failed to update folder's subfolder count: \n" + "   Context:  " + context
                    + "\n" + "   Path:     " + path + "\n" + "   Result:   " + result);
        }
        // Done
        if (logger.isDebugEnabled()) {
            logger.debug("Incremented the subfolder count on " + context + "/" + path + " by " + folderCountInc);
        }
    }

    /**
     * Increment the count of the files in a folder.
     * 
     * @param context           the context in which the folder path is valid (mandatory)
     * @param path              the folder path relative to the given context
     * @param fileCountInc      the file count increment (can be negative)
     */
    public void incrementFileCount(String context, String path, long fileCountInc) {
        DBObject queryObj = BasicDBObjectBuilder.start().add(FIELD_CONTEXT, context).add(FIELD_PATH, path).get();
        DBObject updateObj = BasicDBObjectBuilder.start().push("$inc").add(FIELD_FILE_COUNT, fileCountInc).pop()
                .get();
        WriteResult result = collection.update(queryObj, updateObj);
        if (result.getN() != 1) {
            throw new RuntimeException("Failed to update folder's file count: \n" + "   Context:  " + context + "\n"
                    + "   Path:     " + path + "\n" + "   Result:   " + result);
        }
        // Done
        if (logger.isDebugEnabled()) {
            logger.debug("Incremented the file count on " + context + "/" + path + " by " + fileCountInc);
        }
    }

    /**
     * Produces a count of the folders that have the given context and path as a <b>parent</b> folder.
     * <p/>
     * If the following files exist:
     * <pre>
     *      /home/tests/a
     *      /home/tests/b
     * </pre>
     * then the counts for:
     * <pre>
     *      /home/tests
     * </pre>
     * will be <tt>2</tt>
     * 
     * @return      the folder count
     */
    public long countChildFolders(String context, String path) {
        DBObject queryObj = BasicDBObjectBuilder.start().add(FIELD_CONTEXT, context).add(FIELD_PARENT_PATH, path)
                .get();
        long count = collection.count(queryObj);
        // Done
        if (logger.isDebugEnabled()) {
            logger.debug("Count of children for " + context + path + ": " + count);
        }
        return count;
    }

    /**
     * Count folders without either subfolders or files
     * 
     * @return                  count of folders without subfolders or files
     */
    public long countEmptyFolders(String context) {
        DBObject queryObj = BasicDBObjectBuilder.start().add(FIELD_CONTEXT, context).add(FIELD_FOLDER_COUNT, 0L)
                .add(FIELD_FILE_COUNT, 0L).get();
        long count = collection.count(queryObj);
        // Done
        if (logger.isDebugEnabled()) {
            logger.debug("There are " + count + " empty folders in context '" + context + "'.");
        }
        return count;
    }

    /**
     * Get a list of folders that have the given context and path as a <b>parent</b> folder.
     * <p/>
     * If the path given is:
     * <pre>
     *      /home/tests
     * </pre>
     * then the following results can be returned:
     * <pre>
     *      /home/tests/a
     *      /home/tests/b
     * </pre>
     * 
     * @param context           the context in which the folder path is valid (mandatory)
     * @param path              the path that will be the parent of all child folders returned
     * @param skip              the number of entries to skip
     * @param limit             the number of entries to return
     * @return                  the child folders
     */
    public List<FolderData> getChildFolders(String context, String path, int skip, int limit) {
        DBObject queryObj = BasicDBObjectBuilder.start().add(FIELD_CONTEXT, context).add(FIELD_PARENT_PATH, path)
                .get();
        DBObject sortObj = BasicDBObjectBuilder.start().add(FIELD_CONTEXT, 1).add(FIELD_PARENT_PATH, 1).get();
        DBCursor cursor = collection.find(queryObj).sort(sortObj).skip(skip).limit(limit);
        List<FolderData> results = fromDBCursor(cursor);
        // Done
        if (logger.isDebugEnabled()) {
            logger.debug("Found " + results.size() + " results in folder " + context + path);
        }
        return results;
    }

    /**
     * Get a list of folders filtered by the number of child files and/or folders, returning
     * results sorted according to the parameters supplied.
     * <p/>
     * Apart from the context, all parametes are optional.  However, for best performance,
     * do not mix the file and folder levels; the underlying query performance will be OK
     * but the sorting will not be ideal.
     * <p/>
     * The sort precedence is <b>folderCount-fileCount</b>.
     * 
     * @param context           the context in which the folder path is valid (mandatory)
     * @param minLevel          the minimum folder level to consider (inclusive, optional)
     * @param maxLevel          the maximum folder level to consider (inclusive, optional)
     * @param minFiles          the minimum number of files in the folder (inclusive, optional)
     * @param maxFiles          the maximum number of files in the folder (inclusive, optional)
     * @param skip              the number of entries to skip
     * @param limit             the number of entries to return
     * @return                  the folders with the correct number of children
     */
    public List<FolderData> getFoldersByCounts(String context, Long minLevel, Long maxLevel, Long minFolders,
            Long maxFolders, Long minFiles, Long maxFiles, int skip, int limit) {
        if (context == null) {
            throw new IllegalArgumentException();
        }

        BasicDBObjectBuilder queryObjBuilder = BasicDBObjectBuilder.start();
        BasicDBObjectBuilder sortObjBuilder = BasicDBObjectBuilder.start();

        queryObjBuilder.add(FIELD_CONTEXT, context);
        if (minLevel != null || maxLevel != null) {
            queryObjBuilder.push(FIELD_LEVEL);
            {
                if (minLevel != null) {
                    queryObjBuilder.add("$gte", minLevel);
                }
                if (maxLevel != null) {
                    queryObjBuilder.add("$lte", maxLevel);
                }
                // No sorting by level!
            }
            queryObjBuilder.pop();
        }
        if (minFolders != null || maxFolders != null) {
            queryObjBuilder.push(FIELD_FOLDER_COUNT);
            {
                if (minFolders != null) {
                    queryObjBuilder.add("$gte", minFolders);
                }
                if (maxFolders != null) {
                    queryObjBuilder.add("$lte", maxFolders);
                }
                // We have to sort by the counts
                sortObjBuilder.add(FIELD_FOLDER_COUNT, 1);
            }
            queryObjBuilder.pop();
        }
        if (minFiles != null || maxFiles != null) {
            queryObjBuilder.push(FIELD_FILE_COUNT);
            {
                if (minFiles != null) {
                    queryObjBuilder.add("$gte", minFiles);
                }
                if (maxFiles != null) {
                    queryObjBuilder.add("$lte", maxFiles);
                }
                // We have to sort by the counts
                sortObjBuilder.add(FIELD_FILE_COUNT, 1);
            }
            queryObjBuilder.pop();
        }
        DBObject queryObj = queryObjBuilder.get();
        DBObject sortObj = sortObjBuilder.get();

        DBCursor cursor = collection.find(queryObj).sort(sortObj).skip(skip).limit(limit);
        List<FolderData> results = fromDBCursor(cursor);
        // Done
        if (logger.isDebugEnabled()) {
            logger.debug("Found " + results.size() + " results for file counts: \n" + "   context:    " + context
                    + "\n" + "   minLevel:   " + minLevel + "\n" + "   maxLevel:   " + maxLevel + "\n"
                    + "   minFiles:   " + minFiles + "\n" + "   maxFiles:   " + maxFiles + "\n" + "   skip:       "
                    + skip + "\n" + "   limit:      " + limit);
        }
        return results;
    }
}