All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.ibm.icu.util.LocalePriorityList Maven / Gradle / Ivy

The newest version!
// © 2016 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html
/*
 *******************************************************************************
 * Copyright (C) 2010-2016, Google, Inc.; International Business Machines      *
 * Corporation and others. All Rights Reserved.                                *
 *******************************************************************************
 */

package com.ibm.icu.util;

import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Provides an immutable list of languages/locales in priority order.
 * The string format is based on the Accept-Language format
 * (RFC 2616 Section 14.4), such as
 * "af, en, fr;q=0.9". Syntactically it is slightly
 * more lenient, in allowing extra whitespace between elements, extra commas,
 * and more than 3 decimals (on input). The qvalues must be between 0 and 1.
 *
 * 

In theory, Accept-Language indicates the relative 'quality' of each item, * but in practice, all of the browsers just take an ordered list, like * "en, fr, de", and synthesize arbitrary quality values that put these in the * right order, like: "en, fr;q=0.7, de;q=0.3". The quality values in these de facto * semantics thus have nothing to do with the relative qualities of the * original. Accept-Language also doesn't * specify the interpretation of multiple instances, eg what "en, fr, en;q=.5" * means. *

There are various ways to build a LocalePriorityList, such * as using the following equivalent patterns: * *

 * list = LocalePriorityList.add("af, en, fr;q=0.9").build();
 *
 * list2 = LocalePriorityList
 *  .add(ULocale.forString("af"))
 *  .add(ULocale.ENGLISH)
 *  .add(ULocale.FRENCH, 0.9d)
 *  .build();
 * 
* When the list is built, the internal values are sorted in descending order by weight, * and then by input order. * That is, if two languages/locales have the same weight, the first one in the original order comes first. * If exactly the same language tag appears multiple times, the last one wins. * *

There are two options when building. * If preserveWeights are on, then "de;q=0.3, ja;q=0.3, en, fr;q=0.7, de " would result in the following: *

 en;q=1.0
 * de;q=1.0
 * fr;q=0.7
 * ja;q=0.3
* If it is off (the default), then all weights are reset to 1.0 after reordering. * This is to match the effect of the Accept-Language semantics as used in browsers, and results in the following: * *
 en;q=1.0
 * de;q=1.0
 * fr;q=1.0
 * ja;q=1.0
