mirror of
https://github.com/apache/struts.git
synced 2026-08-05 06:36:58 +00:00
WW-5652 Add support for Java records and Optional serialization in the JSON plugin. (#1792)
* feat(json): support serialization of Java records in JSON processing * feat(json): add support for serializing Optional values in JSON processing
This commit is contained in:
committed by
GitHub
parent
963a81f43d
commit
60bc7b8d7a
@@ -35,6 +35,7 @@ import java.beans.PropertyDescriptor;
|
||||
import java.lang.reflect.Array;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.RecordComponent;
|
||||
import java.text.CharacterIterator;
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
@@ -57,6 +58,7 @@ import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.regex.Pattern;
|
||||
@@ -160,6 +162,11 @@ public class StrutsJSONWriter implements JSONWriter {
|
||||
return;
|
||||
}
|
||||
|
||||
if (object instanceof Optional<?> optional) {
|
||||
this.value(optional.orElse(null), method);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.stack.contains(object)) {
|
||||
Class<?> clazz = object.getClass();
|
||||
|
||||
@@ -213,6 +220,8 @@ public class StrutsJSONWriter implements JSONWriter {
|
||||
this.string(object);
|
||||
} else if (object instanceof Enum<?> enumValue) {
|
||||
this.enumeration(enumValue);
|
||||
} else if (object.getClass().isRecord()) {
|
||||
this.record(object);
|
||||
} else {
|
||||
processCustom(object, method);
|
||||
}
|
||||
@@ -305,6 +314,62 @@ public class StrutsJSONWriter implements JSONWriter {
|
||||
this.add("}");
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize a Java record by iterating its record components.
|
||||
*
|
||||
* @param object the record instance
|
||||
* @throws JSONException in case of error during serialize
|
||||
*/
|
||||
protected void record(Object object) throws JSONException {
|
||||
this.add("{");
|
||||
|
||||
try {
|
||||
boolean hasData = false;
|
||||
|
||||
for (RecordComponent component : object.getClass().getRecordComponents()) {
|
||||
String name = component.getName();
|
||||
Method accessor = component.getAccessor();
|
||||
|
||||
if (accessor.isAnnotationPresent(JSON.class)) {
|
||||
JSONAnnotationFinder jsonFinder = new JSONAnnotationFinder(accessor).invoke();
|
||||
|
||||
if (!jsonFinder.shouldSerialize()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (jsonFinder.getName() != null) {
|
||||
name = jsonFinder.getName();
|
||||
}
|
||||
}
|
||||
|
||||
String expr = null;
|
||||
|
||||
if (this.buildExpr) {
|
||||
expr = this.expandExpr(name);
|
||||
if (this.shouldExcludeProperty(expr)) continue;
|
||||
expr = this.setExprStack(expr);
|
||||
}
|
||||
|
||||
Object value = accessor.invoke(object);
|
||||
|
||||
if (accessor.isAnnotationPresent(JSONFieldBridge.class)) {
|
||||
value = getBridgedValue(accessor, value);
|
||||
}
|
||||
|
||||
boolean propertyPrinted = this.add(name, value, accessor, hasData);
|
||||
hasData = hasData || propertyPrinted;
|
||||
|
||||
if (this.buildExpr) {
|
||||
this.setExprStack(expr);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new JSONException(e);
|
||||
}
|
||||
|
||||
this.add("}");
|
||||
}
|
||||
|
||||
protected BeanInfo getBeanInfoIgnoreHierarchy(final Class<?> clazz) throws IntrospectionException {
|
||||
BeanInfo beanInfo = BEAN_INFO_CACHE_IGNORE_HIERARCHY.get(clazz);
|
||||
if (beanInfo != null) {
|
||||
@@ -443,11 +508,15 @@ public class StrutsJSONWriter implements JSONWriter {
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean isAbsent(Object value) {
|
||||
return value == null || (value instanceof Optional<?> opt && opt.isEmpty());
|
||||
}
|
||||
|
||||
/*
|
||||
* Add name/value pair to buffer
|
||||
*/
|
||||
protected boolean add(String name, Object value, Method method, boolean hasData) throws JSONException {
|
||||
if (excludeNullProperties && value == null) {
|
||||
if (excludeNullProperties && isAbsent(value)) {
|
||||
return false;
|
||||
}
|
||||
if (hasData) {
|
||||
@@ -472,7 +541,7 @@ public class StrutsJSONWriter implements JSONWriter {
|
||||
boolean hasData = false;
|
||||
while (it.hasNext()) {
|
||||
Map.Entry<?, ?> entry = (Map.Entry<?, ?>) it.next();
|
||||
if (excludeNullProperties && entry.getValue() == null) {
|
||||
if (excludeNullProperties && isAbsent(entry.getValue())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ import java.lang.annotation.Target;
|
||||
/*
|
||||
* Annotation used to customize serialization
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Target({ElementType.METHOD, ElementType.RECORD_COMPONENT})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface JSON {
|
||||
String name() default "";
|
||||
|
||||
@@ -26,7 +26,7 @@ import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Target(ElementType.METHOD)
|
||||
@Target({ElementType.METHOD, ElementType.RECORD_COMPONENT})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface JSONFieldBridge {
|
||||
Class<? extends FieldBridge> impl() default StringBridge.class;
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
*/
|
||||
package org.apache.struts2.json;
|
||||
|
||||
import org.apache.struts2.json.annotations.JSON;
|
||||
import org.apache.struts2.json.annotations.JSONFieldBridge;
|
||||
import org.apache.struts2.junit.util.TestUtils;
|
||||
import org.junit.Test;
|
||||
@@ -38,7 +39,9 @@ import java.util.Calendar;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.TimeZone;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
@@ -315,4 +318,160 @@ public class StrutsJSONWriterTest {
|
||||
assertTrue(json.contains("\"calendar\":\"2012-12-23T10:10:10\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSerializeSimpleRecord() throws Exception {
|
||||
record Person(String name, int age) {}
|
||||
Person r = new Person("Alice", 30);
|
||||
JSONWriter jsonWriter = new StrutsJSONWriter();
|
||||
String json = jsonWriter.write(r);
|
||||
assertEquals("{\"name\":\"Alice\",\"age\":30}", json);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSerializeRecordWithNullField() throws Exception {
|
||||
record RecordWithNullField(String name, String optional) {}
|
||||
RecordWithNullField r = new RecordWithNullField("Bob", null);
|
||||
JSONWriter jsonWriter = new StrutsJSONWriter();
|
||||
String withNullJson = jsonWriter.write(r);
|
||||
assertEquals("{\"name\":\"Bob\",\"optional\":null}", withNullJson);
|
||||
String nonNullJson = jsonWriter.write(r, null, null, true);
|
||||
assertEquals("{\"name\":\"Bob\"}", nonNullJson);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testSerializeNestedRecord() throws Exception {
|
||||
record InnerRecord(String label) {}
|
||||
record OuterRecord(String label, InnerRecord inner) {}
|
||||
OuterRecord r = new OuterRecord("outer", new InnerRecord("inner"));
|
||||
JSONWriter jsonWriter = new StrutsJSONWriter();
|
||||
String json = jsonWriter.write(r);
|
||||
assertEquals("{\"label\":\"outer\",\"inner\":{\"label\":\"inner\"}}", json);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSerializeRecordSkipsComponentAnnotatedWithSerializeFalse() throws Exception {
|
||||
record RecordWithAnnotatedComponent(@JSON(serialize = false) String secret, String visible) {}
|
||||
RecordWithAnnotatedComponent r = new RecordWithAnnotatedComponent("topsecret", "hello");
|
||||
JSONWriter jsonWriter = new StrutsJSONWriter();
|
||||
String json = jsonWriter.write(r);
|
||||
assertFalse("secret component should be excluded", json.contains("secret"));
|
||||
assertTrue(json.contains("\"visible\":\"hello\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSerializeRecordUsesRenamedComponentFromJsonAnnotation() throws Exception {
|
||||
record RecordWithRenamedComponent(@JSON(name = "fullName") String name, int age) {}
|
||||
RecordWithRenamedComponent r = new RecordWithRenamedComponent("Alice", 30);
|
||||
JSONWriter jsonWriter = new StrutsJSONWriter();
|
||||
String json = jsonWriter.write(r);
|
||||
assertFalse("original key 'name' should not appear", json.contains("\"name\""));
|
||||
assertTrue(json.contains("\"fullName\":\"Alice\""));
|
||||
assertTrue(json.contains("\"age\":30"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSerializeRecordWithListField() throws Exception {
|
||||
record RecordWithList(String title, List<String> tags) {}
|
||||
RecordWithList r = new RecordWithList("news", List.of("java", "records"));
|
||||
JSONWriter jsonWriter = new StrutsJSONWriter();
|
||||
String json = jsonWriter.write(r);
|
||||
assertEquals("{\"title\":\"news\",\"tags\":[\"java\",\"records\"]}", json);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSerializeRecordIncludePropertyFilter() throws Exception {
|
||||
record Person(String name, int age) {}
|
||||
Person r = new Person("Alice", 30);
|
||||
JSONWriter jsonWriter = new StrutsJSONWriter();
|
||||
List<Pattern> include = List.of(Pattern.compile("name"));
|
||||
String json = jsonWriter.write(r, null, include, false);
|
||||
assertTrue(json.contains("\"name\""));
|
||||
assertFalse(json.contains("\"age\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSerializeRecordExcludePropertyFilter() throws Exception {
|
||||
record Person(String name, int age) {}
|
||||
Person r = new Person("Alice", 30);
|
||||
JSONWriter jsonWriter = new StrutsJSONWriter();
|
||||
List<Pattern> exclude = List.of(Pattern.compile("age"));
|
||||
String json = jsonWriter.write(r, exclude, null, false);
|
||||
assertTrue(json.contains("\"name\""));
|
||||
assertFalse(json.contains("\"age\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSerializeRecordWithFieldBridge() throws Exception {
|
||||
record LinkRecord(@JSONFieldBridge URL homepage, String name) {}
|
||||
URL url = URI.create("https://www.google.com").toURL();
|
||||
LinkRecord r = new LinkRecord(url, "Struts");
|
||||
JSONWriter jsonWriter = new StrutsJSONWriter();
|
||||
String json = jsonWriter.write(r);
|
||||
assertTrue(json.contains("\"homepage\":\"https:\\/\\/www.google.com\""));
|
||||
assertTrue(json.contains("\"name\":\"Struts\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSerializeOptionalPresent() throws Exception {
|
||||
JSONWriter jsonWriter = new StrutsJSONWriter();
|
||||
assertEquals("\"hello\"", jsonWriter.write(Optional.of("hello")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSerializeOptionalEmpty() throws Exception {
|
||||
JSONWriter jsonWriter = new StrutsJSONWriter();
|
||||
assertEquals("null", jsonWriter.write(Optional.empty()));
|
||||
}
|
||||
|
||||
class BeanWithOptional {
|
||||
private String field;
|
||||
|
||||
public Optional<String> getField() {
|
||||
return Optional.ofNullable(field);
|
||||
}
|
||||
|
||||
public void setField(String field) {
|
||||
this.field = field;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSerializeBeanWithOptionalWithNull() throws Exception {
|
||||
BeanWithOptional bean = new BeanWithOptional();
|
||||
bean.setField(null);
|
||||
JSONWriter jsonWriter = new StrutsJSONWriter();
|
||||
String json = jsonWriter.write(bean);
|
||||
assertTrue(json.contains("\"field\":null"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSerializeBeanWithOptionalWithValue() throws Exception {
|
||||
BeanWithOptional bean = new BeanWithOptional();
|
||||
bean.setField("value");
|
||||
JSONWriter jsonWriter = new StrutsJSONWriter();
|
||||
String json = jsonWriter.write(bean);
|
||||
assertTrue(json.contains("\"field\":\"value\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMapWithOptionalEmptyValueExcludedWhenExcludeNullProperties() throws Exception {
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
map.put("present", Optional.of("hello"));
|
||||
map.put("absent", Optional.empty());
|
||||
JSONWriter jsonWriter = new StrutsJSONWriter();
|
||||
String json = jsonWriter.write(map, null, null, true);
|
||||
assertEquals("{\"present\":\"hello\"}", json);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMapWithOptionalEmptyValueIncludedAsNull() throws Exception {
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
map.put("present", Optional.of("hello"));
|
||||
map.put("absent", Optional.empty());
|
||||
JSONWriter jsonWriter = new StrutsJSONWriter();
|
||||
String json = jsonWriter.write(map, null, null, false);
|
||||
assertEquals("{\"present\":\"hello\",\"absent\":null}", json);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user