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.util.ArrayList; 035import java.util.Collections; 036import java.util.HashSet; 037import java.util.Iterator; 038import java.util.List; 039import java.util.NavigableMap; 040import java.util.Objects; 041import java.util.ServiceConfigurationError; 042import java.util.ServiceLoader; 043import java.util.Set; 044import java.util.concurrent.ConcurrentHashMap; 045import java.util.concurrent.ConcurrentMap; 046import java.util.concurrent.CopyOnWriteArrayList; 047 048import org.threeten.bp.DateTimeException; 049import org.threeten.bp.ZoneId; 050import org.threeten.bp.ZonedDateTime; 051 052/** 053 * Provider of time-zone rules to the system. 054 * <p> 055 * This class manages the configuration of time-zone rules. 056 * The static methods provide the public API that can be used to manage the providers. 057 * The abstract methods provide the SPI that allows rules to be provided. 058 * <p> 059 * Rules are looked up primarily by zone ID, as used by {@link ZoneId}. 060 * Only zone region IDs may be used, zone offset IDs are not used here. 061 * <p> 062 * Time-zone rules are political, thus the data can change at any time. 063 * Each provider will provide the latest rules for each zone ID, but they 064 * may also provide the history of how the rules changed. 065 * 066 * <h3>Specification for implementors</h3> 067 * This interface is a service provider that can be called by multiple threads. 068 * Implementations must be immutable and thread-safe. 069 * <p> 070 * Providers must ensure that once a rule has been seen by the application, the 071 * rule must continue to be available. 072 * <p> 073 * Many systems would like to update time-zone rules dynamically without stopping the JVM. 074 * When examined in detail, this is a complex problem. 075 * Providers may choose to handle dynamic updates, however the default provider does not. 076 */ 077public abstract class ZoneRulesProvider { 078 079 /** 080 * The set of loaded providers. 081 */ 082 private static final CopyOnWriteArrayList<ZoneRulesProvider> PROVIDERS = new CopyOnWriteArrayList<>(); 083 /** 084 * The lookup from zone region ID to provider. 085 */ 086 private static final ConcurrentMap<String, ZoneRulesProvider> ZONES = new ConcurrentHashMap<>(512, 0.75f, 2); 087 static { 088 ServiceLoader<ZoneRulesProvider> sl = ServiceLoader.load(ZoneRulesProvider.class, ZoneRulesProvider.class.getClassLoader()); 089 List<ZoneRulesProvider> loaded = new ArrayList<>(); 090 Iterator<ZoneRulesProvider> it = sl.iterator(); 091 while (it.hasNext()) { 092 ZoneRulesProvider provider; 093 try { 094 provider = it.next(); 095 } catch (ServiceConfigurationError ex) { 096 if (ex.getCause() instanceof SecurityException) { 097 continue; // ignore the security exception, try the next provider 098 } 099 throw ex; 100 } 101 registerProvider0(provider); 102 } 103 // CopyOnWriteList could be slow if lots of providers and each added individually 104 PROVIDERS.addAll(loaded); 105 } 106 107 //------------------------------------------------------------------------- 108 /** 109 * Gets the set of available zone IDs. 110 * <p> 111 * These zone IDs are loaded and available for use by {@code ZoneId}. 112 * 113 * @return a modifiable copy of the set of zone IDs, not null 114 */ 115 public static Set<String> getAvailableZoneIds() { 116 return new HashSet<>(ZONES.keySet()); 117 } 118 119 /** 120 * Gets the rules for the zone ID. 121 * <p> 122 * This returns the latest available rules for the zone ID. 123 * <p> 124 * This method relies on time-zone data provider files that are configured. 125 * These are loaded using a {@code ServiceLoader}. 126 * 127 * @param zoneId the zone region ID as used by {@code ZoneId}, not null 128 * @return the rules for the ID, not null 129 * @throws ZoneRulesException if the zone ID is unknown 130 */ 131 public static ZoneRules getRules(String zoneId) { 132 Objects.requireNonNull(zoneId, "zoneId"); 133 return getProvider(zoneId).provideRules(zoneId); 134 } 135 136 /** 137 * Gets the history of rules for the zone ID. 138 * <p> 139 * Time-zones are defined by governments and change frequently. 140 * This method allows applications to find the history of changes to the 141 * rules for a single zone ID. The map is keyed by a string, which is the 142 * version string associated with the rules. 143 * <p> 144 * The exact meaning and format of the version is provider specific. 145 * The version must follow lexicographical order, thus the returned map will 146 * be order from the oldest known rules to the newest available rules. 147 * The default 'TZDB' group uses version numbering consisting of the year 148 * followed by a letter, such as '2009e' or '2012f'. 149 * <p> 150 * Implementations must provide a result for each valid zone ID, however 151 * they do not have to provide a history of rules. 152 * Thus the map will always contain one element, and will only contain more 153 * than one element if historical rule information is available. 154 * 155 * @param zoneId the zone region ID as used by {@code ZoneId}, not null 156 * @return a modifiable copy of the history of the rules for the ID, sorted 157 * from oldest to newest, not null 158 * @throws ZoneRulesException if the zone ID is unknown 159 */ 160 public static NavigableMap<String, ZoneRules> getVersions(String zoneId) { 161 Objects.requireNonNull(zoneId, "zoneId"); 162 return getProvider(zoneId).provideVersions(zoneId); 163 } 164 165 /** 166 * Gets the provider for the zone ID. 167 * 168 * @param zoneId the zone region ID as used by {@code ZoneId}, not null 169 * @return the provider, not null 170 * @throws ZoneRulesException if the zone ID is unknown 171 */ 172 private static ZoneRulesProvider getProvider(String zoneId) { 173 ZoneRulesProvider provider = ZONES.get(zoneId); 174 if (provider == null) { 175 if (ZONES.isEmpty()) { 176 throw new ZoneRulesException("No time-zone data files registered"); 177 } 178 throw new ZoneRulesException("Unknown time-zone ID: " + zoneId); 179 } 180 return provider; 181 } 182 183 //------------------------------------------------------------------------- 184 /** 185 * Registers a zone rules provider. 186 * <p> 187 * This adds a new provider to those currently available. 188 * A provider supplies rules for one or more zone IDs. 189 * A provider cannot be registered if it supplies a zone ID that has already been 190 * registered. See the notes on time-zone IDs in {@link ZoneId}, especially 191 * the section on using the concept of a "group" to make IDs unique. 192 * <p> 193 * To ensure the integrity of time-zones already created, there is no way 194 * to deregister providers. 195 * 196 * @param provider the provider to register, not null 197 * @throws ZoneRulesException if a region is already registered 198 */ 199 public static void registerProvider(ZoneRulesProvider provider) { 200 Objects.requireNonNull(provider, "provider"); 201 registerProvider0(provider); 202 PROVIDERS.add(provider); 203 } 204 205 /** 206 * Registers the provider. 207 * 208 * @param provider the provider to register, not null 209 * @throws ZoneRulesException if unable to complete the registration 210 */ 211 private static void registerProvider0(ZoneRulesProvider provider) { 212 for (String zoneId : provider.provideZoneIds()) { 213 Objects.requireNonNull(zoneId, "zoneId"); 214 ZoneRulesProvider old = ZONES.putIfAbsent(zoneId, provider.provideBind(zoneId)); 215 if (old != null) { 216 throw new ZoneRulesException( 217 "Unable to register zone as one already registered with that ID: " + zoneId + 218 ", currently loading from provider: " + provider); 219 } 220 } 221 } 222 223 //------------------------------------------------------------------------- 224 /** 225 * Refreshes the rules from the underlying data provider. 226 * <p> 227 * This method is an extension point that allows providers to refresh their 228 * rules dynamically at a time of the applications choosing. 229 * After calling this method, the offset stored in any {@link ZonedDateTime} 230 * may be invalid for the zone ID. 231 * <p> 232 * Dynamic behavior is entirely optional and most providers, including the 233 * default provider, do not support it. 234 * 235 * @return true if the rules were updated 236 * @throws ZoneRulesException if an error occurs during the refresh 237 */ 238 public static boolean refresh() { 239 boolean changed = false; 240 for (ZoneRulesProvider provider : PROVIDERS) { 241 changed |= provider.provideRefresh(); 242 } 243 return changed; 244 } 245 246 //----------------------------------------------------------------------- 247 /** 248 * Constructor. 249 */ 250 protected ZoneRulesProvider() { 251 } 252 253 //----------------------------------------------------------------------- 254 /** 255 * SPI method to get the available zone IDs. 256 * <p> 257 * This obtains the IDs that this {@code ZoneRulesProvider} provides. 258 * A provider should provide data for at least one region. 259 * <p> 260 * The returned regions remain available and valid for the lifetime of the application. 261 * A dynamic provider may increase the set of regions as more data becomes available. 262 * 263 * @return the unmodifiable set of region IDs being provided, not null 264 */ 265 protected abstract Set<String> provideZoneIds(); 266 267 /** 268 * SPI method to bind to the specified zone ID. 269 * <p> 270 * {@code ZoneRulesProvider} has a lookup from zone ID to provider. 271 * This method is used when building that lookup, allowing providers 272 * to insert a derived provider that is precisely tuned to the zone ID. 273 * This replaces two hash map lookups by one, enhancing performance. 274 * <p> 275 * This optimization is optional. Returning {@code this} is acceptable. 276 * <p> 277 * This implementation creates a bound provider that caches the 278 * rules from the underlying provider. The request to version history 279 * is forward on to the underlying. This is suitable for providers that 280 * cannot change their contents during the lifetime of the JVM. 281 * 282 * @param zoneId the zone region ID as used by {@code ZoneId}, not null 283 * @return the resolved provider for the ID, not null 284 * @throws DateTimeException if there is no provider for the specified group 285 */ 286 protected ZoneRulesProvider provideBind(String zoneId) { 287 return new BoundProvider(this, zoneId); 288 } 289 290 /** 291 * SPI method to get the rules for the zone ID. 292 * <p> 293 * This loads the rules for the region and version specified. 294 * The version may be null to indicate the "latest" version. 295 * 296 * @param regionId the time-zone region ID, not null 297 * @return the rules, not null 298 * @throws DateTimeException if rules cannot be obtained 299 */ 300 protected abstract ZoneRules provideRules(String regionId); 301 302 /** 303 * SPI method to get the history of rules for the zone ID. 304 * <p> 305 * This returns a map of historical rules keyed by a version string. 306 * The exact meaning and format of the version is provider specific. 307 * The version must follow lexicographical order, thus the returned map will 308 * be order from the oldest known rules to the newest available rules. 309 * The default 'TZDB' group uses version numbering consisting of the year 310 * followed by a letter, such as '2009e' or '2012f'. 311 * <p> 312 * Implementations must provide a result for each valid zone ID, however 313 * they do not have to provide a history of rules. 314 * Thus the map will always contain one element, and will only contain more 315 * than one element if historical rule information is available. 316 * <p> 317 * The returned versions remain available and valid for the lifetime of the application. 318 * A dynamic provider may increase the set of versions as more data becomes available. 319 * 320 * @param zoneId the zone region ID as used by {@code ZoneId}, not null 321 * @return a modifiable copy of the history of the rules for the ID, sorted 322 * from oldest to newest, not null 323 * @throws ZoneRulesException if the zone ID is unknown 324 */ 325 protected abstract NavigableMap<String, ZoneRules> provideVersions(String zoneId); 326 327 /** 328 * SPI method to refresh the rules from the underlying data provider. 329 * <p> 330 * This method provides the opportunity for a provider to dynamically 331 * recheck the underlying data provider to find the latest rules. 332 * This could be used to load new rules without stopping the JVM. 333 * Dynamic behavior is entirely optional and most providers do not support it. 334 * <p> 335 * This implementation returns false. 336 * 337 * @return true if the rules were updated 338 * @throws DateTimeException if an error occurs during the refresh 339 */ 340 protected boolean provideRefresh() { 341 return false; 342 } 343 344 //------------------------------------------------------------------------- 345 /** 346 * A provider bound to a single zone ID. 347 */ 348 private static class BoundProvider extends ZoneRulesProvider { 349 private final ZoneRulesProvider provider; 350 private final String zoneId; 351 private final ZoneRules rules; 352 353 private BoundProvider(ZoneRulesProvider provider, String zoneId) { 354 this.provider = provider; 355 this.zoneId = zoneId; 356 this.rules = provider.provideRules(zoneId); 357 } 358 359 @Override 360 protected Set<String> provideZoneIds() { 361 return new HashSet<>(Collections.singleton(zoneId)); 362 } 363 364 @Override 365 protected ZoneRules provideRules(String regionId) { 366 return rules; 367 } 368 369 @Override 370 protected NavigableMap<String, ZoneRules> provideVersions(String zoneId) { 371 return provider.provideVersions(zoneId); 372 } 373 374 @Override 375 public String toString() { 376 return zoneId; 377 } 378 } 379 380}