<commit_before>/*
 * This library is part of OpenCms -
 * the Open Source Content Management System
 *
 * Copyright (c) Alkacon Software GmbH (http://www.alkacon.com)
 *
 * This library 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 2.1 of the License, or (at your option) any later version.
 *
 * This library 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.
 *
 * For further information about Alkacon Software, please see the
 * company website: http://www.alkacon.com
 *
 * For further information about OpenCms, please see the
 * project website: http://www.opencms.org
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package org.opencms.ade.publish;

import org.opencms.main.CmsLog;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import org.apache.commons.logging.Log;

/**
 * Helper class for splitting a publish list into publish groups.<p>
 * 
 * @param <RESOURCE> the resource class type
 * @param <GROUP> the resource group class type
 * 
 * @since 8.0.0
 */
public abstract class A_CmsPublishGroupHelper<RESOURCE, GROUP> {

    /** An enum representing the age of a publish list resource. */
    public enum GroupAge {
        /** group age constant. */
        medium,
        /** group age constant. */
        old,
        /** group age constant. */
        young
    }

    /** The log instance for this class. */
    private static final Log LOG = CmsLog.getLog(A_CmsPublishGroupHelper.class);

    /** The gap between session groups. */
    protected static final int GROUP_SESSIONS_GAP = 8 * 60 * 60 * 1000;

    /** The current locale. */
    private Locale m_locale;

    /**
     * Creates a new publish group helper for a given locale.<p>
     * 
     * @param locale the locale to use 
     */
    public A_CmsPublishGroupHelper(Locale locale) {

        m_locale = locale;
    }

    /**
     * Given a descending list of dates represented as longs, this method computes a map from the dates
     * to their age in (local) days.<p>
     *    
     * @param sortedDates a descending list of dates represented as longs 
     * 
     * @return a map from dates to ages (measured in days)  
     */
    public Map<Long, Integer> computeDays(List<Long> sortedDates) {

        if (sortedDates.isEmpty()) {
            return Collections.<Long, Integer> emptyMap();
        }
        Map<Long, Integer> days = new HashMap<Long, Integer>();
        long lastDate = System.currentTimeMillis();
        int dayCounter = 0;
        for (Long dateObj : sortedDates) {
            long date = dateObj.longValue();
            long dayDifference = getDayDifference(lastDate, date);
            dayCounter += dayDifference;
            lastDate = date;
            days.put(dateObj, new Integer(dayCounter));
        }
        return days;
    }

    /**
     * Computes a map from modification date to number of (local) days since the modification date.<p>
     * 
     * @param resources a list of resources
     * 
     * @return a map from modification dates to the number of days since the modification date 
     */
    public Map<Long, Integer> computeDaysForResources(List<RESOURCE> resources) {

        Map<Long, Integer> result = computeDays(getModificationDates(resources));
        if (LOG.isDebugEnabled()) {
            for (RESOURCE res : resources) {
                LOG.debug("Resource "
                    + getRootPath(res)
                    + " is "
                    + result.get(new Long(getDateLastModified(res)))
                    + " days old.");
            }
        }
        return result;
    }

    /**
     * Gets the difference in days between to dates given as longs.<p>
     * 
     * The first date must be later than the second date.
     * 
     * @param first the first date 
     * @param second the second date 
     * 
     * @return the difference between the two dates in days 
     */
    public int getDayDifference(long first, long second) {

        if (first < second) {
            throw new IllegalArgumentException();
        }
        Calendar firstDay = getStartOfDay(first);
        Calendar secondDay = getStartOfDay(second);
        int result = 0;
        while (firstDay.after(secondDay)) {
            firstDay.add(Calendar.DAY_OF_MONTH, -1);
            result += 1;
        }
        return result;
    }

