mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-06-23 16:05:09 +00:00
Update AGENTS guidelines (#3556)
## Summary - clarify Codex contribution instructions - remove `test.sh` reference and require `./gradlew build` - add Developer Guide, AI note and translation policy ## Testing - `./gradlew spotlessApply` - `./gradlew build`
This commit is contained in:
parent
9fe49c494d
commit
218d21f07a
24
AGENTS.md
Normal file
24
AGENTS.md
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# Codex Contribution Guidelines for Stirling-PDF
|
||||||
|
|
||||||
|
This file provides high-level instructions for Codex when modifying any files within this repository. Follow these rules to ensure changes remain consistent with the existing project structure.
|
||||||
|
|
||||||
|
## 1. Code Style and Formatting
|
||||||
|
- Respect the `.editorconfig` settings located in the repository root. Java files use 4 spaces; HTML, JS, and Python generally use 2 spaces. Lines should end with `LF`.
|
||||||
|
- Format Java code with `./gradlew spotlessApply` before committing.
|
||||||
|
- Review `DeveloperGuide.md` for project structure and design details before making significant changes.
|
||||||
|
|
||||||
|
## 2. Testing
|
||||||
|
- Run `./gradlew build` before committing changes to ensure the project compiles.
|
||||||
|
- If the build cannot complete due to environment restrictions, DO NOT COMMIT THE CHANGE
|
||||||
|
|
||||||
|
## 3. Commits
|
||||||
|
- Keep commits focused. Group related changes together and provide concise commit messages.
|
||||||
|
- Ensure the working tree is clean (`git status`) before concluding your work.
|
||||||
|
|
||||||
|
## 4. Pull Requests
|
||||||
|
- Summarize what was changed and why. Include build results from `./gradlew build` in the PR description.
|
||||||
|
- Note that the code was generated with the assistance of AI.
|
||||||
|
|
||||||
|
## 5. Translations
|
||||||
|
- Only modify `messages_en_GB.properties` when adding or updating translations.
|
||||||
|
|
@ -47,19 +47,20 @@ public class KeygenLicenseVerifier {
|
|||||||
|
|
||||||
private static final ObjectMapper objectMapper = new ObjectMapper();
|
private static final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
private final ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
// Shared HTTP client for connection pooling
|
// Shared HTTP client for connection pooling
|
||||||
private static final HttpClient httpClient = HttpClient.newBuilder()
|
private static final HttpClient httpClient =
|
||||||
.version(HttpClient.Version.HTTP_2)
|
HttpClient.newBuilder()
|
||||||
.connectTimeout(java.time.Duration.ofSeconds(10))
|
.version(HttpClient.Version.HTTP_2)
|
||||||
.build();
|
.connectTimeout(java.time.Duration.ofSeconds(10))
|
||||||
|
.build();
|
||||||
|
|
||||||
// License metadata context class to avoid shared mutable state
|
// License metadata context class to avoid shared mutable state
|
||||||
private static class LicenseContext {
|
private static class LicenseContext {
|
||||||
private boolean isFloatingLicense = false;
|
private boolean isFloatingLicense = false;
|
||||||
private int maxMachines = 1; // Default to 1 if not specified
|
private int maxMachines = 1; // Default to 1 if not specified
|
||||||
private boolean isEnterpriseLicense = false;
|
private boolean isEnterpriseLicense = false;
|
||||||
|
|
||||||
public LicenseContext() {}
|
public LicenseContext() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -248,7 +249,7 @@ public class KeygenLicenseVerifier {
|
|||||||
// Check for floating license
|
// Check for floating license
|
||||||
context.isFloatingLicense = attributesObj.optBoolean("floating", false);
|
context.isFloatingLicense = attributesObj.optBoolean("floating", false);
|
||||||
context.maxMachines = attributesObj.optInt("maxMachines", 1);
|
context.maxMachines = attributesObj.optInt("maxMachines", 1);
|
||||||
|
|
||||||
// Extract metadata
|
// Extract metadata
|
||||||
JSONObject metadataObj = attributesObj.optJSONObject("metadata");
|
JSONObject metadataObj = attributesObj.optJSONObject("metadata");
|
||||||
if (metadataObj != null) {
|
if (metadataObj != null) {
|
||||||
@ -411,14 +412,16 @@ public class KeygenLicenseVerifier {
|
|||||||
// Check for floating license in policy
|
// Check for floating license in policy
|
||||||
boolean policyFloating = policyObj.optBoolean("floating", false);
|
boolean policyFloating = policyObj.optBoolean("floating", false);
|
||||||
int policyMaxMachines = policyObj.optInt("maxMachines", 1);
|
int policyMaxMachines = policyObj.optInt("maxMachines", 1);
|
||||||
|
|
||||||
// Policy settings take precedence
|
// Policy settings take precedence
|
||||||
if (policyFloating) {
|
if (policyFloating) {
|
||||||
context.isFloatingLicense = true;
|
context.isFloatingLicense = true;
|
||||||
context.maxMachines = policyMaxMachines;
|
context.maxMachines = policyMaxMachines;
|
||||||
log.info("Policy defines floating license with max machines: {}", context.maxMachines);
|
log.info(
|
||||||
|
"Policy defines floating license with max machines: {}",
|
||||||
|
context.maxMachines);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract max users and isEnterprise from policy or metadata
|
// Extract max users and isEnterprise from policy or metadata
|
||||||
int users = policyObj.optInt("users", 1);
|
int users = policyObj.optInt("users", 1);
|
||||||
context.isEnterpriseLicense = policyObj.optBoolean("isEnterprise", false);
|
context.isEnterpriseLicense = policyObj.optBoolean("isEnterprise", false);
|
||||||
@ -474,7 +477,8 @@ public class KeygenLicenseVerifier {
|
|||||||
activateMachine(licenseKey, licenseId, machineFingerprint, context);
|
activateMachine(licenseKey, licenseId, machineFingerprint, context);
|
||||||
if (activated) {
|
if (activated) {
|
||||||
// Revalidate after activation
|
// Revalidate after activation
|
||||||
validationResponse = validateLicense(licenseKey, machineFingerprint, context);
|
validationResponse =
|
||||||
|
validateLicense(licenseKey, machineFingerprint, context);
|
||||||
isValid =
|
isValid =
|
||||||
validationResponse != null
|
validationResponse != null
|
||||||
&& validationResponse
|
&& validationResponse
|
||||||
@ -494,8 +498,8 @@ public class KeygenLicenseVerifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private JsonNode validateLicense(String licenseKey, String machineFingerprint, LicenseContext context)
|
private JsonNode validateLicense(
|
||||||
throws Exception {
|
String licenseKey, String machineFingerprint, LicenseContext context) throws Exception {
|
||||||
String requestBody =
|
String requestBody =
|
||||||
String.format(
|
String.format(
|
||||||
"{\"meta\":{\"key\":\"%s\",\"scope\":{\"fingerprint\":\"%s\"}}}",
|
"{\"meta\":{\"key\":\"%s\",\"scope\":{\"fingerprint\":\"%s\"}}}",
|
||||||
@ -514,7 +518,8 @@ public class KeygenLicenseVerifier {
|
|||||||
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
|
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
HttpResponse<String> response =
|
||||||
|
httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
log.info("ValidateLicenseResponse body: {}", response.body());
|
log.info("ValidateLicenseResponse body: {}", response.body());
|
||||||
JsonNode jsonResponse = objectMapper.readTree(response.body());
|
JsonNode jsonResponse = objectMapper.readTree(response.body());
|
||||||
if (response.statusCode() == 200) {
|
if (response.statusCode() == 200) {
|
||||||
@ -527,21 +532,23 @@ public class KeygenLicenseVerifier {
|
|||||||
log.info("License validity: " + isValid);
|
log.info("License validity: " + isValid);
|
||||||
log.info("Validation detail: " + detail);
|
log.info("Validation detail: " + detail);
|
||||||
log.info("Validation code: " + code);
|
log.info("Validation code: " + code);
|
||||||
|
|
||||||
// Check if the license itself has floating attribute
|
// Check if the license itself has floating attribute
|
||||||
JsonNode licenseAttrs = jsonResponse.path("data").path("attributes");
|
JsonNode licenseAttrs = jsonResponse.path("data").path("attributes");
|
||||||
if (!licenseAttrs.isMissingNode()) {
|
if (!licenseAttrs.isMissingNode()) {
|
||||||
context.isFloatingLicense = licenseAttrs.path("floating").asBoolean(false);
|
context.isFloatingLicense = licenseAttrs.path("floating").asBoolean(false);
|
||||||
context.maxMachines = licenseAttrs.path("maxMachines").asInt(1);
|
context.maxMachines = licenseAttrs.path("maxMachines").asInt(1);
|
||||||
|
|
||||||
log.info("License floating (from license): {}, maxMachines: {}",
|
log.info(
|
||||||
context.isFloatingLicense, context.maxMachines);
|
"License floating (from license): {}, maxMachines: {}",
|
||||||
|
context.isFloatingLicense,
|
||||||
|
context.maxMachines);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also check the policy for floating license support if included
|
// Also check the policy for floating license support if included
|
||||||
JsonNode includedNode = jsonResponse.path("included");
|
JsonNode includedNode = jsonResponse.path("included");
|
||||||
JsonNode policyNode = null;
|
JsonNode policyNode = null;
|
||||||
|
|
||||||
if (includedNode.isArray()) {
|
if (includedNode.isArray()) {
|
||||||
for (JsonNode node : includedNode) {
|
for (JsonNode node : includedNode) {
|
||||||
if ("policies".equals(node.path("type").asText())) {
|
if ("policies".equals(node.path("type").asText())) {
|
||||||
@ -550,20 +557,23 @@ public class KeygenLicenseVerifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (policyNode != null) {
|
if (policyNode != null) {
|
||||||
// Check if this is a floating license from policy
|
// Check if this is a floating license from policy
|
||||||
boolean policyFloating = policyNode.path("attributes").path("floating").asBoolean(false);
|
boolean policyFloating =
|
||||||
|
policyNode.path("attributes").path("floating").asBoolean(false);
|
||||||
int policyMaxMachines = policyNode.path("attributes").path("maxMachines").asInt(1);
|
int policyMaxMachines = policyNode.path("attributes").path("maxMachines").asInt(1);
|
||||||
|
|
||||||
// Policy takes precedence over license attributes
|
// Policy takes precedence over license attributes
|
||||||
if (policyFloating) {
|
if (policyFloating) {
|
||||||
context.isFloatingLicense = true;
|
context.isFloatingLicense = true;
|
||||||
context.maxMachines = policyMaxMachines;
|
context.maxMachines = policyMaxMachines;
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("License floating (from policy): {}, maxMachines: {}",
|
log.info(
|
||||||
context.isFloatingLicense, context.maxMachines);
|
"License floating (from policy): {}, maxMachines: {}",
|
||||||
|
context.isFloatingLicense,
|
||||||
|
context.maxMachines);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract user count, default to 1 if not specified
|
// Extract user count, default to 1 if not specified
|
||||||
@ -593,86 +603,104 @@ public class KeygenLicenseVerifier {
|
|||||||
return jsonResponse;
|
return jsonResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean activateMachine(String licenseKey, String licenseId, String machineFingerprint,
|
private boolean activateMachine(
|
||||||
LicenseContext context) throws Exception {
|
String licenseKey, String licenseId, String machineFingerprint, LicenseContext context)
|
||||||
|
throws Exception {
|
||||||
// For floating licenses, we first need to check if we need to deregister any machines
|
// For floating licenses, we first need to check if we need to deregister any machines
|
||||||
if (context.isFloatingLicense) {
|
if (context.isFloatingLicense) {
|
||||||
log.info("Processing floating license activation. Max machines allowed: {}", context.maxMachines);
|
log.info(
|
||||||
|
"Processing floating license activation. Max machines allowed: {}",
|
||||||
|
context.maxMachines);
|
||||||
|
|
||||||
// Get the current machines for this license
|
// Get the current machines for this license
|
||||||
JsonNode machinesResponse = fetchMachinesForLicense(licenseKey, licenseId);
|
JsonNode machinesResponse = fetchMachinesForLicense(licenseKey, licenseId);
|
||||||
if (machinesResponse != null) {
|
if (machinesResponse != null) {
|
||||||
JsonNode machines = machinesResponse.path("data");
|
JsonNode machines = machinesResponse.path("data");
|
||||||
int currentMachines = machines.size();
|
int currentMachines = machines.size();
|
||||||
|
|
||||||
log.info("Current machine count: {}, Max allowed: {}", currentMachines, context.maxMachines);
|
log.info(
|
||||||
|
"Current machine count: {}, Max allowed: {}",
|
||||||
|
currentMachines,
|
||||||
|
context.maxMachines);
|
||||||
|
|
||||||
// Check if the current fingerprint is already activated
|
// Check if the current fingerprint is already activated
|
||||||
boolean isCurrentMachineActivated = false;
|
boolean isCurrentMachineActivated = false;
|
||||||
String currentMachineId = null;
|
String currentMachineId = null;
|
||||||
|
|
||||||
for (JsonNode machine : machines) {
|
for (JsonNode machine : machines) {
|
||||||
if (machineFingerprint.equals(machine.path("attributes").path("fingerprint").asText())) {
|
if (machineFingerprint.equals(
|
||||||
|
machine.path("attributes").path("fingerprint").asText())) {
|
||||||
isCurrentMachineActivated = true;
|
isCurrentMachineActivated = true;
|
||||||
currentMachineId = machine.path("id").asText();
|
currentMachineId = machine.path("id").asText();
|
||||||
log.info("Current machine is already activated with ID: {}", currentMachineId);
|
log.info(
|
||||||
|
"Current machine is already activated with ID: {}",
|
||||||
|
currentMachineId);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the current machine is already activated, there's no need to do anything
|
// If the current machine is already activated, there's no need to do anything
|
||||||
if (isCurrentMachineActivated) {
|
if (isCurrentMachineActivated) {
|
||||||
log.info("Machine already activated. No action needed.");
|
log.info("Machine already activated. No action needed.");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we've reached the max machines limit, we need to deregister the oldest machine
|
// If we've reached the max machines limit, we need to deregister the oldest machine
|
||||||
if (currentMachines >= context.maxMachines) {
|
if (currentMachines >= context.maxMachines) {
|
||||||
log.info("Max machines reached. Deregistering oldest machine to make room for the new machine.");
|
log.info(
|
||||||
|
"Max machines reached. Deregistering oldest machine to make room for the new machine.");
|
||||||
|
|
||||||
// Find the oldest machine based on creation timestamp
|
// Find the oldest machine based on creation timestamp
|
||||||
if (machines.size() > 0) {
|
if (machines.size() > 0) {
|
||||||
// Find the machine with the oldest creation date
|
// Find the machine with the oldest creation date
|
||||||
String oldestMachineId = null;
|
String oldestMachineId = null;
|
||||||
java.time.Instant oldestTime = null;
|
java.time.Instant oldestTime = null;
|
||||||
|
|
||||||
for (JsonNode machine : machines) {
|
for (JsonNode machine : machines) {
|
||||||
String createdStr = machine.path("attributes").path("created").asText(null);
|
String createdStr =
|
||||||
|
machine.path("attributes").path("created").asText(null);
|
||||||
if (createdStr != null && !createdStr.isEmpty()) {
|
if (createdStr != null && !createdStr.isEmpty()) {
|
||||||
try {
|
try {
|
||||||
java.time.Instant createdTime = java.time.Instant.parse(createdStr);
|
java.time.Instant createdTime =
|
||||||
|
java.time.Instant.parse(createdStr);
|
||||||
if (oldestTime == null || createdTime.isBefore(oldestTime)) {
|
if (oldestTime == null || createdTime.isBefore(oldestTime)) {
|
||||||
oldestTime = createdTime;
|
oldestTime = createdTime;
|
||||||
oldestMachineId = machine.path("id").asText();
|
oldestMachineId = machine.path("id").asText();
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("Could not parse creation time for machine: {}", e.getMessage());
|
log.warn(
|
||||||
|
"Could not parse creation time for machine: {}",
|
||||||
|
e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we couldn't determine the oldest by timestamp, use the first one
|
// If we couldn't determine the oldest by timestamp, use the first one
|
||||||
if (oldestMachineId == null) {
|
if (oldestMachineId == null) {
|
||||||
log.warn("Could not determine oldest machine by timestamp, using first machine in list");
|
log.warn(
|
||||||
|
"Could not determine oldest machine by timestamp, using first machine in list");
|
||||||
oldestMachineId = machines.path(0).path("id").asText();
|
oldestMachineId = machines.path(0).path("id").asText();
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("Deregistering machine with ID: {}", oldestMachineId);
|
log.info("Deregistering machine with ID: {}", oldestMachineId);
|
||||||
|
|
||||||
boolean deregistered = deregisterMachine(licenseKey, oldestMachineId);
|
boolean deregistered = deregisterMachine(licenseKey, oldestMachineId);
|
||||||
if (!deregistered) {
|
if (!deregistered) {
|
||||||
log.error("Failed to deregister machine. Cannot proceed with activation.");
|
log.error(
|
||||||
|
"Failed to deregister machine. Cannot proceed with activation.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
log.info("Machine deregistered successfully. Proceeding with activation of new machine.");
|
log.info(
|
||||||
|
"Machine deregistered successfully. Proceeding with activation of new machine.");
|
||||||
} else {
|
} else {
|
||||||
log.error("License has reached machine limit but no machines were found to deregister. This is unexpected.");
|
log.error(
|
||||||
|
"License has reached machine limit but no machines were found to deregister. This is unexpected.");
|
||||||
// We'll still try to activate, but it might fail
|
// We'll still try to activate, but it might fail
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Proceed with machine activation
|
// Proceed with machine activation
|
||||||
String hostname;
|
String hostname;
|
||||||
try {
|
try {
|
||||||
@ -720,7 +748,8 @@ public class KeygenLicenseVerifier {
|
|||||||
.POST(HttpRequest.BodyPublishers.ofString(body.toString()))
|
.POST(HttpRequest.BodyPublishers.ofString(body.toString()))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
HttpResponse<String> response =
|
||||||
|
httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
log.info("activateMachine Response body: " + response.body());
|
log.info("activateMachine Response body: " + response.body());
|
||||||
if (response.statusCode() == 201) {
|
if (response.statusCode() == 201) {
|
||||||
log.info("Machine activated successfully");
|
log.info("Machine activated successfully");
|
||||||
@ -738,61 +767,76 @@ public class KeygenLicenseVerifier {
|
|||||||
private String generateMachineFingerprint() {
|
private String generateMachineFingerprint() {
|
||||||
return GeneralUtils.generateMachineFingerprint();
|
return GeneralUtils.generateMachineFingerprint();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches all machines associated with a specific license
|
* Fetches all machines associated with a specific license
|
||||||
*
|
*
|
||||||
* @param licenseKey The license key to check
|
* @param licenseKey The license key to check
|
||||||
* @param licenseId The license ID
|
* @param licenseId The license ID
|
||||||
* @return JsonNode containing the list of machines, or null if an error occurs
|
* @return JsonNode containing the list of machines, or null if an error occurs
|
||||||
* @throws Exception if an error occurs during the HTTP request
|
* @throws Exception if an error occurs during the HTTP request
|
||||||
*/
|
*/
|
||||||
private JsonNode fetchMachinesForLicense(String licenseKey, String licenseId) throws Exception {
|
private JsonNode fetchMachinesForLicense(String licenseKey, String licenseId) throws Exception {
|
||||||
HttpRequest request = HttpRequest.newBuilder()
|
HttpRequest request =
|
||||||
.uri(URI.create(BASE_URL + "/" + ACCOUNT_ID + "/licenses/" + licenseId + "/machines"))
|
HttpRequest.newBuilder()
|
||||||
.header("Content-Type", "application/vnd.api+json")
|
.uri(
|
||||||
.header("Accept", "application/vnd.api+json")
|
URI.create(
|
||||||
.header("Authorization", "License " + licenseKey)
|
BASE_URL
|
||||||
.GET()
|
+ "/"
|
||||||
.build();
|
+ ACCOUNT_ID
|
||||||
|
+ "/licenses/"
|
||||||
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
+ licenseId
|
||||||
|
+ "/machines"))
|
||||||
|
.header("Content-Type", "application/vnd.api+json")
|
||||||
|
.header("Accept", "application/vnd.api+json")
|
||||||
|
.header("Authorization", "License " + licenseKey)
|
||||||
|
.GET()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
HttpResponse<String> response =
|
||||||
|
httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
log.info("fetchMachinesForLicense Response body: {}", response.body());
|
log.info("fetchMachinesForLicense Response body: {}", response.body());
|
||||||
|
|
||||||
if (response.statusCode() == 200) {
|
if (response.statusCode() == 200) {
|
||||||
return objectMapper.readTree(response.body());
|
return objectMapper.readTree(response.body());
|
||||||
} else {
|
} else {
|
||||||
log.error("Error fetching machines for license. Status code: {}, error: {}",
|
log.error(
|
||||||
response.statusCode(), response.body());
|
"Error fetching machines for license. Status code: {}, error: {}",
|
||||||
|
response.statusCode(),
|
||||||
|
response.body());
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deregisters a machine from a license
|
* Deregisters a machine from a license
|
||||||
*
|
*
|
||||||
* @param licenseKey The license key
|
* @param licenseKey The license key
|
||||||
* @param machineId The ID of the machine to deregister
|
* @param machineId The ID of the machine to deregister
|
||||||
* @return true if deregistration was successful, false otherwise
|
* @return true if deregistration was successful, false otherwise
|
||||||
*/
|
*/
|
||||||
private boolean deregisterMachine(String licenseKey, String machineId) {
|
private boolean deregisterMachine(String licenseKey, String machineId) {
|
||||||
try {
|
try {
|
||||||
HttpRequest request = HttpRequest.newBuilder()
|
HttpRequest request =
|
||||||
.uri(URI.create(BASE_URL + "/" + ACCOUNT_ID + "/machines/" + machineId))
|
HttpRequest.newBuilder()
|
||||||
.header("Content-Type", "application/vnd.api+json")
|
.uri(URI.create(BASE_URL + "/" + ACCOUNT_ID + "/machines/" + machineId))
|
||||||
.header("Accept", "application/vnd.api+json")
|
.header("Content-Type", "application/vnd.api+json")
|
||||||
.header("Authorization", "License " + licenseKey)
|
.header("Accept", "application/vnd.api+json")
|
||||||
.DELETE()
|
.header("Authorization", "License " + licenseKey)
|
||||||
.build();
|
.DELETE()
|
||||||
|
.build();
|
||||||
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
|
||||||
|
HttpResponse<String> response =
|
||||||
|
httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
|
|
||||||
if (response.statusCode() == 204) {
|
if (response.statusCode() == 204) {
|
||||||
log.info("Machine {} successfully deregistered", machineId);
|
log.info("Machine {} successfully deregistered", machineId);
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
log.error("Error deregistering machine. Status code: {}, error: {}",
|
log.error(
|
||||||
response.statusCode(), response.body());
|
"Error deregistering machine. Status code: {}, error: {}",
|
||||||
|
response.statusCode(),
|
||||||
|
response.body());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
@ -1,18 +1,17 @@
|
|||||||
package stirling.software.SPDF.service;
|
package stirling.software.SPDF.service;
|
||||||
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
import static org.mockito.Mockito.*;
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.nio.file.*;
|
import java.nio.file.*;
|
||||||
|
import java.nio.file.Files;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
||||||
import org.apache.pdfbox.Loader;
|
import org.apache.pdfbox.Loader;
|
||||||
|
import org.apache.pdfbox.cos.COSName;
|
||||||
import org.apache.pdfbox.pdmodel.*;
|
import org.apache.pdfbox.pdmodel.*;
|
||||||
import org.apache.pdfbox.pdmodel.common.PDStream;
|
import org.apache.pdfbox.pdmodel.common.PDStream;
|
||||||
import org.aspectj.lang.annotation.Before;
|
|
||||||
import org.apache.pdfbox.cos.COSName;
|
|
||||||
import org.junit.jupiter.api.*;
|
import org.junit.jupiter.api.*;
|
||||||
import org.junit.jupiter.api.parallel.Execution;
|
import org.junit.jupiter.api.parallel.Execution;
|
||||||
import org.junit.jupiter.api.parallel.ExecutionMode;
|
import org.junit.jupiter.api.parallel.ExecutionMode;
|
||||||
@ -25,7 +24,7 @@ import stirling.software.SPDF.service.SpyPDFDocumentFactory.StrategyType;
|
|||||||
|
|
||||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
||||||
@Execution(value = ExecutionMode.SAME_THREAD)
|
@Execution(value = ExecutionMode.SAME_THREAD)
|
||||||
class CustomPDFDocumentFactoryTest {
|
class CustomPDFDocumentFactoryTest {
|
||||||
|
|
||||||
private SpyPDFDocumentFactory factory;
|
private SpyPDFDocumentFactory factory;
|
||||||
@ -43,12 +42,7 @@ class CustomPDFDocumentFactoryTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@CsvSource({
|
@CsvSource({"5,MEMORY_ONLY", "20,MIXED", "60,TEMP_FILE"})
|
||||||
"5,MEMORY_ONLY",
|
|
||||||
"20,MIXED",
|
|
||||||
"60,TEMP_FILE"
|
|
||||||
|
|
||||||
})
|
|
||||||
void testStrategy_FileInput(int sizeMB, StrategyType expected) throws IOException {
|
void testStrategy_FileInput(int sizeMB, StrategyType expected) throws IOException {
|
||||||
File file = writeTempFile(inflatePdf(basePdfBytes, sizeMB));
|
File file = writeTempFile(inflatePdf(basePdfBytes, sizeMB));
|
||||||
try (PDDocument doc = factory.load(file)) {
|
try (PDDocument doc = factory.load(file)) {
|
||||||
@ -57,12 +51,7 @@ class CustomPDFDocumentFactoryTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@CsvSource({
|
@CsvSource({"5,MEMORY_ONLY", "20,MIXED", "60,TEMP_FILE"})
|
||||||
"5,MEMORY_ONLY",
|
|
||||||
"20,MIXED",
|
|
||||||
"60,TEMP_FILE"
|
|
||||||
|
|
||||||
})
|
|
||||||
void testStrategy_ByteArray(int sizeMB, StrategyType expected) throws IOException {
|
void testStrategy_ByteArray(int sizeMB, StrategyType expected) throws IOException {
|
||||||
byte[] inflated = inflatePdf(basePdfBytes, sizeMB);
|
byte[] inflated = inflatePdf(basePdfBytes, sizeMB);
|
||||||
try (PDDocument doc = factory.load(inflated)) {
|
try (PDDocument doc = factory.load(inflated)) {
|
||||||
@ -71,12 +60,7 @@ class CustomPDFDocumentFactoryTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@CsvSource({
|
@CsvSource({"5,MEMORY_ONLY", "20,MIXED", "60,TEMP_FILE"})
|
||||||
"5,MEMORY_ONLY",
|
|
||||||
"20,MIXED",
|
|
||||||
"60,TEMP_FILE"
|
|
||||||
|
|
||||||
})
|
|
||||||
void testStrategy_InputStream(int sizeMB, StrategyType expected) throws IOException {
|
void testStrategy_InputStream(int sizeMB, StrategyType expected) throws IOException {
|
||||||
byte[] inflated = inflatePdf(basePdfBytes, sizeMB);
|
byte[] inflated = inflatePdf(basePdfBytes, sizeMB);
|
||||||
try (PDDocument doc = factory.load(new ByteArrayInputStream(inflated))) {
|
try (PDDocument doc = factory.load(new ByteArrayInputStream(inflated))) {
|
||||||
@ -85,30 +69,22 @@ class CustomPDFDocumentFactoryTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@CsvSource({
|
@CsvSource({"5,MEMORY_ONLY", "20,MIXED", "60,TEMP_FILE"})
|
||||||
"5,MEMORY_ONLY",
|
|
||||||
"20,MIXED",
|
|
||||||
"60,TEMP_FILE"
|
|
||||||
|
|
||||||
})
|
|
||||||
void testStrategy_MultipartFile(int sizeMB, StrategyType expected) throws IOException {
|
void testStrategy_MultipartFile(int sizeMB, StrategyType expected) throws IOException {
|
||||||
byte[] inflated = inflatePdf(basePdfBytes, sizeMB);
|
byte[] inflated = inflatePdf(basePdfBytes, sizeMB);
|
||||||
MockMultipartFile multipart = new MockMultipartFile("file", "doc.pdf", "application/pdf", inflated);
|
MockMultipartFile multipart =
|
||||||
|
new MockMultipartFile("file", "doc.pdf", "application/pdf", inflated);
|
||||||
try (PDDocument doc = factory.load(multipart)) {
|
try (PDDocument doc = factory.load(multipart)) {
|
||||||
assertEquals(expected, factory.lastStrategyUsed);
|
assertEquals(expected, factory.lastStrategyUsed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@CsvSource({
|
@CsvSource({"5,MEMORY_ONLY", "20,MIXED", "60,TEMP_FILE"})
|
||||||
"5,MEMORY_ONLY",
|
|
||||||
"20,MIXED",
|
|
||||||
"60,TEMP_FILE"
|
|
||||||
|
|
||||||
})
|
|
||||||
void testStrategy_PDFFile(int sizeMB, StrategyType expected) throws IOException {
|
void testStrategy_PDFFile(int sizeMB, StrategyType expected) throws IOException {
|
||||||
byte[] inflated = inflatePdf(basePdfBytes, sizeMB);
|
byte[] inflated = inflatePdf(basePdfBytes, sizeMB);
|
||||||
MockMultipartFile multipart = new MockMultipartFile("file", "doc.pdf", "application/pdf", inflated);
|
MockMultipartFile multipart =
|
||||||
|
new MockMultipartFile("file", "doc.pdf", "application/pdf", inflated);
|
||||||
PDFFile pdfFile = new PDFFile();
|
PDFFile pdfFile = new PDFFile();
|
||||||
pdfFile.setFileInput(multipart);
|
pdfFile.setFileInput(multipart);
|
||||||
try (PDDocument doc = factory.load(pdfFile)) {
|
try (PDDocument doc = factory.load(pdfFile)) {
|
||||||
@ -125,14 +101,16 @@ class CustomPDFDocumentFactoryTest {
|
|||||||
stream.getCOSObject().setItem(COSName.TYPE, COSName.XOBJECT);
|
stream.getCOSObject().setItem(COSName.TYPE, COSName.XOBJECT);
|
||||||
stream.getCOSObject().setItem(COSName.SUBTYPE, COSName.IMAGE);
|
stream.getCOSObject().setItem(COSName.SUBTYPE, COSName.IMAGE);
|
||||||
|
|
||||||
doc.getDocumentCatalog().getCOSObject().setItem(COSName.getPDFName("DummyBigStream"), stream.getCOSObject());
|
doc.getDocumentCatalog()
|
||||||
|
.getCOSObject()
|
||||||
|
.setItem(COSName.getPDFName("DummyBigStream"), stream.getCOSObject());
|
||||||
|
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
doc.save(out);
|
doc.save(out);
|
||||||
return out.toByteArray();
|
return out.toByteArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testLoadFromPath() throws IOException {
|
void testLoadFromPath() throws IOException {
|
||||||
File file = writeTempFile(inflatePdf(basePdfBytes, 5));
|
File file = writeTempFile(inflatePdf(basePdfBytes, 5));
|
||||||
@ -151,29 +129,29 @@ class CustomPDFDocumentFactoryTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// neeed to add password pdf
|
// neeed to add password pdf
|
||||||
// @Test
|
// @Test
|
||||||
// void testLoadPasswordProtectedPdfFromInputStream() throws IOException {
|
// void testLoadPasswordProtectedPdfFromInputStream() throws IOException {
|
||||||
// try (InputStream is = getClass().getResourceAsStream("/protected.pdf")) {
|
// try (InputStream is = getClass().getResourceAsStream("/protected.pdf")) {
|
||||||
// assertNotNull(is, "protected.pdf must be present in src/test/resources");
|
// assertNotNull(is, "protected.pdf must be present in src/test/resources");
|
||||||
// try (PDDocument doc = factory.load(is, "test123")) {
|
// try (PDDocument doc = factory.load(is, "test123")) {
|
||||||
// assertNotNull(doc);
|
// assertNotNull(doc);
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// @Test
|
// @Test
|
||||||
// void testLoadPasswordProtectedPdfFromMultipart() throws IOException {
|
// void testLoadPasswordProtectedPdfFromMultipart() throws IOException {
|
||||||
// try (InputStream is = getClass().getResourceAsStream("/protected.pdf")) {
|
// try (InputStream is = getClass().getResourceAsStream("/protected.pdf")) {
|
||||||
// assertNotNull(is, "protected.pdf must be present in src/test/resources");
|
// assertNotNull(is, "protected.pdf must be present in src/test/resources");
|
||||||
// byte[] bytes = is.readAllBytes();
|
// byte[] bytes = is.readAllBytes();
|
||||||
// MockMultipartFile file = new MockMultipartFile("file", "protected.pdf", "application/pdf", bytes);
|
// MockMultipartFile file = new MockMultipartFile("file", "protected.pdf",
|
||||||
// try (PDDocument doc = factory.load(file, "test123")) {
|
// "application/pdf", bytes);
|
||||||
// assertNotNull(doc);
|
// try (PDDocument doc = factory.load(file, "test123")) {
|
||||||
// }
|
// assertNotNull(doc);
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testLoadReadOnlySkipsPostProcessing() throws IOException {
|
void testLoadReadOnlySkipsPostProcessing() throws IOException {
|
||||||
PdfMetadataService mockService = mock(PdfMetadataService.class);
|
PdfMetadataService mockService = mock(PdfMetadataService.class);
|
||||||
@ -186,7 +164,6 @@ class CustomPDFDocumentFactoryTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testCreateNewDocument() throws IOException {
|
void testCreateNewDocument() throws IOException {
|
||||||
try (PDDocument doc = factory.createNewDocument()) {
|
try (PDDocument doc = factory.createNewDocument()) {
|
||||||
@ -198,7 +175,7 @@ class CustomPDFDocumentFactoryTest {
|
|||||||
void testCreateNewDocumentBasedOnOldDocument() throws IOException {
|
void testCreateNewDocumentBasedOnOldDocument() throws IOException {
|
||||||
byte[] inflated = inflatePdf(basePdfBytes, 5);
|
byte[] inflated = inflatePdf(basePdfBytes, 5);
|
||||||
try (PDDocument oldDoc = Loader.loadPDF(inflated);
|
try (PDDocument oldDoc = Loader.loadPDF(inflated);
|
||||||
PDDocument newDoc = factory.createNewDocumentBasedOnOldDocument(oldDoc)) {
|
PDDocument newDoc = factory.createNewDocumentBasedOnOldDocument(oldDoc)) {
|
||||||
assertNotNull(newDoc);
|
assertNotNull(newDoc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -241,7 +218,6 @@ class CustomPDFDocumentFactoryTest {
|
|||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void cleanup() {
|
void cleanup() {
|
||||||
System.gc();
|
System.gc();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
package stirling.software.SPDF.service;
|
package stirling.software.SPDF.service;
|
||||||
|
|
||||||
import org.apache.pdfbox.io.RandomAccessStreamCache.StreamCacheCreateFunction;
|
import org.apache.pdfbox.io.RandomAccessStreamCache.StreamCacheCreateFunction;
|
||||||
|
|
||||||
import stirling.software.SPDF.service.CustomPDFDocumentFactory;
|
|
||||||
import stirling.software.SPDF.service.PdfMetadataService;
|
|
||||||
|
|
||||||
class SpyPDFDocumentFactory extends CustomPDFDocumentFactory {
|
class SpyPDFDocumentFactory extends CustomPDFDocumentFactory {
|
||||||
enum StrategyType {
|
enum StrategyType {
|
||||||
MEMORY_ONLY, MIXED, TEMP_FILE
|
MEMORY_ONLY,
|
||||||
}
|
MIXED,
|
||||||
|
TEMP_FILE
|
||||||
|
}
|
||||||
|
|
||||||
public StrategyType lastStrategyUsed;
|
public StrategyType lastStrategyUsed;
|
||||||
|
|
||||||
public SpyPDFDocumentFactory(PdfMetadataService service) {
|
public SpyPDFDocumentFactory(PdfMetadataService service) {
|
||||||
@ -28,4 +28,4 @@ class SpyPDFDocumentFactory extends CustomPDFDocumentFactory {
|
|||||||
this.lastStrategyUsed = type;
|
this.lastStrategyUsed = type;
|
||||||
return super.getStreamCacheFunction(contentSize); // delegate to real behavior
|
return super.getStreamCacheFunction(contentSize); // delegate to real behavior
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,25 +27,28 @@ class CustomHtmlSanitizerTest {
|
|||||||
|
|
||||||
private static Stream<Arguments> provideHtmlTestCases() {
|
private static Stream<Arguments> provideHtmlTestCases() {
|
||||||
return Stream.of(
|
return Stream.of(
|
||||||
Arguments.of(
|
Arguments.of(
|
||||||
"<p>This is <strong>valid</strong> HTML with <em>formatting</em>.</p>",
|
"<p>This is <strong>valid</strong> HTML with <em>formatting</em>.</p>",
|
||||||
new String[] {"<p>", "<strong>", "<em>"}
|
new String[] {"<p>", "<strong>", "<em>"}),
|
||||||
),
|
Arguments.of(
|
||||||
Arguments.of(
|
"<p>Text with <b>bold</b>, <i>italic</i>, <u>underline</u>, "
|
||||||
"<p>Text with <b>bold</b>, <i>italic</i>, <u>underline</u>, "
|
+ "<em>emphasis</em>, <strong>strong</strong>, <strike>strikethrough</strike>, "
|
||||||
+ "<em>emphasis</em>, <strong>strong</strong>, <strike>strikethrough</strike>, "
|
+ "<s>strike</s>, <sub>subscript</sub>, <sup>superscript</sup>, "
|
||||||
+ "<s>strike</s>, <sub>subscript</sub>, <sup>superscript</sup>, "
|
+ "<tt>teletype</tt>, <code>code</code>, <big>big</big>, <small>small</small>.</p>",
|
||||||
+ "<tt>teletype</tt>, <code>code</code>, <big>big</big>, <small>small</small>.</p>",
|
new String[] {
|
||||||
new String[] {"<b>bold</b>", "<i>italic</i>", "<em>emphasis</em>", "<strong>strong</strong>"}
|
"<b>bold</b>",
|
||||||
),
|
"<i>italic</i>",
|
||||||
Arguments.of(
|
"<em>emphasis</em>",
|
||||||
"<div>Division</div><h1>Heading 1</h1><h2>Heading 2</h2><h3>Heading 3</h3>"
|
"<strong>strong</strong>"
|
||||||
+ "<h4>Heading 4</h4><h5>Heading 5</h5><h6>Heading 6</h6>"
|
}),
|
||||||
+ "<blockquote>Blockquote</blockquote><ul><li>List item</li></ul>"
|
Arguments.of(
|
||||||
+ "<ol><li>Ordered item</li></ol>",
|
"<div>Division</div><h1>Heading 1</h1><h2>Heading 2</h2><h3>Heading 3</h3>"
|
||||||
new String[] {"<div>", "<h1>", "<h6>", "<blockquote>", "<ul>", "<ol>", "<li>"}
|
+ "<h4>Heading 4</h4><h5>Heading 5</h5><h6>Heading 6</h6>"
|
||||||
)
|
+ "<blockquote>Blockquote</blockquote><ul><li>List item</li></ul>"
|
||||||
);
|
+ "<ol><li>Ordered item</li></ol>",
|
||||||
|
new String[] {
|
||||||
|
"<div>", "<h1>", "<h6>", "<blockquote>", "<ul>", "<ol>", "<li>"
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package stirling.software.SPDF.utils;
|
package stirling.software.SPDF.utils;
|
||||||
|
|
||||||
import io.github.pixee.security.ZipSecurity;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
@ -29,6 +28,8 @@ import org.springframework.http.ResponseEntity;
|
|||||||
import org.springframework.mock.web.MockMultipartFile;
|
import org.springframework.mock.web.MockMultipartFile;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import io.github.pixee.security.ZipSecurity;
|
||||||
|
|
||||||
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
|
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -214,7 +215,8 @@ class PDFToFileTest {
|
|||||||
|
|
||||||
// Verify the content by unzipping it
|
// Verify the content by unzipping it
|
||||||
try (ZipInputStream zipStream =
|
try (ZipInputStream zipStream =
|
||||||
ZipSecurity.createHardenedInputStream(new java.io.ByteArrayInputStream(response.getBody()))) {
|
ZipSecurity.createHardenedInputStream(
|
||||||
|
new java.io.ByteArrayInputStream(response.getBody()))) {
|
||||||
ZipEntry entry;
|
ZipEntry entry;
|
||||||
boolean foundMdFiles = false;
|
boolean foundMdFiles = false;
|
||||||
boolean foundImage = false;
|
boolean foundImage = false;
|
||||||
@ -286,7 +288,8 @@ class PDFToFileTest {
|
|||||||
|
|
||||||
// Verify the content by unzipping it
|
// Verify the content by unzipping it
|
||||||
try (ZipInputStream zipStream =
|
try (ZipInputStream zipStream =
|
||||||
ZipSecurity.createHardenedInputStream(new java.io.ByteArrayInputStream(response.getBody()))) {
|
ZipSecurity.createHardenedInputStream(
|
||||||
|
new java.io.ByteArrayInputStream(response.getBody()))) {
|
||||||
ZipEntry entry;
|
ZipEntry entry;
|
||||||
boolean foundMainHtml = false;
|
boolean foundMainHtml = false;
|
||||||
boolean foundIndexHtml = false;
|
boolean foundIndexHtml = false;
|
||||||
@ -437,7 +440,8 @@ class PDFToFileTest {
|
|||||||
|
|
||||||
// Verify the content by unzipping it
|
// Verify the content by unzipping it
|
||||||
try (ZipInputStream zipStream =
|
try (ZipInputStream zipStream =
|
||||||
ZipSecurity.createHardenedInputStream(new java.io.ByteArrayInputStream(response.getBody()))) {
|
ZipSecurity.createHardenedInputStream(
|
||||||
|
new java.io.ByteArrayInputStream(response.getBody()))) {
|
||||||
ZipEntry entry;
|
ZipEntry entry;
|
||||||
boolean foundMainFile = false;
|
boolean foundMainFile = false;
|
||||||
boolean foundMediaFiles = false;
|
boolean foundMediaFiles = false;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user