View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.lang3.time;
18  
19  import static org.junit.jupiter.api.Assertions.assertEquals;
20  import static org.junit.jupiter.api.Assertions.assertNotEquals;
21  import static org.junit.jupiter.api.Assertions.assertNotNull;
22  import static org.junit.jupiter.api.Assertions.fail;
23  import static org.junit.jupiter.api.Assumptions.assumeFalse;
24  import static org.junit.jupiter.api.Assumptions.assumeTrue;
25  
26  import java.text.DateFormatSymbols;
27  import java.text.ParseException;
28  import java.time.ZoneId;
29  import java.util.ArrayList;
30  import java.util.Comparator;
31  import java.util.Date;
32  import java.util.List;
33  import java.util.Locale;
34  import java.util.Map;
35  import java.util.Map.Entry;
36  import java.util.Objects;
37  import java.util.Set;
38  import java.util.TimeZone;
39  import java.util.concurrent.atomic.AtomicInteger;
40  
41  import org.apache.commons.lang3.AbstractLangTest;
42  import org.apache.commons.lang3.ArraySorter;
43  import org.apache.commons.lang3.JavaVersion;
44  import org.apache.commons.lang3.LocaleUtils;
45  import org.apache.commons.lang3.SystemUtils;
46  import org.junit.jupiter.api.AfterAll;
47  import org.junit.jupiter.api.Disabled;
48  import org.junit.jupiter.api.Test;
49  import org.junit.jupiter.params.ParameterizedTest;
50  import org.junit.jupiter.params.provider.MethodSource;
51  import org.junit.jupiter.params.provider.ValueSource;
52  import org.junitpioneer.jupiter.DefaultLocale;
53  import org.junitpioneer.jupiter.DefaultTimeZone;
54  import org.junitpioneer.jupiter.ReadsDefaultLocale;
55  import org.junitpioneer.jupiter.ReadsDefaultTimeZone;
56  
57  /**
58   * Tests {@link FastDateParser}.
59   */
60  /* Make test reproducible */ @DefaultLocale(language = "en")
61  /* Make test reproducible */ @DefaultTimeZone(TimeZones.GMT_ID)
62  /* Make test reproducible */ @ReadsDefaultLocale
63  /* Make test reproducible */ @ReadsDefaultTimeZone
64  class FastDateParser_TimeZoneStrategyTest extends AbstractLangTest {
65  
66      private static final List<Locale> Java11Failures = new ArrayList<>();
67      private static final List<Locale> Java17Failures = new ArrayList<>();
68      private static final AtomicInteger fails = new AtomicInteger();
69  
70      @AfterAll
71      public static void afterAll() {
72          if (!Java17Failures.isEmpty()) {
73              System.err.printf("Actual failures on Java 17+: %,d%n%s%n", Java17Failures.size(), Java17Failures);
74          }
75          if (!Java11Failures.isEmpty()) {
76              System.err.printf("Actual failures on Java 11: %,d%n%s%n", Java11Failures.size(), Java11Failures);
77          }
78      }
79  
80      static Set<Entry<String, String>> getZoneIdStream() {
81          return ZoneId.SHORT_IDS.entrySet();
82      }
83  
84      private String[][] getZoneStringsSorted(final Locale locale) {
85          return ArraySorter.sort(DateFormatSymbols.getInstance(locale).getZoneStrings(), Comparator.comparing(array -> array[0]));
86      }
87  
88      /**
89       * Tests that known short {@link ZoneId}s still parse since all short IDs are deprecated starting in Java 25, but are not removed.
90       *
91       * TODO: Why don't all short IDs parse, even on Java 8?
92       *
93       * @throws ParseException Thrown on test failure.
94       */
95      @ParameterizedTest
96      @ValueSource(strings = { "ACT", "CST" })
97      void testJava25DeprecatedZoneId(final String shortId) throws ParseException {
98          final FastDateParser parser = new FastDateParser("dd.MM.yyyy HH:mm:ss z", TimeZone.getTimeZone(shortId), Locale.getDefault());
99          final Date date1 = parser.parse("26.10.2014 02:00:00 " + shortId);
100         // 1) parsing returns a value and doesn't throw.
101         assertNotNull(date1);
102         // 2) Something reasonable, note that getYear() subtracts 1900.
103         assertEquals(2014, date1.getYear() + 1900);
104     }
105 
106     /**
107      * Tests that {@link ZoneId#SHORT_IDS} keys and values still works as they are deprecated starting in Java 25, but not removed yet.
108      *
109      * TODO: Why don't all short IDs parse, even on Java 8?
110      *
111      * @throws ParseException Thrown on test failure.
112      */
113     @Disabled
114     @ParameterizedTest
115     @MethodSource("getZoneIdStream")
116     void testJava25DeprecatedZoneIds(final Map.Entry<String, String> entry) throws ParseException {
117         final FastDateParser parser = new FastDateParser("dd.MM.yyyy HH:mm:ss z", TimeZone.getDefault(), Locale.GERMAN);
118         final Date date1 = parser.parse("26.10.2014 02:00:00 " + entry.getKey());
119         final Date date2 = parser.parse("26.10.2014 02:00:00 " + entry.getValue());
120         assertNotEquals(date1.getTime(), date2.getTime());
121     }
122 
123     @Test
124     void testLang1219() throws ParseException {
125         final FastDateParser parser = new FastDateParser("dd.MM.yyyy HH:mm:ss z", TimeZone.getDefault(), Locale.GERMAN);
126         final Date summer = parser.parse("26.10.2014 02:00:00 MESZ");
127         final Date standard = parser.parse("26.10.2014 02:00:00 MEZ");
128         assertNotEquals(summer.getTime(), standard.getTime());
129     }
130 
131     @ParameterizedTest
132     @MethodSource("org.apache.commons.lang3.LocaleUtils#availableLocaleList()")
133     void testTimeZoneStrategy_DateFormatSymbols(final Locale locale) {
134         testTimeZoneStrategyPattern_DateFormatSymbols_getZoneStrings(locale);
135     }
136     @ParameterizedTest
137     @MethodSource("org.apache.commons.lang3.LocaleUtils#availableLocaleList()")
138     void testTimeZoneStrategy_TimeZone(final Locale locale) {
139         testTimeZoneStrategyPattern_TimeZone_getAvailableIDs(locale);
140     }
141 
142     private void testTimeZoneStrategyPattern(final String languageTag, final String source) throws ParseException {
143         final Locale locale = Locale.forLanguageTag(languageTag);
144         final TimeZone timeZone = TimeZones.getTimeZone("Etc/UTC");
145         assumeFalse(LocaleUtils.isLanguageUndetermined(locale), () -> toFailureMessage(locale, languageTag, timeZone));
146         assumeTrue(LocaleUtils.isAvailableLocale(locale), () -> toFailureMessage(locale, languageTag, timeZone));
147         final FastDateParser parser = new FastDateParser("z", timeZone, locale);
148         parser.parse(source);
149         testTimeZoneStrategyPattern_TimeZone_getAvailableIDs(locale);
150     }
151 
152     private void testTimeZoneStrategyPattern_DateFormatSymbols_getZoneStrings(final Locale locale) {
153         Objects.requireNonNull(locale, "locale");
154         assumeFalse(LocaleUtils.isLanguageUndetermined(locale), () -> toFailureMessage(locale, null, null));
155         assumeTrue(LocaleUtils.isAvailableLocale(locale), () -> toFailureMessage(locale, null, null));
156 
157         final String[][] zones = getZoneStringsSorted(locale);
158         for (final String[] zone : zones) {
159             for (int zIndex = 1; zIndex < zone.length; ++zIndex) {
160                 final String tzDisplay = zone[zIndex];
161                 if (tzDisplay == null) {
162                     break;
163                 }
164                 final TimeZone timeZone = TimeZone.getDefault();
165                 final FastDateParser parser = new FastDateParser("z", timeZone, locale);
166                 // An exception will be thrown and the test will fail if parsing isn't successful
167                 try {
168                     parser.parse(tzDisplay);
169                 } catch (final ParseException e) {
170                     // Hack Start
171                     // See failures on GitHub Actions builds for Java 17.
172                     final String localeStr = locale.toString();
173                     if (SystemUtils.isJavaVersionAtLeast(JavaVersion.JAVA_17)
174                             && (localeStr.contains("_") || "Coordinated Universal Time".equals(tzDisplay)
175                                     || "sommartid – Atyrau".equals(tzDisplay))) {
176                         Java17Failures.add(locale);
177                         // Mark as an assumption failure instead of a hard fail
178                         System.err.printf(
179                                 "[%,d][%s] Java %s %s - Mark as an assumption failure instead of a hard fail: locale = '%s', parse = '%s'%n",
180                                 fails.incrementAndGet(),
181                                 Thread.currentThread().getName(),
182                                 SystemUtils.JAVA_VENDOR,
183                                 SystemUtils.JAVA_VM_VERSION,
184                                 localeStr, tzDisplay);
185                         assumeTrue(false, localeStr);
186                         continue;
187                     }
188                     if (SystemUtils.IS_JAVA_11
189                             && (localeStr.contains("_") || "Coordinated Universal Time".equals(tzDisplay))) {
190                         Java11Failures.add(locale);
191                         // Mark as an assumption failure instead of a hard fail
192                         System.err.printf(
193                                 "[%,d][%s] Java %s %s - Mark as an assumption failure instead of a hard fail: locale = '%s', parse = '%s'%n",
194                                 fails.incrementAndGet(),
195                                 Thread.currentThread().getName(),
196                                 SystemUtils.JAVA_VENDOR,
197                                 SystemUtils.JAVA_VM_VERSION,
198                                 localeStr, tzDisplay);
199                         assumeTrue(false, localeStr);
200                         continue;
201                     }
202                     // Hack End
203                     fail(String.format("%s: with locale = %s, zIndex = %,d, tzDisplay = '%s', parser = '%s'", e,
204                             localeStr, zIndex, tzDisplay, parser), e);
205                 }
206             }
207         }
208     }
209 
210     /**
211      * Breaks randomly on GitHub for Locale "pt_PT", TimeZone "Etc/UTC" if we do not check if the Locale's language is "undetermined".
212      *
213      * @throws ParseException
214      */
215     private void testTimeZoneStrategyPattern_TimeZone_getAvailableIDs(final Locale locale) {
216         Objects.requireNonNull(locale, "locale");
217         assumeFalse(LocaleUtils.isLanguageUndetermined(locale), () -> toFailureMessage(locale, null, null));
218         assumeTrue(LocaleUtils.isAvailableLocale(locale), () -> toFailureMessage(locale, null, null));
219         for (final String id : ArraySorter.sort(TimeZone.getAvailableIDs())) {
220             final TimeZone timeZone = TimeZones.getTimeZone(id);
221             final String displayName = timeZone.getDisplayName(locale);
222             final FastDateParser parser = new FastDateParser("z", timeZone, locale);
223             try {
224                 parser.parse(displayName);
225             } catch (final ParseException e) {
226                 // Missing "Zulu" or something else in broken JDK's GH builds?
227                 // Call LocaleUtils again
228                 fail(String.format("%s: with id = '%s', displayName = '%s', %s, parser = '%s'", e, id, displayName,
229                         toFailureMessage(locale, null, timeZone), parser.toStringAll()), e);
230             }
231         }
232     }
233 
234     @Test
235     void testTimeZoneStrategyPattern_zh_HK_Hans() throws ParseException {
236         testTimeZoneStrategyPattern("zh_HK_#Hans", "?????????");
237     }
238 
239     /**
240      * Breaks randomly on GitHub for Locale "pt_PT", TimeZone "Etc/UTC" if we do not check if the Locale's language is "undetermined".
241      *
242      * <pre>{@code
243      * java.text.ParseException: Unparseable date: Horário do Meridiano de Greenwich: with tzDefault =
244      * sun.util.calendar.ZoneInfo[id="Etc/UTC",offset=0,dstSavings=0,useDaylight=false,transitions=0,lastRule=null], locale = pt_LU, zones[][] size = '601',
245      * zone[] size = '7', zIndex = 3, tzDisplay = 'Horário do Meridiano de Greenwich'
246      * }</pre>
247      *
248      * @throws ParseException Test failure
249      */
250     @Test
251     void testTimeZoneStrategyPatternPortugal() throws ParseException {
252         testTimeZoneStrategyPattern("pt_PT", "Horário do Meridiano de Greenwich");
253     }
254 
255     /**
256      * Breaks randomly on GitHub for Locale "sr_ME_#Cyrl", TimeZone "Etc/UTC" if we do not check if the Locale's language is "undetermined".
257      *
258      * <pre>{@code
259      * java.text.ParseException: Unparseable date: Srednje vreme po Griniču: with tzDefault = sun.util.calendar.ZoneInfo[id="Etc/UTC",
260      * offset=0,dstSavings=0,useDaylight=false,transitions=0,lastRule=null], locale = sr_ME_#Cyrl, zones[][] size = '601',
261      * zone[] size = '7', zIndex = 3, tzDisplay = 'Srednje vreme po Griniču'
262      * }</pre>
263      *
264      * @throws ParseException Test failure
265      */
266     @Test
267     void testTimeZoneStrategyPatternSuriname() throws ParseException {
268         testTimeZoneStrategyPattern("sr_ME_#Cyrl", "Srednje vreme po Griniču");
269     }
270 
271     private String toFailureMessage(final Locale locale, final String languageTag, final TimeZone timeZone) {
272         return String.format("locale = %s, languageTag = '%s', isAvailableLocale = %s, isLanguageUndetermined = %s, timeZone = %s", languageTag, locale,
273                 LocaleUtils.isAvailableLocale(locale), LocaleUtils.isLanguageUndetermined(locale), TimeZones.toTimeZone(timeZone));
274     }
275 }