From 0d2e79c2ed0e3f3e12184d5caeda66d94eed8c8f Mon Sep 17 00:00:00 2001 From: fred Date: Tue, 9 Dec 2025 10:36:59 -0300 Subject: [PATCH 1/8] feat: add license validation --- .../solver/core/enterprise/License.java | 42 +++++++++++++++++++ .../TimefoldSolverEnterpriseService.java | 20 +++++++++ 2 files changed, 62 insertions(+) create mode 100644 core/src/main/java/ai/timefold/solver/core/enterprise/License.java diff --git a/core/src/main/java/ai/timefold/solver/core/enterprise/License.java b/core/src/main/java/ai/timefold/solver/core/enterprise/License.java new file mode 100644 index 0000000000..d48969dd1d --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/enterprise/License.java @@ -0,0 +1,42 @@ +package ai.timefold.solver.core.enterprise; + +public final class License { + + // TODO - Replace with the correct signed CA + public static final String CA = """ + -----BEGIN CERTIFICATE----- + MIIFYTCCA0mgAwIBAgIUb39y+k9L8Nn+/5gLsOFkhBlpJ5MwDQYJKoZIhvcNAQEL + BQAwQDELMAkGA1UEBhMCQkUxETAPBgNVBAoMCFRpbWVmb2xkMR4wHAYDVQQDDBVU + aW1lZm9sZCBMaWNlbnNpbmcgQ0EwHhcNMjUxMjA5MTEwOTE3WhcNMzUxMjA3MTEw + OTE3WjBAMQswCQYDVQQGEwJCRTERMA8GA1UECgwIVGltZWZvbGQxHjAcBgNVBAMM + FVRpbWVmb2xkIExpY2Vuc2luZyBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC + AgoCggIBAMMzRBxF5B0KT/DlZ894bkoYD/nqK+qUoSC9P/E87yeX7oZiVfITGXjy + ZpJuG48zRH+7BLYG4ELHBixGI8OsLlGbAihPm70pn8QU/0aHJ9lSUDa5dsyuYmr7 + gEhb45tHKgGscinCikjC8po3SGKIbgzqm5B/UmSWPoGeNHU0suYg37zlkwvyV1uj + rG1Pm2RJS35/U79bunyOpNjKmP6c75ccYMCG4VSdueF3et6bPiiYMY3I6mAS9anz + X9jKXgSJQR0uJPPUo9FfI91K8zLjhuze4BP4GAyHrIIdQgjRx1++90Vtgdfj9Odo + oQY64QDouO/nLNIGPV3K2cb5nQgtL41BpGfgFkkkDZZRua4GREDyW1LoPRD6wlGr + Ud0pnntL+KA7Ul75D5w4pqP5ZezuFVaHW/mT/gyDclei5QlebHJk/2Ibx1lrdwA7 + MhD87aW1gfmiHUPbg6et+Oo/Dir8xja5SnZ0j008zUCXPVUk/BnCOPtEpCZ3aNIp + y3b8CQ7EIBU4VLE07B2tI6ZoArEXp+yRBCplxAUpmx88e9WaO9eLOFoDJnpHZcOz + QOer2uLmw7cesV8piuSGRB0aRWcW626Th6OgwGhYGHH2EXk1JZAO89nnEatpRDTW + 0W1WTzH7q3yZ1eUwyy6WwZByTNYl9kyiXQaQ64v2Rh3hlobY3Dk7AgMBAAGjUzBR + MB0GA1UdDgQWBBQnum2seFZuX6i1e2nnqMY3IhxmBTAfBgNVHSMEGDAWgBQnum2s + eFZuX6i1e2nnqMY3IhxmBTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUA + A4ICAQCBCRZEfv2Iog2r28dFd7FrhwnFt3s2jh2BSYDTJMbVUPl52Mvwandjnem9 + Rd+bIpWIP9wQrbGHuBpK5fBLq/HI6MrWGEI9a2ZI3hOVD8igAfU1nJedu3FKtTkk + xhfXc+VG6Djr3Uv5w4euYhdxV86jIUwQnuiJB3vZCVdZlNpyhm7D7f8eappOP0qf + syhGFhfJ0jm7FGVjwX8B8UMwsG1hvvrAiupubbTzRdRxqNc9zmjKGL6o+DDwVWSA + FPz3dOCTcV+w6R9IjH6KyYT7Xj6/lOOwMpzzhraB5KTxLOZf4N0s4PVF7C6PT1Z/ + 0hMYVCS28Gw56XjhPbMGtTHCgAx76w4uIuUNHJNa2DC/1nudxl1bQwxwkrCUC3Hh + iuGWUWAeoIbJrJQdPGDru+JKchkBafRdUpVvvRwcnjSw2cuKgP93elX+dHYrGfXm + mXbhHHQNixRAQj7i78vzoXQcEsJRvdWGFXkjt8yIVObRNtuMlZPmce+btFWq8cTl + ncuhpe+jwz9C0BjqVtAc8nH+eYPKFyGTDlsz7hzQb7gNrYTrNdhFCkpuhb+axfGp + ZPj0mbAaNlpYm/mlYt+jY5YmYJNomZKhxM8O09pOwLNrLbWUhrLczbeph1Iu33LE + KYb0W/hmsuvLXt6O5/fudgMzZ2oaVGKX4B89B2U8aYY28xyu/w== + -----END CERTIFICATE-----"""; + + private License() { + // Empty constructor + } +} diff --git a/core/src/main/java/ai/timefold/solver/core/enterprise/TimefoldSolverEnterpriseService.java b/core/src/main/java/ai/timefold/solver/core/enterprise/TimefoldSolverEnterpriseService.java index 13489dc0c5..1599cf463c 100644 --- a/core/src/main/java/ai/timefold/solver/core/enterprise/TimefoldSolverEnterpriseService.java +++ b/core/src/main/java/ai/timefold/solver/core/enterprise/TimefoldSolverEnterpriseService.java @@ -1,6 +1,7 @@ package ai.timefold.solver.core.enterprise; import java.lang.reflect.InvocationTargetException; +import java.security.cert.CertificateException; import java.util.Objects; import java.util.function.BiFunction; import java.util.function.Function; @@ -86,8 +87,15 @@ static TimefoldSolverEnterpriseService loadOrFail(Feature feature) { var communityVersion = getVersionString(TimefoldSolverEnterpriseService.class); var enterpriseVersion = getVersionString(service.getClass()); if (Objects.equals(communityVersion, enterpriseVersion)) { // Identical versions. + // We validate the user license at this point + try { + service.validateUserLicense(); + } catch (CertificateException e) { + throw new IllegalStateException(e); + } return service; } else if (enterpriseVersion.equals(DEVELOPMENT_SNAPSHOT)) { // Don't enforce when running Enterprise tests. + // We do not validate the user license for development snapshots. return service; } throw new IllegalStateException(""" @@ -100,13 +108,25 @@ static TimefoldSolverEnterpriseService loadOrFail(Feature feature) { static T buildOrDefault(Function builder, Supplier defaultValue) { try { var service = load(); + var enterpriseVersion = getVersionString(service.getClass()); + if (!enterpriseVersion.equals(DEVELOPMENT_SNAPSHOT)) { + // We validate the user license at this point + service.validateUserLicense(); + } return builder.apply(service); } catch (ClassNotFoundException | InvocationTargetException | NoSuchMethodException | InstantiationException | IllegalAccessException e) { return defaultValue.get(); + } catch (CertificateException e) { + throw new IllegalStateException(e); } } + /** + * It performs a security validation to ensure that the user license is valid. + */ + void validateUserLicense() throws CertificateException; + TopologicalOrderGraph buildTopologyGraph(int size); Class From 7dd930b73393392293d9b871797f1bfbfe70464e Mon Sep 17 00:00:00 2001 From: fred Date: Tue, 9 Dec 2025 11:02:38 -0300 Subject: [PATCH 2/8] chore: refactoring --- .../solver/core/enterprise/License.java | 42 ------------------- 1 file changed, 42 deletions(-) delete mode 100644 core/src/main/java/ai/timefold/solver/core/enterprise/License.java diff --git a/core/src/main/java/ai/timefold/solver/core/enterprise/License.java b/core/src/main/java/ai/timefold/solver/core/enterprise/License.java deleted file mode 100644 index d48969dd1d..0000000000 --- a/core/src/main/java/ai/timefold/solver/core/enterprise/License.java +++ /dev/null @@ -1,42 +0,0 @@ -package ai.timefold.solver.core.enterprise; - -public final class License { - - // TODO - Replace with the correct signed CA - public static final String CA = """ - -----BEGIN CERTIFICATE----- - MIIFYTCCA0mgAwIBAgIUb39y+k9L8Nn+/5gLsOFkhBlpJ5MwDQYJKoZIhvcNAQEL - BQAwQDELMAkGA1UEBhMCQkUxETAPBgNVBAoMCFRpbWVmb2xkMR4wHAYDVQQDDBVU - aW1lZm9sZCBMaWNlbnNpbmcgQ0EwHhcNMjUxMjA5MTEwOTE3WhcNMzUxMjA3MTEw - OTE3WjBAMQswCQYDVQQGEwJCRTERMA8GA1UECgwIVGltZWZvbGQxHjAcBgNVBAMM - FVRpbWVmb2xkIExpY2Vuc2luZyBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC - AgoCggIBAMMzRBxF5B0KT/DlZ894bkoYD/nqK+qUoSC9P/E87yeX7oZiVfITGXjy - ZpJuG48zRH+7BLYG4ELHBixGI8OsLlGbAihPm70pn8QU/0aHJ9lSUDa5dsyuYmr7 - gEhb45tHKgGscinCikjC8po3SGKIbgzqm5B/UmSWPoGeNHU0suYg37zlkwvyV1uj - rG1Pm2RJS35/U79bunyOpNjKmP6c75ccYMCG4VSdueF3et6bPiiYMY3I6mAS9anz - X9jKXgSJQR0uJPPUo9FfI91K8zLjhuze4BP4GAyHrIIdQgjRx1++90Vtgdfj9Odo - oQY64QDouO/nLNIGPV3K2cb5nQgtL41BpGfgFkkkDZZRua4GREDyW1LoPRD6wlGr - Ud0pnntL+KA7Ul75D5w4pqP5ZezuFVaHW/mT/gyDclei5QlebHJk/2Ibx1lrdwA7 - MhD87aW1gfmiHUPbg6et+Oo/Dir8xja5SnZ0j008zUCXPVUk/BnCOPtEpCZ3aNIp - y3b8CQ7EIBU4VLE07B2tI6ZoArEXp+yRBCplxAUpmx88e9WaO9eLOFoDJnpHZcOz - QOer2uLmw7cesV8piuSGRB0aRWcW626Th6OgwGhYGHH2EXk1JZAO89nnEatpRDTW - 0W1WTzH7q3yZ1eUwyy6WwZByTNYl9kyiXQaQ64v2Rh3hlobY3Dk7AgMBAAGjUzBR - MB0GA1UdDgQWBBQnum2seFZuX6i1e2nnqMY3IhxmBTAfBgNVHSMEGDAWgBQnum2s - eFZuX6i1e2nnqMY3IhxmBTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUA - A4ICAQCBCRZEfv2Iog2r28dFd7FrhwnFt3s2jh2BSYDTJMbVUPl52Mvwandjnem9 - Rd+bIpWIP9wQrbGHuBpK5fBLq/HI6MrWGEI9a2ZI3hOVD8igAfU1nJedu3FKtTkk - xhfXc+VG6Djr3Uv5w4euYhdxV86jIUwQnuiJB3vZCVdZlNpyhm7D7f8eappOP0qf - syhGFhfJ0jm7FGVjwX8B8UMwsG1hvvrAiupubbTzRdRxqNc9zmjKGL6o+DDwVWSA - FPz3dOCTcV+w6R9IjH6KyYT7Xj6/lOOwMpzzhraB5KTxLOZf4N0s4PVF7C6PT1Z/ - 0hMYVCS28Gw56XjhPbMGtTHCgAx76w4uIuUNHJNa2DC/1nudxl1bQwxwkrCUC3Hh - iuGWUWAeoIbJrJQdPGDru+JKchkBafRdUpVvvRwcnjSw2cuKgP93elX+dHYrGfXm - mXbhHHQNixRAQj7i78vzoXQcEsJRvdWGFXkjt8yIVObRNtuMlZPmce+btFWq8cTl - ncuhpe+jwz9C0BjqVtAc8nH+eYPKFyGTDlsz7hzQb7gNrYTrNdhFCkpuhb+axfGp - ZPj0mbAaNlpYm/mlYt+jY5YmYJNomZKhxM8O09pOwLNrLbWUhrLczbeph1Iu33LE - KYb0W/hmsuvLXt6O5/fudgMzZ2oaVGKX4B89B2U8aYY28xyu/w== - -----END CERTIFICATE-----"""; - - private License() { - // Empty constructor - } -} From fad0332784d0092902e16f6011de3bcd8d01f9bf Mon Sep 17 00:00:00 2001 From: fred Date: Tue, 9 Dec 2025 11:57:27 -0300 Subject: [PATCH 3/8] chore: fix loading --- .../TimefoldSolverEnterpriseService.java | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/enterprise/TimefoldSolverEnterpriseService.java b/core/src/main/java/ai/timefold/solver/core/enterprise/TimefoldSolverEnterpriseService.java index 1599cf463c..97eb2abd21 100644 --- a/core/src/main/java/ai/timefold/solver/core/enterprise/TimefoldSolverEnterpriseService.java +++ b/core/src/main/java/ai/timefold/solver/core/enterprise/TimefoldSolverEnterpriseService.java @@ -44,7 +44,7 @@ public interface TimefoldSolverEnterpriseService { String COMMUNITY_COORDINATES = "ai.timefold.solver:timefold-solver-core"; String ENTERPRISE_NAME = "Enterprise Edition"; String ENTERPRISE_COORDINATES = "ai.timefold.solver.enterprise:timefold-solver-enterprise-core"; - String DEVELOPMENT_SNAPSHOT = "Development Snapshot"; + String DEVELOPMENT_SNAPSHOT = "v999-SNAPSHOT"; static String identifySolverVersion() { var packaging = COMMUNITY_NAME; @@ -87,15 +87,16 @@ static TimefoldSolverEnterpriseService loadOrFail(Feature feature) { var communityVersion = getVersionString(TimefoldSolverEnterpriseService.class); var enterpriseVersion = getVersionString(service.getClass()); if (Objects.equals(communityVersion, enterpriseVersion)) { // Identical versions. - // We validate the user license at this point - try { - service.validateUserLicense(); - } catch (CertificateException e) { - throw new IllegalStateException(e); + if (!enterpriseVersion.equals(DEVELOPMENT_SNAPSHOT)) { + // We validate the user license at this point + try { + service.validateUserLicense(); + } catch (CertificateException e) { + throw new IllegalStateException("User license validation has failed (%s).".formatted(enterpriseVersion), e); + } } return service; } else if (enterpriseVersion.equals(DEVELOPMENT_SNAPSHOT)) { // Don't enforce when running Enterprise tests. - // We do not validate the user license for development snapshots. return service; } throw new IllegalStateException(""" From fb4cc59149ffd74f2f6134f26a56d4419ca6b69a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Petrovick=C3=BD?= Date: Wed, 10 Dec 2025 10:00:07 +0100 Subject: [PATCH 4/8] Move most of the validation logic to Enterprise --- .../TimefoldSolverEnterpriseService.java | 64 +++++++------------ 1 file changed, 24 insertions(+), 40 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/enterprise/TimefoldSolverEnterpriseService.java b/core/src/main/java/ai/timefold/solver/core/enterprise/TimefoldSolverEnterpriseService.java index 97eb2abd21..6f7d06c5d1 100644 --- a/core/src/main/java/ai/timefold/solver/core/enterprise/TimefoldSolverEnterpriseService.java +++ b/core/src/main/java/ai/timefold/solver/core/enterprise/TimefoldSolverEnterpriseService.java @@ -1,8 +1,7 @@ package ai.timefold.solver.core.enterprise; import java.lang.reflect.InvocationTargetException; -import java.security.cert.CertificateException; -import java.util.Objects; +import java.lang.reflect.Method; import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Supplier; @@ -44,7 +43,7 @@ public interface TimefoldSolverEnterpriseService { String COMMUNITY_COORDINATES = "ai.timefold.solver:timefold-solver-core"; String ENTERPRISE_NAME = "Enterprise Edition"; String ENTERPRISE_COORDINATES = "ai.timefold.solver.enterprise:timefold-solver-enterprise-core"; - String DEVELOPMENT_SNAPSHOT = "v999-SNAPSHOT"; + String DEVELOPMENT_SNAPSHOT = "Development Snapshot"; static String identifySolverVersion() { var packaging = COMMUNITY_NAME; @@ -58,76 +57,53 @@ static String identifySolverVersion() { return packaging + " " + version; } - private static String getVersionString(Class clz) { + static String getVersionString(Class clz) { var version = clz.getPackage().getImplementationVersion(); return (version == null ? DEVELOPMENT_SNAPSHOT : "v" + version); } + @SuppressWarnings("unchecked") static TimefoldSolverEnterpriseService load() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { // Avoids ServiceLoader by using reflection directly. var clz = (Class) Class .forName("ai.timefold.solver.enterprise.core.DefaultTimefoldSolverEnterpriseService"); - return clz.getDeclaredConstructor().newInstance(); + Method method = clz.getMethod("getInstance", Function.class); + return (TimefoldSolverEnterpriseService) method.invoke(null, + (Function, String>) TimefoldSolverEnterpriseService::getVersionString); } static TimefoldSolverEnterpriseService loadOrFail(Feature feature) { - TimefoldSolverEnterpriseService service; try { - service = load(); + return load(); + } catch (EnterpriseLicenseException cause) { + throw new IllegalStateException(""" + No valid Timefold Enterprise License was found. + Please contact Timefold to obtain a valid license, + or if you believe that this message was given in error.""", + cause); } catch (Exception cause) { throw new IllegalStateException(""" %s requested but %s %s not found on classpath. Either add the %s dependency, or %s. - Note: %s %s is a commercial product. Visit https://timefold.ai to find out more.""" + Note: %s %s is a commercial product. + Visit https://timefold.ai to find out more.""" .formatted(feature.getName(), SOLVER_NAME, ENTERPRISE_NAME, feature.getWorkaround(), ENTERPRISE_COORDINATES, SOLVER_NAME, ENTERPRISE_NAME), cause); } - var communityVersion = getVersionString(TimefoldSolverEnterpriseService.class); - var enterpriseVersion = getVersionString(service.getClass()); - if (Objects.equals(communityVersion, enterpriseVersion)) { // Identical versions. - if (!enterpriseVersion.equals(DEVELOPMENT_SNAPSHOT)) { - // We validate the user license at this point - try { - service.validateUserLicense(); - } catch (CertificateException e) { - throw new IllegalStateException("User license validation has failed (%s).".formatted(enterpriseVersion), e); - } - } - return service; - } else if (enterpriseVersion.equals(DEVELOPMENT_SNAPSHOT)) { // Don't enforce when running Enterprise tests. - return service; - } - throw new IllegalStateException(""" - Detected mismatch between versions of %s %s (%s) and %s (%s). - Ensure your project uses the same version of %s and %s dependencies.""" - .formatted(SOLVER_NAME, COMMUNITY_NAME, communityVersion, ENTERPRISE_NAME, enterpriseVersion, - COMMUNITY_COORDINATES, ENTERPRISE_COORDINATES)); } static T buildOrDefault(Function builder, Supplier defaultValue) { try { var service = load(); - var enterpriseVersion = getVersionString(service.getClass()); - if (!enterpriseVersion.equals(DEVELOPMENT_SNAPSHOT)) { - // We validate the user license at this point - service.validateUserLicense(); - } return builder.apply(service); } catch (ClassNotFoundException | InvocationTargetException | NoSuchMethodException | InstantiationException | IllegalAccessException e) { return defaultValue.get(); - } catch (CertificateException e) { - throw new IllegalStateException(e); } } - /** - * It performs a security validation to ensure that the user license is valid. - */ - void validateUserLicense() throws CertificateException; - TopologicalOrderGraph buildTopologyGraph(int size); Class @@ -187,4 +163,12 @@ public String getWorkaround() { } + final class EnterpriseLicenseException extends RuntimeException { + + public EnterpriseLicenseException(String message, Exception cause) { + super(message, cause); + } + + } + } From 536e52d2c76d79e3fe75095710625efe7ee947d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Petrovick=C3=BD?= Date: Thu, 11 Dec 2025 08:03:24 +0100 Subject: [PATCH 5/8] Clean up around exceptions --- .../TimefoldSolverEnterpriseService.java | 38 ++++++++++--------- .../support/VariableListenerSupport.java | 2 +- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/enterprise/TimefoldSolverEnterpriseService.java b/core/src/main/java/ai/timefold/solver/core/enterprise/TimefoldSolverEnterpriseService.java index 6f7d06c5d1..9ccae813d6 100644 --- a/core/src/main/java/ai/timefold/solver/core/enterprise/TimefoldSolverEnterpriseService.java +++ b/core/src/main/java/ai/timefold/solver/core/enterprise/TimefoldSolverEnterpriseService.java @@ -50,21 +50,16 @@ static String identifySolverVersion() { try { load(); packaging = ENTERPRISE_NAME; - } catch (Exception e) { + } catch (Throwable e) { // No need to do anything, just checking if Enterprise exists. } var version = getVersionString(SolverFactory.class); return packaging + " " + version; } - static String getVersionString(Class clz) { - var version = clz.getPackage().getImplementationVersion(); - return (version == null ? DEVELOPMENT_SNAPSHOT : "v" + version); - } - @SuppressWarnings("unchecked") - static TimefoldSolverEnterpriseService load() throws ClassNotFoundException, NoSuchMethodException, - InvocationTargetException, InstantiationException, IllegalAccessException { + static TimefoldSolverEnterpriseService load() + throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { // Avoids ServiceLoader by using reflection directly. var clz = (Class) Class .forName("ai.timefold.solver.enterprise.core.DefaultTimefoldSolverEnterpriseService"); @@ -73,6 +68,11 @@ static TimefoldSolverEnterpriseService load() throws ClassNotFoundException, NoS (Function, String>) TimefoldSolverEnterpriseService::getVersionString); } + static String getVersionString(Class clz) { + var version = clz.getPackage().getImplementationVersion(); + return (version == null ? DEVELOPMENT_SNAPSHOT : "v" + version); + } + static TimefoldSolverEnterpriseService loadOrFail(Feature feature) { try { return load(); @@ -82,24 +82,22 @@ static TimefoldSolverEnterpriseService loadOrFail(Feature feature) { Please contact Timefold to obtain a valid license, or if you believe that this message was given in error.""", cause); - } catch (Exception cause) { + } catch (Throwable cause) { throw new IllegalStateException(""" - %s requested but %s %s not found on classpath. - Either add the %s dependency, or %s. + %s requested but %s %s could not be loaded. + Maybe add the %s dependency, or %s. Note: %s %s is a commercial product. - Visit https://timefold.ai to find out more.""" + Visit https://timefold.ai to find out more, or contact Timefold customer support.""" .formatted(feature.getName(), SOLVER_NAME, ENTERPRISE_NAME, feature.getWorkaround(), ENTERPRISE_COORDINATES, SOLVER_NAME, ENTERPRISE_NAME), cause); } } - static T buildOrDefault(Function builder, Supplier defaultValue) { + static T loadOrDefault(Function builder, Supplier defaultValue) { try { - var service = load(); - return builder.apply(service); - } catch (ClassNotFoundException | InvocationTargetException | NoSuchMethodException | InstantiationException - | IllegalAccessException e) { + return builder.apply(load()); + } catch (Throwable e) { return defaultValue.get(); } } @@ -165,7 +163,11 @@ public String getWorkaround() { final class EnterpriseLicenseException extends RuntimeException { - public EnterpriseLicenseException(String message, Exception cause) { + public EnterpriseLicenseException(String message) { + super(message); + } + + public EnterpriseLicenseException(String message, Throwable cause) { super(message, cause); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/listener/support/VariableListenerSupport.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/listener/support/VariableListenerSupport.java index c78eee1e12..3afa5cd5e4 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/listener/support/VariableListenerSupport.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/listener/support/VariableListenerSupport.java @@ -58,7 +58,7 @@ public final class VariableListenerSupport implements SupplyManager { public static VariableListenerSupport create(InnerScoreDirector scoreDirector) { return new VariableListenerSupport<>(scoreDirector, new NotifiableRegistry<>(scoreDirector.getSolutionDescriptor()), - TimefoldSolverEnterpriseService.buildOrDefault(service -> service::buildTopologyGraph, + TimefoldSolverEnterpriseService.loadOrDefault(service -> service::buildTopologyGraph, () -> DefaultTopologicalOrderGraph::new)); } From 6da8c08f671f000642c0280888291472f76783e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Petrovick=C3=BD?= Date: Thu, 11 Dec 2025 08:12:36 +0100 Subject: [PATCH 6/8] Add docs --- .../enterprise-edition.adoc | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/docs/src/modules/ROOT/pages/enterprise-edition/enterprise-edition.adoc b/docs/src/modules/ROOT/pages/enterprise-edition/enterprise-edition.adoc index 90c0904b69..2cc838aa0c 100644 --- a/docs/src/modules/ROOT/pages/enterprise-edition/enterprise-edition.adoc +++ b/docs/src/modules/ROOT/pages/enterprise-edition/enterprise-edition.adoc @@ -13,7 +13,9 @@ You are allowed to use Timefold Solver Enterprise Edition for evaluation and dev Please https://timefold.ai/contact[contact Timefold] to obtain the credentials necessary to start your evaluation. -TIP: Looking for quicker time-to-value? Timefold offers https://docs.timefold.ai/[pre-built, fully tuned optimization models], no constraint building required. Just plug into our API and start optimizing immediately. +TIP: Looking for quicker time-to-value? +Timefold offers https://docs.timefold.ai/[pre-built, fully tuned optimization models], no constraint building required. +Just plug into our API and start optimizing immediately. For a high-level overview of the differences between Timefold offerings, see https://timefold.ai/pricing[Timefold Pricing]. @@ -21,8 +23,22 @@ see https://timefold.ai/pricing[Timefold Pricing]. [#switchToEnterpriseEdition] == Switch to Enterprise Edition -To switch from Timefold Solver Community Edition to Enterprise Edition, -first reference the Enterprise Edition Maven repository in your project: +// Uncomment the following when ready to enable the Enterprise license +// ============================= +// To switch from Timefold Solver Community Edition to Enterprise Edition, +// https://timefold.ai/contact[contact us first] to obtain an Enterprise License. +// This license is represented by a file named `timefold-enterprise-license.pem`, +// which you can introduce to your project by one of the following methods: +// +// * Place the file in the user's home directory. +// * Store the path to the file in `TIMEFOLD_ENTERPRISE_LICENSE` system property. +// * Place the file in the root of your application classpath. +// +// Having done that, reference the Enterprise Edition Maven repository in your project: +// ============================= +// Also delete the next line of text. + +First reference the Enterprise Edition Maven repository in your project: [tabs] ==== @@ -91,7 +107,8 @@ repositories { -- ==== -Having done the above, replace references to Community Edition artifacts by their Enterprise Edition counterparts +Finally, having done all of the above, +replace references to Community Edition artifacts by their Enterprise Edition counterparts as shown in the following table: |=== From 7e7de2f1d4a9e38464f1ac03a30cf6e3195e26d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Petrovick=C3=BD?= Date: Fri, 12 Dec 2025 12:19:34 +0100 Subject: [PATCH 7/8] Sonar --- .../core/enterprise/TimefoldSolverEnterpriseService.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/enterprise/TimefoldSolverEnterpriseService.java b/core/src/main/java/ai/timefold/solver/core/enterprise/TimefoldSolverEnterpriseService.java index 9ccae813d6..cd39ebb6e6 100644 --- a/core/src/main/java/ai/timefold/solver/core/enterprise/TimefoldSolverEnterpriseService.java +++ b/core/src/main/java/ai/timefold/solver/core/enterprise/TimefoldSolverEnterpriseService.java @@ -50,7 +50,7 @@ static String identifySolverVersion() { try { load(); packaging = ENTERPRISE_NAME; - } catch (Throwable e) { + } catch (Exception e) { // No need to do anything, just checking if Enterprise exists. } var version = getVersionString(SolverFactory.class); @@ -82,7 +82,7 @@ static TimefoldSolverEnterpriseService loadOrFail(Feature feature) { Please contact Timefold to obtain a valid license, or if you believe that this message was given in error.""", cause); - } catch (Throwable cause) { + } catch (Exception cause) { throw new IllegalStateException(""" %s requested but %s %s could not be loaded. Maybe add the %s dependency, or %s. @@ -97,7 +97,7 @@ static TimefoldSolverEnterpriseService loadOrFail(Feature feature) { static T loadOrDefault(Function builder, Supplier defaultValue) { try { return builder.apply(load()); - } catch (Throwable e) { + } catch (Exception e) { return defaultValue.get(); } } From fbd1ed95528802ec48554bd6a21e0edb1f0363b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Petrovick=C3=BD?= Date: Fri, 12 Dec 2025 14:49:59 +0100 Subject: [PATCH 8/8] Don't include the actual license --- .../ROOT/pages/enterprise-edition/enterprise-edition.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/modules/ROOT/pages/enterprise-edition/enterprise-edition.adoc b/docs/src/modules/ROOT/pages/enterprise-edition/enterprise-edition.adoc index 2cc838aa0c..c8073401ad 100644 --- a/docs/src/modules/ROOT/pages/enterprise-edition/enterprise-edition.adoc +++ b/docs/src/modules/ROOT/pages/enterprise-edition/enterprise-edition.adoc @@ -27,7 +27,7 @@ see https://timefold.ai/pricing[Timefold Pricing]. // ============================= // To switch from Timefold Solver Community Edition to Enterprise Edition, // https://timefold.ai/contact[contact us first] to obtain an Enterprise License. -// This license is represented by a file named `timefold-enterprise-license.pem`, +// This license is represented by a file named `timefold-solver-enterprise-license.pem`, // which you can introduce to your project by one of the following methods: // // * Place the file in the user's home directory.