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}