    /**
     * Splits a list of resources into groups.<p>
     * 
     * @param resources the list of resources
     *  
     * @return the list of groups 
     */
    public List<GROUP> getGroups(List<RESOURCE> resources) {

        List<RESOURCE> sortedResources = new ArrayList<RESOURCE>(resources);
        Collections.sort(sortedResources, new Comparator<RESOURCE>() {

            public int compare(RESOURCE r1, RESOURCE r2) {

                if (r1 == r2) {
                    return 0;
                }

                long date1 = getDateLastModified(r1);
                long date2 = getDateLastModified(r2);

                return (date1 > date2) ? -1 : (date1 < date2) ? 1 : 0;
            }
        });

        Map<Long, Integer> daysMap = computeDaysForResources(sortedResources);
        Map<GroupAge, List<RESOURCE>> resourcesByAge = partitionPublishResourcesByAge(sortedResources, daysMap);
        List<List<RESOURCE>> youngGroups = partitionYoungResources(resourcesByAge.get(GroupAge.young));
        List<List<RESOURCE>> mediumGroups = partitionMediumResources(resourcesByAge.get(GroupAge.medium), daysMap);
        List<RESOURCE> oldGroup = resourcesByAge.get(GroupAge.old);
        List<GROUP> resultGroups = new ArrayList<GROUP>();
        for (List<RESOURCE> groupRes : youngGroups) {
            String name = getPublishGroupName(groupRes, GroupAge.young);
            resultGroups.add(createGroup(name, groupRes));
        }
        for (List<RESOURCE> groupRes : mediumGroups) {
            String name = getPublishGroupName(groupRes, GroupAge.medium);
            resultGroups.add(createGroup(name, groupRes));
        }
        if (!oldGroup.isEmpty()) {
            String oldName = getPublishGroupName(oldGroup, GroupAge.old);
            resultGroups.add(createGroup(oldName, oldGroup));
        }
        return resultGroups;
    }

    /**
     * Given a list of resources, this method returns a list of their modification dates.<p>
     * 
     * @param resources a list of resources
     *  
     * @return the modification dates of the resources, in the same order as the resources 
     */
    public List<Long> getModificationDates(List<RESOURCE> resources) {

        List<Long> result = new ArrayList<Long>();
        for (RESOURCE res : resources) {
            result.add(new Long(getDateLastModified(res)));
        }
        return result;
    }

    /**
     * Returns the localized name for a given publish group based on its age.<p>
     * 
     * @param resources the resources of the publish group 
     * @param age the age of the publish group
     * 
     * @return the localized name of the publish group 
     */
    public String getPublishGroupName(List<RESOURCE> resources, GroupAge age) {

        long groupDate = getDateLastModified(resources.get(0));
        String groupName;
        switch (age) {
            case young:
                groupName = Messages.get().getBundle(m_locale).key(
                    Messages.GUI_GROUPNAME_SESSION_1,
                    new Date(groupDate));

                break;
            case medium:
                groupName = Messages.get().getBundle(m_locale).key(Messages.GUI_GROUPNAME_DAY_1, new Date(groupDate));
                break;
            case old:
            default:
                groupName = Messages.get().getBundle(m_locale).key(Messages.GUI_GROUPNAME_EVERYTHING_ELSE_0);
                break;
        }
        return groupName;
    }

    /**
     * Returns a calendar object representing the start of the day in which a given time lies.<p>
     * 
     * @param time a long representing a time 
     * 
     * @return a calendar object which represents the day in which the time lies 
     */
    public Calendar getStartOfDay(long time) {

        Calendar cal = Calendar.getInstance();
        cal.setTimeInMillis(time);
        int year = cal.get(Calendar.YEAR);
        int month = cal.get(Calendar.MONTH);
        int day = cal.get(Calendar.DAY_OF_MONTH);
        Calendar result = Calendar.getInstance();
        result.set(Calendar.YEAR, year);
        result.set(Calendar.MONTH, month);
        result.set(Calendar.DAY_OF_MONTH, day);
        return result;
    }

    /**
     * Computes publish groups for a list of resources with age "medium".<p>
     * 
     * @param resources the list of resources
     * @param days a map from modification dates to the number of days since the modification 
     * 
     * @return a list of publish groups 
     */
    public List<List<RESOURCE>> partitionMediumResources(List<RESOURCE> resources, Map<Long, Integer> days) {

        if (resources.isEmpty()) {
            return Collections.<List<RESOURCE>> emptyList();
        }
        RESOURCE firstRes = resources.get(0);
        int lastDay = days.get(new Long(getDateLastModified(firstRes))).intValue();
        List<List<RESOURCE>> result = new ArrayList<List<RESOURCE>>();
        List<RESOURCE> currentGroup = new ArrayList<RESOURCE>();
        result.add(currentGroup);
        for (RESOURCE res : resources) {
            LOG.debug("Processing medium-aged resource " + getRootPath(res));
            int day = days.get(new Long(getDateLastModified(res))).intValue();
            if (day != lastDay) {
                LOG.debug("=== new group ===");
                currentGroup = new ArrayList<RESOURCE>();
                result.add(currentGroup);
            }
            lastDay = day;
            currentGroup.add(res);
        }
        return result;
    }

