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.notification;
029
030import org.opencms.db.CmsDbEntryNotFoundException;
031import org.opencms.db.CmsUserSettings;
032import org.opencms.file.CmsObject;
033import org.opencms.file.CmsPropertyDefinition;
034import org.opencms.file.CmsResource;
035import org.opencms.file.CmsResourceFilter;
036import org.opencms.file.CmsUser;
037import org.opencms.i18n.CmsLocaleManager;
038import org.opencms.main.CmsException;
039import org.opencms.main.CmsLog;
040import org.opencms.main.OpenCms;
041import org.opencms.util.CmsStringUtil;
042
043import java.util.ArrayList;
044import java.util.Calendar;
045import java.util.Collection;
046import java.util.Date;
047import java.util.GregorianCalendar;
048import java.util.HashMap;
049import java.util.Iterator;
050import java.util.List;
051import java.util.Map;
052import java.util.TimeZone;
053
054import org.apache.commons.logging.Log;
055import org.apache.commons.mail.EmailException;
056
057/**
058 * The basic class for the content notification feature in OpenCms. Collects all resources that require a notification,
059 * creates and sends notifications to their responsible users.<p/>
060 *
061 */
062public class CmsNotificationCandidates {
063
064    /** The log object for this class. */
065    private static final Log LOG = CmsLog.getLog(CmsNotificationCandidates.class);
066
067    /** the CmsObject. */
068    private CmsObject m_cms;
069
070    /** The resources which come into question for notifications of responsible users. */
071    private List<CmsExtendedNotificationCause> m_resources;
072
073    /**
074     * Collects all resources that will expire in short time, or will become valid, or are not modified since a long time.<p>
075     *
076     * @param cms the CmsObject
077     *
078     * @throws CmsException if something goes wrong
079     */
080    public CmsNotificationCandidates(CmsObject cms)
081    throws CmsException {
082
083        m_resources = new ArrayList<CmsExtendedNotificationCause>();
084        m_cms = cms;
085        m_cms.getRequestContext().setCurrentProject(
086            m_cms.readProject(OpenCms.getSystemInfo().getNotificationProject()));
087        String folder = "/";
088        GregorianCalendar now = new GregorianCalendar(TimeZone.getDefault(), CmsLocaleManager.getDefaultLocale());
089        now.setTimeInMillis(System.currentTimeMillis());
090        GregorianCalendar inOneWeek = (GregorianCalendar)now.clone();
091        inOneWeek.add(Calendar.WEEK_OF_YEAR, 1);
092        Iterator<CmsResource> resources;
093        CmsResource resource;
094
095        // read all files with the 'notification-interval' property set
096        try {
097            resources = m_cms.readResourcesWithProperty(
098                folder,
099                CmsPropertyDefinition.PROPERTY_NOTIFICATION_INTERVAL).iterator();
100            while (resources.hasNext()) {
101                resource = resources.next();
102                int notification_interval = Integer.parseInt(
103                    m_cms.readPropertyObject(
104                        resource,
105                        CmsPropertyDefinition.PROPERTY_NOTIFICATION_INTERVAL,
106                        true).getValue());
107                GregorianCalendar intervalBefore = new GregorianCalendar(
108                    TimeZone.getDefault(),
109                    CmsLocaleManager.getDefaultLocale());
110                intervalBefore.setTimeInMillis(resource.getDateLastModified());
111                intervalBefore.add(Calendar.DAY_OF_YEAR, notification_interval);
112                GregorianCalendar intervalAfter = (GregorianCalendar)intervalBefore.clone();
113                intervalAfter.add(Calendar.WEEK_OF_YEAR, -1);
114
115                for (int i = 0; (i < 100) && intervalAfter.getTime().before(now.getTime()); i++) {
116                    if (intervalBefore.getTime().after(now.getTime())) {
117                        m_resources.add(
118                            new CmsExtendedNotificationCause(
119                                resource,
120                                CmsExtendedNotificationCause.RESOURCE_UPDATE_REQUIRED,
121                                intervalBefore.getTime()));
122                    }
123                    intervalBefore.add(Calendar.DAY_OF_YEAR, notification_interval);
124                    intervalAfter.add(Calendar.DAY_OF_YEAR, notification_interval);
125                }
126            }
127        } catch (CmsDbEntryNotFoundException e) {
128            // no resources with property 'notification-interval', ignore
129        }
130
131        // read all files that were not modified longer than the max notification-time
132        GregorianCalendar oneYearAgo = (GregorianCalendar)now.clone();
133        oneYearAgo.add(Calendar.DAY_OF_YEAR, -OpenCms.getSystemInfo().getNotificationTime());
134        // create a resource filter to get the resources with
135        CmsResourceFilter filter = CmsResourceFilter.IGNORE_EXPIRATION.addRequireLastModifiedBefore(
136            oneYearAgo.getTimeInMillis());
137        resources = m_cms.readResources(folder, filter).iterator();
138        while (resources.hasNext()) {
139            resource = resources.next();
140            m_resources.add(
141                new CmsExtendedNotificationCause(
142                    resource,
143                    CmsExtendedNotificationCause.RESOURCE_OUTDATED,
144                    new Date(resource.getDateLastModified())));
145        }
146
147        // get all resources that will expire within the next week
148        CmsResourceFilter resourceFilter = CmsResourceFilter.IGNORE_EXPIRATION.addRequireExpireBefore(
149            inOneWeek.getTimeInMillis());
150        resourceFilter = resourceFilter.addRequireExpireAfter(now.getTimeInMillis());
151        resources = m_cms.readResources(folder, resourceFilter).iterator();
152        while (resources.hasNext()) {
153            resource = resources.next();
154            m_resources.add(
155                new CmsExtendedNotificationCause(
156                    resource,
157                    CmsExtendedNotificationCause.RESOURCE_EXPIRES,
158                    new Date(resource.getDateExpired())));
159        }
160
161        // get all resources that will release within the next week
162        resourceFilter = CmsResourceFilter.IGNORE_EXPIRATION.addRequireReleaseBefore(inOneWeek.getTimeInMillis());
163        resourceFilter = resourceFilter.addRequireReleaseAfter(now.getTimeInMillis());
164        resources = m_cms.readResources(folder, resourceFilter).iterator();
165        while (resources.hasNext()) {
166            resource = resources.next();
167            m_resources.add(
168                new CmsExtendedNotificationCause(
169                    resource,
170                    CmsExtendedNotificationCause.RESOURCE_RELEASE,
171                    new Date(resource.getDateReleased())));
172        }
173    }
174
175    /**
176     * Sends all notifications to the responsible users.<p>
177     *
178     * @return a string listing all responsibles that a notification was sent to
179     *
180     * @throws CmsException if something goes wrong
181     */
182    public String notifyResponsibles() throws CmsException {
183
184        Iterator<CmsContentNotification> notifications = filterConfirmedResources(getContentNotifications()).iterator();
185        if (notifications.hasNext()) {
186            StringBuffer result = new StringBuffer(
187                Messages.get().getBundle().key(Messages.LOG_NOTIFICATIONS_SENT_TO_0));
188            result.append(' ');
189            while (notifications.hasNext()) {
190                CmsContentNotification contentNotification = notifications.next();
191                result.append(contentNotification.getResponsible().getName());
192                if (notifications.hasNext()) {
193                    result.append(", ");
194                }
195                try {
196                    contentNotification.send();
197                } catch (EmailException e) {
198                    LOG.error(e.getLocalizedMessage(), e);
199                }
200            }
201            return result.toString();
202        } else {
203            return Messages.get().getBundle().key(Messages.LOG_NO_NOTIFICATIONS_SENT_0);
204        }
205    }
206
207    /**
208     * Returns a collection of CmsContentNotifications, one for each responsible that receives a notification.<p>
209     *
210     * @return the list of CmsContentNotifications, one for each responsible that receives a notification
211     *
212     * @throws CmsException if something goes wrong
213     */
214    protected Collection<CmsContentNotification> getContentNotifications() throws CmsException {
215
216        // get all owners for the resource
217        Iterator<CmsExtendedNotificationCause> notificationCandidates = m_resources.iterator();
218        Map<CmsUser, CmsContentNotification> result = new HashMap<CmsUser, CmsContentNotification>();
219        while (notificationCandidates.hasNext()) {
220            CmsExtendedNotificationCause resourceInfo = notificationCandidates.next();
221            CmsResource resource = resourceInfo.getResource();
222            // skip, if content notification is not enabled for this resource
223            String enableNotification = m_cms.readPropertyObject(
224                resource,
225                CmsPropertyDefinition.PROPERTY_ENABLE_NOTIFICATION,
226                true).getValue();
227            if (Boolean.valueOf(enableNotification).booleanValue()) {
228                try {
229                    Iterator<CmsUser> responsibles = m_cms.readResponsibleUsers(resource).iterator();
230                    while (responsibles.hasNext()) {
231                        CmsUser responsible = responsibles.next();
232                        if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(responsible.getEmail())) {
233                            // check, if resultset already contains a content notification for the user
234                            CmsContentNotification contentNotification = result.get(responsible);
235
236                            // if not add a new content notification
237                            if (contentNotification == null) {
238                                contentNotification = new CmsContentNotification(responsible, m_cms);
239                                result.put(responsible, contentNotification);
240                            }
241                            List<CmsExtendedNotificationCause> resourcesForResponsible = contentNotification.getNotificationCauses();
242                            if (resourcesForResponsible == null) {
243                                resourcesForResponsible = new ArrayList<CmsExtendedNotificationCause>();
244                                contentNotification.setNotificationCauses(resourcesForResponsible);
245                            }
246                            resourcesForResponsible.add(resourceInfo);
247                        }
248                    }
249                } catch (CmsException e) {
250                    if (LOG.isInfoEnabled()) {
251                        LOG.error(e.getLocalizedMessage(), e);
252                    }
253                }
254            }
255        }
256        return result.values();
257    }
258
259    /**
260     * Updates the resources that were confirmed by the user. That means deletes the resources that need not a
261     * notification any more.
262     * removes all resources which do not occur in the candidate list.<p>
263     *
264     * @param contentNotifications the list of {@link CmsContentNotification} objects to remove from the set of confirmed resources
265     * @return a new CmsConfirmedResources Object which all the resource removed
266     */
267    private Collection<CmsContentNotification> filterConfirmedResources(
268        Collection<CmsContentNotification> contentNotifications) {
269
270        Iterator<CmsContentNotification> notifications = contentNotifications.iterator();
271        while (notifications.hasNext()) {
272            CmsContentNotification contentNotification = notifications.next();
273            CmsUser responsible = contentNotification.getResponsible();
274            // check, if user was already notified
275            List<?> confirmedResourcesList = (List<?>)responsible.getAdditionalInfo(
276                CmsUserSettings.ADDITIONAL_INFO_CONFIRMED_RESOURCES);
277            if (confirmedResourcesList == null) {
278                confirmedResourcesList = new ArrayList<Object>();
279                responsible.setAdditionalInfo(
280                    CmsUserSettings.ADDITIONAL_INFO_CONFIRMED_RESOURCES,
281                    new ArrayList<Object>());
282            }
283
284            List<CmsExtendedNotificationCause> notificationCandidates = contentNotification.getNotificationCauses();
285
286            List<CmsExtendedNotificationCause> notificationResources = new ArrayList<CmsExtendedNotificationCause>(
287                notificationCandidates);
288            // remove already confirmed resources
289            Iterator<?> i = confirmedResourcesList.iterator();
290            while (i.hasNext()) {
291                Object o = i.next();
292                if (notificationResources.contains(o)) {
293                    notificationResources.remove(o);
294                }
295            }
296            // filter confirmed resources
297            i = new ArrayList<Object>(confirmedResourcesList).iterator();
298            while (i.hasNext()) {
299                Object o = i.next();
300                if (!notificationCandidates.contains(o)) {
301                    confirmedResourcesList.remove(o);
302                }
303            }
304
305            if (notificationResources.isEmpty()) {
306                // Remove notification, if resource list is empty
307                contentNotifications.remove(contentNotification);
308            } else {
309                contentNotification.setNotificationCauses(notificationResources);
310            }
311            try {
312                m_cms.writeUser(responsible);
313            } catch (CmsException e) {
314                LOG.error(e.getLocalizedMessage(), e);
315            }
316        }
317        return contentNotifications;
318    }
319}