org.apereo.portal.groups.filesystem.FileSystemGroupStore.java Source code

Java tutorial

Introduction

Here is the source code for org.apereo.portal.groups.filesystem.FileSystemGroupStore.java

Source

/**
 * Licensed to Apereo under one or more contributor license
 * agreements. See the NOTICE file distributed with this work
 * for additional information regarding copyright ownership.
 * Apereo licenses this file to you under the Apache License,
 * Version 2.0 (the "License"); you may not use this file
 * except in compliance with the License.  You may obtain a
 * copy of the License at the following location:
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apereo.portal.groups.filesystem;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FilenameFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.regex.Pattern;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apereo.portal.EntityIdentifier;
import org.apereo.portal.groups.IEntityGroup;
import org.apereo.portal.groups.ILockableEntityGroup;
import org.apereo.portal.groups.EntityGroupImpl;
import org.apereo.portal.groups.EntityImpl;
import org.apereo.portal.groups.GroupServiceConfiguration;
import org.apereo.portal.groups.GroupsException;
import org.apereo.portal.groups.ICompositeGroupService;
import org.apereo.portal.groups.IEntity;
import org.apereo.portal.groups.IEntityGroupStore;
import org.apereo.portal.groups.IEntitySearcher;
import org.apereo.portal.groups.IEntityStore;
import org.apereo.portal.groups.IGroupMember;
import org.apereo.portal.security.IPerson;
import org.apereo.portal.services.GroupService;
import org.apereo.portal.spring.locator.EntityTypesLocator;

/**
 * This class is an <code>IEntityGroupStore</code> that uses the native file
 * system for its back end.  It also implements <code>IEntityStore</code> and
 * a no-op <code>IEntitySearcher</code>.  You can substitute a functional entity
 * searcher by adding it to the group service element for this component in the
 * configuration document, <code>compositeGroupServices.xml</code>.
 * <p>
 * A groups file system looks like this:
 * <p><code>
 * <hr width="100%">
 *  --&nbsp;groups root<br>
 * <blockquote>&nbsp;--&nbsp;org.apereo.portal.ChannelDefinition<br>
 *     <blockquote>&nbsp;--&nbsp;channel definition file<br>
 *                 &nbsp;--&nbsp;channel definition file<br>
 *        ...<br>
 *     </blockquote>
 * &nbsp;--&nbsp;org.apereo.portal.security.IPerson<br>
 *     <blockquote>&nbsp;--&nbsp;person directory<br>
 *         <blockquote>&nbsp;--&nbsp;person file <br>
 *                     &nbsp;--&nbsp;person file <br>
 *                     ...<br>
 *         </blockquote>
 *        &nbsp;--&nbsp;person directory <br>
 *     </blockquote>
 *      etc.<br>
 * </blockquote>
 * <hr width="100%">
 * </code><p>
 * The groups root is a file system directory declared in the group service
 * configuration document, where it is an attribute of the filesystem group
 * service element.  This directory has sub-directories, each named for the
 * underlying entity type that groups in that sub-directory contain.  If a
 * service only contains groups of IPersons, the groups root would have 1
 * sub-directory named org.apereo.portal.security.IPerson.
 * <p>
 * A directory named for a type may contain both sub-directories and files.
 * The sub-directories represent groups that can contain other groups.  The
 * files represent groups that can contain entity as well as group members.
 * The files contain keys, one to a line, and look like this:
 * <p><code>
 * <hr width="100%">
 * #&nbsp;this is a comment<br>
 * #&nbsp;another comment<br>
 * <br>
 * key1 Key One<br>
 * key2<br>
 * group:org$jasig$portal$security$IPerson/someDirectory/someFile<br>
 * key3<br>
 * &nbsp;# comment <br>
 * <hr width="100%">
 *</code><p>
 * Blank lines and lines that start with the <code>COMMENT</code> String (here
 * <code>#</code>) are ignored.  The first token on a non-ignored line is
 * assumed to be a group member key.  If the key starts with the
 * <code>GROUP_PREFIX</code> (here <code>:group</code>), it is treated as a
 * local group key.  Otherwise, it is assumed to be an entity key.  The rest of
 * the tokens on the line are ignored.
 * <p>
 * The file above contains 3 entity keys, <code>key1</code>, <code>key2</code>,
 * and <code>key3</code>, and 1 group key,
 * <code>org$jasig$portal$security$IPerson/someDirectory/someFile</code>.  It
 * represents a group with 3 entity members and 1 group member.  The local key
 * of a group is its file path starting at the type name, with the
 * <code>FileSystemGroupStore.SUBSTITUTE_PERIOD</code> character substituted
 * for the real period character.
 * <p>
 * The store is not implemented as a singleton, so you can have multiple
 * concurrent instances pointing to different groups root directories.
 * <p>
 *
 * @author Dan Ellentuck
 */
