Skip to content

Commit

Permalink
JacksonDataObjectMapper: Allow read/write constraints config
Browse files Browse the repository at this point in the history
Jackson's object mapper enforces some read/write constraints since
release 2.15. The maximum length for strings was limited to 20mb as
default. Since Scout dataobject mapper may be used with base64-encoded
binary data occasionally, 20mb may not be sufficient for each case.
This fix increases the default limit to 100mb and adds support to
modify all constraints using config properties.

See
eclipse-ee4j/jersey#5283
FasterXML/jackson-core#962
FasterXML/jackson-core#964

376418
  • Loading branch information
paolobazzi committed Mar 14, 2024
1 parent ec18051 commit 158dada
Show file tree
Hide file tree
Showing 2 changed files with 319 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,26 @@
import java.io.InputStream;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import org.eclipse.scout.rt.dataobject.DataObjectHelper;
import org.eclipse.scout.rt.dataobject.DoEntity;
import org.eclipse.scout.rt.dataobject.DoEntityBuilder;
import org.eclipse.scout.rt.dataobject.DoEntityHolder;
import org.eclipse.scout.rt.dataobject.IDataObjectMapper;
import org.eclipse.scout.rt.dataobject.IDoEntity;
import org.eclipse.scout.rt.dataobject.testing.TestingDataObjectHelper;
import org.eclipse.scout.rt.jackson.dataobject.JacksonDataObjectMapper.StreamReadConstraintsConfigProperty;
import org.eclipse.scout.rt.jackson.dataobject.JacksonDataObjectMapper.StreamWriteConstraintsConfigProperty;
import org.eclipse.scout.rt.jackson.dataobject.fixture.ITestBaseEntityDo;
import org.eclipse.scout.rt.jackson.dataobject.fixture.TestComplexEntityDo;
import org.eclipse.scout.rt.jackson.dataobject.fixture.TestCustomImplementedEntityDo;
Expand All @@ -40,11 +49,15 @@
import org.eclipse.scout.rt.platform.exception.PlatformException;
import org.eclipse.scout.rt.platform.util.Assertions.AssertionException;
import org.eclipse.scout.rt.platform.util.CloneUtility;
import org.eclipse.scout.rt.platform.util.CollectionUtility;
import org.eclipse.scout.rt.testing.platform.BeanTestingHelper;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import com.fasterxml.jackson.core.StreamReadConstraints;
import com.fasterxml.jackson.core.StreamReadConstraints.Builder;
import com.fasterxml.jackson.core.StreamWriteConstraints;
import com.fasterxml.jackson.databind.type.TypeFactory;

/**
Expand Down Expand Up @@ -210,4 +223,182 @@ public void testDisabledDefaultTyping() {
assertNull(m_mapper.getObjectMapper().getDeserializationConfig().getDefaultTyper(TypeFactory.defaultInstance().constructType(DoEntity.class)));
assertNull(m_mapper.getObjectMapper().getDeserializationConfig().getDefaultTyper(TypeFactory.defaultInstance().constructType(Object.class)));
}

protected final String m_longStringValue = IntStream.range(1, 10_0000).mapToObj(Integer::toString).collect(Collectors.joining());

/**
* NOTE: This test case just covers the current behavior of Jackson. This behavior is not enforced or verified by the
* Scout data object mapper and may change in a future release of Scout using a newer version of Jackson.
*/
@Test
public void testStreamReadConstraints_maxStringLength() {
// Jackson does not check string length for very short input streams
assertEquals("12345", runTestStreamReadConstraints(b -> b.maxStringLength(3), "{\"attribute\" : \"12345\"}").get("attribute"));

// Jackson checks string length correctly only for longer input streams
assertThrows(PlatformException.class, () -> runTestStreamReadConstraints(b -> b.maxStringLength(3), "{\"attribute\" : \"" + m_longStringValue + "\"}"));
}

/**
* NOTE: This test case just covers the current behavior of Jackson. This behavior is not enforced or verified by the
* Scout data object mapper and may change in a future release of Scout using a newer version of Jackson.
*/
@Test
public void testStreamReadConstraints_maxDocumentLength() {
// Jackson does not check document length for very short input streams
assertEquals("12345", runTestStreamReadConstraints(b -> b.maxDocumentLength(3), "{\"attribute\" : \"12345\"}").get("attribute"));

// Jackson checks document length correctly only for longer input streams
assertThrows(PlatformException.class, () -> runTestStreamReadConstraints(b -> b.maxDocumentLength(3), "{\"attribute\" : \"" + m_longStringValue + "\"}"));
}