    /**
     * Partitions a list of resources by their age in (local) days since the last modification.<p>
     * 
     * @param resources the list of resources to partition 
     * @param days the map from modification dates to the number of (local) days since the modification 
     * 
     * @return a map from age enum values to the list of resources which fall into the corresponding age group  
     */
    public Map<GroupAge, List<RESOURCE>> partitionPublishResourcesByAge(
        List<RESOURCE> resources,
        Map<Long, Integer> days) {

        List<RESOURCE> youngRes = new ArrayList<RESOURCE>();
        List<RESOURCE> mediumRes = new ArrayList<RESOURCE>();
        List<RESOURCE> oldRes = new ArrayList<RESOURCE>();
        for (RESOURCE res : resources) {
            int day = days.get(new Long(getDateLastModified(res))).intValue();
            List<RESOURCE> listToAddTo = null;
            if (day < 7) {
                listToAddTo = youngRes;
                LOG.debug("Classifying publish resource " + getRootPath(res) + " as young");
            } else if (day < 28) {
                listToAddTo = mediumRes;
                LOG.debug("Classifying publish resource " + getRootPath(res) + " as medium-aged");
            } else {
                listToAddTo = oldRes;
                LOG.debug("Classifying publish resource " + getRootPath(res) + " as old");
            }
            listToAddTo.add(res);
        }
        Map<GroupAge, List<RESOURCE>> result = new HashMap<GroupAge, List<RESOURCE>>();
        result.put(GroupAge.young, youngRes);
        result.put(GroupAge.medium, mediumRes);
        result.put(GroupAge.old, oldRes);
        return result;
    }

    /**
     * Partitions the list of young resources into publish groups.<p>
     * 
     * @param resources the list of resources to partition 
     * 
     * @return a partition of the resources into publish groups 
     */
    public List<List<RESOURCE>> partitionYoungResources(List<RESOURCE> resources) {

        if (resources.isEmpty()) {
            return Collections.<List<RESOURCE>> emptyList();
        }
        List<List<RESOURCE>> result = new ArrayList<List<RESOURCE>>();
        List<RESOURCE> currentGroup = new ArrayList<RESOURCE>();
        result.add(currentGroup);

        long lastDate = getDateLastModified(resources.get(0));
        for (RESOURCE res : resources) {
            LOG.debug("Processing young resource " + getRootPath(res));
            long resDate = getDateLastModified(res);
            if ((lastDate - resDate) > GROUP_SESSIONS_GAP) {
                LOG.debug("=== new group ===");
                currentGroup = new ArrayList<RESOURCE>();
                result.add(currentGroup);
            }
            lastDate = resDate;
            currentGroup.add(res);
        }
        return result;
    }

    /**
     * Creates a named group of resources.<p>
     * 
     * @param name the name of the group 
     * @param resources the resources which should be put in the group 
     * 
     * @return the named group 
     */
    protected abstract GROUP createGroup(String name, List<RESOURCE> resources);

    /**
     * Gets the last modification date of a resource.<p>
     * 
     * @param res the resource
     * 
     * @return the last modification date of res 
     */
    protected abstract long getDateLastModified(RESOURCE res);

