DateTimeZone data updated to version 2026cgtz.
diff --git a/src/main/java/org/joda/time/DateTimeZone.java b/src/main/java/org/joda/time/DateTimeZone.java
index 549522d55..fcf38217f 100644
--- a/src/main/java/org/joda/time/DateTimeZone.java
+++ b/src/main/java/org/joda/time/DateTimeZone.java
@@ -92,10 +92,12 @@
* However, the factory that accepts a TimeZone will attempt to convert from
* the old short id to a suitable long id.
*
- * There are four approaches to loading time-zone data, which are tried in this order:
+ * There are five approaches to loading time-zone data, which are tried in this order:
*
* - load the specific {@link Provider} specified by the system property
* {@code org.joda.time.DateTimeZone.Provider}.
+ *
- load a {@link Provider} declared in
+ * {@code META-INF/services/org.joda.time.tz.Provider}.
*
- load {@link ZoneInfoProvider} using the data in the filing system folder
* pointed to by system property {@code org.joda.time.DateTimeZone.Folder}.
*
- load {@link ZoneInfoProvider} using the data in the classpath location
@@ -103,7 +105,9 @@
*
- load {@link UTCProvider}
*
*
- * Unless you override the standard behaviour, the default if the third approach.
+ * Service provider declarations that cannot be loaded, created or validated are ignored.
+ *
+ * Unless you override the standard behaviour, the default is the fourth approach.
*
* DateTimeZone is thread-safe and immutable, and all subclasses must be as
* well.
@@ -517,10 +521,12 @@ private static Provider validateProvider(Provider provider) {
/**
* Gets the default zone provider.
*
- * This tries four approaches to loading data:
+ * This tries five approaches to loading data:
*
* - loads the provider identifier by the system property
*
org.joda.time.DateTimeZone.Provider.
+ * - loads a provider declared in
+ *
META-INF/services/org.joda.time.tz.Provider.
* - load
ZoneInfoProvider using the data in the filing system folder
* pointed to by system property org.joda.time.DateTimeZone.Folder.
* - loads
ZoneInfoProvider using the data in the classpath location
@@ -528,31 +534,29 @@ private static Provider validateProvider(Provider provider) {
* - loads
UTCProvider.
*
*
- * Unless you override the standard behaviour, the default if the third approach.
+ * Unless you override the standard behaviour, the default is the fourth approach.
*
* @return the default name provider
*/
private static Provider getDefaultProvider() {
- // approach 1
- try {
- String providerClass = System.getProperty("org.joda.time.DateTimeZone.Provider");
- if (providerClass != null) {
- try {
- // do not initialize the class until the type has been checked
- Class> cls = Class.forName(providerClass, false, DateTimeZone.class.getClassLoader());
- if (!Provider.class.isAssignableFrom(cls)) {
- throw new IllegalArgumentException("System property referred to class that does not implement " + Provider.class);
+ // approaches 1 and 2
+ Provider loadedProvider = ServiceProviderLoader.load(
+ "org.joda.time.DateTimeZone.Provider",
+ Provider.class,
+ new ServiceProviderLoader.Predicate() {
+ public boolean test(Provider provider) {
+ try {
+ validateProvider(provider);
+ return true;
+ } catch (IllegalArgumentException ex) {
+ return false;
+ }
}
- Provider provider = cls.asSubclass(Provider.class).getConstructor().newInstance();
- return validateProvider(provider);
- } catch (Exception ex) {
- throw new RuntimeException(ex);
- }
- }
- } catch (SecurityException ex) {
- // ignored
+ });
+ if (loadedProvider != null) {
+ return loadedProvider;
}
- // approach 2
+ // approach 3
try {
String dataFolder = System.getProperty("org.joda.time.DateTimeZone.Folder");
if (dataFolder != null) {
@@ -566,14 +570,14 @@ private static Provider getDefaultProvider() {
} catch (SecurityException ex) {
// ignored
}
- // approach 3
+ // approach 4
try {
Provider provider = new ZoneInfoProvider(DEFAULT_TZ_DATA_PATH);
return validateProvider(provider);
} catch (Exception ex) {
ex.printStackTrace();
}
- // approach 4
+ // approach 5
return new UTCProvider();
}
@@ -621,30 +625,16 @@ public static void setNameProvider(NameProvider nameProvider) throws SecurityExc
/**
* Gets the default name provider.
*
- * Tries the system property org.joda.time.DateTimeZone.NameProvider.
- * Then uses DefaultNameProvider.
+ * Tries the system property org.joda.time.DateTimeZone.NameProvider,
+ * then service providers declared in
+ * META-INF/services/org.joda.time.tz.NameProvider,
+ * then uses DefaultNameProvider.
*
* @return the default name provider
*/
private static NameProvider getDefaultNameProvider() {
- NameProvider nameProvider = null;
- try {
- String providerClass = System.getProperty("org.joda.time.DateTimeZone.NameProvider");
- if (providerClass != null) {
- try {
- // do not initialize the class until the type has been checked
- Class> cls = Class.forName(providerClass, false, DateTimeZone.class.getClassLoader());
- if (!NameProvider.class.isAssignableFrom(cls)) {
- throw new IllegalArgumentException("System property referred to class that does not implement " + NameProvider.class);
- }
- nameProvider = cls.asSubclass(NameProvider.class).getConstructor().newInstance();
- } catch (Exception ex) {
- throw new RuntimeException(ex);
- }
- }
- } catch (SecurityException ex) {
- // ignore
- }
+ NameProvider nameProvider = ServiceProviderLoader.load(
+ "org.joda.time.DateTimeZone.NameProvider", NameProvider.class, null);
if (nameProvider == null) {
nameProvider = new DefaultNameProvider();
diff --git a/src/main/java/org/joda/time/ServiceProviderLoader.java b/src/main/java/org/joda/time/ServiceProviderLoader.java
new file mode 100644
index 000000000..5bc57f56e
--- /dev/null
+++ b/src/main/java/org/joda/time/ServiceProviderLoader.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright 2026 Stephen Colebourne
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.joda.time;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.UnsupportedEncodingException;
+import java.net.URL;
+import java.util.Collection;
+import java.util.Enumeration;
+import java.util.LinkedHashSet;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * A utility for looking up service provider implementations.
+ *
+ * @author Brett Okken
+ * @since 2.15
+ */
+final class ServiceProviderLoader {
+
+ private static final Logger LOGGER = Logger.getLogger(ServiceProviderLoader.class.getName());
+
+ /**
+ * Tests whether a service provider is valid for use.
+ *
+ * @param the service provider type
+ */
+ static interface Predicate {
+
+ /**
+ * Tests the service provider.
+ *
+ * @param value the service provider, not null
+ * @return true if the service provider is valid
+ */
+ boolean test(T value);
+ }
+
+ /**
+ * Loads the configured service provider, or the first discovered service provider that can be
+ * created and validated. A configured provider failure is fatal, while a discovered provider
+ * failure is ignored so that the next provider can be attempted.
+ *
+ * @param the service provider type
+ * @param property the system property used to configure the provider, not null
+ * @param service the service provider type, not null
+ * @param predicate the predicate used to validate providers, null to accept all
+ * @return the configured provider or first valid discovered provider, null if none
+ * @throws RuntimeException if a configured provider cannot be loaded, created or validated
+ */
+ static T load(String property, Class service, Predicate super T> predicate) {
+ String propertyClass = null;
+ try {
+ propertyClass = System.getProperty(property);
+ } catch (SecurityException ex) {
+ // ignored
+ }
+ if (propertyClass != null) {
+ try {
+ ClassLoader loader = service.getClassLoader();
+ Class> cls = Class.forName(propertyClass, false, loader);
+ if (!service.isAssignableFrom(cls)) {
+ throw new IllegalArgumentException(
+ "System property referred to class that does not implement " + service);
+ }
+ T provider = cls.asSubclass(service).getConstructor().newInstance();
+ if (!test(predicate, provider)) {
+ throw new IllegalArgumentException("System property referred to an invalid provider");
+ }
+ return provider;
+ } catch (Exception ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ for (Class extends T> providerClass : lookupServiceProviders(service)) {
+ try {
+ T provider = providerClass.getConstructor().newInstance();
+ if (test(predicate, provider)) {
+ return provider;
+ }
+ LOGGER.log(Level.FINE, "Service provider failed validation " + providerClass.getName());
+ } catch (Exception ex) {
+ LOGGER.log(Level.FINE, "Unable to create service provider " + providerClass.getName(), ex);
+ } catch (LinkageError ex) {
+ LOGGER.log(Level.FINE, "Unable to create service provider " + providerClass.getName(), ex);
+ }
+ }
+ return null;
+ }
+
+ private static boolean test(Predicate super T> predicate, T provider) {
+ return predicate == null || predicate.test(provider);
+ }
+
+ private static Collection> lookupServiceProviders(Class service) {
+ String serviceName = service.getName();
+ String resourceName = "META-INF/services/" + serviceName;
+ ClassLoader loader = getClassLoader(service);
+ Set> providers = new LinkedHashSet>();
+
+ if (loader == null) {
+ return providers;
+ }
+ try {
+ Enumeration resources = loader.getResources(resourceName);
+ while (resources.hasMoreElements()) {
+ loadResource(service, loader, resources.nextElement(), providers);
+ }
+ } catch (IOException ex) {
+ LOGGER.log(Level.FINE, "Unable to find service provider files for " + serviceName, ex);
+ } catch (SecurityException ex) {
+ LOGGER.log(Level.FINE, "Unable to find service provider files for " + serviceName, ex);
+ }
+ return providers;
+ }
+
+ private static void loadResource(
+ Class service, ClassLoader loader, URL resource, Set> providers) {
+ InputStream in = null;
+ try {
+ in = resource.openStream();
+ BufferedReader reader = new BufferedReader(new InputStreamReader(in, "UTF-8"));
+ String line;
+ while ((line = reader.readLine()) != null) {
+ int comment = line.indexOf('#');
+ if (comment >= 0) {
+ line = line.substring(0, comment);
+ }
+ line = line.trim();
+ if (line.length() > 0) {
+ loadClass(service, loader, resource, line, providers);
+ }
+ }
+ } catch (UnsupportedEncodingException ex) {
+ throw new AssertionError(ex);
+ } catch (IOException ex) {
+ LOGGER.log(Level.FINE, "Unable to read service provider file " + resource, ex);
+ } catch (SecurityException ex) {
+ LOGGER.log(Level.FINE, "Unable to read service provider file " + resource, ex);
+ } finally {
+ if (in != null) {
+ try {
+ in.close();
+ } catch (IOException ex) {
+ LOGGER.log(Level.FINE, "Unable to close service provider file " + resource, ex);
+ }
+ }
+ }
+ }
+
+ private static void loadClass(
+ Class service, ClassLoader loader, URL resource, String className,
+ Set> providers) {
+ try {
+ Class> provider = Class.forName(className, false, loader);
+ if (service.isAssignableFrom(provider)) {
+ providers.add(provider.asSubclass(service));
+ } else {
+ LOGGER.log(
+ Level.FINE,
+ className + " in " + resource + " does not implement " + service.getName());
+ }
+ } catch (ClassNotFoundException ex) {
+ LOGGER.log(Level.FINE, "Unable to load service provider " + className + " from " + resource, ex);
+ } catch (LinkageError ex) {
+ LOGGER.log(Level.FINE, "Unable to load service provider " + className + " from " + resource, ex);
+ } catch (SecurityException ex) {
+ LOGGER.log(Level.FINE, "Unable to load service provider " + className + " from " + resource, ex);
+ }
+ }
+
+ private static ClassLoader getClassLoader(Class> service) {
+ ClassLoader loader = null;
+ try {
+ loader = Thread.currentThread().getContextClassLoader();
+ } catch (SecurityException ex) {
+ LOGGER.log(Level.FINE, "Unable to access the context class loader", ex);
+ }
+ if (loader == null) {
+ try {
+ loader = service.getClassLoader();
+ } catch (SecurityException ex) {
+ LOGGER.log(Level.FINE, "Unable to access the service class loader", ex);
+ }
+ }
+ if (loader == null) {
+ try {
+ loader = ServiceProviderLoader.class.getClassLoader();
+ } catch (SecurityException ex) {
+ LOGGER.log(Level.FINE, "Unable to access the service provider loader class loader", ex);
+ }
+ }
+ if (loader == null) {
+ try {
+ loader = ClassLoader.getSystemClassLoader();
+ } catch (SecurityException ex) {
+ LOGGER.log(Level.FINE, "Unable to access the system class loader", ex);
+ }
+ }
+ return loader;
+ }
+
+}
diff --git a/src/test/java/org/joda/time/TestAll.java b/src/test/java/org/joda/time/TestAll.java
index 3a579c755..890bdb210 100644
--- a/src/test/java/org/joda/time/TestAll.java
+++ b/src/test/java/org/joda/time/TestAll.java
@@ -121,6 +121,7 @@ public static Test suite() {
suite.addTest(TestDateTimeConstants.suite());
suite.addTest(TestDateTimeUtils.suite());
suite.addTest(TestDateTimeZone.suite());
+ suite.addTest(TestServiceProviderLoader.suite());
suite.addTest(TestDateTimeZoneCutover.suite());
suite.addTest(TestPeriodType.suite());
// suite.addTest(TestParseISO.suite());
diff --git a/src/test/java/org/joda/time/TestServiceProviderLoader.java b/src/test/java/org/joda/time/TestServiceProviderLoader.java
new file mode 100644
index 000000000..aa21f060f
--- /dev/null
+++ b/src/test/java/org/joda/time/TestServiceProviderLoader.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright 2026 Stephen Colebourne
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.joda.time;
+
+import java.io.File;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.Collections;
+import java.util.Locale;
+import java.util.Set;
+
+import org.joda.time.tz.DefaultNameProvider;
+import org.joda.time.tz.NameProvider;
+import org.joda.time.tz.Provider;
+import org.joda.time.tz.UTCProvider;
+import org.joda.time.tz.ZoneInfoProvider;
+
+import junit.framework.TestCase;
+import junit.framework.TestSuite;
+
+/**
+ * Tests service provider discovery for DateTimeZone.
+ */
+public class TestServiceProviderLoader extends TestCase {
+
+ private static final String PROVIDER_PROPERTY = "org.joda.time.DateTimeZone.Provider";
+ private static final String NAME_PROVIDER_PROPERTY = "org.joda.time.DateTimeZone.NameProvider";
+
+ private ClassLoader originalClassLoader;
+ private String originalProviderProperty;
+ private String originalNameProviderProperty;
+
+ public TestServiceProviderLoader(String name) {
+ super(name);
+ }
+
+ public static TestSuite suite() {
+ return new TestSuite(TestServiceProviderLoader.class);
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ originalClassLoader = Thread.currentThread().getContextClassLoader();
+ originalProviderProperty = System.getProperty(PROVIDER_PROPERTY);
+ originalNameProviderProperty = System.getProperty(NAME_PROVIDER_PROPERTY);
+ System.getProperties().remove(PROVIDER_PROPERTY);
+ System.getProperties().remove(NAME_PROVIDER_PROPERTY);
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ Thread.currentThread().setContextClassLoader(originalClassLoader);
+ restoreProperty(PROVIDER_PROPERTY, originalProviderProperty);
+ restoreProperty(NAME_PROVIDER_PROPERTY, originalNameProviderProperty);
+ DateTimeZone.setProvider(null);
+ DateTimeZone.setNameProvider(null);
+ }
+
+ public void testProviderUsesFirstUsableSpiDeclaration() throws Exception {
+ useSpiDirectory("valid");
+
+ DateTimeZone.setProvider(null);
+
+ assertEquals(FirstProvider.class, DateTimeZone.getProvider().getClass());
+ }
+
+ public void testNameProviderUsesFirstUsableSpiDeclaration() throws Exception {
+ useSpiDirectory("valid");
+
+ DateTimeZone.setNameProvider(null);
+
+ assertEquals(FirstNameProvider.class, DateTimeZone.getNameProvider().getClass());
+ }
+
+ public void testSpiFailuresFallBackToBuiltInProviders() throws Exception {
+ useSpiDirectory("unusable");
+
+ DateTimeZone.setProvider(null);
+ DateTimeZone.setNameProvider(null);
+
+ assertEquals(ZoneInfoProvider.class, DateTimeZone.getProvider().getClass());
+ assertEquals(DefaultNameProvider.class, DateTimeZone.getNameProvider().getClass());
+ }
+
+ public void testSystemPropertiesTakePrecedenceOverSpi() throws Exception {
+ useSpiDirectory("valid");
+ System.setProperty(PROVIDER_PROPERTY, UTCProvider.class.getName());
+ System.setProperty(NAME_PROVIDER_PROPERTY, DefaultNameProvider.class.getName());
+
+ DateTimeZone.setProvider(null);
+ DateTimeZone.setNameProvider(null);
+
+ assertEquals(UTCProvider.class, DateTimeZone.getProvider().getClass());
+ assertEquals(DefaultNameProvider.class, DateTimeZone.getNameProvider().getClass());
+ }
+
+ public void testBadSystemPropertiesDoNotFallBackToSpi() throws Exception {
+ useSpiDirectory("valid");
+ System.setProperty(PROVIDER_PROPERTY, InvalidProvider.class.getName());
+ try {
+ DateTimeZone.setProvider(null);
+ fail();
+ } catch (RuntimeException ex) {
+ // expected
+ }
+
+ System.getProperties().remove(PROVIDER_PROPERTY);
+ System.setProperty(NAME_PROVIDER_PROPERTY, String.class.getName());
+ try {
+ DateTimeZone.setNameProvider(null);
+ fail();
+ } catch (RuntimeException ex) {
+ // expected
+ }
+ }
+
+ private void useSpiDirectory(String directory) throws Exception {
+ URL url = new File("src/test/resources/spi/" + directory).toURI().toURL();
+ ClassLoader loader = new URLClassLoader(new URL[] {url}, originalClassLoader);
+ Thread.currentThread().setContextClassLoader(loader);
+ }
+
+ private static void restoreProperty(String name, String value) {
+ if (value == null) {
+ System.getProperties().remove(name);
+ } else {
+ System.setProperty(name, value);
+ }
+ }
+
+ public static class UncreatableProvider implements Provider {
+ public UncreatableProvider(String ignored) {
+ }
+
+ public Set getAvailableIDs() {
+ return Collections.singleton("UTC");
+ }
+
+ public DateTimeZone getZone(String id) {
+ return DateTimeZone.UTC;
+ }
+ }
+
+ public static class InvalidProvider implements Provider {
+ public Set getAvailableIDs() {
+ return Collections.emptySet();
+ }
+
+ public DateTimeZone getZone(String id) {
+ return null;
+ }
+ }
+
+ public static class FirstProvider implements Provider {
+ public Set getAvailableIDs() {
+ return Collections.singleton("UTC");
+ }
+
+ public DateTimeZone getZone(String id) {
+ return DateTimeZone.UTC;
+ }
+ }
+
+ public static class SecondProvider extends FirstProvider {
+ }
+
+ public static class UncreatableNameProvider implements NameProvider {
+ public UncreatableNameProvider(String ignored) {
+ }
+
+ public String getShortName(Locale locale, String id, String nameKey) {
+ return null;
+ }
+
+ public String getName(Locale locale, String id, String nameKey) {
+ return null;
+ }
+ }
+
+ public static class FirstNameProvider implements NameProvider {
+ public String getShortName(Locale locale, String id, String nameKey) {
+ return "first";
+ }
+
+ public String getName(Locale locale, String id, String nameKey) {
+ return "first";
+ }
+ }
+
+ public static class SecondNameProvider extends FirstNameProvider {
+ }
+}
diff --git a/src/test/resources/spi/unusable/META-INF/services/org.joda.time.tz.NameProvider b/src/test/resources/spi/unusable/META-INF/services/org.joda.time.tz.NameProvider
new file mode 100644
index 000000000..6d594a317
--- /dev/null
+++ b/src/test/resources/spi/unusable/META-INF/services/org.joda.time.tz.NameProvider
@@ -0,0 +1,3 @@
+invalid.MissingNameProvider
+java.lang.String
+org.joda.time.TestServiceProviderLoader$UncreatableNameProvider
diff --git a/src/test/resources/spi/unusable/META-INF/services/org.joda.time.tz.Provider b/src/test/resources/spi/unusable/META-INF/services/org.joda.time.tz.Provider
new file mode 100644
index 000000000..a2e274699
--- /dev/null
+++ b/src/test/resources/spi/unusable/META-INF/services/org.joda.time.tz.Provider
@@ -0,0 +1,4 @@
+invalid.MissingProvider
+java.lang.String
+org.joda.time.TestServiceProviderLoader$UncreatableProvider
+org.joda.time.TestServiceProviderLoader$InvalidProvider
diff --git a/src/test/resources/spi/valid/META-INF/services/org.joda.time.tz.NameProvider b/src/test/resources/spi/valid/META-INF/services/org.joda.time.tz.NameProvider
new file mode 100644
index 000000000..12bcea48d
--- /dev/null
+++ b/src/test/resources/spi/valid/META-INF/services/org.joda.time.tz.NameProvider
@@ -0,0 +1,4 @@
+# The first provider cannot be created, so the second is used.
+org.joda.time.TestServiceProviderLoader$UncreatableNameProvider
+org.joda.time.TestServiceProviderLoader$FirstNameProvider
+org.joda.time.TestServiceProviderLoader$SecondNameProvider
diff --git a/src/test/resources/spi/valid/META-INF/services/org.joda.time.tz.Provider b/src/test/resources/spi/valid/META-INF/services/org.joda.time.tz.Provider
new file mode 100644
index 000000000..2839220db
--- /dev/null
+++ b/src/test/resources/spi/valid/META-INF/services/org.joda.time.tz.Provider
@@ -0,0 +1,8 @@
+# Missing, incompatible, uncreatable, and invalid providers are skipped.
+invalid.MissingProvider
+java.lang.String
+org.joda.time.TestServiceProviderLoader$UncreatableProvider
+org.joda.time.TestServiceProviderLoader$InvalidProvider
+org.joda.time.TestServiceProviderLoader$FirstProvider
+org.joda.time.TestServiceProviderLoader$SecondProvider
+org.joda.time.TestServiceProviderLoader$FirstProvider