jakarta.faces.convert.DateTimeConverter Maven / Gradle / Ivy
/*
* Copyright (c) 1997, 2020 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the
* Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
* version 2 with the GNU Classpath Exception, which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
*/
package jakarta.faces.convert;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.format.FormatStyle;
import java.time.temporal.TemporalAccessor;
import java.time.temporal.TemporalQuery;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
import jakarta.faces.component.PartialStateHolder;
import jakarta.faces.component.UIComponent;
import jakarta.faces.context.FacesContext;
/**
*
* {@link Converter} implementation for
* java.util.Date
values.
*
*
*
* The getAsObject()
method parses a String into a java.util.Date
, according to the following
* algorithm:
*
*
* - If the specified String is null, return a
null
. Otherwise, trim leading and trailing whitespace
* before proceeding.
* - If the specified String - after trimming - has a zero length, return
null
.
* - If the
locale
property is not null, use that Locale
for managing parsing. Otherwise,
* use the Locale
from the UIViewRoot
.
*
* - If a
pattern
has been specified, its syntax must conform the rules specified by
* java.text.SimpleDateFormat
or {@code
* java.time.format.DateTimeFormatter}. Which of these two formatters is used depends on the value of
* {@code type}. Such a pattern will be used to parse, and the type
, dateStyle
, and
* timeStyle
properties will be ignored, unless the value of {@code
* type} is one of the {@code java.time} specific values listed in {@link #setType}. In this case,
* {@code DateTimeFormatter.ofPattern(String, Locale)} must be called, passing the value of {@code pattern} as the first
* argument and the current {@code Locale} as the second argument, and this formatter must be used to parse the incoming
* value.
*
* - If a
pattern
has not been specified, parsing will be based on the type
property, which
* expects a date value, a time value, both, or one of several values specific to
* classes in {@code java.time} as listed in {@link #setType}. Any date and time values included will be parsed
* in accordance to the styles specified by dateStyle
and timeStyle
, respectively.
* - If a
timezone
has been specified, it must be passed to the underlying DateFormat
* instance. Otherwise the "GMT" timezone is used.
* - In all cases, parsing must be non-lenient; the given string must strictly adhere to the parsing format.
*
*
*
* The getAsString()
method expects a value of type java.util.Date
(or a subclass), and
* creates a formatted String according to the following algorithm:
*
*
* - If the specified value is null, return a zero-length String.
* - If the specified value is a String, return it unmodified.
* - If the
locale
property is not null, use that Locale
for managing formatting. Otherwise,
* use the Locale
from the UIViewRoot
.
* - If a
timezone
has been specified, it must be passed to the underlying DateFormat
* instance. Otherwise the "GMT" timezone is used.
*
* - If a
pattern
has been specified, its syntax must conform the rules specified by
* java.text.SimpleDateFormat
or {@code
* java.time.format.DateTimeFormatter}. Which of these two formatters is used depends on the value of
* {@code type}. Such a pattern will be used to format, and the type
, dateStyle
, and
* timeStyle
properties will be ignored, unless the value of {@code
* type} is one of the {@code java.time} specific values listed in {@link #setType}. In this case, {@code
* DateTimeFormatter.ofPattern(String, Locale)} must be called, passing the value of {@code pattern} as the first
* argument and the current {@code Locale} as the second argument, and this formatter must be used to format the
* outgoing value.
*
* - If a
pattern
has not been specified, formatting will be based on the type
property,
* which includes a date value, a time value, both or into the formatted String. Any date and time values included will
* be formatted in accordance to the styles specified by dateStyle
and timeStyle
,
* respectively.
*
*/
public class DateTimeConverter implements Converter, PartialStateHolder {
// ------------------------------------------------------ Manifest Constants
/**
*
* The standard converter id for this converter.
*
*/
public static final String CONVERTER_ID = "jakarta.faces.DateTime";
/**
*
* The message identifier of the {@link jakarta.faces.application.FacesMessage} to be created if the conversion to
* Date
fails. The message format string for this message may optionally include the following
* placeholders:
*
* {0}
replaced by the unconverted value.
* {1}
replaced by an example value.
* {2}
replaced by a String
whose value is the label of the input component that produced
* this message.
*
*/
public static final String DATE_ID = "jakarta.faces.converter.DateTimeConverter.DATE";
/**
*
* The message identifier of the {@link jakarta.faces.application.FacesMessage} to be created if the conversion to
* Time
fails. The message format string for this message may optionally include the following
* placeholders:
*
* {0}
replaced by the unconverted value.
* {1}
replaced by an example value.
* {2}
replaced by a String
whose value is the label of the input component that produced
* this message.
*
*/
public static final String TIME_ID = "jakarta.faces.converter.DateTimeConverter.TIME";
/**
*
* The message identifier of the {@link jakarta.faces.application.FacesMessage} to be created if the conversion to
* DateTime
fails. The message format string for this message may optionally include the following
* placeholders:
*
* {0}
replaced by the unconverted value.
* {1}
replaced by an example value.
* {2}
replaced by a String
whose value is the label of the input component that produced
* this message.
*
*/
public static final String DATETIME_ID = "jakarta.faces.converter.DateTimeConverter.DATETIME";
/**
*
* The message identifier of the {@link jakarta.faces.application.FacesMessage} to be created if the conversion of the
* DateTime
value to String
fails. The message format string for this message may optionally
* include the following placeholders:
*
* {0}
relaced by the unconverted value.
* {1}
replaced by a String
whose value is the label of the input component that produced
* this message.
*
*/
public static final String STRING_ID = "jakarta.faces.converter.STRING";
private static final TimeZone DEFAULT_TIME_ZONE = TimeZone.getTimeZone("GMT");
// ------------------------------------------------------ Instance Variables
private String dateStyle = "default";
private Locale locale = null;
private String pattern = null;
private String timeStyle = "default";
private TimeZone timeZone = DEFAULT_TIME_ZONE;
private String type = "date";
// -------------------------------------------------------------- Properties
/**
*
* Return the style to be used to format or parse dates. If not set, the default value, default
, is
* returned.
*
*
* @return the style
*/
public String getDateStyle() {
return dateStyle;
}
/**
*
* Set the style to be used to format or parse dates. Valid values are default
, short
,
* medium
, long
, and full
. An invalid value will cause a
* {@link ConverterException} when getAsObject()
or getAsString()
is called.
*
*
* @param dateStyle The new style code
*/
public void setDateStyle(String dateStyle) {
clearInitialState();
this.dateStyle = dateStyle;
}
/**
*
* Return the Locale
to be used when parsing or formatting dates and times. If not explicitly set, the
* Locale
stored in the {@link jakarta.faces.component.UIViewRoot} for the current request is returned.
*
*
* @return the {@code Locale}
*/
public Locale getLocale() {
if (locale == null) {
locale = getLocale(FacesContext.getCurrentInstance());
}
return locale;
}
/**
*
* Set the Locale
to be used when parsing or formatting dates and times. If set to null
, the
* Locale
stored in the {@link jakarta.faces.component.UIViewRoot} for the current request will be
* utilized.
*
*
* @param locale The new Locale
(or null
)
*/
public void setLocale(Locale locale) {
clearInitialState();
this.locale = locale;
}
/**
*
* Return the format pattern to be used when formatting and parsing dates and times.
*
*
* @return the pattern
*/
public String getPattern() {
return pattern;
}
/**
*
* Set the format pattern to be used when formatting and parsing dates and times. Valid values are those supported by
* java.text.SimpleDateFormat
. An invalid value will cause a {@link ConverterException} when
* getAsObject()
or getAsString()
is called.
*
*
* @param pattern The new format pattern
*/
public void setPattern(String pattern) {
clearInitialState();
this.pattern = pattern;
}
/**
*
* Return the style to be used to format or parse times. If not set, the default value, default
, is
* returned.
*
*
* @return the time style
*/
public String getTimeStyle() {
return timeStyle;
}
/**
*
* Set the style to be used to format or parse times. Valid values are default
, short
,
* medium
, long
, and full
. An invalid value will cause a
* {@link ConverterException} when getAsObject()
or getAsString()
is called.
*
*
* @param timeStyle The new style code
*/
public void setTimeStyle(String timeStyle) {
clearInitialState();
this.timeStyle = timeStyle;
}
/**
*
* Return the TimeZone
used to interpret a time value. If not explicitly set, the default time zone of
* GMT
returned.
*
*
* @return the {@code TimeZone}
*/
public TimeZone getTimeZone() {
return timeZone;
}
/**
*
* Set the TimeZone
used to interpret a time value.
*
*
* @param timeZone The new time zone
*/
public void setTimeZone(TimeZone timeZone) {
clearInitialState();
this.timeZone = timeZone;
}
/**
*
* Return the type of value to be formatted or parsed. If not explicitly set, the default type, date
is
* returned.
*
*
* @return the type
*/
public String getType() {
return type;
}
/**
*
* Set the type of value to be formatted or parsed. Valid values are
* both
, date
, time
{@code localDate}, {@code
* localDateTime}, {@code localTime}, {@code offsetTime}, {@code
* offsetDateTime}, or {@code zonedDateTime}. The values starting with "local", "offset" and "zoned" correspond to Java
* SE 8 Date Time API classes in package java.time
with the name derived by upper casing the first letter.
* For example, java.time.LocalDate
for the value "localDate"
. An invalid value will
* cause a {@link ConverterException} when getAsObject()
or getAsString()
is called.
*
*
* @param type The new date style
*/
public void setType(String type) {
clearInitialState();
this.type = type;
}
// ------------------------------------------------------- Converter Methods
/**
* @throws ConverterException {@inheritDoc}
* @throws NullPointerException {@inheritDoc}
*/
@Override
public Object getAsObject(FacesContext context, UIComponent component, String value) {
if (context == null || component == null) {
throw new NullPointerException();
}
Object returnValue = null;
FormatWrapper parser = null;
try {
// If the specified value is null or zero-length, return null
if (value == null) {
return null;
}
value = value.trim();
if (value.length() < 1) {
return null;
}
// Identify the Locale to use for parsing
Locale locale = getLocale(context);
// Create and configure the parser to be used
parser = getDateFormat(locale);
if (null != timeZone) {
parser.setTimeZone(timeZone);
}
// Perform the requested parsing
returnValue = parser.parse(value);
} catch (ParseException | DateTimeParseException e) {
if (null != type) {
switch (type) {
case "date":
case "localDate":
throw new ConverterException(
MessageFactory.getMessage(context, DATE_ID, value, parser.formatNow(), MessageFactory.getLabel(context, component)), e);
case "time":
case "localTime":
case "offsetTime":
throw new ConverterException(
MessageFactory.getMessage(context, TIME_ID, value, parser.formatNow(), MessageFactory.getLabel(context, component)), e);
case "both":
case "localDateTime":
case "offsetDateTime":
case "zonedDateTime":
throw new ConverterException(
MessageFactory.getMessage(context, DATETIME_ID, value, parser.formatNow(), MessageFactory.getLabel(context, component)), e);
}
}
} catch (Exception e) {
throw new ConverterException(e);
}
return returnValue;
}
private static class FormatWrapper {
private final DateFormat df;
private final DateTimeFormatter dtf;
private final TemporalQuery from;
private FormatWrapper(DateFormat wrapped) {
df = wrapped;
dtf = null;
from = null;
}
private FormatWrapper(DateTimeFormatter dtf, TemporalQuery from) {
df = null;
this.dtf = dtf;
this.from = from;
}
private Object parse(CharSequence text) throws ParseException {
Object result = null != df ? df.parse((String) text) : dtf.parse(text, from);
return result;
}
private String format(Object obj) {
return null != df ? df.format(obj) : dtf.format((TemporalAccessor) obj);
}
private String formatNow() {
return null != df ? df.format(new Date()) : dtf.format(ZonedDateTime.now());
}
private void setTimeZone(TimeZone zone) {
if (null != df) {
df.setTimeZone(zone);
}
}
}
/**
* @throws ConverterException {@inheritDoc}
* @throws NullPointerException {@inheritDoc}
*/
@Override
public String getAsString(FacesContext context, UIComponent component, Object value) {
if (context == null || component == null) {
throw new NullPointerException();
}
try {
// If the specified value is null, return a zero-length String
if (value == null) {
return "";
}
// If the incoming value is still a string, play nice
// and return the value unmodified
if (value instanceof String) {
return (String) value;
}
// Identify the Locale to use for formatting
Locale locale = getLocale(context);
// Create and configure the formatter to be used
FormatWrapper formatter = getDateFormat(locale);
if (null != timeZone) {
formatter.setTimeZone(timeZone);
}
// Perform the requested formatting
return formatter.format(value);
} catch (ConverterException e) {
throw new ConverterException(MessageFactory.getMessage(context, STRING_ID, value, MessageFactory.getLabel(context, component)), e);
} catch (Exception e) {
throw new ConverterException(MessageFactory.getMessage(context, STRING_ID, value, MessageFactory.getLabel(context, component)), e);
}
}
// --------------------------------------------------------- Private Methods
/**
*
* Return a DateFormat
instance to use for formatting and parsing in this {@link Converter}.
*
*
* @param locale The Locale
used to select formatting and parsing conventions
* @throws ConverterException if no instance can be created
*/
private FormatWrapper getDateFormat(Locale locale) {
// PENDING(craigmcc) - Implement pooling if needed for performance?
if (pattern == null && type == null) {
throw new IllegalArgumentException("Either pattern or type must" + " be specified.");
}
DateFormat df = null;
DateTimeFormatter dtf = null;
TemporalQuery from = null;
if (pattern != null && !isJavaTimeType(type)) {
df = new SimpleDateFormat(pattern, locale);
} else if (type.equals("both")) {
df = DateFormat.getDateTimeInstance(getStyle(dateStyle), getStyle(timeStyle), locale);
} else if (type.equals("date")) {
df = DateFormat.getDateInstance(getStyle(dateStyle), locale);
} else if (type.equals("time")) {
df = DateFormat.getTimeInstance(getStyle(timeStyle), locale);
} else if (type.equals("localDate")) {
if (null != pattern) {
dtf = DateTimeFormatter.ofPattern(pattern, locale);
} else {
dtf = DateTimeFormatter.ofLocalizedDate(getFormatStyle(dateStyle)).withLocale(locale);
}
from = LocalDate::from;
} else if (type.equals("localDateTime")) {
if (null != pattern) {
dtf = DateTimeFormatter.ofPattern(pattern, locale);
} else {
dtf = DateTimeFormatter.ofLocalizedDateTime(getFormatStyle(dateStyle), getFormatStyle(timeStyle)).withLocale(locale);
}
from = LocalDateTime::from;
} else if (type.equals("localTime")) {
if (null != pattern) {
dtf = DateTimeFormatter.ofPattern(pattern, locale);
} else {
dtf = DateTimeFormatter.ofLocalizedTime(getFormatStyle(timeStyle)).withLocale(locale);
}
from = LocalTime::from;
} else if (type.equals("offsetTime")) {
if (null != pattern) {
dtf = DateTimeFormatter.ofPattern(pattern, locale);
} else {
dtf = DateTimeFormatter.ISO_OFFSET_TIME.withLocale(locale);
}
from = OffsetTime::from;
} else if (type.equals("offsetDateTime")) {
if (null != pattern) {
dtf = DateTimeFormatter.ofPattern(pattern, locale);
} else {
dtf = DateTimeFormatter.ISO_OFFSET_DATE_TIME.withLocale(locale);
}
from = OffsetDateTime::from;
} else if (type.equals("zonedDateTime")) {
if (null != pattern) {
dtf = DateTimeFormatter.ofPattern(pattern, locale);
} else {
dtf = DateTimeFormatter.ISO_ZONED_DATE_TIME.withLocale(locale);
}
from = ZonedDateTime::from;
} else {
// PENDING(craigmcc) - i18n
throw new IllegalArgumentException("Invalid type: " + type);
}
if (null != df) {
df.setLenient(false);
return new FormatWrapper(df);
} else if (null != dtf) {
return new FormatWrapper(dtf, from);
}
// PENDING(craigmcc) - i18n
throw new IllegalArgumentException("Invalid type: " + type);
}
private static boolean isJavaTimeType(String type) {
boolean result = false;
if (null != type && type.length() > 1) {
char c = type.charAt(0);
result = c == 'l' || c == 'o' || c == 'z';
}
return result;
}
/**
*
* Return the Locale
we will use for localizing our formatting and parsing processing.
*
*
* @param context The {@link FacesContext} for the current request
*/
private Locale getLocale(FacesContext context) {
// PENDING(craigmcc) - JSTL localization context?
Locale locale = this.locale;
if (locale == null) {
locale = context.getViewRoot().getLocale();
}
return locale;
}
/**
*
* Return the style constant for the specified style name.
*
*
* @param name Name of the style for which to return a constant
* @throws ConverterException if the style name is not valid
*/
private static int getStyle(String name) {
if (null != name) {
switch (name) {
case "default":
return DateFormat.DEFAULT;
case "short":
return DateFormat.SHORT;
case "medium":
return DateFormat.MEDIUM;
case "long":
return DateFormat.LONG;
case "full":
return DateFormat.FULL;
}
}
// PENDING(craigmcc) - i18n
throw new ConverterException("Invalid style '" + name + '\'');
}
private static FormatStyle getFormatStyle(String name) {
if (null != name) {
switch (name) {
case "default":
case "medium":
return FormatStyle.MEDIUM;
case "short":
return FormatStyle.SHORT;
case "long":
return FormatStyle.LONG;
case "full":
return FormatStyle.FULL;
}
}
// PENDING(craigmcc) - i18n
throw new ConverterException("Invalid style '" + name + '\'');
}
// ----------------------------------------------------- StateHolder Methods
@Override
public Object saveState(FacesContext context) {
if (context == null) {
throw new NullPointerException();
}
if (!initialStateMarked()) {
Object values[] = new Object[6];
values[0] = dateStyle;
values[1] = locale;
values[2] = pattern;
values[3] = timeStyle;
values[4] = timeZone;
values[5] = type;
return values;
}
return null;
}
@Override
public void restoreState(FacesContext context, Object state) {
if (context == null) {
throw new NullPointerException();
}
if (state != null) {
Object values[] = (Object[]) state;
dateStyle = (String) values[0];
locale = (Locale) values[1];
pattern = (String) values[2];
timeStyle = (String) values[3];
timeZone = (TimeZone) values[4];
type = (String) values[5];
}
}
private boolean transientFlag;
@Override
public boolean isTransient() {
return transientFlag;
}
@Override
public void setTransient(boolean transientFlag) {
this.transientFlag = transientFlag;
}
private boolean initialState;
@Override
public void markInitialState() {
initialState = true;
}
@Override
public boolean initialStateMarked() {
return initialState;
}
@Override
public void clearInitialState() {
initialState = false;
}
}