Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/changes/changes.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@
<body>

<!-- types are add, fix, remove, update -->
<release version="2.15.0" date="TBD" description="v2.15.0">
<action dev="bokken" type="add">
Allow DateTimeZone Provider and NameProvider implementations to be discovered using service provider files.
</action>
</release>
<release version="2.14.3" date="2026-07-26" description="v2.14.3">
<action dev="jodastephen" type="update">
DateTimeZone data updated to version 2026cgtz.
Expand Down
78 changes: 34 additions & 44 deletions src/main/java/org/joda/time/DateTimeZone.java
Original file line number Diff line number Diff line change
Expand Up @@ -92,18 +92,22 @@
* However, the factory that accepts a TimeZone will attempt to convert from
* the old short id to a suitable long id.
* <p>
* 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:
* <ol>
* <li>load the specific {@link Provider} specified by the system property
* {@code org.joda.time.DateTimeZone.Provider}.
* <li>load a {@link Provider} declared in
* {@code META-INF/services/org.joda.time.tz.Provider}.
* <li>load {@link ZoneInfoProvider} using the data in the filing system folder
* pointed to by system property {@code org.joda.time.DateTimeZone.Folder}.
* <li>load {@link ZoneInfoProvider} using the data in the classpath location
* {@code org/joda/time/tz/data}.
* <li>load {@link UTCProvider}
* </ol>
* <p>
* Unless you override the standard behaviour, the default if the third approach.
* Service provider declarations that cannot be loaded, created or validated are ignored.
* <p>
* Unless you override the standard behaviour, the default is the fourth approach.
* <p>
* DateTimeZone is thread-safe and immutable, and all subclasses must be as
* well.
Expand Down Expand Up @@ -517,42 +521,42 @@ private static Provider validateProvider(Provider provider) {
/**
* Gets the default zone provider.
* <p>
* This tries four approaches to loading data:
* This tries five approaches to loading data:
* <ol>
* <li>loads the provider identifier by the system property
* <code>org.joda.time.DateTimeZone.Provider</code>.
* <li>loads a provider declared in
* <code>META-INF/services/org.joda.time.tz.Provider</code>.
* <li>load <code>ZoneInfoProvider</code> using the data in the filing system folder
* pointed to by system property <code>org.joda.time.DateTimeZone.Folder</code>.
* <li>loads <code>ZoneInfoProvider</code> using the data in the classpath location
* <code>org/joda/time/tz/data</code>.
* <li>loads <code>UTCProvider</code>.
* </ol>
* <p>
* 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<Provider>() {
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) {
Expand All @@ -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();
}

Expand Down Expand Up @@ -621,30 +625,16 @@ public static void setNameProvider(NameProvider nameProvider) throws SecurityExc
/**
* Gets the default name provider.
* <p>
* Tries the system property <code>org.joda.time.DateTimeZone.NameProvider</code>.
* Then uses <code>DefaultNameProvider</code>.
* Tries the system property <code>org.joda.time.DateTimeZone.NameProvider</code>,
* then service providers declared in
* <code>META-INF/services/org.joda.time.tz.NameProvider</code>,
* then uses <code>DefaultNameProvider</code>.
*
* @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();
Expand Down
222 changes: 222 additions & 0 deletions src/main/java/org/joda/time/ServiceProviderLoader.java
Original file line number Diff line number Diff line change
@@ -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 <T> the service provider type
*/
static interface Predicate<T> {

/**
* 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 <T> 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> T load(String property, Class<T> 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 <T> boolean test(Predicate<? super T> predicate, T provider) {
return predicate == null || predicate.test(provider);
}

private static <T> Collection<Class<? extends T>> lookupServiceProviders(Class<T> service) {
String serviceName = service.getName();
String resourceName = "META-INF/services/" + serviceName;
ClassLoader loader = getClassLoader(service);
Set<Class<? extends T>> providers = new LinkedHashSet<Class<? extends T>>();

if (loader == null) {
return providers;
}
try {
Enumeration<URL> 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 <T> void loadResource(
Class<T> service, ClassLoader loader, URL resource, Set<Class<? extends T>> 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 <T> void loadClass(
Class<T> service, ClassLoader loader, URL resource, String className,
Set<Class<? extends T>> 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;
}

}
1 change: 1 addition & 0 deletions src/test/java/org/joda/time/TestAll.java
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
Loading