diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8a94efd --- /dev/null +++ b/.gitignore @@ -0,0 +1,60 @@ +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs +hs_err_pid* +replay_pid* + +# Maven +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +.mvn/wrapper/maven-wrapper.jar + +# Gradle +.gradle/ +build/ + +# IDE +.idea/ +*.iml +*.ipr +*.iws +.project +.classpath +.settings/ +.vscode/ +*.swp +*.swo +.factorypath + +# OS +.DS_Store +Thumbs.db + +# IronLicensing cache +.ironlicensing/ diff --git a/README.md b/README.md index 76b40c8..faa40bb 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,358 @@ -# ironlicensing-java -IronLicensing SDK for Java - Software licensing and activation +# IronLicensing Java SDK + +Official Java SDK for [IronLicensing](https://ironlicensing.com) - Software licensing and activation for your applications. + +## Installation + +### Maven + +```xml + + com.ironservices + licensing + 1.0.0 + +``` + +### Gradle + +```groovy +implementation 'com.ironservices:licensing:1.0.0' +``` + +## Quick Start + +### Using Static API + +```java +import com.ironservices.licensing.*; + +public class Main { + public static void main(String[] args) { + // Initialize the SDK + IronLicensing.init("pk_live_your_public_key", "your-product-slug"); + + // Validate a license + LicenseResult result = IronLicensing.validate("IRON-XXXX-XXXX-XXXX-XXXX"); + if (result.isValid()) { + System.out.println("License is valid!"); + System.out.println("Status: " + result.getLicense().getStatus()); + } else { + System.out.println("Validation failed: " + result.getError()); + } + + // Check for features + if (IronLicensing.hasFeature("premium")) { + System.out.println("Premium features enabled!"); + } + } +} +``` + +### Using Client Instance + +```java +import com.ironservices.licensing.*; + +public class Main { + public static void main(String[] args) { + LicenseOptions options = LicenseOptions.builder("pk_live_your_public_key", "your-product-slug") + .debug(true) + .build(); + + LicenseClient client = new LicenseClient(options); + + // Activate license + LicenseResult result = client.activate("IRON-XXXX-XXXX-XXXX-XXXX", "My Machine"); + + if (result.isValid()) { + System.out.println("Activated! License type: " + result.getLicense().getType()); + } + } +} +``` + +## Configuration + +### Builder Pattern + +```java +LicenseOptions options = LicenseOptions.builder("pk_live_xxx", "your-product") + .apiBaseUrl("https://api.ironlicensing.com") // Custom API URL + .debug(true) // Enable debug logging + .enableOfflineCache(true) // Cache for offline use + .cacheValidationMinutes(60) // Cache duration + .offlineGraceDays(7) // Offline grace period + .httpTimeout(Duration.ofSeconds(30)) // Request timeout + .build(); +``` + +### Fluent Setters + +```java +LicenseOptions options = new LicenseOptions("pk_live_xxx", "your-product") + .setDebug(true) + .setEnableOfflineCache(true); +``` + +## License Validation + +```java +// Synchronous validation +LicenseResult result = client.validate("IRON-XXXX-XXXX-XXXX-XXXX"); + +// Asynchronous validation +client.validateAsync("IRON-XXXX-XXXX-XXXX-XXXX") + .thenAccept(r -> { + if (r.isValid()) { + License license = r.getLicense(); + System.out.println("License: " + license.getKey()); + System.out.println("Status: " + license.getStatus()); + System.out.println("Type: " + license.getType()); + System.out.printf("Activations: %d/%d%n", + license.getCurrentActivations(), + license.getMaxActivations()); + } + }); +``` + +## License Activation + +```java +// Simple activation (uses hostname as machine name) +LicenseResult result = client.activate("IRON-XXXX-XXXX-XXXX-XXXX"); + +// With custom machine name +LicenseResult result = client.activate("IRON-XXXX-XXXX-XXXX-XXXX", "Production Server"); + +if (result.isValid()) { + System.out.println("License activated successfully!"); + + // View activations + for (Activation activation : result.getActivations()) { + System.out.printf("- %s (%s)%n", activation.getMachineName(), activation.getPlatform()); + } +} + +// Async activation +client.activateAsync(licenseKey, "My Machine") + .thenAccept(r -> System.out.println("Activated: " + r.isValid())); +``` + +## License Deactivation + +```java +// Synchronous +if (client.deactivate()) { + System.out.println("License deactivated from this machine"); +} + +// Asynchronous +client.deactivateAsync() + .thenAccept(success -> { + if (success) { + System.out.println("Deactivated successfully"); + } + }); +``` + +## Feature Checking + +```java +// Check if feature is available +if (client.hasFeature("advanced-analytics")) { + // Enable advanced analytics +} + +// Require feature (throws LicenseRequiredException if not available) +try { + client.requireFeature("export-pdf"); + // Feature is available, continue with export +} catch (LicenseRequiredException e) { + System.out.println("Feature not available: " + e.getFeature()); +} + +// Get feature details +Feature feature = client.getFeature("max-users"); +if (feature != null) { + System.out.printf("Feature: %s - %s%n", feature.getName(), feature.getDescription()); +} +``` + +## Trial Management + +```java +LicenseResult result = client.startTrial("user@example.com"); + +if (result.isValid()) { + System.out.println("Trial started!"); + System.out.println("Trial key: " + result.getLicense().getKey()); + + String expiresAt = result.getLicense().getExpiresAt(); + if (expiresAt != null) { + System.out.println("Expires: " + expiresAt); + } +} +``` + +## In-App Purchase + +```java +// Get available tiers +List tiers = client.getTiers(); +for (ProductTier tier : tiers) { + System.out.printf("%s - $%.2f %s%n", tier.getName(), tier.getPrice(), tier.getCurrency()); +} + +// Start checkout +CheckoutResult checkout = client.startPurchase("tier-id", "user@example.com"); +if (checkout.isSuccess()) { + System.out.println("Checkout URL: " + checkout.getCheckoutUrl()); + // Open URL in browser for user to complete purchase +} + +// Async purchase +client.startPurchaseAsync("tier-id", "user@example.com") + .thenAccept(c -> { + if (c.isSuccess()) { + // Handle success + } + }); +``` + +## License Status + +```java +// Get current license +License license = client.getLicense(); +if (license != null) { + System.out.println("Licensed to: " + license.getEmail()); +} + +// Check status +LicenseStatus status = client.getStatus(); +switch (status) { + case VALID: + System.out.println("License is valid"); + break; + case EXPIRED: + System.out.println("License has expired"); + break; + case TRIAL: + System.out.println("Running in trial mode"); + break; + case NOT_ACTIVATED: + System.out.println("No license activated"); + break; + default: + System.out.println("Status: " + status); +} + +// Quick checks +if (client.isLicensed()) { + System.out.println("Application is licensed"); +} + +if (client.isTrial()) { + System.out.println("Running in trial mode"); +} +``` + +## License Change Listener + +```java +client.setOnLicenseChanged(license -> { + if (license != null) { + System.out.println("License updated: " + license.getStatus()); + } else { + System.out.println("License removed"); + } +}); +``` + +## License Types + +| Type | Description | +|------|-------------| +| `PERPETUAL` | One-time purchase, never expires | +| `SUBSCRIPTION` | Recurring payment, expires if not renewed | +| `TRIAL` | Time-limited trial license | + +## License Statuses + +| Status | Description | +|--------|-------------| +| `VALID` | License is valid and active | +| `EXPIRED` | License has expired | +| `SUSPENDED` | License temporarily suspended | +| `REVOKED` | License permanently revoked | +| `TRIAL` | Active trial license | +| `TRIAL_EXPIRED` | Trial period ended | +| `NOT_ACTIVATED` | No license activated | + +## Thread Safety + +The client is thread-safe and can be used concurrently: + +```java +ExecutorService executor = Executors.newFixedThreadPool(10); +for (int i = 0; i < 10; i++) { + executor.submit(() -> { + if (client.hasFeature("concurrent-feature")) { + // Safe to call from multiple threads + } + }); +} +executor.shutdown(); +``` + +## Error Handling + +```java +// Validation errors +LicenseResult result = client.validate(licenseKey); +if (!result.isValid()) { + String error = result.getError(); + switch (error) { + case "license_not_found": + System.out.println("Invalid license key"); + break; + case "license_expired": + System.out.println("Your license has expired"); + break; + case "max_activations_reached": + System.out.println("No more activations available"); + break; + default: + System.out.println("Error: " + error); + } +} + +// Feature requirement errors +try { + client.requireFeature("premium"); +} catch (LicenseRequiredException e) { + System.out.printf("Feature '%s' requires a valid license%n", e.getFeature()); +} +``` + +## Machine ID + +The SDK automatically generates and persists a unique machine ID at `~/.ironlicensing/machine_id`. This ID is used for: +- Tracking activations per machine +- Preventing license sharing +- Offline validation + +```java +String machineId = client.getMachineId(); +``` + +## Requirements + +- Java 11 or later +- OkHttp 4.x +- Gson 2.x + +## License + +MIT License - see LICENSE file for details. diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..518f40e --- /dev/null +++ b/pom.xml @@ -0,0 +1,98 @@ + + + 4.0.0 + + com.ironservices + licensing + 1.0.0 + jar + + IronLicensing Java SDK + Official Java SDK for IronLicensing - Software licensing and activation + https://github.com/IronServices/ironlicensing-java + + + + MIT License + https://opensource.org/licenses/MIT + + + + + + IronServices + support@ironservices.com + IronServices + https://ironservices.com + + + + + scm:git:git://github.com/IronServices/ironlicensing-java.git + scm:git:ssh://github.com:IronServices/ironlicensing-java.git + https://github.com/IronServices/ironlicensing-java/tree/main + + + + 11 + 11 + UTF-8 + + + + + com.squareup.okhttp3 + okhttp + 4.12.0 + + + com.google.code.gson + gson + 2.10.1 + + + org.junit.jupiter + junit-jupiter + 5.10.0 + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + + org.apache.maven.plugins + maven-source-plugin + 3.3.0 + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.6.0 + + + attach-javadocs + + jar + + + + + + + diff --git a/src/main/java/com/ironservices/licensing/Activation.java b/src/main/java/com/ironservices/licensing/Activation.java new file mode 100644 index 0000000..ef36280 --- /dev/null +++ b/src/main/java/com/ironservices/licensing/Activation.java @@ -0,0 +1,81 @@ +package com.ironservices.licensing; + +import com.google.gson.annotations.SerializedName; + +/** + * Represents an activation of a license on a machine. + */ +public class Activation { + @SerializedName("id") + private String id; + + @SerializedName("machineId") + private String machineId; + + @SerializedName("machineName") + private String machineName; + + @SerializedName("platform") + private String platform; + + @SerializedName("activatedAt") + private String activatedAt; + + @SerializedName("lastSeenAt") + private String lastSeenAt; + + public Activation() {} + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getMachineId() { + return machineId; + } + + public void setMachineId(String machineId) { + this.machineId = machineId; + } + + public String getMachineName() { + return machineName; + } + + public void setMachineName(String machineName) { + this.machineName = machineName; + } + + public String getPlatform() { + return platform; + } + + public void setPlatform(String platform) { + this.platform = platform; + } + + public String getActivatedAt() { + return activatedAt; + } + + public void setActivatedAt(String activatedAt) { + this.activatedAt = activatedAt; + } + + public String getLastSeenAt() { + return lastSeenAt; + } + + public void setLastSeenAt(String lastSeenAt) { + this.lastSeenAt = lastSeenAt; + } + + @Override + public String toString() { + return "Activation{id='" + id + "', machineName='" + machineName + "', platform='" + platform + "'}"; + } +} diff --git a/src/main/java/com/ironservices/licensing/CheckoutResult.java b/src/main/java/com/ironservices/licensing/CheckoutResult.java new file mode 100644 index 0000000..39e7cfe --- /dev/null +++ b/src/main/java/com/ironservices/licensing/CheckoutResult.java @@ -0,0 +1,76 @@ +package com.ironservices.licensing; + +import com.google.gson.annotations.SerializedName; + +/** + * Represents the result of starting a checkout. + */ +public class CheckoutResult { + @SerializedName("success") + private boolean success; + + @SerializedName("checkoutUrl") + private String checkoutUrl; + + @SerializedName("sessionId") + private String sessionId; + + @SerializedName("error") + private String error; + + public CheckoutResult() {} + + public CheckoutResult(boolean success, String error) { + this.success = success; + this.error = error; + } + + public static CheckoutResult success(String checkoutUrl, String sessionId) { + CheckoutResult result = new CheckoutResult(); + result.success = true; + result.checkoutUrl = checkoutUrl; + result.sessionId = sessionId; + return result; + } + + public static CheckoutResult failure(String error) { + return new CheckoutResult(false, error); + } + + public boolean isSuccess() { + return success; + } + + public void setSuccess(boolean success) { + this.success = success; + } + + public String getCheckoutUrl() { + return checkoutUrl; + } + + public void setCheckoutUrl(String checkoutUrl) { + this.checkoutUrl = checkoutUrl; + } + + public String getSessionId() { + return sessionId; + } + + public void setSessionId(String sessionId) { + this.sessionId = sessionId; + } + + public String getError() { + return error; + } + + public void setError(String error) { + this.error = error; + } + + @Override + public String toString() { + return "CheckoutResult{success=" + success + ", checkoutUrl='" + checkoutUrl + "'}"; + } +} diff --git a/src/main/java/com/ironservices/licensing/Feature.java b/src/main/java/com/ironservices/licensing/Feature.java new file mode 100644 index 0000000..49a8142 --- /dev/null +++ b/src/main/java/com/ironservices/licensing/Feature.java @@ -0,0 +1,77 @@ +package com.ironservices.licensing; + +import com.google.gson.annotations.SerializedName; +import java.util.Map; + +/** + * Represents a feature in a license. + */ +public class Feature { + @SerializedName("key") + private String key; + + @SerializedName("name") + private String name; + + @SerializedName("enabled") + private boolean enabled; + + @SerializedName("description") + private String description; + + @SerializedName("metadata") + private Map metadata; + + public Feature() {} + + public Feature(String key, String name, boolean enabled) { + this.key = key; + this.name = name; + this.enabled = enabled; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Map getMetadata() { + return metadata; + } + + public void setMetadata(Map metadata) { + this.metadata = metadata; + } + + @Override + public String toString() { + return "Feature{key='" + key + "', name='" + name + "', enabled=" + enabled + "}"; + } +} diff --git a/src/main/java/com/ironservices/licensing/IronLicensing.java b/src/main/java/com/ironservices/licensing/IronLicensing.java new file mode 100644 index 0000000..911029d --- /dev/null +++ b/src/main/java/com/ironservices/licensing/IronLicensing.java @@ -0,0 +1,209 @@ +package com.ironservices.licensing; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; + +/** + * Static facade for the IronLicensing SDK. + * Provides a simple global API for license operations. + */ +public final class IronLicensing { + private static volatile LicenseClient client; + private static final Object lock = new Object(); + + private IronLicensing() {} + + /** + * Initializes the global IronLicensing client. + * + * @param publicKey The public key for your product + * @param productSlug The product slug + */ + public static void init(String publicKey, String productSlug) { + init(new LicenseOptions(publicKey, productSlug)); + } + + /** + * Initializes the global IronLicensing client with options. + * + * @param options Configuration options + */ + public static void init(LicenseOptions options) { + synchronized (lock) { + client = new LicenseClient(options); + } + } + + /** + * Gets the global client instance. + * + * @return The client, or null if not initialized + */ + public static LicenseClient getClient() { + return client; + } + + private static LicenseClient requireClient() { + LicenseClient c = client; + if (c == null) { + throw new IllegalStateException("IronLicensing not initialized. Call IronLicensing.init() first."); + } + return c; + } + + /** + * Validates a license key. + * + * @param licenseKey The license key to validate + * @return The validation result + */ + public static LicenseResult validate(String licenseKey) { + return requireClient().validate(licenseKey); + } + + /** + * Validates a license key asynchronously. + * + * @param licenseKey The license key to validate + * @return A CompletableFuture with the validation result + */ + public static CompletableFuture validateAsync(String licenseKey) { + return requireClient().validateAsync(licenseKey); + } + + /** + * Activates a license key on this machine. + * + * @param licenseKey The license key to activate + * @return The activation result + */ + public static LicenseResult activate(String licenseKey) { + return requireClient().activate(licenseKey); + } + + /** + * Activates a license key with a custom machine name. + * + * @param licenseKey The license key to activate + * @param machineName The machine name + * @return The activation result + */ + public static LicenseResult activate(String licenseKey, String machineName) { + return requireClient().activate(licenseKey, machineName); + } + + /** + * Deactivates the current license from this machine. + * + * @return true if deactivation was successful + */ + public static boolean deactivate() { + return requireClient().deactivate(); + } + + /** + * Starts a trial for the given email. + * + * @param email The email address for the trial + * @return The trial result + */ + public static LicenseResult startTrial(String email) { + return requireClient().startTrial(email); + } + + /** + * Checks if a feature is available. + * + * @param featureKey The feature key to check + * @return true if the feature is enabled + */ + public static boolean hasFeature(String featureKey) { + return requireClient().hasFeature(featureKey); + } + + /** + * Requires a feature to be available. + * + * @param featureKey The feature key to require + * @throws LicenseRequiredException if the feature is not available + */ + public static void requireFeature(String featureKey) throws LicenseRequiredException { + requireClient().requireFeature(featureKey); + } + + /** + * Gets a feature from the current license. + * + * @param featureKey The feature key + * @return The feature, or null if not found + */ + public static Feature getFeature(String featureKey) { + return requireClient().getFeature(featureKey); + } + + /** + * Gets the current license. + * + * @return The current license, or null if not licensed + */ + public static License getLicense() { + return requireClient().getLicense(); + } + + /** + * Gets the current license status. + * + * @return The license status + */ + public static LicenseStatus getStatus() { + return requireClient().getStatus(); + } + + /** + * Checks if the application is licensed. + * + * @return true if licensed + */ + public static boolean isLicensed() { + return requireClient().isLicensed(); + } + + /** + * Checks if running in trial mode. + * + * @return true if in trial mode + */ + public static boolean isTrial() { + return requireClient().isTrial(); + } + + /** + * Gets available product tiers. + * + * @return List of product tiers + */ + public static List getTiers() { + return requireClient().getTiers(); + } + + /** + * Starts a checkout session. + * + * @param tierId The tier ID to purchase + * @param email The customer's email + * @return The checkout result + */ + public static CheckoutResult startPurchase(String tierId, String email) { + return requireClient().startPurchase(tierId, email); + } + + /** + * Sets a listener for license changes. + * + * @param listener The listener to call when license changes + */ + public static void setOnLicenseChanged(Consumer listener) { + requireClient().setOnLicenseChanged(listener); + } +} diff --git a/src/main/java/com/ironservices/licensing/License.java b/src/main/java/com/ironservices/licensing/License.java new file mode 100644 index 0000000..9373505 --- /dev/null +++ b/src/main/java/com/ironservices/licensing/License.java @@ -0,0 +1,186 @@ +package com.ironservices.licensing; + +import com.google.gson.annotations.SerializedName; +import java.time.Instant; +import java.util.List; +import java.util.Map; + +/** + * Represents license information. + */ +public class License { + @SerializedName("id") + private String id; + + @SerializedName("key") + private String key; + + @SerializedName("status") + private LicenseStatus status; + + @SerializedName("type") + private LicenseType type; + + @SerializedName("email") + private String email; + + @SerializedName("name") + private String name; + + @SerializedName("company") + private String company; + + @SerializedName("features") + private List features; + + @SerializedName("maxActivations") + private int maxActivations; + + @SerializedName("currentActivations") + private int currentActivations; + + @SerializedName("expiresAt") + private String expiresAt; + + @SerializedName("createdAt") + private String createdAt; + + @SerializedName("lastValidatedAt") + private String lastValidatedAt; + + @SerializedName("metadata") + private Map metadata; + + public License() {} + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public LicenseStatus getStatus() { + return status; + } + + public void setStatus(LicenseStatus status) { + this.status = status; + } + + public LicenseType getType() { + return type; + } + + public void setType(LicenseType type) { + this.type = type; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getCompany() { + return company; + } + + public void setCompany(String company) { + this.company = company; + } + + public List getFeatures() { + return features; + } + + public void setFeatures(List features) { + this.features = features; + } + + public int getMaxActivations() { + return maxActivations; + } + + public void setMaxActivations(int maxActivations) { + this.maxActivations = maxActivations; + } + + public int getCurrentActivations() { + return currentActivations; + } + + public void setCurrentActivations(int currentActivations) { + this.currentActivations = currentActivations; + } + + public String getExpiresAt() { + return expiresAt; + } + + public void setExpiresAt(String expiresAt) { + this.expiresAt = expiresAt; + } + + public String getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(String createdAt) { + this.createdAt = createdAt; + } + + public String getLastValidatedAt() { + return lastValidatedAt; + } + + public void setLastValidatedAt(String lastValidatedAt) { + this.lastValidatedAt = lastValidatedAt; + } + + public Map getMetadata() { + return metadata; + } + + public void setMetadata(Map metadata) { + this.metadata = metadata; + } + + public boolean hasFeature(String featureKey) { + if (features == null) return false; + return features.stream() + .anyMatch(f -> f.getKey().equals(featureKey) && f.isEnabled()); + } + + public Feature getFeature(String featureKey) { + if (features == null) return null; + return features.stream() + .filter(f -> f.getKey().equals(featureKey)) + .findFirst() + .orElse(null); + } + + @Override + public String toString() { + return "License{id='" + id + "', key='" + key + "', status=" + status + ", type=" + type + "}"; + } +} diff --git a/src/main/java/com/ironservices/licensing/LicenseClient.java b/src/main/java/com/ironservices/licensing/LicenseClient.java new file mode 100644 index 0000000..28fefbb --- /dev/null +++ b/src/main/java/com/ironservices/licensing/LicenseClient.java @@ -0,0 +1,362 @@ +package com.ironservices.licensing; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.Consumer; + +/** + * Main client for the IronLicensing SDK. + * Thread-safe and can be used concurrently. + */ +public class LicenseClient { + private final LicenseOptions options; + private final Transport transport; + private final ReadWriteLock lock = new ReentrantReadWriteLock(); + + private License currentLicense; + private String licenseKey; + private Consumer onLicenseChanged; + + /** + * Creates a new LicenseClient with the given options. + * + * @param options Configuration options + */ + public LicenseClient(LicenseOptions options) { + this.options = options; + this.transport = new Transport(options); + if (options.isDebug()) { + log("Client initialized"); + } + } + + /** + * Creates a new LicenseClient with public key and product slug. + * + * @param publicKey The public key for your product + * @param productSlug The product slug + */ + public LicenseClient(String publicKey, String productSlug) { + this(new LicenseOptions(publicKey, productSlug)); + } + + private void log(String message) { + if (options.isDebug()) { + System.out.println("[IronLicensing] " + message); + } + } + + /** + * Sets a listener for license changes. + * + * @param listener The listener to call when license changes + */ + public void setOnLicenseChanged(Consumer listener) { + this.onLicenseChanged = listener; + } + + /** + * Validates a license key. + * + * @param licenseKey The license key to validate + * @return The validation result + */ + public LicenseResult validate(String licenseKey) { + LicenseResult result = transport.validate(licenseKey); + if (result.isValid() && result.getLicense() != null) { + updateLicense(licenseKey, result.getLicense()); + } + return result; + } + + /** + * Validates a license key asynchronously. + * + * @param licenseKey The license key to validate + * @return A CompletableFuture with the validation result + */ + public CompletableFuture validateAsync(String licenseKey) { + return CompletableFuture.supplyAsync(() -> validate(licenseKey)); + } + + /** + * Activates a license key on this machine. + * + * @param licenseKey The license key to activate + * @return The activation result + */ + public LicenseResult activate(String licenseKey) { + return activate(licenseKey, null); + } + + /** + * Activates a license key on this machine with a custom machine name. + * + * @param licenseKey The license key to activate + * @param machineName Optional machine name + * @return The activation result + */ + public LicenseResult activate(String licenseKey, String machineName) { + LicenseResult result = transport.activate(licenseKey, machineName); + if (result.isValid() && result.getLicense() != null) { + updateLicense(licenseKey, result.getLicense()); + } + return result; + } + + /** + * Activates a license key asynchronously. + * + * @param licenseKey The license key to activate + * @param machineName Optional machine name + * @return A CompletableFuture with the activation result + */ + public CompletableFuture activateAsync(String licenseKey, String machineName) { + return CompletableFuture.supplyAsync(() -> activate(licenseKey, machineName)); + } + + /** + * Deactivates the current license from this machine. + * + * @return true if deactivation was successful + */ + public boolean deactivate() { + lock.readLock().lock(); + String key; + try { + key = this.licenseKey; + } finally { + lock.readLock().unlock(); + } + + if (key == null || key.isEmpty()) { + return false; + } + + if (transport.deactivate(key)) { + lock.writeLock().lock(); + try { + this.currentLicense = null; + this.licenseKey = null; + } finally { + lock.writeLock().unlock(); + } + notifyLicenseChanged(null); + return true; + } + return false; + } + + /** + * Deactivates the current license asynchronously. + * + * @return A CompletableFuture with the deactivation result + */ + public CompletableFuture deactivateAsync() { + return CompletableFuture.supplyAsync(this::deactivate); + } + + /** + * Starts a trial for the given email. + * + * @param email The email address for the trial + * @return The trial result + */ + public LicenseResult startTrial(String email) { + LicenseResult result = transport.startTrial(email); + if (result.isValid() && result.getLicense() != null) { + updateLicense(result.getLicense().getKey(), result.getLicense()); + } + return result; + } + + /** + * Starts a trial asynchronously. + * + * @param email The email address for the trial + * @return A CompletableFuture with the trial result + */ + public CompletableFuture startTrialAsync(String email) { + return CompletableFuture.supplyAsync(() -> startTrial(email)); + } + + /** + * Checks if a feature is available in the current license. + * + * @param featureKey The feature key to check + * @return true if the feature is enabled + */ + public boolean hasFeature(String featureKey) { + lock.readLock().lock(); + try { + return currentLicense != null && currentLicense.hasFeature(featureKey); + } finally { + lock.readLock().unlock(); + } + } + + /** + * Requires a feature to be available, throws if not. + * + * @param featureKey The feature key to require + * @throws LicenseRequiredException if the feature is not available + */ + public void requireFeature(String featureKey) throws LicenseRequiredException { + if (!hasFeature(featureKey)) { + throw new LicenseRequiredException(featureKey); + } + } + + /** + * Gets a feature from the current license. + * + * @param featureKey The feature key + * @return The feature, or null if not found + */ + public Feature getFeature(String featureKey) { + lock.readLock().lock(); + try { + if (currentLicense != null) { + return currentLicense.getFeature(featureKey); + } + return null; + } finally { + lock.readLock().unlock(); + } + } + + /** + * Gets the current license. + * + * @return The current license, or null if not licensed + */ + public License getLicense() { + lock.readLock().lock(); + try { + return currentLicense; + } finally { + lock.readLock().unlock(); + } + } + + /** + * Gets the current license status. + * + * @return The license status + */ + public LicenseStatus getStatus() { + lock.readLock().lock(); + try { + if (currentLicense != null) { + return currentLicense.getStatus(); + } + return LicenseStatus.NOT_ACTIVATED; + } finally { + lock.readLock().unlock(); + } + } + + /** + * Checks if the application is licensed. + * + * @return true if licensed (valid or trial) + */ + public boolean isLicensed() { + lock.readLock().lock(); + try { + if (currentLicense == null) return false; + LicenseStatus status = currentLicense.getStatus(); + return status == LicenseStatus.VALID || status == LicenseStatus.TRIAL; + } finally { + lock.readLock().unlock(); + } + } + + /** + * Checks if running in trial mode. + * + * @return true if in trial mode + */ + public boolean isTrial() { + lock.readLock().lock(); + try { + if (currentLicense == null) return false; + return currentLicense.getStatus() == LicenseStatus.TRIAL || + currentLicense.getType() == LicenseType.TRIAL; + } finally { + lock.readLock().unlock(); + } + } + + /** + * Gets available product tiers for purchase. + * + * @return List of product tiers + */ + public List getTiers() { + return transport.getTiers(); + } + + /** + * Gets available product tiers asynchronously. + * + * @return A CompletableFuture with the list of tiers + */ + public CompletableFuture> getTiersAsync() { + return CompletableFuture.supplyAsync(this::getTiers); + } + + /** + * Starts a checkout session for the specified tier. + * + * @param tierId The tier ID to purchase + * @param email The customer's email + * @return The checkout result with URL + */ + public CheckoutResult startPurchase(String tierId, String email) { + return transport.startCheckout(tierId, email); + } + + /** + * Starts a checkout session asynchronously. + * + * @param tierId The tier ID to purchase + * @param email The customer's email + * @return A CompletableFuture with the checkout result + */ + public CompletableFuture startPurchaseAsync(String tierId, String email) { + return CompletableFuture.supplyAsync(() -> startPurchase(tierId, email)); + } + + /** + * Gets the machine ID used for activations. + * + * @return The machine ID + */ + public String getMachineId() { + return transport.getMachineId(); + } + + private void updateLicense(String key, License license) { + lock.writeLock().lock(); + try { + this.licenseKey = key; + this.currentLicense = license; + } finally { + lock.writeLock().unlock(); + } + notifyLicenseChanged(license); + } + + private void notifyLicenseChanged(License license) { + if (onLicenseChanged != null) { + try { + onLicenseChanged.accept(license); + } catch (Exception e) { + log("License change listener error: " + e.getMessage()); + } + } + } +} diff --git a/src/main/java/com/ironservices/licensing/LicenseOptions.java b/src/main/java/com/ironservices/licensing/LicenseOptions.java new file mode 100644 index 0000000..38950b6 --- /dev/null +++ b/src/main/java/com/ironservices/licensing/LicenseOptions.java @@ -0,0 +1,153 @@ +package com.ironservices.licensing; + +import java.time.Duration; + +/** + * Configuration options for the LicenseClient. + */ +public class LicenseOptions { + private static final String DEFAULT_API_BASE_URL = "https://api.ironlicensing.com"; + private static final Duration DEFAULT_HTTP_TIMEOUT = Duration.ofSeconds(30); + private static final int DEFAULT_CACHE_VALIDATION_MINUTES = 60; + private static final int DEFAULT_OFFLINE_GRACE_DAYS = 7; + + private String publicKey; + private String productSlug; + private String apiBaseUrl = DEFAULT_API_BASE_URL; + private boolean debug = false; + private boolean enableOfflineCache = true; + private int cacheValidationMinutes = DEFAULT_CACHE_VALIDATION_MINUTES; + private int offlineGraceDays = DEFAULT_OFFLINE_GRACE_DAYS; + private Duration httpTimeout = DEFAULT_HTTP_TIMEOUT; + + public LicenseOptions() {} + + public LicenseOptions(String publicKey, String productSlug) { + this.publicKey = publicKey; + this.productSlug = productSlug; + } + + public static Builder builder(String publicKey, String productSlug) { + return new Builder(publicKey, productSlug); + } + + public String getPublicKey() { + return publicKey; + } + + public LicenseOptions setPublicKey(String publicKey) { + this.publicKey = publicKey; + return this; + } + + public String getProductSlug() { + return productSlug; + } + + public LicenseOptions setProductSlug(String productSlug) { + this.productSlug = productSlug; + return this; + } + + public String getApiBaseUrl() { + return apiBaseUrl; + } + + public LicenseOptions setApiBaseUrl(String apiBaseUrl) { + this.apiBaseUrl = apiBaseUrl; + return this; + } + + public boolean isDebug() { + return debug; + } + + public LicenseOptions setDebug(boolean debug) { + this.debug = debug; + return this; + } + + public boolean isEnableOfflineCache() { + return enableOfflineCache; + } + + public LicenseOptions setEnableOfflineCache(boolean enableOfflineCache) { + this.enableOfflineCache = enableOfflineCache; + return this; + } + + public int getCacheValidationMinutes() { + return cacheValidationMinutes; + } + + public LicenseOptions setCacheValidationMinutes(int cacheValidationMinutes) { + this.cacheValidationMinutes = cacheValidationMinutes; + return this; + } + + public int getOfflineGraceDays() { + return offlineGraceDays; + } + + public LicenseOptions setOfflineGraceDays(int offlineGraceDays) { + this.offlineGraceDays = offlineGraceDays; + return this; + } + + public Duration getHttpTimeout() { + return httpTimeout; + } + + public LicenseOptions setHttpTimeout(Duration httpTimeout) { + this.httpTimeout = httpTimeout; + return this; + } + + public static class Builder { + private final LicenseOptions options; + + public Builder(String publicKey, String productSlug) { + this.options = new LicenseOptions(publicKey, productSlug); + } + + public Builder apiBaseUrl(String apiBaseUrl) { + options.setApiBaseUrl(apiBaseUrl); + return this; + } + + public Builder debug(boolean debug) { + options.setDebug(debug); + return this; + } + + public Builder enableOfflineCache(boolean enable) { + options.setEnableOfflineCache(enable); + return this; + } + + public Builder cacheValidationMinutes(int minutes) { + options.setCacheValidationMinutes(minutes); + return this; + } + + public Builder offlineGraceDays(int days) { + options.setOfflineGraceDays(days); + return this; + } + + public Builder httpTimeout(Duration timeout) { + options.setHttpTimeout(timeout); + return this; + } + + public LicenseOptions build() { + if (options.getPublicKey() == null || options.getPublicKey().isEmpty()) { + throw new IllegalArgumentException("Public key is required"); + } + if (options.getProductSlug() == null || options.getProductSlug().isEmpty()) { + throw new IllegalArgumentException("Product slug is required"); + } + return options; + } + } +} diff --git a/src/main/java/com/ironservices/licensing/LicenseRequiredException.java b/src/main/java/com/ironservices/licensing/LicenseRequiredException.java new file mode 100644 index 0000000..aa0d0f0 --- /dev/null +++ b/src/main/java/com/ironservices/licensing/LicenseRequiredException.java @@ -0,0 +1,17 @@ +package com.ironservices.licensing; + +/** + * Exception thrown when a required feature is not available in the current license. + */ +public class LicenseRequiredException extends RuntimeException { + private final String feature; + + public LicenseRequiredException(String feature) { + super("Feature '" + feature + "' requires a valid license"); + this.feature = feature; + } + + public String getFeature() { + return feature; + } +} diff --git a/src/main/java/com/ironservices/licensing/LicenseResult.java b/src/main/java/com/ironservices/licensing/LicenseResult.java new file mode 100644 index 0000000..3e94e3b --- /dev/null +++ b/src/main/java/com/ironservices/licensing/LicenseResult.java @@ -0,0 +1,87 @@ +package com.ironservices.licensing; + +import com.google.gson.annotations.SerializedName; +import java.util.List; + +/** + * Represents the result of a license validation or activation. + */ +public class LicenseResult { + @SerializedName("valid") + private boolean valid; + + @SerializedName("license") + private License license; + + @SerializedName("activations") + private List activations; + + @SerializedName("error") + private String error; + + @SerializedName("cached") + private boolean cached; + + public LicenseResult() {} + + public LicenseResult(boolean valid, String error) { + this.valid = valid; + this.error = error; + } + + public static LicenseResult success(License license) { + LicenseResult result = new LicenseResult(); + result.valid = true; + result.license = license; + return result; + } + + public static LicenseResult failure(String error) { + return new LicenseResult(false, error); + } + + public boolean isValid() { + return valid; + } + + public void setValid(boolean valid) { + this.valid = valid; + } + + public License getLicense() { + return license; + } + + public void setLicense(License license) { + this.license = license; + } + + public List getActivations() { + return activations; + } + + public void setActivations(List activations) { + this.activations = activations; + } + + public String getError() { + return error; + } + + public void setError(String error) { + this.error = error; + } + + public boolean isCached() { + return cached; + } + + public void setCached(boolean cached) { + this.cached = cached; + } + + @Override + public String toString() { + return "LicenseResult{valid=" + valid + ", error='" + error + "'}"; + } +} diff --git a/src/main/java/com/ironservices/licensing/LicenseStatus.java b/src/main/java/com/ironservices/licensing/LicenseStatus.java new file mode 100644 index 0000000..3e8584b --- /dev/null +++ b/src/main/java/com/ironservices/licensing/LicenseStatus.java @@ -0,0 +1,54 @@ +package com.ironservices.licensing; + +import com.google.gson.annotations.SerializedName; + +/** + * Represents the status of a license. + */ +public enum LicenseStatus { + @SerializedName("valid") + VALID("valid"), + + @SerializedName("expired") + EXPIRED("expired"), + + @SerializedName("suspended") + SUSPENDED("suspended"), + + @SerializedName("revoked") + REVOKED("revoked"), + + @SerializedName("invalid") + INVALID("invalid"), + + @SerializedName("trial") + TRIAL("trial"), + + @SerializedName("trial_expired") + TRIAL_EXPIRED("trial_expired"), + + @SerializedName("not_activated") + NOT_ACTIVATED("not_activated"), + + @SerializedName("unknown") + UNKNOWN("unknown"); + + private final String value; + + LicenseStatus(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public static LicenseStatus fromValue(String value) { + for (LicenseStatus status : values()) { + if (status.value.equals(value)) { + return status; + } + } + return UNKNOWN; + } +} diff --git a/src/main/java/com/ironservices/licensing/LicenseType.java b/src/main/java/com/ironservices/licensing/LicenseType.java new file mode 100644 index 0000000..5d0a3a4 --- /dev/null +++ b/src/main/java/com/ironservices/licensing/LicenseType.java @@ -0,0 +1,36 @@ +package com.ironservices.licensing; + +import com.google.gson.annotations.SerializedName; + +/** + * Represents the type of a license. + */ +public enum LicenseType { + @SerializedName("perpetual") + PERPETUAL("perpetual"), + + @SerializedName("subscription") + SUBSCRIPTION("subscription"), + + @SerializedName("trial") + TRIAL("trial"); + + private final String value; + + LicenseType(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public static LicenseType fromValue(String value) { + for (LicenseType type : values()) { + if (type.value.equals(value)) { + return type; + } + } + return PERPETUAL; + } +} diff --git a/src/main/java/com/ironservices/licensing/ProductTier.java b/src/main/java/com/ironservices/licensing/ProductTier.java new file mode 100644 index 0000000..3ba9520 --- /dev/null +++ b/src/main/java/com/ironservices/licensing/ProductTier.java @@ -0,0 +1,104 @@ +package com.ironservices.licensing; + +import com.google.gson.annotations.SerializedName; +import java.util.List; + +/** + * Represents a product tier available for purchase. + */ +public class ProductTier { + @SerializedName("id") + private String id; + + @SerializedName("slug") + private String slug; + + @SerializedName("name") + private String name; + + @SerializedName("description") + private String description; + + @SerializedName("price") + private double price; + + @SerializedName("currency") + private String currency; + + @SerializedName("billingPeriod") + private String billingPeriod; + + @SerializedName("features") + private List features; + + public ProductTier() {} + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getSlug() { + return slug; + } + + public void setSlug(String slug) { + this.slug = slug; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public double getPrice() { + return price; + } + + public void setPrice(double price) { + this.price = price; + } + + public String getCurrency() { + return currency; + } + + public void setCurrency(String currency) { + this.currency = currency; + } + + public String getBillingPeriod() { + return billingPeriod; + } + + public void setBillingPeriod(String billingPeriod) { + this.billingPeriod = billingPeriod; + } + + public List getFeatures() { + return features; + } + + public void setFeatures(List features) { + this.features = features; + } + + @Override + public String toString() { + return "ProductTier{id='" + id + "', name='" + name + "', price=" + price + " " + currency + "}"; + } +} diff --git a/src/main/java/com/ironservices/licensing/Transport.java b/src/main/java/com/ironservices/licensing/Transport.java new file mode 100644 index 0000000..e0d2584 --- /dev/null +++ b/src/main/java/com/ironservices/licensing/Transport.java @@ -0,0 +1,220 @@ +package com.ironservices.licensing; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import okhttp3.*; + +import java.io.*; +import java.lang.reflect.Type; +import java.net.InetAddress; +import java.nio.file.*; +import java.util.*; +import java.util.concurrent.TimeUnit; + +/** + * HTTP transport layer for IronLicensing API. + */ +class Transport { + private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); + + private final String baseUrl; + private final String publicKey; + private final String productSlug; + private final boolean debug; + private final OkHttpClient httpClient; + private final Gson gson; + private final String machineId; + + Transport(LicenseOptions options) { + this.baseUrl = options.getApiBaseUrl(); + this.publicKey = options.getPublicKey(); + this.productSlug = options.getProductSlug(); + this.debug = options.isDebug(); + this.httpClient = new OkHttpClient.Builder() + .connectTimeout(options.getHttpTimeout().toMillis(), TimeUnit.MILLISECONDS) + .readTimeout(options.getHttpTimeout().toMillis(), TimeUnit.MILLISECONDS) + .writeTimeout(options.getHttpTimeout().toMillis(), TimeUnit.MILLISECONDS) + .build(); + this.gson = new GsonBuilder().create(); + this.machineId = getOrCreateMachineId(); + } + + private void log(String message) { + if (debug) { + System.out.println("[IronLicensing] " + message); + } + } + + private String getOrCreateMachineId() { + try { + Path idPath = Paths.get(System.getProperty("user.home"), ".ironlicensing", "machine_id"); + if (Files.exists(idPath)) { + return new String(Files.readAllBytes(idPath)).trim(); + } + String id = UUID.randomUUID().toString(); + Files.createDirectories(idPath.getParent()); + Files.write(idPath, id.getBytes()); + return id; + } catch (IOException e) { + return UUID.randomUUID().toString(); + } + } + + String getMachineId() { + return machineId; + } + + private String getHostname() { + try { + return InetAddress.getLocalHost().getHostName(); + } catch (Exception e) { + return "unknown"; + } + } + + private String getPlatform() { + String os = System.getProperty("os.name", "").toLowerCase(); + if (os.contains("win")) return "windows"; + if (os.contains("mac")) return "macos"; + if (os.contains("nix") || os.contains("nux")) return "linux"; + return os; + } + + private Request.Builder createRequest(String path) { + return new Request.Builder() + .url(baseUrl + path) + .addHeader("Content-Type", "application/json") + .addHeader("X-Public-Key", publicKey) + .addHeader("X-Product-Slug", productSlug); + } + + LicenseResult validate(String licenseKey) { + log("Validating: " + licenseKey.substring(0, Math.min(10, licenseKey.length())) + "..."); + + Map body = new HashMap<>(); + body.put("licenseKey", licenseKey); + body.put("machineId", machineId); + + Request request = createRequest("/api/v1/validate") + .post(RequestBody.create(gson.toJson(body), JSON)) + .build(); + + return executeRequest(request); + } + + LicenseResult activate(String licenseKey, String machineName) { + log("Activating: " + licenseKey.substring(0, Math.min(10, licenseKey.length())) + "..."); + + if (machineName == null || machineName.isEmpty()) { + machineName = getHostname(); + } + + Map body = new HashMap<>(); + body.put("licenseKey", licenseKey); + body.put("machineId", machineId); + body.put("machineName", machineName); + body.put("platform", getPlatform()); + + Request request = createRequest("/api/v1/activate") + .post(RequestBody.create(gson.toJson(body), JSON)) + .build(); + + return executeRequest(request); + } + + boolean deactivate(String licenseKey) { + log("Deactivating license"); + + Map body = new HashMap<>(); + body.put("licenseKey", licenseKey); + body.put("machineId", machineId); + + Request request = createRequest("/api/v1/deactivate") + .post(RequestBody.create(gson.toJson(body), JSON)) + .build(); + + try (Response response = httpClient.newCall(request).execute()) { + return response.isSuccessful(); + } catch (IOException e) { + log("Deactivation failed: " + e.getMessage()); + return false; + } + } + + LicenseResult startTrial(String email) { + log("Starting trial for: " + email); + + Map body = new HashMap<>(); + body.put("email", email); + body.put("machineId", machineId); + + Request request = createRequest("/api/v1/trial") + .post(RequestBody.create(gson.toJson(body), JSON)) + .build(); + + return executeRequest(request); + } + + List getTiers() { + log("Fetching product tiers"); + + Request request = createRequest("/api/v1/tiers") + .get() + .build(); + + try (Response response = httpClient.newCall(request).execute()) { + if (response.isSuccessful() && response.body() != null) { + String json = response.body().string(); + Type type = new TypeToken>>(){}.getType(); + Map> result = gson.fromJson(json, type); + return result.getOrDefault("tiers", Collections.emptyList()); + } + } catch (IOException e) { + log("Failed to fetch tiers: " + e.getMessage()); + } + return Collections.emptyList(); + } + + CheckoutResult startCheckout(String tierId, String email) { + log("Starting checkout for tier: " + tierId); + + Map body = new HashMap<>(); + body.put("tierId", tierId); + body.put("email", email); + + Request request = createRequest("/api/v1/checkout") + .post(RequestBody.create(gson.toJson(body), JSON)) + .build(); + + try (Response response = httpClient.newCall(request).execute()) { + String json = response.body() != null ? response.body().string() : "{}"; + if (response.isSuccessful()) { + CheckoutResult result = gson.fromJson(json, CheckoutResult.class); + result.setSuccess(true); + return result; + } else { + Map errorResponse = gson.fromJson(json, + new TypeToken>(){}.getType()); + return CheckoutResult.failure(errorResponse.getOrDefault("error", "Checkout failed")); + } + } catch (IOException e) { + return CheckoutResult.failure(e.getMessage()); + } + } + + private LicenseResult executeRequest(Request request) { + try (Response response = httpClient.newCall(request).execute()) { + String json = response.body() != null ? response.body().string() : "{}"; + if (response.isSuccessful()) { + return gson.fromJson(json, LicenseResult.class); + } else { + Map errorResponse = gson.fromJson(json, + new TypeToken>(){}.getType()); + return LicenseResult.failure(errorResponse.getOrDefault("error", "Request failed")); + } + } catch (IOException e) { + return LicenseResult.failure(e.getMessage()); + } + } +}