/* ======================================================================
   Parts Copyright 2006 University of Leeds, Oxford University, University of the Highlands and Islands.

   Licensed 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

       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.bodington.server.userimport.oucsir;

import java.security.acl.NotOwnerException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import org.apache.log4j.Logger;
import org.bodington.server.BuildingServerException;
import org.bodington.server.realm.Acl;
import org.bodington.server.realm.AclEntry;
import org.bodington.server.realm.Group;
import org.bodington.server.realm.Permission;
import org.bodington.server.resources.Resource;
import org.bodington.server.resources.ResourceTree;
import org.bodington.server.resources.ResourceTreeManager;
import org.bodington.server.userimport.ConfigurationException;
import org.bodington.server.userimport.GeneratedUser;
import org.bodington.server.userimport.GroupCreationException;
import org.bodington.server.userimport.GroupManager;
import org.bodington.server.userimport.NotInitialisedException;
import org.bodington.server.userimport.SelectionException;
import org.bodington.server.userimport.UserGroupImporter;

/**
 * Creates and saves groups and their resources, and will also find existing
 * groups. Acts as a cache for all groups it has loaded.
 * @author Mats Henrikson
 * @author buckett
 */
public class OUCSGroupManager implements GroupManager
{

    /** A cache for all the groups that this Manager knows about. */
    private Map groups; 
    /** A cache for all the resources that this Manager knows about. */
    private Map resources;
    /** A cache for all the path lists that this manager has had requested. */
    private Map pathLists = new HashMap();
    /** A cache for all the path strings that this manager has had requested. */
    private Map pathStrings = new HashMap();
    /** The total number of created groups. */
    private int groupsCreated;
    /** The total number of created resources. */
    private int resourcesCreated;
    /** The InformationManager to get the descriptions of groups from. */
    private final OUCSInformationResource ir;

    /** Should we create groups with a year suffix? */
    private final boolean CREATE_YEAR;
    /** The number 0 or 1 to be used when deciding on a year suffix. */
    private final int YEAR_INCREMENT;

    /** A Logger instance to log to. */
    private static final Logger log = Logger.getLogger(OUCSGroupManager.class);

    // the description strings for groups
    private static final String ALL = "All members of ";
    private static final String SENMEM = "Senior members of ";
    private static final String STAFF = "Staff members of ";
    private static final String GRAD = "Postgraduate members of ";
    private static final String YGRAD = " postgraduate members of ";
    private static final String UGRAD = "Undergraduate members of ";
    private static final String YUGRAD = " undergraduate members of ";
    private static final String STUDENT = "Student members of ";
    private static final String Y = "Year ";
    private static final String GRAD_PATH = "campus.grad";
    private static final String STAFF_PATH = "campus.staff";
    private static final String STUDENTS_PATH = "campus.students";
    private static final String OXFORD_PATH = "campus.oxford";

    /**
     * Initialises the GroupManager with an OUCSInformationResource from which
     * it can get the descriptions of groups.
     * <p>
     * <dl>
     * <dt><strong>usrgrpgen.grpmgr.increment.year</strong></dt>
     * <dd>Should the year suffix on year groups be incremented by one to allow
     * for Registrations incrementing the year in October, while the real year
     * increments by one on December 31... This should be true from the instant
     * Registrations increment their years until 12pm December 31. If you don't
     * understand what this means then set usrgrpgen.create.year.groups
     * to false and no year groups will be created at all. This is a required
     * field.</dd>
     * <dt><strong>usrgrpgen.grpmgr.create.year.groups</strong></dt>
     * <dd>Set this to false if no groups that has a year suffix should be
     * created at all. If this is missing it defaults to false.</dd>
     * </dl>
     * </p>
     * @param ir The InformationManager to get group descriptions from.
     * @param props The Properties instance to use.
     * @throws ConfigurationException Throws a CE if it couldn't find the
     *         options it needs.
     */
    public OUCSGroupManager(Properties props, OUCSInformationResource ir)
        throws ConfigurationException
    {
        String yearInc = props.getProperty("usrgrpgen.grpmgr.increment.year");
        if (yearInc == null)
        {
            throw new ConfigurationException(
                "Please specify if groups with a year suffix should have "
                    + "the year incremented by one or not.");
        }
        else
        {
            YEAR_INCREMENT = Boolean.valueOf(yearInc).booleanValue() ? 1 : 0;
        }
        CREATE_YEAR = Boolean.valueOf(
            props.getProperty("usrgrpgen.grpmgr.create.year.groups", "false"))
            .booleanValue();
        this.ir = ir;
        try
        {
            bootstrap();
        }
        // any exception here is fatal
        catch (Exception e)
        {
            throw new ConfigurationException(e);
        }
    }