    /**
     * Gets the root path of a resource.<p>
     * 
     * @param res the resource
     *  
     * @return the root path of res 
     */
    protected abstract String getRootPath(RESOURCE res);
}
<commit_msg>Implemented fix for publish dialog with resources that have a modification date in the future.<commit_after>/*
 * This library is part of OpenCms -
 * the Open Source Content Management System
 *
 * Copyright (c) Alkacon Software GmbH (http://www.alkacon.com)
 *
 * This library 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 2.1 of the License, or (at your option) any later version.
 *
 * This library 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.
 *
 * For further information about Alkacon Software, please see the
 * company website: http://www.alkacon.com
 *
 * For further information about OpenCms, please see the
 * project website: http://www.opencms.org
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package org.opencms.ade.publish;

import org.opencms.main.CmsLog;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import org.apache.commons.logging.Log;

/**
 * Helper class for splitting a publish list into publish groups.<p>
 * 
 * @param <RESOURCE> the resource class type
 * @param <GROUP> the resource group class type
 * 
 * @since 8.0.0
 */
public abstract class A_CmsPublishGroupHelper<RESOURCE, GROUP> {

    /** An enum representing the age of a publish list resource. */
    public enum GroupAge {
        /** group age constant. */
        medium,
        /** group age constant. */
        old,
        /** group age constant. */
        young
    }

    /** The gap between session groups. */
    protected static final int GROUP_SESSIONS_GAP = 8 * 60 * 60 * 1000;

    /** The log instance for this class. */
    private static final Log LOG = CmsLog.getLog(A_CmsPublishGroupHelper.class);

    /** The current locale. */
    private Locale m_locale;

    /**
     * Creates a new publish group helper for a given locale.<p>
     * 
     * @param locale the locale to use 
     */
    public A_CmsPublishGroupHelper(Locale locale) {

        m_locale = locale;
    }

    /**
     * Given a descending list of dates represented as longs, this method computes a map from the dates
     * to their age in (local) days.<p>
     *    
     * @param sortedDates a descending list of dates represented as longs 
     * 
     * @return a map from dates to ages (measured in days)  
     */
    public Map<Long, Integer> computeDays(List<Long> sortedDates) {

        if (sortedDates.isEmpty()) {
            return Collections.<Long, Integer> emptyMap();
        }
        Map<Long, Integer> days = new HashMap<Long, Integer>();
        long lastDate = System.currentTimeMillis();
        int dayCounter = 0;
        for (Long dateObj : sortedDates) {
            long date = dateObj.longValue();
            long dayDifference = getDayDifference(lastDate, date);
            dayCounter += dayDifference;
            lastDate = date;
            days.put(dateObj, new Integer(dayCounter));
        }
        return days;
    }

    /**
     * Computes a map from modification date to number of (local) days since the modification date.<p>
     * 
     * @param resources a list of resources
     * 
     * @return a map from modification dates to the number of days since the modification date 
     */
    public Map<Long, Integer> computeDaysForResources(List<RESOURCE> resources) {

        Map<Long, Integer> result = computeDays(getModificationDates(resources));
        if (LOG.isDebugEnabled()) {
            for (RESOURCE res : resources) {
                LOG.debug("Resource "
                    + getRootPath(res)
                    + " is "
                    + result.get(new Long(getDateLastModified(res)))
                    + " days old.");
            }
        }
        return result;
    }

    /**
     * Gets the difference in days between to dates given as longs.<p>
     * 
     * The first date must be later than the second date.
     * 
     * @param first the first date 
     * @param second the second date 
     * 
     * @return the difference between the two dates in days 
     */
    public int getDayDifference(long first, long second) {

        Calendar firstDay = getStartOfDay(first);
        Calendar secondDay = getStartOfDay(second);
        int result = 0;
        if (first >= second) {
            while (firstDay.after(secondDay)) {
                firstDay.add(Calendar.DAY_OF_MONTH, -1);
                result += 1;
            }
        } else {
            while (secondDay.after(firstDay)) {
                secondDay.add(Calendar.DAY_OF_MONTH, -1);
                result -= 1;
            }
        }
        return result;
    }

