This is an automated email from the ASF dual-hosted git repository. lukaszlenart pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/struts.git
The following commit(s) were added to refs/heads/master by this push: new 7e912742e Basic LocalDateTime support (#532) 7e912742e is described below commit 7e912742edd55c77cec37a9fbfbe077b683ebf83 Author: gregh3269 <gregh3...@gmail.com> AuthorDate: Tue May 17 06:31:30 2022 +0100 Basic LocalDateTime support (#532) * Basic LocalDateTime support * Basic LocalDateTime support * Basic LocalDateTime support * Basic LocalDateTime support * Basic LocalDateTime support Co-authored-by: Greg Huber <ghu...@guestales.co.uk> Co-authored-by: Greg Huber <ghu...@apache.org> --- .../xwork2/conversion/impl/DateConverter.java | 117 +++++++++++++++------ .../conversion/impl/XWorkBasicConverter.java | 3 + .../xwork2/interceptor/ParametersInterceptor.java | 2 +- .../xwork2/conversion/impl/DateConverterTest.java | 41 ++++++++ 4 files changed, 130 insertions(+), 33 deletions(-) diff --git a/core/src/main/java/com/opensymphony/xwork2/conversion/impl/DateConverter.java b/core/src/main/java/com/opensymphony/xwork2/conversion/impl/DateConverter.java index 99c1e2f62..18068d334 100644 --- a/core/src/main/java/com/opensymphony/xwork2/conversion/impl/DateConverter.java +++ b/core/src/main/java/com/opensymphony/xwork2/conversion/impl/DateConverter.java @@ -18,28 +18,34 @@ */ package com.opensymphony.xwork2.conversion.impl; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.apache.struts2.conversion.TypeConversionException; -import com.opensymphony.xwork2.ActionContext; -import com.opensymphony.xwork2.TextProvider; -import com.opensymphony.xwork2.util.ValueStack; - import java.lang.reflect.Constructor; import java.lang.reflect.Member; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.time.temporal.TemporalAccessor; import java.util.Date; import java.util.Locale; import java.util.Map; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.struts2.conversion.TypeConversionException; + +import com.opensymphony.xwork2.ActionContext; +import com.opensymphony.xwork2.TextProvider; +import com.opensymphony.xwork2.util.ValueStack; + public class DateConverter extends DefaultTypeConverter { private final static Logger LOG = LogManager.getLogger(DateConverter.class); @Override - public Object convertValue(Map<String, Object> context, Object target, Member member, String propertyName, Object value, Class toType) { + public Object convertValue(Map<String, Object> context, Object target, Member member, String propertyName, + Object value, Class toType) { Date result = null; if (value instanceof String && ((String) value).length() > 0) { @@ -52,15 +58,12 @@ public class DateConverter extends DefaultTypeConverter { } else if (java.sql.Timestamp.class == toType) { Date check = null; SimpleDateFormat dtfmt = (SimpleDateFormat) DateFormat.getDateTimeInstance(DateFormat.SHORT, - DateFormat.MEDIUM, - locale); - SimpleDateFormat fullfmt = new SimpleDateFormat(dtfmt.toPattern() + MILLISECOND_FORMAT, - locale); + DateFormat.MEDIUM, locale); + SimpleDateFormat fullfmt = new SimpleDateFormat(dtfmt.toPattern() + MILLISECOND_FORMAT, locale); - SimpleDateFormat dfmt = (SimpleDateFormat) DateFormat.getDateInstance(DateFormat.SHORT, - locale); + SimpleDateFormat dfmt = (SimpleDateFormat) DateFormat.getDateInstance(DateFormat.SHORT, locale); - SimpleDateFormat[] fmts = {fullfmt, dtfmt, dfmt}; + SimpleDateFormat[] fmts = { fullfmt, dtfmt, dfmt }; for (SimpleDateFormat fmt : fmts) { try { check = fmt.parse(sa); @@ -85,8 +88,33 @@ public class DateConverter extends DefaultTypeConverter { } catch (ParseException ignore) { } } + } else if (java.time.LocalDateTime.class == toType) { + DateTimeFormatter dtf = null; + TemporalAccessor check; + DateTimeFormatter[] dfs = getDateTimeFormats(ActionContext.of(context), locale); + + for (DateTimeFormatter df1 : dfs) { + try { + check = df1.parse(sa); + dtf = df1; + if (check != null) { + break; + } + } catch (DateTimeParseException ignore) { + } + } + if (dtf == null) { + throw new TypeConversionException("Could not parse date"); + } else { + try { + return LocalDateTime.parse(sa, dtf); + } catch (DateTimeParseException e) { + throw new TypeConversionException("Could not parse date", e); + } + } } - //final fallback for dates without time + + // final fallback for dates without time if (df == null) { df = DateFormat.getDateInstance(DateFormat.SHORT, locale); } @@ -95,15 +123,17 @@ public class DateConverter extends DefaultTypeConverter { result = df.parse(sa); if (!(Date.class == toType)) { try { - Constructor<?> constructor = toType.getConstructor(new Class[]{long.class}); - return constructor.newInstance(new Object[]{Long.valueOf(result.getTime())}); + Constructor<?> constructor = toType.getConstructor(new Class[] { long.class }); + return constructor.newInstance(new Object[] { Long.valueOf(result.getTime()) }); } catch (Exception e) { - throw new TypeConversionException("Couldn't create class " + toType + " using default (long) constructor", e); + throw new TypeConversionException( + "Couldn't create class " + toType + " using default (long) constructor", e); } } } catch (ParseException e) { throw new TypeConversionException("Could not parse date", e); } + } else if (Date.class.isAssignableFrom(value.getClass())) { result = (Date) value; } @@ -111,16 +141,16 @@ public class DateConverter extends DefaultTypeConverter { } /** - * The user defined global date format, - * see {@link org.apache.struts2.components.Date#DATETAG_PROPERTY} + * The user defined global date format, see + * {@link org.apache.struts2.components.Date#DATETAG_PROPERTY} * * @param context current ActionContext - * @param locale current Locale to convert to - * @return defined global format + * + * @return defined global date string format */ - protected DateFormat getGlobalDateFormat(ActionContext context, Locale locale) { + protected String getGlobalDateString(ActionContext context) { final String dateTagProperty = org.apache.struts2.components.Date.DATETAG_PROPERTY; - SimpleDateFormat globalDateFormat = null; + String globalDateString = null; final TextProvider tp = findProviderInStack(context.getValueStack()); @@ -130,23 +160,29 @@ public class DateConverter extends DefaultTypeConverter { // is the same as input = DATETAG_PROPERTY if (globalFormat != null && !dateTagProperty.equals(globalFormat)) { LOG.debug("Found \"{}\" as \"{}\"", dateTagProperty, globalFormat); - globalDateFormat = new SimpleDateFormat(globalFormat, locale); + globalDateString = globalFormat; } else { LOG.debug("\"{}\" has not been defined, ignoring it", dateTagProperty); } } - return globalDateFormat; + return globalDateString; } /** * Retrieves the list of date formats to be used when converting dates + * * @param context the current ActionContext - * @param locale the current locale of the action + * @param locale the current locale of the action * @return a list of DateFormat to be used for date conversion */ private DateFormat[] getDateFormats(ActionContext context, Locale locale) { - DateFormat globalDateFormat = getGlobalDateFormat(context, locale); + DateFormat globalDateFormat = null; + String globalFormat = getGlobalDateString(context); + // + if (globalFormat != null) { + globalDateFormat = new SimpleDateFormat(globalFormat, locale); + } DateFormat dt1 = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.LONG, locale); DateFormat dt2 = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM, locale); @@ -156,20 +192,37 @@ public class DateConverter extends DefaultTypeConverter { DateFormat d2 = DateFormat.getDateInstance(DateFormat.MEDIUM, locale); DateFormat d3 = DateFormat.getDateInstance(DateFormat.LONG, locale); - DateFormat rfc3339 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); + DateFormat rfc3339 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); DateFormat rfc3339dateOnly = new SimpleDateFormat("yyyy-MM-dd"); final DateFormat[] dateFormats; if (globalDateFormat == null) { - dateFormats = new DateFormat[]{dt1, dt2, dt3, rfc3339, d1, d2, d3, rfc3339dateOnly}; + dateFormats = new DateFormat[] { dt1, dt2, dt3, rfc3339, d1, d2, d3, rfc3339dateOnly }; } else { - dateFormats = new DateFormat[]{globalDateFormat, dt1, dt2, dt3, rfc3339, d1, d2, d3, rfc3339dateOnly}; + dateFormats = new DateFormat[] { globalDateFormat, dt1, dt2, dt3, rfc3339, d1, d2, d3, rfc3339dateOnly }; } return dateFormats; } + /** + * Retrieves the list of date time formats to be used when converting dates + * + * @param context the current ActionContext + * @param locale the current locale of the action + * + * @return a list of DateTimeFormatter to be used for date conversion + */ + protected DateTimeFormatter[] getDateTimeFormats(ActionContext context, Locale locale) { + + DateTimeFormatter df1 = DateTimeFormatter.ISO_LOCAL_DATE_TIME; + + final DateTimeFormatter[] dateFormats = new DateTimeFormatter[] { df1 }; + + return dateFormats; + } + private TextProvider findProviderInStack(ValueStack stack) { // TODO: ValueStack will never be null, this is just a workaround for tests if (stack == null) { diff --git a/core/src/main/java/com/opensymphony/xwork2/conversion/impl/XWorkBasicConverter.java b/core/src/main/java/com/opensymphony/xwork2/conversion/impl/XWorkBasicConverter.java index d415a8d1a..3c9eb3ad1 100644 --- a/core/src/main/java/com/opensymphony/xwork2/conversion/impl/XWorkBasicConverter.java +++ b/core/src/main/java/com/opensymphony/xwork2/conversion/impl/XWorkBasicConverter.java @@ -25,6 +25,7 @@ import com.opensymphony.xwork2.inject.Inject; import org.apache.struts2.StrutsConstants; import java.lang.reflect.Member; +import java.time.LocalDateTime; import java.util.Calendar; import java.util.Collection; import java.util.Date; @@ -99,6 +100,8 @@ public class XWorkBasicConverter extends DefaultTypeConverter { result = doConvertToArray(context, o, member, propertyName, value, toType); } else if (Date.class.isAssignableFrom(toType)) { result = doConvertToDate(context, value, toType); + } else if (LocalDateTime.class.isAssignableFrom(toType)) { + result = doConvertToDate(context, value, toType); } else if (Calendar.class.isAssignableFrom(toType)) { result = doConvertToCalendar(context, value); } else if (Collection.class.isAssignableFrom(toType)) { diff --git a/core/src/main/java/com/opensymphony/xwork2/interceptor/ParametersInterceptor.java b/core/src/main/java/com/opensymphony/xwork2/interceptor/ParametersInterceptor.java index 69b717c6c..781a8f3e5 100644 --- a/core/src/main/java/com/opensymphony/xwork2/interceptor/ParametersInterceptor.java +++ b/core/src/main/java/com/opensymphony/xwork2/interceptor/ParametersInterceptor.java @@ -201,7 +201,7 @@ public class ParametersInterceptor extends MethodFilterInterceptor { ReflectionContextState.setReportingConversionErrors(context, true); //keep locale from original context - newStack.getActionContext().withLocale(stack.getActionContext().getLocale()); + newStack.getActionContext().withLocale(stack.getActionContext().getLocale()).withValueStack(stack); } boolean memberAccessStack = newStack instanceof MemberAccessValueStack; diff --git a/core/src/test/java/com/opensymphony/xwork2/conversion/impl/DateConverterTest.java b/core/src/test/java/com/opensymphony/xwork2/conversion/impl/DateConverterTest.java index 774958365..fa954661e 100644 --- a/core/src/test/java/com/opensymphony/xwork2/conversion/impl/DateConverterTest.java +++ b/core/src/test/java/com/opensymphony/xwork2/conversion/impl/DateConverterTest.java @@ -28,6 +28,7 @@ import org.apache.struts2.conversion.TypeConversionException; import java.sql.Time; import java.sql.Timestamp; import java.text.DateFormat; +import java.time.LocalDateTime; import java.util.Date; import java.util.HashMap; import java.util.Locale; @@ -43,6 +44,9 @@ public class DateConverterTest extends StrutsInternalTestCase { private final static String DATE_STR = "2020-03-20"; private final static String DATE_CONVERTED = "Fri Mar 20 00:00:00"; private final static String INVALID_DATE = "99/99/2010"; + private final static String LOCALDATETIME_STR = "2020-03-20T00:00:00.000000"; + private final static String LOCALDATETIME_CONVERTED = "2020-03-20T00:00"; + private final static String INVALID_LOCALDATETIME = "2010-99-99T00:00"; private final static String MESSAGE_PARSE_ERROR = "Could not parse date"; private final static String MESSAGE_DEFAULT_CONSTRUCTOR_ERROR = "Couldn't create class null using default (long) constructor"; @@ -118,6 +122,43 @@ public class DateConverterTest extends StrutsInternalTestCase { } } + public void testLocalDateTimeType() { + DateConverter converter = new DateConverter(); + + Map<String, String> map = new HashMap<>(); + map.put(org.apache.struts2.components.Date.DATETAG_PROPERTY, "yyyy-MM-dd"); + ValueStack stack = new StubValueStack(); + stack.push(new StubTextProvider(map)); + + ActionContext context = ActionContext.of(new HashMap<>()) + .withLocale(new Locale("es_MX", "MX")) + .withValueStack(stack); + + Object value = converter.convertValue(context.getContextMap(), null, null, null, LOCALDATETIME_STR, LocalDateTime.class); + assertTrue(value.toString().startsWith(LOCALDATETIME_CONVERTED)); + } + + public void testLocalDateTimeTypeConversionExceptionWhenParseError() { + DateConverter converter = new DateConverter(); + + Map<String, String> map = new HashMap<>(); + map.put(org.apache.struts2.components.Date.DATETAG_PROPERTY, "yyyy-MM-dd"); + ValueStack stack = new StubValueStack(); + stack.push(new StubTextProvider(map)); + + ActionContext context = ActionContext.of(new HashMap<>()) + .withLocale(new Locale("es_MX", "MX")) + .withValueStack(stack); + + try { + converter.convertValue(context.getContextMap(), null, null, null, INVALID_LOCALDATETIME, LocalDateTime.class); + fail("TypeConversionException expected - Conversion error occurred"); + } catch (Exception ex) { + assertEquals(TypeConversionException.class, ex.getClass()); + assertEquals(MESSAGE_PARSE_ERROR, ex.getMessage()); + } + } + @Override protected void setUp() throws Exception { super.setUp();