XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEServerManager

【Java】我的世界XFE服务器管理器

公开
关注 0 Fork 0 Star 0
UTF-8
package com.xfestudio.xfeservermanager.core.trigger;

import java.time.DateTimeException;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;

/** Normalizes interval configuration and calculates server-local wall-clock occurrences. */
public final class TriggerIntervalSchedule {
    public static final long MAXIMUM_SECONDS = 31_536_000L;
    private static final long SECONDS_PER_DAY = 86_400L;
    private static final Set<String> PARAMETERS = Set.of("hours", "minutes", "seconds");

    private TriggerIntervalSchedule() { }

    /**
     * Converts either legacy {@code seconds=<total>} or hour/minute/second components to the
     * canonical, persistence-safe total-seconds representation. Supplying hours or minutes opts
     * into component mode, in which minutes and seconds are each restricted to 0..59.
     */
    public static Map<String, String> normalizeConfiguration(Map<String, String> configuration) {
        Map<String, String> supplied = new LinkedHashMap<>(
                configuration == null ? Map.of() : configuration);
        for (String parameter : supplied.keySet()) {
            if (!PARAMETERS.contains(parameter)) {
                throw new IllegalArgumentException(
                        "schedule.interval does not accept event configuration parameter " + parameter);
            }
        }
        boolean componentMode = supplied.containsKey("hours") || supplied.containsKey("minutes");
        long total;
        if (componentMode) {
            long hours = component(supplied, "hours", Long.MAX_VALUE);
            long minutes = component(supplied, "minutes", 59);
            long seconds = component(supplied, "seconds", 59);
            try {
                total = Math.addExact(Math.addExact(Math.multiplyExact(hours, 3_600L),
                        Math.multiplyExact(minutes, 60L)), seconds);
            } catch (ArithmeticException overflow) {
                throw invalidInterval(overflow);
            }
        } else {
            total = requiredLong(supplied.get("seconds"), "seconds");
        }
        validateTotal(total);
        return Map.of("seconds", Long.toString(total));
    }

    public static long totalSeconds(Map<String, String> configuration) {
        return Long.parseLong(normalizeConfiguration(configuration).get("seconds"));
    }

    /**
     * Returns the most recent due boundary, never a backlog. Alignment uses a continuous local
     * wall-clock timeline anchored at the local 1970-01-01 day boundary. A candidate must not
     * predate activation and must be strictly newer than the last completed persisted epoch.
     */
    public static Optional<Instant> dueOccurrence(
            Instant now, Instant activatedAt, Instant completedOccurrence,
            long intervalSeconds, ZoneId zone) {
        Objects.requireNonNull(now, "now");
        Objects.requireNonNull(activatedAt, "activatedAt");
        Objects.requireNonNull(zone, "zone");
        validateTotal(intervalSeconds);
        Instant candidate = alignedAtOrBefore(now, intervalSeconds, zone);
        if (candidate.isBefore(activatedAt)
                || (completedOccurrence != null && !candidate.isAfter(completedOccurrence))) {
            return Optional.empty();
        }
        return Optional.of(candidate);
    }

    private static Instant alignedAtOrBefore(Instant now, long intervalSeconds, ZoneId zone) {
        LocalDateTime localNow = LocalDateTime.ofInstant(now, zone);
        long wallSecond;
        try {
            wallSecond = Math.addExact(Math.multiplyExact(
                    localNow.toLocalDate().toEpochDay(), SECONDS_PER_DAY),
                    localNow.toLocalTime().toSecondOfDay());
        } catch (ArithmeticException overflow) {
            throw new IllegalArgumentException("instant is outside the supported local schedule range", overflow);
        }
        long aligned = Math.multiplyExact(Math.floorDiv(wallSecond, intervalSeconds), intervalSeconds);
        // A boundary inside a daylight-saving gap is shifted forward by ZoneRules and can land
        // after now. Jump back by enough full intervals rather than walking one second at a time.
        for (int attempt = 0; attempt < 4; attempt++) {
            Instant candidate = wallSecondToInstant(aligned, zone);
            if (!candidate.isAfter(now)) return candidate;
            long ahead = Math.max(0L, candidate.getEpochSecond() - now.getEpochSecond());
            long intervals = Math.floorDiv(ahead, intervalSeconds) + 1L;
            aligned = Math.subtractExact(aligned, Math.multiplyExact(intervals, intervalSeconds));
        }
        throw new IllegalArgumentException("could not resolve a local interval boundary");
    }

    private static Instant wallSecondToInstant(long wallSecond, ZoneId zone) {
        long epochDay = Math.floorDiv(wallSecond, SECONDS_PER_DAY);
        int secondOfDay = (int) Math.floorMod(wallSecond, SECONDS_PER_DAY);
        try {
            return LocalDateTime.of(LocalDate.ofEpochDay(epochDay), LocalTime.ofSecondOfDay(secondOfDay))
                    .atZone(zone).toInstant();
        } catch (DateTimeException exception) {
            throw new IllegalArgumentException("local interval boundary is outside the supported range", exception);
        }
    }

    private static long component(Map<String, String> supplied, String name, long maximum) {
        if (!supplied.containsKey(name)) return 0L;
        long parsed = requiredLong(supplied.get(name), name);
        if (parsed < 0 || parsed > maximum) throw invalidComponent(name, maximum, null);
        return parsed;
    }

    private static long requiredLong(String value, String name) {
        if (value == null || value.isBlank()) throw invalidComponent(name, Long.MAX_VALUE, null);
        try {
            return Long.parseLong(value.strip());
        } catch (NumberFormatException exception) {
            throw invalidComponent(name, Long.MAX_VALUE, exception);
        }
    }

    private static void validateTotal(long seconds) {
        if (seconds < 1 || seconds > MAXIMUM_SECONDS) throw invalidInterval(null);
    }

    private static IllegalArgumentException invalidComponent(
            String name, long maximum, RuntimeException cause) {
        String range = maximum == Long.MAX_VALUE ? "a whole number" : "between 0 and " + maximum;
        return new IllegalArgumentException(name + " must be " + range, cause);
    }

    private static IllegalArgumentException invalidInterval(RuntimeException cause) {
        return new IllegalArgumentException(
                "interval seconds must be between 1 and " + MAXIMUM_SECONDS, cause);
    }
}