From 41bc574f75d24bc71242befd135914e1d69a3bcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20K=C3=B6rber?= Date: Mon, 24 Apr 2023 21:27:06 +0200 Subject: [PATCH] Enhance Accept-Language header Before this patch, it was only the language tag of the selected Locale. Now it also offers the language itself (without the country) and any other available language as fallback. It is also possible to set the locale to null, which will accept any language. --- .../java/org/shredzone/acme4j/Session.java | 22 +++++++++-- .../acme4j/connector/DefaultConnection.java | 2 +- .../shredzone/acme4j/toolbox/AcmeUtils.java | 24 ++++++++++++ .../org/shredzone/acme4j/SessionTest.java | 26 +++++++++++++ .../connector/DefaultConnectionTest.java | 39 ++++++++++--------- .../acme4j/toolbox/AcmeUtilsTest.java | 20 ++++++++++ 6 files changed, 111 insertions(+), 22 deletions(-) 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. */