001package com.identityworksllc.iiq.common.plugin.vo;
002
003import com.fasterxml.jackson.annotation.JsonAutoDetect;
004import org.apache.commons.logging.Log;
005import org.apache.commons.logging.LogFactory;
006
007import java.time.Instant;
008import java.time.LocalDate;
009import java.time.ZoneId;
010import java.time.ZonedDateTime;
011import java.time.format.DateTimeFormatter;
012import java.time.temporal.TemporalAccessor;
013import java.util.Date;
014
015/**
016 * The JAX-RS spec requires that an input bean for {@link javax.ws.rs.QueryParam}
017 * or {@link javax.ws.rs.PathParam} have either a String constructor or a static
018 * valueOf(String) method. This is that bean, for dates.
019 *
020 * The string input must either the ISO8601 local date, offset date time, or instant
021 * formats. Other formats will not be interpreted successfully.
022 *
023 * The stored value will be a {@link ZonedDateTime} by default.
024 *
025 * @see <a href="https://docs.oracle.com/javaee/7/api/javax/ws/rs/QueryParam.html">QueryParam</a>
026 */
027@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
028public class InputDate {
029
030    /**
031     * The regex to recognize an ISO8601 date (yyyy-mm-dd)
032     */
033    private static final String ISO8601_DATE_REGEX = "^\\d{4}-\\d\\d-\\d\\d$";
034
035    /**
036     * The regex to recognize an ISO8601 date-time, with "T" in the middle
037     */
038    private static final String ISO8601_FULL_REGEX = "^\\d{4}-\\d\\d-\\d\\dT\\d\\d:\\d\\d:\\d\\d(.\\d+)?(([+-]\\d\\d:\\d\\d)|Z)?$";
039
040    /**
041     * The logger
042     */
043    private static final Log logger = LogFactory.getLog(InputDate.class);
044
045    /**
046     * Translates the String to an InputDate. This is the method required by JAX-RS.
047     *
048     * @param input The input string, in either epoch milliseconds or ISO8601 format
049     * @return The InputDate
050     */
051    public static InputDate valueOf(String input) {
052        if (logger.isDebugEnabled()) {
053            logger.debug("Parsing input date: " + input);
054        }
055
056        InputDate result;
057
058        if (input.matches("^\\d+$")) {
059            // Epoch timestamp
060            long timestamp = Long.parseLong(input);
061            result = new InputDate(timestamp);
062        } else if (input.matches(ISO8601_FULL_REGEX)) {
063            TemporalAccessor ta = DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(input);
064            result = new InputDate(ZonedDateTime.from(ta));
065        } else if (input.matches(ISO8601_DATE_REGEX)) {
066            TemporalAccessor ta = DateTimeFormatter.ISO_DATE.parse(input);
067            LocalDate ld = LocalDate.from(ta);
068            result = new InputDate(ld.atStartOfDay(ZoneId.systemDefault()));
069        } else {
070            throw new IllegalArgumentException("Unrecognized date format (expected epoch milliseconds, ISO8601 date (yyyy-mm-dd), or ISO8601 date-time): " + input);
071        }
072
073        if (logger.isDebugEnabled()) {
074            logger.debug("Parsed input date: " + result.date);
075        }
076
077        return result;
078    }
079
080    /**
081     * The stored date-time object
082     */
083    private final ZonedDateTime date;
084
085    /**
086     * Constructs a new InputDate from a long value
087     * @param input an epoch millisecond timestamp
088     */
089    public InputDate(long input) {
090        this.date = Instant.ofEpochMilli(input).atZone(ZoneId.systemDefault());
091    }
092
093    /**
094     * Constructs a new InputDate from an instant
095     * @param from An instant
096     */
097    public InputDate(Instant from) {
098        this.date = from.atZone(ZoneId.systemDefault());
099    }
100
101    /**
102     * Constructs a new InputDate from a {@link Date}
103     * @param input The input date
104     */
105    public InputDate(Date input) {
106        this.date = ZonedDateTime.ofInstant(input.toInstant(), ZoneId.systemDefault());
107    }
108
109    /**
110     * Constructs a new InputDate from a ZonedDateTime. The value is
111     * stored directly in this class.
112     *
113     * @param zdt The {@link ZonedDateTime} input
114     */
115    public InputDate(ZonedDateTime zdt) {
116        this.date = zdt;
117    }
118
119    /**
120     * Returns the epoch millisecond rendition of the date
121     *
122     * @return The date in epoch milliseconds
123     */
124    public long toEpochMillis() {
125        return date.toInstant().toEpochMilli();
126    }
127
128    /**
129     * Returns the java.util.Date object
130     *
131     * @return The java date object
132     */
133    public Date toJavaData() {
134        return Date.from(date.toInstant());
135    }
136
137    @Override
138    public String toString() {
139        return DateTimeFormatter.ISO_DATE_TIME.format(this.date);
140    }
141
142    /**
143     * Returns the input date as a ZonedDateTime
144     *
145     * @return The ZonedDateTime
146     */
147    public ZonedDateTime toZonedDateTime() {
148        return date;
149    }
150}