    /**
     * Creates and saves all necessary groups and resources for a user.
     * @param u The user to create groups for.
     * @throws GroupCreationException Throws a GCE if something goes wrong.
     */
    public void createAllGroups(GeneratedUser u) throws GroupCreationException
    {
        OUCSGeneratedUser user = null;
        try
        {
            user = (OUCSGeneratedUser) u;
            Collection groupPaths = new ArrayList();
            groupPaths.addAll(getGroupPaths(user, CREATE_YEAR, YEAR_INCREMENT));
            user.setGroupCollection(groupPaths);
            // skips groups that exist and create the rest
            for (Iterator i = groupPaths.iterator(); i.hasNext();)
            {
                List path = (List) i.next();
                if (groupExists(path))
                {
                    continue;
                }

                // get or create all required resources, then create the group
                Resource groupResource = getGroupResource(path);
                createGroup(path, groupResource);
            }
        }
        catch (SelectionException se)
        {
            throw new GroupCreationException(se);
        }
        catch (NotOwnerException noe)
        {
            throw new GroupCreationException(noe);
        }
        catch (NullPointerException npe)
        {
            /*
             * Just throw a new exception here and go on with the rest of the
             * users as this isn't a fatal problem.
             */
            log.warn(user.getUniqueID() + ": has invalid data, no groups "
                + "were retreived from this user:\n"
                + UserGroupImporter.getExceptionLog(npe));
        }
        catch (BuildingServerException bse)
        {
            /*
             * If a BSE is thrown then there is most likely something seriously
             * wrong, so stop everything.
             */
            throw new GroupCreationException(bse);
        }
    }

    /**
     * Puts a User in all the groups they should be in.
     * @param u The User who should be put in their groups.
     * @throws NotInitialisedException Throws a NIE if the user has not been
     *         loaded.
     */
    public void putInGroups(GeneratedUser u) throws NotInitialisedException
    {
        OUCSGeneratedUser user = (OUCSGeneratedUser) u;
        Collection groupPaths = user.getGroupCollection();
        Collection usersGroups = new ArrayList();
        for (Iterator i = groupPaths.iterator(); i.hasNext();)
        {
            List path = (List) i.next();
            usersGroups.add(groups.get(path));
        }
        user.addToGroups(usersGroups);
    }

    /**
     * Creates a specific group and tags it onto the passed Resource.
     * @param path The path to the Group.
     * @param resource The Resource the groups should br created at.
     * @throws BuildingServerException Throws a BSE if something goes wrong.
     */
    private void createGroup(List path, Resource resource)
        throws BuildingServerException
    {
        Group group = new Group();
        group.setName(pathAsString(path));
        group.setResourceId(resource.getResourceId());
        group.setDescription(getGroupText(path));
        group.save();
        groupsCreated++;
        log.info("Saved new group: " + pathAsString(path) + ", resource id: "
            + group.getResourceId());
        groups.put(Collections.unmodifiableList(path), group);
    }

    /**
     * Adds the standard ACLs to a resource.
     * @param resource The resource to add the ACLs for.
     * @throws BuildingServerException Throws a BSE if something goes wrong.
     * @throws NotOwnerException Throws a NOE if ownerships are wrong, which
     *         shouldn't happend as we should be running with sysadmin privs.
     */
    private void addStandardACLs(Resource resource)
        throws BuildingServerException, NotOwnerException
    {
        AclEntry staff = new AclEntry();
        AclEntry students = new AclEntry();
        staff.addPermission(Permission.SEE);
        staff.addPermission(Permission.VIEW);
        staff.setGroup((Group) groups.get(pathList(STAFF_PATH)));
        students.addPermission(Permission.SEE);
        students.setGroup((Group) groups.get(pathList(STUDENTS_PATH)));
        Acl acl = resource.getAcl();
        acl.addEntry(staff);
        acl.addEntry(students);
        acl.save();
        log.debug("Saved new ACL entry for resource "
            + resource.getResourceId());
    }

