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