/**
* NOTE: This test case just covers the current behavior of Jackson. This behavior is not enforced or verified by the
* Scout data object mapper and may change in a future release of Scout using a newer version of Jackson.
*/
@Test
public void testStreamReadConstraints_maxNumberLength() {
assertThrows(PlatformException.class, () -> runTestStreamReadConstraints(b -> b.maxNumberLength(3), "{\"attribute\" : 1234}"));
}

/**
* NOTE: This test case just covers the current behavior of Jackson. This behavior is not enforced or verified by the
* Scout data object mapper and may change in a future release of Scout using a newer version of Jackson.
*/
@Test
public void testStreamReadConstraints_maxNameLength() {
assertThrows(PlatformException.class, () -> runTestStreamReadConstraints(b -> b.maxNameLength(8), "{\"attribute\": 1234}"));
}

/**
* NOTE: This test case just covers the current behavior of Jackson. This behavior is not enforced or verified by the
* Scout data object mapper and may change in a future release of Scout using a newer version of Jackson.
*/
@Test
public void testStreamReadConstraints_maxNestingDepth() {
assertThrows(PlatformException.class, () -> runTestStreamReadConstraints(b -> b.maxNestingDepth(1), "{\"attribute\" : []}"));
}

/**
* NOTE: This test case just covers the current behavior of Jackson. This behavior is not enforced or verified by the
* Scout data object mapper and may change in a future release of Scout using a newer version of Jackson.
*/
@Test
public void testStreamReadConstraints_maxBigIntScale() {
// BigDecimal scale (100001) magnitude exceeds the maximum allowed (100000)
BigDecimal value = new BigDecimal("1").setScale(100_001, RoundingMode.UNNECESSARY);

// reading a big integer attribute which is given as very large decimal value in JSON
// See com.fasterxml.jackson.core.StreamReadConstraints#validateBigIntegerScale for fixed scale limit of 100k
String json = m_mapper.writeValue(BEANS.get(DoEntityBuilder.class).put("bigIntegerAttribute", value).build());
assertThrows(PlatformException.class, () -> runTestStreamReadConstraints(b -> b.maxNumberLength(100_002), json, TestComplexEntityDo.class));
}

protected DoEntity runTestStreamReadConstraints(Consumer<Builder> builderConsumer, String json) {
return runTestStreamReadConstraints(builderConsumer, json, DoEntity.class);
}

protected <T> T runTestStreamReadConstraints(Consumer<Builder> builderConsumer, String json, Class<T> expectedClass) {
Builder builder = StreamReadConstraints.builder();
builderConsumer.accept(builder);
IBean bean = BeanTestingHelper.get().mockConfigProperty(StreamReadConstraintsConfigProperty.class, builder.build());
try {
JacksonDataObjectMapper mapper = new JacksonDataObjectMapper(); // force new instance to apply config change
return mapper.readValue(json, expectedClass);
}
finally {
BeanTestingHelper.get().unregisterBean(bean);
}
}

@Test
public void testParseStreamReadConstraintsProperty_defaultValues() {
StreamReadConstraints constraints = BEANS.get(StreamReadConstraintsConfigProperty.class).parse(CollectionUtility.emptyHashMap());
assertEquals(1000, constraints.getMaxNestingDepth());
assertEquals(-1, constraints.getMaxDocumentLength());
assertEquals(50_000, constraints.getMaxNameLength());
assertEquals(1000, constraints.getMaxNumberLength());
assertEquals(100_000_000, constraints.getMaxStringLength());
}

@Test
public void testParseStreamReadConstraintsProperty_invalidKey() {
assertThrows(PlatformException.class, () -> BEANS.get(StreamReadConstraintsConfigProperty.class).parse(Map.of("foo", "bar")));
}