    /**
     * Bootstraps the Resource and Group caches by putting essential resources
     * and groups in them.
     * @throws BuildingServerException Throws a BSE if something goes wrong.
     */
    private void bootstrap() throws BuildingServerException
    {
        // bootstrap the Resource cache
        resources = new HashMap();
        List path = new ArrayList();
        path.add("site");
        ResourceTree tree = ResourceTreeManager.getInstance();
        resources.put(Collections.unmodifiableList(path), tree
            .findRootResource());

        // bootstrap the Group cache
        groups = new HashMap();
        path = new ArrayList();
        Group g = Group.findGroupByName(STAFF_PATH);
        if (g != null)
            groups.put(Collections.unmodifiableList(pathList(STAFF_PATH)), g);
        else
            throw new BuildingServerException("Couldn't find staff group: "+ STAFF_PATH);
        g = Group.findGroupByName(STUDENTS_PATH);
        if (g != null)
            groups.put(Collections.unmodifiableList(pathList(STUDENTS_PATH)), g);
        else
            throw new BuildingServerException("Couldn't find student path: "+ STUDENTS_PATH);
    }

    /**
     * Gets or creates a resource for a group.
     * @param p The path List for the group that needs a new resource.
     * @return The Resource that this group should be created at.
     * @throws BuildingServerException Throws a BSE if something goes wrong.
     * @throws SelectionException Throws an SE if there is a problem finding
     *         essential Resources.
     */
    private Resource getGroupResource(List p) throws BuildingServerException,
        SelectionException, NotOwnerException
    {
        List path = new ArrayList(pathList("site.admin.ground.campus"));
        path.addAll(p);

        // check if we have it in the cache already
        Resource end = (Resource) resources.get(path);
        if (end != null)
        {
            return end;
        }

        // walk backwards (from the leaf to the root)
        // through the path until a resource that exists is found in the cache
        // end is set to the last found resource
        List removed = new ArrayList();
        List partial = new ArrayList(path);
        for (int i = partial.size() - 1; partial.size() > 0; i--)
        {
            removed.add(partial.remove(i));
            end = (Resource) resources.get(partial);
            if (end != null)
            {
                break;
            } // found it
        }

        /*
         * At the very least the root Resource should have been found now, so
         * try to find the child resources if necessary.
         */
        ResourceTree tree = ResourceTreeManager.getInstance();
        Collections.reverse(removed);
        for (Iterator i = removed.iterator(); i.hasNext();)
        {
            String name = (String) i.next();
            Enumeration children = end.findChildren();
            children: while (children.hasMoreElements())
            {
                Resource child = (Resource) children.nextElement();
                if (child.getName().equals(name))
                {
                    i.remove();
                    partial.add(name);
                    resources.put(Collections.unmodifiableList(partial), child);
                    end = child;
                    break children;
                }
            }
        }

        /*
         * If there are still path elements left in removed list then create
         * them as new resources.
         */
        for (Iterator i = removed.iterator(); i.hasNext();)
        {
            String name = (String) i.next();
            Resource created = new Resource();
            created.setName(name);


            // if this is not the final element just set some placeholders
            if (i.hasNext())
            {
                String longName = pathAsString(p);
                created.setTitle(longName);
                created.setDescription(longName);
                created.setIntroduction(longName);
                created.setHttpFacilityNo(27); // groupcontainer
            }
            // if this is the final element of the path then set the extra info
            else
            {
                String title = getGroupText(p);
                end.setTitle(title);
                end.setDescription("Group category for " + title);
                end.setIntroduction("Use this area to create groups for "
                    + title);
                end.save();
                log.debug("Saved modified resource " + end.getResourceId());
                created.setTitle(pathAsString(p));
                String description = getGroupDescription(p);
                created.setDescription(description);
                created.setIntroduction(description);
                created.setHttpFacilityNo(6); // Permission
            }

            created.setUseParentAcl(false);
            tree.addResource(end, created);
            resourcesCreated++;
            log.debug("Saved new resource " + created.getResourceId());
            addStandardACLs(created);
            partial.add(name);
            resources.put(Collections.unmodifiableList(partial), created);
            end = created;
        }
        return end;
    }

    /**
     * Checks to see if a group already exists in the Bodington system.
     * @param path The path to the group as a List.
     * @return Returns true if the group exists, false otherwise.
     * @throws BuildingServerException Throws a BSE if something goes wrong.
     */
    private boolean groupExists(List path) throws BuildingServerException
    {
        if (groups.containsKey(path))
        {
            return true;
        }
        Group g = Group.findGroupByName(pathAsString(path));
        boolean exists = g == null ? false : true;
        if (exists)
        {
            groups.put(Collections.unmodifiableList(path), g);
        }
        return exists;
    }

