diff --git a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MimeTypes.java b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MimeTypes.java index 8cc493092f53..ad7d4991c0ef 100644 --- a/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MimeTypes.java +++ b/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MimeTypes.java @@ -18,12 +18,14 @@ import java.io.InputStreamReader; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Properties; +import java.util.Set; import org.eclipse.jetty.util.FileID; import org.eclipse.jetty.util.Index; @@ -37,6 +39,12 @@ public class MimeTypes { static final Logger LOG = LoggerFactory.getLogger(MimeTypes.class); + private static final Set KNOWN_LOCALES = Set.copyOf(Arrays.asList(Locale.getAvailableLocales())); + + public static boolean isKnownLocale(Locale locale) + { + return KNOWN_LOCALES.contains(locale); + } /** Enumeration of predefined MimeTypes. This is not exhaustive */ public enum Type diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java index ae6a849c1b4a..2dbf39788887 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/Request.java @@ -20,22 +20,22 @@ import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.security.Principal; +import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.concurrent.TimeoutException; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; -import java.util.stream.Collectors; import org.eclipse.jetty.http.CookieCache; import org.eclipse.jetty.http.HttpCookie; -import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpScheme; import org.eclipse.jetty.http.HttpURI; import org.eclipse.jetty.http.MetaData; +import org.eclipse.jetty.http.MimeTypes; import org.eclipse.jetty.http.Trailers; import org.eclipse.jetty.io.Content; import org.eclipse.jetty.server.internal.HttpChannelState; @@ -119,6 +119,7 @@ public interface Request extends Attributes, Content.Source { String CACHE_ATTRIBUTE = Request.class.getCanonicalName() + ".CookieCache"; String COOKIE_ATTRIBUTE = Request.class.getCanonicalName() + ".Cookies"; + List DEFAULT_LOCALES = List.of(Locale.getDefault()); /** * an ID unique within the lifetime scope of the {@link ConnectionMetaData#getId()}). @@ -452,26 +453,27 @@ static List getLocales(Request request) { HttpFields fields = request.getHeaders(); if (fields == null) - return List.of(Locale.getDefault()); + return DEFAULT_LOCALES; List acceptable = fields.getQualityCSV(HttpHeader.ACCEPT_LANGUAGE); - // handle no locale - if (acceptable.isEmpty()) - return List.of(Locale.getDefault()); - - return acceptable.stream().map(language -> + // return sorted list of locals, with known locales in quality order before unknown locales in quality order + return switch (acceptable.size()) { - language = HttpField.stripParameters(language); - String country = ""; - int dash = language.indexOf('-'); - if (dash > -1) + case 0 -> DEFAULT_LOCALES; + case 1 -> List.of(Locale.forLanguageTag(acceptable.get(0))); + default -> { - country = language.substring(dash + 1).trim(); - language = language.substring(0, dash).trim(); + List locales = acceptable.stream().map(Locale::forLanguageTag).toList(); + List known = locales.stream().filter(MimeTypes::isKnownLocale).toList(); + if (known.size() == locales.size()) + yield locales; // All locales are known + List unknown = locales.stream().filter(l -> !MimeTypes.isKnownLocale(l)).toList(); + locales = new ArrayList<>(known); + locales.addAll(unknown); + yield locales; // List of known locales before unknown locales } - return new Locale(language, country); - }).collect(Collectors.toList()); + }; } // TODO: consider inline and remove. diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java index 08d9f4534022..d81bab9c4e73 100644 --- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java +++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java @@ -17,7 +17,9 @@ import java.nio.ByteBuffer; import java.util.Arrays; import java.util.List; +import java.util.Locale; import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; import org.eclipse.jetty.http.HttpCookie; import org.eclipse.jetty.http.HttpHeader; @@ -30,6 +32,9 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; @@ -372,6 +377,39 @@ public boolean handle(org.eclipse.jetty.server.Request request, Response respons assertThat(response.getStatus(), is(HttpStatus.OK_200)); } + public static Stream localeTests() + { + return Stream.of( + Arguments.of(null, List.of(Locale.getDefault().toLanguageTag()).toString()), + Arguments.of("zz", "[zz]"), + Arguments.of("en", "[en]"), + Arguments.of("en-gb", List.of(Locale.UK.toLanguageTag()).toString()), + Arguments.of("en-us", List.of(Locale.US.toLanguageTag()).toString()), + Arguments.of("EN-US", List.of(Locale.US.toLanguageTag()).toString()), + Arguments.of("en-us,en-gb", List.of(Locale.US.toLanguageTag(), Locale.UK.toLanguageTag()).toString()), + Arguments.of("en-us;q=0.5,fr;q=0.0,en-gb;q=1.0", List.of(Locale.UK.toLanguageTag(), Locale.US.toLanguageTag()).toString()), + Arguments.of("en-us;q=0.5,zz-yy;q=0.7,en-gb;q=1.0", List.of(Locale.UK.toLanguageTag(), Locale.US.toLanguageTag(), "zz-YY").toString()) + ); + } + + @ParameterizedTest + @MethodSource("localeTests") + public void testAcceptableLocales(String acceptLanguage, String expectedLocales) throws Exception + { + acceptLanguage = acceptLanguage == null ? "" : (HttpHeader.ACCEPT_LANGUAGE.asString() + ": " + acceptLanguage + "\n"); + String rawRequest = """ + GET / HTTP/1.1 + Host: tester + Connection: close + %s + """.formatted(acceptLanguage); + + HttpTester.Response response = HttpTester.parseResponse(connector.getResponse(rawRequest)); + assertNotNull(response); + assertThat(response.getStatus(), is(HttpStatus.OK_200)); + assertThat(response.getContent(), containsString("locales=" + expectedLocales)); + } + private static void checkCookieResult(String containedCookie, String[] notContainedCookies, String response) { assertNotNull(containedCookie); diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/DumpHandler.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/DumpHandler.java index 783b4e763042..8e4037113556 100644 --- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/DumpHandler.java +++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/DumpHandler.java @@ -17,6 +17,7 @@ import java.io.OutputStreamWriter; import java.io.Writer; import java.nio.charset.StandardCharsets; +import java.util.Locale; import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpHeader; @@ -151,6 +152,7 @@ public boolean handle(Request request, Response response, Callback callback) thr writer.write("
httpURI.path=" + httpURI.getPath() + "

\n"); writer.write("
httpURI.query=" + httpURI.getQuery() + "

\n"); writer.write("
httpURI.pathQuery=" + httpURI.getPathQuery() + "

\n"); + writer.write("
locales=" + Request.getLocales(request).stream().map(Locale::toLanguageTag).toList() + "

\n"); writer.write("
pathInContext=" + Request.getPathInContext(request) + "

\n"); writer.write("
contentType=" + request.getHeaders().get(HttpHeader.CONTENT_TYPE) + "

\n"); writer.write("
servername=" + Request.getServerName(request) + "

\n");