    /**
     * Splits a list of resources into groups.<p>
     * 
     * @param resources the list of resources
     *  
     * @return the list of groups 
     */
    public List<GROUP> getGroups(List<RESOURCE> resources) {

        List<RESOURCE> sortedResources = new ArrayList<RESOURCE>(resources);
        Collections.sort(sortedResources, new Comparator<RESOURCE>() {

            public int compare(RESOURCE r1, RESOURCE r2) {

                if (r1 == r2) {
                    return 0;
                }

                long date1 = getDateLastModified(r1);
                long date2 = getDateLastModified(r2);

                return (date1 > date2) ? -1 : (date1 < date2) ? 1 : 0;
            }
        });

        Map<Long, Integer> daysMap = computeDaysForResources(sortedResources);
        Map<GroupAge, List<RESOURCE>> resourcesByAge = partitionPublishResourcesByAge(sortedResources, daysMap);
        List<List<RESOURCE>> youngGroups = partitionYoungResources(resourcesByAge.get(GroupAge.young));
        List<List<RESOURCE>> mediumGroups = partitionMediumResources(resourcesByAge.get(GroupAge.medium), daysMap);
        List<RESOURCE> oldGroup = resourcesByAge.get(GroupAge.old);
        List<GROUP> resultGroups = new ArrayList<GROUP>();
        for (List<RESOURCE> groupRes : youngGroups) {
            String name = getPublishGroupName(groupRes, GroupAge.young);
            resultGroups.add(createGroup(name, groupRes));
        }
        for (List<RESOURCE> groupRes : mediumGroups) {
            String name = getPublishGroupName(groupRes, GroupAge.medium);
            resultGroups.add(createGroup(name, groupRes));
        }
        if (!oldGroup.isEmpty()) {
            String oldName = getPublishGroupName(oldGroup, GroupAge.old);
            resultGroups.add(createGroup(oldName, oldGroup));
        }
        return resultGroups;
    }

    /**
     * Given a list of resources, this method returns a list of their modification dates.<p>
     * 
     * @param resources a list of resources
     *  
     * @return the modification dates of the resources, in the same order as the resources 
     */
    public List<Long> getModificationDates(List<RESOURCE> resources) {

        List<Long> result = new ArrayList<Long>();
        for (RESOURCE res : resources) {
            result.add(new Long(getDateLastModified(res)));
        }
        return result;
    }

    /**
     * Returns the localized name for a given publish group based on its age.<p>
     * 
     * @param resources the resources of the publish group 
     * @param age the age of the publish group
     * 
     * @return the localized name of the publish group 
     */
    public String getPublishGroupName(List<RESOURCE> resources, GroupAge age) {

        long groupDate = getDateLastModified(resources.get(0));
        String groupName;
        switch (age) {
            case young:
                groupName = Messages.get().getBundle(m_locale).key(
                    Messages.GUI_GROUPNAME_SESSION_1,
                    new Date(groupDate));

                break;
            case medium:
                groupName = Messages.get().getBundle(m_locale).key(Messages.GUI_GROUPNAME_DAY_1, new Date(groupDate));
                break;
            case old:
            default:
                groupName = Messages.get().getBundle(m_locale).key(Messages.GUI_GROUPNAME_EVERYTHING_ELSE_0);
                break;
        }
        return groupName;
    }

    /**
     * Returns a calendar object representing the start of the day in which a given time lies.<p>
     * 
     * @param time a long representing a time 
     * 
     * @return a calendar object which represents the day in which the time lies 
     */
    public Calendar getStartOfDay(long time) {

        Calendar cal = Calendar.getInstance();
        cal.setTimeInMillis(time);
        int year = cal.get(Calendar.YEAR);
        int month = cal.get(Calendar.MONTH);
        int day = cal.get(Calendar.DAY_OF_MONTH);
        Calendar result = Calendar.getInstance();
        result.set(Calendar.YEAR, year);
        result.set(Calendar.MONTH, month);
        result.set(Calendar.DAY_OF_MONTH, day);
        return result;
    }

    /**
     * Computes publish groups for a list of resources with age "medium".<p>
     * 
     * @param resources the list of resources
     * @param days a map from modification dates to the number of days since the modification 
     * 
     * @return a list of publish groups 
     */
    public List<List<RESOURCE>> partitionMediumResources(List<RESOURCE> resources, Map<Long, Integer> days) {

        if (resources.isEmpty()) {
            return Collections.<List<RESOURCE>> emptyList();
        }
        RESOURCE firstRes = resources.get(0);
        int lastDay = days.get(new Long(getDateLastModified(firstRes))).intValue();
        List<List<RESOURCE>> result = new ArrayList<List<RESOURCE>>();
        List<RESOURCE> currentGroup = new ArrayList<RESOURCE>();
        result.add(currentGroup);
        for (RESOURCE res : resources) {
            LOG.debug("Processing medium-aged resource " + getRootPath(res));
            int day = days.get(new Long(getDateLastModified(res))).intValue();
            if (day != lastDay) {
                LOG.debug("=== new group ===");
                currentGroup = new ArrayList<RESOURCE>();
                result.add(currentGroup);
            }
            lastDay = day;
            currentGroup.add(res);
        }
        return result;
    }