public class FileSystemGroupStore implements IEntityGroupStore, IEntityStore, IEntitySearcher {
    private static final Log log = LogFactory.getLog(FileSystemGroupStore.class);
    // File system constants for unix/windows compatibility:
    protected static char FORWARD_SLASH = '/';
    protected static char BACK_SLASH = '\\';

    // Group file constants:
    protected static String COMMENT = "#";
    protected static String GROUP_PREFIX = "group:";

    // The period is legal in filesystem names but could conflict with
    // the node separator in the group key.
    protected static char PERIOD = '.';
    protected static char SUBSTITUTE_PERIOD = '$';
    protected boolean useSubstitutePeriod = false;

    private static String DEBUG_CLASS_NAME = "FileSystemGroupStore";

    // Path to groups root directory.
    private String groupsRootPath;

    // Either back slash or forward slash.
    protected char goodSeparator;
    protected char badSeparator;

    // Cache of retrieved groups.
    private Map cache;

    private FilenameFilter fileFilter = new FileFilter();

    private Class defaultEntityType;

    // Value holder adds last modified timestamp.
    private class GroupHolder {
        private long lastModified = 0;
        private IEntityGroup group;

        protected GroupHolder(IEntityGroup g, long lm) {
            this.group = g;
            this.lastModified = lm;
        }

        protected IEntityGroup getGroup() {
            return group;
        }

        protected long getLastModified() {
            return lastModified;
        }
    }

    private class FileFilter implements FilenameFilter {
        /**
         * Tests if a specified file should be included in a file list.
         *
         * @param   dir    the directory in which the file was found.
         * @param   name   the name of the file.
         * @return  <code>true</code> if and only if the name should be
         * included in the file list; <code>false</code> otherwise.
         */
        public boolean accept(File dir, String name) {
            return (!name.startsWith("#")) && (!name.startsWith("%")) && (!name.startsWith("."))
                    && (!name.endsWith("~")) && (!name.endsWith(".tmp")) && (!name.endsWith(".temp"))
                    && (!name.endsWith(".txt"));
        }
    }

    /**
     * FileSystemGroupStore constructor.
     */
    public FileSystemGroupStore() {
        this(null);
    }

    /**
     * FileSystemGroupStore constructor.
     */
    public FileSystemGroupStore(GroupServiceConfiguration cfg) {
        super();
        initialize(cfg);
    }

    /**
     * @return GroupHolder
     */
    protected GroupHolder cacheGet(String key) {
        return (GroupHolder) getCache().get(key);
    }

    /**
     *
     */
    protected void cachePut(String key, Object val) {
        getCache().put(key, val);
    }

    /**
     *
     */
    protected String conformSeparatorChars(String s) {
        return s.replace(getBadSeparator(), getGoodSeparator());
    }

    /**
     * Delete this <code>IEntityGroup</code> from the data store.  We assume that
     * groups will be deleted via the file system, not the group service.
     * @param group org.apereo.portal.groups.IEntityGroup
     */
    public void delete(IEntityGroup group) throws GroupsException {
        throw new UnsupportedOperationException("FileSystemGroupStore.delete() not supported");
    }

    /**
     * Returns an instance of the <code>IEntityGroup</code> from the data store.
     * @return org.apereo.portal.groups.IEntityGroup
     * @param file java.io.File
     */
    private IEntityGroup find(File file) throws GroupsException {
        return find(getKeyFromFile(file));
    }

