Skip to content

Commit 4574528

Browse files
committed
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
1 parent 52cca48 commit 4574528

File tree

35 files changed

+1560
-260
lines changed

35 files changed

+1560
-260
lines changed

spring-beans/src/main/java/org/springframework/beans/propertyeditors/TimeZoneEditor.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
import java.beans.PropertyEditorSupport;
2020
import java.util.TimeZone;
2121

22+
import org.springframework.util.StringUtils;
23+
2224
/**
2325
* Editor for {@code java.util.TimeZone}, translating timezone IDs into
2426
* {@code TimeZone} objects. Exposes the {@code TimeZone} ID as a text
@@ -33,7 +35,7 @@ public class TimeZoneEditor extends PropertyEditorSupport {
3335

3436
@Override
3537
public void setAsText(String text) throws IllegalArgumentException {
36-
setValue(TimeZone.getTimeZone(text));
38+
setValue(StringUtils.parseTimeZoneString(text));
3739
}
3840

3941
@Override
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Copyright 2002-2013 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.beans.propertyeditors;
18+
19+
import java.time.ZoneId;
20+
21+
import junit.framework.TestCase;
22+
23+
/**
24+
* @author Nicholas Williams
25+
*/
26+
public class ZoneIdEditorTests extends TestCase {
27+
28+
public void testAmericaChicago() {
29+
ZoneIdEditor editor = new ZoneIdEditor();
30+
editor.setAsText("America/Chicago");
31+
32+
ZoneId zoneId = (ZoneId) editor.getValue();
33+
assertNotNull("The zone ID should not be null.", zoneId);
34+
assertEquals("The zone ID is not correct.", ZoneId.of("America/Chicago"), zoneId);
35+
36+
assertEquals("The text version is not correct.", "America/Chicago", editor.getAsText());
37+
}
38+
39+
public void testAmericaLosAngeles() {
40+
ZoneIdEditor editor = new ZoneIdEditor();
41+
editor.setAsText("America/Los_Angeles");
42+
43+
ZoneId zoneId = (ZoneId) editor.getValue();
44+
assertNotNull("The zone ID should not be null.", zoneId);
45+
assertEquals("The zone ID is not correct.", ZoneId.of("America/Los_Angeles"), zoneId);
46+
47+
assertEquals("The text version is not correct.", "America/Los_Angeles", editor.getAsText());
48+
}
49+
50+
public void testGetNullAsText() {
51+
ZoneIdEditor editor = new ZoneIdEditor();
52+
53+
assertEquals("The returned value is not correct.", "", editor.getAsText());
54+
}
55+
56+
public void testGetValueAsText() {
57+
ZoneIdEditor editor = new ZoneIdEditor();
58+
editor.setValue(ZoneId.of("America/New_York"));
59+
60+
assertEquals("The text version is not correct.", "America/New_York", editor.getAsText());
61+
}
62+
63+
}

