package com.xfestudio.xfeservermanager.core.trigger;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.WeekFields;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
/** Deterministic wall-clock values made available to every trigger execution. */
public final class TriggerTimeContext {
public static final String DEFAULT_PATTERN = "uuuu-MM-dd HH:mm:ss";
public static final String FORMAT_HINT = "推荐 uuuu-MM-dd HH:mm:ss;格式区分大小写:MM 是月、mm 是分、"
+ "HH 是 24 小时。yyyy 也可表示年号年;不要使用周历年 YYYY 或年内日 DD。 / "
+ "Use Java time patterns; uuuu-MM-dd HH:mm:ss is recommended. Patterns are case-sensitive: "
+ "MM is month, mm is minute, and HH is the 24-hour clock. yyyy is also accepted for the "
+ "year-of-era; YYYY is week-based, and DD is day-of-year.";
private static final int MAX_PATTERN_LENGTH = 64;
private static final int MAX_FORMATTED_LENGTH = 256;
private TriggerTimeContext() { }
/** Creates the immutable, server-local clock vocabulary for one captured instant. */
public static Map<String, Object> values(Instant instant, ZoneId zone) {
Objects.requireNonNull(instant, "instant");
Objects.requireNonNull(zone, "zone");
ZonedDateTime time = instant.atZone(zone);
WeekFields weeks = WeekFields.ISO;
Map<String, Object> values = new LinkedHashMap<>();
values.put("server.time", time.format(DateTimeFormatter.ofPattern(DEFAULT_PATTERN, Locale.ROOT)));
values.put("server.time.iso", time.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME));
values.put("server.timezone", zone.getId());
values.put("server.time.offset", time.getOffset().getId());
values.put("server.time.epochSecond", instant.getEpochSecond());
values.put("server.time.epochMilli", instant.toEpochMilli());
values.put("server.time.year", time.getYear());
values.put("server.time.month", time.getMonthValue());
values.put("server.time.monthName", time.getMonth().name());
values.put("server.time.day", time.getDayOfMonth());
values.put("server.time.dayOfYear", time.getDayOfYear());
values.put("server.time.dayOfWeek", time.getDayOfWeek().name());
values.put("server.time.dayOfWeekNumber", time.getDayOfWeek().getValue());
values.put("server.time.weekOfYear", time.get(weeks.weekOfWeekBasedYear()));
values.put("server.time.hour24", time.getHour());
values.put("server.time.hour12", time.getHour() % 12 == 0 ? 12 : time.getHour() % 12);
values.put("server.time.minute", time.getMinute());
values.put("server.time.second", time.getSecond());
values.put("server.time.millisecond", time.getNano() / 1_000_000);
values.put("server.time.amPm", time.getHour() < 12 ? "AM" : "PM");
return Map.copyOf(values);
}
/** Formats an instant with the bounded pattern language accepted in message variables. */
public static String format(Instant instant, ZoneId zone, String pattern) {
validatePattern(pattern);
String result = DateTimeFormatter.ofPattern(pattern, Locale.ROOT).format(instant.atZone(zone));
if (result.length() > MAX_FORMATTED_LENGTH) {
throw new IllegalArgumentException("formatted trigger time exceeds 256 characters");
}
return result;
}
/**
* Validates a deliberately small DateTimeFormatter subset. It is broad enough for normal
* dates, localized separators, weekday names and UTC offsets, while excluding expensive or
* frequently-confused pattern fields.
*/
public static void validatePattern(String pattern) {
if (pattern == null || pattern.isBlank() || pattern.length() > MAX_PATTERN_LENGTH) {
throw new IllegalArgumentException("time format must contain 1 to 64 characters");
}
boolean quoted = false;
for (int index = 0; index < pattern.length(); index++) {
char value = pattern.charAt(index);
if (Character.isISOControl(value) || value == '{' || value == '}') {
throw new IllegalArgumentException("time format contains a forbidden character");
}
if (value == '\'') {
if (index + 1 < pattern.length() && pattern.charAt(index + 1) == '\'') {
index++;
} else {
quoted = !quoted;
}
continue;
}
if (quoted || !isAsciiLetter(value)) continue;
if (value == 'Y') {
throw new IllegalArgumentException(
"use uuuu (recommended) or yyyy for a calendar year; YYYY is week-based");
}
if (value == 'D') {
throw new IllegalArgumentException("use dd for the day of month; DD is day-of-year");
}
if ("uyMdHhmsSEeaXxZz".indexOf(value) < 0) {
throw new IllegalArgumentException("unsupported time format letter: " + value);
}
}
if (quoted) throw new IllegalArgumentException("time format has an unterminated quoted literal");
try {
DateTimeFormatter.ofPattern(pattern, Locale.ROOT);
} catch (IllegalArgumentException exception) {
throw new IllegalArgumentException("invalid time format: " + exception.getMessage(), exception);
}
}
private static boolean isAsciiLetter(char value) {
return value >= 'A' && value <= 'Z' || value >= 'a' && value <= 'z';
}
}
package com.xfestudio.xfeservermanager.core.trigger;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.WeekFields;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
/** Deterministic wall-clock values made available to every trigger execution. */
public final class TriggerTimeContext {
public static final String DEFAULT_PATTERN = "uuuu-MM-dd HH:mm:ss";
public static final String FORMAT_HINT = "推荐 uuuu-MM-dd HH:mm:ss;格式区分大小写:MM 是月、mm 是分、"
+ "HH 是 24 小时。yyyy 也可表示年号年;不要使用周历年 YYYY 或年内日 DD。 / "
+ "Use Java time patterns; uuuu-MM-dd HH:mm:ss is recommended. Patterns are case-sensitive: "
+ "MM is month, mm is minute, and HH is the 24-hour clock. yyyy is also accepted for the "
+ "year-of-era; YYYY is week-based, and DD is day-of-year.";
private static final int MAX_PATTERN_LENGTH = 64;
private static final int MAX_FORMATTED_LENGTH = 256;
private TriggerTimeContext() { }
/** Creates the immutable, server-local clock vocabulary for one captured instant. */
public static Map<String, Object> values(Instant instant, ZoneId zone) {
Objects.requireNonNull(instant, "instant");
Objects.requireNonNull(zone, "zone");
ZonedDateTime time = instant.atZone(zone);
WeekFields weeks = WeekFields.ISO;
Map<String, Object> values = new LinkedHashMap<>();
values.put("server.time", time.format(DateTimeFormatter.ofPattern(DEFAULT_PATTERN, Locale.ROOT)));
values.put("server.time.iso", time.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME));
values.put("server.timezone", zone.getId());
values.put("server.time.offset", time.getOffset().getId());
values.put("server.time.epochSecond", instant.getEpochSecond());
values.put("server.time.epochMilli", instant.toEpochMilli());
values.put("server.time.year", time.getYear());
values.put("server.time.month", time.getMonthValue());
values.put("server.time.monthName", time.getMonth().name());
values.put("server.time.day", time.getDayOfMonth());
values.put("server.time.dayOfYear", time.getDayOfYear());
values.put("server.time.dayOfWeek", time.getDayOfWeek().name());
values.put("server.time.dayOfWeekNumber", time.getDayOfWeek().getValue());
values.put("server.time.weekOfYear", time.get(weeks.weekOfWeekBasedYear()));
values.put("server.time.hour24", time.getHour());
values.put("server.time.hour12", time.getHour() % 12 == 0 ? 12 : time.getHour() % 12);
values.put("server.time.minute", time.getMinute());
values.put("server.time.second", time.getSecond());
values.put("server.time.millisecond", time.getNano() / 1_000_000);
values.put("server.time.amPm", time.getHour() < 12 ? "AM" : "PM");
return Map.copyOf(values);
}
/** Formats an instant with the bounded pattern language accepted in message variables. */
public static String format(Instant instant, ZoneId zone, String pattern) {
validatePattern(pattern);
String result = DateTimeFormatter.ofPattern(pattern, Locale.ROOT).format(instant.atZone(zone));
if (result.length() > MAX_FORMATTED_LENGTH) {
throw new IllegalArgumentException("formatted trigger time exceeds 256 characters");
}
return result;
}
/**
* Validates a deliberately small DateTimeFormatter subset. It is broad enough for normal
* dates, localized separators, weekday names and UTC offsets, while excluding expensive or
* frequently-confused pattern fields.
*/
public static void validatePattern(String pattern) {
if (pattern == null || pattern.isBlank() || pattern.length() > MAX_PATTERN_LENGTH) {
throw new IllegalArgumentException("time format must contain 1 to 64 characters");
}
boolean quoted = false;
for (int index = 0; index < pattern.length(); index++) {
char value = pattern.charAt(index);
if (Character.isISOControl(value) || value == '{' || value == '}') {
throw new IllegalArgumentException("time format contains a forbidden character");
}
if (value == '\'') {
if (index + 1 < pattern.length() && pattern.charAt(index + 1) == '\'') {
index++;
} else {
quoted = !quoted;
}
continue;
}
if (quoted || !isAsciiLetter(value)) continue;
if (value == 'Y') {
throw new IllegalArgumentException(
"use uuuu (recommended) or yyyy for a calendar year; YYYY is week-based");
}
if (value == 'D') {
throw new IllegalArgumentException("use dd for the day of month; DD is day-of-year");
}
if ("uyMdHhmsSEeaXxZz".indexOf(value) < 0) {
throw new IllegalArgumentException("unsupported time format letter: " + value);
}
}
if (quoted) throw new IllegalArgumentException("time format has an unterminated quoted literal");
try {
DateTimeFormatter.ofPattern(pattern, Locale.ROOT);
} catch (IllegalArgumentException exception) {
throw new IllegalArgumentException("invalid time format: " + exception.getMessage(), exception);
}
}
private static boolean isAsciiLetter(char value) {
return value >= 'A' && value <= 'Z' || value >= 'a' && value <= 'z';
}
}