001/* 002 * Copyright 2023 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.plugins.pagesubscription; 017 018import java.text.ParseException; 019import java.text.SimpleDateFormat; 020import java.time.DayOfWeek; 021import java.time.ZonedDateTime; 022import java.time.temporal.ChronoUnit; 023import java.time.temporal.TemporalAdjusters; 024import java.util.ArrayList; 025import java.util.HashMap; 026import java.util.List; 027import java.util.Map; 028 029import org.apache.avalon.framework.activity.Initializable; 030import org.apache.avalon.framework.component.Component; 031import org.apache.avalon.framework.logger.LogEnabled; 032import org.apache.avalon.framework.logger.Logger; 033import org.apache.avalon.framework.service.ServiceException; 034import org.apache.avalon.framework.service.ServiceManager; 035import org.apache.avalon.framework.service.Serviceable; 036import org.apache.cocoon.xml.AttributesImpl; 037import org.apache.cocoon.xml.XMLUtils; 038import org.apache.commons.lang3.StringUtils; 039import org.quartz.CronScheduleBuilder; 040import org.quartz.CronTrigger; 041import org.xml.sax.ContentHandler; 042import org.xml.sax.SAXException; 043 044import org.ametys.core.util.DateUtils; 045import org.ametys.core.util.I18nUtils; 046import org.ametys.plugins.pagesubscription.BroadcastChannelHelper.BroadcastChannel; 047import org.ametys.plugins.pagesubscription.type.SubscriptionType.FrequencyTiming; 048import org.ametys.plugins.repository.query.expression.DateExpression; 049import org.ametys.plugins.repository.query.expression.Expression; 050import org.ametys.plugins.repository.query.expression.Expression.Operator; 051import org.ametys.runtime.config.Config; 052import org.ametys.runtime.i18n.I18nizableText; 053import org.ametys.runtime.i18n.I18nizableTextParameter; 054import org.ametys.runtime.model.ElementDefinition; 055 056/** 057 * Helper for frequency 058 */ 059public class FrequencyHelper implements Component, Serviceable, Initializable, LogEnabled 060{ 061 /** The i18n utils */ 062 protected static I18nUtils _i18nUtils; 063 064 private static long _frequencyDay; 065 private static String _frequencyTime; 066 067 private static Logger _logger; 068 069 /** 070 * The frequency type. 071 */ 072 public enum Frequency 073 { 074 /** Now */ 075 INSTANT, 076 /** Every day */ 077 DAILY, 078 /** Every week */ 079 WEEKLY, 080 /** Every month */ 081 MONTHLY; 082 } 083 084 public void enableLogging(Logger logger) 085 { 086 _logger = logger; 087 } 088 089 public void service(ServiceManager manager) throws ServiceException 090 { 091 _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE); 092 } 093 094 public void initialize() throws Exception 095 { 096 _frequencyDay = Config.getInstance().getValue("page-subscription.frequency.day", true, 1L); 097 _frequencyTime = Config.getInstance().getValue("page-subscription.frequency.hour", true, "10:00"); 098 } 099 100 /** 101 * Get the default day of frequency (weekly or monthly) 102 * @return the default day of frequency 103 */ 104 public static long getDefaultFrequencyDay() 105 { 106 return _frequencyDay; 107 } 108 109 /** 110 * Get the default time of frequency (daily, weekly or monthly) 111 * @return the default time of frequency 112 */ 113 public static String getDefaultFrequencyTime() 114 { 115 return _frequencyTime; 116 } 117 118 private static I18nizableText _getEntry(long day) 119 { 120 try 121 { 122 ElementDefinition def = (ElementDefinition) Config.getModel().getChild("page-subscription.frequency.day"); 123 return def.getEnumerator().getEntry(day); 124 } 125 catch (Exception e) 126 { 127 _logger.error("An error occurred getting day from value '" + day + "'", e); 128 } 129 130 return null; 131 } 132 133 /** 134 * SAX frequencies 135 * @param contentHandler the content handler to sax intos 136 * @throws SAXException if an error occured while saxing 137 */ 138 public static void saxFrequencies(ContentHandler contentHandler) throws SAXException 139 { 140 XMLUtils.startElement(contentHandler, "frequencies"); 141 for (Frequency frequency : Frequency.values()) 142 { 143 if (frequency != Frequency.INSTANT) 144 { 145 AttributesImpl attrs = new AttributesImpl(); 146 attrs.addCDATAAttribute("name", frequency.name()); 147 XMLUtils.startElement(contentHandler, "frequency", attrs); 148 getLabel(frequency).toSAX(contentHandler, "label"); 149 getSmartLabel(frequency).toSAX(contentHandler, "smartLabel"); 150 XMLUtils.endElement(contentHandler, "frequency"); 151 } 152 } 153 XMLUtils.endElement(contentHandler, "frequencies"); 154 } 155 /** 156 * Get the label for the given frequency 157 * @param frequency the frequency 158 * @return the label 159 */ 160 public static I18nizableText getLabel(Frequency frequency) 161 { 162 return new I18nizableText("plugin.page-subscription", "PLUGINS_PAGE_SUBSCRIBE_USER_SUBSCRIPTIONS_FREQUENCY_LABEL_" + frequency.name()); 163 } 164 165 /** 166 * Get the smart label with day and hour of the given frequency 167 * @param frequency the frequency 168 * @return the smart label 169 */ 170 public static I18nizableText getSmartLabel(Frequency frequency) 171 { 172 return getSmartLabel(frequency, null); 173 } 174 175 /** 176 * Get the smart label with day and hour of the given frequency 177 * @param frequency the frequency 178 * @param timing the frequency timing. Can be null to get the default config param 179 * @return the smart label 180 */ 181 public static I18nizableText getSmartLabel(Frequency frequency, FrequencyTiming timing) 182 { 183 Map<String, I18nizableTextParameter> params = new HashMap<>(); 184 I18nizableText hourI18n = timing != null && timing.time() != null 185 ? new I18nizableText(_getHour(timing.time())) 186 : new I18nizableText(_getHour(_frequencyTime)); 187 switch (frequency) 188 { 189 case DAILY: 190 params.put("hour", hourI18n); 191 break; 192 case WEEKLY: 193 case MONTHLY: 194 I18nizableText dayI18n = timing != null && timing.day() != null 195 ? _getEntry(timing.day()) 196 : _getEntry(_frequencyDay); 197 if (dayI18n != null) 198 { 199 params.put("day", dayI18n); 200 } 201 params.put("hour", hourI18n); 202 break; 203 default: // Do nothing .. 204 } 205 return new I18nizableText("plugin.page-subscription", "PLUGINS_PAGE_SUBSCRIBE_USER_SUBSCRIPTIONS_FREQUENCY_SMART_LABEL_" + frequency.name(), params); 206 } 207 208 /** 209 * Get full label with frequency and broadcast channel 210 * @param frequency the frequency 211 * @param channels the broadcast channels 212 * @return the frequency and broadcast channel label 213 */ 214 public static I18nizableText getFullLabel(Frequency frequency, List<BroadcastChannel> channels) 215 { 216 return getFullLabel(frequency, channels, null); 217 } 218 219 /** 220 * Get full label with frequency and broadcast channel 221 * @param frequency the frequency 222 * @param channels the broadcast channels 223 * @param timing the frequency timing of the full label. Can be null to get the default config param 224 * @return the frequency and broadcast channel label 225 */ 226 public static I18nizableText getFullLabel(Frequency frequency, List<BroadcastChannel> channels, FrequencyTiming timing) 227 { 228 Map<String, I18nizableTextParameter> params = new HashMap<>(); 229 I18nizableText dayI18n = timing != null && timing.day() != null 230 ? _getEntry(timing.day()) 231 : _getEntry(_frequencyDay); 232 if (dayI18n != null) 233 { 234 params.put("day", dayI18n); 235 } 236 I18nizableText broadcastChannelKey = new I18nizableText("plugin.page-subscription", _getBroadcastChannelKey(channels)); 237 params.put("broadcastChannel", broadcastChannelKey); 238 239 return new I18nizableText("plugin.page-subscription", "PLUGINS_PAGE_SUBSCRIBE_USER_SUBSCRIPTIONS_FREQUENCY_" + frequency.name() + "_AND_BROADCAST_CHANNEL_LABEL", params); 240 } 241 242 private static String _getBroadcastChannelKey(List<BroadcastChannel> channels) 243 { 244 if (channels.contains(BroadcastChannel.MAIL) && channels.contains(BroadcastChannel.SITE)) 245 { 246 return "PLUGINS_PAGE_SUBSCRIBE_USER_SUBSCRIPTIONS_BROADCAST_CHANNEL_MSG_SITE_AND_MAIL"; 247 } 248 else if (channels.contains(BroadcastChannel.SITE)) 249 { 250 return "PLUGINS_PAGE_SUBSCRIBE_USER_SUBSCRIPTIONS_BROADCAST_CHANNEL_MSG_SITE"; 251 } 252 else if (channels.contains(BroadcastChannel.MAIL)) 253 { 254 return "PLUGINS_PAGE_SUBSCRIBE_USER_SUBSCRIPTIONS_BROADCAST_CHANNEL_MSG_MAIL"; 255 } 256 257 return "PLUGINS_PAGE_SUBSCRIBE_USER_SUBSCRIPTIONS_BROADCAST_CHANNEL_MSG_NONE"; 258 } 259 260 private static String _getHour(String time) 261 { 262 String stringFormat = _i18nUtils.translate(new I18nizableText("plugin.page-subscription", "PLUGINS_PAGE_SUBSCRIBE_USER_SUBSCRIPTIONS_FREQUENCY_HOUR_FORMAT")); 263 264 SimpleDateFormat defaultFormat = new SimpleDateFormat("HH:mm"); 265 SimpleDateFormat simpleDateFormat = new SimpleDateFormat(stringFormat); 266 try 267 { 268 return simpleDateFormat.format(defaultFormat.parse(time)); 269 } 270 catch (ParseException e) 271 { 272 return time; 273 } 274 } 275 276 /** 277 * Get the frequency timing of the subscription 278 * @param subscription the subscription 279 * @return the frequency timing 280 */ 281 public static FrequencyTiming getTiming(Subscription subscription) 282 { 283 FrequencyTiming forceFrequencyTiming = subscription.getForceFrequencyTiming(); 284 long day = forceFrequencyTiming.day() != null ? forceFrequencyTiming.day() : getDefaultFrequencyDay(); 285 String time = StringUtils.isNotBlank(forceFrequencyTiming.time()) ? forceFrequencyTiming.time() : getDefaultFrequencyTime(); 286 287 return new FrequencyTiming(day, time); 288 } 289 290 /** 291 * Get cron from frequency and timing 292 * @param frequency the frequency 293 * @param frequencyTiming the timing 294 * @return the cron expression 295 */ 296 public static String getCron(Frequency frequency, FrequencyTiming frequencyTiming) 297 { 298 int minutes = (int) frequencyTiming.minutes(); 299 int hour = (int) frequencyTiming.hour(); 300 301 switch (frequency) 302 { 303 case DAILY: 304 { 305 CronScheduleBuilder builder = CronScheduleBuilder.dailyAtHourAndMinute(hour, minutes); 306 CronTrigger trigger = (CronTrigger) builder.build(); 307 return trigger.getCronExpression(); 308 } 309 case WEEKLY: 310 { 311 CronScheduleBuilder builder = CronScheduleBuilder.weeklyOnDayAndHourAndMinute(_getDay(frequencyTiming), hour, minutes); 312 CronTrigger trigger = (CronTrigger) builder.build(); 313 return trigger.getCronExpression(); 314 } 315 case MONTHLY: 316 { 317 return "0 " + minutes + " " + hour + " ? * " + _getDay(frequencyTiming) + "#1"; 318 } 319 default: 320 throw new IllegalArgumentException("Can create subscription schedulable with the given frequency: " + frequency); 321 } 322 } 323 324 private static int _getDay(FrequencyTiming frequencyTiming) 325 { 326 // Mapping Ametys day API to Quartz day API 327 // Monday 1 => 2 328 // Tuesday 2 => 3 329 // Wednesday 3 => 4 330 // Thursday 4 => 5 331 // Friday 5 => 6 332 // Saturday 6 => 7 333 // Sunday 7 => 1 334 int day = frequencyTiming.day().intValue(); 335 return day == 7 336 ? 1 337 : day + 1; 338 } 339 340 /** 341 * Get the date expressions for a given frequency to get activities before the notification date 342 * @param frequency the frequency 343 * @param notificationDate the notification date 344 * @return date expressions 345 */ 346 public static List<Expression> getDateExpressions(Frequency frequency, ZonedDateTime notificationDate) 347 { 348 List<Expression> dateExprs = new ArrayList<>(); 349 switch (frequency) 350 { 351 case INSTANT: 352 { 353 // For this frequency, get all news since the last past 6 months 354 // FIXME ca sort d'où ces 6 mois ?? 355 ZonedDateTime startDate = notificationDate.minusMonths(6); 356 dateExprs.add(new DateExpression("date", Operator.GT, DateUtils.asDate(startDate))); 357 break; 358 } 359 case DAILY: 360 { 361 // Frequency is each day at HH:mm 362 // => get contents updated 363 // - between yesterday-1 at HH:mm and yesterday at HH:mm if current time is before HH:mm 364 // - or between yesterday at HH:mm and today at HH:mm if current time is after HH:mm 365 ZonedDateTime startDate = notificationDate.minusDays(1); 366 367 dateExprs.add(new DateExpression("date", Operator.GT, DateUtils.asDate(startDate))); 368 dateExprs.add(new DateExpression("date", Operator.LT, DateUtils.asDate(notificationDate))); 369 break; 370 } 371 case WEEKLY: 372 { 373 // Frequency is each D day the week at HH:mm 374 // => get contents updated 375 // - between D-14 at HH:mm and D-7 at HH:mm if current time is before D day at HH:mm 376 // - or between D-7 day at HH:mm and D day at HH:mm if current time is after D day at HH:mm 377 ZonedDateTime startDate = notificationDate.minusDays(7); 378 379 dateExprs.add(new DateExpression("date", Operator.GT, DateUtils.asDate(startDate))); 380 dateExprs.add(new DateExpression("date", Operator.LT, DateUtils.asDate(notificationDate))); 381 break; 382 } 383 case MONTHLY: 384 { 385 // Frequency is each 1st D day the month M at HH:mm 386 // => get contents updated 387 // - between 1st D day of the month M-2 at HH:mm and 1st D day of the month M-1 if current time is before 1st D day of the month at HH:mm 388 // - or between 1st D day of the month M-1 at HH:mm and 1st D day of the month M at HH:mm if current time is after 1st D day of the month at HH:mm 389 ZonedDateTime startDate = notificationDate.minusMonths(1); 390 391 dateExprs.add(new DateExpression("date", Operator.GT, DateUtils.asDate(startDate))); 392 dateExprs.add(new DateExpression("date", Operator.LT, DateUtils.asDate(notificationDate))); 393 break; 394 } 395 default: 396 } 397 398 return dateExprs; 399 } 400 401 /** 402 * Get the notification date for a given frequency and timing 403 * @param frequency the frequency 404 * @param timing the timing 405 * @return the notification date 406 */ 407 public static ZonedDateTime getNotificationDate(Frequency frequency, FrequencyTiming timing) 408 { 409 ZonedDateTime now = ZonedDateTime.now(); 410 411 long day = timing.day(); 412 int hour = (int) timing.hour(); 413 int minute = (int) timing.minutes(); 414 415 switch (frequency) 416 { 417 case INSTANT: 418 { 419 // Return the today date 420 return now.truncatedTo(ChronoUnit.DAYS); 421 } 422 case DAILY: 423 { 424 // Frequency is each day at HH:mm 425 // => get notification date 426 // - notification date is yesterday at HH:mm if current time is before HH:mm 427 // - or notification date is today at HH:mm if current time is after HH:mm 428 return _isCurrentDayBeforeDailyTime(now, hour, minute) 429 ? now.minusDays(1).truncatedTo(ChronoUnit.DAYS).withHour(hour).withMinute(minute) 430 : now.truncatedTo(ChronoUnit.DAYS).withHour(hour).withMinute(minute); 431 } 432 case WEEKLY: 433 { 434 // Frequency is each D day the week at HH:mm 435 // => get notification date 436 // - notification date is D-7 at HH:mm if current time is before D day at HH:mm 437 // - or notification date is D day at HH:mm if current time is after D day at HH:mm 438 ZonedDateTime dayOfWeek = now.with(TemporalAdjusters.previousOrSame(DayOfWeek.of((int) day))); // get previous or same day of the week 439 return dayOfWeek.getDayOfMonth() == now.getDayOfMonth() && _isCurrentDayBeforeDailyTime(now, hour, minute) 440 ? dayOfWeek.minusDays(7).truncatedTo(ChronoUnit.DAYS).withHour(hour).withMinute(minute) 441 : dayOfWeek.truncatedTo(ChronoUnit.DAYS).withHour(hour).withMinute(minute); 442 } 443 case MONTHLY: 444 { 445 // Frequency is each 1st D day the month M at HH:mm 446 // => get notification date 447 // - notification date is the 1st D day of the month M-1 if current time is before 1st D day of the month at HH:mm 448 // - or notification date is the 1st D day of the month M at HH:mm if current time is after 1st D day of the month at HH:mm 449 ZonedDateTime dayOfMonth = now.with(TemporalAdjusters.firstInMonth(DayOfWeek.of((int) day))); // get first day of the month 450 return now.getDayOfMonth() < dayOfMonth.getDayOfMonth() || now.getDayOfMonth() == day && _isCurrentDayBeforeDailyTime(now, hour, minute) 451 ? dayOfMonth.minusMonths(1).truncatedTo(ChronoUnit.DAYS).withHour(hour).withMinute(minute) 452 : dayOfMonth.truncatedTo(ChronoUnit.DAYS).withHour(hour).withMinute(minute); 453 } 454 default: 455 } 456 457 return null; 458 } 459 460 private static boolean _isCurrentDayBeforeDailyTime(ZonedDateTime now, int hour, int minute) 461 { 462 return now.getHour() < hour // is before if the current hour is before the given time hour 463 || now.getHour() == hour && now.getMinute() < minute; // or is before if the current hour is equal to the given time hour and the current minute is before the given time minute 464 } 465}