Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement quality lists for Locales #9983

Merged
merged 1 commit into from
Jun 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -37,6 +39,12 @@
public class MimeTypes
{
static final Logger LOG = LoggerFactory.getLogger(MimeTypes.class);
private static final Set<Locale> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<Locale> DEFAULT_LOCALES = List.of(Locale.getDefault());

/**
* an ID unique within the lifetime scope of the {@link ConnectionMetaData#getId()}).
Expand Down Expand Up @@ -452,26 +453,27 @@ static List<Locale> getLocales(Request request)
{
HttpFields fields = request.getHeaders();
if (fields == null)
return List.of(Locale.getDefault());
return DEFAULT_LOCALES;

List<String> 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)));
lorban marked this conversation as resolved.
Show resolved Hide resolved
default ->
{
country = language.substring(dash + 1).trim();
language = language.substring(0, dash).trim();
List<Locale> locales = acceptable.stream().map(Locale::forLanguageTag).toList();
lorban marked this conversation as resolved.
Show resolved Hide resolved
List<Locale> known = locales.stream().filter(MimeTypes::isKnownLocale).toList();
if (known.size() == locales.size())
yield locales; // All locales are known
List<Locale> 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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<Arguments> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -151,6 +152,7 @@ public boolean handle(Request request, Response response, Callback callback) thr
writer.write("<pre>httpURI.path=" + httpURI.getPath() + "</pre><br/>\n");
writer.write("<pre>httpURI.query=" + httpURI.getQuery() + "</pre><br/>\n");
writer.write("<pre>httpURI.pathQuery=" + httpURI.getPathQuery() + "</pre><br/>\n");
writer.write("<pre>locales=" + Request.getLocales(request).stream().map(Locale::toLanguageTag).toList() + "</pre><br/>\n");
writer.write("<pre>pathInContext=" + Request.getPathInContext(request) + "</pre><br/>\n");
writer.write("<pre>contentType=" + request.getHeaders().get(HttpHeader.CONTENT_TYPE) + "</pre><br/>\n");
writer.write("<pre>servername=" + Request.getServerName(request) + "</pre><br/>\n");
Expand Down