1 /*
2  * hunt-time: A time library for D programming language.
3  *
4  * Copyright (C) 2015-2018 HuntLabs
5  *
6  * Website: https://www.huntlabs.net/
7  *
8  * Licensed under the Apache-2.0 License.
9  *
10  */
11 
12 module hunt.time.zone.ZoneRulesProvider;
13 
14 // import hunt.security.AccessController;
15 // import hunt.security.PrivilegedAction;
16 // import hunt.time.ZoneId;
17 // import hunt.time.ZonedDateTime;
18 import hunt.collection.ArrayList;
19 import hunt.collection.HashSet;
20 import hunt.collection.Iterator;
21 import hunt.collection.List;
22 import hunt.collection.NavigableMap;
23 import hunt.collection.Set;
24 import hunt.collection.HashMap;
25 import hunt.collection.Collections;
26 
27 import hunt.time.zone.ZoneRules;
28 import hunt.time.zone.ZoneRulesException;
29 import hunt.time.util.Common;
30 import hunt.time.util.ServiceLoader;
31 import hunt.time.zone.TzdbZoneRulesProvider;
32 
33 version (HUNT_DEBUG) import hunt.logging.ConsoleLogger;
34 
35 import std.concurrency : initOnce;
36 
37 /**
38  * Provider of time-zone rules to the system.
39  * !(p)
40  * This class manages the configuration of time-zone rules.
41  * The static methods provide the API that can be used to manage the providers.
42  * The abstract methods provide the SPI that allows rules to be provided.
43  * !(p)
44  * ZoneRulesProvider may be installed _in an instance of the Java Platform as
45  * extension classes, that is, jar files placed into any of the usual extension
46  * directories. Installed providers are loaded using the service-provider loading
47  * facility defined by the {@link ServiceLoader} class. A ZoneRulesProvider
48  * identifies itself with a provider configuration file named
49  * {@code hunt.time.zone.ZoneRulesProvider} _in the resource directory
50  * {@code META-INF/services}. The file should contain a line that specifies the
51  * fully qualified concrete zonerules-provider class name.
52  * Providers may also be made available by adding them to the class path or by
53  * registering themselves via {@link #registerProvider} method.
54  * !(p)
55  * The Java virtual machine has a default provider that provides zone rules
56  * for the time-zones defined by IANA Time Zone Database (TZDB). If the system
57  * property {@code hunt.time.zone.DefaultZoneRulesProvider} is defined then
58  * it is taken to be the fully-qualified name of a concrete ZoneRulesProvider
59  * class to be loaded as the default provider, using the system class loader.
60  * If this system property is not defined, a system-default provider will be
61  * loaded to serve as the default provider.
62  * !(p)
63  * Rules are looked up primarily by zone ID, as used by {@link ZoneId}.
64  * Only zone region IDs may be used, zone offset IDs are not used here.
65  * !(p)
66  * Time-zone rules are political, thus the data can change at any time.
67  * Each provider will provide the latest rules for each zone ID, but they
68  * may also provide the history of how the rules changed.
69  *
70  * @implSpec
71  * This interface is a service provider that can be called by multiple threads.
72  * Implementations must be immutable and thread-safe.
73  * !(p)
74  * Providers must ensure that once a rule has been seen by the application, the
75  * rule must continue to be available.
76  * !(p)
77  * Providers are encouraged to implement a meaningful {@code toString} method.
78  * !(p)
79  * Many systems would like to update time-zone rules dynamically without stopping the JVM.
80  * When examined _in detail, this is a complex problem.
81  * Providers may choose to handle dynamic updates, however the default provider does not.
82  *
83  * @since 1.8
84  */
85 abstract class ZoneRulesProvider {
86 
87     /**
88      * The set of loaded providers.
89      */
90     static ArrayList!(ZoneRulesProvider) PROVIDERS() {
91         initializeGlobals();
92         return _PROVIDERS;
93     }
94 
95     /**
96      * The lookup from zone ID to provider.
97      */
98     static HashMap!(string, ZoneRulesProvider) ZONES() {
99         initializeGlobals();
100         return _ZONES;
101     }
102     
103     // __gshared ConcurrentMap!(string, ZoneRulesProvider) ZONES;
104 
105     /**
106      * The zone ID data
107      */
108     private __gshared Set!(string) ZONE_IDS;
109     private __gshared ArrayList!(ZoneRulesProvider) _PROVIDERS;
110     private __gshared HashMap!(string, ZoneRulesProvider) _ZONES;
111 
112     private static void initializeGlobals() {
113         __gshared bool isInitialized = false;
114         initOnce!(isInitialized)({
115             _PROVIDERS = new ArrayList!(ZoneRulesProvider)();
116             _ZONES = new HashMap!(string, ZoneRulesProvider)(512, 0.75f/* , 2 */);
117             registerProvider0(new TzdbZoneRulesProvider());
118             return true;
119         }());
120     }
121 
122     // FIXME: Needing refactor or cleanup -@zhangxueping at 4/4/2019, 5:51:46 PM
123     // 
124     // shared static this() {
125     //     // registerProvider(new TzdbZoneRulesProvider());
126     // }
127 
128     
129     //-------------------------------------------------------------------------
130     /**
131      * Gets the set of available zone IDs.
132      * !(p)
133      * These IDs are the string form of a {@link ZoneId}.
134      *
135      * @return the unmodifiable set of zone IDs, not null
136      */
137     static Set!(string) getAvailableZoneIds() {
138         initializeGlobals();
139         return ZONE_IDS;
140     }
141 
142 
143     /**
144      * Gets the rules for the zone ID.
145      * !(p)
146      * This returns the latest available rules for the zone ID.
147      * !(p)
148      * This method relies on time-zone data provider files that are configured.
149      * These are loaded using a {@code ServiceLoader}.
150      * !(p)
151      * The caching flag is designed to allow provider implementations to
152      * prevent the rules being cached _in {@code ZoneId}.
153      * Under normal circumstances, the caching of zone rules is highly desirable
154      * as it will provide greater performance. However, there is a use case where
155      * the caching would not be desirable, see {@link #provideRules}.
156      *
157      * @param zoneId the zone ID as defined by {@code ZoneId}, not null
158      * @param forCaching whether the rules are being queried for caching,
159      * true if the returned rules will be cached by {@code ZoneId},
160      * false if they will be returned to the user without being cached _in {@code ZoneId}
161      * @return the rules, null if {@code forCaching} is true and this
162      * is a dynamic provider that wants to prevent caching _in {@code ZoneId},
163      * otherwise not null
164      * @throws ZoneRulesException if rules cannot be obtained for the zone ID
165      */
166     static ZoneRules getRules(string zoneId, bool forCaching) {
167         assert(zoneId, "zoneId");
168         return getProvider(zoneId).provideRules(zoneId, forCaching);
169     }
170 
171     /**
172      * Gets the history of rules for the zone ID.
173      * !(p)
174      * Time-zones are defined by governments and change frequently.
175      * This method allows applications to find the history of changes to the
176      * rules for a single zone ID. The map is keyed by a string, which is the
177      * version string associated with the rules.
178      * !(p)
179      * The exact meaning and format of the version is provider specific.
180      * The version must follow lexicographical order, thus the returned map will
181      * be order from the oldest known rules to the newest available rules.
182      * The default 'TZDB' group uses version numbering consisting of the year
183      * followed by a letter, such as '2009e' or '2012f'.
184      * !(p)
185      * Implementations must provide a result for each valid zone ID, however
186      * they do not have to provide a history of rules.
187      * Thus the map will always contain one element, and will only contain more
188      * than one element if historical rule information is available.
189      *
190      * @param zoneId  the zone ID as defined by {@code ZoneId}, not null
191      * @return a modifiable copy of the history of the rules for the ID, sorted
192      *  from oldest to newest, not null
193      * @throws ZoneRulesException if history cannot be obtained for the zone ID
194      */
195     static NavigableMap!(string, ZoneRules) getVersions(string zoneId) {
196         assert(zoneId, "zoneId");
197         return getProvider(zoneId).provideVersions(zoneId);
198     }
199 
200     /**
201      * Gets the provider for the zone ID.
202      *
203      * @param zoneId  the zone ID as defined by {@code ZoneId}, not null
204      * @return the provider, not null
205      * @throws ZoneRulesException if the zone ID is unknown
206      */
207     private static ZoneRulesProvider getProvider(string zoneId) {
208         ZoneRulesProvider provider = ZONES.get(zoneId);
209         // version (HUNT_DEBUG) tracef("zoneId=%s, ZONES.size=%d", zoneId, ZONES.size());
210         if (provider is null) {
211             if (ZONES.isEmpty()) {
212                 throw new ZoneRulesException("No time-zone data files registered");
213             }
214             throw new ZoneRulesException("Unknown time-zone ID: " ~ zoneId);
215         }
216         return provider;
217     }
218 
219     //-------------------------------------------------------------------------
220     /**
221      * Registers a zone rules provider.
222      * !(p)
223      * This adds a new provider to those currently available.
224      * A provider supplies rules for one or more zone IDs.
225      * A provider cannot be registered if it supplies a zone ID that has already been
226      * registered. See the notes on time-zone IDs _in {@link ZoneId}, especially
227      * the section on using the concept of a "group" to make IDs unique.
228      * !(p)
229      * To ensure the integrity of time-zones already created, there is no way
230      * to deregister providers.
231      *
232      * @param provider  the provider to register, not null
233      * @throws ZoneRulesException if a zone ID is already registered
234      */
235     static void registerProvider(ZoneRulesProvider provider) {
236         assert(provider !is null, "provider");
237         synchronized {
238             // registerProvider0(provider);
239             // PROVIDERS.add(provider);
240             initializeGlobals();
241             registerProvider0(provider);
242         }
243     }
244 
245     /**
246      * Registers the provider.
247      *
248      * @param provider  the provider to register, not null
249      * @throws ZoneRulesException if unable to complete the registration
250      */
251     private static void registerProvider0(ZoneRulesProvider provider) {
252         foreach(string zoneId ; provider.provideZoneIds()) {
253             assert(zoneId, "zoneId");
254             ZoneRulesProvider old = _ZONES.putIfAbsent(zoneId, provider);
255             if (old !is null) {
256                 throw new ZoneRulesException(
257                     "Unable to register zone as one already registered with that ID: " ~ zoneId ~
258                     ", currently loading from provider: " ~ provider.toString);
259             }
260         }
261         // Set!(string) combinedSet = new HashSet!(string)();
262         // foreach(data; ZONES.keySet()) {
263         //     combinedSet.add(data);
264         // }
265         // import std.stdio;
266         // writeln("zone set : ",combinedSet);
267         // ZONE_IDS = /* Collections.unmodifiableSet */(combinedSet);
268         ZONE_IDS = new HashSet!(string)();
269         foreach(data; _ZONES.keySet()) {
270             ZONE_IDS.add(data);
271         }
272 
273         _PROVIDERS.add(provider);
274     }
275 
276     /**
277      * Refreshes the rules from the underlying data provider.
278      * !(p)
279      * This method allows an application to request that the providers check
280      * for any updates to the provided rules.
281      * After calling this method, the offset stored _in any {@link ZonedDateTime}
282      * may be invalid for the zone ID.
283      * !(p)
284      * Dynamic update of rules is a complex problem and most applications
285      * should not use this method or dynamic rules.
286      * To achieve dynamic rules, a provider implementation will have to be written
287      * as per the specification of this class.
288      * In addition, instances of {@code ZoneRules} must not be cached _in the
289      * application as they will become stale. However, the bool flag on
290      * {@link #provideRules(string, bool)} allows provider implementations
291      * to control the caching of {@code ZoneId}, potentially ensuring that
292      * all objects _in the system see the new rules.
293      * Note that there is likely to be a cost _in performance of a dynamic rules
294      * provider. Note also that no dynamic rules provider is _in this specification.
295      *
296      * @return true if the rules were updated
297      * @throws ZoneRulesException if an error occurs during the refresh
298      */
299     static bool refresh() {
300         bool changed = false;
301         foreach(ZoneRulesProvider provider ; PROVIDERS) {
302             changed |= provider.provideRefresh();
303         }
304         return changed;
305     }
306 
307     /**
308      * Constructor.
309      */
310     protected this() {
311     }
312 
313     //-----------------------------------------------------------------------
314     /**
315      * SPI method to get the available zone IDs.
316      * !(p)
317      * This obtains the IDs that this {@code ZoneRulesProvider} provides.
318      * A provider should provide data for at least one zone ID.
319      * !(p)
320      * The returned zone IDs remain available and valid for the lifetime of the application.
321      * A dynamic provider may increase the set of IDs as more data becomes available.
322      *
323      * @return the set of zone IDs being provided, not null
324      * @throws ZoneRulesException if a problem occurs while providing the IDs
325      */
326     protected abstract Set!(string) provideZoneIds();
327 
328     /**
329      * SPI method to get the rules for the zone ID.
330      * !(p)
331      * This loads the rules for the specified zone ID.
332      * The provider implementation must validate that the zone ID is valid and
333      * available, throwing a {@code ZoneRulesException} if it is not.
334      * The result of the method _in the valid case depends on the caching flag.
335      * !(p)
336      * If the provider implementation is not dynamic, then the result of the
337      * method must be the non-null set of rules selected by the ID.
338      * !(p)
339      * If the provider implementation is dynamic, then the flag gives the option
340      * of preventing the returned rules from being cached _in {@link ZoneId}.
341      * When the flag is true, the provider is permitted to return null, where
342      * null will prevent the rules from being cached _in {@code ZoneId}.
343      * When the flag is false, the provider must return non-null rules.
344      *
345      * @param zoneId the zone ID as defined by {@code ZoneId}, not null
346      * @param forCaching whether the rules are being queried for caching,
347      * true if the returned rules will be cached by {@code ZoneId},
348      * false if they will be returned to the user without being cached _in {@code ZoneId}
349      * @return the rules, null if {@code forCaching} is true and this
350      * is a dynamic provider that wants to prevent caching _in {@code ZoneId},
351      * otherwise not null
352      * @throws ZoneRulesException if rules cannot be obtained for the zone ID
353      */
354     protected abstract ZoneRules provideRules(string zoneId, bool forCaching);
355 
356     /**
357      * SPI method to get the history of rules for the zone ID.
358      * !(p)
359      * This returns a map of historical rules keyed by a version string.
360      * The exact meaning and format of the version is provider specific.
361      * The version must follow lexicographical order, thus the returned map will
362      * be order from the oldest known rules to the newest available rules.
363      * The default 'TZDB' group uses version numbering consisting of the year
364      * followed by a letter, such as '2009e' or '2012f'.
365      * !(p)
366      * Implementations must provide a result for each valid zone ID, however
367      * they do not have to provide a history of rules.
368      * Thus the map will contain at least one element, and will only contain
369      * more than one element if historical rule information is available.
370      * !(p)
371      * The returned versions remain available and valid for the lifetime of the application.
372      * A dynamic provider may increase the set of versions as more data becomes available.
373      *
374      * @param zoneId  the zone ID as defined by {@code ZoneId}, not null
375      * @return a modifiable copy of the history of the rules for the ID, sorted
376      *  from oldest to newest, not null
377      * @throws ZoneRulesException if history cannot be obtained for the zone ID
378      */
379     protected abstract NavigableMap!(string, ZoneRules) provideVersions(string zoneId);
380 
381     /**
382      * SPI method to refresh the rules from the underlying data provider.
383      * !(p)
384      * This method provides the opportunity for a provider to dynamically
385      * recheck the underlying data provider to find the latest rules.
386      * This could be used to load new rules without stopping the JVM.
387      * Dynamic behavior is entirely optional and most providers do not support it.
388      * !(p)
389      * This implementation returns false.
390      *
391      * @return true if the rules were updated
392      * @throws ZoneRulesException if an error occurs during the refresh
393      */
394     protected bool provideRefresh() {
395         return false;
396     }
397 
398 }