    /**
     * Returns the description of a group as a String. If it can find
     * information about the group, then use that, otherwise use the
     * group path name.
     * @param path The path to the group as a list.
     * @return The description of the group as a String.
     */
    private String getGroupDescription(List path)
    {
        final String LAST = (String) path.get(path.size() - 1);
        String desc = null;

        // check if this is one of the special groups
        if (path.size() <= 2)
        {
            if (LAST.endsWith("users"))
            {
                desc = "All users of the system.";
            }
            else if (LAST.endsWith("students"))
            {
                desc = "All student users of the system.";
            }
            else if (LAST.endsWith("staff"))
            {
                desc = "All staff users of the system.";
            }
            else
            {
                log.debug("Can't map group name: "+ LAST);
            }
            return desc;
        }

        desc = getGroupText(path);

        // put in the extra explanatory string
        if (LAST.equals("all"))
        {
            desc = ALL + desc;
        }
        else if (LAST.equals("students"))
        {
            desc = STUDENT + desc;
        }
        else if (LAST.equals("staff"))
        {
            desc = STAFF + desc;
        }
        else if (LAST.equals("ugrad"))
        {
            desc = UGRAD + desc;
        }
        else if (LAST.equals("grad"))
        {
            desc = GRAD + desc;
        }
        else if (LAST.equals("senmem"))
        {
            desc = SENMEM + desc;
        }
        else if (LAST.startsWith("grad_"))
        {
            String year = LAST.substring(LAST.lastIndexOf('_')+1);
            desc = Y + year + YGRAD + desc;
        }
        else if (LAST.startsWith("ugrad_"))
        {
            String year = LAST.substring(LAST.lastIndexOf('_')+1);
            desc = Y + year + YUGRAD + desc;
        }
        else
        {
            log.debug("Can't map group name: "+ LAST);
        }
        return desc;
    }

    /**
     * Returns the long name for a Group, but just the name,
     * nothing else.
     * @param path The path to the group.
     * @return Returns the name of the group, or the path to the
     *         group as a String if no name was found.
     */
    private String getGroupText(List path)
    {
        String desc = null;
        if (path.size() > 2)
        {
            final String CODE = (String) path.get(path.size() - 2);
            final String TYPE = (String) path.get(path.size() - 3);
            if (TYPE.equals("college"))
            {
                desc = ir.getCollegeName(CODE);
            }
            else if (TYPE.equals("dept"))
            {
                desc = ir.getDepartmentName(CODE);
            }
            else if (TYPE.equals("course"))
            {
                desc = ir.getCourseName(CODE);
            }
        }
        if (desc == null)
        {
            desc = pathAsString(path);
            log.debug("Can't map name "+ desc);
        }
        return desc;
    }

    /**
     * Returns a group path List as a String.
     * @param path The path List to make into a String.
     * @return The String representation of the path.
     */
    private String pathAsString(List path)
    {
        // check for it in the cache
        String s = (String) pathStrings.get(path);
        if (s != null)
        {
            return s;
        }

        // do it the hard way
        StringBuffer sb = new StringBuffer();
        for (int k = 0; k < path.size(); k++)
        {
            sb.append((String) path.get(k)).append(".");
        }
        if (sb.length() > 0)
        {
            sb.delete(sb.length() - 1, sb.length());
        }
        s = sb.toString();
        pathStrings.put(Collections.unmodifiableList(path), s);
        return s;
    }

