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 */
16package com.android.libcore.timezone.tzlookup;
17
18import java.io.FileOutputStream;
19import java.io.IOException;
20import java.io.OutputStreamWriter;
21import java.io.StringReader;
22import java.io.StringWriter;
23import java.io.Writer;
24import java.nio.charset.StandardCharsets;
25import java.time.Instant;
26import java.util.ArrayList;
27import java.util.List;
28import javax.xml.stream.XMLOutputFactory;
29import javax.xml.stream.XMLStreamException;
30import javax.xml.stream.XMLStreamWriter;
31import javax.xml.transform.OutputKeys;
32import javax.xml.transform.Transformer;
33import javax.xml.transform.TransformerException;
34import javax.xml.transform.TransformerFactory;
35import javax.xml.transform.stream.StreamResult;
36import javax.xml.transform.stream.StreamSource;
37
38/**
39 * A class that knows about the structure of the tzlookup.xml file.
40 */
41final 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