@Test
public void testParseStreamReadConstraintsProperty_values() {
StreamReadConstraints constraints = BEANS.get(StreamReadConstraintsConfigProperty.class).parse(Map.of(
StreamReadConstraintsConfigProperty.MAX_NESTING_DEPTH, "1",
StreamReadConstraintsConfigProperty.MAX_DOCUMENT_LENGTH, "2",
StreamReadConstraintsConfigProperty.MAX_NAME_LENGTH, "3",
StreamReadConstraintsConfigProperty.MAX_NUMBER_LENGTH, "4",
StreamReadConstraintsConfigProperty.MAX_STRING_LENGTH, "5"));

assertEquals(1, constraints.getMaxNestingDepth());
assertEquals(2, constraints.getMaxDocumentLength());
assertEquals(3, constraints.getMaxNameLength());
assertEquals(4, constraints.getMaxNumberLength());
assertEquals(5, constraints.getMaxStringLength());
}

@Test
public void testParseStreamReadConstraintsProperty_incompleteValues() {
StreamReadConstraints constraints = BEANS.get(StreamReadConstraintsConfigProperty.class).parse(Map.of(
StreamReadConstraintsConfigProperty.MAX_NESTING_DEPTH, "1",
StreamReadConstraintsConfigProperty.MAX_DOCUMENT_LENGTH, "2",
StreamReadConstraintsConfigProperty.MAX_NAME_LENGTH, "3"));

assertEquals(1, constraints.getMaxNestingDepth());
assertEquals(2, constraints.getMaxDocumentLength());
assertEquals(3, constraints.getMaxNameLength());
assertEquals(1000, constraints.getMaxNumberLength());
assertEquals(100_000_000, constraints.getMaxStringLength());
}

/**
* NOTE: This test case just covers the current behavior of Jackson. This behavior is not enforced or verified by the
* Scout data object mapper and may change in a future release of Scout using a newer version of Jackson.
*/
@Test
public void testStreamWriteConstraints_maxNestingDepth() {
IDoEntity entity = BEANS.get(DoEntityBuilder.class).put("attribute", "a").build();
assertEquals("{\"attribute\":\"a\"}", runTestStreamWriteConstraints(b -> b.maxNestingDepth(1), entity));

IDoEntity nestedEntity = BEANS.get(DoEntityBuilder.class).put("attribute", BEANS.get(DoEntityBuilder.class).put("attribute", "a").build()).build();
assertThrows(PlatformException.class, () -> runTestStreamWriteConstraints(b -> b.maxNestingDepth(1), nestedEntity));

IDoEntity listEntity = BEANS.get(DoEntityBuilder.class).putList("attribute", "a", "b").build();
assertThrows(PlatformException.class, () -> runTestStreamWriteConstraints(b -> b.maxNestingDepth(1), listEntity));
}

protected String runTestStreamWriteConstraints(Consumer<StreamWriteConstraints.Builder> builderConsumer, IDoEntity entity) {
StreamWriteConstraints.Builder builder = StreamWriteConstraints.builder();
builderConsumer.accept(builder);
IBean bean = BeanTestingHelper.get().mockConfigProperty(StreamWriteConstraintsConfigProperty.class, builder.build());
try {
JacksonDataObjectMapper mapper = new JacksonDataObjectMapper(); // force new instance to apply config change
return mapper.writeValue(entity);
}
finally {
BeanTestingHelper.get().unregisterBean(bean);
}
}

@Test
public void testParseStreamWriteConstraintsProperty_defaultValues() {
StreamWriteConstraints constraints = BEANS.get(StreamWriteConstraintsConfigProperty.class).parse(CollectionUtility.emptyHashMap());
assertEquals(1000, constraints.getMaxNestingDepth());
}

@Test
public void testParseStreamWriteConstraintsProperty_invalidKey() {
assertThrows(PlatformException.class, () -> BEANS.get(StreamWriteConstraintsConfigProperty.class).parse(Map.of("foo", "bar")));
}

