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 = Callable.SKIP_BUILTIN_CHECK)
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        for (String contentId : contentsId)
104        {
105            Content content = _resolver.resolveById(contentId);
106            
107            if (content instanceof DataAndVersionAwareAmetysObject && content instanceof ModifiableContent)
108            {
109                ModelLessDataHolder dataHolder = ((DataAndVersionAwareAmetysObject) content).getUnversionedDataHolder();
110                
111                boolean reminderEnabled = dataHolder.getValue(AlertsConstants.REMINDER_ENABLED, false);
112                boolean unmodifiedAlertEnabled = dataHolder.getValue(AlertsConstants.UNMODIFIED_ALERT_ENABLED, false);
113                // Alerts enabled/disabled
114                Map<String, Object> contentParams = getContentDefaultParameters (content);
115                
116                if (unmodifiedAlertEnabled && unmodifiedAlertDelay > 0 || reminderEnabled)
117                {
118                    I18nizableText ed = (I18nizableText) this._script.getParameters().get("alerts-enabled-description");
119                    I18nizableText msg = new I18nizableText(ed.getCatalogue(), ed.getKey(), Collections.singletonList(_contentHelper.getTitle(content)));
120
121                    contentParams.put("description", msg);
122
123                    @SuppressWarnings("unchecked")
124                    List<Map<String, Object>> enabledAlerts = (List<Map<String, Object>>) results.get("alert-enabled-contents");
125                    enabledAlerts.add(contentParams);
126                }
127                else
128                {
129                    I18nizableText ed = (I18nizableText) this._script.getParameters().get("alerts-disabled-description");
130                    I18nizableText msg = new I18nizableText(ed.getCatalogue(), ed.getKey(), Collections.singletonList(_contentHelper.getTitle(content)));
131
132                    contentParams.put("description", msg);
133
134                    @SuppressWarnings("unchecked")
135                    List<Map<String, Object>> disabledAlerts = (List<Map<String, Object>>) results.get("alert-disabled-contents");
136                    disabledAlerts.add(contentParams);
137                }
138            }
139        }
140        
141        return results;
142    }
143
144    /**
145     * Get information on reminders state.
146     * 
147     * @param contentsId The list of contents' ids
148     * @return informations on reminders state.
149     */
150    @Callable(rights = Callable.SKIP_BUILTIN_CHECK)
151    public Map<String, Object> getAlertsInformations(List<String> contentsId)
152    {
153        Map<String, Object> results = new HashMap<>();
154
155        results.put("no-right-contents", new ArrayList<>());
156
157        List<String> contentIds = new ArrayList<>();
158        AlertsStatus unmodifiedAlertStatus = null;
159        AlertsStatus reminderAlertStatus = null;
160
161        UserIdentity user = _currentUserProvider.getUser();
162        
163        for (String contentId : contentsId)
164        {
165            Content content = _resolver.resolveById(contentId);
166
167            if (_rightManager.hasRight(user, "CMS_Rights_Content_Alerts", content) != RightResult.RIGHT_ALLOW)
168            {
169                Map<String, Object> contentParams = getContentDefaultParameters(content);
170                @SuppressWarnings("unchecked")
171                List<Map<String, Object>> noRightContents = (List<Map<String, Object>>) results.get("no-right-contents");
172                noRightContents.add(contentParams);
173            }
174            else if (content instanceof DataAndVersionAwareAmetysObject && content instanceof ModifiableContent)
175            {
176                contentIds.add(contentId);
177                
178                ModelLessDataHolder dataHolder = ((DataAndVersionAwareAmetysObject) content).getUnversionedDataHolder();
179                
180                boolean unmodifiedAlertEnabled = dataHolder.getValue(AlertsConstants.UNMODIFIED_ALERT_ENABLED, false);
181                if (unmodifiedAlertEnabled)
182                {
183                    String unmodifiedAlertText = dataHolder.getValue(AlertsConstants.UNMODIFIED_ALERT_TEXT, StringUtils.EMPTY);
184                    results.put("unmodifiedAlertText", unmodifiedAlertText);
185                }
186
187                unmodifiedAlertStatus = _getUnmodifiedStatusAlertsInformations(unmodifiedAlertStatus, unmodifiedAlertEnabled);
188
189                boolean reminderEnabled = dataHolder.getValue(AlertsConstants.REMINDER_ENABLED, false);
190                if (reminderEnabled)
191                {
192                    ZonedDateTime reminderDate = dataHolder.getValue(AlertsConstants.REMINDER_DATE, null);
193                    String reminderText = dataHolder.getValue(AlertsConstants.REMINDER_TEXT, StringUtils.EMPTY);
194
195                    results.put("reminderDate", reminderDate != null ? DateUtils.zonedDateTimeToString(reminderDate) : StringUtils.EMPTY);
196                    results.put("reminderText", reminderText);
197                }
198
199                reminderAlertStatus = _getUnmodifiedStatusAlertsInformations(reminderAlertStatus, reminderEnabled);
200            }
201        }
202
203        results.put("contentIds", contentIds);
204        results.put("reminderEnabled", reminderAlertStatus == AlertsStatus.MIXED ? null : reminderAlertStatus == AlertsStatus.ENABLED);
205        results.put("unmodifiedAlertEnabled", unmodifiedAlertStatus == AlertsStatus.MIXED ? null : unmodifiedAlertStatus == AlertsStatus.ENABLED);
206
207        results.put("validation-alert-delay", Config.getInstance().getValue("remind.content.validation.delay"));
208        results.put("unmodified-alert-delay", Config.getInstance().getValue("remind.unmodified.content.delay"));
209
210        return results;
211    }
212
213    private AlertsStatus _getUnmodifiedStatusAlertsInformations(AlertsStatus unmodifiedAlertStatus, boolean unmodifiedAlertEnabled)
214    {
215        AlertsStatus localUnmodifiedAlertStatus = unmodifiedAlertStatus;
216        if (localUnmodifiedAlertStatus == null)
217        {
218            // Initialize the alert status with first content
219            localUnmodifiedAlertStatus = unmodifiedAlertEnabled ? AlertsStatus.ENABLED : AlertsStatus.DISABLED;
220        }
221        else if (localUnmodifiedAlertStatus == AlertsStatus.ENABLED && !unmodifiedAlertEnabled
222                || localUnmodifiedAlertStatus == AlertsStatus.DISABLED && unmodifiedAlertEnabled)
223        {
224            // Alert status is different for at least one content
225            localUnmodifiedAlertStatus = AlertsStatus.MIXED;
226        }
227        return localUnmodifiedAlertStatus;
228    }
229
230    /**
231     * Set alerts on content
232     * 
233     * @param contentIds the content's id
234     * @param params the alerts' parameters
235     * @return The result
236     */
237    @Callable(rights = Callable.SKIP_BUILTIN_CHECK)
238    public Map<String, Object> setAlertsOnContent(List<String> contentIds, Map<String, Object> params)
239    {
240        UserIdentity user = _currentUserProvider.getUser();
241        
242        Map<String, Object> result = new HashMap<>();
243        
244        List<String> successContentIds = new ArrayList<>();
245        List<Map<String, Object>> noRightContents = new ArrayList<>();
246        List<Map<String, Object>> errorContents = new ArrayList<>();
247        
248        for (String contentId : contentIds)
249        {
250            Content content = _resolver.resolveById(contentId);
251            if (_rightManager.hasRight(user, "CMS_Rights_Content_Alerts", content) != RightResult.RIGHT_ALLOW)
252            {
253                getLogger().warn("User {} try to edit alerts on content {} without sufficient rights", user, contentId);
254                noRightContents.add(getContentDefaultParameters(content));
255            }
256            else if (content instanceof ModifiableDataAwareVersionableAmetysObject)
257            {
258                try
259                {
260                    _setAlerts((ModifiableDataAwareVersionableAmetysObject) content, params);
261                    successContentIds.add(contentId);
262                }
263                catch (AmetysRepositoryException e)
264                {
265                    getLogger().warn("Failed to define alerts on content {}", contentId, e);
266                    errorContents.add(getContentDefaultParameters(content));
267                }
268            }
269        }
270
271        boolean instantAlertEnabled = (Boolean) params.get("instantAlertEnabled");
272        if (instantAlertEnabled)
273        {
274            // get the schedulable id provided by the caller. Or the CMS alertSchedulable if none.
275            String schedulableId = params.containsKey("schedulable-id") ? (String) params.get("schedulable-id") : AlertSchedulable.SCHEDULABLE_ID;
276
277            String instantAlertText = (String) params.get("instantAlertText");
278            Map<String, Object> jobParams = new HashMap<>();
279            jobParams.put(AlertSchedulable.JOBDATAMAP_INSTANT_MODE_KEY, true);
280            jobParams.put(AlertSchedulable.JOBDATAMAP_CONTENT_IDS_KEY, contentIds);
281            jobParams.put(AlertSchedulable.JOBDATAMAP_MESSAGE_KEY, instantAlertText);
282            
283            List<String> i18nParams = new ArrayList<>();
284            i18nParams.add(StringUtils.join(contentIds, ", "));
285            
286            Runnable runnable = new DefaultRunnable(_generateRunnableId(), 
287                    new I18nizableText("plugin.cms", "PLUGINS_CMS_CONTENTS_INSTANT_ALERTS_RUNNABLE_LABEL", i18nParams),
288                    new I18nizableText("plugin.cms", contentIds.size() <= 1 ? "PLUGINS_CMS_CONTENTS_INSTANT_ALERTS_RUNNABLE_DESCRIPTION" : "PLUGINS_CMS_CONTENTS_INSTANT_ALERTS_RUNNABLE_DESCRIPTION_MULTIPLE" , i18nParams),
289                    FireProcess.NOW,
290                    null /* cron*/, 
291                    schedulableId, 
292                    true /* removable */, 
293                    false /* modifiable */, 
294                    false /* deactivatable */, 
295                    MisfirePolicy.FIRE_ONCE, 
296                    true /* isVolatile */, 
297                    _currentUserProvider.getUser(), 
298                    jobParams
299                );
300            
301            try
302            {
303                _scheduler.scheduleJob(runnable);
304            }
305            catch (SchedulerException e)
306            {
307                getLogger().error("An error occured while trying to schedule the sending of alerts", e);
308            }
309        }
310        
311        result.put("no-right-contents", noRightContents);
312        result.put("success-content-ids", successContentIds);
313        result.put("error-contents", errorContents);
314        
315        return result;
316    }
317
318    private String _generateRunnableId()
319    {
320        return "org.ametys.cms.alerts.InstantAlertRunnable" + "$" + UUID.randomUUID();
321    }
322
323    /**
324     * Sets the alerts on the specified content.
325     * 
326     * @param content the content to set the alerts on.
327     * @param params the alerts' parameters
328     * @throws AmetysRepositoryException if a repository error occurs.
329     */
330    protected void _setAlerts(ModifiableDataAwareVersionableAmetysObject content, Map<String, Object> params) throws AmetysRepositoryException
331    {
332        ModifiableModelLessDataHolder dataHolder = content.getUnversionedDataHolder();
333
334        // Set alert for unmodified contents
335        if (params.get("unmodifiedAlertEnabled") != null)
336        {
337            boolean unmodifiedAlertEnabled = (Boolean) params.get("unmodifiedAlertEnabled");
338            String unmodifiedAlertText = (String) params.get("unmodifiedAlertText");
339
340            dataHolder.setValue(AlertsConstants.UNMODIFIED_ALERT_ENABLED, unmodifiedAlertEnabled);
341            dataHolder.setValue(AlertsConstants.UNMODIFIED_ALERT_TEXT, StringUtils.trimToEmpty(unmodifiedAlertText));
342        }
343
344        // Set reminders
345        if (params.get("reminderEnabled") != null)
346        {
347            boolean reminderEnabled = (Boolean) params.get("reminderEnabled");
348            dataHolder.setValue(AlertsConstants.REMINDER_ENABLED, reminderEnabled);
349
350            String reminderDateStr = (String) params.get("reminderDate");
351            String reminderText = (String) params.get("reminderText");
352            dataHolder.setValue(AlertsConstants.REMINDER_TEXT, StringUtils.trimToEmpty(reminderText));
353
354            // Parse the reminder date.
355            if (StringUtils.isNotBlank(reminderDateStr))
356            {
357                LocalDate localDate = DateUtils.parseLocalDate(reminderDateStr);
358                if (localDate != null)
359                {
360                    ZonedDateTime zonedDateTime = DateUtils.asZonedDateTime(localDate, null);
361                    dataHolder.setValue(AlertsConstants.REMINDER_DATE, zonedDateTime);
362                }
363                else
364                {
365                    getLogger().error("Unable to parse reminder date " + reminderDateStr);
366                }
367            }
368        }
369
370        content.saveChanges();
371    }
372
373}