diff --git a/README.md b/README.md index e379388..eb7e647 100644 --- a/README.md +++ b/README.md @@ -67,4 +67,73 @@ public class main { } } } +``` + +### Usage Scheduler (without retries) + +```java + +import java.time.Duration; + +public class main { + public static void main(String[] args) { + LicenseScheduler scheduler = LicenseScheduler.startScheduler( + Duration.ofMinutes(5), + apiUrl, + plugin, + licenseKey, + serverId, + result -> { + if (result instanceof LicenseSuccess success) { + if (!success.valid()) { + // Handle invalid license + } + } else if (result instanceof LicenseError error) { + // Log errors if you want + } + } + ); + + // Later: Stop the scheduler + scheduler.stop(); + } +} +``` + + +### Usage Scheduler (with retries) + +```java +import java.time.Duration; + +public class main { + public static void main(String[] args) { + LicenseScheduler scheduler = LicenseScheduler.startSchedulerWithRetries( + Duration.ofMinutes(5), + apiUrl, + plugin, + licenseKey, + serverId, + 3, //max retries, + Duration.ofSeconds(10), //delay between retries + result -> { + if(result instanceof LicenseSuccess success) { + if (!success.valid()) { + // Handle invalid license + } + } else if (result instanceof LicenseError error) { + // Log errors if you want + } + }, + result -> { + // Called only after all retries failed + // Put your "offline" handling here + // Disable plugin or something like that + } + ); + + // Later: Stop the scheduler + scheduler.stop(); + } +} ``` \ No newline at end of file diff --git a/build.gradle b/build.gradle deleted file mode 100644 index 30fcfa9..0000000 --- a/build.gradle +++ /dev/null @@ -1,49 +0,0 @@ -plugins { - id 'java' - id 'maven-publish' -} - -group = 'de.winniepat' -version = '0.1.0' - - -java { - toolchain { - languageVersion = JavaLanguageVersion.of(21) - } - withSourcesJar() - withJavadocJar() -} - -repositories { - mavenCentral() - maven {url = "https://repo.papermc.io/repository/maven-public/" } -} - -dependencies { - implementation 'com.google.code.gson:gson:2.13.1' -} - -tasks.named('test') { - useJUnitPlatform() -} - -publishing { - publications { - mavenJava(MavenPublication) { - from components.java - } - } - - repositories { - maven { - url = uri("https://maven.winniepat.de/repository/maven-releases/") - - credentials { - username = project.property("publish.username") - password = project.property("publish.password") - // the credentials from before are not valid anymore (I am not that dumb) - } - } - } -} \ No newline at end of file diff --git a/lib/build.gradle b/lib/build.gradle index 6dddafd..bbf00b6 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -1,41 +1,50 @@ -/* - * This file was generated by the Gradle 'init' task. - * - * This generated file contains a sample Java library project to get you started. - * For more details on building Java & JVM projects, please refer to https://docs.gradle.org/9.5.1/userguide/building_java_projects.html in the Gradle documentation. - */ - plugins { - // Apply the java-library plugin for API and implementation separation. - id 'java-library' + id 'java' + id 'maven-publish' } -repositories { - // Use Maven Central for resolving dependencies. - mavenCentral() -} +group = 'de.winniepat' +version = '0.1.1' -dependencies { - // Use JUnit Jupiter for testing. - testImplementation libs.junit.jupiter - testRuntimeOnly 'org.junit.platform:junit-platform-launcher' - - // This dependency is exported to consumers, that is to say found on their compile classpath. - api libs.commons.math3 - - // This dependency is used internally, and not exposed to consumers on their own compile classpath. - implementation libs.guava -} - -// Apply a specific Java toolchain to ease working on different environments. java { toolchain { languageVersion = JavaLanguageVersion.of(21) } + withSourcesJar() + withJavadocJar() +} + +repositories { + mavenCentral() + maven {url = "https://repo.papermc.io/repository/maven-public/" } +} + +dependencies { + implementation 'io.papermc.paper:paper-api:1.21.11-R0.1-SNAPSHOT' + implementation 'com.google.code.gson:gson:2.13.1' } tasks.named('test') { - // Use JUnit Platform for unit tests. useJUnitPlatform() } + +publishing { + publications { + mavenJava(MavenPublication) { + from components.java + } + } + + repositories { + maven { + url = uri("https://maven.winniepat.de/repository/maven-releases/") + + credentials { + username = project.property("publish.username") + password = project.property("publish.password") + // the credentials from before are not valid anymore (I am not that dumb) + } + } + } +} \ No newline at end of file diff --git a/src/main/java/de/winniepat/licenselib/LicenseClient.java b/lib/src/main/java/licenselib/LicenseClient.java similarity index 99% rename from src/main/java/de/winniepat/licenselib/LicenseClient.java rename to lib/src/main/java/licenselib/LicenseClient.java index 1cc2a1d..2ac4ee6 100644 --- a/src/main/java/de/winniepat/licenselib/LicenseClient.java +++ b/lib/src/main/java/licenselib/LicenseClient.java @@ -4,7 +4,7 @@ * Licensed under the MIT License */ -package de.winniepat.licenselib; +package licenselib; import com.google.gson.*; diff --git a/src/main/java/de/winniepat/licenselib/LicenseError.java b/lib/src/main/java/licenselib/LicenseError.java similarity index 90% rename from src/main/java/de/winniepat/licenselib/LicenseError.java rename to lib/src/main/java/licenselib/LicenseError.java index bce3c79..eb0af01 100644 --- a/src/main/java/de/winniepat/licenselib/LicenseError.java +++ b/lib/src/main/java/licenselib/LicenseError.java @@ -1,4 +1,4 @@ -package de.winniepat.licenselib; +package licenselib; /** * Represents an error that occurred during the license check process, such as network issues or invalid responses from the server. diff --git a/lib/src/main/java/licenselib/LicenseScheduler.java b/lib/src/main/java/licenselib/LicenseScheduler.java new file mode 100644 index 0000000..b43159f --- /dev/null +++ b/lib/src/main/java/licenselib/LicenseScheduler.java @@ -0,0 +1,194 @@ +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(); + } +} diff --git a/src/main/java/de/winniepat/licenselib/LicenseSuccess.java b/lib/src/main/java/licenselib/LicenseSuccess.java similarity index 97% rename from src/main/java/de/winniepat/licenselib/LicenseSuccess.java rename to lib/src/main/java/licenselib/LicenseSuccess.java index 3f0180a..3f3ff64 100644 --- a/src/main/java/de/winniepat/licenselib/LicenseSuccess.java +++ b/lib/src/main/java/licenselib/LicenseSuccess.java @@ -1,4 +1,4 @@ -package de.winniepat.licenselib; +package licenselib; /** * Represents a successful license check result, containing all relevant information about the license status, plugin, customer, and expiration details. diff --git a/lib/src/main/java/org/example/Library.java b/lib/src/main/java/org/example/Library.java deleted file mode 100644 index b98461b..0000000 --- a/lib/src/main/java/org/example/Library.java +++ /dev/null @@ -1,10 +0,0 @@ -/* - * This source file was generated by the Gradle 'init' task - */ -package org.example; - -public class Library { - public boolean someLibraryMethod() { - return true; - } -} diff --git a/lib/src/test/java/org/example/LibraryTest.java b/lib/src/test/java/org/example/LibraryTest.java deleted file mode 100644 index ef34950..0000000 --- a/lib/src/test/java/org/example/LibraryTest.java +++ /dev/null @@ -1,14 +0,0 @@ -/* - * This source file was generated by the Gradle 'init' task - */ -package org.example; - -import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; - -class LibraryTest { - @Test void someLibraryMethodReturnsTrue() { - Library classUnderTest = new Library(); - assertTrue(classUnderTest.someLibraryMethod(), "someLibraryMethod should return 'true'"); - } -}