1 /* 2 * Copyright (c) 2014, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package javafx.util.converter; 27 28 import java.time.DateTimeException; 29 import java.time.LocalDate; 30 import java.time.LocalTime; 31 import java.time.LocalDateTime; 32 import java.time.chrono.Chronology; 33 import java.time.chrono.ChronoLocalDate; 34 import java.time.chrono.ChronoLocalDateTime; 35 import java.time.chrono.IsoChronology; 36 import java.time.format.DateTimeFormatter; 37 import java.time.format.DateTimeFormatterBuilder; 38 import java.time.format.DecimalStyle; 39 import java.time.format.FormatStyle; 40 import java.time.temporal.Temporal; 41 import java.time.temporal.TemporalAccessor; 42 import java.util.Locale; 43 44 import javafx.util.StringConverter; 45 46 import com.sun.javafx.binding.Logging; 47 48 /** 49 * <p>{@link StringConverter} implementation for {@link LocalDateTime} values.</p> 50 * 51 * @see LocalDateStringConverter 52 * @see LocalTimeStringConverter 53 * @since JavaFX 8u40 54 */ 55 public class LocalDateTimeStringConverter extends StringConverter<LocalDateTime> { 56 57 LdtConverter<LocalDateTime> ldtConverter; 58 59 60 61 // ------------------------------------------------------------ Constructors 62 63 /** 64 * Create a {@link StringConverter} for {@link LocalDateTime} values, using a 65 * default formatter and parser based on {@link IsoChronology}, 66 * {@link FormatStyle#SHORT} for both date and time, and the user's 67 * {@link Locale}. 68 * 69 * <p>This converter ensures symmetry between the toString() and 70 * fromString() methods. Many of the default locale based patterns used by 71 * {@link DateTimeFormatter} will display only two digits for the year when 72 * formatting to a string. This would cause a value like 1955 to be 73 * displayed as 55, which in turn would be parsed back as 2055. This 74 * converter modifies two-digit year patterns to always use four digits. The 75 * input parsing is not affected, so two digit year values can still be 76 * parsed as expected in these locales.</p> 77 */ 78 public LocalDateTimeStringConverter() { 79 ldtConverter = new LdtConverter<LocalDateTime>(LocalDateTime.class, null, null, 80 null, null, null, null); 81 } 82 83 /** 84 * Create a {@link StringConverter} for {@link LocalDateTime} values, using 85 * a default formatter and parser based on {@link IsoChronology}, the 86 * specified {@link FormatStyle} values for date and time, and the user's 87 * {@link Locale}. 88 * 89 * @param dateStyle The {@link FormatStyle} that will be used by the default 90 * formatter and parser for the date. If null then {@link FormatStyle#SHORT} 91 * will be used. 92 * @param timeStyle The {@link FormatStyle} that will be used by the default 93 * formatter and parser for the time. If null then {@link FormatStyle#SHORT} 94 * will be used. 95 */ 96 public LocalDateTimeStringConverter(FormatStyle dateStyle, FormatStyle timeStyle) { 97 ldtConverter = new LdtConverter<LocalDateTime>(LocalDateTime.class, null, null, 98 dateStyle, timeStyle, null, null); 99 } 100 101 /** 102 * Create a {@link StringConverter} for {@link LocalDateTime} values using 103 * the supplied formatter and parser. 104 * 105 * <p>For example, to use a fixed pattern for converting both ways:</p> 106 * <blockquote><pre> 107 * String pattern = "yyyy-MM-dd HH:mm"; 108 * DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern); 109 * StringConverter<LocalDateTime> converter = 110 * DateTimeStringConverter.getLocalDateTimeConverter(formatter, null); 111 * </pre></blockquote> 112 * 113 * Note that the formatter and parser can be created to handle non-default 114 * {@link Locale} and {@link Chronology} as needed. 115 * 116 * @param formatter An instance of {@link DateTimeFormatter} which will be 117 * used for formatting by the toString() method. If null then a default 118 * formatter will be used. 119 * @param parser An instance of {@link DateTimeFormatter} which will be used 120 * for parsing by the fromString() method. This can be identical to 121 * formatter. If null then formatter will be used, and if that is also null, 122 * then a default parser will be used. 123 */ 124 public LocalDateTimeStringConverter(DateTimeFormatter formatter, DateTimeFormatter parser) { 125 ldtConverter = new LdtConverter<LocalDateTime>(LocalDateTime.class, formatter, parser, 126 null, null, null, null); 127 } 128 129 /** 130 * Create a {@link StringConverter} for {@link LocalDateTime} values using a 131 * default formatter and parser, which will be based on the supplied 132 * {@link FormatStyle}s, {@link Locale}, and {@link Chronology}. 133 * 134 * @param dateStyle The {@link FormatStyle} that will be used by the default 135 * formatter and parser for the date. If null then {@link FormatStyle#SHORT} 136 * will be used. 137 * @param timeStyle The {@link FormatStyle} that will be used by the default 138 * formatter and parser for the time. If null then {@link FormatStyle#SHORT} 139 * will be used. 140 * @param locale The {@link Locale} that will be used by the 141 * default formatter and parser. If null then 142 * {@code Locale.getDefault(Locale.Category.FORMAT)} will be used. 143 * @param chronology The {@link Chronology} that will be used by the default 144 * formatter and parser. If null then {@link IsoChronology#INSTANCE} will be 145 * used. 146 */ 147 public LocalDateTimeStringConverter(FormatStyle dateStyle, FormatStyle timeStyle, 148 Locale locale, Chronology chronology) { 149 ldtConverter = new LdtConverter<LocalDateTime>(LocalDateTime.class, null, null, 150 dateStyle, timeStyle, locale, chronology); 151 } 152 153 154 155 // ------------------------------------------------------- Converter Methods 156 157 /** {@inheritDoc} */ 158 @Override public LocalDateTime fromString(String value) { 159 return ldtConverter.fromString(value); 160 } 161 162 /** {@inheritDoc} */ 163 @Override public String toString(LocalDateTime value) { 164 return ldtConverter.toString(value); 165 } 166 167 168 169 static class LdtConverter<T extends Temporal> extends StringConverter<T> { 170 private Class<T> type; 171 Locale locale; 172 Chronology chronology; 173 DateTimeFormatter formatter; 174 DateTimeFormatter parser; 175 FormatStyle dateStyle; 176 FormatStyle timeStyle; 177 178 LdtConverter(Class<T> type, DateTimeFormatter formatter, DateTimeFormatter parser, 179 FormatStyle dateStyle, FormatStyle timeStyle, Locale locale, Chronology chronology) { 180 this.type = type; 181 this.formatter = formatter; 182 this.parser = (parser != null) ? parser : formatter; 183 this.locale = (locale != null) ? locale : Locale.getDefault(Locale.Category.FORMAT); 184 this.chronology = (chronology != null) ? chronology : IsoChronology.INSTANCE; 185 186 if (type == LocalDate.class || type == LocalDateTime.class) { 187 this.dateStyle = (dateStyle != null) ? dateStyle : FormatStyle.SHORT; 188 } 189 190 if (type == LocalTime.class || type == LocalDateTime.class) { 191 this.timeStyle = (timeStyle != null) ? timeStyle : FormatStyle.SHORT; 192 } 193 } 194 195 /** {@inheritDoc} */ 196 @SuppressWarnings({"unchecked"}) 197 @Override public T fromString(String text) { 198 if (text == null || text.isEmpty()) { 199 return null; 200 } 201 202 text = text.trim(); 203 204 if (parser == null) { 205 parser = getDefaultParser(); 206 } 207 208 TemporalAccessor temporal = parser.parse(text); 209 210 if (type == LocalDate.class) { 211 return (T)LocalDate.from(chronology.date(temporal)); 212 } else if (type == LocalTime.class) { 213 return (T)LocalTime.from(temporal); 214 } else { 215 return (T)LocalDateTime.from(chronology.localDateTime(temporal)); 216 } 217 } 218 219 220 /** {@inheritDoc} */ 221 @Override public String toString(T value) { 222 // If the specified value is null, return a zero-length String 223 if (value == null) { 224 return ""; 225 } 226 227 if (formatter == null) { 228 formatter = getDefaultFormatter(); 229 } 230 231 if (value instanceof LocalDate) { 232 ChronoLocalDate cDate; 233 try { 234 cDate = chronology.date(value); 235 } catch (DateTimeException ex) { 236 Logging.getLogger().warning("Converting LocalDate " + value + " to " + chronology + " failed, falling back to IsoChronology.", ex); 237 chronology = IsoChronology.INSTANCE; 238 cDate = (LocalDate)value; 239 } 240 return formatter.format(cDate); 241 } else if (value instanceof LocalDateTime) { 242 ChronoLocalDateTime<? extends ChronoLocalDate> cDateTime; 243 try { 244 cDateTime = chronology.localDateTime(value); 245 } catch (DateTimeException ex) { 246 Logging.getLogger().warning("Converting LocalDateTime " + value + " to " + chronology + " failed, falling back to IsoChronology.", ex); 247 chronology = IsoChronology.INSTANCE; 248 cDateTime = (LocalDateTime)value; 249 } 250 return formatter.format(cDateTime); 251 } else { 252 return formatter.format(value); 253 } 254 } 255 256 257 private DateTimeFormatter getDefaultParser() { 258 String pattern = 259 DateTimeFormatterBuilder.getLocalizedDateTimePattern(dateStyle, timeStyle, 260 chronology, locale); 261 return new DateTimeFormatterBuilder().parseLenient() 262 .appendPattern(pattern) 263 .toFormatter() 264 .withChronology(chronology) 265 .withDecimalStyle(DecimalStyle.of(locale)); 266 } 267 268 /** 269 * <p>Return a default <code>DateTimeFormatter</code> instance to use for formatting 270 * and parsing in this {@link StringConverter}.</p> 271 */ 272 private DateTimeFormatter getDefaultFormatter() { 273 DateTimeFormatter formatter; 274 275 if (dateStyle != null && timeStyle != null) { 276 formatter = DateTimeFormatter.ofLocalizedDateTime(dateStyle, timeStyle); 277 } else if (dateStyle != null) { 278 formatter = DateTimeFormatter.ofLocalizedDate(dateStyle); 279 } else { 280 formatter = DateTimeFormatter.ofLocalizedTime(timeStyle); 281 } 282 283 formatter = formatter.withLocale(locale) 284 .withChronology(chronology) 285 .withDecimalStyle(DecimalStyle.of(locale)); 286 287 if (dateStyle != null) { 288 formatter = fixFourDigitYear(formatter, dateStyle, timeStyle, 289 chronology, locale); 290 } 291 292 return formatter; 293 } 294 295 private DateTimeFormatter fixFourDigitYear(DateTimeFormatter formatter, 296 FormatStyle dateStyle, FormatStyle timeStyle, 297 Chronology chronology, Locale locale) { 298 String pattern = 299 DateTimeFormatterBuilder.getLocalizedDateTimePattern(dateStyle, timeStyle, 300 chronology, locale); 301 if (pattern.contains("yy") && !pattern.contains("yyy")) { 302 // Modify pattern to show four-digit year, including leading zeros. 303 String newPattern = pattern.replace("yy", "yyyy"); 304 formatter = DateTimeFormatter.ofPattern(newPattern) 305 .withDecimalStyle(DecimalStyle.of(locale)); 306 } 307 308 return formatter; 309 } 310 } 311 }