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}