    /**
     * Returns an instance of the <code>IEntityGroup</code> from the data store.
     * @return org.apereo.portal.groups.IEntityGroup
     * @param key java.lang.String
     */
    public IEntityGroup find(String key) throws GroupsException {
        if (log.isDebugEnabled()) {
            log.debug(DEBUG_CLASS_NAME + ".find(): group key: " + key);
        }

        String path = getFilePathFromKey(key);
        File f = new File(path);

        GroupHolder groupHolder = cacheGet(key);

        if (groupHolder == null || (groupHolder.getLastModified() != f.lastModified())) {
            if (log.isDebugEnabled()) {
                log.debug(DEBUG_CLASS_NAME + ".find(): retrieving group from file system for " + path);
            }

            if (!f.exists()) {
                if (log.isDebugEnabled()) {
                    log.debug(DEBUG_CLASS_NAME + ".find(): file does not exist: " + path);
                }
                return null;
            }

            IEntityGroup group = newInstance(f);
            groupHolder = new GroupHolder(group, f.lastModified());
            cachePut(key, groupHolder);
        }
        return groupHolder.getGroup();
    }

    /**
     * Returns an <code>Iterator</code> over the <code>Collection</code> of
     * <code>IEntityGroups</code> that the <code>IEntity</code> belongs to.
     * @return java.util.Iterator
     * @param ent org.apereo.portal.groups.IEntityGroup
     */
    protected Iterator findParentGroups(IEntity ent) throws GroupsException {
        if (log.isDebugEnabled())
            log.debug(DEBUG_CLASS_NAME + ".findParentGroups(): for " + ent);

        List groups = new ArrayList();
        File root = getFileRoot(ent.getType());
        if (root != null) {
            File[] files = getAllFilesBelow(root);

            try {
                for (int i = 0; i < files.length; i++) {
                    Collection ids = getEntityIdsFromFile(files[i]);
                    if (ids.contains(ent.getKey())) {
                        groups.add(find(files[i]));
                    }
                }
            } catch (IOException ex) {
                throw new GroupsException("Problem reading group files", ex);
            }
        }

        return groups.iterator();
    }

    /**
     * Returns an <code>Iterator</code> over the <code>Collection</code> of
     * <code>IEntityGroups</code> that the <code>IGroupMember</code> belongs to.
     * @return java.util.Iterator
     * @param group org.apereo.portal.groups.IEntityGroup
     */
    protected Iterator findParentGroups(IEntityGroup group) throws GroupsException {
        if (log.isDebugEnabled())
            log.debug(DEBUG_CLASS_NAME + ".findParentGroups(): for " + group);

        List groups = new ArrayList();
        {
            String typeName = group.getLeafType().getName();
            File parent = getFile(group).getParentFile();
            if (!parent.getName().equals(typeName)) {
                groups.add(find(parent));
            }

            File root = getFileRoot(group.getLeafType());
            File[] files = getAllFilesBelow(root);
            try {
                for (int i = 0; i < files.length; i++) {
                    Collection ids = getGroupIdsFromFile(files[i]);
                    if (ids.contains(group.getLocalKey())) {
                        groups.add(find(files[i]));
                    }
                }
            } catch (IOException ex) {
                throw new GroupsException("Problem reading group files", ex);
            }
        }
        return groups.iterator();
    }

    /**
     * Returns an <code>Iterator</code> over the <code>Collection</code> of
     * <code>IEntityGroups</code> that the <code>IGroupMember</code> belongs to.
     * @return java.util.Iterator
     * @param gm org.apereo.portal.groups.IEntityGroup
     */
    public Iterator findParentGroups(IGroupMember gm) throws GroupsException {
        if (gm.isGroup()) {
            IEntityGroup group = (IEntityGroup) gm;
            return findParentGroups(group);
        } else {
            IEntity ent = (IEntity) gm;
            return findParentGroups(ent);
        }
    }

    /**
     * Returns an <code>Iterator</code> over the <code>Collection</code> of
     * <code>IEntities</code> that are members of this <code>IEntityGroup</code>.
     * @return java.util.Iterator
     * @param group org.apereo.portal.groups.IEntityGroup
     */
    public java.util.Iterator findEntitiesForGroup(IEntityGroup group) throws GroupsException {
        if (log.isDebugEnabled())
            log.debug(DEBUG_CLASS_NAME + ".findEntitiesForGroup(): retrieving entities for group " + group);

        Collection entities = null;
        File f = getFile(group);
        if (f.isDirectory()) {
            entities = Collections.EMPTY_LIST;
        } else {
            entities = getEntitiesFromFile(f);
        }

        return entities.iterator();
    }

