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