Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
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;
Expand All @@ -57,6 +58,7 @@
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;
Expand Down Expand Up @@ -225,6 +227,10 @@ protected void process(Object object, Method method) throws JSONException {
this.string(object);
} else if (object instanceof Enum<?> enumValue) {
this.enumeration(enumValue);
} else if (object instanceof Optional<?> optional) {
this.value(optional.orElse(null), method);
} else if (object.getClass().isRecord()) {
this.record(object);
} else {
processCustom(object, method);
}
Expand Down Expand Up @@ -318,6 +324,64 @@ protected void bean(Object object) throws JSONException {
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 {
WriteState state = WRITE_STATE.get();

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 (state.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 (state.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) {
Expand Down Expand Up @@ -459,11 +523,15 @@ protected boolean shouldExcludeProperty(String expr) {
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 (WRITE_STATE.get().excludeNullProperties && value == null) {
if (WRITE_STATE.get().excludeNullProperties && isAbsent(value)) {
return false;
}
if (hasData) {
Expand All @@ -489,7 +557,7 @@ protected void map(Map<?, ?> map, Method method) throws JSONException {
boolean hasData = false;
while (it.hasNext()) {
Map.Entry<?, ?> entry = (Map.Entry<?, ?>) it.next();
if (state.excludeNullProperties && entry.getValue() == null) {
if (state.excludeNullProperties && isAbsent(entry.getValue())) {
continue;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
/*
* Annotation used to customize serialization
*/
@Target(ElementType.METHOD)
@Target({ElementType.METHOD, ElementType.RECORD_COMPONENT})
@Retention(RetentionPolicy.RUNTIME)
public @interface JSON {
String name() default "";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -38,12 +39,14 @@
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.TimeZone;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
Expand Down Expand Up @@ -389,4 +392,160 @@ private static void await(CountDownLatch latch) {
}
}

@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);
}

}
Loading