AcceptLanguageHeader.java

/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions copyright [year] [name of copyright owner]".
 *
 * Copyright 2015 ForgeRock AS.
 */

package org.forgerock.http.header;

import static java.util.Collections.*;
import static org.forgerock.http.header.HeaderUtil.*;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;

import org.forgerock.http.protocol.Header;
import org.forgerock.util.Pair;
import org.forgerock.util.i18n.PreferredLocales;

/**
 * A header class representing the Accept-Language HTTP header. String values will include quality
 * attributes to communicate order of preference expressed in the list of {@code Locale} objects
 * contained within.
 */
public final class AcceptLanguageHeader extends Header {

    /**
     * The name of the header.
     */
    public static final String NAME = "Accept-Language";
    private static final Comparator<Pair<Locale, BigDecimal>> LOCALES_QUALITY_COMPARATOR =
        new Comparator<Pair<Locale, BigDecimal>>() {
            @Override
            public int compare(Pair<Locale, BigDecimal> o1, Pair<Locale, BigDecimal> o2) {
                return o2.getSecond().compareTo(o1.getSecond());
            }
        };

    /**
     * Creates an accept language header representation for a {@code PreferredLocales} instance.
     * @param locales The preferred locales.
     * @return The header.
     */
    public static AcceptLanguageHeader valueOf(PreferredLocales locales) {
        return new AcceptLanguageHeader(locales);
    }

    /**
     * Create a header from a list of preferred {@code Locale} instances.
     * @param locales The preferred locales.
     * @return The header.
     */
    public static AcceptLanguageHeader valueOf(List<Locale> locales) {
        return valueOf(new PreferredLocales(locales));
    }

    /**
     * Create a header from a list of preferred {@code Locale} language tags.
     * @param languageTags The preferred locale language tags.
     * @return The header.
     */
    public static AcceptLanguageHeader valueOf(String... languageTags) {
        List<Locale> locales = new ArrayList<>();
        for (String languageTag : languageTags) {
            locales.add(Locale.forLanguageTag(languageTag));
        }
        return valueOf(new PreferredLocales(locales));
    }

    /**
     * Create a header from a list of header values.
     * @param headerValues The Accept-Language header values.
     * @return The header.
     */
    public static AcceptLanguageHeader valueOf(Set<String> headerValues) {
        if (headerValues == null || headerValues.isEmpty()) {
            return null;
        }

        List<Pair<Locale, BigDecimal>> localeWeightings = new ArrayList<>();
        for (String language : split(join(headerValues, ','), ',')) {
            List<String> values = split(language, ';');
            BigDecimal quality = BigDecimal.ONE;
            if (values.size() == 2) {
                String[] parameter = parseParameter(values.get(1).trim());
                if (!"q".equals(parameter[0])) {
                    throw new IllegalArgumentException("Unrecognised parameter: " + parameter[0]);
                }
                quality = new BigDecimal(parameter[1].trim());
            } else if (values.size() != 1) {
                throw new IllegalArgumentException("Unrecognised parameter(s): " + language);
            }
            localeWeightings.add(Pair.of(Locale.forLanguageTag(values.get(0).trim()), quality));
        }

        sort(localeWeightings, LOCALES_QUALITY_COMPARATOR);
        List<Locale> locales = new ArrayList<>(localeWeightings.size());
        for (Pair<Locale, BigDecimal> locale : localeWeightings) {
            locales.add(locale.getFirst());
        }
        return new AcceptLanguageHeader(new PreferredLocales(locales));
    }

    private final PreferredLocales locales;

    private AcceptLanguageHeader(PreferredLocales locales) {
        this.locales = locales;
    }

    /**
     * Returns the {@code PreferredLocales} instance that represents this header.
     * @return The instance.
     */
    public PreferredLocales getLocales() {
        return locales;
    }

    @Override
    public String getName() {
        return NAME;
    }

    @Override
    public List<String> getValues() {
        StringBuilder valueString = new StringBuilder();
        final List<Locale> locales = this.locales.getLocales();
        BigDecimal qualityStep = getQualityStep(locales.size());
        BigDecimal quality = BigDecimal.ONE;

        for (Locale locale : locales) {
            if (valueString.length() != 0) {
                valueString.append(",");
            }
            valueString.append(locale.equals(Locale.ROOT) ? "*" : locale.toLanguageTag())
                    .append(";q=")
                    .append(quality.toString());
            quality = quality.subtract(qualityStep);
        }

        return singletonList(valueString.toString());
    }

    static BigDecimal getQualityStep(int numberLocales) {
        if (numberLocales <= 1) {
            return BigDecimal.ONE;
        }
        // Find the value to decrement quality by
        // This results in 0.1 for up to 10 locales, 0.01 for up to 100, etc.
        int nextPowerOfTen = (int) Math.ceil(Math.log10((double) numberLocales));
        return BigDecimal.ONE.divide(BigDecimal.TEN.pow(nextPowerOfTen));
    }

    static class Factory extends HeaderFactory<AcceptLanguageHeader> {

        @Override
        public AcceptLanguageHeader parse(String value) {
            return valueOf(singleton(value));
        }

        @Override
        public AcceptLanguageHeader parse(List<String> values) {
            return valueOf(new LinkedHashSet<>(values));
        }
    }
}