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}