001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2015 ForgeRock AS.
015 */
016
017package org.forgerock.http.header;
018
019import static java.util.Collections.*;
020import static org.forgerock.http.header.HeaderUtil.*;
021
022import java.math.BigDecimal;
023import java.util.ArrayList;
024import java.util.Comparator;
025import java.util.LinkedHashSet;
026import java.util.List;
027import java.util.Locale;
028import java.util.Set;
029
030import org.forgerock.http.protocol.Header;
031import org.forgerock.util.Pair;
032import org.forgerock.util.i18n.PreferredLocales;
033
034/**
035 * A header class representing the Accept-Language HTTP header. String values will include quality
036 * attributes to communicate order of preference expressed in the list of {@code Locale} objects
037 * contained within.
038 */
039public final class AcceptLanguageHeader extends Header {
040
041    /**
042     * The name of the header.
043     */
044    public static final String NAME = "Accept-Language";
045    private static final Comparator<Pair<Locale, BigDecimal>> LOCALES_QUALITY_COMPARATOR =
046        new Comparator<Pair<Locale, BigDecimal>>() {
047            @Override
048            public int compare(Pair<Locale, BigDecimal> o1, Pair<Locale, BigDecimal> o2) {
049                return o2.getSecond().compareTo(o1.getSecond());
050            }
051        };
052
053    /**
054     * Creates an accept language header representation for a {@code PreferredLocales} instance.
055     * @param locales The preferred locales.
056     * @return The header.
057     */
058    public static AcceptLanguageHeader valueOf(PreferredLocales locales) {
059        return new AcceptLanguageHeader(locales);
060    }
061
062    /**
063     * Create a header from a list of preferred {@code Locale} instances.
064     * @param locales The preferred locales.
065     * @return The header.
066     */
067    public static AcceptLanguageHeader valueOf(List<Locale> locales) {
068        return valueOf(new PreferredLocales(locales));
069    }
070
071    /**
072     * Create a header from a list of preferred {@code Locale} language tags.
073     * @param languageTags The preferred locale language tags.
074     * @return The header.
075     */
076    public static AcceptLanguageHeader valueOf(String... languageTags) {
077        List<Locale> locales = new ArrayList<>();
078        for (String languageTag : languageTags) {
079            locales.add(Locale.forLanguageTag(languageTag));
080        }
081        return valueOf(new PreferredLocales(locales));
082    }
083
084    /**
085     * Create a header from a list of header values.
086     * @param headerValues The Accept-Language header values.
087     * @return The header.
088     */
089    public static AcceptLanguageHeader valueOf(Set<String> headerValues) {
090        if (headerValues == null || headerValues.isEmpty()) {
091            return null;
092        }
093
094        List<Pair<Locale, BigDecimal>> localeWeightings = new ArrayList<>();
095        for (String language : split(join(headerValues, ','), ',')) {
096            List<String> values = split(language, ';');
097            BigDecimal quality = BigDecimal.ONE;
098            if (values.size() == 2) {
099                String[] parameter = parseParameter(values.get(1).trim());
100                if (!"q".equals(parameter[0])) {
101                    throw new IllegalArgumentException("Unrecognised parameter: " + parameter[0]);
102                }
103                quality = new BigDecimal(parameter[1].trim());
104            } else if (values.size() != 1) {
105                throw new IllegalArgumentException("Unrecognised parameter(s): " + language);
106            }
107            localeWeightings.add(Pair.of(Locale.forLanguageTag(values.get(0).trim()), quality));
108        }
109
110        sort(localeWeightings, LOCALES_QUALITY_COMPARATOR);
111        List<Locale> locales = new ArrayList<>(localeWeightings.size());
112        for (Pair<Locale, BigDecimal> locale : localeWeightings) {
113            locales.add(locale.getFirst());
114        }
115        return new AcceptLanguageHeader(new PreferredLocales(locales));
116    }
117
118    private final PreferredLocales locales;
119
120    private AcceptLanguageHeader(PreferredLocales locales) {
121        this.locales = locales;
122    }
123
124    /**
125     * Returns the {@code PreferredLocales} instance that represents this header.
126     * @return The instance.
127     */
128    public PreferredLocales getLocales() {
129        return locales;
130    }
131
132    @Override
133    public String getName() {
134        return NAME;
135    }
136
137    @Override
138    public List<String> getValues() {
139        StringBuilder valueString = new StringBuilder();
140        final List<Locale> locales = this.locales.getLocales();
141        BigDecimal qualityStep = getQualityStep(locales.size());
142        BigDecimal quality = BigDecimal.ONE;
143
144        for (Locale locale : locales) {
145            if (valueString.length() != 0) {
146                valueString.append(",");
147            }
148            valueString.append(locale.equals(Locale.ROOT) ? "*" : locale.toLanguageTag())
149                    .append(";q=")
150                    .append(quality.toString());
151            quality = quality.subtract(qualityStep);
152        }
153
154        return singletonList(valueString.toString());
155    }
156
157    static BigDecimal getQualityStep(int numberLocales) {
158        if (numberLocales <= 1) {
159            return BigDecimal.ONE;
160        }
161        // Find the value to decrement quality by
162        // This results in 0.1 for up to 10 locales, 0.01 for up to 100, etc.
163        int nextPowerOfTen = (int) Math.ceil(Math.log10((double) numberLocales));
164        return BigDecimal.ONE.divide(BigDecimal.TEN.pow(nextPowerOfTen));
165    }
166
167    static class Factory extends HeaderFactory<AcceptLanguageHeader> {
168
169        @Override
170        public AcceptLanguageHeader parse(String value) {
171            return valueOf(singleton(value));
172        }
173
174        @Override
175        public AcceptLanguageHeader parse(List<String> values) {
176            return valueOf(new LinkedHashSet<>(values));
177        }
178    }
179}