001/*
002 *  Copyright 2010 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.cms.alerts;
017
018import java.time.LocalDate;
019import java.time.ZonedDateTime;
020import java.util.ArrayList;
021import java.util.Collections;
022import java.util.HashMap;
023import java.util.List;
024import java.util.Map;
025import java.util.UUID;
026
027import org.apache.avalon.framework.service.ServiceException;
028import org.apache.avalon.framework.service.ServiceManager;
029import org.apache.commons.lang3.StringUtils;
030import org.quartz.SchedulerException;
031
032import org.ametys.cms.clientsideelement.SmartContentClientSideElement;
033import org.ametys.cms.repository.Content;
034import org.ametys.cms.repository.ModifiableContent;
035import org.ametys.core.right.RightManager.RightResult;
036import org.ametys.core.schedule.Runnable;
037import org.ametys.core.schedule.Runnable.FireProcess;
038import org.ametys.core.schedule.Runnable.MisfirePolicy;
039import org.ametys.core.ui.Callable;
040import org.ametys.core.user.UserIdentity;
041import org.ametys.core.util.DateUtils;
042import org.ametys.plugins.core.impl.schedule.DefaultRunnable;
043import org.ametys.plugins.core.schedule.Scheduler;
044import org.ametys.plugins.repository.AmetysRepositoryException;
045import org.ametys.plugins.repository.data.holder.ModelLessDataHolder;
046import org.ametys.plugins.repository.data.holder.ModifiableModelLessDataHolder;
047import org.ametys.plugins.repository.version.DataAndVersionAwareAmetysObject;
048import org.ametys.plugins.repository.version.ModifiableDataAwareVersionableAmetysObject;
049import org.ametys.runtime.config.Config;
050import org.ametys.runtime.i18n.I18nizableText;
051
052/**
053 * This element creates a toggle button representing the reminders state.
054 */
055public class ContentAlertsClientSideElement extends SmartContentClientSideElement
056{
057    /** The ametys scheduler */
058    protected Scheduler _scheduler;
059    
060    @Override
061    public void service(ServiceManager serviceManager) throws ServiceException
062    {
063        super.service(serviceManager);
064        _scheduler = (Scheduler) serviceManager.lookup(Scheduler.ROLE);
065    }
066
067    enum AlertsStatus
068    {
069        /** All the alerts are enabled */
070        ENABLED,
071
072        /** All the alerts are disabled */
073        DISABLED,
074
075        /** Some alerts are enabled, others are disabled */
076        MIXED
077    }
078    
079    @Override
080    public List<Script> getScripts(boolean ignoreRights, Map<String, Object> contextParameters)
081    {
082        boolean enabled = Config.getInstance().getValue("remind.content.enabled");
083        if (enabled)
084        {
085            return super.getScripts(ignoreRights, contextParameters);
086        }
087
088        return new ArrayList<>();
089    }
090
091    
092    @Override
093    @Callable (rights = "CMS_Rights_Content_Alerts")
094    public Map<String, Object> getStatus(List<String> contentsId)
095    {
096        Map<String, Object> results = super.getStatus(contentsId);
097        
098        results.put("alert-enabled-contents", new ArrayList<>());
099        results.put("alert-disabled-contents", new ArrayList<>());
100        
101        Long unmodifiedAlertDelay = Config.getInstance().getValue("remind.unmodified.content.delay");
102        
103        @SuppressWarnings("unchecked")
104        List<Map<String, Object>> allrightContents = (List<Map<String, Object>>) results.get("allright-contents");
105        // iterate only on allright contents to take advantage of the check done by call to parent
106        for (Map<String, Object> allRightContent : allrightContents)
107        {
108            String contentId = (String) allRightContent.get("id");
109            Content content = _resolver.resolveById(contentId);
110            
111            if (content instanceof DataAndVersionAwareAmetysObject && content instanceof ModifiableContent)
112            {
113                ModelLessDataHolder dataHolder = ((DataAndVersionAwareAmetysObject) content).getUnversionedDataHolder();
114                
115                boolean reminderEnabled = dataHolder.getValue(AlertsConstants.REMINDER_ENABLED, false);
116                boolean unmodifiedAlertEnabled = dataHolder.getValue(AlertsConstants.UNMODIFIED_ALERT_ENABLED, false);
117                // Alerts enabled/disabled
118                Map<String, Object> contentParams = getContentDefaultParameters (content);
119                
120                if (unmodifiedAlertEnabled && unmodifiedAlertDelay > 0 || reminderEnabled)
121                {
122                    I18nizableText ed = (I18nizableText) this._script.getParameters().get("alerts-enabled-description");
123                    I18nizableText msg = new I18nizableText(ed.getCatalogue(), ed.getKey(), Collections.singletonList(_contentHelper.getTitle(content)));
124
125                    contentParams.put("description", msg);
126
127                    @SuppressWarnings("unchecked")
128                    List<Map<String, Object>> enabledAlerts = (List<Map<String, Object>>) results.get("alert-enabled-contents");
129                    enabledAlerts.add(contentParams);
130                }
131                else
132                {
133                    I18nizableText ed = (I18nizableText) this._script.getParameters().get("alerts-disabled-description");
134                    I18nizableText msg = new I18nizableText(ed.getCatalogue(), ed.getKey(), Collections.singletonList(_contentHelper.getTitle(content)));
135
136                    contentParams.put("description", msg);
137
138                    @SuppressWarnings("unchecked")
139                    List<Map<String, Object>> disabledAlerts = (List<Map<String, Object>>) results.get("alert-disabled-contents");
140                    disabledAlerts.add(contentParams);
141                }
142            }
143        }
144        
145        return results;
146    }
147
148    /**
149     * Get information on reminders state.
150     * 
151     * @param contentsId The list of contents' ids
152     * @return informations on reminders state.
153     */
154    @Callable(rights = Callable.CHECKED_BY_IMPLEMENTATION)
155    public Map<String, Object> getAlertsInformations(List<String> contentsId)
156    {
157        Map<String, Object> results = new HashMap<>();
158
159        results.put("no-right-contents", new ArrayList<>());
160
161        List<String> contentIds = new ArrayList<>();
162        AlertsStatus unmodifiedAlertStatus = null;
163        AlertsStatus reminderAlertStatus = null;
164
165        UserIdentity user = _currentUserProvider.getUser();
166        
167        for (String contentId : contentsId)
168        {
169            Content content = _resolver.resolveById(contentId);
170
171            if (_rightManager.hasRight(user, "CMS_Rights_Content_Alerts", content) != RightResult.RIGHT_ALLOW)
172            {
173                Map<String, Object> contentParams = getContentDefaultParameters(content);
174                @SuppressWarnings("unchecked")
175                List<Map<String, Object>> noRightContents = (List<Map<String, Object>>) results.get("no-right-contents");
176                noRightContents.add(contentParams);
177            }
178            else if (content instanceof DataAndVersionAwareAmetysObject && content instanceof ModifiableContent)
179            {
180                contentIds.add(contentId);
181                
182                ModelLessDataHolder dataHolder = ((DataAndVersionAwareAmetysObject) content).getUnversionedDataHolder();
183                
184                boolean unmodifiedAlertEnabled = dataHolder.getValue(AlertsConstants.UNMODIFIED_ALERT_ENABLED, false);
185                if (unmodifiedAlertEnabled)
186                {
187                    String unmodifiedAlertText = dataHolder.getValue(AlertsConstants.UNMODIFIED_ALERT_TEXT, StringUtils.EMPTY);
188                    results.put("unmodifiedAlertText", unmodifiedAlertText);
189                }
190
191                unmodifiedAlertStatus = _getUnmodifiedStatusAlertsInformations(unmodifiedAlertStatus, unmodifiedAlertEnabled);
192
193                boolean reminderEnabled = dataHolder.getValue(AlertsConstants.REMINDER_ENABLED, false);
194                if (reminderEnabled)
195                {
196                    ZonedDateTime reminderDate = dataHolder.getValue(AlertsConstants.REMINDER_DATE, null);
197                    String reminderText = dataHolder.getValue(AlertsConstants.REMINDER_TEXT, StringUtils.EMPTY);
198
199                    results.put("reminderDate", reminderDate != null ? DateUtils.zonedDateTimeToString(reminderDate) : StringUtils.EMPTY);
200                    results.put("reminderText", reminderText);
201                }
202
203                reminderAlertStatus = _getUnmodifiedStatusAlertsInformations(reminderAlertStatus, reminderEnabled);
204            }
205        }
206
207        results.put("contentIds", contentIds);
208        results.put("reminderEnabled", reminderAlertStatus == AlertsStatus.MIXED ? null : reminderAlertStatus == AlertsStatus.ENABLED);
209        results.put("unmodifiedAlertEnabled", unmodifiedAlertStatus == AlertsStatus.MIXED ? null : unmodifiedAlertStatus == AlertsStatus.ENABLED);
210
211        results.put("validation-alert-delay", Config.getInstance().getValue("remind.content.validation.delay"));
212        results.put("unmodified-alert-delay", Config.getInstance().getValue("remind.unmodified.content.delay"));
213
214        return results;
215    }
216
217    private AlertsStatus _getUnmodifiedStatusAlertsInformations(AlertsStatus unmodifiedAlertStatus, boolean unmodifiedAlertEnabled)
218    {
219        AlertsStatus localUnmodifiedAlertStatus = unmodifiedAlertStatus;
220        if (localUnmodifiedAlertStatus == null)
221        {
222            // Initialize the alert status with first content
223            localUnmodifiedAlertStatus = unmodifiedAlertEnabled ? AlertsStatus.ENABLED : AlertsStatus.DISABLED;
224        }
225        else if (localUnmodifiedAlertStatus == AlertsStatus.ENABLED && !unmodifiedAlertEnabled
226                || localUnmodifiedAlertStatus == AlertsStatus.DISABLED && unmodifiedAlertEnabled)
227        {
228            // Alert status is different for at least one content
229            localUnmodifiedAlertStatus = AlertsStatus.MIXED;
230        }
231        return localUnmodifiedAlertStatus;
232    }
233
234    /**
235     * Set alerts on content
236     * 
237     * @param contentIds the content's id
238     * @param params the alerts' parameters
239     * @return The result
240     */
241    @Callable(rights = Callable.CHECKED_BY_IMPLEMENTATION)
242    public Map<String, Object> setAlertsOnContent(List<String> contentIds, Map<String, Object> params)
243    {
244        UserIdentity user = _currentUserProvider.getUser();
245        
246        Map<String, Object> result = new HashMap<>();
247        
248        List<String> successContentIds = new ArrayList<>();
249        List<Map<String, Object>> noRightContents = new ArrayList<>();
250        List<Map<String, Object>> errorContents = new ArrayList<>();
251        
252        for (String contentId : contentIds)
253        {
254            Content content = _resolver.resolveById(contentId);
255            if (_rightManager.hasRight(user, "CMS_Rights_Content_Alerts", content) != RightResult.RIGHT_ALLOW)
256            {
257                getLogger().warn("User {} try to edit alerts on content {} without sufficient rights", user, contentId);
258                noRightContents.add(getContentDefaultParameters(content));
259            }
260            else if (content instanceof ModifiableDataAwareVersionableAmetysObject)
261            {
262                try
263                {
264                    _setAlerts((ModifiableDataAwareVersionableAmetysObject) content, params);
265                    successContentIds.add(contentId);
266                }
267                catch (AmetysRepositoryException e)
268                {
269                    getLogger().warn("Failed to define alerts on content {}", contentId, e);
270                    errorContents.add(getContentDefaultParameters(content));
271                }
272            }
273        }
274
275        boolean instantAlertEnabled = (Boolean) params.get("instantAlertEnabled");
276        if (instantAlertEnabled)
277        {
278            // get the schedulable id provided by the caller. Or the CMS alertSchedulable if none.
279            String schedulableId = params.containsKey("schedulable-id") ? (String) params.get("schedulable-id") : AlertSchedulable.SCHEDULABLE_ID;
280
281            String instantAlertText = (String) params.get("instantAlertText");
282            Map<String, Object> jobParams = new HashMap<>();
283            jobParams.put(AlertSchedulable.JOBDATAMAP_INSTANT_MODE_KEY, true);
284            jobParams.put(AlertSchedulable.JOBDATAMAP_CONTENT_IDS_KEY, contentIds);
285            jobParams.put(AlertSchedulable.JOBDATAMAP_MESSAGE_KEY, instantAlertText);
286            
287            List<String> i18nParams = new ArrayList<>();
288            i18nParams.add(StringUtils.join(contentIds, ", "));
289            
290            Runnable runnable = new DefaultRunnable(_generateRunnableId(),
291                    new I18nizableText("plugin.cms", "PLUGINS_CMS_CONTENTS_INSTANT_ALERTS_RUNNABLE_LABEL", i18nParams),
292                    new I18nizableText("plugin.cms", contentIds.size() <= 1 ? "PLUGINS_CMS_CONTENTS_INSTANT_ALERTS_RUNNABLE_DESCRIPTION" : "PLUGINS_CMS_CONTENTS_INSTANT_ALERTS_RUNNABLE_DESCRIPTION_MULTIPLE" , i18nParams),
293                    FireProcess.NOW,
294                    null /* cron*/,
295                    schedulableId,
296                    true /* removable */,
297                    false /* modifiable */,
298                    false /* deactivatable */,
299                    MisfirePolicy.FIRE_ONCE,
300                    true /* isVolatile */,
301                    _currentUserProvider.getUser(),
302                    jobParams
303                );
304            
305            try
306            {
307                _scheduler.scheduleJob(runnable);
308            }
309            catch (SchedulerException e)
310            {
311                getLogger().error("An error occured while trying to schedule the sending of alerts", e);
312            }
313        }
314        
315        result.put("no-right-contents", noRightContents);
316        result.put("success-content-ids", successContentIds);
317        result.put("error-contents", errorContents);
318        
319        return result;
320    }
321
322    private String _generateRunnableId()
323    {
324        return "org.ametys.cms.alerts.InstantAlertRunnable" + "$" + UUID.randomUUID();
325    }
326
327    /**
328     * Sets the alerts on the specified content.
329     * 
330     * @param content the content to set the alerts on.
331     * @param params the alerts' parameters
332     * @throws AmetysRepositoryException if a repository error occurs.
333     */
334    protected void _setAlerts(ModifiableDataAwareVersionableAmetysObject content, Map<String, Object> params) throws AmetysRepositoryException
335    {
336        ModifiableModelLessDataHolder dataHolder = content.getUnversionedDataHolder();
337
338        // Set alert for unmodified contents
339        if (params.get("unmodifiedAlertEnabled") != null)
340        {
341            boolean unmodifiedAlertEnabled = (Boolean) params.get("unmodifiedAlertEnabled");
342            String unmodifiedAlertText = (String) params.get("unmodifiedAlertText");
343
344            dataHolder.setValue(AlertsConstants.UNMODIFIED_ALERT_ENABLED, unmodifiedAlertEnabled);
345            dataHolder.setValue(AlertsConstants.UNMODIFIED_ALERT_TEXT, StringUtils.trimToEmpty(unmodifiedAlertText));
346        }
347
348        // Set reminders
349        if (params.get("reminderEnabled") != null)
350        {
351            boolean reminderEnabled = (Boolean) params.get("reminderEnabled");
352            dataHolder.setValue(AlertsConstants.REMINDER_ENABLED, reminderEnabled);
353
354            String reminderDateStr = (String) params.get("reminderDate");
355            String reminderText = (String) params.get("reminderText");
356            dataHolder.setValue(AlertsConstants.REMINDER_TEXT, StringUtils.trimToEmpty(reminderText));
357
358            // Parse the reminder date.
359            if (StringUtils.isNotBlank(reminderDateStr))
360            {
361                LocalDate localDate = DateUtils.parseLocalDate(reminderDateStr);
362                if (localDate != null)
363                {
364                    ZonedDateTime zonedDateTime = DateUtils.asZonedDateTime(localDate, null);
365                    dataHolder.setValue(AlertsConstants.REMINDER_DATE, zonedDateTime);
366                }
367                else
368                {
369                    getLogger().error("Unable to parse reminder date " + reminderDateStr);
370                }
371            }
372        }
373
374        content.saveChanges();
375    }
376
377}