    /**
     * Returns an instance of the <code>ILockableEntityGroup</code> from the data store.
     * @return org.apereo.portal.groups.IEntityGroup
     * @param key java.lang.String
     */
    public ILockableEntityGroup findLockable(String key) throws GroupsException {
        throw new UnsupportedOperationException(DEBUG_CLASS_NAME + ".findLockable() not supported");
    }

    /**
     * Returns a <code>String[]</code> containing the keys of  <code>IEntityGroups</code>
     * that are members of this <code>IEntityGroup</code>.  In a composite group
     * system, a group may contain a member group from a different service.  This is
     * called a foreign membership, and is only possible in an internally-managed
     * service.  A group store in such a service can return the key of a foreign member
     * group, but not the group itself, which can only be returned by its local store.
     *
     * @return String[]
     * @param group org.apereo.portal.groups.IEntityGroup
     */
    public java.lang.String[] findMemberGroupKeys(IEntityGroup group) throws GroupsException {
        String[] keys;
        File f = getFile(group);
        if (f.isDirectory()) {
            File[] files = f.listFiles();
            keys = new String[files.length];
            for (int i = 0; i < files.length; i++) {
                keys[i] = getKeyFromFile(files[i]);
            }
        } else {
            try {
                Collection groupKeys = getGroupIdsFromFile(f);
                keys = (String[]) groupKeys.toArray(new String[groupKeys.size()]);
            } catch (IOException ex) {
                throw new GroupsException(
                        DEBUG_CLASS_NAME + ".findMemberGroupKeys(): " + "problem finding group members", ex);
            }
        }
        return keys;
    }

    /**
     * Returns an <code>Iterator</code> over the <code>Collection</code> of
     * <code>IEntityGroups</code> that are members of this <code>IEntityGroup</code>.
     * @return java.util.Iterator
     * @param group org.apereo.portal.groups.IEntityGroup
     */
    public java.util.Iterator findMemberGroups(IEntityGroup group) throws GroupsException {
        String[] keys = findMemberGroupKeys(group); // No foreign groups here.
        List groups = new ArrayList(keys.length);
        for (int i = 0; i < keys.length; i++) {
            groups.add(find(keys[i]));
        }
        return groups.iterator();
    }

    /**
     * Recursive search of directories underneath dir for files that match filter.
     * @return java.util.Set
     */
    public Set getAllDirectoriesBelow(File dir) {
        Set allDirectories = new HashSet();
        if (dir.isDirectory()) {
            primGetAllDirectoriesBelow(dir, allDirectories);
        }
        return allDirectories;
    }

    /**
     * Recursive search of directories underneath dir for files that match filter.
     */
    public File[] getAllFilesBelow(File dir) {
        Set allFiles = new HashSet();
        if (dir.isDirectory()) {
            primGetAllFilesBelow(dir, allFiles);
        }
        return (File[]) allFiles.toArray(new File[allFiles.size()]);
    }

    /**
     * Returns the filesystem separator character NOT in use.
     * @return char
     */
    protected char getBadSeparator() {
        return badSeparator;
    }

    /**
     * @return java.util.Map
     */
    protected java.util.Map getCache() {
        return cache;
    }

    /**
     * Returns a Class representing the default entity type.
     * @return Class
     */
    protected Class getDefaultEntityType() {
        return defaultEntityType;
    }

    /**
     * @param idFile java.io.File - a file of ids.
     * @return entities Collection.
     */
    protected Collection getEntitiesFromFile(File idFile) throws GroupsException {
        if (log.isDebugEnabled())
            log.debug(DEBUG_CLASS_NAME + "getEntitiesFromFile(): for " + idFile.getPath());

        Collection ids = null;
        Class type = getEntityType(idFile);
        if (EntityTypesLocator.getEntityTypes().getEntityIDFromType(type) == null) {
            throw new GroupsException("Invalid entity type: " + type);
        }
        try {
            ids = getEntityIdsFromFile(idFile);
        } catch (Exception ex) {
            throw new GroupsException("Problem retrieving keys from file", ex);
        }

        Collection entities = new ArrayList(ids.size());

        for (Iterator itr = ids.iterator(); itr.hasNext();) {
            String key = (String) itr.next();
            entities.add(GroupService.getEntity(key, type));
        }

        if (log.isDebugEnabled())
            log.debug(DEBUG_CLASS_NAME + "getEntitiesFromFile(): Retrieved " + entities.size() + " entities");

        return entities;
    }

