• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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