Skip to content
Merged
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 @@ -30,6 +30,7 @@ public class JSONConstants {

public static final String JSON_WRITER = "struts.json.writer";
public static final String JSON_READER = "struts.json.reader";
public static final String JSON_WRITER_USE_TYPE_CONVERTERS = "struts.json.writer.useTypeConverters";
public static final String RESULT_EXCLUDE_PROXY_PROPERTIES = "struts.json.result.excludeProxyProperties";
public static final String DATE_FORMAT = "struts.json.dateformat";
public static final String JSON_MAX_ELEMENTS = "struts.json.maxElements";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.ActionContext;
import org.apache.struts2.conversion.TypeConverter;
import org.apache.struts2.conversion.impl.XWorkConverter;
import org.apache.struts2.inject.Inject;
import org.apache.struts2.json.annotations.JSON;
import org.apache.struts2.json.annotations.JSONFieldBridge;
Expand All @@ -32,6 +35,7 @@
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.File;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
Expand Down Expand Up @@ -103,12 +107,28 @@ public static void clearBeanInfoCaches() {
private boolean excludeNullProperties;
private boolean excludeProxyProperties;
private ProxyService proxyService;
private XWorkConverter xworkConverter;
private boolean useTypeConverters;

@Inject
public void setProxyService(ProxyService proxyService) {
this.proxyService = proxyService;
}

@Inject
public void setXWorkConverter(XWorkConverter xworkConverter) {
this.xworkConverter = xworkConverter;
}

@Inject(value = JSONConstants.JSON_WRITER_USE_TYPE_CONVERTERS, required = false)
public void setUseTypeConverters(String useTypeConverters) {
setUseTypeConverters(Boolean.parseBoolean(useTypeConverters));
}

public void setUseTypeConverters(boolean useTypeConverters) {
this.useTypeConverters = useTypeConverters;
}

@Inject(value = JSONConstants.RESULT_EXCLUDE_PROXY_PROPERTIES, required = false)
public void setExcludeProxyProperties(String excludeProxyProperties) {
setExcludeProxyProperties(Boolean.parseBoolean(excludeProxyProperties));
Expand Down Expand Up @@ -220,13 +240,48 @@ protected void process(Object object, Method method) throws JSONException {
this.string(object);
} else if (object instanceof Enum<?> enumValue) {
this.enumeration(enumValue);
} else {
processObject(object, method);
}

this.stack.pop();
}

private void processObject(Object object, Method method) throws JSONException {
String converted = this.convertToString(object);
if (converted != null) {
this.string(converted);
} else if (object.getClass().isRecord()) {
this.record(object);
} else {
processCustom(object, method);
}
}

this.stack.pop();
/**
* Converts the object with a type converter registered for its class, when
* {@link JSONConstants#JSON_WRITER_USE_TYPE_CONVERTERS} is enabled.
*
* @param object object
* @return converted value, or null when no registered converter produced a String
*/
protected String convertToString(Object object) {
if (!useTypeConverters || xworkConverter == null || object instanceof File) {
return null;
}
TypeConverter typeConverter = xworkConverter.lookup(object.getClass());
if (typeConverter == null) {
return null;
}
try {
ActionContext actionContext = ActionContext.getContext();
Map<String, Object> context = actionContext != null ? actionContext.getContextMap() : new HashMap<>();
Object converted = typeConverter.convertValue(context, null, null, null, object, String.class);
return converted instanceof String string ? string : null;
} catch (RuntimeException e) {
LOG.debug("Type converter {} failed to convert {}, serializing it as a bean", typeConverter, object.getClass(), e);
return null;
}
}

/**
Expand Down
1 change: 1 addition & 0 deletions plugins/json/src/main/resources/struts-plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@

<constant name="struts.json.writer" value="struts"/>
<constant name="struts.json.reader" value="struts"/>
<constant name="struts.json.writer.useTypeConverters" value="false"/>
<constant name="struts.json.maxElements" value="10000"/>
<constant name="struts.json.maxDepth" value="64"/>
<constant name="struts.json.maxLength" value="2097152"/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.struts2.json;

import org.apache.struts2.conversion.TypeConversionException;
import org.apache.struts2.conversion.impl.XWorkConverter;
import org.apache.struts2.json.annotations.JSONFieldBridge;
import org.apache.struts2.json.bridge.FieldBridge;
import org.apache.struts2.junit.StrutsTestCase;
import org.apache.struts2.util.StrutsTypeConverter;

import java.io.File;
import java.util.Calendar;
import java.util.Date;
import java.util.Map;

public class StrutsJSONWriterTypeConverterTest extends StrutsTestCase {

private XWorkConverter converter;
private StrutsJSONWriter writer;

@Override
protected void setUp() throws Exception {
super.setUp();
converter = container.getInstance(XWorkConverter.class);
writer = (StrutsJSONWriter) container.getInstance(JSONWriter.class);
writer.setUseTypeConverters(true);
}

public void testDisabledByDefault() throws Exception {
assertEquals("false", container.getInstance(String.class, JSONConstants.JSON_WRITER_USE_TYPE_CONVERTERS));
converter.registerConverter(Money.class.getName(), new MoneyConverter());

String json = container.getInstance(JSONWriter.class).write(new Money("12.50", "EUR"));

assertEquals("{\"amount\":\"12.50\",\"currency\":\"EUR\"}", json);
}

public void testRegisteredConverterWritesString() throws Exception {
converter.registerConverter(Money.class.getName(), new MoneyConverter());

assertEquals("\"12.50 EUR\"", writer.write(new Money("12.50", "EUR")));
}

public void testConverterAppliesToNestedProperty() throws Exception {
converter.registerConverter(Money.class.getName(), new MoneyConverter());

assertEquals("{\"price\":\"12.50 EUR\"}", writer.write(new Order(new Money("12.50", "EUR"))));
}

public void testConverterRegisteredForInterface() throws Exception {
converter.registerConverter(Identifier.class.getName(), new IdentifierConverter());

assertEquals("\"ID-42\"", writer.write(new OrderId(42)));
}

public void testConverterRegisteredForRecord() throws Exception {
converter.registerConverter(Temperature.class.getName(), new TemperatureConverter());

assertEquals("\"21.5C\"", writer.write(new Temperature(21.5)));
}

public void testWithoutConverterWritesBean() throws Exception {
assertEquals("{\"amount\":\"12.50\",\"currency\":\"EUR\"}", writer.write(new Money("12.50", "EUR")));
}

public void testFieldBridgeWinsOverConverter() throws Exception {
converter.registerConverter(Money.class.getName(), new MoneyConverter());

assertEquals("{\"price\":\"bridged\"}", writer.write(new BridgedOrder(new Money("12.50", "EUR"))));
}

public void testFailingConverterFallsBackToBean() throws Exception {
converter.registerConverter(Money.class.getName(), new FailingConverter());

assertEquals("{\"amount\":\"12.50\",\"currency\":\"EUR\"}", writer.write(new Money("12.50", "EUR")));
}

public void testBuiltInTypesIgnoreRegisteredConverters() throws Exception {
converter.registerConverter(Date.class.getName(), new ConstantConverter());
converter.registerConverter(Integer.class.getName(), new ConstantConverter());
writer.setDateFormatter("yyyy");
Calendar calendar = Calendar.getInstance();
calendar.set(2026, Calendar.OCTOBER, 10);

assertEquals("\"2026\"", writer.write(calendar.getTime()));
assertEquals("7", writer.write(7));
}

public void testUploadedFileConverterIsNotUsedForFiles() throws Exception {
String json = writer.write(new File("upload.txt"));

assertTrue(json, json.startsWith("{"));
}

public static class Money {
private final String amount;
private final String currency;

public Money(String amount, String currency) {
this.amount = amount;
this.currency = currency;
}

public String getAmount() {
return amount;
}

public String getCurrency() {
return currency;
}
}

public static class Order {
private final Money price;

public Order(Money price) {
this.price = price;
}

public Money getPrice() {
return price;
}
}

public static class BridgedOrder {
private final Money price;

public BridgedOrder(Money price) {
this.price = price;
}

@JSONFieldBridge(impl = ConstantBridge.class)
public Money getPrice() {
return price;
}
}

public static class ConstantBridge implements FieldBridge {
@Override
public String objectToString(Object object) {
return "bridged";
}
}

public interface Identifier {
long getValue();
}

public static class OrderId implements Identifier {
private final long value;

public OrderId(long value) {
this.value = value;
}

@Override
public long getValue() {
return value;
}
}

public record Temperature(double celsius) {
}

public static class MoneyConverter extends StrutsTypeConverter {
@Override
public Object convertFromString(Map context, String[] values, Class toClass) {
throw new UnsupportedOperationException();
}

@Override
public String convertToString(Map context, Object o) {
Money money = (Money) o;
return money.getAmount() + " " + money.getCurrency();
}
}

public static class IdentifierConverter extends StrutsTypeConverter {
@Override
public Object convertFromString(Map context, String[] values, Class toClass) {
throw new UnsupportedOperationException();
}

@Override
public String convertToString(Map context, Object o) {
return "ID-" + ((Identifier) o).getValue();
}
}

public static class TemperatureConverter extends StrutsTypeConverter {
@Override
public Object convertFromString(Map context, String[] values, Class toClass) {
throw new UnsupportedOperationException();
}

@Override
public String convertToString(Map context, Object o) {
return ((Temperature) o).celsius() + "C";
}
}

public static class ConstantConverter extends StrutsTypeConverter {
@Override
public Object convertFromString(Map context, String[] values, Class toClass) {
throw new UnsupportedOperationException();
}

@Override
public String convertToString(Map context, Object o) {
return "converted";
}
}

public static class FailingConverter extends StrutsTypeConverter {
@Override
public Object convertFromString(Map context, String[] values, Class toClass) {
throw new UnsupportedOperationException();
}

@Override
public String convertToString(Map context, Object o) {
throw new TypeConversionException("cannot convert " + o);
}
}
}
Loading