Comprehensive update to the framework's TimeZone handling, including a new TimeZoneAwareLocaleContext and a LocaleContextResolver for Spring MVC

A few noteworthy minor changes: LocaleContext.getLocale() may return null in special cases (not by default), which our own accessing classes are able to handle now. If there is a non-null TimeZone user setting, we're exposing it to all collaborating libraries, in particular to JSTL, Velocity and JasperReports. Our JSR-310 and Joda-Time support falls back to checking the general LocaleContext TimeZone now, adapting it to their time zone types, if no more specific setting has been provided. Our DefaultConversionService has TimeZone<->ZoneId converters registered. And finally, we're using a custom parseTimeZoneString method now that doesn't accept the TimeZone.getTimeZone(String) GMT fallback for an invalid time zone id anymore.

Issue: SPR-1528
This commit is contained in:
Juergen Hoeller
2013-10-04 23:14:08 +02:00
parent 52cca48f40
commit 4574528a27
35 changed files with 1570 additions and 270 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 the original author or authors.
* Copyright 2002-2013 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,6 +21,7 @@ import java.util.UUID;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.converter.ConverterRegistry;
import org.springframework.util.ClassUtils;
/**
* A specialization of {@link GenericConversionService} configured by default with
@@ -31,10 +32,16 @@ import org.springframework.core.convert.converter.ConverterRegistry;
* {@code ConverterRegistry} instance.
*
* @author Chris Beams
* @author Juergen Hoeller
* @since 3.1
*/
public class DefaultConversionService extends GenericConversionService {
/** Java 8's java.time package available? */
private static final boolean zoneIdAvailable =
ClassUtils.isPresent("java.time.ZoneId", DefaultConversionService.class.getClassLoader());
/**
* Create a new {@code DefaultConversionService} with the set of
* {@linkplain DefaultConversionService#addDefaultConverters(ConverterRegistry) default converters}.
@@ -43,46 +50,55 @@ public class DefaultConversionService extends GenericConversionService {
addDefaultConverters(this);
}
// static utility methods
/**
* Add converters appropriate for most environments.
* @param converterRegistry the registry of converters to add to (must also be castable to ConversionService)
* @throws ClassCastException if the converterRegistry could not be cast to a ConversionService
* @param converterRegistry the registry of converters to add to (must also be castable to ConversionService,
* e.g. being a {@link ConfigurableConversionService})
* @throws ClassCastException if the given ConverterRegistry could not be cast to a ConversionService
*/
public static void addDefaultConverters(ConverterRegistry converterRegistry) {
addScalarConverters(converterRegistry);
addCollectionConverters(converterRegistry);
addBinaryConverters(converterRegistry);
addFallbackConverters(converterRegistry);
converterRegistry.addConverter(new ByteBufferConverter((ConversionService) converterRegistry));
if (zoneIdAvailable) {
ZoneIdConverterRegistrar.registerZoneIdConverters(converterRegistry);
}
converterRegistry.addConverter(new ObjectToObjectConverter());
converterRegistry.addConverter(new IdToEntityConverter((ConversionService) converterRegistry));
converterRegistry.addConverter(new FallbackObjectToStringConverter());
}
// internal helpers
private static void addScalarConverters(ConverterRegistry converterRegistry) {
ConversionService conversionService = (ConversionService) converterRegistry;
converterRegistry.addConverter(new StringToBooleanConverter());
converterRegistry.addConverter(Boolean.class, String.class, new ObjectToStringConverter());
converterRegistry.addConverterFactory(new NumberToNumberConverterFactory());
converterRegistry.addConverterFactory(new StringToNumberConverterFactory());
converterRegistry.addConverter(Number.class, String.class, new ObjectToStringConverter());
converterRegistry.addConverterFactory(new NumberToNumberConverterFactory());
converterRegistry.addConverter(new StringToCharacterConverter());
converterRegistry.addConverter(Character.class, String.class, new ObjectToStringConverter());
converterRegistry.addConverter(new NumberToCharacterConverter());
converterRegistry.addConverterFactory(new CharacterToNumberFactory());
converterRegistry.addConverter(new StringToBooleanConverter());
converterRegistry.addConverter(Boolean.class, String.class, new ObjectToStringConverter());
converterRegistry.addConverterFactory(new StringToEnumConverterFactory());
converterRegistry.addConverter(Enum.class, String.class, new EnumToStringConverter(conversionService));
converterRegistry.addConverter(Enum.class, String.class,
new EnumToStringConverter((ConversionService) converterRegistry));
converterRegistry.addConverter(new StringToLocaleConverter());
converterRegistry.addConverter(Locale.class, String.class, new ObjectToStringConverter());
converterRegistry.addConverter(new PropertiesToStringConverter());
converterRegistry.addConverter(new StringToPropertiesConverter());
converterRegistry.addConverter(new PropertiesToStringConverter());
converterRegistry.addConverter(new StringToUUIDConverter());
converterRegistry.addConverter(UUID.class, String.class, new ObjectToStringConverter());
@@ -90,6 +106,7 @@ public class DefaultConversionService extends GenericConversionService {
private static void addCollectionConverters(ConverterRegistry converterRegistry) {
ConversionService conversionService = (ConversionService) converterRegistry;
converterRegistry.addConverter(new ArrayToCollectionConverter(conversionService));
converterRegistry.addConverter(new CollectionToArrayConverter(conversionService));
@@ -110,16 +127,16 @@ public class DefaultConversionService extends GenericConversionService {
converterRegistry.addConverter(new ObjectToCollectionConverter(conversionService));
}
private static void addBinaryConverters(ConverterRegistry converterRegistry) {
ConversionService conversionService = (ConversionService) converterRegistry;
converterRegistry.addConverter(new ByteBufferConverter(conversionService));
}
private static void addFallbackConverters(ConverterRegistry converterRegistry) {
ConversionService conversionService = (ConversionService) converterRegistry;
converterRegistry.addConverter(new ObjectToObjectConverter());
converterRegistry.addConverter(new IdToEntityConverter(conversionService));
converterRegistry.addConverter(new FallbackObjectToStringConverter());
/**
* Inner class to avoid a hard-coded dependency on Java 8's {@link java.time.ZoneId}.
*/
private static final class ZoneIdConverterRegistrar {
public static void registerZoneIdConverters(ConverterRegistry converterRegistry) {
converterRegistry.addConverter(new TimeZoneToZoneIdConverter());
converterRegistry.addConverter(new ZoneIdToTimeZoneConverter());
}
}
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright 2002-2013 the original author or authors.
*
* 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.springframework.core.convert.support;
import java.time.ZoneId;
import java.util.TimeZone;
import org.springframework.core.convert.converter.Converter;
/**
* Simple Converter from {@link java.util.TimeZone} to Java 8's {@link java.time.ZoneId}.
*
* <p>Note that Spring's default ConversionService setup understands the 'of' convention that
* the JSR-310 {@code java.time} package consistently uses. That convention is implemented
* reflectively in {@link ObjectToObjectConverter}, not in specific JSR-310 converters.
*
* @author Juergen Hoeller
* @since 4.0
* @see ZoneIdToTimeZoneConverter
*/
class TimeZoneToZoneIdConverter implements Converter<TimeZone, ZoneId> {
@Override
public ZoneId convert(TimeZone source) {
return source.toZoneId();
}
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright 2002-2013 the original author or authors.
*
* 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.springframework.core.convert.support;
import java.time.ZoneId;
import java.util.TimeZone;
import org.springframework.core.convert.converter.Converter;
/**
* Simple Converter from Java 8's {@link java.time.ZoneId} to {@link java.util.TimeZone}.
*
* <p>Note that Spring's default ConversionService setup understands the 'of' convention that
* the JSR-310 {@code java.time} package consistently uses. That convention is implemented
* reflectively in {@link ObjectToObjectConverter}, not in specific JSR-310 converters.
*
* @author Juergen Hoeller
* @since 4.0
* @see TimeZoneToZoneIdConverter
*/
class ZoneIdToTimeZoneConverter implements Converter<ZoneId, TimeZone> {
@Override
public TimeZone convert(ZoneId source) {
return TimeZone.getTimeZone(source);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 the original author or authors.
* Copyright 2002-2013 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -28,6 +28,7 @@ import java.util.Locale;
import java.util.Properties;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.TimeZone;
import java.util.TreeSet;
/**
@@ -674,10 +675,11 @@ public abstract class StringUtils {
/**
* Parse the given {@code localeString} value into a {@link Locale}.
* <p>This is the inverse operation of {@link Locale#toString Locale's toString}.
* @param localeString the locale string, following {@code Locale's}
* @param localeString the locale String, following {@code Locale's}
* {@code toString()} format ("en", "en_UK", etc);
* also accepts spaces as separators, as an alternative to underscores
* @return a corresponding {@code Locale} instance
* @throws IllegalArgumentException in case of an invalid locale specification
*/
public static Locale parseLocaleString(String localeString) {
String[] parts = tokenizeToStringArray(localeString, "_ ", false, false);
@@ -719,6 +721,22 @@ public abstract class StringUtils {
return locale.getLanguage() + (hasText(locale.getCountry()) ? "-" + locale.getCountry() : "");
}
/**
* Parse the given {@code timeZoneString} value into a {@link TimeZone}.
* @param timeZoneString the time zone String, following {@link TimeZone#getTimeZone(String)}
* but throwing {@link IllegalArgumentException} in case of an invalid time zone specification
* @return a corresponding {@link TimeZone} instance
* @throws IllegalArgumentException in case of an invalid time zone specification
*/
public static TimeZone parseTimeZoneString(String timeZoneString) {
TimeZone timeZone = TimeZone.getTimeZone(timeZoneString);
if ("GMT".equals(timeZone.getID()) && !timeZoneString.startsWith("GMT")) {
// We don't want that GMT fallback...
throw new IllegalArgumentException("Invalid time zone specification '" + timeZoneString + "'");
}
return timeZone;
}
//---------------------------------------------------------------------
// Convenience methods for working with String arrays