spring-context/src/main/java/org/springframework/context/i18n/LocaleContext.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2005 the original author or authors.
2+
* Copyright 2002-2013 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -26,14 +26,15 @@
2626
*
2727
* @author Juergen Hoeller
2828
* @since 1.2
29-
* @see LocaleContextHolder
30-
* @see java.util.Locale
29+
* @see LocaleContextHolder#getLocale()
30+
* @see TimeZoneAwareLocaleContext
3131
*/
3232
public interface LocaleContext {
3333

3434
/**
3535
* Return the current Locale, which can be fixed or determined dynamically,
3636
* depending on the implementation strategy.
37+
* @return the current Locale, or {@code null} if no specific Locale associated
3738
*/
3839
Locale getLocale();
3940

spring-context/src/main/java/org/springframework/context/i18n/LocaleContextHolder.java

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2012 the original author or authors.
2+
* Copyright 2002-2013 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,6 +17,7 @@
1717
package org.springframework.context.i18n;
1818

1919
import java.util.Locale;
20+
import java.util.TimeZone;
2021

2122
import org.springframework.core.NamedInheritableThreadLocal;
2223
import org.springframework.core.NamedThreadLocal;
@@ -59,18 +60,26 @@ public static void resetLocaleContext() {
5960
/**
6061
* Associate the given LocaleContext with the current thread,
6162
* <i>not</i> exposing it as inheritable for child threads.
63+
* <p>The given LocaleContext may be a {@link TimeZoneAwareLocaleContext},
64+
* containing a locale with associated time zone information.
6265
* @param localeContext the current LocaleContext
66+
* @see SimpleLocaleContext
67+
* @see SimpleTimeZoneAwareLocaleContext
6368
*/
6469
public static void setLocaleContext(LocaleContext localeContext) {
6570
setLocaleContext(localeContext, false);
6671
}
6772

6873
/**
6974
* Associate the given LocaleContext with the current thread.
75+
* <p>The given LocaleContext may be a {@link TimeZoneAwareLocaleContext},
76+
* containing a locale with associated time zone information.
7077
* @param localeContext the current LocaleContext,
7178
* or {@code null} to reset the thread-bound context
7279
* @param inheritable whether to expose the LocaleContext as inheritable
7380
* for child threads (using an {@link InheritableThreadLocal})
81+
* @see SimpleLocaleContext
82+
* @see SimpleTimeZoneAwareLocaleContext
7483
*/
7584
public static void setLocaleContext(LocaleContext localeContext, boolean inheritable) {
7685
if (localeContext == null) {
@@ -128,15 +137,53 @@ public static void setLocale(Locale locale, boolean inheritable) {
128137

129138
/**
130139
* Return the Locale associated with the current thread, if any,
131-
* or the system default Locale else.
140+
* or the system default Locale else. This is effectively a
141+
* replacement for {@link java.util.Locale#getDefault()},
142+
* able to optionally respect a user-level Locale setting.
143+
* <p>Note: This method has a fallback to the system default Locale.
144+
* If you'd like to check for the raw LocaleContext content
145+
* (which may indicate no specific locale through {@code null}, use
146+
* {@link #getLocaleContext()} and call {@link LocaleContext#getLocale()}
132147
* @return the current Locale, or the system default Locale if no
133148
* specific Locale has been associated with the current thread
134149
* @see LocaleContext#getLocale()
135150
* @see java.util.Locale#getDefault()
136151
*/
137152
public static Locale getLocale() {
138153
LocaleContext localeContext = getLocaleContext();
139-
return (localeContext != null ? localeContext.getLocale() : Locale.getDefault());
154+
if (localeContext != null) {
155+
Locale locale = localeContext.getLocale();
156+
if (locale != null) {
157+
return locale;
158+
}
159+
}
160+
return Locale.getDefault();
161+
}
162+
163+
/**
164+
* Return the TimeZone associated with the current thread, if any,
165+
* or the system default TimeZone else. This is effectively a
166+
* replacement for {@link java.util.TimeZone#getDefault()},
167+
* able to optionally respect a user-level TimeZone setting.
168+
* <p>Note: This method has a fallback to the system default Locale.
169+
* If you'd like to check for the raw LocaleContext content
170+
* (which may indicate no specific time zone through {@code null}, use
171+
* {@link #getLocaleContext()} and call {@link TimeZoneAwareLocaleContext#getTimeZone()}
172+
* after downcasting to {@link TimeZoneAwareLocaleContext}.
173+
* @return the current TimeZone, or the system default TimeZone if no
174+
* specific TimeZone has been associated with the current thread
175+
* @see TimeZoneAwareLocaleContext#getTimeZone()
176+
* @see java.util.TimeZone#getDefault()
177+
*/
178+
public static TimeZone getTimeZone() {
179+
LocaleContext localeContext = getLocaleContext();
180+
if (localeContext instanceof TimeZoneAwareLocaleContext) {
181+
TimeZone timeZone = ((TimeZoneAwareLocaleContext) localeContext).getTimeZone();
182+
if (timeZone != null) {
183+
return timeZone;
184+
}
185+
}
186+
return TimeZone.getDefault();
140187
}
141188

142189
}

spring-context/src/main/java/org/springframework/context/i18n/SimpleLocaleContext.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2012 the original author or authors.
2+
* Copyright 2002-2013 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -18,14 +18,15 @@
1818

1919
import java.util.Locale;
2020

21-
import org.springframework.util.Assert;
22-
2321
/**
2422
* Simple implementation of the {@link LocaleContext} interface,
2523
* always returning a specified {@code Locale}.
2624
*
2725
* @author Juergen Hoeller
2826
* @since 1.2
27+
* @see LocaleContextHolder#setLocaleContext
28+
* @see LocaleContextHolder#getLocale()
29+
* @see SimpleTimeZoneAwareLocaleContext
2930
*/
3031
public class SimpleLocaleContext implements LocaleContext {
3132

@@ -34,11 +35,10 @@ public class SimpleLocaleContext implements LocaleContext {
3435

3536
/**
3637
* Create a new SimpleLocaleContext that exposes the specified Locale.
37-
* Every {@code getLocale()} will return this Locale.
38+
* Every {@link #getLocale()} call will return this Locale.
3839
* @param locale the Locale to expose
3940
*/
4041
public SimpleLocaleContext(Locale locale) {
41-
Assert.notNull(locale, "Locale must not be null");
4242
this.locale = locale;
4343
}
4444

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright 2002-2013 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.context.i18n;
18+
19+
import java.util.Locale;
20+
import java.util.TimeZone;
21+
22+
/**
23+
* Simple implementation of the {@link TimeZoneAwareLocaleContext} interface,
24+
* always returning a specified {@code Locale} and {@code TimeZone}.
25+
*
26+
* <p>Note: Prefer the use of {@link SimpleLocaleContext} when only setting
27+
* a Locale but no TimeZone.
28+
*
29+
* @author Juergen Hoeller
30+
* @since 4.0
31+
* @see LocaleContextHolder#setLocaleContext
32+
* @see LocaleContextHolder#getTimeZone()
33+
*/
34+
public class SimpleTimeZoneAwareLocaleContext extends SimpleLocaleContext implements TimeZoneAwareLocaleContext {
35+
36+
private final TimeZone timeZone;
37+
38+
39+
/**
40+
* Create a new SimpleTimeZoneAwareLocaleContext that exposes the specified
41+
* Locale and TimeZone. Every {@link #getLocale()} call will return the given
42+
* Locale, and every {@link #getTimeZone()} call will return the given TimeZone.
43+
* @param locale the Locale to expose
44+
* @param timeZone the TimeZone to expose
45+
*/
46+
public SimpleTimeZoneAwareLocaleContext(Locale locale, TimeZone timeZone) {
47+
super(locale);
48+
this.timeZone = timeZone;
49+
}
50+
51+
52+
public TimeZone getTimeZone() {
53+
return this.timeZone;
54+
}
55+
56+
@Override
57+
public String toString() {
58+
return super.toString() + " " + this.timeZone.toString();
59+
}
60+
61+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright 2002-2013 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.context.i18n;
18+
19+
import java.util.TimeZone;
20+
21+
/**
22+
* Extension of {@link LocaleContext}, adding awareness of the current time zone.
23+
*
24+
* <p>Having this variant of LocaleContext set to {@link LocaleContextHolder} means
25+
* that some TimeZone-aware infrastructure has been configured, even if it may not
26+
* be able to produce a non-null TimeZone at the moment.
27+
*
28+
* @author Juergen Hoeller
29+
* @since 4.0
30+
* @see LocaleContextHolder#getTimeZone()
31+
*/
32+
public interface TimeZoneAwareLocaleContext extends LocaleContext {
33+
34+
/**
35+
* Return the current TimeZone, which can be fixed or determined dynamically,
36+
* depending on the implementation strategy.
37+
* @return the current TimeZone, or {@code null} if no specific TimeZone associated
38+
*/
39+
TimeZone getTimeZone();
40+
41+
}

spring-context/src/main/java/org/springframework/format/datetime/joda/JodaTimeContext.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,16 @@
1616

1717
package org.springframework.format.datetime.joda;
1818

19+
import java.util.TimeZone;
20+
1921
import org.joda.time.Chronology;
2022
import org.joda.time.DateTimeZone;
2123
import org.joda.time.format.DateTimeFormatter;
2224

25+
import org.springframework.context.i18n.LocaleContext;
26+
import org.springframework.context.i18n.LocaleContextHolder;
27+
import org.springframework.context.i18n.TimeZoneAwareLocaleContext;
28+
2329
/**
2430
* A context that holds user-specific Joda-Time settings such as the user's
2531
* Chronology (calendar system) and time zone.
@@ -53,6 +59,11 @@ public Chronology getChronology() {
5359

5460
/**
5561
* Set the user's time zone.
62+
* <p>Alternatively, set a {@link TimeZoneAwareLocaleContext} on
63+
* {@link LocaleContextHolder}. This context class will fall back to
64+
* checking the locale context if no setting has been provided here.
65+
* @see org.springframework.context.i18n.LocaleContextHolder#getTimeZone()
66+
* @see org.springframework.context.i18n.LocaleContextHolder#setLocaleContext
5667
*/
5768
public void setTimeZone(DateTimeZone timeZone) {
5869
this.timeZone = timeZone;
@@ -80,6 +91,15 @@ public DateTimeFormatter getFormatter(DateTimeFormatter formatter) {
8091
if (this.timeZone != null) {
8192
formatter = formatter.withZone(this.timeZone);
8293
}
94+
else {
95+
LocaleContext localeContext = LocaleContextHolder.getLocaleContext();
96+
if (localeContext instanceof TimeZoneAwareLocaleContext) {
97+
TimeZone timeZone = ((TimeZoneAwareLocaleContext) localeContext).getTimeZone();
98+
if (timeZone != null) {
99+
formatter = formatter.withZone(DateTimeZone.forTimeZone(timeZone));
100+
}
101+
}
102+
}
83103
return formatter;
84104
}
85105

0 commit comments

Comments
 (0)