    /**
     * Returns a Collection containing all the paths of the groups that a user
     * should belong to. The instances stored in the collection are Lists with
     * the path of the group stored as Strings. The path omits the first common
     * identifiers "site", "admin", "ground", "campus", as they are exactly the
     * same for every group.
     * @param user The user we are adding the groups
     * @return An unmodifiable Collection containing a number of Lists full of
     *         Strings.
     */
    private Collection getGroupPaths(OUCSGeneratedUser user,
        boolean createYear, int yearIncrement)
    {
        /*
         * The groups below are examples of the regular group categories they
         * should be added to: campus.college.ball.all campus.college.ball.staff
         * campus.college.ball.senmem campus.college.ball.students
         * campus.college.ball.ugrad campus.college.ball.ugrad_start_yr
         * campus.college.ball.grad campus.college.ball.grad_start_yr
         * campus.course.clmf.all campus.course.clmf.students
         * campus.course.clmf.ugrad campus.course.clmf.ugrad_start_yr
         * campus.course.clmf.grad campus.course.clmf.grad_start_yr
         * campus.dept.classics.all campus.dept.classics.staff
         * campus.dept.classics.senmem These have been split into (mainly) three
         * methods that returns each group of groups.
         */
        Collection groups = user.getGroupCollection();
        if (groups != null)
        {
            return groups;
        }
        groups = new ArrayList();
        groups.addAll(getSpecialGroupPaths(user));
        groups.addAll(getDeptGroupPaths(user));
        groups.addAll(getCourseGroupPaths(user, createYear, yearIncrement));
        groups.addAll(getCollegeGroupPaths(user, createYear, yearIncrement));
        return Collections.unmodifiableCollection(groups);
    }

    /**
     * Splits up a group path string of the format path.to.group into a List of
     * Strings.
     * @param path The path String to split.
     * @return A List with the path elements as Strings.
     */
    private List pathList(String path)
    {
        // check for it in the cache
        List l = (List) pathLists.get(path);
        if (l != null)
        {
            return l;
        }

        // do it the hard way
        String array[] = path.split("\\.");
        l = new ArrayList(Arrays.asList(array));
        pathLists.put(path, Collections.unmodifiableList(l));
        return l;
    }

    /**
     * Returns a Collection of Lists with the paths to the 'special' groups this
     * user should be a member of.
     * @param user The user to get groups for.
     * @return A Collection of Lists, which in turn contain the paths to the
     *         groups as Strings.
     */
    private Collection getSpecialGroupPaths(OUCSGeneratedUser user)
    {
        Collection groups = new ArrayList();
        OUCSStatus status = user.getStatus();
        if (status == null) return groups;
        if (status.equals(OUCSStatus.STAFF)
            || status.equals(OUCSStatus.COLLEGE)
            || status.equals(OUCSStatus.DEPT)
            || status.equals(OUCSStatus.SENMEM))
        {
            // if the user is staff then add them to the staff groups
            groups.add(pathList("allstaff"));
            groups.add(pathList(STAFF_PATH));
        }
        else if (status.equals(OUCSStatus.UNDERGRAD)
            || status.equals(OUCSStatus.POSTGRAD)
            || status.equals(OUCSStatus.STUDENT))
        {
            // if they are a student then add them to the student groups
            groups.add(pathList("allstudents"));
            groups.add(pathList(STUDENTS_PATH));
            // If they are a graduate add them to the grad group.
            if (status.equals(OUCSStatus.POSTGRAD))
                groups.add(pathList(GRAD_PATH));
        }
        // add the user to the users groups
        groups.add(pathList("allusers"));
        groups.add(pathList("campus.users"));
        groups.add(pathList(OXFORD_PATH));
        return groups;
    }

    /**
     * Returns a Collection of Lists with the paths to the department groups
     * this user should be a member of.
     * @param user The user to check.
     * @return A Collection of Lists, which in turn contain the paths to the
     *         groups as Strings.
     */
    private Collection getDeptGroupPaths(OUCSGeneratedUser user)
    {
        Collection groups = new HashSet();
        OUCSStatus status = user.getStatus();
        OUCSDepartment dept = user.getDepartment();
        if (status == null || dept == null) return groups;

        final String DEPT = "campus.dept." + dept.getCode();
        groups.add(pathList(DEPT + ".all"));
        if (status.equals(OUCSStatus.SENMEM))
        {
            groups.add(pathList(DEPT + ".senmem"));
            groups.add(pathList(DEPT + ".staff"));
        }
        if (status.equals(OUCSStatus.STAFF))
        {
            groups.add(pathList(DEPT + ".staff"));
        }
        return groups;
    }

