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