* @author [email protected] * @stable ICU 4.4 */ public class LocalePriorityList implements Iterable { private static final Double D1 = 1.0d; private static final Pattern languageSplitter = Pattern.compile("\\s*,\\s*"); private static final Pattern weightSplitter = Pattern .compile("\\s*(\\S*)\\s*;\\s*q\\s*=\\s*(\\S*)"); private final Map languagesAndWeights; /** * Creates a Builder and adds locales, each with weight 1.0. * * @param locales locales/languages to be added * @return a new builder with these locales, for chaining * @stable ICU 4.4 */ public static Builder add(ULocale... locales) { return new Builder().add(locales); } /** * Creates a Builder and adds a locale with a specified weight. * A zero or negative weight leads to removing the locale. * A weight greater than 1 is pinned to 1. * * @param locale locale/language to be added * @param weight value from 0.0 to 1.0 * @return a new builder with this locale, for chaining * @stable ICU 4.4 */ public static Builder add(ULocale locale, final double weight) { return new Builder().add(locale, weight); } /** * Creates a Builder and adds locales with weights. * * @param list list of locales with weights * @return a new builder with these locales, for chaining * @stable ICU 4.4 */ public static Builder add(LocalePriorityList list) { return new Builder(list); } /** * Creates a Builder, parses the RFC 2616 string, and adds locales with weights accordingly. * * @param acceptLanguageString String in RFC 2616 format (leniently parsed) * @return a new builder with these locales, for chaining * @stable ICU 4.4 */ public static Builder add(String acceptLanguageString) { return new Builder().add(acceptLanguageString); } /** * Returns the weight for a given language/locale, or null if there is none. * Note that the weights may be adjusted from those used to build the list. * * @param locale to get weight of * @return weight * @stable ICU 4.4 */ public Double getWeight(ULocale locale) { return languagesAndWeights.get(locale); } /** * Returns the locales as an immutable Set view. * The set has the same iteration order as this object itself. * * @return the locales * @stable ICU 65 */ public Set getULocales() { return languagesAndWeights.keySet(); } /** * {@inheritDoc} * @stable ICU 4.4 */ @Override public String toString() { final StringBuilder result = new StringBuilder(); for (Entry entry : languagesAndWeights.entrySet()) { ULocale language = entry.getKey(); double weight = entry.getValue(); if (result.length() != 0) { result.append(", "); } result.append(language); if (weight != 1.0) { result.append(";q=").append(weight); } } return result.toString(); } /** * {@inheritDoc} * @stable ICU 4.4 */ @Override public Iterator iterator() { return languagesAndWeights.keySet().iterator(); } /** * {@inheritDoc} * @stable ICU 4.4 */ @Override public boolean equals(final Object o) { if (o == null) { return false; } if (this == o) { return true; } try { final LocalePriorityList that = (LocalePriorityList) o; return languagesAndWeights.equals(that.languagesAndWeights); } catch (final RuntimeException e) { return false; } } /** * {@inheritDoc} * @stable ICU 4.4 */ @Override public int hashCode() { return languagesAndWeights.hashCode(); } // ==================== Privates ==================== private LocalePriorityList(final Map languageToWeight) { this.languagesAndWeights = languageToWeight; } /** * Class used for building LocalePriorityLists. * @stable ICU 4.4 */ public static class Builder { /** * These store the input languages and weights, in chronological order, * where later additions override previous ones. */ private Map languageToWeight; /** * The builder is reusable but rarely reused. Avoid cloning the map when not needed. * Exactly one of languageToWeight and built is null. */ private LocalePriorityList built; private boolean hasWeights = false; // other than 1.0 /** * Private constructor, only used by LocalePriorityList */ private Builder() { languageToWeight = new LinkedHashMap<>(); } private Builder(LocalePriorityList list) { built = list; for (Double value : list.languagesAndWeights.values()) { double weight = value; assert 0.0 < weight && weight <= 1.0; if (weight != 1.0) { hasWeights = true; break; } } } /** * Creates a LocalePriorityList. This is equivalent to * {@link Builder#build(boolean) Builder.build(false)}. * * @return A LocalePriorityList * @stable ICU 4.4 */ public LocalePriorityList build() { return build(false); } /** * Creates a LocalePriorityList. * * @param preserveWeights when true, each locale's given weight is preserved. * @return A LocalePriorityList * @stable ICU 4.4 */ public LocalePriorityList build(boolean preserveWeights) { if (built != null) { // Calling build() again without changing anything in between. // Just return the same immutable list. return built; } Map temp; if (hasWeights) { // Walk through the input list, collecting the items with the same weights. final TreeMap> weightToLanguages = new TreeMap<>(myDescendingDouble); for (Entry entry : languageToWeight.entrySet()) { ULocale lang = entry.getKey(); Double weight = entry.getValue(); List s = weightToLanguages.get(weight); if (s == null) { weightToLanguages.put(weight, s = new LinkedList<>()); } s.add(lang); } // We now have a bunch of items sorted by weight, then chronologically. // We can now create a list in the right order. if (weightToLanguages.size() <= 1) { // There is at most one weight. temp = languageToWeight; if (weightToLanguages.isEmpty() || weightToLanguages.firstKey() == 1.0) { hasWeights = false; } } else { temp = new LinkedHashMap<>(); for (Entry> langEntry : weightToLanguages.entrySet()) { final Double weight = preserveWeights ? langEntry.getKey() : D1; for (final ULocale lang : langEntry.getValue()) { temp.put(lang, weight); } } } } else { // Nothing to sort. temp = languageToWeight; } languageToWeight = null; return built = new LocalePriorityList(Collections.unmodifiableMap(temp)); } /** * Adds locales with weights. * * @param list list of locales with weights * @return this, for chaining * @stable ICU 4.4 */ public Builder add(final LocalePriorityList list) { for (Entry entry : list.languagesAndWeights.entrySet()) { add(entry.getKey(), entry.getValue()); } return this; } /** * Adds a locale with weight 1.0. * * @param locale to add with weight 1.0 * @return this, for chaining * @stable ICU 4.4 */ public Builder add(final ULocale locale) { return add(locale, 1.0); } /** * Adds locales, each with weight 1.0. * * @param locales locales/languages to be added * @return this, for chaining. * @stable ICU 4.4 */ public Builder add(ULocale... locales) { for (final ULocale languageCode : locales) { add(languageCode, 1.0); } return this; } /** * Adds a locale with a specified weight. * Overrides any previous weight for the locale. * A zero or negative weight leads to removing the locale. * A weight greater than 1 is pinned to 1. * * @param locale language/locale to add * @param weight value between 0.0 and 1.1 * @return this, for chaining. * @stable ICU 4.4 */ public Builder add(final ULocale locale, double weight) { if (languageToWeight == null) { // Builder reuse after build(). languageToWeight = new LinkedHashMap<>(built.languagesAndWeights); built = null; } if (languageToWeight.containsKey(locale)) { languageToWeight.remove(locale); } Double value; if (weight <= 0.0) { return this; // skip zeros } else if (weight >= 1.0) { value = D1; } else { value = weight; hasWeights = true; } languageToWeight.put(locale, value); return this; } /** * Parses the RFC 2616 string, and adds locales with weights accordingly. * * @param acceptLanguageList in RFC 2616 format (leniently parsed) * @return this, for chaining. * @stable ICU 4.4 */ public Builder add(final String acceptLanguageList) { final String[] items = languageSplitter.split(acceptLanguageList.trim()); final Matcher itemMatcher = weightSplitter.matcher(""); for (final String item : items) { if (itemMatcher.reset(item).matches()) { final ULocale language = new ULocale(itemMatcher.group(1)); final double weight = Double.parseDouble(itemMatcher.group(2)); if (!(0.0 <= weight && weight <= 1.0)) { // do ! for NaN throw new IllegalArgumentException( "Illegal weight, must be 0..1: " + weight); } add(language, weight); } else if (item.length() != 0) { add(new ULocale(item)); } } return this; } } private static Comparator myDescendingDouble = new Comparator() { @Override public int compare(Double o1, Double o2) { int result = o1.compareTo(o2); return result > 0 ? -1 : result < 0 ? 1 : 0; // Reverse the order. } }; }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy