From 89da2a5c0128515459c57c12ca07d83a82742e53 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Thu, 24 Oct 2024 13:59:17 +0100 Subject: [PATCH] Auto detect presence of external dependencies (LibreOffice etc) and disable/enable features dynamically (#2082) * Create ExternalAppDepConfig.java * Update EndpointConfiguration.java * Hardening suggestions for Stirling-PDF / ExternalAppDepConfig (#2083) Switch order of literals to prevent NullPointerException Co-authored-by: pixeebot[bot] <104101892+pixeebot[bot]@users.noreply.github.com> --------- Co-authored-by: pixeebot[bot] <104101892+pixeebot[bot]@users.noreply.github.com> --- .../SPDF/config/EndpointConfiguration.java | 37 ++++- .../SPDF/config/ExternalAppDepConfig.java | 146 ++++++++++++++++++ 2 files changed, 180 insertions(+), 3 deletions(-) create mode 100644 src/main/java/stirling/software/SPDF/config/ExternalAppDepConfig.java diff --git a/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java b/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java index e0ae56a1..9c0b3bf4 100644 --- a/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java +++ b/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java @@ -5,6 +5,7 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -42,7 +43,7 @@ public class EndpointConfiguration { public void disableEndpoint(String endpoint) { if (!endpointStatuses.containsKey(endpoint) || endpointStatuses.get(endpoint) != false) { - logger.info("Disabling {}", endpoint); + logger.debug("Disabling {}", endpoint); endpointStatuses.put(endpoint, false); } } @@ -76,6 +77,23 @@ public class EndpointConfiguration { } } + public void logDisabledEndpointsSummary() { + List disabledList = + endpointStatuses.entrySet().stream() + .filter(entry -> !entry.getValue()) // only get disabled endpoints (value + // is false) + .map(Map.Entry::getKey) + .sorted() + .collect(Collectors.toList()); + + if (!disabledList.isEmpty()) { + logger.info( + "Total disabled endpoints: {}. Disabled endpoints: {}", + disabledList.size(), + String.join(", ", disabledList)); + } + } + public void init() { // Adding endpoints to "PageOps" group addEndpointToGroup("PageOps", "remove-pages"); @@ -163,14 +181,12 @@ public class EndpointConfiguration { // python addEndpointToGroup("Python", "extract-image-scans"); - addEndpointToGroup("Python", REMOVE_BLANKS); addEndpointToGroup("Python", "html-to-pdf"); addEndpointToGroup("Python", "url-to-pdf"); addEndpointToGroup("Python", "pdf-to-img"); // openCV addEndpointToGroup("OpenCV", "extract-image-scans"); - addEndpointToGroup("OpenCV", REMOVE_BLANKS); // LibreOffice addEndpointToGroup("LibreOffice", "repair"); @@ -230,6 +246,17 @@ public class EndpointConfiguration { addEndpointToGroup("Javascript", "sign"); addEndpointToGroup("Javascript", "compare"); addEndpointToGroup("Javascript", "adjust-contrast"); + + // Ghostscript dependent endpoints + addEndpointToGroup("Ghostscript", "compress-pdf"); + addEndpointToGroup("Ghostscript", "pdf-to-pdfa"); + + // Weasyprint dependent endpoints + addEndpointToGroup("Weasyprint", "html-to-pdf"); + addEndpointToGroup("Weasyprint", "url-to-pdf"); + + // Pdftohtml dependent endpoints + addEndpointToGroup("Pdftohtml", "pdf-to-html"); } private void processEnvironmentConfigs() { @@ -251,5 +278,9 @@ public class EndpointConfiguration { } } + public Set getEndpointsForGroup(String group) { + return endpointGroups.getOrDefault(group, new HashSet<>()); + } + private static final String REMOVE_BLANKS = "remove-blanks"; } diff --git a/src/main/java/stirling/software/SPDF/config/ExternalAppDepConfig.java b/src/main/java/stirling/software/SPDF/config/ExternalAppDepConfig.java new file mode 100644 index 00000000..c939bd60 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/config/ExternalAppDepConfig.java @@ -0,0 +1,146 @@ +package stirling.software.SPDF.config; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; + +import jakarta.annotation.PostConstruct; +import lombok.extern.slf4j.Slf4j; + +@Configuration +@Slf4j +public class ExternalAppDepConfig { + @Autowired private EndpointConfiguration endpointConfiguration; + + private boolean isCommandAvailable(String command) { + try { + ProcessBuilder processBuilder = new ProcessBuilder(); + if (System.getProperty("os.name").toLowerCase().contains("windows")) { + processBuilder.command("where", command); + } else { + processBuilder.command("which", command); + } + Process process = processBuilder.start(); + int exitCode = process.waitFor(); + return exitCode == 0; + } catch (Exception e) { + log.debug("Error checking for command {}: {}", command, e.getMessage()); + return false; + } + } + + private final Map> commandToGroupMapping = + new HashMap<>() { + { + put("gs", List.of("Ghostscript")); + put("soffice", List.of("LibreOffice")); + put("ocrmypdf", List.of("OCRmyPDF")); + put("weasyprint", List.of("Weasyprint")); + put("pdftohtml", List.of("Pdftohtml")); + } + }; + + private List getAffectedFeatures(String group) { + return endpointConfiguration.getEndpointsForGroup(group).stream() + .map(endpoint -> formatEndpointAsFeature(endpoint)) + .collect(Collectors.toList()); + } + + private String formatEndpointAsFeature(String endpoint) { + // First replace common terms + String feature = endpoint.replace("-", " ").replace("pdf", "PDF").replace("img", "image"); + + // Split into words and capitalize each word + return Arrays.stream(feature.split("\\s+")) + .map(word -> capitalizeWord(word)) + .collect(Collectors.joining(" ")); + } + + private String capitalizeWord(String word) { + if (word.isEmpty()) { + return word; + } + if ("pdf".equalsIgnoreCase(word)) { + return "PDF"; + } + return word.substring(0, 1).toUpperCase() + word.substring(1).toLowerCase(); + } + + private void checkDependencyAndDisableGroup(String command) { + boolean isAvailable = isCommandAvailable(command); + if (!isAvailable) { + List affectedGroups = commandToGroupMapping.get(command); + + if (affectedGroups != null) { + for (String group : affectedGroups) { + List affectedFeatures = getAffectedFeatures(group); + endpointConfiguration.disableGroup(group); + log.warn( + "Missing dependency: {} - Disabling group: {} (Affected features: {})", + command, + group, + affectedFeatures != null && !affectedFeatures.isEmpty() + ? String.join(", ", affectedFeatures) + : "unknown"); + } + } + } + } + + @PostConstruct + public void checkDependencies() { + + // Check core dependencies + checkDependencyAndDisableGroup("gs"); + checkDependencyAndDisableGroup("soffice"); + checkDependencyAndDisableGroup("ocrmypdf"); + checkDependencyAndDisableGroup("weasyprint"); + checkDependencyAndDisableGroup("pdftohtml"); + + // Special handling for Python/OpenCV dependencies + boolean pythonAvailable = isCommandAvailable("python3") || isCommandAvailable("python"); + if (!pythonAvailable) { + List pythonFeatures = getAffectedFeatures("Python"); + List openCVFeatures = getAffectedFeatures("OpenCV"); + + endpointConfiguration.disableGroup("Python"); + endpointConfiguration.disableGroup("OpenCV"); + log.warn( + "Missing dependency: Python - Disabling Python features: {} and OpenCV features: {}", + String.join(", ", pythonFeatures), + String.join(", ", openCVFeatures)); + } else { + // If Python is available, check for OpenCV + try { + ProcessBuilder processBuilder = new ProcessBuilder(); + if (System.getProperty("os.name").toLowerCase().contains("windows")) { + processBuilder.command("python", "-c", "import cv2"); + } else { + processBuilder.command("python3", "-c", "import cv2"); + } + Process process = processBuilder.start(); + int exitCode = process.waitFor(); + if (exitCode != 0) { + List openCVFeatures = getAffectedFeatures("OpenCV"); + endpointConfiguration.disableGroup("OpenCV"); + log.warn( + "OpenCV not available in Python - Disabling OpenCV features: {}", + String.join(", ", openCVFeatures)); + } + } catch (Exception e) { + List openCVFeatures = getAffectedFeatures("OpenCV"); + endpointConfiguration.disableGroup("OpenCV"); + log.warn( + "Error checking OpenCV: {} - Disabling OpenCV features: {}", + e.getMessage(), + String.join(", ", openCVFeatures)); + } + } + endpointConfiguration.logDisabledEndpointsSummary(); + } +}