Home | History | Annotate | Download | only in tzlookup
      1 /*
      2  * Copyright (C) 2017 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package com.android.libcore.timezone.tzlookup;
     17 
     18 import java.io.FileOutputStream;
     19 import java.io.IOException;
     20 import java.io.OutputStreamWriter;
     21 import java.io.StringReader;
     22 import java.io.StringWriter;
     23 import java.io.Writer;
     24 import java.nio.charset.StandardCharsets;
     25 import java.time.Instant;
     26 import java.util.ArrayList;
     27 import java.util.List;
     28 import javax.xml.stream.XMLOutputFactory;
     29 import javax.xml.stream.XMLStreamException;
     30 import javax.xml.stream.XMLStreamWriter;
     31 import javax.xml.transform.OutputKeys;
     32 import javax.xml.transform.Transformer;
     33 import javax.xml.transform.TransformerException;
     34 import javax.xml.transform.TransformerFactory;
     35 import javax.xml.transform.stream.StreamResult;
     36 import javax.xml.transform.stream.StreamSource;
     37 
     38 /**
     39  * A class that knows about the structure of the tzlookup.xml file.
     40  */
     41 final class TzLookupFile {
     42 
     43     // <timezones ianaversion="2017b">
     44     private static final String TIMEZONES_ELEMENT = "timezones";
     45     private static final String IANA_VERSION_ATTRIBUTE = "ianaversion";
     46 
     47     // <countryzones>
     48     private static final String COUNTRY_ZONES_ELEMENT = "countryzones";
     49 
     50     // <country code="iso_code" default="olson_id" everutc="n|y">
     51     private static final String COUNTRY_ELEMENT = "country";
     52     private static final String COUNTRY_CODE_ATTRIBUTE = "code";
     53     private static final String DEFAULT_ATTRIBUTE = "default";
     54     private static final String EVER_USES_UTC_ATTRIBUTE = "everutc";
     55 
     56     // <id [picker="n|y"]>
     57     private static final String ZONE_ID_ELEMENT = "id";
     58     // Default when unspecified is "y" / true.
     59     private static final String ZONE_SHOW_IN_PICKER_ATTRIBUTE = "picker";
     60     // The time when the zone stops being distinct from another of the country's zones (inclusive).
     61     private static final String ZONE_NOT_USED_AFTER_ATTRIBUTE = "notafter";
     62 
     63 
     64     // Short encodings for boolean attributes.
     65     private static final String ATTRIBUTE_FALSE = "n";
     66     private static final String ATTRIBUTE_TRUE = "y";
     67 
     68     static void write(TimeZones timeZones, String outputFile)
     69             throws XMLStreamException, IOException {
     70         /*
     71          * The required XML structure is:
     72          * <timezones ianaversion="2017b">
     73          *   <countryzones>
     74          *     <country code="us" default="America/New_York" everutc="n">
     75          *       <!-- -5:00 -->
     76          *       <id notafter="1234">America/New_York"</id>
     77          *       ...
     78          *       <!-- -8:00 -->
     79          *       <id picker="n">America/Los_Angeles</id>
     80          *       ...
     81          *     </country>
     82          *     <country code="gb" default="Europe/London" everutc="y">
     83          *       <!-- 0:00 -->
     84          *       <id>Europe/London</id>
     85          *     </country>
     86          *   </countryzones>
     87          * </timezones>
     88          */
     89 
     90         StringWriter writer = new StringWriter();
     91         writeRaw(timeZones, writer);
     92         String rawXml = writer.getBuffer().toString();
     93 
     94         TransformerFactory factory = TransformerFactory.newInstance();
     95         try (Writer fileWriter = new OutputStreamWriter(
     96                 new FileOutputStream(outputFile), StandardCharsets.UTF_8)) {
     97 
     98             // Transform the XML with the identity transform but with indenting
     99             // so it's more human-readable.
    100             Transformer transformer = factory.newTransformer();
    101             transformer.setOutputProperty(OutputKeys.INDENT, "yes");
    102             transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "1");
    103             transformer.transform(
    104                     new StreamSource(new StringReader(rawXml)), new StreamResult(fileWriter));
    105         } catch (TransformerException e) {
    106             throw new XMLStreamException(e);
    107         }
    108     }
    109 
    110     private static void writeRaw(TimeZones timeZones, Writer fileWriter)
    111             throws XMLStreamException {
    112         XMLOutputFactory xmlOutputFactory = XMLOutputFactory.newFactory();
    113         XMLStreamWriter xmlWriter = xmlOutputFactory.createXMLStreamWriter(fileWriter);
    114         xmlWriter.writeStartDocument();
    115         xmlWriter.writeComment("\n\n **** Autogenerated file - DO NOT EDIT ****\n\n");
    116         TimeZones.writeXml(timeZones, xmlWriter);
    117         xmlWriter.writeEndDocument();
    118     }
    119 
    120     static class TimeZones {
    121 
    122         private final String ianaVersion;
    123         private CountryZones countryZones;
    124 
    125         TimeZones(String ianaVersion) {
    126             this.ianaVersion = ianaVersion;
    127         }
    128 
    129         void setCountryZones(CountryZones countryZones) {
    130             this.countryZones = countryZones;
    131         }
    132 
    133         static void writeXml(TimeZones timeZones, XMLStreamWriter writer)
    134                 throws XMLStreamException {
    135             writer.writeStartElement(TIMEZONES_ELEMENT);
    136             writer.writeAttribute(IANA_VERSION_ATTRIBUTE, timeZones.ianaVersion);
    137             CountryZones.writeXml(timeZones.countryZones, writer);
    138             writer.writeEndElement();
    139         }
    140     }
    141 
    142     static class CountryZones {
    143 
    144         private final List<Country> countries = new ArrayList<>();
    145 
    146         CountryZones() {
    147         }
    148 
    149         static void writeXml(CountryZones countryZones, XMLStreamWriter writer)
    150                 throws XMLStreamException {
    151             writer.writeStartElement(COUNTRY_ZONES_ELEMENT);
    152             for (Country country : countryZones.countries) {
    153                 Country.writeXml(country, writer);
    154             }
    155             writer.writeEndElement();
    156         }
    157 
    158         void addCountry(Country country) {
    159             countries.add(country);
    160         }
    161     }
    162 
    163     static class Country {
    164 
    165         private final String isoCode;
    166         private final String defaultTimeZoneId;
    167         private final boolean everUsesUtc;
    168         private final List<TimeZoneMapping> timeZoneIds = new ArrayList<>();
    169 
    170         Country(String isoCode, String defaultTimeZoneId, boolean everUsesUtc) {
    171             this.defaultTimeZoneId = defaultTimeZoneId;
    172             this.isoCode = isoCode;
    173             this.everUsesUtc = everUsesUtc;
    174         }
    175 
    176         void addTimeZoneIdentifier(TimeZoneMapping timeZoneId) {
    177             timeZoneIds.add(timeZoneId);
    178         }
    179 
    180         static void writeXml(Country country, XMLStreamWriter writer)
    181                 throws XMLStreamException {
    182             writer.writeStartElement(COUNTRY_ELEMENT);
    183             writer.writeAttribute(COUNTRY_CODE_ATTRIBUTE, country.isoCode);
    184             writer.writeAttribute(DEFAULT_ATTRIBUTE, country.defaultTimeZoneId);
    185             writer.writeAttribute(EVER_USES_UTC_ATTRIBUTE, encodeBooleanAttribute(
    186                     country.everUsesUtc));
    187             for (TimeZoneMapping timeZoneId : country.timeZoneIds) {
    188                 TimeZoneMapping.writeXml(timeZoneId, writer);
    189             }
    190             writer.writeEndElement();
    191         }
    192     }
    193 
    194     private static String encodeBooleanAttribute(boolean value) {
    195         return value ? ATTRIBUTE_TRUE : ATTRIBUTE_FALSE;
    196     }
    197 
    198     private static String encodeLongAttribute(long epochMillis) {
    199         return Long.toString(epochMillis);
    200     }
    201 
    202     static class TimeZoneMapping {
    203 
    204         private final String olsonId;
    205         private final boolean showInPicker;
    206         private final Instant notUsedAfterInclusive;
    207 
    208         TimeZoneMapping(String olsonId, boolean showInPicker, Instant notUsedAfterInclusive) {
    209             this.olsonId = olsonId;
    210             this.showInPicker = showInPicker;
    211             this.notUsedAfterInclusive = notUsedAfterInclusive;
    212         }
    213 
    214         static void writeXml(TimeZoneMapping timeZoneId, XMLStreamWriter writer)
    215                 throws XMLStreamException {
    216             writer.writeStartElement(ZONE_ID_ELEMENT);
    217             if (!timeZoneId.showInPicker) {
    218                 writer.writeAttribute(ZONE_SHOW_IN_PICKER_ATTRIBUTE, encodeBooleanAttribute(false));
    219             }
    220             if (timeZoneId.notUsedAfterInclusive != null) {
    221                 writer.writeAttribute(ZONE_NOT_USED_AFTER_ATTRIBUTE,
    222                         encodeLongAttribute(timeZoneId.notUsedAfterInclusive.toEpochMilli()));
    223             }
    224             writer.writeCharacters(timeZoneId.olsonId);
    225             writer.writeEndElement();
    226         }
    227     }
    228 }
    229