@Test
public void testParseStreamWriteConstraintsProperty_values() {
StreamWriteConstraints constraints = BEANS.get(StreamWriteConstraintsConfigProperty.class).parse(Map.of(StreamReadConstraintsConfigProperty.MAX_NESTING_DEPTH, "1"));
assertEquals(1, constraints.getMaxNestingDepth());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,32 @@
import java.io.InputStream;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.eclipse.scout.rt.dataobject.IDataObject;
import org.eclipse.scout.rt.dataobject.IDataObjectMapper;
import org.eclipse.scout.rt.dataobject.IDoEntity;
import org.eclipse.scout.rt.dataobject.IValueFormatConstants;
import org.eclipse.scout.rt.platform.ApplicationScoped;
import org.eclipse.scout.rt.platform.BEANS;
import org.eclipse.scout.rt.platform.config.AbstractConfigProperty;
import org.eclipse.scout.rt.platform.config.CONFIG;
import org.eclipse.scout.rt.platform.config.ConfigUtility;
import org.eclipse.scout.rt.platform.exception.PlatformException;
import org.eclipse.scout.rt.platform.exception.PlatformExceptionTranslator;
import org.eclipse.scout.rt.platform.util.Assertions;
import org.eclipse.scout.rt.platform.util.LazyValue;
import org.eclipse.scout.rt.platform.util.ObjectUtility;
import org.eclipse.scout.rt.platform.util.TypeCastUtility;

import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.StreamReadConstraints;
import com.fasterxml.jackson.core.StreamWriteConstraints;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;

Expand Down Expand Up @@ -129,7 +143,12 @@ public ObjectMapper getObjectMapper() {
* Creates new {@link ObjectMapper} instance configured to be used with {@link IDoEntity}.
*/
protected ObjectMapper createObjectMapperInstance(boolean ignoreTypeAttribute) {
ObjectMapper om = new ObjectMapper();
// setup custom-configured JsonFactory used for ObjectMapper
JsonFactory jsonFactory = JsonFactory.builder()
.streamReadConstraints(CONFIG.getPropertyValue(StreamReadConstraintsConfigProperty.class))
.streamWriteConstraints(CONFIG.getPropertyValue(StreamWriteConstraintsConfigProperty.class))
.build();
ObjectMapper om = new ObjectMapper(jsonFactory);
ScoutDataObjectModule scoutDataObjectModule = BEANS.get(ScoutDataObjectModule.class).withIgnoreTypeAttribute(ignoreTypeAttribute);
prepareScoutDataModuleContext(scoutDataObjectModule.getModuleContext());
om.registerModule(scoutDataObjectModule);
Expand All @@ -146,4 +165,112 @@ protected ObjectMapper createObjectMapperInstance(boolean ignoreTypeAttribute) {
protected void prepareScoutDataModuleContext(ScoutDataObjectModuleContext moduleContext) {
// nop
}

/**
* {@link StreamReadConstraints} for {@link JsonFactory}.
*/
public static class StreamReadConstraintsConfigProperty extends AbstractConfigProperty<StreamReadConstraints, Map<String, String>> {

static final String MAX_NESTING_DEPTH = "maxNestingDepth";
static final String MAX_DOCUMENT_LENGTH = "maxDocumentLength";
static final String MAX_NAME_LENGTH = "maxNameLength";
static final String MAX_NUMBER_LENGTH = "maxNumberLength";
static final String MAX_STRING_LENGTH = "maxStringLength";

/**
* Default setting for maximum string length is 100 MB.<br>
* Jackson default value is {@link StreamReadConstraints#DEFAULT_MAX_STRING_LEN}.
*/
public static final int DEFAULT_MAX_STRING_LEN = 100_000_000;

@Override
public String getKey() {
return "scout.dataobject.jackson.streamReadConstraints";
}

@Override
public String description() {
return String.format("Jackson constraints to use for JSON reading.\n"
+ "Map property with the keys as follows:\n"
+ "- %s: Sets the maximum nesting depth. The depth is a count of objects and arrays that have not been closed, `{` and `[` respectively. (default: %d)\n"
+ "- %s: Sets the maximum allowed document length (for positive values over 0) or indicate that any length is acceptable (0 or negative number). The length is in input units of the input source, that is, in bytes or chars. (default: %d)\n"
+ "- %s: Sets the maximum name length (in chars or bytes, depending on input context). (default: %d)\n"
+ "- %s: Sets the maximum number length (in chars or bytes, depending on input context). (default: %d)\n"
+ "- %s: Sets the maximum string length for a single attribute value of type text (in chars or bytes, depending on input context). (default: %d)\n",
MAX_NESTING_DEPTH, StreamReadConstraints.DEFAULT_MAX_DEPTH,
MAX_DOCUMENT_LENGTH, StreamReadConstraints.DEFAULT_MAX_DOC_LEN,
MAX_NAME_LENGTH, StreamReadConstraints.DEFAULT_MAX_NAME_LEN,
MAX_NUMBER_LENGTH, StreamReadConstraints.DEFAULT_MAX_NUM_LEN,
MAX_STRING_LENGTH, DEFAULT_MAX_STRING_LEN);
}

@Override
public Map<String, String> readFromSource(String namespace) {
return ConfigUtility.getPropertyMap(getKey(), null, namespace);
}

@Override
public StreamReadConstraints getDefaultValue() {
return parse(Collections.emptyMap()); // defaults are on a per key base
}

@Override
protected StreamReadConstraints parse(Map<String, String> value) {
Set<String> invalidMapKeys = new HashSet<>(value.keySet());
Arrays.asList(MAX_NESTING_DEPTH, MAX_DOCUMENT_LENGTH, MAX_NAME_LENGTH, MAX_NUMBER_LENGTH, MAX_STRING_LENGTH).forEach(invalidMapKeys::remove);
if (!invalidMapKeys.isEmpty()) {
throw new PlatformException("Invalid values for map property {}: {}", getKey(), invalidMapKeys);
}
return StreamReadConstraints.builder()
.maxNestingDepth(ObjectUtility.nvl(TypeCastUtility.castValue(value.get(MAX_NESTING_DEPTH), Integer.class), StreamReadConstraints.DEFAULT_MAX_DEPTH))
.maxDocumentLength(ObjectUtility.nvl(TypeCastUtility.castValue(value.get(MAX_DOCUMENT_LENGTH), Long.class), StreamReadConstraints.DEFAULT_MAX_DOC_LEN))
.maxNameLength(ObjectUtility.nvl(TypeCastUtility.castValue(value.get(MAX_NAME_LENGTH), Integer.class), StreamReadConstraints.DEFAULT_MAX_NAME_LEN))
.maxNumberLength(ObjectUtility.nvl(TypeCastUtility.castValue(value.get(MAX_NUMBER_LENGTH), Integer.class), StreamReadConstraints.DEFAULT_MAX_NUM_LEN))
.maxStringLength(ObjectUtility.nvl(TypeCastUtility.castValue(value.get(MAX_STRING_LENGTH), Integer.class), DEFAULT_MAX_STRING_LEN))
.build();
}
}

/**
* {@link StreamReadConstraints} for {@link JsonFactory}.
*/
public static class StreamWriteConstraintsConfigProperty extends AbstractConfigProperty<StreamWriteConstraints, Map<String, String>> {

private static final String MAX_NESTING_DEPTH = "maxNestingDepth";

@Override
public String getKey() {
return "scout.dataobject.jackson.streamWriteConstraints";
}

@Override
public String description() {
return String.format("Jackson constraints to use for JSON writing.\n"
+ "Map property with the keys as follows:\n"
+ "- %s: Sets the maximum nesting depth. The depth is a count of objects and arrays that have not been closed, `{` and `[` respectively. (default: %d)\n",
MAX_NESTING_DEPTH, StreamWriteConstraints.DEFAULT_MAX_DEPTH);
}

@Override
public Map<String, String> readFromSource(String namespace) {
return ConfigUtility.getPropertyMap(getKey(), null, namespace);
}

@Override
public StreamWriteConstraints getDefaultValue() {
return parse(Collections.emptyMap()); // defaults are on a per key base
}

@Override
protected StreamWriteConstraints parse(Map<String, String> value) {
Set<String> invalidMapKeys = new HashSet<>(value.keySet());
Arrays.asList(MAX_NESTING_DEPTH).forEach(invalidMapKeys::remove);
if (!invalidMapKeys.isEmpty()) {
throw new PlatformException("Invalid values for map property {}: {}", getKey(), invalidMapKeys);
}
return StreamWriteConstraints.builder()
.maxNestingDepth(ObjectUtility.nvl(TypeCastUtility.castValue(value.get(MAX_NESTING_DEPTH), Integer.class), StreamWriteConstraints.DEFAULT_MAX_DEPTH))
.build();
}
}
}

0 comments on commit 158dada

Please sign in to comment.