    /**
     * @param idFile java.io.File - a file of ids.
     * @return String[] ids.
     */
    protected Collection getEntityIdsFromFile(File idFile) throws IOException, FileNotFoundException {
        if (log.isDebugEnabled())
            log.debug(DEBUG_CLASS_NAME + "getEntityIdsFromFile(): Reading " + idFile.getPath());

        Collection ids = getIdsFromFile(idFile, false);

        if (log.isDebugEnabled())
            log.debug(DEBUG_CLASS_NAME + "getEntityIdsFromFile(): Retrieved " + ids.size() + " IDs");

        return ids;
    }

    /**
     * @param f File
     * @return java.lang.Class
     * The Class is the first node of the full path name.
     */
    protected Class getEntityType(File f) {
        String path = f.getPath();
        String afterRootPath = null;
        Class type = null;
        if (path.startsWith(getGroupsRootPath())) {
            afterRootPath = path.substring(getGroupsRootPath().length());
            int end = afterRootPath.indexOf(File.separatorChar);
            String typeName = afterRootPath.substring(0, end);

            try {
                type = Class.forName(typeName);
            } catch (ClassNotFoundException cnfe) {
            }
        }
        return type;
    }

    /**
     * @param group IEntityGroup.
     * @return File
     */
    protected File getFile(IEntityGroup group) {
        String key = getFilePathFromKey(group.getLocalKey());
        return new File(key);
    }

    /**
     *
     */
    protected String getFilePathFromKey(String key) {
        if (log.isDebugEnabled())
            log.debug(DEBUG_CLASS_NAME + ".getFilePathFromKey(): for key: " + key);

        String groupKey = useSubstitutePeriod ? key.replace(SUBSTITUTE_PERIOD, PERIOD) : key;

        String fullKey = getGroupsRootPath() + groupKey;

        if (log.isDebugEnabled())
            log.debug(DEBUG_CLASS_NAME + ".getFilePathFromKey(): full key: " + fullKey);

        return conformSeparatorChars(fullKey);
    }

    /**
     * Returns a File that is the root for groups of the given type.
     */
    protected File getFileRoot(Class type) {
        String path = getGroupsRootPath() + type.getName();
        File f = new File(path);
        return (f.exists()) ? f : null;
    }

    /**
     * Returns the filesystem separator character in use.
     * @return char
     */
    protected char getGoodSeparator() {
        return goodSeparator;
    }

    /**
     * @param idFile java.io.File - a file of ids.
     * @return String[] ids.
     */
    protected Collection getGroupIdsFromFile(File idFile) throws IOException, FileNotFoundException {
        if (log.isDebugEnabled())
            log.debug(DEBUG_CLASS_NAME + "getGroupIdsFromFile(): Reading " + idFile.getPath());

        Collection ids = getIdsFromFile(idFile, true);

        if (log.isDebugEnabled())
            log.debug(DEBUG_CLASS_NAME + "getGroupIdsFromFile(): Retrieved " + ids.size() + " IDs");

        return ids;
    }

    /**
     * @return java.lang.String
     */
    public java.lang.String getGroupsRootPath() {
        return groupsRootPath;
    }

    /**
     * @param idFile java.io.File - a file of ids.
     * @return String[] ids.
     */
    protected Collection getIdsFromFile(File idFile, boolean groupIds) throws IOException, FileNotFoundException {
        Collection ids = new HashSet();
        BufferedReader br = new BufferedReader(new FileReader(idFile));
        String line, tok;

        line = br.readLine();
        while (line != null) {
            line = line.trim();
            if (!line.startsWith(COMMENT) && (line.length() > 0)) {
                StringTokenizer st = new StringTokenizer(line);
                tok = st.nextToken();
                if (tok != null) {
                    if (tok.startsWith(GROUP_PREFIX)) {
                        if (groupIds) {
                            ids.add(tok.substring(GROUP_PREFIX.length()));
                        }
                    } else {
                        if (!groupIds) {
                            ids.add(tok);
                        }
                    }
                }
            }
            line = br.readLine();
        }
        br.close();

        return ids;
    }

