1 /* 2 * Copyright (c) 2007-present, Stephen Colebourne & Michael Nascimento Santos 3 * 4 * All rights reserved. 5 * 6 * Redistribution and use in source and binary forms, with or without 7 * modification, are permitted provided that the following conditions are met: 8 * 9 * * Redistributions of source code must retain the above copyright notice, 10 * this list of conditions and the following disclaimer. 11 * 12 * * Redistributions in binary form must reproduce the above copyright notice, 13 * this list of conditions and the following disclaimer in the documentation 14 * and/or other materials provided with the distribution. 15 * 16 * * Neither the name of JSR-310 nor the names of its contributors 17 * may be used to endorse or promote products derived from this software 18 * without specific prior written permission. 19 * 20 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 24 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 25 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 26 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 27 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 28 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 29 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 30 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 */ 32 package org.threeten.bp.zone; 33 34 import java.io.ByteArrayInputStream; 35 import java.io.DataInputStream; 36 import java.io.IOException; 37 import java.io.InputStream; 38 import java.io.StreamCorruptedException; 39 import java.net.URL; 40 import java.util.Arrays; 41 import java.util.Enumeration; 42 import java.util.HashSet; 43 import java.util.List; 44 import java.util.NavigableMap; 45 import java.util.Set; 46 import java.util.TreeMap; 47 import java.util.concurrent.ConcurrentNavigableMap; 48 import java.util.concurrent.ConcurrentSkipListMap; 49 import java.util.concurrent.CopyOnWriteArraySet; 50 import java.util.concurrent.atomic.AtomicReferenceArray; 51 52 import org.threeten.bp.jdk8.Jdk8Methods; 53 54 /** 55 * Loads time-zone rules for 'TZDB'. 56 * <p> 57 * This class is public for the service loader to access. 58 * 59 * <h3>Specification for implementors</h3> 60 * This class is immutable and thread-safe. 61 */ 62 public final class TzdbZoneRulesProvider extends ZoneRulesProvider { 63 // TODO: can this be private/hidden in any way? 64 // service loader seems to need it to be public 65 66 /** 67 * All the regions that are available. 68 */ 69 private List<String> regionIds; 70 /** 71 * All the versions that are available. 72 */ 73 private final ConcurrentNavigableMap<String, Version> versions = new ConcurrentSkipListMap<String, Version>(); 74 /** 75 * All the URLs that have been loaded. 76 * Uses String to avoid equals() on URL. 77 */ 78 private Set<String> loadedUrls = new CopyOnWriteArraySet<String>(); 79 80 /** 81 * Creates an instance. 82 * Created by the {@code ServiceLoader}. 83 * 84 * @throws ZoneRulesException if unable to load 85 */ TzdbZoneRulesProvider()86 public TzdbZoneRulesProvider() { 87 super(); 88 if (load(ZoneRulesProvider.class.getClassLoader()) == false) { 89 throw new ZoneRulesException("No time-zone rules found for 'TZDB'"); 90 } 91 } 92 93 /** 94 * Creates an instance and loads the specified URL. 95 * <p> 96 * This could be used to wrap this provider in another instance. 97 * 98 * @param url the URL to load, not null 99 * @throws ZoneRulesException if unable to load 100 */ TzdbZoneRulesProvider(URL url)101 public TzdbZoneRulesProvider(URL url) { 102 super(); 103 try { 104 if (load(url) == false) { 105 throw new ZoneRulesException("No time-zone rules found: " + url); 106 } 107 } catch (Exception ex) { 108 throw new ZoneRulesException("Unable to load TZDB time-zone rules: " + url, ex); 109 } 110 } 111 112 /** 113 * Creates an instance and loads the specified input stream. 114 * <p> 115 * This could be used to wrap this provider in another instance. 116 * 117 * @param stream the stream to load, not null, not closed after use 118 * @throws ZoneRulesException if unable to load 119 */ TzdbZoneRulesProvider(InputStream stream)120 public TzdbZoneRulesProvider(InputStream stream) { 121 super(); 122 try { 123 load(stream); 124 } catch (Exception ex) { 125 throw new ZoneRulesException("Unable to load TZDB time-zone rules", ex); 126 } 127 } 128 129 //----------------------------------------------------------------------- 130 @Override provideZoneIds()131 protected Set<String> provideZoneIds() { 132 return new HashSet<String>(regionIds); 133 } 134 135 @Override provideRules(String zoneId, boolean forCaching)136 protected ZoneRules provideRules(String zoneId, boolean forCaching) { 137 Jdk8Methods.requireNonNull(zoneId, "zoneId"); 138 ZoneRules rules = versions.lastEntry().getValue().getRules(zoneId); 139 if (rules == null) { 140 throw new ZoneRulesException("Unknown time-zone ID: " + zoneId); 141 } 142 return rules; 143 } 144 145 @Override provideVersions(String zoneId)146 protected NavigableMap<String, ZoneRules> provideVersions(String zoneId) { 147 TreeMap<String, ZoneRules> map = new TreeMap<String, ZoneRules>(); 148 for (Version version : versions.values()) { 149 ZoneRules rules = version.getRules(zoneId); 150 if (rules != null) { 151 map.put(version.versionId, rules); 152 } 153 } 154 return map; 155 } 156 157 //------------------------------------------------------------------------- 158 /** 159 * Loads the rules. 160 * 161 * @param classLoader the class loader to use, not null 162 * @return true if updated 163 * @throws ZoneRulesException if unable to load 164 */ load(ClassLoader classLoader)165 private boolean load(ClassLoader classLoader) { 166 boolean updated = false; 167 URL url = null; 168 try { 169 Enumeration<URL> en = classLoader.getResources("org/threeten/bp/TZDB.dat"); 170 while (en.hasMoreElements()) { 171 url = en.nextElement(); 172 updated |= load(url); 173 } 174 } catch (Exception ex) { 175 throw new ZoneRulesException("Unable to load TZDB time-zone rules: " + url, ex); 176 } 177 return updated; 178 } 179 180 /** 181 * Loads the rules from a URL, often in a jar file. 182 * 183 * @param url the jar file to load, not null 184 * @return true if updated 185 * @throws ClassNotFoundException if a classpath error occurs 186 * @throws IOException if an IO error occurs 187 * @throws ZoneRulesException if the data is already loaded for the version 188 */ load(URL url)189 private boolean load(URL url) throws ClassNotFoundException, IOException, ZoneRulesException { 190 boolean updated = false; 191 if (loadedUrls.add(url.toExternalForm())) { 192 InputStream in = null; 193 try { 194 in = url.openStream(); 195 updated |= load(in); 196 } finally { 197 if (in != null) { 198 in.close(); 199 } 200 } 201 } 202 return updated; 203 } 204 205 /** 206 * Loads the rules from an input stream. 207 * 208 * @param in the stream to load, not null, not closed after use 209 * @throws Exception if an error occurs 210 */ load(InputStream in)211 private boolean load(InputStream in) throws IOException, StreamCorruptedException { 212 boolean updated = false; 213 Iterable<Version> loadedVersions = loadData(in); 214 for (Version loadedVersion : loadedVersions) { 215 // see https://github.com/ThreeTen/threetenbp/pull/28 for issue wrt 216 // multiple versions of lib on classpath 217 Version existing = versions.putIfAbsent(loadedVersion.versionId, loadedVersion); 218 if (existing != null && !existing.versionId.equals(loadedVersion.versionId)) { 219 throw new ZoneRulesException("Data already loaded for TZDB time-zone rules version: " + loadedVersion.versionId); 220 } 221 updated = true; 222 } 223 return updated; 224 } 225 226 /** 227 * Loads the rules from an input stream. 228 * 229 * @param in the stream to load, not null, not closed after use 230 * @throws Exception if an error occurs 231 */ loadData(InputStream in)232 private Iterable<Version> loadData(InputStream in) throws IOException, StreamCorruptedException { 233 DataInputStream dis = new DataInputStream(in); 234 if (dis.readByte() != 1) { 235 throw new StreamCorruptedException("File format not recognised"); 236 } 237 // group 238 String groupId = dis.readUTF(); 239 if ("TZDB".equals(groupId) == false) { 240 throw new StreamCorruptedException("File format not recognised"); 241 } 242 // versions 243 int versionCount = dis.readShort(); 244 String[] versionArray = new String[versionCount]; 245 for (int i = 0; i < versionCount; i++) { 246 versionArray[i] = dis.readUTF(); 247 } 248 // regions 249 int regionCount = dis.readShort(); 250 String[] regionArray = new String[regionCount]; 251 for (int i = 0; i < regionCount; i++) { 252 regionArray[i] = dis.readUTF(); 253 } 254 regionIds = Arrays.asList(regionArray); 255 // rules 256 int ruleCount = dis.readShort(); 257 Object[] ruleArray = new Object[ruleCount]; 258 for (int i = 0; i < ruleCount; i++) { 259 byte[] bytes = new byte[dis.readShort()]; 260 dis.readFully(bytes); 261 ruleArray[i] = bytes; 262 } 263 AtomicReferenceArray<Object> ruleData = new AtomicReferenceArray<Object>(ruleArray); 264 // link version-region-rules 265 Set<Version> versionSet = new HashSet<Version>(versionCount); 266 for (int i = 0; i < versionCount; i++) { 267 int versionRegionCount = dis.readShort(); 268 String[] versionRegionArray = new String[versionRegionCount]; 269 short[] versionRulesArray = new short[versionRegionCount]; 270 for (int j = 0; j < versionRegionCount; j++) { 271 versionRegionArray[j] = regionArray[dis.readShort()]; 272 versionRulesArray[j] = dis.readShort(); 273 } 274 versionSet.add(new Version(versionArray[i], versionRegionArray, versionRulesArray, ruleData)); 275 } 276 return versionSet; 277 } 278 279 @Override toString()280 public String toString() { 281 return "TZDB"; 282 } 283 284 //----------------------------------------------------------------------- 285 /** 286 * A version of the TZDB rules. 287 */ 288 static class Version { 289 private final String versionId; 290 private final String[] regionArray; 291 private final short[] ruleIndices; 292 private final AtomicReferenceArray<Object> ruleData; 293 Version(String versionId, String[] regionIds, short[] ruleIndices, AtomicReferenceArray<Object> ruleData)294 Version(String versionId, String[] regionIds, short[] ruleIndices, AtomicReferenceArray<Object> ruleData) { 295 this.ruleData = ruleData; 296 this.versionId = versionId; 297 this.regionArray = regionIds; 298 this.ruleIndices = ruleIndices; 299 } 300 getRules(String regionId)301 ZoneRules getRules(String regionId) { 302 int regionIndex = Arrays.binarySearch(regionArray, regionId); 303 if (regionIndex < 0) { 304 return null; 305 } 306 try { 307 return createRule(ruleIndices[regionIndex]); 308 } catch (Exception ex) { 309 throw new ZoneRulesException("Invalid binary time-zone data: TZDB:" + regionId + ", version: " + versionId, ex); 310 } 311 } 312 createRule(short index)313 ZoneRules createRule(short index) throws Exception { 314 Object obj = ruleData.get(index); 315 if (obj instanceof byte[]) { 316 byte[] bytes = (byte[]) obj; 317 DataInputStream dis = new DataInputStream(new ByteArrayInputStream(bytes)); 318 obj = Ser.read(dis); 319 ruleData.set(index, obj); 320 } 321 return (ZoneRules) obj; 322 } 323 324 @Override toString()325 public String toString() { 326 return versionId; 327 } 328 } 329 330 } 331