001/* 002 * Copyright (c) 2007-2013, Stephen Colebourne & Michael Nascimento Santos 003 * 004 * All rights reserved. 005 * 006 * Redistribution and use in source and binary forms, with or without 007 * modification, are permitted provided that the following conditions are met: 008 * 009 * * Redistributions of source code must retain the above copyright notice, 010 * this list of conditions and the following disclaimer. 011 * 012 * * Redistributions in binary form must reproduce the above copyright notice, 013 * this list of conditions and the following disclaimer in the documentation 014 * and/or other materials provided with the distribution. 015 * 016 * * Neither the name of JSR-310 nor the names of its contributors 017 * may be used to endorse or promote products derived from this software 018 * without specific prior written permission. 019 * 020 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 021 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 022 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 023 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 024 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 025 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 026 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 027 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 028 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 029 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 030 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 031 */ 032package org.threeten.bp.zone; 033 034import java.io.ByteArrayInputStream; 035import java.io.DataInputStream; 036import java.io.IOException; 037import java.io.InputStream; 038import java.io.StreamCorruptedException; 039import java.net.URL; 040import java.util.Arrays; 041import java.util.Enumeration; 042import java.util.HashSet; 043import java.util.NavigableMap; 044import java.util.Objects; 045import java.util.Set; 046import java.util.TreeMap; 047import java.util.concurrent.ConcurrentNavigableMap; 048import java.util.concurrent.ConcurrentSkipListMap; 049import java.util.concurrent.CopyOnWriteArraySet; 050import java.util.concurrent.atomic.AtomicReferenceArray; 051 052/** 053 * Loads time-zone rules for 'TZDB'. 054 * <p> 055 * This class is public for the service loader to access. 056 * 057 * <h3>Specification for implementors</h3> 058 * This class is immutable and thread-safe. 059 */ 060public final class TzdbZoneRulesProvider extends ZoneRulesProvider { 061 // TODO: can this be private/hidden in any way? 062 // service loader seems to need it to be public 063 064 /** 065 * All the regions that are available. 066 */ 067 private final Set<String> regionIds = new CopyOnWriteArraySet<>(); 068 /** 069 * All the versions that are available. 070 */ 071 private final ConcurrentNavigableMap<String, Version> versions = new ConcurrentSkipListMap<>(); 072 /** 073 * All the URLs that have been loaded. 074 * Uses String to avoid equals() on URL. 075 */ 076 private Set<String> loadedUrls = new CopyOnWriteArraySet<>(); 077 078 /** 079 * Creates an instance. 080 * Created by the {@code ServiceLoader}. 081 * 082 * @throws ZoneRulesException if unable to load 083 */ 084 public TzdbZoneRulesProvider() { 085 super(); 086 if (load(ZoneRulesProvider.class.getClassLoader()) == false) { 087 throw new ZoneRulesException("No time-zone rules found for 'TZDB'"); 088 } 089 } 090 091 //----------------------------------------------------------------------- 092 @Override 093 protected Set<String> provideZoneIds() { 094 return new HashSet<>(regionIds); 095 } 096 097 @Override 098 protected ZoneRules provideRules(String zoneId) { 099 Objects.requireNonNull(zoneId, "zoneId"); 100 ZoneRules rules = versions.lastEntry().getValue().getRules(zoneId); 101 if (rules == null) { 102 throw new ZoneRulesException("Unknown time-zone ID: " + zoneId); 103 } 104 return rules; 105 } 106 107 @Override 108 protected NavigableMap<String, ZoneRules> provideVersions(String zoneId) { 109 TreeMap<String, ZoneRules> map = new TreeMap<>(); 110 for (Version version : versions.values()) { 111 ZoneRules rules = version.getRules(zoneId); 112 if (rules != null) { 113 map.put(version.versionId, rules); 114 } 115 } 116 return map; 117 } 118 119 //------------------------------------------------------------------------- 120 /** 121 * Loads the rules. 122 * 123 * @param classLoader the class loader to use, not null 124 * @return true if updated 125 * @throws ZoneRulesException if unable to load 126 */ 127 private boolean load(ClassLoader classLoader) { 128 boolean updated = false; 129 URL url = null; 130 try { 131 Enumeration<URL> en = classLoader.getResources("org/threeten/bp/TZDB.dat"); 132 while (en.hasMoreElements()) { 133 url = en.nextElement(); 134 if (loadedUrls.add(url.toExternalForm())) { 135 Iterable<Version> loadedVersions = load(url); 136 for (Version loadedVersion : loadedVersions) { 137 if (versions.putIfAbsent(loadedVersion.versionId, loadedVersion) != null) { 138 throw new ZoneRulesException("Data already loaded for TZDB time-zone rules version: " + loadedVersion.versionId); 139 } 140 } 141 updated = true; 142 } 143 } 144 } catch (Exception ex) { 145 throw new ZoneRulesException("Unable to load TZDB time-zone rules: " + url, ex); 146 } 147 return updated; 148 } 149 150 /** 151 * Loads the rules from a URL, often in a jar file. 152 * 153 * @param url the jar file to load, not null 154 * @throws Exception if an error occurs 155 */ 156 private Iterable<Version> load(URL url) throws ClassNotFoundException, IOException { 157 try (InputStream in = url.openStream()) { 158 DataInputStream dis = new DataInputStream(in); 159 if (dis.readByte() != 1) { 160 throw new StreamCorruptedException("File format not recognised"); 161 } 162 // group 163 String groupId = dis.readUTF(); 164 if ("TZDB".equals(groupId) == false) { 165 throw new StreamCorruptedException("File format not recognised"); 166 } 167 // versions 168 int versionCount = dis.readShort(); 169 String[] versionArray = new String[versionCount]; 170 for (int i = 0; i < versionCount; i++) { 171 versionArray[i] = dis.readUTF(); 172 } 173 // regions 174 int regionCount = dis.readShort(); 175 String[] regionArray = new String[regionCount]; 176 for (int i = 0; i < regionCount; i++) { 177 regionArray[i] = dis.readUTF(); 178 } 179 regionIds.addAll(Arrays.asList(regionArray)); 180 // rules 181 int ruleCount = dis.readShort(); 182 Object[] ruleArray = new Object[ruleCount]; 183 for (int i = 0; i < ruleCount; i++) { 184 byte[] bytes = new byte[dis.readShort()]; 185 dis.readFully(bytes); 186 ruleArray[i] = bytes; 187 } 188 AtomicReferenceArray<Object> ruleData = new AtomicReferenceArray<>(ruleArray); 189 // link version-region-rules 190 Set<Version> versionSet = new HashSet<Version>(versionCount); 191 for (int i = 0; i < versionCount; i++) { 192 int versionRegionCount = dis.readShort(); 193 String[] versionRegionArray = new String[versionRegionCount]; 194 short[] versionRulesArray = new short[versionRegionCount]; 195 for (int j = 0; j < versionRegionCount; j++) { 196 versionRegionArray[j] = regionArray[dis.readShort()]; 197 versionRulesArray[j] = dis.readShort(); 198 } 199 versionSet.add(new Version(versionArray[i], versionRegionArray, versionRulesArray, ruleData)); 200 } 201 return versionSet; 202 } 203 } 204 205 @Override 206 public String toString() { 207 return "TZDB"; 208 } 209 210 //----------------------------------------------------------------------- 211 /** 212 * A version of the TZDB rules. 213 */ 214 static class Version { 215 private final String versionId; 216 private final String[] regionArray; 217 private final short[] ruleIndices; 218 private final AtomicReferenceArray<Object> ruleData; 219 220 Version(String versionId, String[] regionIds, short[] ruleIndices, AtomicReferenceArray<Object> ruleData) { 221 this.ruleData = ruleData; 222 this.versionId = versionId; 223 this.regionArray = regionIds; 224 this.ruleIndices = ruleIndices; 225 } 226 227 ZoneRules getRules(String regionId) { 228 int regionIndex = Arrays.binarySearch(regionArray, regionId); 229 if (regionIndex < 0) { 230 return null; 231 } 232 try { 233 return createRule(ruleIndices[regionIndex]); 234 } catch (Exception ex) { 235 throw new ZoneRulesException("Invalid binary time-zone data: TZDB:" + regionId + ", version: " + versionId, ex); 236 } 237 } 238 239 ZoneRules createRule(short index) throws Exception { 240 Object obj = ruleData.get(index); 241 if (obj instanceof byte[]) { 242 byte[] bytes = (byte[]) obj; 243 DataInputStream dis = new DataInputStream(new ByteArrayInputStream(bytes)); 244 obj = Ser.read(dis); 245 ruleData.set(index, obj); 246 } 247 return (ZoneRules) obj; 248 } 249 250 @Override 251 public String toString() { 252 return versionId; 253 } 254 } 255 256}