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 }