    /**
     *
     */
    protected String getKeyFromFile(File f) {
        String key = null;
        if (f.getPath().startsWith(getGroupsRootPath())) {
            key = f.getPath().substring(getGroupsRootPath().length());

            if (useSubstitutePeriod) {
                key = key.replace(PERIOD, SUBSTITUTE_PERIOD);
            }
        }
        return key;
    }

    /**
     *
     */
    protected void initialize(GroupServiceConfiguration cfg) {
        cache = Collections.synchronizedMap(new HashMap());

        goodSeparator = File.separatorChar;
        badSeparator = (goodSeparator == FORWARD_SLASH) ? BACK_SLASH : FORWARD_SLASH;

        defaultEntityType = IPerson.class;
        GroupServiceConfiguration config = cfg;
        if (config == null) {
            try {
                config = GroupServiceConfiguration.getConfiguration();
            } catch (Exception ex) {
                throw new RuntimeException(ex);
            }
        }

        String sep = config.getNodeSeparator();
        if (sep != null) {
            String period = String.valueOf(PERIOD);
            useSubstitutePeriod = sep.equals(period);
        }
    }

    /**
     * @return org.apereo.portal.groups.IEntityGroup
     */
    private IEntityGroup newInstance(File f) throws GroupsException {
        String key = getKeyFromFile(f);
        String name = f.getName();
        Class cl = getEntityType(f);
        return newInstance(key, cl, name);
    }

    /**
     * @return org.apereo.portal.groups.IEntityGroup
     * We assume that new groups will be created updated via the file system,
     * not the group service.
     */
    public IEntityGroup newInstance(Class entityType) throws GroupsException {
        throw new UnsupportedOperationException(DEBUG_CLASS_NAME + ".newInstance(Class cl) not supported");
    }

    public IEntity newInstance(String key, Class type) throws GroupsException {
        if (EntityTypesLocator.getEntityTypes().getEntityIDFromType(type) == null) {
            throw new GroupsException("Invalid group type: " + type);
        }
        return new EntityImpl(key, type);
    }

    /**
     * @return org.apereo.portal.groups.IEntityGroup
     */
    private IEntityGroup newInstance(String newKey, Class newType, String newName) throws GroupsException {
        EntityGroupImpl egi = new EntityGroupImpl(newKey, newType);
        egi.primSetName(newName);
        return egi;
    }

    /**
     * Returns all directories under dir.
     */
    private void primGetAllDirectoriesBelow(File dir, Set allDirectories) {
        File[] files = dir.listFiles(fileFilter);
        for (int i = 0; i < files.length; i++) {
            if (files[i].isDirectory()) {
                primGetAllDirectoriesBelow(files[i], allDirectories);
                allDirectories.add(files[i]);
            }
        }
    }

    /**
     * Returns all files (not directories) underneath dir.
     */
    private void primGetAllFilesBelow(File dir, Set allFiles) {
        File[] files = dir.listFiles(fileFilter);
        for (int i = 0; i < files.length; i++) {
            if (files[i].isDirectory()) {
                primGetAllFilesBelow(files[i], allFiles);
            } else {
                allFiles.add(files[i]);
            }
        }
    }

    /**
     * Find EntityIdentifiers for entities whose name matches the query string
     * according to the specified method and is of the specified type
     */
    public EntityIdentifier[] searchForEntities(String query, int method, Class type) throws GroupsException {
        return new EntityIdentifier[0];
    }

