package licenselib; import java.time.Duration; import java.util.Objects; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; /** * Periodically checks a license and exposes the most recent result. */ public final class LicenseScheduler implements AutoCloseable { private final ScheduledExecutorService executor; private final ScheduledFuture task; private final AtomicReference lastResult; private LicenseScheduler( ScheduledExecutorService executor, ScheduledFuture task, AtomicReference lastResult ) { this.executor = executor; this.task = task; this.lastResult = lastResult; } /** * Starts a scheduler that checks the license at the given interval. * @param interval Interval between checks * @param apiUrl Server API url * @param plugin Plugin id matching the one on the backend api * @param licenseKey License key issued by the api * @param serverId Server id matching the one on the backend api * @param onResult Optional callback invoked after every check * @return Scheduler handle for stopping and reading the last result */ public static LicenseScheduler startScheduler( Duration interval, String apiUrl, String plugin, String licenseKey, String serverId, Consumer onResult ) { Objects.requireNonNull(interval, "interval"); if (interval.isZero() || interval.isNegative()) { throw new IllegalArgumentException("interval must be positive"); } AtomicReference lastResult = new AtomicReference<>(); ThreadFactory threadFactory = runnable -> { Thread thread = new Thread(runnable, "license-scheduler"); thread.setDaemon(true); return thread; }; ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(threadFactory); ScheduledFuture task = executor.scheduleAtFixedRate(() -> { LicenseClient.LicenseResult result = LicenseClient.check(apiUrl, plugin, licenseKey, serverId); lastResult.set(result); if (onResult != null) { onResult.accept(result); } }, 0, interval.toMillis(), TimeUnit.MILLISECONDS); return new LicenseScheduler(executor, task, lastResult); } /** * Starts a scheduler that retries on errors before invoking offline handling. * @param interval Interval between checks * @param apiUrl Server API url * @param plugin Plugin id matching the one on the backend api * @param licenseKey License key issued by the api * @param serverId Server id matching the one on the backend api * @param maxRetries Number of retries after a failed check * @param retryDelay Delay between retries * @param onResult Optional callback invoked after every attempt * @param onOffline Invoked only after all retries fail * @return Scheduler handle for stopping and reading the last result */ public static LicenseScheduler startSchedulerWithRetries( Duration interval, String apiUrl, String plugin, String licenseKey, String serverId, int maxRetries, Duration retryDelay, Consumer onResult, Consumer onOffline ) { Objects.requireNonNull(interval, "interval"); Objects.requireNonNull(retryDelay, "retryDelay"); if (interval.isZero() || interval.isNegative()) { throw new IllegalArgumentException("interval must be positive"); } if (retryDelay.isNegative()) { throw new IllegalArgumentException("retryDelay must not be negative"); } if (maxRetries < 0) { throw new IllegalArgumentException("maxRetries must not be negative"); } AtomicReference lastResult = new AtomicReference<>(); ThreadFactory threadFactory = runnable -> { Thread thread = new Thread(runnable, "license-scheduler"); thread.setDaemon(true); return thread; }; ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(threadFactory); ScheduledFuture task = executor.scheduleAtFixedRate(() -> { LicenseClient.LicenseResult result = attemptWithRetries( apiUrl, plugin, licenseKey, serverId, maxRetries, retryDelay, onResult, onOffline ); lastResult.set(result); }, 0, interval.toMillis(), TimeUnit.MILLISECONDS); return new LicenseScheduler(executor, task, lastResult); } private static LicenseClient.LicenseResult attemptWithRetries( String apiUrl, String plugin, String licenseKey, String serverId, int maxRetries, Duration retryDelay, Consumer onResult, Consumer onOffline ) { int attempt = 0; LicenseClient.LicenseResult result; while (true) { result = LicenseClient.check(apiUrl, plugin, licenseKey, serverId); if (onResult != null) { onResult.accept(result); } if (!(result instanceof LicenseError)) { return result; } if (attempt >= maxRetries) { if (onOffline != null) { onOffline.accept(result); } return result; } attempt++; if (!retryDelay.isZero()) { try { Thread.sleep(retryDelay.toMillis()); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return result; } } } } /** * Returns the most recent license check result, or null if none has completed yet. * @return The last license check result, or null if not available */ public LicenseClient.LicenseResult getLastResult() { return lastResult.get(); } /** * Stops further checks and shuts down the scheduler. */ public void stop() { task.cancel(false); executor.shutdown(); } @Override public void close() { stop(); } }