com.azure.core.util.DateTimeRfc1123 Maven / Gradle / Ivy
Show all versions of azure-core Show documentation
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package com.azure.core.util;
import com.azure.core.util.logging.ClientLogger;
import com.fasterxml.jackson.annotation.JsonCreator;
import java.nio.charset.StandardCharsets;
import java.time.DateTimeException;
import java.time.DayOfWeek;
import java.time.LocalDateTime;
import java.time.Month;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
/**
* Wrapper over java.time.OffsetDateTime used for specifying RFC1123 format during serialization and deserialization.
*/
public final class DateTimeRfc1123 {
private static final ClientLogger LOGGER = new ClientLogger(DateTimeRfc1123.class);
/**
* The actual datetime object.
*/
private final OffsetDateTime dateTime;
/**
* Creates a new DateTimeRfc1123 object with the specified DateTime.
* @param dateTime The DateTime object to wrap.
*/
public DateTimeRfc1123(OffsetDateTime dateTime) {
this.dateTime = dateTime;
}
/**
* Creates a new DateTimeRfc1123 object with the specified DateTime.
* @param formattedString The datetime string in RFC1123 format
*/
public DateTimeRfc1123(String formattedString) {
this.dateTime = parse(formattedString);
}
/**
* Returns the underlying DateTime.
* @return The underlying DateTime.
*/
public OffsetDateTime getDateTime() {
return this.dateTime;
}
/**
* JSON creator for DateTimeRfc1123.
*
* If {@code date} is null or an empty string null will be returned.
*
* @param date RFC1123 datetime string.
* @return The DateTimeRfc1123 representation of the datetime string, or null if {@code date} is null or empty.
*/
@JsonCreator
static DateTimeRfc1123 fromString(final String date) {
if (CoreUtils.isNullOrEmpty(date)) {
return null;
}
return new DateTimeRfc1123(date);
}
/**
* Parses the RFC1123 format datetime string into OffsetDateTime.
*
* @param date The datetime string in RFC1123 format
* @return The underlying OffsetDateTime.
*
* @throws DateTimeException If the processing character is not a digit character.
* @throws IllegalArgumentException if the given character is not recognized in the pattern of Month. such as 'Jan'.
* @throws IndexOutOfBoundsException if the {@code beginIndex} is negative, or beginIndex is larger than length of
* {@code date}.
*/
private static OffsetDateTime parse(final String date) {
try {
return OffsetDateTime.of(LocalDateTime.of(parseInt(date, 12, 16), // year
parseMonth(date), // month
parseInt(date, 5, 7), // dayOfMonth
parseInt(date, 17, 19), // hour
parseInt(date, 20, 22), // minute
parseInt(date, 23, 25), // second
0 // nanoOfSecond
), ZoneOffset.UTC);
} catch (DateTimeException | IllegalArgumentException | IndexOutOfBoundsException e) {
return OffsetDateTime.parse(date, DateTimeFormatter.RFC_1123_DATE_TIME);
}
}
/**
* Parses the specified substring of datetime to a 'int' value.
*
* @param date The datetime string in RFC1123 format.
* @param beginIndex The beginning index, inclusive.
* @param endIndex The ending index, exclusive.
* @return The specified substring.
*
* @throws DateTimeException If the processing character is not digit character.
*/
private static int parseInt(final String date, final int beginIndex, final int endIndex) {
int num = 0;
for (int i = beginIndex; i < endIndex; i++) {
final char c = date.charAt(i);
if (c < '0' || c > '9') {
throw LOGGER.logExceptionAsError(new DateTimeException("Invalid date time: " + date));
}
num = num * 10 + (c - '0');
}
return num;
}
/**
* Parses the specified month substring of date time to a {@link Month}.
*
* Previously this was implemented to return the integer representing the month ({@code 1-12}) but using an integer
* to create {@link LocalDateTime} incurs a range validation check. Now this is implemented to return {@link Month}
* which removes the range validation check.
*
* @param date The date time string in RFC1123 format.
* @return The {@link Month} value which represents the month of year.
* @throws IllegalArgumentException if the given character is not recognized in the pattern of Month. such as 'Jan'.
* @throws IndexOutOfBoundsException if the {@code beginIndex} is negative, or beginIndex is larger than length of
* {@code date}.
*/
private static Month parseMonth(final CharSequence date) {
switch (date.charAt(8)) {
case 'J':
// Jan, Jun, Jul
switch (date.charAt(9)) {
case 'a':
return Month.JANUARY;
case 'u':
switch (date.charAt(10)) {
case 'n':
return Month.JUNE;
case 'l':
return Month.JULY;
default:
throw LOGGER.logExceptionAsError(new IllegalArgumentException("Unknown month " + date));
}
default:
throw LOGGER.logExceptionAsError(new IllegalArgumentException("Unknown month " + date));
}
case 'F':
return Month.FEBRUARY;
case 'M':
// Mar, May
switch (date.charAt(10)) {
case 'r':
return Month.MARCH;
case 'y':
return Month.MAY;
default:
throw LOGGER.logExceptionAsError(new IllegalArgumentException("Unknown month " + date));
}
case 'A':
// Apr, Aug
switch (date.charAt(10)) {
case 'r':
return Month.APRIL;
case 'g':
return Month.AUGUST;
default:
throw LOGGER.logExceptionAsError(new IllegalArgumentException("Unknown month " + date));
}
case 'S':
return Month.SEPTEMBER;
case 'O':
return Month.OCTOBER;
case 'N':
return Month.NOVEMBER;
case 'D':
return Month.DECEMBER;
default:
throw LOGGER.logExceptionAsError(new IllegalArgumentException("Unknown month " + date));
}
}
/**
* Convert the {@link OffsetDateTime dateTime} to date time string in RFC1123 format.
*
* @param dateTime The date time in OffsetDateTime format.
* @return The date time string in RFC1123 format.
* @throws IllegalArgumentException If {@link OffsetDateTime#getDayOfWeek()} or
* {@link OffsetDateTime#getDayOfMonth()} is an unknown value.
*/
public static String toRfc1123String(OffsetDateTime dateTime) {
// ensure datetime is UTC offset.
dateTime = dateTime.withOffsetSameInstant(ZoneOffset.UTC);
byte[] bytes = new byte[29];
final DayOfWeek dayOfWeek = dateTime.getDayOfWeek();
switch (dayOfWeek) {
case MONDAY:
bytes[0] = 'M';
bytes[1] = 'o';
bytes[2] = 'n';
break;
case TUESDAY:
bytes[0] = 'T';
bytes[1] = 'u';
bytes[2] = 'e';
break;
case WEDNESDAY:
bytes[0] = 'W';
bytes[1] = 'e';
bytes[2] = 'd';
break;
case THURSDAY:
bytes[0] = 'T';
bytes[1] = 'h';
bytes[2] = 'u';
break;
case FRIDAY:
bytes[0] = 'F';
bytes[1] = 'r';
bytes[2] = 'i';
break;
case SATURDAY:
bytes[0] = 'S';
bytes[1] = 'a';
bytes[2] = 't';
break;
case SUNDAY:
bytes[0] = 'S';
bytes[1] = 'u';
bytes[2] = 'n';
break;
default:
throw LOGGER.logExceptionAsError(new IllegalArgumentException("Unknown day of week " + dayOfWeek));
}
bytes[3] = ',';
bytes[4] = ' ';
zeroPad(dateTime.getDayOfMonth(), bytes, 5);
bytes[7] = ' ';
final Month month = dateTime.getMonth();
switch (month) {
case JANUARY:
bytes[8] = 'J';
bytes[9] = 'a';
bytes[10] = 'n';
break;
case FEBRUARY:
bytes[8] = 'F';
bytes[9] = 'e';
bytes[10] = 'b';
break;
case MARCH:
bytes[8] = 'M';
bytes[9] = 'a';
bytes[10] = 'r';
break;
case APRIL:
bytes[8] = 'A';
bytes[9] = 'p';
bytes[10] = 'r';
break;
case MAY:
bytes[8] = 'M';
bytes[9] = 'a';
bytes[10] = 'y';
break;
case JUNE:
bytes[8] = 'J';
bytes[9] = 'u';
bytes[10] = 'n';
break;
case JULY:
bytes[8] = 'J';
bytes[9] = 'u';
bytes[10] = 'l';
break;
case AUGUST:
bytes[8] = 'A';
bytes[9] = 'u';
bytes[10] = 'g';
break;
case SEPTEMBER:
bytes[8] = 'S';
bytes[9] = 'e';
bytes[10] = 'p';
break;
case OCTOBER:
bytes[8] = 'O';
bytes[9] = 'c';
bytes[10] = 't';
break;
case NOVEMBER:
bytes[8] = 'N';
bytes[9] = 'o';
bytes[10] = 'v';
break;
case DECEMBER:
bytes[8] = 'D';
bytes[9] = 'e';
bytes[10] = 'c';
break;
default:
throw LOGGER.logExceptionAsError(new IllegalArgumentException("Unknown month " + month));
}
bytes[11] = ' ';
int year = dateTime.getYear();
int round = year / 1000;
bytes[12] = (byte) ('0' + round);
year = year - (1000 * round);
round = year / 100;
bytes[13] = (byte) ('0' + round);
year = year - (100 * round);
round = year / 10;
bytes[14] = (byte) ('0' + round);
bytes[15] = (byte) ('0' + (year - (10 * round)));
bytes[16] = ' ';
zeroPad(dateTime.getHour(), bytes, 17);
bytes[19] = ':';
zeroPad(dateTime.getMinute(), bytes, 20);
bytes[22] = ':';
zeroPad(dateTime.getSecond(), bytes, 23);
bytes[25] = ' ';
bytes[26] = 'G';
bytes[27] = 'M';
bytes[28] = 'T';
// Use UTF-8 as it's more performant than ASCII in Java 8
return new String(bytes, StandardCharsets.UTF_8);
}
private static void zeroPad(int value, byte[] bytes, int index) {
if (value < 10) {
bytes[index++] = '0';
bytes[index] = (byte) ('0' + value);
} else {
int high = value / 10;
bytes[index++] = (byte) ('0' + high);
bytes[index] = (byte) ('0' + (value - (10 * high)));
}
}
@Override
public String toString() {
return toRfc1123String(this.dateTime);
}
@Override
public int hashCode() {
return this.dateTime.hashCode();
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (!(obj instanceof DateTimeRfc1123)) {
return false;
}
DateTimeRfc1123 rhs = (DateTimeRfc1123) obj;
return this.dateTime.equals(rhs.getDateTime());
}
}