/* * Copyright 2012 Jason Miller * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package jj.http.server; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpHeaders; /** * <p> * Parses the accept-lang header from a given headers set, * and makes the contents available as a {@link List} of * {@link Locale}s in order of preference as determined by * q-values * * <p> * The algorithm is kinda wasteful of memory. oh well * * @author jason * */ class AcceptLangHeaderReader { private static final class SortableLocale implements Comparable<SortableLocale> { final Locale locale; final BigDecimal qValue; SortableLocale(final Locale locale, final String qValue) { this.locale = locale; this.qValue = new BigDecimal(qValue); } @Override public int compareTo(SortableLocale o) { // sort them in reverse! return o.qValue.compareTo(qValue); } } private static final Pattern LIST_SPLITTER = Pattern.compile("\\s*,\\s*"); private static final Pattern VALUE_PARSER = // this pattern is kinda complicated to read, it is meant to match the grammar specified at // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4 // group 1 is the lang // group 2 is the country, if any // group 3 is the q-value, if any Pattern.compile("^(?:([a-zA-Z]{1,8}(?:-[a-zA-Z]{1,8})?)|\\*)(?:;q=((?:0\\.\\d{1,3})|1|0))?$"); private final List<Locale> locales; private boolean badRequest = false; // need to get a hold of the server default locale somewhere? AcceptLangHeaderReader(final HttpHeaders requestHeaders) { locales = parseLocales(LIST_SPLITTER.split(requestHeaders.get(HttpHeaderNames.ACCEPT_LANGUAGE))); } private List<Locale> parseLocales(String[] incomingValues) { ArrayList<SortableLocale> holder = new ArrayList<>(incomingValues.length); for (String value : incomingValues) { Matcher matcher = VALUE_PARSER.matcher(value); if (!matcher.matches()) { // it either matches or we hates you. HATES badRequest = true; break; } String languageTag = matcher.group(1); String qValue = matcher.group(2); if (qValue == null) qValue = "1"; Locale locale = Locale.forLanguageTag(languageTag); if (!"0".equals(qValue)) { // "0" means don't use... which really we can't guarantee, // if, for instance, it's the only Locale we support holder.add(new SortableLocale(locale, qValue)); } } Collections.sort(holder); ArrayList<Locale> result = new ArrayList<>(holder.size()); for (SortableLocale sortableLocale : holder) { result.add(sortableLocale.locale); } return Collections.unmodifiableList(result); } public List<Locale> locales() { return locales; } public boolean isBadRequest() { return badRequest; } }