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;
025
026import org.apache.avalon.framework.service.ServiceException;
027import org.apache.avalon.framework.service.ServiceManager;
028import org.apache.commons.lang3.StringUtils;
029
030import org.ametys.cms.content.ContentHelper;
031import org.ametys.cms.lock.LockContentManager;
032import org.ametys.cms.repository.Content;
033import org.ametys.cms.repository.ModifiableContent;
034import org.ametys.core.ui.Callable;
035import org.ametys.core.ui.StaticClientSideElement;
036import org.ametys.core.util.DateUtils;
037import org.ametys.plugins.repository.AmetysObjectResolver;
038import org.ametys.plugins.repository.AmetysRepositoryException;
039import org.ametys.plugins.repository.data.holder.ModelLessDataHolder;
040import org.ametys.plugins.repository.data.holder.ModifiableModelLessDataHolder;
041import org.ametys.plugins.repository.lock.LockAwareAmetysObject;
042import org.ametys.plugins.repository.version.DataAndVersionAwareAmetysObject;
043import org.ametys.plugins.repository.version.ModifiableDataAwareVersionableAmetysObject;
044import org.ametys.runtime.config.Config;
045import org.ametys.runtime.i18n.I18nizableText;
046
047/**
048 * This element creates a toggle button representing the reminders state.
049 */
050public class ContentAlertsClientSideElement extends StaticClientSideElement
051{
052    /** Repository content. */
053    protected AmetysObjectResolver _resolver;
054
055    /** The lock manager. */
056    protected LockContentManager _lockManager;
057
058    /** The service manager */
059    protected ServiceManager _smanager;
060    
061    /** The content helper */
062    protected ContentHelper _contentHelper;
063
064    @Override
065    public void service(ServiceManager serviceManager) throws ServiceException
066    {
067        super.service(serviceManager);
068        _smanager = serviceManager;
069        _resolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE);
070        _lockManager = (LockContentManager) serviceManager.lookup(LockContentManager.ROLE);
071        _contentHelper = (ContentHelper) serviceManager.lookup(ContentHelper.ROLE);
072    }
073
074    enum AlertsStatus 
075    {
076        /** All the alerts are enabled */
077        ENABLED,
078
079        /** All the alerts are disabled */
080        DISABLED,
081
082        /** Some alerts are enabled, others are disabled */
083        MIXED
084    }
085
086    /**
087     * Get information on reminders state.
088     * 
089     * @param contentsId The list of contents' ids
090     * @return informations on reminders state.
091     */
092    @Callable
093    public Map<String, Object> getAlertsInformations(List<String> contentsId)
094    {
095        Map<String, Object> results = new HashMap<>();
096
097        Long validationAlertDelay = Config.getInstance().getValue("remind.content.validation.delay");
098        Long unmodifiedAlertDelay = Config.getInstance().getValue("remind.unmodified.content.delay");
099
100        results.put("alert-enabled-contents", new ArrayList<Map<String, Object>>());
101        results.put("alert-disabled-contents", new ArrayList<Map<String, Object>>());
102        results.put("all-right-contents", new ArrayList<Map<String, Object>>());
103        results.put("not-unlockable-contents", new ArrayList<Map<String, Object>>());
104
105        AlertsStatus unmodifiedAlertStatus = null;
106        AlertsStatus reminderAlertStatus = null;
107
108        for (String contentId : contentsId)
109        {
110            Content content = _resolver.resolveById(contentId);
111
112            if (content instanceof DataAndVersionAwareAmetysObject && content instanceof ModifiableContent)
113            {
114                Map<String, Object> contentParams = getContentDefaultParameters(content);
115
116                // Test if the content is locked and cannot be unlocked by the
117                // current user.
118                boolean isValid = !isUnlockable(content, results);
119
120                ModelLessDataHolder dataHolder = ((DataAndVersionAwareAmetysObject) content).getUnversionedDataHolder();
121                
122                boolean unmodifiedAlertEnabled = dataHolder.getValue(AlertsConstants.UNMODIFIED_ALERT_ENABLED, false);
123                if (unmodifiedAlertEnabled)
124                {
125                    String unmodifiedAlertText = dataHolder.getValue(AlertsConstants.UNMODIFIED_ALERT_TEXT, StringUtils.EMPTY);
126                    results.put("unmodifiedAlertText", unmodifiedAlertText);
127                }
128
129                unmodifiedAlertStatus = _getUnmodifiedStatusAlertsInformations(unmodifiedAlertStatus, unmodifiedAlertEnabled);
130
131                boolean reminderEnabled = dataHolder.getValue(AlertsConstants.REMINDER_ENABLED, false);
132                if (reminderEnabled)
133                {
134                    ZonedDateTime reminderDate = dataHolder.getValue(AlertsConstants.REMINDER_DATE, null);
135                    String reminderText = dataHolder.getValue(AlertsConstants.REMINDER_TEXT, StringUtils.EMPTY);
136
137                    results.put("reminderDate", reminderDate != null ? DateUtils.zonedDateTimeToString(reminderDate) : StringUtils.EMPTY);
138                    results.put("reminderText", reminderText);
139                }
140
141                reminderAlertStatus = _getUnmodifiedStatusAlertsInformations(reminderAlertStatus, reminderEnabled);
142
143                // Alerts enabled/disabled
144                _getEnabledDisabledAlertsInformations(results, unmodifiedAlertDelay, content, contentParams, unmodifiedAlertEnabled, reminderEnabled);
145
146                // All right
147                if (isValid)
148                {
149                    @SuppressWarnings("unchecked")
150                    List<Map<String, Object>> validContents = (List<Map<String, Object>>) results.get("all-right-contents");
151                    validContents.add(contentParams);
152                }
153            }
154        }
155
156        results.put("reminderEnabled", reminderAlertStatus == AlertsStatus.MIXED ? null : (reminderAlertStatus == AlertsStatus.ENABLED ? true : false));
157        results.put("unmodifiedAlertEnabled", unmodifiedAlertStatus == AlertsStatus.MIXED ? null : (unmodifiedAlertStatus == AlertsStatus.ENABLED ? true : false));
158
159        results.put("validation-alert-delay", validationAlertDelay);
160        results.put("unmodified-alert-delay", unmodifiedAlertDelay);
161
162        return results;
163    }
164
165    private AlertsStatus _getUnmodifiedStatusAlertsInformations(AlertsStatus unmodifiedAlertStatus, boolean unmodifiedAlertEnabled)
166    {
167        AlertsStatus localUnmodifiedAlertStatus = unmodifiedAlertStatus;
168        if (localUnmodifiedAlertStatus == null)
169        {
170            // Initialize the alert status with first content
171            localUnmodifiedAlertStatus = unmodifiedAlertEnabled ? AlertsStatus.ENABLED : AlertsStatus.DISABLED;
172        }
173        else if (localUnmodifiedAlertStatus == AlertsStatus.ENABLED && !unmodifiedAlertEnabled
174                || localUnmodifiedAlertStatus == AlertsStatus.DISABLED && unmodifiedAlertEnabled)
175        {
176            // Alert status is different for at least one content
177            localUnmodifiedAlertStatus = AlertsStatus.MIXED;
178        }
179        return localUnmodifiedAlertStatus;
180    }
181
182    private void _getEnabledDisabledAlertsInformations(Map<String, Object> results, Long unmodifiedAlertDelay, Content content, Map<String, Object> contentParams,
183            boolean unmodifiedAlertEnabled, boolean reminderEnabled)
184    {
185        if (unmodifiedAlertEnabled && unmodifiedAlertDelay > 0 || reminderEnabled)
186        {
187            I18nizableText ed = (I18nizableText) this._script.getParameters().get("alerts-enabled-description");
188            I18nizableText msg = new I18nizableText(ed.getCatalogue(), ed.getKey(), Collections.singletonList(_contentHelper.getTitle(content)));
189
190            contentParams.put("description", msg);
191
192            @SuppressWarnings("unchecked")
193            List<Map<String, Object>> enabledAlerts = (List<Map<String, Object>>) results.get("alert-enabled-contents");
194            enabledAlerts.add(contentParams);
195        }
196        else
197        {
198            I18nizableText ed = (I18nizableText) this._script.getParameters().get("alerts-disabled-description");
199            I18nizableText msg = new I18nizableText(ed.getCatalogue(), ed.getKey(), Collections.singletonList(_contentHelper.getTitle(content)));
200
201            contentParams.put("description", msg);
202
203            @SuppressWarnings("unchecked")
204            List<Map<String, Object>> disabledAlerts = (List<Map<String, Object>>) results.get("alert-disabled-contents");
205            disabledAlerts.add(contentParams);
206        }
207    }
208
209    /**
210     * Test if the content is locked and cannot be unlocked by the current user.
211     * <br>
212     * If this is so, fill the "not-unlockable" list in the result map.
213     * 
214     * @param content the content to test.
215     * @param results the result map.
216     * @return true if the content is locked and cannot be unlocked by the
217     *         current user, false otherwise.
218     */
219    @SuppressWarnings("unchecked")
220    protected boolean isUnlockable(Content content, Map<String, Object> results)
221    {
222        boolean isUnlockable = false;
223
224        if (content instanceof LockAwareAmetysObject)
225        {
226            LockAwareAmetysObject lockAwareContent = (LockAwareAmetysObject) content;
227
228            if (lockAwareContent.isLocked() && !_lockManager.canUnlock(lockAwareContent))
229            {
230                Map<String, Object> contentParams = getContentDefaultParameters(content);
231
232                I18nizableText ed = (I18nizableText) this._script.getParameters().get("not-unlockable-description");
233                I18nizableText msg = new I18nizableText(ed.getCatalogue(), ed.getKey(), Collections.singletonList(_contentHelper.getTitle(content)));
234
235                contentParams.put("description", msg);
236
237                List<Map<String, Object>> notUnlockable = (List<Map<String, Object>>) results.get("not-unlockable-contents");
238                notUnlockable.add(contentParams);
239
240                isUnlockable = true;
241            }
242        }
243
244        return isUnlockable;
245    }
246
247    /**
248     * Get the default content's parameters
249     * 
250     * @param content The content
251     * @return The default parameters
252     */
253    protected Map<String, Object> getContentDefaultParameters(Content content)
254    {
255        Map<String, Object> contentParams = new HashMap<>();
256        contentParams.put("id", content.getId());
257        contentParams.put("title", _contentHelper.getTitle(content));
258
259        return contentParams;
260    }
261
262    /**
263     * Set alerts on content
264     * 
265     * @param contentIds the content's id
266     * @param params the alerts' parameters
267     * @throws ServiceException if an error occurred while retrieving the alert
268     *             scheduler component
269     */
270    @Callable
271    public void setAlertsOnContent(List<String> contentIds, Map<String, Object> params) throws ServiceException
272    {
273        for (String contentId : contentIds)
274        {
275            Content content = _resolver.resolveById(contentId);
276            if (content instanceof ModifiableDataAwareVersionableAmetysObject)
277            {
278                _setAlerts((ModifiableDataAwareVersionableAmetysObject) content, params);
279            }
280        }
281
282        String role = params.containsKey("role") ? (String) params.get("role") : AlertScheduler.ROLE;
283        AlertScheduler alertScheduler = (AlertScheduler) _smanager.lookup(role);
284
285        boolean instantAlertEnabled = (Boolean) params.get("instantAlertEnabled");
286        if (instantAlertEnabled)
287        {
288            String instantAlertText = (String) params.get("instantAlertText");
289            alertScheduler.sendInstantAlerts(contentIds, org.apache.commons.lang3.StringUtils.trimToEmpty(instantAlertText));
290        }
291    }
292
293    /**
294     * Sets the alerts on the specified content.
295     * 
296     * @param content the content to set the alerts on.
297     * @param params the alerts' parameters
298     * @throws AmetysRepositoryException if a repository error occurs.
299     */
300    protected void _setAlerts(ModifiableDataAwareVersionableAmetysObject content, Map<String, Object> params) throws AmetysRepositoryException
301    {
302        ModifiableModelLessDataHolder dataHolder = content.getUnversionedDataHolder();
303
304        // Set alert for unmodified contents
305        if (params.get("unmodifiedAlertEnabled") != null)
306        {
307            boolean unmodifiedAlertEnabled = (Boolean) params.get("unmodifiedAlertEnabled");
308            String unmodifiedAlertText = (String) params.get("unmodifiedAlertText");
309
310            dataHolder.setValue(AlertsConstants.UNMODIFIED_ALERT_ENABLED, unmodifiedAlertEnabled);
311            dataHolder.setValue(AlertsConstants.UNMODIFIED_ALERT_TEXT, StringUtils.trimToEmpty(unmodifiedAlertText));
312        }
313
314        // Set reminders
315        if (params.get("reminderEnabled") != null)
316        {
317            boolean reminderEnabled = (Boolean) params.get("reminderEnabled");
318            dataHolder.setValue(AlertsConstants.REMINDER_ENABLED, reminderEnabled);
319
320            String reminderDateStr = (String) params.get("reminderDate");
321            String reminderText = (String) params.get("reminderText");
322            dataHolder.setValue(AlertsConstants.REMINDER_TEXT, StringUtils.trimToEmpty(reminderText));
323
324            // Parse the reminder date.
325            if (StringUtils.isNotBlank(reminderDateStr))
326            {
327                LocalDate localDate = DateUtils.parseLocalDate(reminderDateStr);
328                if (localDate != null)
329                {
330                    ZonedDateTime zonedDateTime = DateUtils.asZonedDateTime(localDate, null);
331                    dataHolder.setValue(AlertsConstants.REMINDER_DATE, zonedDateTime);
332                }
333                else
334                {
335                    getLogger().error("Unable to parse reminder date " + reminderDateStr);
336                }
337            }
338        }
339
340        content.saveChanges();
341    }
342
343    @Override
344    public List<Script> getScripts(boolean ignoreRights, Map<String, Object> contextParameters)
345    {
346        boolean enabled = Config.getInstance().getValue("remind.content.enabled");
347        if (enabled)
348        {
349            return super.getScripts(ignoreRights, contextParameters);
350        }
351
352        return new ArrayList<>();
353    }
354}