diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/Account.java b/acme4j-client/src/main/java/org/shredzone/acme4j/Account.java index c67b160e..01910923 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/Account.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/Account.java @@ -27,6 +27,7 @@ import java.util.Optional; import org.shredzone.acme4j.connector.Resource; import org.shredzone.acme4j.connector.ResourceIterator; import org.shredzone.acme4j.exception.AcmeException; +import org.shredzone.acme4j.exception.AcmeNotSupportedException; import org.shredzone.acme4j.exception.AcmeProtocolException; import org.shredzone.acme4j.exception.AcmeServerException; import org.shredzone.acme4j.toolbox.AcmeUtils; @@ -125,7 +126,7 @@ public class Account extends AcmeJsonResource { if (ordersUrl.isEmpty()) { // Let's Encrypt does not provide this field at the moment, although it's required. // See https://github.com/letsencrypt/boulder/issues/3335 - throw new AcmeProtocolException("This ACME server does not support getOrders()"); + throw new AcmeNotSupportedException("getOrders()"); } return new ResourceIterator<>(getLogin(), KEY_ORDERS, ordersUrl.get(), Login::bindOrder); } diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/Metadata.java b/acme4j-client/src/main/java/org/shredzone/acme4j/Metadata.java index 3294096f..d0a108a4 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/Metadata.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/Metadata.java @@ -21,6 +21,7 @@ import java.time.Duration; import java.util.Collection; import java.util.Optional; +import org.shredzone.acme4j.exception.AcmeNotSupportedException; import org.shredzone.acme4j.toolbox.JSON; import org.shredzone.acme4j.toolbox.JSON.Value; @@ -86,14 +87,17 @@ public class Metadata { /** * Returns the minimum acceptable value for the maximum validity of a certificate - * before auto-renewal. Empty if the CA does not support short-term auto-renewal. + * before auto-renewal. * * @since 2.3 + * @throws AcmeNotSupportedException if the server does not support auto-renewal. */ - public Optional getAutoRenewalMinLifetime() { - return meta.get("auto-renewal").optional().map(Value::asObject) - .map(j -> j.get("min-lifetime")) - .map(Value::asDuration); + public Duration getAutoRenewalMinLifetime() { + return meta.getFeature("auto-renewal") + .map(Value::asObject) + .orElseGet(JSON::empty) + .get("min-lifetime") + .asDuration(); } /** @@ -101,21 +105,28 @@ public class Metadata { * date. * * @since 2.3 + * @throws AcmeNotSupportedException if the server does not support auto-renewal. */ - public Optional getAutoRenewalMaxDuration() { - return meta.get("auto-renewal").optional().map(Value::asObject) - .map(j -> j.get("max-duration")) - .map(Value::asDuration); + public Duration getAutoRenewalMaxDuration() { + return meta.getFeature("auto-renewal") + .map(Value::asObject) + .orElseGet(JSON::empty) + .get("max-duration") + .asDuration(); } /** * Returns whether the CA also allows to fetch STAR certificates via GET request. * * @since 2.6 + * @throws AcmeNotSupportedException if the server does not support auto-renewal. */ public boolean isAutoRenewalGetAllowed() { - return meta.get("auto-renewal").optional().map(Value::asObject) - .map(j -> j.get("allow-certificate-get")) + return meta.getFeature("auto-renewal").optional() + .map(Value::asObject) + .orElseGet(JSON::empty) + .get("allow-certificate-get") + .optional() .map(Value::asBoolean) .orElse(false); } diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/Order.java b/acme4j-client/src/main/java/org/shredzone/acme4j/Order.java index 52faac39..f8c8e418 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/Order.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/Order.java @@ -22,6 +22,7 @@ import java.util.List; import java.util.Optional; import org.shredzone.acme4j.exception.AcmeException; +import org.shredzone.acme4j.exception.AcmeNotSupportedException; import org.shredzone.acme4j.toolbox.JSON; import org.shredzone.acme4j.toolbox.JSON.Value; import org.shredzone.acme4j.toolbox.JSONBuilder; @@ -178,10 +179,10 @@ public class Order extends AcmeJsonResource { * Returns the earliest date of validity of the first certificate issued. * * @since 2.3 + * @throws AcmeNotSupportedException if auto-renewal is not supported */ public Optional getAutoRenewalStartDate() { - return getJSON().get("auto-renewal") - .optional() + return getJSON().getFeature("auto-renewal") .map(Value::asObject) .orElseGet(JSON::empty) .get("start-date") @@ -193,39 +194,39 @@ public class Order extends AcmeJsonResource { * Returns the latest date of validity of the last certificate issued. * * @since 2.3 + * @throws AcmeNotSupportedException if auto-renewal is not supported */ - public Optional getAutoRenewalEndDate() { - return getJSON().get("auto-renewal") - .optional() + public Instant getAutoRenewalEndDate() { + return getJSON().getFeature("auto-renewal") .map(Value::asObject) .orElseGet(JSON::empty) .get("end-date") - .optional() - .map(Value::asInstant); + .asInstant(); } /** * Returns the maximum lifetime of each certificate. * * @since 2.3 + * @throws AcmeNotSupportedException if auto-renewal is not supported */ - public Optional getAutoRenewalLifetime() { - return getJSON().get("auto-renewal") + public Duration getAutoRenewalLifetime() { + return getJSON().getFeature("auto-renewal") .optional() .map(Value::asObject) .orElseGet(JSON::empty) .get("lifetime") - .optional() - .map(Value::asDuration); + .asDuration(); } /** * Returns the pre-date period of each certificate. * * @since 2.7 + * @throws AcmeNotSupportedException if auto-renewal is not supported */ public Optional getAutoRenewalLifetimeAdjust() { - return getJSON().get("auto-renewal") + return getJSON().getFeature("auto-renewal") .optional() .map(Value::asObject) .orElseGet(JSON::empty) @@ -241,7 +242,7 @@ public class Order extends AcmeJsonResource { * @since 2.6 */ public boolean isAutoRenewalGetEnabled() { - return getJSON().get("auto-renewal") + return getJSON().getFeature("auto-renewal") .optional() .map(Value::asObject) .orElseGet(JSON::empty) @@ -258,7 +259,7 @@ public class Order extends AcmeJsonResource { */ public void cancelAutoRenewal() throws AcmeException { if (!getSession().getMetadata().isAutoRenewalEnabled()) { - throw new AcmeException("CA does not support short-term automatic renewals"); + throw new AcmeNotSupportedException("auto-renewal"); } LOG.debug("cancel"); diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/OrderBuilder.java b/acme4j-client/src/main/java/org/shredzone/acme4j/OrderBuilder.java index f33ebbf9..a333cce6 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/OrderBuilder.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/OrderBuilder.java @@ -25,7 +25,7 @@ import java.util.Set; import edu.umd.cs.findbugs.annotations.Nullable; import org.shredzone.acme4j.connector.Resource; import org.shredzone.acme4j.exception.AcmeException; -import org.shredzone.acme4j.exception.AcmeProtocolException; +import org.shredzone.acme4j.exception.AcmeNotSupportedException; import org.shredzone.acme4j.toolbox.JSONBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -278,7 +278,7 @@ public class OrderBuilder { var session = login.getSession(); if (autoRenewal && !session.getMetadata().isAutoRenewalEnabled()) { - throw new AcmeException("CA does not support short-term automatic renewals"); + throw new AcmeNotSupportedException("auto-renewal"); } LOG.debug("create"); diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/exception/AcmeNotSupportedException.java b/acme4j-client/src/main/java/org/shredzone/acme4j/exception/AcmeNotSupportedException.java new file mode 100644 index 00000000..f8356cd4 --- /dev/null +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/exception/AcmeNotSupportedException.java @@ -0,0 +1,34 @@ +/* + * acme4j - Java ACME client + * + * Copyright (C) 2023 Richard "Shred" Körber + * http://acme4j.shredzone.org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + */ +package org.shredzone.acme4j.exception; + +/** + * A runtime exception that is thrown if the ACME server does not support a certain + * feature. It might be either because that feature is optional, or because the server + * is not fully RFC compliant. + */ +public class AcmeNotSupportedException extends AcmeProtocolException { + private static final long serialVersionUID = 3434074002226584731L; + + /** + * Creates a new {@link AcmeNotSupportedException}. + * + * @param feature + * Feature that is not supported + */ + public AcmeNotSupportedException(String feature) { + super("Server does not support " + feature); + } + +} diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/toolbox/JSON.java b/acme4j-client/src/main/java/org/shredzone/acme4j/toolbox/JSON.java index 4a0ea6c9..d7b2c2b0 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/toolbox/JSON.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/toolbox/JSON.java @@ -50,6 +50,7 @@ import org.jose4j.lang.JoseException; import org.shredzone.acme4j.Identifier; import org.shredzone.acme4j.Problem; import org.shredzone.acme4j.Status; +import org.shredzone.acme4j.exception.AcmeNotSupportedException; import org.shredzone.acme4j.exception.AcmeProtocolException; /** @@ -159,6 +160,21 @@ public final class JSON implements Serializable { data.get(key)); } + /** + * Returns the {@link Value} of the given key. + * + * @param key + * Key to read + * @return {@link Value} of the key + * @throws AcmeNotSupportedException + * if the key is not present. The key is used as feature name. + */ + public Value getFeature(String key) { + return new Value( + path.isEmpty() ? key : path + '.' + key, + data.get(key)).onFeature(key); + } + /** * Returns the content as JSON string. */ @@ -304,6 +320,22 @@ public final class JSON implements Serializable { return val != null ? Optional.of(this) : Optional.empty(); } + /** + * Returns this value. If the value was {@code null}, an + * {@link AcmeNotSupportedException} is thrown. This method is used for mandatory + * fields that are only present if a certain feature is supported by the server. + * + * @param feature + * Feature name + * @return itself + */ + public Value onFeature(String feature) { + if (val == null) { + throw new AcmeNotSupportedException(feature); + } + return this; + } + /** * Returns this value as an {@link Optional} of the desired type, for further * mapping and filtering. diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/OrderBuilderTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/OrderBuilderTest.java index 6dad5a6b..5c76120e 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/OrderBuilderTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/OrderBuilderTest.java @@ -30,7 +30,7 @@ import java.util.Optional; import org.assertj.core.api.AutoCloseableSoftAssertions; import org.junit.jupiter.api.Test; import org.shredzone.acme4j.connector.Resource; -import org.shredzone.acme4j.exception.AcmeException; +import org.shredzone.acme4j.exception.AcmeNotSupportedException; import org.shredzone.acme4j.provider.TestableConnectionProvider; import org.shredzone.acme4j.toolbox.JSON; import org.shredzone.acme4j.toolbox.JSONBuilder; @@ -107,11 +107,16 @@ public class OrderBuilderTest { .isEqualTo("2016-01-10T00:00:00Z"); softly.assertThat(order.getStatus()).isEqualTo(Status.PENDING); softly.assertThat(order.isAutoRenewing()).isFalse(); - softly.assertThat(order.getAutoRenewalStartDate()).isEmpty(); - softly.assertThat(order.getAutoRenewalEndDate()).isEmpty(); - softly.assertThat(order.getAutoRenewalLifetime()).isEmpty(); - softly.assertThat(order.getAutoRenewalLifetimeAdjust()).isEmpty(); - softly.assertThat(order.isAutoRenewalGetEnabled()).isFalse(); + softly.assertThatExceptionOfType(AcmeNotSupportedException.class) + .isThrownBy(order::getAutoRenewalStartDate); + softly.assertThatExceptionOfType(AcmeNotSupportedException.class) + .isThrownBy(order::getAutoRenewalEndDate); + softly.assertThatExceptionOfType(AcmeNotSupportedException.class) + .isThrownBy(order::getAutoRenewalLifetime); + softly.assertThatExceptionOfType(AcmeNotSupportedException.class) + .isThrownBy(order::getAutoRenewalLifetimeAdjust); + softly.assertThatExceptionOfType(AcmeNotSupportedException.class) + .isThrownBy(order::isAutoRenewalGetEnabled); softly.assertThat(order.getLocation()).isEqualTo(locationUrl); softly.assertThat(order.getAuthorizations()).isNotNull(); softly.assertThat(order.getAuthorizations()).hasSize(2); @@ -172,8 +177,8 @@ public class OrderBuilderTest { softly.assertThat(order.getNotAfter()).isEmpty(); softly.assertThat(order.isAutoRenewing()).isTrue(); softly.assertThat(order.getAutoRenewalStartDate().orElseThrow()).isEqualTo(autoRenewStart); - softly.assertThat(order.getAutoRenewalEndDate().orElseThrow()).isEqualTo(autoRenewEnd); - softly.assertThat(order.getAutoRenewalLifetime().orElseThrow()).isEqualTo(validity); + softly.assertThat(order.getAutoRenewalEndDate()).isEqualTo(autoRenewEnd); + softly.assertThat(order.getAutoRenewalLifetime()).isEqualTo(validity); softly.assertThat(order.getAutoRenewalLifetimeAdjust().orElseThrow()).isEqualTo(predate); softly.assertThat(order.isAutoRenewalGetEnabled()).isTrue(); softly.assertThat(order.getLocation()).isEqualTo(locationUrl); @@ -187,7 +192,7 @@ public class OrderBuilderTest { */ @Test public void testAutoRenewOrderCertificateFails() { - assertThrows(AcmeException.class, () -> { + assertThrows(AcmeNotSupportedException.class, () -> { var provider = new TestableConnectionProvider(); provider.putTestResource(Resource.NEW_ORDER, resourceUrl); diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/OrderTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/OrderTest.java index a35ae955..dde465f6 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/OrderTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/OrderTest.java @@ -26,6 +26,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import org.assertj.core.api.AutoCloseableSoftAssertions; import org.junit.jupiter.api.Test; +import org.shredzone.acme4j.exception.AcmeNotSupportedException; import org.shredzone.acme4j.provider.TestableConnectionProvider; import org.shredzone.acme4j.toolbox.JSON; import org.shredzone.acme4j.toolbox.JSONBuilder; @@ -84,11 +85,16 @@ public class OrderTest { softly.assertThat(order.getFinalizeLocation()).isEqualTo(finalizeUrl); softly.assertThat(order.isAutoRenewing()).isFalse(); - softly.assertThat(order.getAutoRenewalStartDate()).isEmpty(); - softly.assertThat(order.getAutoRenewalEndDate()).isEmpty(); - softly.assertThat(order.getAutoRenewalLifetime()).isEmpty(); - softly.assertThat(order.getAutoRenewalLifetimeAdjust()).isEmpty(); - softly.assertThat(order.isAutoRenewalGetEnabled()).isFalse(); + softly.assertThatExceptionOfType(AcmeNotSupportedException.class) + .isThrownBy(order::getAutoRenewalStartDate); + softly.assertThatExceptionOfType(AcmeNotSupportedException.class) + .isThrownBy(order::getAutoRenewalEndDate); + softly.assertThatExceptionOfType(AcmeNotSupportedException.class) + .isThrownBy(order::getAutoRenewalLifetime); + softly.assertThatExceptionOfType(AcmeNotSupportedException.class) + .isThrownBy(order::getAutoRenewalLifetimeAdjust); + softly.assertThatExceptionOfType(AcmeNotSupportedException.class) + .isThrownBy(order::isAutoRenewalGetEnabled); softly.assertThat(order.getError()).isNotEmpty(); softly.assertThat(order.getError().orElseThrow().getType()) @@ -261,9 +267,9 @@ public class OrderTest { softly.assertThat(order.isAutoRenewing()).isTrue(); softly.assertThat(order.getAutoRenewalStartDate().orElseThrow()) .isEqualTo("2016-01-01T00:00:00Z"); - softly.assertThat(order.getAutoRenewalEndDate().orElseThrow()) + softly.assertThat(order.getAutoRenewalEndDate()) .isEqualTo("2017-01-01T00:00:00Z"); - softly.assertThat(order.getAutoRenewalLifetime().orElseThrow()) + softly.assertThat(order.getAutoRenewalLifetime()) .isEqualTo(Duration.ofHours(168)); softly.assertThat(order.getAutoRenewalLifetimeAdjust().orElseThrow()) .isEqualTo(Duration.ofDays(6)); @@ -302,15 +308,16 @@ public class OrderTest { var order = login.bindOrder(locationUrl); try (var softly = new AutoCloseableSoftAssertions()) { - softly.assertThat(order.getCertificate()).isEmpty(); + softly.assertThatExceptionOfType(IllegalStateException.class) + .isThrownBy(order::getCertificate); softly.assertThat(order.getAutoRenewalCertificate().orElseThrow().getLocation()) .isEqualTo(url("https://example.com/acme/cert/1234")); softly.assertThat(order.isAutoRenewing()).isTrue(); softly.assertThat(order.getAutoRenewalStartDate().orElseThrow()) .isEqualTo("2018-01-01T00:00:00Z"); - softly.assertThat(order.getAutoRenewalEndDate().orElseThrow()) + softly.assertThat(order.getAutoRenewalEndDate()) .isEqualTo("2019-01-01T00:00:00Z"); - softly.assertThat(order.getAutoRenewalLifetime().orElseThrow()) + softly.assertThat(order.getAutoRenewalLifetime()) .isEqualTo(Duration.ofHours(168)); softly.assertThat(order.getAutoRenewalLifetimeAdjust().orElseThrow()) .isEqualTo(Duration.ofDays(6)); diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/SessionTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/SessionTest.java index c1cee8e8..cadcd056 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/SessionTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/SessionTest.java @@ -30,6 +30,7 @@ import org.junit.jupiter.api.Test; import org.mockito.ArgumentMatchers; import org.shredzone.acme4j.connector.Resource; import org.shredzone.acme4j.exception.AcmeException; +import org.shredzone.acme4j.exception.AcmeNotSupportedException; import org.shredzone.acme4j.provider.AcmeProvider; import org.shredzone.acme4j.provider.GenericAcmeProvider; import org.shredzone.acme4j.toolbox.AcmeUtils; @@ -169,10 +170,8 @@ public class SessionTest { .isEqualTo("https://www.example.com/"); softly.assertThat(meta.getCaaIdentities()).containsExactlyInAnyOrder("example.com"); softly.assertThat(meta.isAutoRenewalEnabled()).isTrue(); - softly.assertThat(meta.getAutoRenewalMaxDuration().orElseThrow()) - .isEqualTo(Duration.ofDays(365)); - softly.assertThat(meta.getAutoRenewalMinLifetime().orElseThrow()) - .isEqualTo(Duration.ofHours(24)); + softly.assertThat(meta.getAutoRenewalMaxDuration()).isEqualTo(Duration.ofDays(365)); + softly.assertThat(meta.getAutoRenewalMinLifetime()).isEqualTo(Duration.ofHours(24)); softly.assertThat(meta.isAutoRenewalGetAllowed()).isTrue(); softly.assertThat(meta.isExternalAccountRequired()).isTrue(); softly.assertThat(meta.getJSON()).isNotNull(); @@ -218,9 +217,12 @@ public class SessionTest { softly.assertThat(meta.getWebsite()).isEmpty(); softly.assertThat(meta.getCaaIdentities()).isEmpty(); softly.assertThat(meta.isAutoRenewalEnabled()).isFalse(); - softly.assertThat(meta.getAutoRenewalMaxDuration()).isEmpty(); - softly.assertThat(meta.getAutoRenewalMinLifetime()).isEmpty(); - softly.assertThat(meta.isAutoRenewalGetAllowed()).isFalse(); + softly.assertThatExceptionOfType(AcmeNotSupportedException.class) + .isThrownBy(meta::getAutoRenewalMaxDuration); + softly.assertThatExceptionOfType(AcmeNotSupportedException.class) + .isThrownBy(meta::getAutoRenewalMinLifetime); + softly.assertThatExceptionOfType(AcmeNotSupportedException.class) + .isThrownBy(meta::isAutoRenewalGetAllowed); } } diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/exception/AcmeNotSupportedExceptionTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/exception/AcmeNotSupportedExceptionTest.java new file mode 100644 index 00000000..88260e54 --- /dev/null +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/exception/AcmeNotSupportedExceptionTest.java @@ -0,0 +1,34 @@ +/* + * acme4j - Java ACME client + * + * Copyright (C) 2023 Richard "Shred" Körber + * http://acme4j.shredzone.org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + */ +package org.shredzone.acme4j.exception; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +/** + * Unit tests for {@link AcmeNotSupportedException}. + */ +public class AcmeNotSupportedExceptionTest { + + @Test + public void testAcmeNotSupportedException() { + var msg = "revoke"; + var ex = new AcmeNotSupportedException(msg); + assertThat(ex).isInstanceOf(RuntimeException.class); + assertThat(ex.getMessage()).isEqualTo("Server does not support revoke"); + assertThat(ex.getCause()).isNull(); + } + +} diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/toolbox/JSONTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/toolbox/JSONTest.java index 188cecb7..b1794e5b 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/toolbox/JSONTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/toolbox/JSONTest.java @@ -16,6 +16,7 @@ package org.shredzone.acme4j.toolbox; import static java.nio.charset.StandardCharsets.UTF_8; import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.shredzone.acme4j.toolbox.TestUtils.url; @@ -34,6 +35,7 @@ import java.util.stream.Collectors; import org.junit.jupiter.api.Test; import org.shredzone.acme4j.Status; +import org.shredzone.acme4j.exception.AcmeNotSupportedException; import org.shredzone.acme4j.exception.AcmeProtocolException; import org.shredzone.acme4j.toolbox.JSON.Value; @@ -235,6 +237,14 @@ public class JSONTest { assertThat(json.get("none").optional().isPresent()).isFalse(); assertThat(json.get("none").map(Value::asString).isPresent()).isFalse(); + assertThatExceptionOfType(AcmeNotSupportedException.class) + .isThrownBy(() -> json.getFeature("none")) + .withMessage("Server does not support none"); + + assertThatExceptionOfType(AcmeNotSupportedException.class) + .isThrownBy(() -> json.get("none").onFeature("my-feature")) + .withMessage("Server does not support my-feature"); + assertThrows(AcmeProtocolException.class, () -> json.get("none").asString(), "asString"); diff --git a/src/doc/docs/migration.md b/src/doc/docs/migration.md index 6223d0f6..e2fe2e63 100644 --- a/src/doc/docs/migration.md +++ b/src/doc/docs/migration.md @@ -5,6 +5,7 @@ This document will help you migrate your code to the latest _acme4j_ version. ## Migration to Version 3.0.0 - All `@Nullable` return values have been reviewed. Collections may now be empty, but never `null`. Most of the other return values are now either `Optional`, or throwing an exception if more reasonable. If your code fails to compile because the return type has changed to `Optional`, you can simply add `.orElse(null)` to restore the old behavior. But often your code will reveal a better way to handle the former `null` pointer instead. +- A new `AcmeNotSupportedException` is thrown if a feature is not supported by the server. It is a subclass of the `AcmeProtocolException` runtime exception. - Starting with _acme4j_ v3, we will require the smallest Java SE LTS version that is still receiving premier support according to the [Oracle Java SE Support Roadmap](https://www.oracle.com/java/technologies/java-se-support-roadmap.html). At the moment of writing, these are Java 11 and Java 17, so _acme4j_ requires Java 11 starting from now. With the prospected release of Java 21 (LTS) in September 2023, we will start to require Java 17, and so on. If you still need Java 8, you can use _acme4j_ v2, which will receive bugfixes until September 2023. - Changed to `java.net.http` client. Due to limitations of the API, HTTP errors are only thrown with the error code, but not with the error message. If you checked the message in unit tests, be prepared that the error message might have changed. - acme4j now accepts HTTP gzip compression. It is enabled by default, but can be disabled in the `NetworkSettings` if it causes problems or impedes debugging.