/*
* -----------------------------------------------------------------------
* Copyright © 2013-2016 Meno Hochschild, <http://www.menodata.de/>
* -----------------------------------------------------------------------
* This file (AttributeSet.java) is part of project Time4J.
*
* Time4J is free software: You can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published
* by the Free Software Foundation, either version 2.1 of the License, or
* (at your option) any later version.
*
* Time4J is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Time4J. If not, see <http://www.gnu.org/licenses/>.
* -----------------------------------------------------------------------
*/
package net.time4j.format.expert;
import net.time4j.base.ResourceLoader;
import net.time4j.engine.AttributeKey;
import net.time4j.engine.AttributeQuery;
import net.time4j.engine.ChronoCondition;
import net.time4j.engine.ChronoDisplay;
import net.time4j.engine.Chronology;
import net.time4j.format.Attributes;
import net.time4j.format.Leniency;
import net.time4j.format.NumberSymbolProvider;
import net.time4j.format.NumberSystem;
import net.time4j.format.OutputContext;
import net.time4j.format.TextWidth;
import net.time4j.i18n.LanguageMatch;
import net.time4j.i18n.SymbolProviderSPI;
import java.util.Collections;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* <p>A decorator for standard format attributes. </p>
*
* @author Meno Hochschild
* @since 3.0
*/
final class AttributeSet
implements AttributeQuery {
//~ Statische Felder/Initialisierungen --------------------------------
static final AttributeKey<String> PLUS_SIGN = Attributes.createKey("PLUS_SIGN", String.class);
static final AttributeKey<String> MINUS_SIGN = Attributes.createKey("MINUS_SIGN", String.class);
private static final NumberSymbolProvider NUMBER_SYMBOLS;
static {
NumberSymbolProvider p = null;
int count = 0;
for (NumberSymbolProvider tmp : ResourceLoader.getInstance().services(NumberSymbolProvider.class)) {
int size = tmp.getAvailableLocales().length;
if (size > count) {
p = tmp;
count = size;
}
}
if (p == null) {
p = SymbolProviderSPI.INSTANCE;
}
NUMBER_SYMBOLS = p;
}
private static final char ISO_DECIMAL_SEPARATOR = (
Boolean.getBoolean("net.time4j.format.iso.decimal.dot")
? '.'
: ',' // Empfehlung des ISO-Standards
);
private static final ConcurrentMap<String, NumericalSymbols> NUMBER_SYMBOL_CACHE =
new ConcurrentHashMap<>();
private static final NumericalSymbols DEFAULT_NUMERICAL_SYMBOLS =
new NumericalSymbols(NumberSystem.ARABIC, '0', ISO_DECIMAL_SEPARATOR, "+", "-");
//~ Instanzvariablen --------------------------------------------------
private final Map<String, Object> internals;
private final Attributes attributes;
private final Locale locale;
private final int level; // Ebene der optionalen Verarbeitungshierarchie
private final int section; // Identifiziert eine optionale Attributsektion
private final ChronoCondition<ChronoDisplay> printCondition; // nullable
//~ Konstruktoren -----------------------------------------------------
AttributeSet(
Attributes attributes,
Locale locale
) {
this(attributes, locale, 0, 0, null);
}
AttributeSet(
Attributes attributes,
Locale locale,
int level,
int section,
ChronoCondition<ChronoDisplay> printCondition
) {
super();
if (attributes == null) {
throw new NullPointerException("Missing format attributes.");
}
this.attributes = attributes;
this.locale = ((locale == null) ? Locale.ROOT : locale);
this.level = level;
this.section = section;
this.printCondition = printCondition;
this.internals = Collections.emptyMap();
}
private AttributeSet(
Attributes attributes,
Locale locale,
int level,
int section,
ChronoCondition<ChronoDisplay> printCondition,
Map<String, Object> internals
) {
super();
if (attributes == null) {
throw new NullPointerException("Missing format attributes.");
}
this.attributes = attributes;
this.locale = ((locale == null) ? Locale.ROOT : locale);
this.level = level;
this.section = section;
this.printCondition = printCondition;
this.internals = Collections.unmodifiableMap(internals);
}
//~ Methoden ----------------------------------------------------------
@Override
public boolean contains(AttributeKey<?> key) {
if (this.internals.containsKey(key.name())) {
return true;
}
return this.attributes.contains(key);
}
@Override
public <A> A get(AttributeKey<A> key) {
if (this.internals.containsKey(key.name())) {
return key.type().cast(this.internals.get(key.name()));
}
return this.attributes.get(key);
}
@Override
public <A> A get(
AttributeKey<A> key,
A defaultValue
) {
if (this.internals.containsKey(key.name())) {
return key.type().cast(this.internals.get(key.name()));
}
return this.attributes.get(key, defaultValue);
}
/**
* <p>Compares all internal format attributes. </p>
*/
/*[deutsch]
* <p>Vergleicht auf Basis aller internen Formatattribute. </p>
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
} else if (obj instanceof AttributeSet) {
AttributeSet that = (AttributeSet) obj;
return (
this.attributes.equals(that.attributes)
&& this.locale.equals(that.locale)
&& (this.level == that.level)
&& (this.section == that.section)
&& isEqual(this.printCondition, that.printCondition)
&& this.internals.equals(that.internals)
);
} else {
return false;
}
}
/*[deutsch]
* <p>Berechnet den Hash-Code. </p>
*/
@Override
public int hashCode() {
return 7 * this.attributes.hashCode() + 37 * this.internals.hashCode();
}
/**
* <p>Supports mainly debugging. </p>
*/
/*[deutsch]
* <p>Dient vorwiegend der Debugging-Unterstützung. </p>
*/
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(this.getClass().getName());
sb.append("[attributes=");
sb.append(this.attributes);
sb.append(",locale=");
sb.append(this.locale);
sb.append(",level=");
sb.append(this.level);
sb.append(",section=");
sb.append(this.section);
sb.append(",print-condition=");
sb.append(this.printCondition);
sb.append(",other=");
sb.append(this.internals);
sb.append(']');
return sb.toString();
}
Attributes getAttributes() {
return this.attributes;
}
Locale getLocale() {
return this.locale;
}
int getLevel() {
return this.level;
}
int getSection() {
return this.section;
}
ChronoCondition<ChronoDisplay> getCondition() {
return this.printCondition; // nullable
}
static AttributeSet createDefaults(
Chronology<?> chronology,
Attributes attributes,
Locale locale
) {
Attributes.Builder builder = new Attributes.Builder(chronology);
builder.set(Attributes.LENIENCY, Leniency.SMART);
builder.set(Attributes.TEXT_WIDTH, TextWidth.WIDE);
builder.set(Attributes.OUTPUT_CONTEXT, OutputContext.FORMAT);
builder.set(Attributes.PAD_CHAR, ' ');
builder.setAll(attributes);
AttributeSet as = new AttributeSet(builder.build(), locale);
return as.withLocale(locale);
}
/**
* <p>Setzt die Attribute neu. </p>
*
* @param attributes new format attributes
*/
AttributeSet withAttributes(Attributes attributes) {
return new AttributeSet(attributes, this.locale, this.level, this.section, this.printCondition, this.internals);
}
/**
* <p>Setzt das angegebene interne Attribut neu. </p>
*
* @param <A> generic type of attribute value
* @param key attribute key
* @param value attribute value (if {@code null} then the attribute will be removed else inserted or updated)
* @return changed attribute set
* @since 3.11/4.8
*/
<A> AttributeSet withInternal(
AttributeKey<A> key,
A value
) {
Map<String, Object> map = new HashMap<>(this.internals);
if (value == null) {
map.remove(key.name());
} else {
map.put(key.name(), value);
}
return new AttributeSet(this.attributes, this.locale, this.level, this.section, this.printCondition, map);
}
/**
* <p>Setzt die Sprach- und Ländereinstellung. </p>
*
* <p>Die Attribute {@link Attributes#NUMBER_SYSTEM}, {@link Attributes#ZERO_DIGIT},
* {@link Attributes#DECIMAL_SEPARATOR} und {@link Attributes#LANGUAGE} werden automatisch mit angepasst. </p>
*
* @param locale new language and country setting
* @return this instance for method chaining
*/
AttributeSet withLocale(Locale locale) {
Attributes.Builder builder = new Attributes.Builder();
builder.setAll(this.attributes);
String plus;
String minus;
String lang = LanguageMatch.getAlias(locale);
String country = locale.getCountry();
if (
lang.isEmpty()
&& country.isEmpty()
) {
locale = Locale.ROOT;
builder.set(Attributes.NUMBER_SYSTEM, NumberSystem.ARABIC);
builder.set(Attributes.DECIMAL_SEPARATOR, ISO_DECIMAL_SEPARATOR);
plus = "+";
minus = "-";
} else {
String key = lang;
if (!country.isEmpty()) {
key = key + "_" + country;
}
NumericalSymbols symbols = NUMBER_SYMBOL_CACHE.get(key);
if (symbols == null) {
try {
symbols =
new NumericalSymbols(
NUMBER_SYMBOLS.getDefaultNumberSystem(locale),
NUMBER_SYMBOLS.getZeroDigit(locale),
NUMBER_SYMBOLS.getDecimalSeparator(locale),
NUMBER_SYMBOLS.getPlusSign(locale),
NUMBER_SYMBOLS.getMinusSign(locale)
);
} catch (RuntimeException re) {
symbols = DEFAULT_NUMERICAL_SYMBOLS;
}
NumericalSymbols old =
NUMBER_SYMBOL_CACHE.putIfAbsent(key, symbols);
if (old != null) {
symbols = old;
}
}
builder.set(Attributes.NUMBER_SYSTEM, symbols.numsys);
builder.set(Attributes.ZERO_DIGIT, symbols.zeroDigit); // allow deviation
builder.set(Attributes.DECIMAL_SEPARATOR, symbols.decimalSeparator);
plus = symbols.plus;
minus = symbols.minus;
}
builder.setLanguage(locale);
Map<String, Object> newInternals = new HashMap<>(this.internals);
newInternals.put(PLUS_SIGN.name(), plus);
newInternals.put(MINUS_SIGN.name(), minus);
return new AttributeSet(builder.build(), locale, this.level, this.section, this.printCondition, newInternals);
}
// used to create merged global attributes for a new formatter
static AttributeSet merge(
AttributeSet outer,
AttributeSet inner
) {
Map<String, Object> internalsNew = new HashMap<>();
internalsNew.putAll(inner.internals);
internalsNew.putAll(outer.internals);
Attributes attributesNew =
new Attributes.Builder()
.setAll(inner.attributes)
.setAll(outer.attributes)
.build();
AttributeSet as = new AttributeSet(attributesNew, Locale.ROOT, 0, 0, null, internalsNew);
return as.withLocale(outer.locale);
}
private static boolean isEqual(Object o1, Object o2) {
return ((o1 == null) ? (o2 == null) : o1.equals(o2));
}
//~ Innere Klassen ----------------------------------------------------
private static class NumericalSymbols {
//~ Instanzvariablen ----------------------------------------------
private final NumberSystem numsys;
private final char zeroDigit;
private final char decimalSeparator;
private final String plus;
private final String minus;
//~ Konstruktoren -------------------------------------------------
NumericalSymbols(
NumberSystem numsys,
char zeroDigit,
char decimalSeparator,
String plus,
String minus
) {
super();
this.numsys = numsys;
this.zeroDigit = zeroDigit;
this.decimalSeparator = decimalSeparator;
this.plus = plus;
this.minus = minus;
}
}
}