src/share/classes/java/time/zone/ZoneRulesProvider.java

Print this page

        

@@ -59,15 +59,15 @@
  * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
 package java.time.zone;
 
-import java.time.DateTimeException;
+import java.security.AccessController;
+import java.security.PrivilegedAction;
 import java.time.ZoneId;
 import java.time.ZonedDateTime;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.NavigableMap;
 import java.util.Objects;

@@ -83,10 +83,29 @@
  * <p>
  * This class manages the configuration of time-zone rules.
  * The static methods provide the public API that can be used to manage the providers.
  * The abstract methods provide the SPI that allows rules to be provided.
  * <p>
+ * ZoneRulesProvider may be installed in an instance of the Java Platform as
+ * extension classes, that is, jar files placed into any of the usual extension
+ * directories. Installed providers are loaded using the service-provider loading
+ * facility defined by the {@link ServiceLoader} class. A ZoneRulesProvider
+ * identifies itself with a provider configuration file named
+ * {@code java.time.zone.ZoneRulesProvider} in the resource directory
+ * {@code META-INF/services}. The file should contain a line that specifies the
+ * fully qualified concrete zonerules-provider class name.
+ * Providers may also be made available by adding them to the class path or by
+ * registering themselves via {@link #registerProvider} method.
+ * <p>
+ * The Java virtual machine has a default provider that provides zone rules
+ * for the time-zones defined by IANA Time Zone Database (TZDB). If the system
+ * property {@code java.time.zone.DefaultZoneRulesProvider} is defined then
+ * it is taken to be the fully-qualified name of a concrete ZoneRulesProvider
+ * class to be loaded as the default provider, using the system class loader.
+ * If this system property is not defined, a system-default provider will be
+ * loaded to serve as the default provider.
+ * <p>
  * Rules are looked up primarily by zone ID, as used by {@link ZoneId}.
  * Only zone region IDs may be used, zone offset IDs are not used here.
  * <p>
  * Time-zone rules are political, thus the data can change at any time.
  * Each provider will provide the latest rules for each zone ID, but they

@@ -97,10 +116,12 @@
  * Implementations must be immutable and thread-safe.
  * <p>
  * Providers must ensure that once a rule has been seen by the application, the
  * rule must continue to be available.
  * <p>
+*  Providers are encouraged to implement a meaningful {@code toString} method.
+ * <p>
  * Many systems would like to update time-zone rules dynamically without stopping the JVM.
  * When examined in detail, this is a complex problem.
  * Providers may choose to handle dynamic updates, however the default provider does not.
  *
  * @since 1.8

@@ -110,17 +131,38 @@
     /**
      * The set of loaded providers.
      */
     private static final CopyOnWriteArrayList<ZoneRulesProvider> PROVIDERS = new CopyOnWriteArrayList<>();
     /**
-     * The lookup from zone region ID to provider.
+     * The lookup from zone ID to provider.
      */
     private static final ConcurrentMap<String, ZoneRulesProvider> ZONES = new ConcurrentHashMap<>(512, 0.75f, 2);
+
     static {
+        // if the property java.time.zone.DefaultZoneRulesProvider is
+        // set then its value is the class name of the default provider
+        final List<ZoneRulesProvider> loaded = new ArrayList<>();
+        AccessController.doPrivileged(new PrivilegedAction() {
+            public Object run() {
+                String prop = System.getProperty("java.time.zone.DefaultZoneRulesProvider");
+                if (prop != null) {
+                    try {
+                        Class<?> c = Class.forName(prop, true, ClassLoader.getSystemClassLoader());
+                        ZoneRulesProvider provider = ZoneRulesProvider.class.cast(c.newInstance());
+                        registerProvider(provider);
+                        loaded.add(provider);
+                    } catch (Exception x) {
+                        throw new Error(x);
+                    }
+                } else {
         registerProvider(new TzdbZoneRulesProvider());
+                }
+                return null;
+            }
+        });
+
         ServiceLoader<ZoneRulesProvider> sl = ServiceLoader.load(ZoneRulesProvider.class, ClassLoader.getSystemClassLoader());
-        List<ZoneRulesProvider> loaded = new ArrayList<>();
         Iterator<ZoneRulesProvider> it = sl.iterator();
         while (it.hasNext()) {
             ZoneRulesProvider provider;
             try {
                 provider = it.next();

@@ -128,21 +170,30 @@
                 if (ex.getCause() instanceof SecurityException) {
                     continue;  // ignore the security exception, try the next provider
                 }
                 throw ex;
             }
+            boolean found = false;
+            for (ZoneRulesProvider p : loaded) {
+                if (p.getClass() == provider.getClass()) {
+                    found = true;
+                }
+            }
+            if (!found) {
             registerProvider0(provider);
+                loaded.add(provider);
+            }
         }
         // CopyOnWriteList could be slow if lots of providers and each added individually
         PROVIDERS.addAll(loaded);
     }
 
     //-------------------------------------------------------------------------
     /**
      * Gets the set of available zone IDs.
      * <p>
-     * These zone IDs are loaded and available for use by {@code ZoneId}.
+     * These IDs are the string form of a {@link ZoneId}.
      *
      * @return a modifiable copy of the set of zone IDs, not null
      */
     public static Set<String> getAvailableZoneIds() {
         return new HashSet<>(ZONES.keySet());

@@ -153,18 +204,29 @@
      * <p>
      * This returns the latest available rules for the zone ID.
      * <p>
      * This method relies on time-zone data provider files that are configured.
      * These are loaded using a {@code ServiceLoader}.
-     *
-     * @param zoneId  the zone region ID as used by {@code ZoneId}, not null
-     * @return the rules for the ID, not null
-     * @throws ZoneRulesException if the zone ID is unknown
+     * <p>
+     * The caching flag is designed to allow provider implementations to
+     * prevent the rules being cached in {@code ZoneId}.
+     * Under normal circumstances, the caching of zone rules is highly desirable
+     * as it will provide greater performance. However, there is a use case where
+     * the caching would not be desirable, see {@link #provideRules}.
+     *
+     * @param zoneId the zone ID as defined by {@code ZoneId}, not null
+     * @param forCaching whether the rules are being queried for caching,
+     * true if the returned rules will be cached by {@code ZoneId},
+     * false if they will be returned to the user without being cached in {@code ZoneId}
+     * @return the rules, null if {@code forCaching} is true and this
+     * is a dynamic provider that wants to prevent caching in {@code ZoneId},
+     * otherwise not null
+     * @throws ZoneRulesException if rules cannot be obtained for the zone ID
      */
-    public static ZoneRules getRules(String zoneId) {
+    public static ZoneRules getRules(String zoneId, boolean forCaching) {
         Objects.requireNonNull(zoneId, "zoneId");
-        return getProvider(zoneId).provideRules(zoneId);
+        return getProvider(zoneId).provideRules(zoneId, forCaching);
     }
 
     /**
      * Gets the history of rules for the zone ID.
      * <p>

@@ -182,24 +244,24 @@
      * Implementations must provide a result for each valid zone ID, however
      * they do not have to provide a history of rules.
      * Thus the map will always contain one element, and will only contain more
      * than one element if historical rule information is available.
      *
-     * @param zoneId  the zone region ID as used by {@code ZoneId}, not null
+     * @param zoneId  the zone ID as defined by {@code ZoneId}, not null
      * @return a modifiable copy of the history of the rules for the ID, sorted
      *  from oldest to newest, not null
-     * @throws ZoneRulesException if the zone ID is unknown
+     * @throws ZoneRulesException if history cannot be obtained for the zone ID
      */
     public static NavigableMap<String, ZoneRules> getVersions(String zoneId) {
         Objects.requireNonNull(zoneId, "zoneId");
         return getProvider(zoneId).provideVersions(zoneId);
     }
 
     /**
      * Gets the provider for the zone ID.
      *
-     * @param zoneId  the zone region ID as used by {@code ZoneId}, not null
+     * @param zoneId  the zone ID as defined by {@code ZoneId}, not null
      * @return the provider, not null
      * @throws ZoneRulesException if the zone ID is unknown
      */
     private static ZoneRulesProvider getProvider(String zoneId) {
         ZoneRulesProvider provider = ZONES.get(zoneId);

@@ -224,11 +286,11 @@
      * <p>
      * To ensure the integrity of time-zones already created, there is no way
      * to deregister providers.
      *
      * @param provider  the provider to register, not null
-     * @throws ZoneRulesException if a region is already registered
+     * @throws ZoneRulesException if a zone ID is already registered
      */
     public static void registerProvider(ZoneRulesProvider provider) {
         Objects.requireNonNull(provider, "provider");
         registerProvider0(provider);
         PROVIDERS.add(provider);

@@ -241,30 +303,38 @@
      * @throws ZoneRulesException if unable to complete the registration
      */
     private static void registerProvider0(ZoneRulesProvider provider) {
         for (String zoneId : provider.provideZoneIds()) {
             Objects.requireNonNull(zoneId, "zoneId");
-            ZoneRulesProvider old = ZONES.putIfAbsent(zoneId, provider.provideBind(zoneId));
+            ZoneRulesProvider old = ZONES.putIfAbsent(zoneId, provider);
             if (old != null) {
                 throw new ZoneRulesException(
                     "Unable to register zone as one already registered with that ID: " + zoneId +
                     ", currently loading from provider: " + provider);
             }
         }
     }
 
-    //-------------------------------------------------------------------------
     /**
      * Refreshes the rules from the underlying data provider.
      * <p>
-     * This method is an extension point that allows providers to refresh their
-     * rules dynamically at a time of the applications choosing.
+     * This method allows an application to request that the providers check
+     * for any updates to the provided rules.
      * After calling this method, the offset stored in any {@link ZonedDateTime}
      * may be invalid for the zone ID.
      * <p>
-     * Dynamic behavior is entirely optional and most providers, including the
-     * default provider, do not support it.
+     * Dynamic update of rules is a complex problem and most applications
+     * should not use this method or dynamic rules.
+     * To achieve dynamic rules, a provider implementation will have to be written
+     * as per the specification of this class.
+     * In addition, instances of {@code ZoneRules} must not be cached in the
+     * application as they will become stale. However, the boolean flag on
+     * {@link #provideRules(String, boolean)} allows provider implementations
+     * to control the caching of {@code ZoneId}, potentially ensuring that
+     * all objects in the system see the new rules.
+     * Note that there is likely to be a cost in performance of a dynamic rules
+     * provider. Note also that no dynamic rules provider is in this specification.
      *
      * @return true if the rules were updated
      * @throws ZoneRulesException if an error occurs during the refresh
      */
     public static boolean refresh() {

@@ -273,11 +343,10 @@
             changed |= provider.provideRefresh();
         }
         return changed;
     }
 
-    //-----------------------------------------------------------------------
     /**
      * Constructor.
      */
     protected ZoneRulesProvider() {
     }

@@ -285,53 +354,47 @@
     //-----------------------------------------------------------------------
     /**
      * SPI method to get the available zone IDs.
      * <p>
      * This obtains the IDs that this {@code ZoneRulesProvider} provides.
-     * A provider should provide data for at least one region.
+     * A provider should provide data for at least one zone ID.
      * <p>
-     * The returned regions remain available and valid for the lifetime of the application.
-     * A dynamic provider may increase the set of regions as more data becomes available.
+     * The returned zone IDs remain available and valid for the lifetime of the application.
+     * A dynamic provider may increase the set of IDs as more data becomes available.
      *
-     * @return the unmodifiable set of region IDs being provided, not null
+     * @return the set of zone IDs being provided, not null
+     * @throws ZoneRulesException if a problem occurs while providing the IDs
      */
     protected abstract Set<String> provideZoneIds();
 
     /**
-     * SPI method to bind to the specified zone ID.
-     * <p>
-     * {@code ZoneRulesProvider} has a lookup from zone ID to provider.
-     * This method is used when building that lookup, allowing providers
-     * to insert a derived provider that is precisely tuned to the zone ID.
-     * This replaces two hash map lookups by one, enhancing performance.
-     * <p>
-     * This optimization is optional. Returning {@code this} is acceptable.
-     * <p>
-     * This implementation creates a bound provider that caches the
-     * rules from the underlying provider. The request to version history
-     * is forward on to the underlying. This is suitable for providers that
-     * cannot change their contents during the lifetime of the JVM.
-     *
-     * @param zoneId  the zone region ID as used by {@code ZoneId}, not null
-     * @return the resolved provider for the ID, not null
-     * @throws DateTimeException if there is no provider for the specified group
-     */
-    protected ZoneRulesProvider provideBind(String zoneId) {
-        return new BoundProvider(this, zoneId);
-    }
-
-    /**
      * SPI method to get the rules for the zone ID.
      * <p>
-     * This loads the rules for the region and version specified.
-     * The version may be null to indicate the "latest" version.
-     *
-     * @param regionId  the time-zone region ID, not null
-     * @return the rules, not null
-     * @throws DateTimeException if rules cannot be obtained
+     * This loads the rules for the specified zone ID.
+     * The provider implementation must validate that the zone ID is valid and
+     * available, throwing a {@code ZoneRulesException} if it is not.
+     * The result of the method in the valid case depends on the caching flag.
+     * <p>
+     * If the provider implementation is not dynamic, then the result of the
+     * method must be the non-null set of rules selected by the ID.
+     * <p>
+     * If the provider implementation is dynamic, then the flag gives the option
+     * of preventing the returned rules from being cached in {@link ZoneId}.
+     * When the flag is true, the provider is permitted to return null, where
+     * null will prevent the rules from being cached in {@code ZoneId}.
+     * When the flag is false, the provider must return non-null rules.
+     *
+     * @param zoneId the zone ID as defined by {@code ZoneId}, not null
+     * @param forCaching whether the rules are being queried for caching,
+     * true if the returned rules will be cached by {@code ZoneId},
+     * false if they will be returned to the user without being cached in {@code ZoneId}
+     * @return the rules, null if {@code forCaching} is true and this
+     * is a dynamic provider that wants to prevent caching in {@code ZoneId},
+     * otherwise not null
+     * @throws ZoneRulesException if rules cannot be obtained for the zone ID
      */
-    protected abstract ZoneRules provideRules(String regionId);
+    protected abstract ZoneRules provideRules(String zoneId, boolean forCaching);
 
     /**
      * SPI method to get the history of rules for the zone ID.
      * <p>
      * This returns a map of historical rules keyed by a version string.

@@ -341,20 +404,20 @@
      * The default 'TZDB' group uses version numbering consisting of the year
      * followed by a letter, such as '2009e' or '2012f'.
      * <p>
      * Implementations must provide a result for each valid zone ID, however
      * they do not have to provide a history of rules.
-     * Thus the map will always contain one element, and will only contain more
-     * than one element if historical rule information is available.
+     * Thus the map will contain at least one element, and will only contain
+     * more than one element if historical rule information is available.
      * <p>
      * The returned versions remain available and valid for the lifetime of the application.
      * A dynamic provider may increase the set of versions as more data becomes available.
      *
-     * @param zoneId  the zone region ID as used by {@code ZoneId}, not null
+     * @param zoneId  the zone ID as defined by {@code ZoneId}, not null
      * @return a modifiable copy of the history of the rules for the ID, sorted
      *  from oldest to newest, not null
-     * @throws ZoneRulesException if the zone ID is unknown
+     * @throws ZoneRulesException if history cannot be obtained for the zone ID
      */
     protected abstract NavigableMap<String, ZoneRules> provideVersions(String zoneId);
 
     /**
      * SPI method to refresh the rules from the underlying data provider.

@@ -365,48 +428,12 @@
      * Dynamic behavior is entirely optional and most providers do not support it.
      * <p>
      * This implementation returns false.
      *
      * @return true if the rules were updated
-     * @throws DateTimeException if an error occurs during the refresh
+     * @throws ZoneRulesException if an error occurs during the refresh
      */
     protected boolean provideRefresh() {
         return false;
     }
 
-    //-------------------------------------------------------------------------
-    /**
-     * A provider bound to a single zone ID.
-     */
-    private static class BoundProvider extends ZoneRulesProvider {
-        private final ZoneRulesProvider provider;
-        private final String zoneId;
-        private final ZoneRules rules;
-
-        private BoundProvider(ZoneRulesProvider provider, String zoneId) {
-            this.provider = provider;
-            this.zoneId = zoneId;
-            this.rules = provider.provideRules(zoneId);
-        }
-
-        @Override
-        protected Set<String> provideZoneIds() {
-            return new HashSet<>(Collections.singleton(zoneId));
-        }
-
-        @Override
-        protected ZoneRules provideRules(String regionId) {
-            return rules;
-        }
-
-        @Override
-        protected NavigableMap<String, ZoneRules> provideVersions(String zoneId) {
-            return provider.provideVersions(zoneId);
-        }
-
-        @Override
-        public String toString() {
-            return zoneId;
-        }
-    }
-
 }