From 37b9647aa845308648fa1450cc1a763f9bb52c94 Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Fri, 31 Jul 2026 17:01:46 +0000
Subject: [PATCH] [#797] Retry the deletion of the embedded server temporary directory on close() (#798)

---
 opendj-embedded/src/main/java/org/openidentityplatform/opendj/embedded/Config.java |   98 +++++++++++++++++++++++++++++++++++++++++++++++--
 1 files changed, 94 insertions(+), 4 deletions(-)

diff --git a/opendj-embedded/src/main/java/org/openidentityplatform/opendj/embedded/Config.java b/opendj-embedded/src/main/java/org/openidentityplatform/opendj/embedded/Config.java
index 143c867..fe314c8 100644
--- a/opendj-embedded/src/main/java/org/openidentityplatform/opendj/embedded/Config.java
+++ b/opendj-embedded/src/main/java/org/openidentityplatform/opendj/embedded/Config.java
@@ -11,7 +11,7 @@
  * Header, with the fields enclosed by brackets [] replaced by your own identifying
  * information: "Portions Copyright [year] [name of copyright owner]".
  *
- * Copyright 2024 3A Systems LLC.
+ * Copyright 2024-2026 3A Systems LLC.
  */
 
 package org.openidentityplatform.opendj.embedded;
@@ -23,9 +23,9 @@
 public class Config {
 
     private final String CONFIG_PREFIX = Config.class.getPackage().getName();
-    private int port = Integer.parseInt(System.getProperty(CONFIG_PREFIX + ".port", "1389"));
+    private int port = intProperty(CONFIG_PREFIX + ".port", "1389");
 
-    private int adminPort = Integer.parseInt(System.getProperty(CONFIG_PREFIX + ".admin_port", "4444"));
+    private int adminPort = intProperty(CONFIG_PREFIX + ".admin_port", "4444");
 
     private String adminPassword = System.getProperty(CONFIG_PREFIX + ".password", "passw0rd");
 
@@ -33,7 +33,7 @@
 
     private String backendType = System.getProperty(CONFIG_PREFIX + ".backend", "je");
 
-    private int jmxPort = Integer.parseInt(System.getProperty(CONFIG_PREFIX + ".jmx_port", "1689"));
+    private int jmxPort = intProperty(CONFIG_PREFIX + ".jmx_port", "1689");
 
     private String ldifSchema = System.getProperty(CONFIG_PREFIX + ".ldif.schema");
 
@@ -41,6 +41,64 @@
 
     private Set<String> skipSet = new HashSet<>(Arrays.asList(System.getProperty(CONFIG_PREFIX + ".skip", ",ou=sample-skip-group,").toLowerCase().split(";")));
 
+    private long deleteTimeout = timeoutProperty(CONFIG_PREFIX + ".delete_timeout", "10000");
+
+    /**
+     * Returns the value of a system property holding a number.
+     * <p>
+     * These fields are initialized when the instance is created, so an unhandled
+     * {@link NumberFormatException} would surface from the constructor of
+     * {@link EmbeddedOpenDJ} saying only which string was rejected, without naming the
+     * property that carried it.
+     *
+     * @param name
+     *            the name of the system property
+     * @param defaultValue
+     *            the value used when the property is not set
+     * @return the value of the property, or {@code defaultValue} when it is not set
+     * @throws IllegalArgumentException
+     *             If the value of the property is not a number.
+     */
+    private static int intProperty(String name, String defaultValue) {
+        final String value = System.getProperty(name, defaultValue);
+        try {
+            return Integer.parseInt(value);
+        } catch (NumberFormatException e) {
+            throw invalidProperty(name, value, e);
+        }
+    }
+
+    /**
+     * Returns the value of a system property holding a duration in milliseconds.
+     *
+     * @param name
+     *            the name of the system property
+     * @param defaultValue
+     *            the value used when the property is not set
+     * @return the value of the property, or {@code defaultValue} when it is not set
+     * @throws IllegalArgumentException
+     *             If the value of the property is not a number, or is negative.
+     */
+    private static long timeoutProperty(String name, String defaultValue) {
+        final String value = System.getProperty(name, defaultValue);
+        final long timeout;
+        try {
+            timeout = Long.parseLong(value);
+        } catch (NumberFormatException e) {
+            throw invalidProperty(name, value, e);
+        }
+        if (timeout < 0) {
+            throw new IllegalArgumentException("Invalid value \"" + value + "\" for the system property "
+                    + name + ": a timeout in milliseconds cannot be negative");
+        }
+        return timeout;
+    }
+
+    private static IllegalArgumentException invalidProperty(String name, String value, NumberFormatException cause) {
+        return new IllegalArgumentException("Invalid value \"" + value + "\" for the system property "
+                + name + ": expected a number", cause);
+    }
+
     public int getPort() {
         return port;
     }
@@ -115,6 +173,37 @@
         this.skipSet = skipSet;
     }
 
+    /**
+     * Returns how long {@link EmbeddedOpenDJ#close()} retries the deletion of the temporary
+     * directory of the instance, in milliseconds.
+     * <p>
+     * The deletion has to be retried because a file cannot be deleted on Windows while a
+     * handle to it is still open, and the server threads release their handles shortly after
+     * the server has been stopped. Whatever is still locked when this expires is scheduled for
+     * deletion on JVM exit. Set it to {@code 0} to delete once and never wait.
+     *
+     * @return the deletion timeout in milliseconds
+     */
+    public long getDeleteTimeout() {
+        return deleteTimeout;
+    }
+
+    /**
+     * Sets how long {@link EmbeddedOpenDJ#close()} retries the deletion of the temporary
+     * directory of the instance.
+     *
+     * @param deleteTimeout
+     *            the deletion timeout in milliseconds, {@code 0} to delete once and never wait
+     * @throws IllegalArgumentException
+     *             If the timeout is negative.
+     */
+    public void setDeleteTimeout(long deleteTimeout) {
+        if (deleteTimeout < 0) {
+            throw new IllegalArgumentException("The delete timeout cannot be negative, but was " + deleteTimeout);
+        }
+        this.deleteTimeout = deleteTimeout;
+    }
+
     @Override
     public String toString() {
         return "Config {" +
@@ -127,6 +216,7 @@
                 ", ldifSchema='" + ldifSchema + '\'' +
                 ", file='" + file + '\'' +
                 ", skipSet=" + skipSet +
+                ", deleteTimeout=" + deleteTimeout +
                 '}';
     }
 }

--
Gitblit v1.10.0