Skip to content

Commit 7e91274

Browse files
gregh3269Greg HuberGreg Huber
authored
Basic LocalDateTime support (#532)
* Basic LocalDateTime support * Basic LocalDateTime support * Basic LocalDateTime support * Basic LocalDateTime support * Basic LocalDateTime support Co-authored-by: Greg Huber <[email protected]> Co-authored-by: Greg Huber <[email protected]>
1 parent c98b60c commit 7e91274

File tree

4 files changed

+130
-33
lines changed

4 files changed

+130
-33
lines changed

core/src/main/java/com/opensymphony/xwork2/conversion/impl/DateConverter.java

Lines changed: 85 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -18,28 +18,34 @@
1818
*/
1919
package com.opensymphony.xwork2.conversion.impl;
2020

21-
import org.apache.logging.log4j.LogManager;
22-
import org.apache.logging.log4j.Logger;
23-
import org.apache.struts2.conversion.TypeConversionException;
24-
import com.opensymphony.xwork2.ActionContext;
25-
import com.opensymphony.xwork2.TextProvider;
26-
import com.opensymphony.xwork2.util.ValueStack;
27-
2821
import java.lang.reflect.Constructor;
2922
import java.lang.reflect.Member;
3023
import java.text.DateFormat;
3124
import java.text.ParseException;
3225
import java.text.SimpleDateFormat;
26+
import java.time.LocalDateTime;
27+
import java.time.format.DateTimeFormatter;
28+
import java.time.format.DateTimeParseException;
29+
import java.time.temporal.TemporalAccessor;
3330
import java.util.Date;
3431
import java.util.Locale;
3532
import java.util.Map;
3633

34+
import org.apache.logging.log4j.LogManager;
35+
import org.apache.logging.log4j.Logger;
36+
import org.apache.struts2.conversion.TypeConversionException;
37+
38+
import com.opensymphony.xwork2.ActionContext;
39+
import com.opensymphony.xwork2.TextProvider;
40+
import com.opensymphony.xwork2.util.ValueStack;
41+
3742
public class DateConverter extends DefaultTypeConverter {
3843

3944
private final static Logger LOG = LogManager.getLogger(DateConverter.class);
4045

4146
@Override
42-
public Object convertValue(Map<String, Object> context, Object target, Member member, String propertyName, Object value, Class toType) {
47+
public Object convertValue(Map<String, Object> context, Object target, Member member, String propertyName,
48+
Object value, Class toType) {
4349
Date result = null;
4450

4551
if (value instanceof String && ((String) value).length() > 0) {
@@ -52,15 +58,12 @@ public Object convertValue(Map<String, Object> context, Object target, Member me
5258
} else if (java.sql.Timestamp.class == toType) {
5359
Date check = null;
5460
SimpleDateFormat dtfmt = (SimpleDateFormat) DateFormat.getDateTimeInstance(DateFormat.SHORT,
55-
DateFormat.MEDIUM,
56-
locale);
57-
SimpleDateFormat fullfmt = new SimpleDateFormat(dtfmt.toPattern() + MILLISECOND_FORMAT,
58-
locale);
61+
DateFormat.MEDIUM, locale);
62+
SimpleDateFormat fullfmt = new SimpleDateFormat(dtfmt.toPattern() + MILLISECOND_FORMAT, locale);
5963

60-
SimpleDateFormat dfmt = (SimpleDateFormat) DateFormat.getDateInstance(DateFormat.SHORT,
61-
locale);
64+
SimpleDateFormat dfmt = (SimpleDateFormat) DateFormat.getDateInstance(DateFormat.SHORT, locale);
6265

63-
SimpleDateFormat[] fmts = {fullfmt, dtfmt, dfmt};
66+
SimpleDateFormat[] fmts = { fullfmt, dtfmt, dfmt };
6467
for (SimpleDateFormat fmt : fmts) {
6568
try {
6669
check = fmt.parse(sa);
@@ -85,8 +88,33 @@ public Object convertValue(Map<String, Object> context, Object target, Member me
8588
} catch (ParseException ignore) {
8689
}
8790
}
91+
} else if (java.time.LocalDateTime.class == toType) {
92+
DateTimeFormatter dtf = null;
93+
TemporalAccessor check;
94+
DateTimeFormatter[] dfs = getDateTimeFormats(ActionContext.of(context), locale);
95+
96+
for (DateTimeFormatter df1 : dfs) {
97+
try {
98+
check = df1.parse(sa);
99+
dtf = df1;
100+
if (check != null) {
101+
break;
102+
}
103+
} catch (DateTimeParseException ignore) {
104+
}
105+
}
106+
if (dtf == null) {
107+
throw new TypeConversionException("Could not parse date");
108+
} else {
109+
try {
110+
return LocalDateTime.parse(sa, dtf);
111+
} catch (DateTimeParseException e) {
112+
throw new TypeConversionException("Could not parse date", e);
113+
}
114+
}
88115
}
89-
//final fallback for dates without time
116+
117+
// final fallback for dates without time
90118
if (df == null) {
91119
df = DateFormat.getDateInstance(DateFormat.SHORT, locale);
92120
}
@@ -95,32 +123,34 @@ public Object convertValue(Map<String, Object> context, Object target, Member me
95123
result = df.parse(sa);
96124
if (!(Date.class == toType)) {
97125
try {
98-
Constructor<?> constructor = toType.getConstructor(new Class[]{long.class});
99-
return constructor.newInstance(new Object[]{Long.valueOf(result.getTime())});
126+
Constructor<?> constructor = toType.getConstructor(new Class[] { long.class });
127+
return constructor.newInstance(new Object[] { Long.valueOf(result.getTime()) });
100128
} catch (Exception e) {
101-
throw new TypeConversionException("Couldn't create class " + toType + " using default (long) constructor", e);
129+
throw new TypeConversionException(
130+
"Couldn't create class " + toType + " using default (long) constructor", e);
102131
}
103132
}
104133
} catch (ParseException e) {
105134
throw new TypeConversionException("Could not parse date", e);
106135
}
136+
107137
} else if (Date.class.isAssignableFrom(value.getClass())) {
108138
result = (Date) value;
109139
}
110140
return result;
111141
}
112142

113143
/**
114-
* The user defined global date format,
115-
* see {@link org.apache.struts2.components.Date#DATETAG_PROPERTY}
144+
* The user defined global date format, see
145+
* {@link org.apache.struts2.components.Date#DATETAG_PROPERTY}
116146
*
117147
* @param context current ActionContext
118-
* @param locale current Locale to convert to
119-
* @return defined global format
148+
*
149+
* @return defined global date string format
120150
*/
121-
protected DateFormat getGlobalDateFormat(ActionContext context, Locale locale) {
151+
protected String getGlobalDateString(ActionContext context) {
122152
final String dateTagProperty = org.apache.struts2.components.Date.DATETAG_PROPERTY;
123-
SimpleDateFormat globalDateFormat = null;
153+
String globalDateString = null;
124154

125155
final TextProvider tp = findProviderInStack(context.getValueStack());
126156

@@ -130,23 +160,29 @@ protected DateFormat getGlobalDateFormat(ActionContext context, Locale locale) {
130160
// is the same as input = DATETAG_PROPERTY
131161
if (globalFormat != null && !dateTagProperty.equals(globalFormat)) {
132162
LOG.debug("Found \"{}\" as \"{}\"", dateTagProperty, globalFormat);
133-
globalDateFormat = new SimpleDateFormat(globalFormat, locale);
163+
globalDateString = globalFormat;
134164
} else {
135165
LOG.debug("\"{}\" has not been defined, ignoring it", dateTagProperty);
136166
}
137167
}
138168

139-
return globalDateFormat;
169+
return globalDateString;
140170
}
141171

142172
/**
143173
* Retrieves the list of date formats to be used when converting dates
174+
*
144175
* @param context the current ActionContext
145-
* @param locale the current locale of the action
176+
* @param locale the current locale of the action
146177
* @return a list of DateFormat to be used for date conversion
147178
*/
148179
private DateFormat[] getDateFormats(ActionContext context, Locale locale) {
149-
DateFormat globalDateFormat = getGlobalDateFormat(context, locale);
180+
DateFormat globalDateFormat = null;
181+
String globalFormat = getGlobalDateString(context);
182+
//
183+
if (globalFormat != null) {
184+
globalDateFormat = new SimpleDateFormat(globalFormat, locale);
185+
}
150186

151187
DateFormat dt1 = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.LONG, locale);
152188
DateFormat dt2 = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM, locale);
@@ -156,20 +192,37 @@ private DateFormat[] getDateFormats(ActionContext context, Locale locale) {
156192
DateFormat d2 = DateFormat.getDateInstance(DateFormat.MEDIUM, locale);
157193
DateFormat d3 = DateFormat.getDateInstance(DateFormat.LONG, locale);
158194

159-
DateFormat rfc3339 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
195+
DateFormat rfc3339 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
160196
DateFormat rfc3339dateOnly = new SimpleDateFormat("yyyy-MM-dd");
161197

162198
final DateFormat[] dateFormats;
163199

164200
if (globalDateFormat == null) {
165-
dateFormats = new DateFormat[]{dt1, dt2, dt3, rfc3339, d1, d2, d3, rfc3339dateOnly};
201+
dateFormats = new DateFormat[] { dt1, dt2, dt3, rfc3339, d1, d2, d3, rfc3339dateOnly };
166202
} else {
167-
dateFormats = new DateFormat[]{globalDateFormat, dt1, dt2, dt3, rfc3339, d1, d2, d3, rfc3339dateOnly};
203+
dateFormats = new DateFormat[] { globalDateFormat, dt1, dt2, dt3, rfc3339, d1, d2, d3, rfc3339dateOnly };
168204
}
169205

170206
return dateFormats;
171207
}
172208

209+
/**
210+
* Retrieves the list of date time formats to be used when converting dates
211+
*
212+
* @param context the current ActionContext
213+
* @param locale the current locale of the action
214+
*
215+
* @return a list of DateTimeFormatter to be used for date conversion
216+
*/
217+
protected DateTimeFormatter[] getDateTimeFormats(ActionContext context, Locale locale) {
218+
219+
DateTimeFormatter df1 = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
220+
221+
final DateTimeFormatter[] dateFormats = new DateTimeFormatter[] { df1 };
222+
223+
return dateFormats;
224+
}
225+
173226
private TextProvider findProviderInStack(ValueStack stack) {
174227
// TODO: ValueStack will never be null, this is just a workaround for tests
175228
if (stack == null) {

core/src/main/java/com/opensymphony/xwork2/conversion/impl/XWorkBasicConverter.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.apache.struts2.StrutsConstants;
2626

2727
import java.lang.reflect.Member;
28+
import java.time.LocalDateTime;
2829
import java.util.Calendar;
2930
import java.util.Collection;
3031
import java.util.Date;
@@ -99,6 +100,8 @@ public Object convertValue(Map<String, Object> context, Object o, Member member,
99100
result = doConvertToArray(context, o, member, propertyName, value, toType);
100101
} else if (Date.class.isAssignableFrom(toType)) {
101102
result = doConvertToDate(context, value, toType);
103+
} else if (LocalDateTime.class.isAssignableFrom(toType)) {
104+
result = doConvertToDate(context, value, toType);
102105
} else if (Calendar.class.isAssignableFrom(toType)) {
103106
result = doConvertToCalendar(context, value);
104107
} else if (Collection.class.isAssignableFrom(toType)) {

core/src/main/java/com/opensymphony/xwork2/interceptor/ParametersInterceptor.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ protected void setParameters(final Object action, ValueStack stack, HttpParamete
201201
ReflectionContextState.setReportingConversionErrors(context, true);
202202

203203
//keep locale from original context
204-
newStack.getActionContext().withLocale(stack.getActionContext().getLocale());
204+
newStack.getActionContext().withLocale(stack.getActionContext().getLocale()).withValueStack(stack);
205205
}
206206

207207
boolean memberAccessStack = newStack instanceof MemberAccessValueStack;

core/src/test/java/com/opensymphony/xwork2/conversion/impl/DateConverterTest.java

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import java.sql.Time;
2929
import java.sql.Timestamp;
3030
import java.text.DateFormat;
31+
import java.time.LocalDateTime;
3132
import java.util.Date;
3233
import java.util.HashMap;
3334
import java.util.Locale;
@@ -43,6 +44,9 @@ public class DateConverterTest extends StrutsInternalTestCase {
4344
private final static String DATE_STR = "2020-03-20";
4445
private final static String DATE_CONVERTED = "Fri Mar 20 00:00:00";
4546
private final static String INVALID_DATE = "99/99/2010";
47+
private final static String LOCALDATETIME_STR = "2020-03-20T00:00:00.000000";
48+
private final static String LOCALDATETIME_CONVERTED = "2020-03-20T00:00";
49+
private final static String INVALID_LOCALDATETIME = "2010-99-99T00:00";
4650
private final static String MESSAGE_PARSE_ERROR = "Could not parse date";
4751
private final static String MESSAGE_DEFAULT_CONSTRUCTOR_ERROR = "Couldn't create class null using default (long) constructor";
4852

@@ -118,6 +122,43 @@ public void testTypeConversionExceptionWhenUsingLongConstructor() {
118122
}
119123
}
120124

125+
public void testLocalDateTimeType() {
126+
DateConverter converter = new DateConverter();
127+
128+
Map<String, String> map = new HashMap<>();
129+
map.put(org.apache.struts2.components.Date.DATETAG_PROPERTY, "yyyy-MM-dd");
130+
ValueStack stack = new StubValueStack();
131+
stack.push(new StubTextProvider(map));
132+
133+
ActionContext context = ActionContext.of(new HashMap<>())
134+
.withLocale(new Locale("es_MX", "MX"))
135+
.withValueStack(stack);
136+
137+
Object value = converter.convertValue(context.getContextMap(), null, null, null, LOCALDATETIME_STR, LocalDateTime.class);
138+
assertTrue(value.toString().startsWith(LOCALDATETIME_CONVERTED));
139+
}
140+
141+
public void testLocalDateTimeTypeConversionExceptionWhenParseError() {
142+
DateConverter converter = new DateConverter();
143+
144+
Map<String, String> map = new HashMap<>();
145+
map.put(org.apache.struts2.components.Date.DATETAG_PROPERTY, "yyyy-MM-dd");
146+
ValueStack stack = new StubValueStack();
147+
stack.push(new StubTextProvider(map));
148+
149+
ActionContext context = ActionContext.of(new HashMap<>())
150+
.withLocale(new Locale("es_MX", "MX"))
151+
.withValueStack(stack);
152+
153+
try {
154+
converter.convertValue(context.getContextMap(), null, null, null, INVALID_LOCALDATETIME, LocalDateTime.class);
155+
fail("TypeConversionException expected - Conversion error occurred");
156+
} catch (Exception ex) {
157+
assertEquals(TypeConversionException.class, ex.getClass());
158+
assertEquals(MESSAGE_PARSE_ERROR, ex.getMessage());
159+
}
160+
}
161+
121162
@Override
122163
protected void setUp() throws Exception {
123164
super.setUp();

0 commit comments

Comments
 (0)