001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.swaps; 018 019import static org.apache.juneau.commons.reflect.ReflectionUtils.*; 020import static org.apache.juneau.commons.utils.DateUtils.*; 021 022import java.lang.reflect.*; 023import java.time.*; 024import java.time.chrono.*; 025import java.time.format.*; 026import java.time.temporal.*; 027import java.util.*; 028import java.util.concurrent.*; 029 030import org.apache.juneau.*; 031import org.apache.juneau.commons.reflect.*; 032import org.apache.juneau.swap.*; 033 034/** 035 * Swap that converts {@link Temporal} objects to strings. 036 * 037 * <p> 038 * Uses the {@link DateTimeFormatter} class for converting {@link Temporal} objects to and from strings. 039 * 040 * <p> 041 * Supports any of the following temporal objects: 042 * <ul class='javatree'> 043 * <li class='jc'>{@link HijrahDate} 044 * <li class='jc'>{@link Instant} 045 * <li class='jc'>{@link JapaneseDate} 046 * <li class='jc'>{@link LocalDate} 047 * <li class='jc'>{@link LocalDateTime} 048 * <li class='jc'>{@link LocalTime} 049 * <li class='jc'>{@link MinguoDate} 050 * <li class='jc'>{@link OffsetDateTime} 051 * <li class='jc'>{@link OffsetTime} 052 * <li class='jc'>{@link ThaiBuddhistDate} 053 * <li class='jc'>{@link Year} 054 * <li class='jc'>{@link YearMonth} 055 * <li class='jc'>{@link ZonedDateTime} 056 * </ul> 057 * 058 * <h5 class='section'>See Also:</h5><ul> 059 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/SwapBasics">Swap Basics</a> 060 * </ul> 061 */ 062public class TemporalSwap extends StringSwap<Temporal> { 063 064 /** 065 * Default swap to {@link DateTimeFormatter#BASIC_ISO_DATE}. 066 * <p> 067 * Example: <js>"20111203"</js> 068 */ 069 public static class BasicIsoDate extends TemporalSwap { 070 071 /** Default instance.*/ 072 public static final TemporalSwap DEFAULT = new BasicIsoDate(); 073 074 /** Constructor.*/ 075 public BasicIsoDate() { 076 super("BASIC_ISO_DATE", true); 077 } 078 } 079 080 /** 081 * Default swap to {@link DateTimeFormatter#ISO_DATE}. 082 * <p> 083 * Example: <js>"2011-12-03+01:00"</js> or <js>"2011-12-03"</js> 084 */ 085 public static class IsoDate extends TemporalSwap { 086 087 /** Default instance.*/ 088 public static final TemporalSwap DEFAULT = new IsoDate(); 089 090 /** Constructor.*/ 091 public IsoDate() { 092 super("ISO_DATE", true); 093 } 094 } 095 096 /** 097 * Default swap to {@link DateTimeFormatter#ISO_DATE_TIME}. 098 * <p> 099 * Example: <js>"2011-12-03T10:15:30+01:00[Europe/Paris]"</js> 100 */ 101 public static class IsoDateTime extends TemporalSwap { 102 103 /** Default instance.*/ 104 public static final TemporalSwap DEFAULT = new IsoDateTime(); 105 106 /** Constructor.*/ 107 public IsoDateTime() { 108 super("ISO_DATE_TIME", true); 109 } 110 } 111 112 /** 113 * Default swap to {@link DateTimeFormatter#ISO_INSTANT}. 114 * <p> 115 * Example: <js>"2011-12-03T10:15:30Z"</js> 116 */ 117 public static class IsoInstant extends TemporalSwap { 118 119 /** Default instance.*/ 120 public static final TemporalSwap DEFAULT = new IsoInstant(); 121 122 /** Constructor.*/ 123 public IsoInstant() { 124 super("ISO_INSTANT", false); 125 } 126 } 127 128 /** 129 * Default swap to {@link DateTimeFormatter#ISO_LOCAL_DATE}. 130 * <p> 131 * Example: <js>"2011-12-03"</js> 132 */ 133 public static class IsoLocalDate extends TemporalSwap { 134 135 /** Default instance.*/ 136 public static final TemporalSwap DEFAULT = new IsoLocalDate(); 137 138 /** Constructor.*/ 139 public IsoLocalDate() { 140 super("ISO_LOCAL_DATE", false); 141 } 142 } 143 144 /** 145 * Default swap to {@link DateTimeFormatter#ISO_LOCAL_DATE_TIME}. 146 * <p> 147 * Example: <js>"2011-12-03T10:15:30"</js> 148 */ 149 public static class IsoLocalDateTime extends TemporalSwap { 150 151 /** Default instance.*/ 152 public static final TemporalSwap DEFAULT = new IsoLocalDateTime(); 153 154 /** Constructor.*/ 155 public IsoLocalDateTime() { 156 super("ISO_LOCAL_DATE_TIME", true); 157 } 158 } 159 160 /** 161 * Default swap to {@link DateTimeFormatter#ISO_LOCAL_TIME}. 162 * <p> 163 * Example: <js>"10:15:30"</js> 164 */ 165 public static class IsoLocalTime extends TemporalSwap { 166 167 /** Default instance.*/ 168 public static final TemporalSwap DEFAULT = new IsoLocalTime(); 169 170 /** Constructor.*/ 171 public IsoLocalTime() { 172 super("ISO_LOCAL_TIME", true); 173 } 174 } 175 176 /** 177 * Default swap to {@link DateTimeFormatter#ISO_OFFSET_DATE}. 178 * <p> 179 * Example: <js>"2011-12-03"</js> 180 */ 181 public static class IsoOffsetDate extends TemporalSwap { 182 183 /** Default instance.*/ 184 public static final TemporalSwap DEFAULT = new IsoOffsetDate(); 185 186 /** Constructor.*/ 187 public IsoOffsetDate() { 188 super("ISO_OFFSET_DATE", false); 189 } 190 } 191 192 /** 193 * Default swap to {@link DateTimeFormatter#ISO_OFFSET_DATE_TIME}. 194 * <p> 195 * Example: <js>"2011-12-03T10:15:30+01:00"</js> 196 */ 197 public static class IsoOffsetDateTime extends TemporalSwap { 198 199 /** Default instance.*/ 200 public static final TemporalSwap DEFAULT = new IsoOffsetDateTime(); 201 202 /** Constructor.*/ 203 public IsoOffsetDateTime() { 204 super("ISO_OFFSET_DATE_TIME", false); 205 } 206 } 207 208 /** 209 * Default swap to {@link DateTimeFormatter#ISO_OFFSET_TIME}. 210 * <p> 211 * Example: <js>"10:15:30+01:00"</js> 212 */ 213 public static class IsoOffsetTime extends TemporalSwap { 214 215 /** Default instance.*/ 216 public static final TemporalSwap DEFAULT = new IsoOffsetTime(); 217 218 /** Constructor.*/ 219 public IsoOffsetTime() { 220 super("ISO_OFFSET_TIME", false); 221 } 222 } 223 224 /** 225 * Default swap to {@link DateTimeFormatter#ISO_ORDINAL_DATE}. 226 * <p> 227 * Example: <js>"2012-337"</js> 228 */ 229 public static class IsoOrdinalDate extends TemporalSwap { 230 231 /** Default instance.*/ 232 public static final TemporalSwap DEFAULT = new IsoOrdinalDate(); 233 234 /** Constructor.*/ 235 public IsoOrdinalDate() { 236 super("ISO_ORDINAL_DATE", true); 237 } 238 } 239 240 /** 241 * Default swap to {@link DateTimeFormatter#ISO_TIME}. 242 * <p> 243 * Example: <js>"10:15:30+01:00"</js> or <js>"10:15:30"</js> 244 */ 245 public static class IsoTime extends TemporalSwap { 246 247 /** Default instance.*/ 248 public static final TemporalSwap DEFAULT = new IsoTime(); 249 250 /** Constructor.*/ 251 public IsoTime() { 252 super("ISO_TIME", true); 253 } 254 } 255 256 /** 257 * Default swap to {@link DateTimeFormatter#ISO_WEEK_DATE}. 258 * <p> 259 * Example: <js>"2012-W48-6"</js> 260 */ 261 public static class IsoWeekDate extends TemporalSwap { 262 263 /** Default instance.*/ 264 public static final TemporalSwap DEFAULT = new IsoWeekDate(); 265 266 /** Constructor.*/ 267 public IsoWeekDate() { 268 super("ISO_WEEK_DATE", true); 269 } 270 } 271 272 /** 273 * Default swap to {@link DateTimeFormatter#ISO_WEEK_DATE}. 274 * <p> 275 * Example: <js>"2011"</js> 276 */ 277 public static class IsoYear extends TemporalSwap { 278 279 /** Default instance.*/ 280 public static final TemporalSwap DEFAULT = new IsoYear(); 281 282 /** Constructor.*/ 283 public IsoYear() { 284 super("uuuu", true); 285 } 286 } 287 288 /** 289 * Default swap to {@link DateTimeFormatter#ISO_WEEK_DATE}. 290 * <p> 291 * Example: <js>"2011-12"</js> 292 */ 293 public static class IsoYearMonth extends TemporalSwap { 294 295 /** Default instance.*/ 296 public static final TemporalSwap DEFAULT = new IsoYearMonth(); 297 298 /** Constructor.*/ 299 public IsoYearMonth() { 300 super("uuuu-MM", true); 301 } 302 } 303 304 /** 305 * Default swap to {@link DateTimeFormatter#ISO_ZONED_DATE_TIME}. 306 * <p> 307 * Example: <js>"2011-12-03T10:15:30+01:00[Europe/Paris]"</js> 308 */ 309 public static class IsoZonedDateTime extends TemporalSwap { 310 311 /** Default instance.*/ 312 public static final TemporalSwap DEFAULT = new IsoZonedDateTime(); 313 314 /** Constructor.*/ 315 public IsoZonedDateTime() { 316 super("ISO_ZONED_DATE_TIME", false); 317 } 318 } 319 320 /** 321 * Default swap to {@link DateTimeFormatter#RFC_1123_DATE_TIME}. 322 * <p> 323 * Example: <js>"Tue, 3 Jun 2008 11:05:30 GMT"</js> 324 */ 325 public static class Rfc1123DateTime extends TemporalSwap { 326 327 /** Default instance.*/ 328 public static final TemporalSwap DEFAULT = new Rfc1123DateTime(); 329 330 /** Constructor.*/ 331 public Rfc1123DateTime() { 332 super("RFC_1123_DATE_TIME", false); 333 } 334 } 335 336 private static final ZoneId Z = ZoneId.of("Z"); 337 private static final Map<Class<? extends Temporal>,Method> FROM_METHODS = new ConcurrentHashMap<>(); 338 339 private static Method findParseMethod(Class<? extends Temporal> c) throws ExecutableException { 340 var m = FROM_METHODS.get(c); 341 if (m == null) { 342 // @formatter:off 343 m = info(c).getPublicMethod( 344 x -> x.isStatic() 345 && x.isNotDeprecated() 346 && x.hasName("from") 347 && x.hasReturnType(c) 348 && x.hasParameterTypes(TemporalAccessor.class) 349 ) 350 .map(MethodInfo::inner) 351 .orElseThrow(() -> new ExecutableException("Parse method not found on temporal class ''{0}''", c.getSimpleName())); 352 // @formatter:on 353 FROM_METHODS.put(c, m); 354 } 355 return m; 356 } 357 358 private final DateTimeFormatter formatter; 359 private final boolean zoneOptional; 360 361 /** 362 * Constructor. 363 * 364 * @param pattern The timestamp format or name of predefined {@link DateTimeFormatter}. 365 * @param zoneOptional <jk>true</jk> if the time zone on the pattern is optional. 366 */ 367 public TemporalSwap(String pattern, boolean zoneOptional) { 368 super(Temporal.class); 369 this.formatter = getDateTimeFormatter(pattern); 370 this.zoneOptional = zoneOptional; 371 } 372 373 @Override /* Overridden from ObjectSwap */ 374 public String swap(BeanSession session, Temporal o) throws Exception { 375 if (o == null) 376 return null; 377 o = convertToSerializable(session, o); 378 return formatter.format(o); 379 } 380 381 @SuppressWarnings("unchecked") 382 @Override /* Overridden from ObjectSwap */ 383 public Temporal unswap(BeanSession session, String f, ClassMeta<?> hint) throws Exception { 384 if (f == null) 385 return null; 386 if (hint == null) 387 hint = session.getClassMeta(Instant.class); 388 var tc = (Class<? extends Temporal>)hint.inner(); 389 390 var offset = session.getTimeZoneId(); 391 392 if (tc == Instant.class) 393 offset = Z; 394 395 var parseMethod = findParseMethod(tc); 396 397 var ta = defaulting(formatter.parse(f), offset); 398 return (Temporal)parseMethod.invoke(null, ta); 399 } 400 401 private final static TemporalAccessor defaulting(TemporalAccessor t, ZoneId zoneId) { 402 return new DefaultingTemporalAccessor(t, zoneId); 403 } 404 405 /** 406 * Converts the specified temporal object to a form suitable to be serialized using any pattern. 407 * 408 * @param session The current bean session. 409 * @param t The temporal object to convert. 410 * @return The converted temporal object. 411 */ 412 protected Temporal convertToSerializable(BeanSession session, Temporal t) { 413 414 var zoneId = session.getTimeZoneId(); 415 var tc = t.getClass(); 416 417 // Instant is always serialized in GMT. 418 if (tc == Instant.class) 419 return ZonedDateTime.from(defaulting(t, Z)); 420 421 // These can handle any pattern. 422 if (tc == ZonedDateTime.class || tc == OffsetDateTime.class) 423 return t; 424 425 // Pattern optionally includes a time zone, so zoned and local date-times are good. 426 if (zoneOptional()) { 427 if (tc == LocalDateTime.class) 428 return t; 429 if (tc == OffsetTime.class) 430 return ZonedDateTime.from(defaulting(t, zoneId)); 431 return LocalDateTime.from(defaulting(t, zoneId)); 432 } 433 434 return ZonedDateTime.from(defaulting(t, zoneId)); 435 } 436 437 /** 438 * Returns <jk>true</jk> if the time zone on the pattern is optional. 439 * 440 * <p> 441 * If it's not optional, then local dates/times must be converted into zoned times using the session time zone. 442 * Otherwise, local date/times are fine. 443 * 444 * @return <jk>true</jk> if the time zone on the pattern is optional. 445 */ 446 protected boolean zoneOptional() { 447 return zoneOptional; 448 } 449}