/* See LICENSE for licensing and NOTICE for copyright. */
package org.ldaptive.io;
import java.text.ParseException;
import java.time.DateTimeException;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Decodes and encodes a generalized time for use in an ldap attribute value. See
* http://tools.ietf.org/html/rfc4517#section-3.3.13
*
* @author Middleware Services
*/
public class GeneralizedTimeValueTranscoder extends AbstractStringValueTranscoder<ZonedDateTime>
{
/** Pattern for capturing the year in generalized time. */
private static final String YEAR_PATTERN = "(\\d{4})";
/** Pattern for capturing the month in generalized time. */
private static final String MONTH_PATTERN = "((?:\\x30[\\x31-\\x39])|(?:\\x31[\\x30-\\x32]))";
/** Pattern for capturing the day in generalized time. */
private static final String DAY_PATTERN = "((?:\\x30[\\x31-\\x39])" +
"|(?:[\\x31-\\x32][\\x30-\\x39])" +
"|(?:\\x33[\\x30-\\x31]))";
/** Pattern for capturing hours in generalized time. */
private static final String HOUR_PATTERN = "((?:[\\x30-\\x31][\\x30-\\x39])|(?:\\x32[\\x30-\\x33]))";
/** Pattern for capturing optional minutes in generalized time. */
private static final String MIN_PATTERN = "([\\x30-\\x35][\\x30-\\x39])?";
/** Pattern for capturing optional seconds in generalized time. */
private static final String SECOND_PATTERN = "([\\x30-\\x35][\\x30-\\x39])?";
/** Pattern for capturing optional fraction in generalized time. */
private static final String FRACTION_PATTERN = "([,.](\\d+))?";
/** Pattern for capturing timezone in generalized time. */
private static final String TIMEZONE_PATTERN = "(Z|(?:[+-]" + HOUR_PATTERN + MIN_PATTERN + "))";
/** Generalized time format regular expression. */
private static final Pattern TIME_REGEX = Pattern.compile(
YEAR_PATTERN + MONTH_PATTERN + DAY_PATTERN + HOUR_PATTERN + MIN_PATTERN + SECOND_PATTERN + FRACTION_PATTERN +
TIMEZONE_PATTERN);
/** Date format. */
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyyMMddHHmmss.SSS'Z'");
/** Describes the fractional part of a generalized time string. */
private enum FractionalPart {
/** Fractional hours. */
Hours(3600000),
/** Fractional minutes. */
Minutes(60000),
/** Fractional seconds. */
Seconds(1000);
/** Scale factor to convert units to millis. */
private final int scaleFactor;
/**
* Creates a new fractional part.
*
* @param scale scale factor.
*/
FractionalPart(final int scale)
{
scaleFactor = scale;
}
/**
* Converts the given fractional date part to milliseconds.
*
* @param fraction digits of fractional date part
*
* @return fraction converted to milliseconds.
*/
int toMillis(final String fraction)
{
return (int) (Double.parseDouble('.' + fraction) * scaleFactor);
}
}
@Override
public ZonedDateTime decodeStringValue(final String value)
{
try {
return parseGeneralizedTime(value);
} catch (ParseException | DateTimeException e) {
throw new IllegalArgumentException(e);
}
}
@Override
public String encodeStringValue(final ZonedDateTime value)
{
if (value.getZone().normalized().equals(ZoneOffset.UTC)) {
return value.format(DATE_FORMAT);
} else {
return value.withZoneSameInstant(ZoneOffset.UTC).format(DATE_FORMAT);
}
}
@Override
public Class<ZonedDateTime> getType()
{
return ZonedDateTime.class;
}
/**
* Parses the supplied value and returns a date time.
*
* @param value of generalized time to parse
*
* @return date time initialized to the correct time
*
* @throws ParseException if the value does not contain correct generalized time syntax
*/
protected ZonedDateTime parseGeneralizedTime(final String value)
throws ParseException
{
if (value == null) {
throw new IllegalArgumentException("String to parse cannot be null.");
}
final Matcher m = TIME_REGEX.matcher(value);
if (!m.matches()) {
throw new ParseException("Invalid generalized time string.", value.length());
}
// CheckStyle:MagicNumber OFF
final ZoneId zoneId;
final String tzString = m.group(9);
if ("Z".equals(tzString)) {
zoneId = ZoneOffset.UTC;
} else {
zoneId = ZoneId.of("GMT" + tzString);
}
// Set required time fields
final int year = Integer.parseInt(m.group(1));
final int month = Integer.parseInt(m.group(2));
final int dayOfMonth = Integer.parseInt(m.group(3));
final int hour = Integer.parseInt(m.group(4));
FractionalPart fraction = FractionalPart.Hours;
// Set optional minutes
int minutes = 0;
if (m.group(5) != null) {
fraction = FractionalPart.Minutes;
minutes = Integer.parseInt(m.group(5));
}
// Set optional seconds
int seconds = 0;
if (m.group(6) != null) {
fraction = FractionalPart.Seconds;
seconds = Integer.parseInt(m.group(6));
}
// Set optional fractional part
int millis = 0;
if (m.group(7) != null) {
millis = fraction.toMillis(m.group(8));
}
// CheckStyle:MagicNumber ON
return ZonedDateTime.of(
LocalDateTime.of(year, month, dayOfMonth, hour, minutes, seconds).plus(millis, ChronoUnit.MILLIS), zoneId);
}
}