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}