001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software, please see the
018 * company website: http://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: http://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.ade.publish;
029
030import org.opencms.main.CmsLog;
031
032import java.util.ArrayList;
033import java.util.Calendar;
034import java.util.Collections;
035import java.util.Comparator;
036import java.util.Date;
037import java.util.HashMap;
038import java.util.List;
039import java.util.Locale;
040import java.util.Map;
041
042import org.apache.commons.logging.Log;
043
044/**
045 * Helper class for splitting a publish list into publish groups.<p>
046 *
047 * @param <RESOURCE> the resource class type
048 * @param <GROUP> the resource group class type
049 *
050 * @since 8.0.0
051 */
052public abstract class A_CmsPublishGroupHelper<RESOURCE, GROUP> {
053
054    /** An enum representing the age of a publish list resource. */
055    public enum GroupAge {
056        /** group age constant. */
057        medium, /** group age constant. */
058        old, /** group age constant. */
059        young
060    }
061
062    /**
063     * Comparator used for sorting publish resources.<p>
064     */
065    public class SortingComparator implements Comparator<RESOURCE> {
066
067        /**
068         * @see java.util.Comparator#compare(java.lang.Object, java.lang.Object)
069         */
070        public int compare(RESOURCE r1, RESOURCE r2) {
071
072            if (r1 == r2) {
073                return 0;
074            }
075
076            long date1 = getDateLastModified(r1);
077            long date2 = getDateLastModified(r2);
078
079            return (date1 > date2) ? -1 : (date1 < date2) ? 1 : 0;
080        }
081    }
082
083    /** The gap between session groups. */
084    protected static final int GROUP_SESSIONS_GAP = 8 * 60 * 60 * 1000;
085
086    /** The log instance for this class. */
087    private static final Log LOG = CmsLog.getLog(A_CmsPublishGroupHelper.class);
088
089    /** The current locale. */
090    private Locale m_locale;
091
092    /**
093     * Creates a new publish group helper for a given locale.<p>
094     *
095     * @param locale the locale to use
096     */
097    public A_CmsPublishGroupHelper(Locale locale) {
098
099        m_locale = locale;
100    }
101
102    /**
103     * Given a descending list of dates represented as longs, this method computes a map from the dates
104     * to their age in (local) days.<p>
105     *
106     * @param sortedDates a descending list of dates represented as longs
107     *
108     * @return a map from dates to ages (measured in days)
109     */
110    public Map<Long, Integer> computeDays(List<Long> sortedDates) {
111
112        if (sortedDates.isEmpty()) {
113            return Collections.<Long, Integer> emptyMap();
114        }
115        Map<Long, Integer> days = new HashMap<Long, Integer>();
116        long lastDate = System.currentTimeMillis();
117        int dayCounter = 0;
118        for (Long dateObj : sortedDates) {
119            long date = dateObj.longValue();
120            long dayDifference = getDayDifference(lastDate, date);
121            dayCounter += dayDifference;
122            lastDate = date;
123            days.put(dateObj, new Integer(dayCounter));
124        }
125        return days;
126    }
127
128    /**
129     * Computes a map from modification date to number of (local) days since the modification date.<p>
130     *
131     * @param resources a list of resources
132     *
133     * @return a map from modification dates to the number of days since the modification date
134     */
135    public Map<Long, Integer> computeDaysForResources(List<RESOURCE> resources) {
136
137        Map<Long, Integer> result = computeDays(getModificationDates(resources));
138        if (LOG.isDebugEnabled()) {
139            for (RESOURCE res : resources) {
140                LOG.debug(
141                    "Resource "
142                        + getRootPath(res)
143                        + " is "
144                        + result.get(new Long(getDateLastModified(res)))
145                        + " days old.");
146            }
147        }
148        return result;
149    }
150
151    /**
152     * Gets the difference in days between to dates given as longs.<p>
153     *
154     * The first date must be later than the second date.
155     *
156     * @param first the first date
157     * @param second the second date
158     *
159     * @return the difference between the two dates in days
160     */
161    public int getDayDifference(long first, long second) {
162
163        Calendar firstDay = getStartOfDay(first);
164        Calendar secondDay = getStartOfDay(second);
165        int result = 0;
166        if (first >= second) {
167            while (firstDay.after(secondDay)) {
168                firstDay.add(Calendar.DAY_OF_MONTH, -1);
169                result += 1;
170            }
171        } else {
172            while (secondDay.after(firstDay)) {
173                secondDay.add(Calendar.DAY_OF_MONTH, -1);
174                result -= 1;
175            }
176        }
177        return result;
178    }
179
180    /**
181     * Splits a list of resources into groups.<p>
182     *
183     * @param resources the list of resources
184     *
185     * @return the list of groups
186     */
187    public List<GROUP> getGroups(List<RESOURCE> resources) {
188
189        List<RESOURCE> sortedResources = new ArrayList<RESOURCE>(resources);
190        Collections.sort(sortedResources, new SortingComparator());
191        Map<Long, Integer> daysMap = computeDaysForResources(sortedResources);
192        Map<GroupAge, List<RESOURCE>> resourcesByAge = partitionPublishResourcesByAge(sortedResources, daysMap);
193        List<List<RESOURCE>> youngGroups = partitionYoungResources(resourcesByAge.get(GroupAge.young));
194        List<List<RESOURCE>> mediumGroups = partitionMediumResources(resourcesByAge.get(GroupAge.medium), daysMap);
195        List<RESOURCE> oldGroup = resourcesByAge.get(GroupAge.old);
196        List<GROUP> resultGroups = new ArrayList<GROUP>();
197        for (List<RESOURCE> groupRes : youngGroups) {
198            String name = getPublishGroupName(groupRes, GroupAge.young);
199            resultGroups.add(createGroup(name, groupRes));
200        }
201        for (List<RESOURCE> groupRes : mediumGroups) {
202            String name = getPublishGroupName(groupRes, GroupAge.medium);
203            resultGroups.add(createGroup(name, groupRes));
204        }
205        if (!oldGroup.isEmpty()) {
206            String oldName = getPublishGroupName(oldGroup, GroupAge.old);
207            resultGroups.add(createGroup(oldName, oldGroup));
208        }
209        return resultGroups;
210    }
211
212    /**
213     * Given a list of resources, this method returns a list of their modification dates.<p>
214     *
215     * @param resources a list of resources
216     *
217     * @return the modification dates of the resources, in the same order as the resources
218     */
219    public List<Long> getModificationDates(List<RESOURCE> resources) {
220
221        List<Long> result = new ArrayList<Long>();
222        for (RESOURCE res : resources) {
223            result.add(new Long(getDateLastModified(res)));
224        }
225        return result;
226    }
227
228    /**
229     * Returns the localized name for a given publish group based on its age.<p>
230     *
231     * @param resources the resources of the publish group
232     * @param age the age of the publish group
233     *
234     * @return the localized name of the publish group
235     */
236    public String getPublishGroupName(List<RESOURCE> resources, GroupAge age) {
237
238        long groupDate = getDateLastModified(resources.get(0));
239        String groupName;
240        switch (age) {
241            case young:
242                groupName = Messages.get().getBundle(m_locale).key(
243                    Messages.GUI_GROUPNAME_SESSION_1,
244                    new Date(groupDate));
245
246                break;
247            case medium:
248                groupName = Messages.get().getBundle(m_locale).key(Messages.GUI_GROUPNAME_DAY_1, new Date(groupDate));
249                break;
250            case old:
251            default:
252                groupName = Messages.get().getBundle(m_locale).key(Messages.GUI_GROUPNAME_EVERYTHING_ELSE_0);
253                break;
254        }
255        return groupName;
256    }
257
258    /**
259     * Returns a calendar object representing the start of the day in which a given time lies.<p>
260     *
261     * @param time a long representing a time
262     *
263     * @return a calendar object which represents the day in which the time lies
264     */
265    public Calendar getStartOfDay(long time) {
266
267        Calendar cal = Calendar.getInstance();
268        cal.setTimeInMillis(time);
269        int year = cal.get(Calendar.YEAR);
270        int month = cal.get(Calendar.MONTH);
271        int day = cal.get(Calendar.DAY_OF_MONTH);
272        Calendar result = Calendar.getInstance();
273        result.set(Calendar.YEAR, year);
274        result.set(Calendar.MONTH, month);
275        result.set(Calendar.DAY_OF_MONTH, day);
276        return result;
277    }
278
279    /**
280     * Computes publish groups for a list of resources with age "medium".<p>
281     *
282     * @param resources the list of resources
283     * @param days a map from modification dates to the number of days since the modification
284     *
285     * @return a list of publish groups
286     */
287    public List<List<RESOURCE>> partitionMediumResources(List<RESOURCE> resources, Map<Long, Integer> days) {
288
289        if (resources.isEmpty()) {
290            return Collections.<List<RESOURCE>> emptyList();
291        }
292        RESOURCE firstRes = resources.get(0);
293        int lastDay = days.get(new Long(getDateLastModified(firstRes))).intValue();
294        List<List<RESOURCE>> result = new ArrayList<List<RESOURCE>>();
295        List<RESOURCE> currentGroup = new ArrayList<RESOURCE>();
296        result.add(currentGroup);
297        for (RESOURCE res : resources) {
298            LOG.debug("Processing medium-aged resource " + getRootPath(res));
299            int day = days.get(new Long(getDateLastModified(res))).intValue();
300            if (day != lastDay) {
301                LOG.debug("=== new group ===");
302                currentGroup = new ArrayList<RESOURCE>();
303                result.add(currentGroup);
304            }
305            lastDay = day;
306            currentGroup.add(res);
307        }
308        return result;
309    }
310
311    /**
312     * Partitions a list of resources by their age in (local) days since the last modification.<p>
313     *
314     * @param resources the list of resources to partition
315     * @param days the map from modification dates to the number of (local) days since the modification
316     *
317     * @return a map from age enum values to the list of resources which fall into the corresponding age group
318     */
319    public Map<GroupAge, List<RESOURCE>> partitionPublishResourcesByAge(
320        List<RESOURCE> resources,
321        Map<Long, Integer> days) {
322
323        List<RESOURCE> youngRes = new ArrayList<RESOURCE>();
324        List<RESOURCE> mediumRes = new ArrayList<RESOURCE>();
325        List<RESOURCE> oldRes = new ArrayList<RESOURCE>();
326        for (RESOURCE res : resources) {
327            int day = days.get(new Long(getDateLastModified(res))).intValue();
328            List<RESOURCE> listToAddTo = null;
329            if (day < 7) {
330                listToAddTo = youngRes;
331                LOG.debug("Classifying publish resource " + getRootPath(res) + " as young");
332            } else if (day < 28) {
333                listToAddTo = mediumRes;
334                LOG.debug("Classifying publish resource " + getRootPath(res) + " as medium-aged");
335            } else {
336                listToAddTo = oldRes;
337                LOG.debug("Classifying publish resource " + getRootPath(res) + " as old");
338            }
339            listToAddTo.add(res);
340        }
341        Map<GroupAge, List<RESOURCE>> result = new HashMap<GroupAge, List<RESOURCE>>();
342        result.put(GroupAge.young, youngRes);
343        result.put(GroupAge.medium, mediumRes);
344        result.put(GroupAge.old, oldRes);
345        return result;
346    }
347
348    /**
349     * Partitions the list of young resources into publish groups.<p>
350     *
351     * @param resources the list of resources to partition
352     *
353     * @return a partition of the resources into publish groups
354     */
355    public List<List<RESOURCE>> partitionYoungResources(List<RESOURCE> resources) {
356
357        if (resources.isEmpty()) {
358            return Collections.<List<RESOURCE>> emptyList();
359        }
360        List<List<RESOURCE>> result = new ArrayList<List<RESOURCE>>();
361        List<RESOURCE> currentGroup = new ArrayList<RESOURCE>();
362        result.add(currentGroup);
363
364        long lastDate = getDateLastModified(resources.get(0));
365        for (RESOURCE res : resources) {
366            LOG.debug("Processing young resource " + getRootPath(res));
367            long resDate = getDateLastModified(res);
368            if ((lastDate - resDate) > GROUP_SESSIONS_GAP) {
369                LOG.debug("=== new group ===");
370                currentGroup = new ArrayList<RESOURCE>();
371                result.add(currentGroup);
372            }
373            lastDate = resDate;
374            currentGroup.add(res);
375        }
376        return result;
377    }
378
379    /**
380     * Creates a named group of resources.<p>
381     *
382     * @param name the name of the group
383     * @param resources the resources which should be put in the group
384     *
385     * @return the named group
386     */
387    protected abstract GROUP createGroup(String name, List<RESOURCE> resources);
388
389    /**
390     * Gets the last modification date of a resource.<p>
391     *
392     * @param res the resource
393     *
394     * @return the last modification date of res
395     */
396    protected abstract long getDateLastModified(RESOURCE res);
397
398    /**
399     * Gets the root path of a resource.<p>
400     *
401     * @param res the resource
402     *
403     * @return the root path of res
404     */
405    protected abstract String getRootPath(RESOURCE res);
406}