/*
* -----------------------------------------------------------------------
* Copyright © 2013-2016 Meno Hochschild, <http://www.menodata.de/>
* -----------------------------------------------------------------------
* This file (TimezoneNameProcessor.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.UnixTime;
import net.time4j.engine.AttributeQuery;
import net.time4j.engine.ChronoDisplay;
import net.time4j.engine.ChronoElement;
import net.time4j.format.Attributes;
import net.time4j.format.Leniency;
import net.time4j.tz.NameStyle;
import net.time4j.tz.TZID;
import net.time4j.tz.Timezone;
import net.time4j.tz.ZonalOffset;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* <p>Verarbeitet einen Zeitzonen-Namen. </p>
*
* @author Meno Hochschild
* @since 3.0
*/
final class TimezoneNameProcessor
implements FormatProcessor<TZID> {
//~ Statische Felder/Initialisierungen --------------------------------
private static final ConcurrentMap<Locale, TZNames> CACHE_ABBREVIATIONS = new ConcurrentHashMap<>();
private static final ConcurrentMap<Locale, TZNames> CACHE_ZONENAMES = new ConcurrentHashMap<>();
private static final int MAX = 25; // maximum size of cache
private static final String DEFAULT_PROVIDER = "DEFAULT";
//~ Instanzvariablen --------------------------------------------------
private final boolean abbreviated;
private final FormatProcessor<TZID> fallback;
private final Set<TZID> preferredZones;
// quick path optimization
private final Leniency lenientMode;
private final Locale locale;
//~ Konstruktoren -----------------------------------------------------
/**
* <p>Erzeugt eine neue Instanz. </p>
*
* @param abbreviated abbreviations to be used?
*/
TimezoneNameProcessor(boolean abbreviated) {
super();
this.abbreviated = abbreviated;
this.fallback = new LocalizedGMTProcessor(abbreviated);
this.preferredZones = null;
this.lenientMode = Leniency.SMART;
this.locale = Locale.ROOT;
}
/**
* <p>Erzeugt eine neue Instanz. </p>
*
* @param abbreviated abbreviations to be used?
* @param preferredZones preferred timezone ids for resolving duplicates
*/
TimezoneNameProcessor(
boolean abbreviated,
Set<TZID> preferredZones
) {
super();
this.abbreviated = abbreviated;
this.fallback = new LocalizedGMTProcessor(abbreviated);
this.preferredZones = Collections.unmodifiableSet(new LinkedHashSet<>(preferredZones));
this.lenientMode = Leniency.SMART;
this.locale = Locale.ROOT;
}
private TimezoneNameProcessor(
boolean abbreviated,
FormatProcessor<TZID> fallback,
Set<TZID> preferredZones,
Leniency lenientMode,
Locale locale
) {
super();
this.abbreviated = abbreviated;
this.fallback = fallback;
this.preferredZones = preferredZones;
// quick path members
this.lenientMode = lenientMode;
this.locale = locale;
}
//~ Methoden ----------------------------------------------------------
@Override
public void print(
ChronoDisplay formattable,
Appendable buffer,
AttributeQuery attributes,
Set<ElementPosition> positions,
boolean quickPath
) throws IOException {
if (!formattable.hasTimezone()) {
throw new IllegalArgumentException(
"Cannot extract timezone name from: " + formattable);
}
TZID tzid = formattable.getTimezone();
if (tzid instanceof ZonalOffset) {
this.fallback.print(formattable, buffer, attributes, positions, quickPath);
return;
}
String name;
if (formattable instanceof UnixTime) {
Timezone zone = Timezone.of(tzid);
UnixTime ut = UnixTime.class.cast(formattable);
name =
zone.getDisplayName(
this.getStyle(zone.isDaylightSaving(ut)),
quickPath
? this.locale
: attributes.get(Attributes.LANGUAGE, Locale.ROOT));
} else {
throw new IllegalArgumentException(
"Cannot extract timezone name from: " + formattable);
}
int start = -1;
int printed;
if (buffer instanceof CharSequence) {
start = ((CharSequence) buffer).length();
}
buffer.append(name);
printed = name.length();
if (
(start != -1)
&& (printed > 0)
&& (positions != null)
) {
positions.add(
new ElementPosition(
TimezoneElement.TIMEZONE_ID,
start,
start + printed));
}
}
@Override
public void parse(
CharSequence text,
ParseLog status,
AttributeQuery attributes,
ParsedEntity<?> parsedResult,
boolean quickPath
) {
int len = text.length();
int start = status.getPosition();
int pos = start;
if (pos >= len) {
status.setError(start, "Missing timezone name.");
return;
}
Locale lang = (quickPath ? this.locale : attributes.get(Attributes.LANGUAGE, Locale.ROOT));
Leniency leniency = (quickPath ? this.lenientMode : attributes.get(Attributes.LENIENCY, Leniency.SMART));
// evaluation of relevant part of input which might contain the timezone name
StringBuilder name = new StringBuilder();
while (pos < len) {
char c = text.charAt(pos);
if (
Character.isLetter(c) // tz names must start with a letter
|| (!this.abbreviated && (pos > start) && !Character.isDigit(c))
) {
// long tz names can contain almost every char - with the exception of digits
name.append(c);
pos++;
} else {
break;
}
}
String key = name.toString().trim();
pos = start + key.length();
// fallback-case (fixed offset)
if (key.startsWith("GMT") || key.startsWith("UT")) {
this.fallback.parse(text, status, attributes, parsedResult, quickPath);
return;
}
// Zeitzonennamen im Cache suchen und ggf. Cache füllen
ConcurrentMap<Locale, TZNames> cache = (
this.abbreviated
? CACHE_ABBREVIATIONS
: CACHE_ZONENAMES);
TZNames tzNames = cache.get(lang);
if (tzNames == null) {
Map<String, List<TZID>> stdNames =
this.getTimezoneNameMap(lang, false);
Map<String, List<TZID>> dstNames =
this.getTimezoneNameMap(lang, true);
tzNames = new TZNames(stdNames, dstNames);
if (cache.size() < MAX) {
TZNames tmp = cache.putIfAbsent(lang, tzNames);
if (tmp != null) {
tzNames = tmp;
}
}
}
// Zeitzonen-IDs bestimmen
int[] lenbuf = new int[2];
lenbuf[0] = pos;
lenbuf[1] = pos;
List<TZID> stdZones = readZones(tzNames, key, false, lenbuf);
List<TZID> dstZones = readZones(tzNames, key, true, lenbuf);
int sum = stdZones.size() + dstZones.size();
if (sum == 0) {
status.setError(
start,
"Unknown timezone name: " + key);
return;
}
if ((sum > 1) && !leniency.isStrict()) {
stdZones = excludeWinZones(stdZones);
dstZones = excludeWinZones(dstZones);
sum = stdZones.size() + dstZones.size();
}
List<TZID> stdZonesOriginal = stdZones;
List<TZID> dstZonesOriginal = dstZones;
if ((sum > 1) && !leniency.isLax()) {
if (stdZones.size() > 0) {
stdZones = this.resolveUsingPreferred(stdZones, lang, leniency);
}
if (dstZones.size() > 0) {
dstZones = this.resolveUsingPreferred(dstZones, lang, leniency);
}
}
sum = stdZones.size() + dstZones.size();
if (sum == 0) {
List<String> candidates = new ArrayList<>();
for (TZID tzid : stdZonesOriginal) {
candidates.add(tzid.canonical());
}
for (TZID tzid : dstZonesOriginal) {
candidates.add(tzid.canonical());
}
status.setError(
start,
"Time zone name \""
+ key
+ "\" not found among preferred timezones in locale "
+ lang
+ ", candidates=" + candidates);
return;
}
List<TZID> zones;
int index;
if (stdZones.size() > 0) {
zones = stdZones;
index = 0;
if (
(sum == 2)
&& (dstZones.size() > 0)
&& (stdZones.get(0).canonical().equals(dstZones.get(0).canonical()))
) {
dstZones.remove(0);
sum--;
} else if (!dstZones.isEmpty()) {
zones = new ArrayList<>(zones);
zones.addAll(dstZones); // for better error message if not unique
}
} else {
zones = dstZones;
index = 1;
}
// remove alternative provider zones if default provider zone exists
if (sum > 1) {
List<TZID> filtered = null;
for (TZID id : zones) {
if (id.canonical().indexOf('~') == -1) {
if (filtered == null) {
filtered = new ArrayList<>();
}
filtered.add(id);
}
}
if (filtered != null) {
zones = filtered;
sum = zones.size();
}
}
// final step: determining the result
if (
(sum == 1)
|| leniency.isLax()
) {
parsedResult.put(TimezoneElement.TIMEZONE_ID, zones.get(0));
status.setPosition(lenbuf[index]);
if (tzNames.isDaylightSensitive()) {
status.setDaylightSaving(index == 1);
}
} else {
status.setError(
start,
"Time zone name is not unique: \"" + key + "\" in "
+ toString(zones));
}
}
@Override
public ChronoElement<TZID> getElement() {
return TimezoneElement.TIMEZONE_ID;
}
@Override
public FormatProcessor<TZID> withElement(ChronoElement<TZID> element) {
return this;
}
@Override
public boolean isNumerical() {
return false;
}
@Override
public FormatProcessor<TZID> quickPath(
ChronoFormatter<?> formatter,
AttributeQuery attributes,
int reserved
) {
return new TimezoneNameProcessor(
this.abbreviated,
this.fallback,
this.preferredZones,
attributes.get(Attributes.LENIENCY, Leniency.SMART),
attributes.get(Attributes.LANGUAGE, Locale.ROOT)
);
}
private Map<String, List<TZID>> getTimezoneNameMap(
Locale locale,
boolean daylightSaving
) {
List<TZID> zones;
Map<String, List<TZID>> map = new HashMap<>();
for (TZID tzid : Timezone.getAvailableIDs()) {
String tzName =
Timezone.getDisplayName(
tzid,
this.getStyle(daylightSaving),
locale);
if (tzName.equals(tzid.canonical())) {
continue; // registrierte NameProvider haben nichts gefunden!
}
zones = map.get(tzName);
if (zones == null) {
zones = new ArrayList<>();
map.put(tzName, zones);
}
zones.add(tzid);
}
return Collections.unmodifiableMap(map);
}
private static List<TZID> readZones(
TZNames tzNames,
String key,
boolean daylightSaving,
int[] lenbuf
) {
List<TZID> zones = tzNames.search(key, daylightSaving);
if (zones.isEmpty()) {
int last = key.length() - 1;
if (!Character.isLetter(key.charAt(last))) { // maybe interpunctuation char?
zones = tzNames.search(key.substring(0, last), daylightSaving);
if (!zones.isEmpty()) {
int index = (daylightSaving ? 1 : 0);
lenbuf[index]--;
}
}
}
return zones;
}
private static List<TZID> excludeWinZones(List<TZID> zones) {
if (zones.size() > 1) {
List<TZID> candidates = new ArrayList<>(zones);
for (int i = 1, n = zones.size(); i < n; i++) {
TZID tzid = zones.get(i);
if (tzid.canonical().startsWith("WINDOWS~")) {
candidates.remove(tzid);
}
}
if (!candidates.isEmpty()) {
return candidates;
}
}
return zones;
}
private List<TZID> resolveUsingPreferred(
List<TZID> zones,
Locale locale,
Leniency leniency
) {
Map<String, List<TZID>> matched = new HashMap<>();
matched.put(DEFAULT_PROVIDER, new ArrayList<>());
for (TZID tz : zones) {
String id = tz.canonical();
Set<TZID> prefs = this.preferredZones;
String provider = DEFAULT_PROVIDER;
int index = id.indexOf('~');
if (index >= 0) {
provider = id.substring(0, index);
}
if (prefs == null) {
prefs =
Timezone.getPreferredIDs(
locale,
leniency.isSmart(),
provider);
}
for (TZID p : prefs) {
if (p.canonical().equals(id)) {
List<TZID> candidates = matched.get(provider);
if (candidates == null) {
candidates = new ArrayList<>();
matched.put(provider, candidates);
}
candidates.add(p);
break;
}
}
}
List<TZID> candidates = matched.get(DEFAULT_PROVIDER);
List<TZID> result = zones;
if (candidates.isEmpty()) {
matched.remove(DEFAULT_PROVIDER);
boolean found = false;
for (String provider : matched.keySet()) {
candidates = matched.get(provider);
if (!candidates.isEmpty()) {
found = true;
result = candidates;
break;
}
}
if (!found) {
result = Collections.emptyList();
}
} else {
result = candidates;
}
return result;
}
private NameStyle getStyle(boolean daylightSaving) {
if (daylightSaving) {
return (
this.abbreviated
? NameStyle.SHORT_DAYLIGHT_TIME
: NameStyle.LONG_DAYLIGHT_TIME);
} else {
return (
this.abbreviated
? NameStyle.SHORT_STANDARD_TIME
: NameStyle.LONG_STANDARD_TIME);
}
}
private static String toString(List<TZID> ids) {
StringBuilder sb = new StringBuilder(ids.size() * 16);
sb.append('{');
boolean first = true;
for (TZID tzid : ids) {
if (first) {
first = false;
} else {
sb.append(',');
}
sb.append(tzid.canonical());
}
return sb.append('}').toString();
}
//~ Innere Klassen ----------------------------------------------------
private static class TZNames {
//~ Instanzvariablen ----------------------------------------------
private final boolean dstSensitive;
private final Map<String, List<TZID>> stdNames;
private final Map<String, List<TZID>> dstNames;
//~ Konstruktoren -------------------------------------------------
TZNames(
Map<String, List<TZID>> stdNames,
Map<String, List<TZID>> dstNames
) {
super();
this.stdNames = stdNames;
this.dstNames = dstNames;
this.dstSensitive = !stdNames.keySet().equals(dstNames.keySet());
}
//~ Methoden ------------------------------------------------------
boolean isDaylightSensitive() {
return this.dstSensitive;
}
// quick search via hash-access
List<TZID> search(
String key,
boolean daylightSaving
) {
Map<String, List<TZID>> names = (
daylightSaving
? this.dstNames
: this.stdNames);
if (names.containsKey(key)) {
return names.get(key);
}
return Collections.emptyList();
}
}
}