Implement IronLicensing Java SDK

- Add LicenseClient with validation, activation, deactivation
- Add feature checking with hasFeature/requireFeature pattern
- Add trial management and in-app purchase support
- Add CompletableFuture for async operations
- Add thread-safe operations with ReadWriteLock
- Add IronLicensing static facade for global usage
- Add machine ID persistence for activation tracking

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
David Friedel 2025-12-25 11:41:51 +00:00
parent d30fd605bc
commit c88671da39
16 changed files with 2178 additions and 2 deletions

60
.gitignore vendored Normal file
View File

@ -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/

360
README.md
View File

@ -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
<dependency>
<groupId>com.ironservices</groupId>
<artifactId>licensing</artifactId>
<version>1.0.0</version>
</dependency>
```
### 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<ProductTier> 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.

98
pom.xml Normal file
View File

@ -0,0 +1,98 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.ironservices</groupId>
<artifactId>licensing</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>IronLicensing Java SDK</name>
<description>Official Java SDK for IronLicensing - Software licensing and activation</description>
<url>https://github.com/IronServices/ironlicensing-java</url>
<licenses>
<license>
<name>MIT License</name>
<url>https://opensource.org/licenses/MIT</url>
</license>
</licenses>
<developers>
<developer>
<name>IronServices</name>
<email>support@ironservices.com</email>
<organization>IronServices</organization>
<organizationUrl>https://ironservices.com</organizationUrl>
</developer>
</developers>
<scm>
<connection>scm:git:git://github.com/IronServices/ironlicensing-java.git</connection>
<developerConnection>scm:git:ssh://github.com:IronServices/ironlicensing-java.git</developerConnection>
<url>https://github.com/IronServices/ironlicensing-java/tree/main</url>
</scm>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.3.0</version>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.6.0</version>
<executions>
<execution>
<id>attach-javadocs</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@ -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 + "'}";
}
}

View File

@ -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 + "'}";
}
}

View File

@ -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<String, Object> 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<String, Object> getMetadata() {
return metadata;
}
public void setMetadata(Map<String, Object> metadata) {
this.metadata = metadata;
}
@Override
public String toString() {
return "Feature{key='" + key + "', name='" + name + "', enabled=" + enabled + "}";
}
}

View File

@ -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<LicenseResult> 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<ProductTier> 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<License> listener) {
requireClient().setOnLicenseChanged(listener);
}
}

View File

@ -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<Feature> 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<String, Object> 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<Feature> getFeatures() {
return features;
}
public void setFeatures(List<Feature> 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<String, Object> getMetadata() {
return metadata;
}
public void setMetadata(Map<String, Object> 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 + "}";
}
}

View File

@ -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<License> 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<License> 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<LicenseResult> 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<LicenseResult> 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<Boolean> 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<LicenseResult> 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<ProductTier> getTiers() {
return transport.getTiers();
}
/**
* Gets available product tiers asynchronously.
*
* @return A CompletableFuture with the list of tiers
*/
public CompletableFuture<List<ProductTier>> 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<CheckoutResult> 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());
}
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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<Activation> 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<Activation> getActivations() {
return activations;
}
public void setActivations(List<Activation> 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 + "'}";
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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<Feature> 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<Feature> getFeatures() {
return features;
}
public void setFeatures(List<Feature> features) {
this.features = features;
}
@Override
public String toString() {
return "ProductTier{id='" + id + "', name='" + name + "', price=" + price + " " + currency + "}";
}
}

View File

@ -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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<ProductTier> 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<Map<String, List<ProductTier>>>(){}.getType();
Map<String, List<ProductTier>> 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<String, String> 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<String, String> errorResponse = gson.fromJson(json,
new TypeToken<Map<String, String>>(){}.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<String, String> errorResponse = gson.fromJson(json,
new TypeToken<Map<String, String>>(){}.getType());
return LicenseResult.failure(errorResponse.getOrDefault("error", "Request failed"));
}
} catch (IOException e) {
return LicenseResult.failure(e.getMessage());
}
}
}