    /**
     * Returns a Collection of Lists with the paths to the course groups this
     * user should be a member of.
     * @param user The user to get the course groups for.
     * @param createYear If this is true then year groups should be created.
     * @param yearIncrement A number with which to increment year groups with.
     *        Between Registrations incrementing everybodys year number (usually
     *        in October some time) and Dec 31 this should be 1, the rest of the
     *        year it should be 0.
     * @return A Collection with the Lists containing the paths to the groups.
     */
    private Collection getCourseGroupPaths(OUCSGeneratedUser user,
        boolean createYear, int yearIncrement)
    {
        Collection groups = new HashSet();
        // all attributes are accessed as arrays, for future compatibility
        OUCSStatus status = user.getStatus();
        OUCSCourse courses = user.getCourse();
        if (status == null || courses == null) return groups;

        final String COURSE = "reg.course." + courses.getCode();
        groups.add(pathList(COURSE + ".all"));
        if (status.equals(OUCSStatus.STUDENT)
            || status.equals(OUCSStatus.UNDERGRAD)
            || status.equals(OUCSStatus.POSTGRAD))
        {
            groups.add(pathList(COURSE + ".students"));
            if (status.equals(OUCSStatus.POSTGRAD))
            {
                groups.add(pathList(COURSE + ".grad"));
                groups.addAll(getYearGroups(user, COURSE + ".grad", createYear,
                    yearIncrement));
            }
            else if (status.equals(OUCSStatus.UNDERGRAD))
            {
                groups.add(pathList(COURSE + ".ugrad"));
                groups.addAll(getYearGroups(user, COURSE + ".ugrad",
                    createYear, yearIncrement));
            }
        }

        return groups;
    }

    /** Returns a Collection of Lists with the paths to the college groups this
     user should be a member of.
     @param	user		The user being checked.
     @param	create		Should year groups be created? (Having the check in here
     removes one level of indentation from the main loop.)
     @param	increment	0 or 1 to increment the year with.
     @return	A Collection with paths.
     */
    private Collection getCollegeGroupPaths(OUCSGeneratedUser user,
        boolean create, int increment)
    {
        OUCSStatus status = user.getStatus();
        OUCSCollege college = user.getCollege();
        if (status == null || college == null) return Collections.EMPTY_LIST;

        Collection groups = new HashSet();
        final String COLLEGE = "campus.college." + college.getCode();
        groups.add(pathList(COLLEGE + ".all"));
        if (status.equals(OUCSStatus.SENMEM))
        {
            groups.add(pathList(COLLEGE + ".senmem"));
            groups.add(pathList(COLLEGE + ".staff"));
        }
        else if (status.equals(OUCSStatus.STAFF)
            || status.equals(OUCSStatus.COLLEGE)
            || status.equals(OUCSStatus.DEPT))
        {
            groups.add(pathList(COLLEGE + ".staff"));
        }
        else if (status.equals(OUCSStatus.POSTGRAD)
            || status.equals(OUCSStatus.UNDERGRAD)
            || status.equals(OUCSStatus.STUDENT))
        {
            groups.add(pathList(COLLEGE + ".students"));
            if (status.equals(OUCSStatus.POSTGRAD))
            {
                groups.add(pathList(COLLEGE + ".grad"));
                groups.addAll(getYearGroups(user, COLLEGE + ".grad", create,
                    increment));
            }
            else if (status.equals(OUCSStatus.UNDERGRAD))
            {
                groups.add(pathList(COLLEGE + ".ugrad"));
                groups.addAll(getYearGroups(user, COLLEGE + ".ugrad", create,
                    increment));
            }
        }

        return groups;
    }

    /** Get the year groups for a specific path.
     @param	user		The user being checked.
     @param	prefix		The path to add "_start_year_2001" or whatever to.
     @param	create		Should year groups be created? (Having the check in here
     removes one level of indentation from the main loop.)
     @param	increment	0 or 1 to increment the year with.
     @return A Collection with any year group paths in it.
     */
    private Collection getYearGroups(OUCSGeneratedUser user, String prefix,
        boolean create, int increment)
    {
        int year = user.getCourseYear();
        if (create && year != 0)
        {
            int start = Calendar.getInstance().get(Calendar.YEAR) + increment
                - year;
            return Collections.singleton(pathList(prefix +
                // We have shorter group names under reg namespace.
                (prefix.startsWith("reg")?"_":"_start_year_")
                + start));
        }
        else
        {
            return Collections.EMPTY_LIST;
        }
    }

    /** Returns the number of new groups that have been created.
     @return	The number of new groups created.
     */
    public int getCreatedGroups()
    {
        return groupsCreated;
    }

    /** Returns the number of new resources that have been created.
     @return	The number of new resources created.
     */
    public int getCreatedResources()
    {
        return resourcesCreated;
    }
}