    /**
     * Partitions a list of resources by their age in (local) days since the last modification.<p>
     * 
     * @param resources the list of resources to partition 
     * @param days the map from modification dates to the number of (local) days since the modification 
     * 
     * @return a map from age enum values to the list of resources which fall into the corresponding age group  
     */
    public Map<GroupAge, List<RESOURCE>> partitionPublishResourcesByAge(
        List<RESOURCE> resources,
        Map<Long, Integer> days) {

        List<RESOURCE> youngRes = new ArrayList<RESOURCE>();
        List<RESOURCE> mediumRes = new ArrayList<RESOURCE>();
        List<RESOURCE> oldRes = new ArrayList<RESOURCE>();
        for (RESOURCE res : resources) {
            int day = days.get(new Long(getDateLastModified(res))).intValue();
            List<RESOURCE> listToAddTo = null;
            if (day < 7) {
                listToAddTo = youngRes;
                LOG.debug("Classifying publish resource " + getRootPath(res) + " as young");
            } else if (day < 28) {
                listToAddTo = mediumRes;
                LOG.debug("Classifying publish resource " + getRootPath(res) + " as medium-aged");
            } else {
                listToAddTo = oldRes;
                LOG.debug("Classifying publish resource " + getRootPath(res) + " as old");
            }
            listToAddTo.add(res);
        }
        Map<GroupAge, List<RESOURCE>> result = new HashMap<GroupAge, List<RESOURCE>>();
        result.put(GroupAge.young, youngRes);
        result.put(GroupAge.medium, mediumRes);
        result.put(GroupAge.old, oldRes);
        return result;
    }

    /**
     * Partitions the list of young resources into publish groups.<p>
     * 
     * @param resources the list of resources to partition 
     * 
     * @return a partition of the resources into publish groups 
     */
    public List<List<RESOURCE>> partitionYoungResources(List<RESOURCE> resources) {

        if (resources.isEmpty()) {
            return Collections.<List<RESOURCE>> emptyList();
        }
        List<List<RESOURCE>> result = new ArrayList<List<RESOURCE>>();
        List<RESOURCE> currentGroup = new ArrayList<RESOURCE>();
        result.add(currentGroup);

        long lastDate = getDateLastModified(resources.get(0));
        for (RESOURCE res : resources) {
            LOG.debug("Processing young resource " + getRootPath(res));
            long resDate = getDateLastModified(res);
            if ((lastDate - resDate) > GROUP_SESSIONS_GAP) {
                LOG.debug("=== new group ===");
                currentGroup = new ArrayList<RESOURCE>();
                result.add(currentGroup);
            }
            lastDate = resDate;
            currentGroup.add(res);
        }
        return result;
    }

    /**
     * Creates a named group of resources.<p>
     * 
     * @param name the name of the group 
     * @param resources the resources which should be put in the group 
     * 
     * @return the named group 
     */
    protected abstract GROUP createGroup(String name, List<RESOURCE> resources);

    /**
     * Gets the last modification date of a resource.<p>
     * 
     * @param res the resource
     * 
     * @return the last modification date of res 
     */
    protected abstract long getDateLastModified(RESOURCE res);

    /**
     * Gets the root path of a resource.<p>
     * 
     * @param res the resource
     *  
     * @return the root path of res 
     */
    protected abstract String getRootPath(RESOURCE res);
}

{'fraction_non_alphanumeric': 0.09558357399450589, 'fraction_numerical': 0.004085370148622949, 'mean_word_length': 2.2488558352402745, 'pattern_counts': {'":': 0, '<': 188, '<?xml version=': 0, '>': 186, 'https://': 0, 'lorem ipsum': 0, 'www.': 6, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3952561', 'n_tokens_mistral': 7850, 'n_tokens_neox': 7440, 'n_words': 2386}