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