diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/Session.java b/acme4j-client/src/main/java/org/shredzone/acme4j/Session.java index 6d2db56c..3ffd51eb 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/Session.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/Session.java @@ -32,6 +32,7 @@ import org.shredzone.acme4j.connector.Resource; import org.shredzone.acme4j.exception.AcmeException; import org.shredzone.acme4j.provider.AcmeProvider; import org.shredzone.acme4j.provider.GenericAcmeProvider; +import org.shredzone.acme4j.toolbox.AcmeUtils; import org.shredzone.acme4j.toolbox.JSON; import org.shredzone.acme4j.toolbox.JSON.Value; @@ -49,7 +50,8 @@ public class Session { private final AcmeProvider provider; private @Nullable String nonce; - private Locale locale = Locale.getDefault(); + private @Nullable Locale locale = Locale.getDefault(); + private String languageHeader = AcmeUtils.localeToLanguageHeader(Locale.getDefault()); protected @Nullable ZonedDateTime directoryLastModified; protected @Nullable ZonedDateTime directoryExpires; @@ -147,8 +149,10 @@ public class Session { } /** - * Gets the current locale of this session. + * Gets the current locale of this session, or {@code null} if no special language is + * selected. */ + @Nullable public Locale getLocale() { return locale; } @@ -156,9 +160,21 @@ public class Session { /** * Sets the locale used in this session. The locale is passed to the server as * Accept-Language header. The server may respond with localized messages. + * The default is the system's language. If set to {@code null}, no special language + * is selected. */ public void setLocale(@Nullable Locale locale) { - this.locale = locale != null ? locale : Locale.getDefault(); + this.locale = locale; + this.languageHeader = AcmeUtils.localeToLanguageHeader(locale); + } + + /** + * Gets an Accept-Language header value that matches the current locale. + * + * @since 3.0.0 + */ + public String getLanguageHeader() { + return languageHeader; } /** diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/connector/DefaultConnection.java b/acme4j-client/src/main/java/org/shredzone/acme4j/connector/DefaultConnection.java index 96f8bae2..646231cd 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/connector/DefaultConnection.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/connector/DefaultConnection.java @@ -347,7 +347,7 @@ public class DefaultConnection implements Connection { try { var builder = httpConnector.createRequestBuilder(url) .header(ACCEPT_CHARSET_HEADER, DEFAULT_CHARSET) - .header(ACCEPT_LANGUAGE_HEADER, session.getLocale().toLanguageTag()); + .header(ACCEPT_LANGUAGE_HEADER, session.getLanguageHeader()); if (session.networkSettings().isCompressionEnabled()) { builder.header(ACCEPT_ENCODING_HEADER, "gzip"); diff --git a/acme4j-client/src/main/java/org/shredzone/acme4j/toolbox/AcmeUtils.java b/acme4j-client/src/main/java/org/shredzone/acme4j/toolbox/AcmeUtils.java index ccbf8747..9752bd66 100644 --- a/acme4j-client/src/main/java/org/shredzone/acme4j/toolbox/AcmeUtils.java +++ b/acme4j-client/src/main/java/org/shredzone/acme4j/toolbox/AcmeUtils.java @@ -26,6 +26,7 @@ import java.time.Instant; import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.Base64; +import java.util.Locale; import java.util.Objects; import java.util.regex.Pattern; @@ -221,6 +222,29 @@ public final class AcmeUtils { ZoneId.of(tz)).toInstant(); } + /** + * Converts the given locale to an Accept-Language header value. + * + * @param locale + * {@link Locale} to be used in the header + * @return Value that can be used in an Accept-Language header + */ + public static String localeToLanguageHeader(@Nullable Locale locale) { + if (locale == null || "und".equals(locale.toLanguageTag())) { + return "*"; + } + + var langTag = locale.toLanguageTag(); + + var header = new StringBuilder(langTag); + if (langTag.indexOf('-') >= 0) { + header.append(',').append(locale.getLanguage()).append(";q=0.8"); + } + header.append(",*;q=0.1"); + + return header.toString(); + } + /** * Strips the acme error prefix from the error string. *
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 4a857413..88f2d568 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/SessionTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/SessionTest.java @@ -23,6 +23,7 @@ import java.net.URI; import java.net.URL; import java.time.Duration; import java.time.ZonedDateTime; +import java.util.Locale; import org.assertj.core.api.AutoCloseableSoftAssertions; import org.junit.jupiter.api.Test; @@ -31,6 +32,7 @@ import org.shredzone.acme4j.connector.Resource; import org.shredzone.acme4j.exception.AcmeException; import org.shredzone.acme4j.provider.AcmeProvider; import org.shredzone.acme4j.provider.GenericAcmeProvider; +import org.shredzone.acme4j.toolbox.AcmeUtils; import org.shredzone.acme4j.toolbox.TestUtils; /** @@ -212,4 +214,28 @@ public class SessionTest { } } + /** + * Test that the locale is properly set. + */ + @Test + public void testLocale() { + var session = new Session(URI.create(TestUtils.ACME_SERVER_URI)); + + // default configuration + assertThat(session.getLocale()) + .isEqualTo(Locale.getDefault()); + assertThat(session.getLanguageHeader()) + .isEqualTo(AcmeUtils.localeToLanguageHeader(Locale.getDefault())); + + // null + session.setLocale(null); + assertThat(session.getLocale()).isNull(); + assertThat(session.getLanguageHeader()).isEqualTo("*"); + + // a locale + session.setLocale(Locale.CANADA_FRENCH); + assertThat(session.getLocale()).isEqualTo(Locale.CANADA_FRENCH); + assertThat(session.getLanguageHeader()).isEqualTo("fr-CA,fr;q=0.8,*;q=0.1"); + } + } diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/connector/DefaultConnectionTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/connector/DefaultConnectionTest.java index 80a84472..60fb4f32 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/connector/DefaultConnectionTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/connector/DefaultConnectionTest.java @@ -72,6 +72,9 @@ public class DefaultConnectionTest { private static final String DIRECTORY_PATH = "/dir"; private static final String NEW_NONCE_PATH = "/newNonce"; private static final String REQUEST_PATH = "/test/test"; + private static final String TEST_ACCEPT_LANGUAGE = "ja-JP,ja;q=0.8,*;q=0.1"; + private static final String TEST_ACCEPT_CHARSET = "utf-8"; + private static final String TEST_USER_AGENT_PATTERN = "^acme4j/.*$"; private final URL accountUrl = TestUtils.url(TestUtils.ACCOUNT_URL); private Session session; @@ -564,9 +567,9 @@ public class DefaultConnectionTest { verify(getRequestedFor(urlEqualTo(REQUEST_PATH)) .withHeader("Accept", equalTo("application/json")) - .withHeader("Accept-Charset", equalTo("utf-8")) - .withHeader("Accept-Language", equalTo("ja-JP")) - .withHeader("User-Agent", matching("^acme4j/.*$")) + .withHeader("Accept-Charset", equalTo(TEST_ACCEPT_CHARSET)) + .withHeader("Accept-Language", equalTo(TEST_ACCEPT_LANGUAGE)) + .withHeader("User-Agent", matching(TEST_USER_AGENT_PATTERN)) ); } @@ -589,9 +592,9 @@ public class DefaultConnectionTest { verify(getRequestedFor(urlEqualTo(REQUEST_PATH)) .withHeader("If-Modified-Since", equalToDateTime(ifModifiedSince)) .withHeader("Accept", equalTo("application/json")) - .withHeader("Accept-Charset", equalTo("utf-8")) - .withHeader("Accept-Language", equalTo("ja-JP")) - .withHeader("User-Agent", matching("^acme4j/.*$")) + .withHeader("Accept-Charset", equalTo(TEST_ACCEPT_CHARSET)) + .withHeader("Accept-Language", equalTo(TEST_ACCEPT_LANGUAGE)) + .withHeader("User-Agent", matching(TEST_USER_AGENT_PATTERN)) ); } @@ -620,9 +623,9 @@ public class DefaultConnectionTest { verify(postRequestedFor(urlEqualTo(REQUEST_PATH)) .withHeader("Accept", equalTo("application/json")) - .withHeader("Accept-Charset", equalTo("utf-8")) - .withHeader("Accept-Language", equalTo("ja-JP")) - .withHeader("User-Agent", matching("^acme4j/.*$")) + .withHeader("Accept-Charset", equalTo(TEST_ACCEPT_CHARSET)) + .withHeader("Accept-Language", equalTo(TEST_ACCEPT_LANGUAGE)) + .withHeader("User-Agent", matching(TEST_USER_AGENT_PATTERN)) ); var requests = findAll(postRequestedFor(urlEqualTo(REQUEST_PATH))); @@ -673,10 +676,10 @@ public class DefaultConnectionTest { verify(postRequestedFor(urlEqualTo(REQUEST_PATH)) .withHeader("Accept", equalTo("application/json")) - .withHeader("Accept-Charset", equalTo("utf-8")) - .withHeader("Accept-Language", equalTo("ja-JP")) + .withHeader("Accept-Charset", equalTo(TEST_ACCEPT_CHARSET)) + .withHeader("Accept-Language", equalTo(TEST_ACCEPT_LANGUAGE)) .withHeader("Content-Type", equalTo("application/jose+json")) - .withHeader("User-Agent", matching("^acme4j/.*$")) + .withHeader("User-Agent", matching(TEST_USER_AGENT_PATTERN)) ); var requests = findAll(postRequestedFor(urlEqualTo(REQUEST_PATH))); @@ -727,10 +730,10 @@ public class DefaultConnectionTest { verify(postRequestedFor(urlEqualTo(REQUEST_PATH)) .withHeader("Accept", equalTo("application/pem-certificate-chain")) - .withHeader("Accept-Charset", equalTo("utf-8")) - .withHeader("Accept-Language", equalTo("ja-JP")) + .withHeader("Accept-Charset", equalTo(TEST_ACCEPT_CHARSET)) + .withHeader("Accept-Language", equalTo(TEST_ACCEPT_LANGUAGE)) .withHeader("Content-Type", equalTo("application/jose+json")) - .withHeader("User-Agent", matching("^acme4j/.*$")) + .withHeader("User-Agent", matching(TEST_USER_AGENT_PATTERN)) ); } @@ -758,10 +761,10 @@ public class DefaultConnectionTest { verify(postRequestedFor(urlEqualTo(REQUEST_PATH)) .withHeader("Accept", equalTo("application/json")) - .withHeader("Accept-Charset", equalTo("utf-8")) - .withHeader("Accept-Language", equalTo("ja-JP")) + .withHeader("Accept-Charset", equalTo(TEST_ACCEPT_CHARSET)) + .withHeader("Accept-Language", equalTo(TEST_ACCEPT_LANGUAGE)) .withHeader("Content-Type", equalTo("application/jose+json")) - .withHeader("User-Agent", matching("^acme4j/.*$")) + .withHeader("User-Agent", matching(TEST_USER_AGENT_PATTERN)) ); var requests = findAll(postRequestedFor(urlEqualTo(REQUEST_PATH))); diff --git a/acme4j-client/src/test/java/org/shredzone/acme4j/toolbox/AcmeUtilsTest.java b/acme4j-client/src/test/java/org/shredzone/acme4j/toolbox/AcmeUtilsTest.java index 383c311a..12d4c0c7 100644 --- a/acme4j-client/src/test/java/org/shredzone/acme4j/toolbox/AcmeUtilsTest.java +++ b/acme4j-client/src/test/java/org/shredzone/acme4j/toolbox/AcmeUtilsTest.java @@ -27,6 +27,7 @@ import java.net.URI; import java.security.Security; import java.security.cert.CertificateEncodingException; import java.time.temporal.ChronoUnit; +import java.util.Locale; import java.util.stream.Stream; import org.bouncycastle.jce.provider.BouncyCastleProvider; @@ -201,6 +202,25 @@ public class AcmeUtilsTest { "accepted string without time"); } + /** + * Test that locales are correctly converted to language headers. + */ + @Test + public void testLocaleToLanguageHeader() { + assertThat(localeToLanguageHeader(Locale.ENGLISH)) + .isEqualTo("en,*;q=0.1"); + assertThat(localeToLanguageHeader(new Locale("en", "US"))) + .isEqualTo("en-US,en;q=0.8,*;q=0.1"); + assertThat(localeToLanguageHeader(Locale.GERMAN)) + .isEqualTo("de,*;q=0.1"); + assertThat(localeToLanguageHeader(Locale.GERMANY)) + .isEqualTo("de-DE,de;q=0.8,*;q=0.1"); + assertThat(localeToLanguageHeader(new Locale(""))) + .isEqualTo("*"); + assertThat(localeToLanguageHeader(null)) + .isEqualTo("*"); + } + /** * Test that error prefix is correctly removed. */