    /**
     * Returns an EntityIdentifier[] of groups of the given leaf type whose names
     * match the query string according to the search method.
     *
     * @param query String the string used to match group names.
     * @param searchMethod see org.apereo.portal.groups.IGroupConstants.
     * @param leafType the leaf type of the groups we are searching for.
     * @return EntityIdentifier[]
     */
    public EntityIdentifier[] searchForGroups(String query, int searchMethod, Class leafType)
            throws GroupsException {
        List ids = new ArrayList();
        File baseDir = getFileRoot(leafType);

        if (log.isDebugEnabled())
            log.debug(DEBUG_CLASS_NAME + "searchForGroups(): " + query + " method: " + searchMethod + " type: "
                    + leafType);

        if (baseDir != null) {
            String nameFilter = null;

            switch (searchMethod) {
            case IS:
                nameFilter = query;
                break;
            case STARTS_WITH:
                nameFilter = query + ".*";
                break;
            case ENDS_WITH:
                nameFilter = ".*" + query;
                break;
            case CONTAINS:
                nameFilter = ".*" + query + ".*";
                break;
            default:
                throw new GroupsException(
                        DEBUG_CLASS_NAME + ".searchForGroups(): Unknown search method: " + searchMethod);
            }

            final Pattern namePattern = Pattern.compile(nameFilter);
            final FilenameFilter filter = new FilenameFilter() {
                @Override
                public boolean accept(File dir, String name) {
                    return namePattern.matcher(name).matches();
                }
            };

            Set allDirs = getAllDirectoriesBelow(baseDir);
            allDirs.add(baseDir);

            for (Iterator itr = allDirs.iterator(); itr.hasNext();) {
                File[] files = ((File) itr.next()).listFiles(filter);
                for (int filesIdx = 0; filesIdx < files.length; filesIdx++) {
                    String key = getKeyFromFile(files[filesIdx]);
                    EntityIdentifier ei = new EntityIdentifier(key, ICompositeGroupService.GROUP_ENTITY_TYPE);
                    ids.add(ei);
                }
            }
        }

        if (log.isDebugEnabled())
            log.debug(DEBUG_CLASS_NAME + ".searchForGroups(): found " + ids.size() + " files.");

        return (EntityIdentifier[]) ids.toArray(new EntityIdentifier[ids.size()]);
    }

    /**
     * @param newCache java.util.Map
     */
    protected void setCache(java.util.Map newCache) {
        cache = newCache;
    }

    /**
     * @param newGroupsRootPath java.lang.String
     */
    protected void setGroupsRootPath(java.lang.String newGroupsRootPath) {
        groupsRootPath = conformSeparatorChars(newGroupsRootPath) + getGoodSeparator();
    }

    /**
     * Adds or updates the <code>IEntityGroup</code> AND ITS MEMBERSHIPS to the
     * data store, as appropriate.  We assume that groups will be updated via the
     * file system, not the group service.
     * @param group org.apereo.portal.groups.IEntityGroup
     */
    public void update(IEntityGroup group) throws GroupsException {
        throw new UnsupportedOperationException(DEBUG_CLASS_NAME + ".update() not supported");
    }

    /**
     * Commits the group memberships of the <code>IEntityGroup</code> to
     * the data store.  We assume that groups will be updated via the
     * file system, not the group service.
     * @param group org.apereo.portal.groups.IEntityGroup
     */
    public void updateMembers(IEntityGroup group) throws GroupsException {
        throw new UnsupportedOperationException(DEBUG_CLASS_NAME + ".updateMembers() not supported");
    }

    /**
     * Answers if <code>group</code> contains <code>member</code>.
     * @return boolean
     * @param group org.apereo.portal.groups.IEntityGroup
     * @param member org.apereo.portal.groups.IGroupMember
     */
    public boolean contains(IEntityGroup group, IGroupMember member) throws GroupsException {
        File f = getFile(group);
        return (f.isDirectory()) ? directoryContains(f, member) : fileContains(f, member);
    }

    /**
     * Answers if <code>file</code> contains <code>member</code>.  
     * @param file
     * @param member
     * @return boolean
     */
    private boolean fileContains(File file, IGroupMember member) throws GroupsException {
        Collection ids = null;
        try {
            ids = member.isGroup() ? getGroupIdsFromFile(file) : getEntityIdsFromFile(file);
        } catch (Exception ex) {
            throw new GroupsException("Error retrieving ids from file", ex);
        }
        return ids.contains(member.getKey());
    }

    /**
     * Answers if <code>directory</code> contains <code>member</code>.  A 
     * directory can only contain (other) groups.  
     * @param directory java.io.File
     * @param member
     * @return boolean
     */
    private boolean directoryContains(File directory, IGroupMember member) {
        boolean found = false;
        if (member.isGroup()) {
            File memberFile = getFile((IEntityGroup) member);
            File[] files = directory.listFiles();
            for (int i = 0; i < files.length & !found; i++) {
                found = files[i].equals(memberFile);
            }
        }
        return found;
    }

}