From aa4b4252dfa39f4180c69543942d5b66bfaf887a Mon Sep 17 00:00:00 2001 From: Michiel Meeuwissen Date: Tue, 25 Nov 2025 16:22:13 +0100 Subject: [PATCH 01/18] Jackson3 was released. That would require some work if we want to support it. This branch. --- pom.xml | 10 + vpro-shared-jackson3/README.md | 19 + vpro-shared-jackson3/pom.xml | 81 ++++ .../jackson3/BackwardsCompatibleJsonEnum.java | 57 +++ .../java/nl/vpro/jackson3/DateModule.java | 44 ++ .../nl/vpro/jackson3/DateToJsonTimestamp.java | 46 ++ .../jackson3/DurationToJsonTimestamp.java | 70 +++ .../DurationToSecondsFloatTimestamp.java | 46 ++ .../nl/vpro/jackson3/GuavaRangeModule.java | 142 ++++++ .../vpro/jackson3/InstantToJsonTimestamp.java | 58 +++ .../InstantToSecondsFloatTimestamp.java | 60 +++ .../java/nl/vpro/jackson3/IterableJson.java | 99 ++++ .../java/nl/vpro/jackson3/Jackson3Mapper.java | 335 +++++++++++++ .../nl/vpro/jackson3/JsonArrayIterator.java | 449 ++++++++++++++++++ .../jackson3/LenientBooleanDeserializer.java | 46 ++ .../LocalDateTimeToJsonDateWithSpace.java | 56 +++ .../nl/vpro/jackson3/LocalDateToJsonDate.java | 47 ++ .../java/nl/vpro/jackson3/NattySupport.java | 33 ++ .../StringDurationToJsonTimestamp.java | 49 ++ .../StringInstantToJsonTimestamp.java | 107 +++++ .../StringZonedLocalDateToJsonTimestamp.java | 64 +++ .../src/main/java/nl/vpro/jackson3/Utils.java | 60 +++ .../src/main/java/nl/vpro/jackson3/Views.java | 54 +++ .../jackson3/XMLDurationToJsonTimestamp.java | 79 +++ .../ZonedDateTimeToJsonTimestamp.java | 53 +++ .../jackson3/rs/JacksonContextResolver.java | 64 +++ .../jackson3/rs/JsonIdAdderBodyReader.java | 75 +++ .../vpro/jackson3/GuavaRangeModuleTest.java | 85 ++++ .../nl/vpro/jackson3/IterableJsonTest.java | 183 +++++++ .../nl/vpro/jackson3/Jackson2MapperTest.java | 95 ++++ .../vpro/jackson3/JsonArrayIteratorTest.java | 247 ++++++++++ .../LenientBooleanDeserializerTest.java | 54 +++ .../StringInstantToJsonTimestampTest.java | 48 ++ ...ringZonedLocalDateToJsonTimestampTest.java | 10 + .../rs/JsonIdAdderBodyReaderTest.java | 92 ++++ 35 files changed, 3117 insertions(+) create mode 100644 vpro-shared-jackson3/README.md create mode 100644 vpro-shared-jackson3/pom.xml create mode 100644 vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/BackwardsCompatibleJsonEnum.java create mode 100644 vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/DateModule.java create mode 100644 vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/DateToJsonTimestamp.java create mode 100644 vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/DurationToJsonTimestamp.java create mode 100644 vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/DurationToSecondsFloatTimestamp.java create mode 100644 vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/GuavaRangeModule.java create mode 100644 vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/InstantToJsonTimestamp.java create mode 100644 vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/InstantToSecondsFloatTimestamp.java create mode 100644 vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/IterableJson.java create mode 100644 vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/Jackson3Mapper.java create mode 100644 vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/JsonArrayIterator.java create mode 100644 vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/LenientBooleanDeserializer.java create mode 100644 vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/LocalDateTimeToJsonDateWithSpace.java create mode 100644 vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/LocalDateToJsonDate.java create mode 100644 vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/NattySupport.java create mode 100644 vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/StringDurationToJsonTimestamp.java create mode 100644 vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/StringInstantToJsonTimestamp.java create mode 100644 vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/StringZonedLocalDateToJsonTimestamp.java create mode 100644 vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/Utils.java create mode 100644 vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/Views.java create mode 100644 vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/XMLDurationToJsonTimestamp.java create mode 100644 vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/ZonedDateTimeToJsonTimestamp.java create mode 100644 vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/rs/JacksonContextResolver.java create mode 100644 vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/rs/JsonIdAdderBodyReader.java create mode 100644 vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/GuavaRangeModuleTest.java create mode 100644 vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/IterableJsonTest.java create mode 100644 vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/Jackson2MapperTest.java create mode 100644 vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/JsonArrayIteratorTest.java create mode 100644 vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/LenientBooleanDeserializerTest.java create mode 100644 vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/StringInstantToJsonTimestampTest.java create mode 100644 vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/StringZonedLocalDateToJsonTimestampTest.java create mode 100644 vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/rs/JsonIdAdderBodyReaderTest.java diff --git a/pom.xml b/pom.xml index c74824f5b..4476d4b7b 100644 --- a/pom.xml +++ b/pom.xml @@ -20,6 +20,7 @@ 6.2.13 6.5.6 2.20.1 + 3.0.0 6.0.1 5.20.0 @@ -136,6 +137,8 @@ vpro-shared-hibernate-search6 vpro-shared-jackson2 + vpro-shared-jackson3 + vpro-shared-logging vpro-shared-log4j2 vpro-shared-monitoring @@ -587,6 +590,13 @@ pom import + + tools.jackson + jackson-bom + ${jackson3.version} + pom + import + org.apache.httpcomponents httpclient diff --git a/vpro-shared-jackson3/README.md b/vpro-shared-jackson3/README.md new file mode 100644 index 000000000..7f0d0aa00 --- /dev/null +++ b/vpro-shared-jackson3/README.md @@ -0,0 +1,19 @@ +[![javadoc](http://www.javadoc.io/badge/nl.vpro.shared/vpro-shared-jackson2.svg?color=blue)](http://www.javadoc.io/doc/nl.vpro.shared/vpro-shared-jackson2) + +# Jackson2 utilities + +We collect some generic Jackson2 utilities. Mainly `com.fasterxml.jackson.databind.JsonSerializer`s and `com.fasterxml.jackson.databind.JsonDeserializer`s. + +Some of them are bundled in modules. E.g. a `nl.vpro.jackson2.DateModule`, which can will make a `com.fasterxml.jackson.databind.ObjectMapper` recognize `java.time` classes +(but a bit differently then `com.fasterxml.jackson.datatype.jsr310.JavaTimeModule` does, which it predates). + +Also `nl.vpro.jackson2.Views` is provided which defines a few classes which can be used with `@com.fasterxml.jackson.annotation.JsonView`. + +## JsonArrayIterator + +This package also contains `nl.vpro.jackson2.JsonArrayIterator`. A tool to wrap, using jackson, a stream of json object into an iterator of java objects. + + +## AfterUnmarshallDeserializer + +Jackson lacks support for `#afterUnmarshall'. The 'nl.vpro.jackson2.AfterUnmarshalDeserializer' deserializer adds it. This can e.g. be used to add references to parent objects. diff --git a/vpro-shared-jackson3/pom.xml b/vpro-shared-jackson3/pom.xml new file mode 100644 index 000000000..66c292a24 --- /dev/null +++ b/vpro-shared-jackson3/pom.xml @@ -0,0 +1,81 @@ + + + 4.0.0 + + + vpro-shared-parent + nl.vpro.shared + 5.14-SNAPSHOT + + + vpro-shared-jackson3 + vpro-shared-jackson3 + + + + jackson3 + + + + + tools.jackson.core + jackson-databind + + + tools.jackson.module + jackson-module-jakarta-xmlbind-annotations + + + tools.jackson.jakarta.rs + jackson-jakarta-rs-json-provider + true + + + nl.vpro.shared + vpro-shared-util + + + org.skyscreamer + jsonassert + test + + + org.projectlombok + lombok + + + io.github.natty-parser + natty + true + + + jakarta.xml.bind + jakarta.xml.bind-api + true + + + jakarta.ws.rs + jakarta.ws.rs-api + + + org.glassfish.jaxb + jaxb-runtime + test + + + jakarta.annotation + jakarta.annotation-api + provided + + + com.google.guava + guava + true + + + commons-io + commons-io + true + + + diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/BackwardsCompatibleJsonEnum.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/BackwardsCompatibleJsonEnum.java new file mode 100644 index 000000000..e2a6cccbe --- /dev/null +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/BackwardsCompatibleJsonEnum.java @@ -0,0 +1,57 @@ +package nl.vpro.jackson3; + +import lombok.extern.slf4j.Slf4j; + +import java.io.IOException; + +import tools.jackson.core.*; +import tools.jackson.databind.*; + +/** + * Newer jackson version suddenly recognized @XmlEnumValue. This makes it possible to fall back to old behaviour. + * {@link nl.vpro.domain.media.support.Workflow} + */ +@Slf4j +public class BackwardsCompatibleJsonEnum { + + + public static class Serializer extends ValueSerializer> { + + @Override + public void serialize(Enum value, JsonGenerator jgen, SerializationContext ctxt) throws JacksonException { + if (value == null) { + jgen.writeNull(); + } else { + jgen.writeString(value.name()); + } + } + + } + + public static abstract class Deserializer> extends ValueDeserializer { + final Class enumClass; + + public Deserializer(Class enumClass) { + this.enumClass = enumClass; + } + + @Override + public T deserialize(JsonParser jp, DeserializationContext ctxt) { + try { + return Enum.valueOf(enumClass, jp.getValueAsString()); + } catch(IllegalArgumentException iae) { + try { + return Enum.valueOf(enumClass, jp.getValueAsString().toUpperCase()); + } catch (IllegalArgumentException iaeu) { + if (ctxt.getConfig().hasDeserializationFeatures(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL.getMask())) { + return null; + } else { + throw iae; + } + } + + } + } + + } +} diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/DateModule.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/DateModule.java new file mode 100644 index 000000000..1c25cce27 --- /dev/null +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/DateModule.java @@ -0,0 +1,44 @@ +package nl.vpro.jackson3; + +import java.io.Serial; +import java.time.*; +import java.util.Date; + +import tools.jackson.core.Version; +import tools.jackson.databind.module.SimpleModule; + + +/** + * Work around for JACKSON-920 + * @author Michiel Meeuwissen + * @since 0.21 + */ +public class DateModule extends SimpleModule { + + public static final ZoneId ZONE = ZoneId.of("Europe/Amsterdam"); + + + @Serial + private static final long serialVersionUID = 1L; + + public DateModule() { + super(new Version(0, 31, 0, "", "nl.vpro.shared", "vpro-jackson2")); + + // first deserializers + addDeserializer(Date.class, DateToJsonTimestamp.Deserializer.INSTANCE); + addDeserializer(Instant.class, InstantToJsonTimestamp.Deserializer.INSTANCE); + addDeserializer(ZonedDateTime.class, ZonedDateTimeToJsonTimestamp.Deserializer.INSTANCE); + addDeserializer(LocalDate.class, LocalDateToJsonDate.Deserializer.INSTANCE); + addDeserializer(Duration.class, DurationToJsonTimestamp.Deserializer.INSTANCE); + + + // then serializers: + addSerializer(Date.class, DateToJsonTimestamp.Serializer.INSTANCE); + addSerializer(Instant.class, InstantToJsonTimestamp.Serializer.INSTANCE); + addSerializer(ZonedDateTime.class, ZonedDateTimeToJsonTimestamp.Serializer.INSTANCE); + addSerializer(LocalDate.class, LocalDateToJsonDate.Serializer.INSTANCE); + addSerializer(Duration.class, DurationToJsonTimestamp.Serializer.INSTANCE); + + + } +} diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/DateToJsonTimestamp.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/DateToJsonTimestamp.java new file mode 100644 index 000000000..dcbc332ff --- /dev/null +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/DateToJsonTimestamp.java @@ -0,0 +1,46 @@ +package nl.vpro.jackson3; + +import java.io.IOException; +import java.io.Serial; +import java.util.Date; + +import tools.jackson.core.*; +import tools.jackson.databind.*; + +import tools.jackson.databind.deser.std.StdDeserializer; + + +public class DateToJsonTimestamp { + + private DateToJsonTimestamp() {} + + public static class Serializer extends ValueSerializer { + public static final Serializer INSTANCE = new Serializer(); + + @Override + public void serialize(Date value, JsonGenerator jgen, SerializationContext ctxt) throws JacksonException { + if (value == null) { + jgen.writeNull(); + } else { + jgen.writeNumber(value.getTime()); + } + } + + + } + + + public static class Deserializer extends StdDeserializer { + + public static final Deserializer INSTANCE = new Deserializer(Date.class); + + protected Deserializer(Class vc) { + super(vc); + } + + @Override + public Date deserialize(JsonParser jp, DeserializationContext ctxt) { + return _parseDate(jp, ctxt); + } + } +} diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/DurationToJsonTimestamp.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/DurationToJsonTimestamp.java new file mode 100644 index 000000000..a0e6ec6ab --- /dev/null +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/DurationToJsonTimestamp.java @@ -0,0 +1,70 @@ +package nl.vpro.jackson3; + +import java.io.IOException; +import java.time.Duration; +import java.util.Calendar; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.*; + +import nl.vpro.util.TimeUtils; + +/** + * Default Jackson serialized Durations as seconds. In poms we used to serialize durations as Dates, and hence as _milliseconds_. + * @author Michiel Meeuwissen + * @since 0.31 + */ +public class DurationToJsonTimestamp { + + private DurationToJsonTimestamp() {} + + public static class Serializer extends JsonSerializer { + + + public static final Serializer INSTANCE = new Serializer(); + + @Override + public void serialize(Duration value, JsonGenerator jgen, SerializerProvider provider) throws IOException { + if (value == null) { + jgen.writeNull(); + } else { + jgen.writeNumber(value.toMillis()); + } + } + } + + public static class XmlSerializer extends JsonSerializer { + + + public static final Serializer INSTANCE = new Serializer(); + + @Override + public void serialize(javax.xml.datatype.Duration value, JsonGenerator jgen, SerializerProvider provider) throws IOException { + if (value == null) { + jgen.writeNull(); + } else { + jgen.writeNumber(value.getTimeInMillis(Calendar.getInstance())); + } + } + } + + + public static class Deserializer extends JsonDeserializer { + + public static final Deserializer INSTANCE = new Deserializer(); + @Override + public Duration deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException { + if (jp.getCurrentToken() == JsonToken.VALUE_STRING) { + if (jp.getText().isEmpty() && ctxt.hasDeserializationFeatures(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT.getMask())) { + return null; + } else { + return TimeUtils.parseDuration(jp.getText()).orElseThrow(); + } + } else { + return Duration.ofMillis(jp.getLongValue()); + } + } + } +} diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/DurationToSecondsFloatTimestamp.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/DurationToSecondsFloatTimestamp.java new file mode 100644 index 000000000..f1d455a6d --- /dev/null +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/DurationToSecondsFloatTimestamp.java @@ -0,0 +1,46 @@ +package nl.vpro.jackson3; + +import java.io.IOException; +import java.time.Duration; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +/** + * Default Jackson serialized Durations as seconds. In poms we used to serialize durations as Dates, and hence as _milliseconds_. + * @author Michiel Meeuwissen + * @since 0.31 + */ +public class DurationToSecondsFloatTimestamp { + + private DurationToSecondsFloatTimestamp() {} + + public static class Serializer extends JsonSerializer { + + + public static final Serializer INSTANCE = new Serializer(); + + @Override + public void serialize(Duration value, JsonGenerator jgen, SerializerProvider provider) throws IOException { + if (value == null) { + jgen.writeNull(); + } else { + jgen.writeNumber(value.toMillis() / 1000f); + } + } + } + + + public static class Deserializer extends JsonDeserializer { + + public static final Deserializer INSTANCE = new Deserializer(); + @Override + public Duration deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException { + return Duration.ofMillis((long) (Float.parseFloat(jp.getValueAsString()) * 1000)); + } + } +} diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/GuavaRangeModule.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/GuavaRangeModule.java new file mode 100644 index 000000000..11379c9fc --- /dev/null +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/GuavaRangeModule.java @@ -0,0 +1,142 @@ +package nl.vpro.jackson3; + +import lombok.SneakyThrows; + +import java.io.IOException; +import java.io.Serial; + +import com.fasterxml.jackson.core.*; +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.type.CollectionLikeType; +import com.fasterxml.jackson.databind.type.SimpleType; +import com.google.common.collect.BoundType; +import com.google.common.collect.Range; + +public class GuavaRangeModule extends SimpleModule { + + public static final String LOWER_ENDPOINT = "lowerEndpoint"; + public static final String LOWER_BOUND_TYPE = "lowerBoundType"; + public static final String UPPER_ENDPOINT = "upperEndpoint"; + public static final String UPPER_BOUND_TYPE = "upperBoundType"; + + @Serial + private static final long serialVersionUID = -8048846883670339246L; + + public GuavaRangeModule() { + super(new Version(3, 5, 0, "", "nl.vpro.shared", "vpro-jackson2-guavarange")); + addSerializer( Serializer.INSTANCE); + addDeserializer(Range.class, Deserializer.INSTANCE); + } + + public static class Serializer extends com.fasterxml.jackson.databind.ser.std.StdSerializer> { + + @Serial + private static final long serialVersionUID = -4394016847732058088L; + public static Serializer INSTANCE = new Serializer(); + + protected Serializer() { + super(new CollectionLikeType( + SimpleType.constructUnsafe(Range.class), + SimpleType.constructUnsafe(Comparable.class)) { + @Serial + private static final long serialVersionUID = -2803462566784593946L; + }); + } + + @Override + public void serialize(Range value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + if (value == null) { + gen.writeNull(); + } else { + gen.writeStartObject(); + Class type = null; + + if (value.hasLowerBound()) { + type = value.lowerEndpoint().getClass(); + gen.writeObjectField(LOWER_ENDPOINT, value.lowerEndpoint()); + gen.writeObjectField(LOWER_BOUND_TYPE, value.lowerBoundType()); + } + if (value.hasUpperBound()) { + type = value.upperEndpoint().getClass(); + gen.writeObjectField(UPPER_ENDPOINT, value.upperEndpoint()); + gen.writeObjectField(UPPER_BOUND_TYPE, value.upperBoundType()); + } + if (type != null) { + gen.writeObjectField("type", type.getName()); + } + gen.writeEndObject(); + } + } + } + + public static class Deserializer extends StdDeserializer> { + + @Serial + private static final long serialVersionUID = -4394016847732058088L; + public static Deserializer INSTANCE = new Deserializer(); + + + protected Deserializer() { + super(new CollectionLikeType( + SimpleType.constructUnsafe(Range.class), + SimpleType.constructUnsafe(Comparable.class)) { + @Serial + private static final long serialVersionUID = -2803462566784593946L; + }); + } + + + + @SneakyThrows + @Override + public Range deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + + if (p.currentToken() == JsonToken.START_OBJECT) { + p.nextToken(); + } + JsonNode node = ctxt.readValue(p, JsonNode.class); + if (node.has("type")) { + Class type = Class.forName(node.get("type").textValue()); + return of(type, p, node); + } else { + return Range.all(); + } + + } + } + + static > Range of(Class clazz, JsonParser p, JsonNode node) throws IOException { + + BoundType lowerBoundType = null; + C lowerValue = null; + BoundType upperBoundType = null; + C upperValue = null; + + if (node.has(LOWER_ENDPOINT)) { + lowerBoundType = BoundType.valueOf(node.get(LOWER_BOUND_TYPE).asText()); + lowerValue = p.getCodec().treeToValue(node.get(LOWER_ENDPOINT), clazz); + } + if (node.has(UPPER_ENDPOINT)) { + upperBoundType = BoundType.valueOf(node.get(UPPER_BOUND_TYPE).asText()); + upperValue = p.getCodec().treeToValue(node.get(UPPER_ENDPOINT), clazz); + } + if (lowerValue != null) { + if (upperValue != null) { + return Range.range(lowerValue, lowerBoundType, upperValue, upperBoundType); + } else { + return Range.downTo(lowerValue, lowerBoundType); + } + } else { + if (upperValue != null) { + return Range.upTo(upperValue, upperBoundType); + } else { + return Range.all(); + } + } + } + + + +} diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/InstantToJsonTimestamp.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/InstantToJsonTimestamp.java new file mode 100644 index 000000000..5b711ef5e --- /dev/null +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/InstantToJsonTimestamp.java @@ -0,0 +1,58 @@ +package nl.vpro.jackson3; + +import java.io.IOException; +import java.time.Instant; +import java.time.ZonedDateTime; +import java.time.format.DateTimeParseException; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + + +public class InstantToJsonTimestamp { + + private InstantToJsonTimestamp() {} + + public static class Serializer extends JsonSerializer { + + + public static final Serializer INSTANCE = new Serializer(); + + @Override + public void serialize(Instant value, JsonGenerator jgen, SerializerProvider provider) throws IOException { + if (value == null) { + jgen.writeNull(); + } else { + jgen.writeNumber(value.toEpochMilli()); + } + } + } + + + public static class Deserializer extends JsonDeserializer { + + public static final Deserializer INSTANCE = new Deserializer(); + @Override + public Instant deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException { + try { + return Instant.ofEpochMilli(jp.getLongValue()); + } catch (JsonParseException jpe) { + try { + String s = jp.getValueAsString(); + if (s == null) { + return null; + } + return Instant.parse(s); + } catch (DateTimeParseException dtps) { + return ZonedDateTime.parse(jp.getValueAsString()).toInstant(); + } + } + } + } +} diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/InstantToSecondsFloatTimestamp.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/InstantToSecondsFloatTimestamp.java new file mode 100644 index 000000000..bace9eee6 --- /dev/null +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/InstantToSecondsFloatTimestamp.java @@ -0,0 +1,60 @@ +package nl.vpro.jackson3; + +import java.io.IOException; +import java.time.Instant; +import java.time.ZonedDateTime; +import java.time.format.DateTimeParseException; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + + +public class InstantToSecondsFloatTimestamp { + + private InstantToSecondsFloatTimestamp() {} + + public static class Serializer extends JsonSerializer { + + + public static final Serializer INSTANCE = new Serializer(); + + @Override + public void serialize(Instant value, JsonGenerator jgen, SerializerProvider provider) throws IOException { + if (value == null) { + jgen.writeNull(); + } else { + jgen.writeNumber(value.toEpochMilli() / 1000f); + } + } + } + + + public static class Deserializer extends JsonDeserializer { + + public static final Deserializer INSTANCE = new Deserializer(); + @Override + public Instant deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException { + try { + return Instant.ofEpochMilli((long) Float.parseFloat(jp.getValueAsString()) * 1000); + } catch (JsonParseException jpe) { + try { + String s = jp.getValueAsString(); + if (s == null) { + return null; + } + return Instant.parse(s); + } catch (DateTimeParseException dtps) { + return ZonedDateTime.parse(jp.getValueAsString()).toInstant(); + } + } + } + + + } +} diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/IterableJson.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/IterableJson.java new file mode 100644 index 000000000..c28252a48 --- /dev/null +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/IterableJson.java @@ -0,0 +1,99 @@ +package nl.vpro.jackson3; + +import java.io.IOException; +import java.util.*; +import java.util.function.Function; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +/** + * @author Michiel Meeuwissen + * @since 0.32 + */ +public class IterableJson { + + + public static class Serializer extends JsonSerializer> { + @Override + public void serialize(Iterable value, JsonGenerator jgen, SerializerProvider provider) throws IOException { + + if (value == null) { + jgen.writeNull(); + } else { + Iterator i = value.iterator(); + Object v; + if (i.hasNext()) { + v = i.next(); + if (! i.hasNext()) { + jgen.writeObject(v); + } else { + jgen.writeStartArray(); + jgen.writeObject(v); + while (i.hasNext()) { + jgen.writeObject(i.next()); + } + jgen.writeEndArray(); + } + } else { + jgen.writeStartArray(); + jgen.writeEndArray(); + } + } + } + } + + private static final Set> simpleTypes = new HashSet<>(Arrays.asList(String.class, Character.class, Boolean.class, Integer.class, Float.class, Long.class, Double.class)); + public static abstract class Deserializer extends JsonDeserializer> { + + private final Function, Iterable> creator; + + private final Class memberClass; + + private final boolean isSimple; + + + public Deserializer(Function, Iterable> supplier, Class memberClass) { + this.creator = supplier; + this.memberClass = memberClass; + isSimple = memberClass.isPrimitive() || simpleTypes.contains(memberClass); + } + + @SuppressWarnings("unchecked") + public Deserializer(Function, Iterable> supplier) { + this.creator = supplier; + try { + this.memberClass = (Class) supplier.getClass().getMethod("apply").getReturnType().getGenericSuperclass(); + } catch (NoSuchMethodException e) { + throw new RuntimeException(); + } + isSimple = memberClass.isPrimitive() || simpleTypes.contains(memberClass); + } + + + @Override + public Iterable deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException { + if (jp.getParsingContext().inObject()) { + if (! isSimple) { + jp.clearCurrentToken(); + } + T rs = jp.readValueAs(memberClass); + return creator.apply(Collections.singletonList(rs)); + } else if (jp.getParsingContext().inArray()) { + List list = new ArrayList<>(); + jp.clearCurrentToken(); + Iterator i = jp.readValuesAs(memberClass); + while (i.hasNext()) { + list.add(i.next()); + } + return creator.apply(list); + } else { + throw new IllegalStateException(); + } + } + } +} diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/Jackson3Mapper.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/Jackson3Mapper.java new file mode 100644 index 000000000..2a194f596 --- /dev/null +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/Jackson3Mapper.java @@ -0,0 +1,335 @@ +/* + * Copyright (C) 2012 All rights reserved + * VPRO The Netherlands + */ +package nl.vpro.jackson3; + +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; + +import java.io.IOException; +import java.io.Serial; +import java.lang.reflect.InvocationTargetException; +import java.net.http.HttpResponse; +import java.util.function.Consumer; +import java.util.function.Predicate; + +import org.slf4j.event.Level; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.json.JsonReadFeature; +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.Module; +import com.fasterxml.jackson.databind.introspect.AnnotationIntrospectorPair; +import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector; +import com.fasterxml.jackson.databind.ser.PropertyFilter; +import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.module.jakarta.xmlbind.JakartaXmlBindAnnotationIntrospector; +import com.google.common.annotations.Beta; + +import nl.vpro.logging.simple.SimpleLogger; +import nl.vpro.util.LoggingInputStream; + +import static nl.vpro.logging.simple.Slf4jSimpleLogger.slf4j; + +/** + * TODO: Many static public members that are not unmodifiable (e.g. {@link #INSTANCE}). + *

+ * Please use the static getters (like {@link #getInstance()}, so we could change that. + * + * @author Rico + * @author Michiel Meeuwissen + + */ +@Slf4j +public class Jackson3Mapper extends ObjectMapper { + + @Serial + private static final long serialVersionUID = 8353430660109292010L; + + private static boolean loggedAboutAvro = false; + private static boolean loggedAboutFallback = false; + + private static final SimpleFilterProvider FILTER_PROVIDER = new SimpleFilterProvider(); + + + + @Deprecated + public static final Jackson3Mapper INSTANCE = getInstance(); + @Deprecated + public static final Jackson3Mapper LENIENT = getLenientInstance(); + @Deprecated + public static final Jackson3Mapper STRICT = getStrictInstance(); + @Deprecated + public static final Jackson3Mapper PRETTY_STRICT = getPrettyStrictInstance(); + + @Deprecated + public static final Jackson3Mapper PRETTY = getPrettyInstance(); + @Deprecated + public static final Jackson3Mapper PUBLISHER = getPublisherInstance(); + @Deprecated + public static final Jackson3Mapper PRETTY_PUBLISHER = getPublisherInstance(); + @Deprecated + public static final Jackson3Mapper BACKWARDS_PUBLISHER = getBackwardsPublisherInstance(); + + + private static final ThreadLocal THREAD_LOCAL = ThreadLocal.withInitial(Jackson3Mapper::getInstance); + + + public static Jackson3Mapper getInstance() { + Jackson3Mapper mapper = new Jackson3Mapper("instance"); + mapper.setConfig(mapper.getSerializationConfig().withView(Views.Forward.class)); + mapper.setConfig(mapper.getDeserializationConfig().withView(Views.Forward.class)); + return mapper; + + } + + public static Jackson3Mapper getLenientInstance() { + Jackson3Mapper lenient = new Jackson3Mapper("lenient"); + lenient.enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL); + lenient.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + lenient.enable(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES); + lenient.enable(JsonParser.Feature.ALLOW_SINGLE_QUOTES); + lenient.setConfig(lenient.getSerializationConfig().withView(Views.Forward.class)); + lenient.setConfig(lenient.getDeserializationConfig().withView(Views.Forward.class)); + return lenient; + } + + public static Jackson3Mapper getPrettyInstance() { + Jackson3Mapper pretty = new Jackson3Mapper("pretty"); + pretty.enable(SerializationFeature.INDENT_OUTPUT); + return pretty; + } + + public static Jackson3Mapper getPrettyStrictInstance() { + Jackson3Mapper pretty_and_strict = new Jackson3Mapper("pretty_strict"); + pretty_and_strict.enable(SerializationFeature.INDENT_OUTPUT); + + pretty_and_strict.setConfig(pretty_and_strict.getSerializationConfig().withView(Views.Forward.class)); + pretty_and_strict.setConfig(pretty_and_strict.getDeserializationConfig().withView(Views.Forward.class)); + pretty_and_strict.enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); // This gives quite a lot of troubles. Though I'd like it to be set, especially because PRETTY is used in tests. + + return pretty_and_strict; + } + + + public static Jackson3Mapper getStrictInstance() { + Jackson3Mapper strict = new Jackson3Mapper("strict"); + strict.enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + strict.setConfig(strict.getSerializationConfig().withView(Views.Forward.class)); + strict.setConfig(strict.getDeserializationConfig().withView(Views.Forward.class)); + + return strict; + } + + public static Jackson3Mapper getPublisherInstance() { + Jackson3Mapper publisher = new Jackson3Mapper("publisher"); + publisher.setConfig(publisher.getSerializationConfig().withView(Views.ForwardPublisher.class)); + publisher.setConfig(publisher.getDeserializationConfig().withView(Views.Forward.class)); + + return publisher; + } + + public static Jackson3Mapper getPrettyPublisherInstance() { + Jackson3Mapper prettyPublisher = new Jackson3Mapper("pretty_publisher"); + prettyPublisher.setConfig(prettyPublisher.getSerializationConfig().withView(Views.ForwardPublisher.class)); + prettyPublisher.setConfig(prettyPublisher.getDeserializationConfig().withView(Views.Forward.class)); + prettyPublisher.enable(SerializationFeature.INDENT_OUTPUT); + return prettyPublisher; + } + + public static Jackson3Mapper getBackwardsPublisherInstance() { + Jackson3Mapper backwardsPublisher = new Jackson3Mapper("backwards_publisher"); + backwardsPublisher.setConfig(backwardsPublisher.getSerializationConfig().withView(Views.Publisher.class)); + backwardsPublisher.setConfig(backwardsPublisher.getDeserializationConfig().withView(Views.Normal.class)); + backwardsPublisher.enable(SerializationFeature.INDENT_OUTPUT); + return backwardsPublisher; + } + + @Beta + public static Jackson3Mapper getModelInstance() { + Jackson3Mapper model = new Jackson3Mapper("model"); + model.setConfig(model.getSerializationConfig().withView(Views.Model.class)); + return model; + } + + @Beta + public static Jackson3Mapper getModelAndNormalInstance() { + Jackson3Mapper modalAndNormal = new Jackson3Mapper("model_and_normal"); + modalAndNormal.setConfig(modalAndNormal.getSerializationConfig().withView(Views.ModelAndNormal.class)); + return modalAndNormal; + } + + public static Jackson3Mapper getThreadLocal() { + return THREAD_LOCAL.get(); + } + public static void setThreadLocal(Jackson3Mapper set) { + if (set == null) { + THREAD_LOCAL.remove(); + } else { + THREAD_LOCAL.set(set); + } + } + + @SneakyThrows({JsonProcessingException.class}) + public static T lenientTreeToValue(JsonNode jsonNode, Class clazz) { + return getLenientInstance().treeToValue(jsonNode, clazz); + } + + private final String toString; + + private Jackson3Mapper(String toString, Predicate predicate) { + configureMapper(this, predicate); + this.toString = toString; + } + + private Jackson3Mapper(String toString) { + configureMapper(this); + this.toString = toString; + } + + + @SafeVarargs + public static Jackson3Mapper create(String toString, Predicate module, Consumer... consumer) { + Jackson3Mapper result = new Jackson3Mapper(toString, module); + for (Consumer c : consumer){ + c.accept(result); + } + return result; + } + + public static void configureMapper(ObjectMapper mapper) { + configureMapper(mapper, m -> true); + } + + public static void configureMapper(ObjectMapper mapper, Predicate filter) { + mapper.setFilterProvider(FILTER_PROVIDER); + + AnnotationIntrospector introspector = new AnnotationIntrospectorPair( + new JacksonAnnotationIntrospector(), + new JakartaXmlBindAnnotationIntrospector(mapper.getTypeFactory()) + ); + + mapper.setDefaultPropertyInclusion(JsonInclude.Include.NON_EMPTY); + mapper.setAnnotationIntrospector(introspector); + + mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); // This seems a good idea when reading from couchdb or so, but when reading user supplied forms, it is confusing not getting errors. + + mapper.enable(JsonParser.Feature.ALLOW_SINGLE_QUOTES); + mapper.enable(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES); + mapper.enable(MapperFeature.USE_WRAPPER_NAME_AS_PROPERTY_NAME); + + try { + // this should nbe needed, but if I don't do this, resteasy still doesn't allow comments + mapper.enable(JsonParser.Feature.ALLOW_COMMENTS); + + mapper.setConfig(mapper.getDeserializationConfig().with(JsonReadFeature.ALLOW_LEADING_ZEROS_FOR_NUMBERS)); + mapper.setConfig(mapper.getDeserializationConfig().with(JsonReadFeature.ALLOW_JAVA_COMMENTS)); + + } catch (NoClassDefFoundError noClassDefFoundError) { + log.atLevel( loggedAboutFallback ? Level.DEBUG : Level.WARN).log( noClassDefFoundError.getMessage() + " temporary falling back. Please upgrade jackson"); + loggedAboutFallback = true; + + mapper.enable(JsonParser.Feature.ALLOW_COMMENTS); + //noinspection deprecation + mapper.enable(JsonParser.Feature.ALLOW_NUMERIC_LEADING_ZEROS); + + } + + register(mapper, filter, new JavaTimeModule()); + register(mapper, filter, new DateModule()); + // For example normal support for Optional. + Jdk8Module jdk8Module = new Jdk8Module(); + // jdk8Module.configureAbsentsAsNulls(true); This I think it covered by com.fasterxml.jackson.annotation.JsonInclude.Include.NON_ABSENT + register(mapper, filter, jdk8Module); + + mapper.setConfig(mapper.getSerializationConfig().withView(Views.Normal.class)); + mapper.setConfig(mapper.getDeserializationConfig().withView(Views.Normal.class)); + + + //SimpleModule module = new SimpleModule(); + //module.setDeserializerModifier(new AfterUnmarshalModifier()); + //mapper.registerModule(module); + + try { + Class avro = Class.forName("nl.vpro.jackson2.SerializeAvroModule"); + register(mapper, filter, (com.fasterxml.jackson.databind.Module) avro.getDeclaredConstructor().newInstance()); + } catch (ClassNotFoundException ncdfe) { + if (! loggedAboutAvro) { + log.debug("SerializeAvroModule could not be registered because: " + ncdfe.getClass().getName() + " " + ncdfe.getMessage()); + } + loggedAboutAvro = true; + } catch (IllegalAccessException | InstantiationException | InvocationTargetException | NoSuchMethodException e) { + log.error(e.getMessage(), e); + loggedAboutAvro = true; + } + + try { + Class guava = Class.forName("nl.vpro.jackson3.GuavaRangeModule"); + register(mapper, filter, (com.fasterxml.jackson.databind.Module) guava.getDeclaredConstructor().newInstance()); + } catch (ClassNotFoundException ncdfe) { + log.debug(ncdfe.getMessage()); + } catch (IllegalAccessException | InstantiationException | InvocationTargetException | NoSuchMethodException e) { + log.error(e.getMessage(), e); + } + } + + public static void addFilter(String key, PropertyFilter filter) { + FILTER_PROVIDER.addFilter(key, filter); + log.info("Installed filter {} -> {}", key, filter); + } + + private static void register(ObjectMapper mapper, Predicate predicate, Module module) { + if (predicate.test(module)) { + mapper.registerModule(module); + } + } + + @Override + public String toString() { + return Jackson3Mapper.class.getSimpleName() + " (" + toString + ")"; + } + + /** + * Returns a {@link HttpResponse.BodyHandler} that reads the body as a value of the given type, using this ObjectMapper. + * @since 5.11 + */ + public HttpResponse.BodyHandler asBodyHandler(Class type) { + return asBodyHandler(type, nl.vpro.logging.simple.Level.DEBUG); + } + + /** + * Returns a {@link HttpResponse.BodyHandler} that reads the body as a value of the given type, using this ObjectMapper. + * Note that if logging is enabled at the given level commons-io must be available. + * @since 5.11 + */ + public HttpResponse.BodyHandler asBodyHandler(Class type, nl.vpro.logging.simple.Level level) { + + return new HttpResponse.BodyHandler() { + @Override + public HttpResponse.BodySubscriber apply(HttpResponse.ResponseInfo responseInfo) { + return HttpResponse.BodySubscribers.mapping( + HttpResponse.BodySubscribers.ofInputStream(), + body -> { + try { + SimpleLogger simple = slf4j(log); + if (simple.isEnabled(level)) { + body = new LoggingInputStream(simple, body, level); + } + return readValue(body, type); + + } catch (IOException e) { + log.warn(e.getMessage(), e); + return null; + } + }); + } + }; + } +} + + diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/JsonArrayIterator.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/JsonArrayIterator.java new file mode 100644 index 000000000..ecde9057b --- /dev/null +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/JsonArrayIterator.java @@ -0,0 +1,449 @@ +package nl.vpro.jackson3; + +import lombok.*; +import lombok.extern.slf4j.Slf4j; + +import java.io.*; +import java.util.*; +import java.util.function.*; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.slf4j.Logger; + +import com.fasterxml.jackson.core.*; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.NullNode; +import com.google.common.collect.PeekingIterator; +import com.google.common.collect.UnmodifiableIterator; + +import nl.vpro.util.CloseableIterator; +import nl.vpro.util.CountedIterator; + +/** + * @author Michiel Meeuwissen + * @since 1.0 + */ +@Slf4j +public class JsonArrayIterator extends UnmodifiableIterator + implements CloseableIterator, PeekingIterator, CountedIterator { + + private final JsonParser jp; + + private T next = null; + + private boolean needsFindNext = true; + + private Boolean hasNext; + + private final BiFunction valueCreator; + + @Getter + @Setter + private Runnable callback; + + private boolean callBackHasRun = false; + + private final Long size; + + private final Long totalSize; + + private int foundNulls = 0; + + @Setter + private Logger logger = log; + + private long count = 0; + + private boolean skipNulls = true; + + private final Listener eventListener; + + public JsonArrayIterator(InputStream inputStream, Class clazz) throws IOException { + this(inputStream, clazz, null); + } + + public JsonArrayIterator(InputStream inputStream, final Class clazz, Runnable callback) throws IOException { + this(inputStream, null, clazz, callback, null, null, null, null, null, null); + } + + public JsonArrayIterator(InputStream inputStream, final BiFunction valueCreator) throws IOException { + this(inputStream, valueCreator, null, null, null, null, null, null, null, null); + } + + + public static class Builder { + + + } + + /** + * + * @param inputStream The inputstream containing the json + * @param valueCreator A function which converts a json {@link TreeNode} to the desired objects in the iterator. + * @param valueClass If valueCreator is not given, simply the class of the desired object can be given + * Json unmarshalling with the given objectMapper will happen. + * @param callback If the iterator is ready, closed or error this callback will be called. + * @param sizeField The size of the iterator, i.e. the size of the array represented in the json stream + * @param totalSizeField Sometimes the array is part of something bigger, e.g. a page in a search result. The size + * of the 'complete' result can be in the beginning of the json in this field. + * @param objectMapper Default the objectMapper {@link Jackson3Mapper#getLenientInstance()} will be used (in + * conjunction with valueClass, but you may specify another one + * @param logger Default this is logging to nl.vpro.jackson2.JsonArrayIterator, but you may override that. + * @param skipNulls Whether to skip nulls in the array. Default true. + * @param eventListener A listener for events that happen during parsing and iteration of the array. See {@link Event} and extension classes. + * @throws IOException If the json parser could not be created or the piece until the start of the array could + * not be tokenized. + */ + @lombok.Builder(builderClassName = "Builder", builderMethodName = "builder") + private JsonArrayIterator( + @NonNull InputStream inputStream, + @Nullable final BiFunction valueCreator, + @Nullable final Class valueClass, + @Nullable Runnable callback, + @Nullable String sizeField, + @Nullable String totalSizeField, + @Nullable ObjectMapper objectMapper, + @Nullable Logger logger, + @Nullable Boolean skipNulls, + @Nullable Listener eventListener + ) throws IOException { + if (inputStream == null) { + throw new IllegalArgumentException("No inputStream given"); + } + this.jp = (objectMapper == null ? Jackson3Mapper.getLenientInstance() : objectMapper).getFactory().createParser(inputStream); + this.valueCreator = valueCreator == null ? valueCreator(valueClass) : valueCreator; + if (valueCreator != null && valueClass != null) { + throw new IllegalArgumentException(); + } + if (logger != null) { + this.logger = logger; + } + Long tmpSize = null; + Long tmpTotalSize = null; + String fieldName = null; + if (sizeField == null) { + sizeField = "size"; + } + if (totalSizeField == null) { + totalSizeField = "totalSize"; + } + this.eventListener = eventListener == null? (e) -> {} : eventListener; + // find the start of the array, where we will start iterating. + while(true) { + JsonToken token = jp.nextToken(); + if (token == null) { + break; + } + this.eventListener.accept(new TokenEvent(token)); + if (token == JsonToken.FIELD_NAME) { + fieldName = jp.getCurrentName(); + } + if (token == JsonToken.VALUE_NUMBER_INT && sizeField.equals(fieldName)) { + tmpSize = jp.getLongValue(); + this.eventListener.accept(new SizeEvent(tmpSize)); + } + if (token == JsonToken.VALUE_NUMBER_INT && totalSizeField.equals(fieldName)) { + tmpTotalSize = jp.getLongValue(); + this.eventListener.accept(new TotalSizeEvent(tmpSize)); + + } + if (token == JsonToken.START_ARRAY) { + break; + } + } + this.size = tmpSize; + this.totalSize = tmpTotalSize; + this.eventListener.accept(new StartEvent()); + JsonToken token = jp.nextToken(); + this.eventListener.accept(new TokenEvent(token)); + + this.callback = callback; + this.skipNulls = skipNulls == null || skipNulls; + } + + private static BiFunction valueCreator(Class clazz) { + return (jp, tree) -> { + try { + return jp.getCodec().treeToValue(tree, clazz); + } catch (JsonProcessingException e) { + throw new ValueReadException(e); + } + }; + + } + + @Override + public boolean hasNext() { + findNext(); + return hasNext; + } + + @Override + public T peek() { + findNext(); + return next; + } + + @Override + public T next() { + findNext(); + if (! hasNext) { + throw new NoSuchElementException(); + } + T result = next; + next = null; + needsFindNext = true; + hasNext = null; + count += foundNulls; + foundNulls = 0; + count++; + return result; + } + + + @Override + public Long getCount() { + return count; + } + + protected void findNext() { + if(needsFindNext) { + while(true) { + try { + TreeNode tree = jp.readValueAsTree(); + this.eventListener.accept(new TokenEvent(jp.getLastClearedToken())); + + if (jp.getLastClearedToken() == JsonToken.END_ARRAY) { + tree = null; + } else { + if (tree instanceof NullNode && skipNulls) { + foundNulls++; + continue; + } + } + + try { + if (tree == null) { + callback(); + hasNext = false; + } else { + if (foundNulls > 0) { + logger.warn("Found {} nulls. Will be skipped", foundNulls); + } + + next = valueCreator.apply(jp, tree); + eventListener.accept(new NextEvent(next)); + hasNext = true; + } + break; + } catch (ValueReadException jme) { + foundNulls++; + logger.warn(jme.getClass() + " " + jme.getMessage() + " for\n" + tree + "\nWill be skipped"); + } + } catch (IOException e) { + callbackBeforeThrow(new RuntimeException(e)); + } catch (RuntimeException rte) { + callbackBeforeThrow(rte); + } + } + needsFindNext = false; + } + } + + + private void callbackBeforeThrow(RuntimeException e) { + callback(); + next = null; + needsFindNext = false; + hasNext = false; + throw e; + } + + @Override + public void close() throws IOException { + callback(); + this.jp.close(); + + } + + protected void callback() { + if (! callBackHasRun) { + if (callback != null) { + callback.run(); + } + callBackHasRun = true; + } + } + + /** + * Write the entire stream to an output stream + */ + public void write(OutputStream out, final Consumer logging) throws IOException { + write(this, out, logging == null ? null : (c) -> { logging.accept(c); return null;}); + } + + public void writeArray(OutputStream out, final Consumer logging) throws IOException { + writeArray(this, out, logging == null ? null : (c) -> { logging.accept(c); return null;}); + } + + + /** + * Write the entire stream to an output stream + * @deprecated Use {@link #write(OutputStream, Consumer)} + */ + @Deprecated + public void write(OutputStream out, final Function logging) throws IOException { + write(this, out, logging); + } + + /** + * Write the entire stream to an output stream + */ + public static void write( + final CountedIterator iterator, + final OutputStream out, + final Function logging) throws IOException { + try (JsonGenerator jg = Jackson3Mapper.getInstance().getFactory().createGenerator(out)) { + jg.writeStartObject(); + jg.writeArrayFieldStart("array"); + writeObjects(iterator, jg, logging); + jg.writeEndArray(); + jg.writeEndObject(); + jg.flush(); + } + } + + /** + * Write the entire stream to an output stream + */ + public static void writeArray( + final CountedIterator iterator, + final OutputStream out, final Function logging) throws IOException { + try (JsonGenerator jg = Jackson3Mapper.getInstance().getFactory().createGenerator(out)) { + jg.writeStartArray(); + writeObjects(iterator, jg, logging); + jg.writeEndArray(); + jg.flush(); + } + } + + + /** + * Write the entire stream as an array to jsonGenerator + */ + public static void writeObjects( + final CountedIterator iterator, + final JsonGenerator jg, + final Function logging) throws IOException { + while (iterator.hasNext()) { + T change; + try { + change = iterator.next(); + if (change != null) { + jg.writeObject(change); + } else { + jg.writeNull(); + } + if (logging != null) { + logging.apply(change); + } + } catch (Exception e) { + Throwable cause = e.getCause(); + while (cause != null) { + if (cause instanceof InterruptedException) { + return; + } + cause = cause.getCause(); + } + + log.warn(e.getClass().getCanonicalName() + " " + e.getMessage()); + jg.writeObject(e.getMessage()); + } + + } + } + + + @Override + @NonNull + public Optional getSize() { + return Optional.ofNullable(size); + } + + @Override + @NonNull + public Optional getTotalSize() { + return Optional.ofNullable(totalSize); + } + + + + public static class ValueReadException extends RuntimeException { + + @Serial + private static final long serialVersionUID = 6976771876437440576L; + + public ValueReadException(JsonProcessingException e) { + super(e); + } + } + + public class Event { + + public JsonArrayIterator getParent() { + return JsonArrayIterator.this; + } + } + + public class StartEvent extends Event { + } + + + @EqualsAndHashCode(callSuper = true) + @Data + public class TokenEvent extends Event { + final JsonToken token; + + public TokenEvent(JsonToken token) { + this.token = token; + } + } + + @EqualsAndHashCode(callSuper = true) + @Data + public class TotalSizeEvent extends Event { + final long totalSize; + + public TotalSizeEvent(long totalSize) { + this.totalSize = totalSize; + } + } + + @EqualsAndHashCode(callSuper = true) + @Data + public class SizeEvent extends Event { + final long size; + + public SizeEvent(long size) { + this.size = size; + } + } + + public class NextEvent extends Event { + + final T next; + + public NextEvent(T next) { + this.next = next; + } + } + + @FunctionalInterface + public interface Listener extends EventListener, Consumer.Event> { + + + } + + +} diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/LenientBooleanDeserializer.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/LenientBooleanDeserializer.java new file mode 100644 index 000000000..5e3ebf066 --- /dev/null +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/LenientBooleanDeserializer.java @@ -0,0 +1,46 @@ +package nl.vpro.jackson3; + +import java.io.IOException; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; + +/** + * @author Michiel Meeuwissen + * @since 2.3 + */ +public class LenientBooleanDeserializer extends JsonDeserializer { + + public static final LenientBooleanDeserializer INSTANCE = new LenientBooleanDeserializer(); + + private LenientBooleanDeserializer() { + + } + + + @Override + public Boolean deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException { + JsonToken token = jsonParser.getCurrentToken(); + if (jsonParser.isNaN()) { + return false; + } + if (token.isBoolean()) { + return jsonParser.getBooleanValue(); + } else if (token.isNumeric()) { + return jsonParser.getNumberValue().longValue() != 0; + } else { + String text = jsonParser.getText().toLowerCase(); + switch(text) { + case "true": + case "1": + return true; + default: + return false; + } + } + + } +} diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/LocalDateTimeToJsonDateWithSpace.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/LocalDateTimeToJsonDateWithSpace.java new file mode 100644 index 000000000..5ded11996 --- /dev/null +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/LocalDateTimeToJsonDateWithSpace.java @@ -0,0 +1,56 @@ +package nl.vpro.jackson3; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.format.DateTimeParseException; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import nl.vpro.util.TimeUtils; + + +/** + * @since 2.0 + */ +public class LocalDateTimeToJsonDateWithSpace { + + private LocalDateTimeToJsonDateWithSpace() {} + + public static class Serializer extends JsonSerializer { + + public static final Serializer INSTANCE = new Serializer(); + + @Override + public void serialize(LocalDateTime value, JsonGenerator jgen, SerializerProvider provider) throws IOException { + if (value == null) { + jgen.writeNull(); + } else { + jgen.writeString(value.toString().replaceFirst("T", " ")); + } + } + } + + public static class Deserializer extends JsonDeserializer { + + public static final Deserializer INSTANCE = new Deserializer(); + + @Override + public LocalDateTime deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException { + String text = jp.getText(); + if (text == null) { + return null; + } else { + try { + return LocalDateTime.parse(text.replaceFirst(" ", "T")); + } catch (DateTimeParseException dtf) { + return TimeUtils.parse(text).map(i -> i.atZone(TimeUtils.ZONE_ID).toLocalDateTime()).orElseThrow(() -> new IOException("Cannot parse " + text)); + } + } + } + } +} diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/LocalDateToJsonDate.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/LocalDateToJsonDate.java new file mode 100644 index 000000000..3213e5c2f --- /dev/null +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/LocalDateToJsonDate.java @@ -0,0 +1,47 @@ +package nl.vpro.jackson3; + +import java.io.IOException; +import java.time.LocalDate; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + + +public class LocalDateToJsonDate { + + private LocalDateToJsonDate() {} + + public static class Serializer extends JsonSerializer { + + public static final Serializer INSTANCE = new Serializer(); + + @Override + public void serialize(LocalDate value, JsonGenerator jgen, SerializerProvider provider) throws IOException { + if (value == null) { + jgen.writeNull(); + } else { + jgen.writeString(value.toString()); + } + } + } + + + public static class Deserializer extends JsonDeserializer { + + public static final Deserializer INSTANCE = new Deserializer(); + + @Override + public LocalDate deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException { + String text = jp.getText(); + if (text == null) { + return null; + } else { + return LocalDate.parse(text); + } + } + } +} diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/NattySupport.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/NattySupport.java new file mode 100644 index 000000000..4929391bf --- /dev/null +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/NattySupport.java @@ -0,0 +1,33 @@ +package nl.vpro.jackson3; + +import lombok.extern.slf4j.Slf4j; + +import java.time.Instant; +import java.util.*; + +import org.natty.DateGroup; +import org.natty.Parser; + +import nl.vpro.util.BindingUtils; +import nl.vpro.util.DateUtils; + +/** + * The dependency on natty in {@link StringInstantToJsonTimestamp} is optional. Put support for it in this class, so we can just catch the resulting NoClassDefFoundError. + * @author Michiel Meeuwissen + * @since 2.14 + */ + +@Slf4j +class NattySupport { + private static final Parser PARSER = new Parser(TimeZone.getTimeZone(BindingUtils.DEFAULT_ZONE)); + + + static Optional parseDate(String value) { + List groups = PARSER.parse(value); + if (groups.size() == 1) { + log.info("Parsed date '{}' to {}", value, groups.get(0).getDates()); + return Optional.ofNullable(DateUtils.toInstant(groups.get(0).getDates().get(0))); + } + return Optional.empty(); + } +} diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/StringDurationToJsonTimestamp.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/StringDurationToJsonTimestamp.java new file mode 100644 index 000000000..701d210bd --- /dev/null +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/StringDurationToJsonTimestamp.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2016 All rights reserved + * VPRO The Netherlands + */ +package nl.vpro.jackson3; + +import java.io.IOException; +import java.time.Duration; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +/** + * @author rico + * @since 0.37 + */ +public class StringDurationToJsonTimestamp { + + private StringDurationToJsonTimestamp() {} + + public static class Serializer extends JsonSerializer { + public static final StringDurationToJsonTimestamp.Serializer INSTANCE = new StringDurationToJsonTimestamp.Serializer(); + + @Override + public void serialize(String value, JsonGenerator jgen, SerializerProvider provider) throws IOException { + if (value == null) { + jgen.writeNull(); + } else { + jgen.writeNumber(Duration.parse(value).toMillis()); + } + } + } + + + public static class Deserializer extends JsonDeserializer { + + public static final StringDurationToJsonTimestamp.Deserializer INSTANCE = new StringDurationToJsonTimestamp.Deserializer(); + + @Override + public String deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException { + return Duration.ofMillis(jp.getLongValue()).toString(); + } + } +} diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/StringInstantToJsonTimestamp.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/StringInstantToJsonTimestamp.java new file mode 100644 index 000000000..b5288b974 --- /dev/null +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/StringInstantToJsonTimestamp.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2016 All rights reserved + * VPRO The Netherlands + */ +package nl.vpro.jackson3; + +import lombok.extern.slf4j.Slf4j; + +import java.io.IOException; +import java.time.Instant; +import java.util.Optional; + +import jakarta.xml.bind.DatatypeConverter; + +import org.slf4j.event.Level; + +import com.fasterxml.jackson.core.*; +import com.fasterxml.jackson.databind.*; + +/** + * These can be used in conjunction with InstantXmlAdapter, if you want 'millis since epoch' in JSON, but formatted date stamps in xml. + * (this is what we normally do) + * @author Michiel Meeuwissen + * @since 0.39 + */ +@Slf4j +public class StringInstantToJsonTimestamp { + + private static boolean warnedNatty = false; + + private StringInstantToJsonTimestamp() {} + + public static class Serializer extends JsonSerializer { + public static final StringInstantToJsonTimestamp.Serializer INSTANCE = new StringInstantToJsonTimestamp.Serializer(); + + @Override + public void serialize(Object value, JsonGenerator jgen, SerializerProvider provider) throws IOException { + if (value == null) { + jgen.writeNull(); + } else if (value instanceof Instant) { // if no JaxbAnnotationIntrospector + jgen.writeNumber(((Instant) value).toEpochMilli()); + } else if (value instanceof CharSequence) { + try { + jgen.writeNumber(parseDateTime(String.valueOf(value)).toEpochMilli()); + } catch (IllegalArgumentException iae) { + log.warn("Could not parse {}. Writing null to json", value); + jgen.writeNull(); + } + } + } + } + + static Instant parseDateTime(String value) { + try { + long asLong = Long.parseLong(value); + return Instant.ofEpochMilli(asLong); + } catch (NumberFormatException ignore) { + } + try { + return DatatypeConverter.parseTime(value).toInstant(); + } catch (IllegalArgumentException iae) { + try { + Optional natty = NattySupport.parseDate(value); + if (natty.isPresent()) { + return natty.get(); + } else { + log.debug("Natty didn't match"); + } + } catch (NoClassDefFoundError classNotFoundException) { + log.atLevel(warnedNatty ? Level.DEBUG : Level.WARN).log("No natty?: " + classNotFoundException.getMessage()); + warnedNatty = true; + } catch (Throwable e) { + log.debug("Natty couldn't parse {}: {}", value, e.getMessage()); + } + throw iae; + } + } + + public static class Deserializer extends JsonDeserializer { + + public static final StringInstantToJsonTimestamp.Deserializer INSTANCE = new StringInstantToJsonTimestamp.Deserializer(); + + @Override + public Instant deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException { + switch (jp.currentTokenId()) { + case JsonTokenId.ID_NUMBER_INT -> { + return Instant.ofEpochMilli(jp.getLongValue()); + } + case JsonTokenId.ID_NULL -> { + return null; + } + case JsonTokenId.ID_STRING -> { + try { + return parseDateTime(jp.getText()); + } catch (IllegalArgumentException iae) { + log.warn("Could not parse {}. Writing null to json", jp.getText()); + return null; + } + } + default -> { + log.warn("Could not parse {} to instant. Returning null", jp.toString()); + return null; + } + } + } + } +} diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/StringZonedLocalDateToJsonTimestamp.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/StringZonedLocalDateToJsonTimestamp.java new file mode 100644 index 000000000..a3855f3c9 --- /dev/null +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/StringZonedLocalDateToJsonTimestamp.java @@ -0,0 +1,64 @@ +package nl.vpro.jackson3; + +import java.io.IOException; +import java.time.Instant; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.Locale; + +import com.fasterxml.jackson.core.*; +import com.fasterxml.jackson.databind.*; + +import nl.vpro.util.BindingUtils; +import nl.vpro.util.TimeUtils; + +import static nl.vpro.jackson3.DateModule.ZONE; + + +/** + * @since 2.5 + */ +public class StringZonedLocalDateToJsonTimestamp { + + private StringZonedLocalDateToJsonTimestamp() {} + + private static final DateTimeFormatter FORMATTER = DateTimeFormatter + .ofPattern("yyyy-MM-ddZZZZZ") + .withLocale(Locale.US); + + + public static class Serializer extends JsonSerializer { + + public static final Serializer INSTANCE = new Serializer(); + + @Override + public void serialize(Object value, JsonGenerator jgen, SerializerProvider provider) throws IOException { + if (value == null) { + jgen.writeNull(); + } else { + if (value instanceof CharSequence) { + value = LocalDate.parse(value.toString().substring(0, 10)); + } + jgen.writeNumber(((LocalDate) value).atStartOfDay(BindingUtils.DEFAULT_ZONE).toInstant().toEpochMilli()); + } + } + } + + public static class Deserializer extends JsonDeserializer { + + public static final Deserializer INSTANCE = new Deserializer(); + @Override + public LocalDate deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException { + try { + return Instant.ofEpochMilli(jp.getLongValue()).atZone(ZONE).toLocalDate(); + } catch (JsonParseException jps) { + String s = jp.getValueAsString(); + if (s == null) { + return null; + } + return TimeUtils.parseLocalDate(s).orElseThrow(); + //return ZonedDateTime.parse(jp.getValueAsString(), FORMATTER).toLocalDate(); + } + } + } +} diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/Utils.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/Utils.java new file mode 100644 index 000000000..22bfd50d4 --- /dev/null +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/Utils.java @@ -0,0 +1,60 @@ +package nl.vpro.jackson3; + +import java.util.*; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.MapDifference; +import com.google.common.collect.Maps; + +/** + * @author Michiel Meeuwissen + * @since 2.9 + */ +public class Utils { + + + @SuppressWarnings("unchecked") + public static MapDifference flattenedDifference( + JsonNode j1, JsonNode j2) { + ObjectMapper mapper = Jackson3Mapper.getPublisherInstance(); + Map map1 = mapper.convertValue(j1, Map.class); + Map flatten1= flatten(map1); + Map map2 = mapper.convertValue(j2, Map.class); + Map flatten2 = flatten(map2); + + return Maps.difference(flatten1, flatten2); + } + + static Map flatten(Map map) { + return map.entrySet().stream() + .flatMap(Utils::flatten) + .collect(LinkedHashMap::new, (m, e) -> m.put("/" + e.getKey(), e.getValue()), LinkedHashMap::putAll); + } + + private static Stream> flatten(Map.Entry entry) { + + if (entry == null) { + return Stream.empty(); + } + + if (entry.getValue() instanceof Map) { + return ((Map) entry.getValue()).entrySet().stream() + .flatMap(e -> flatten(new AbstractMap.SimpleEntry<>(entry.getKey() + "/" + e.getKey(), e.getValue()))); + } + if (entry.getValue() instanceof Integer) { + entry.setValue(((Integer) entry.getValue()).longValue()); + } + + if (entry.getValue() instanceof List list) { + return IntStream.range(0, list.size()) + .mapToObj(i -> new AbstractMap.SimpleEntry(entry.getKey() + "/" + i, list.get(i))) + .flatMap(Utils::flatten); + } + + return Stream.of(entry); + } + +} diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/Views.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/Views.java new file mode 100644 index 000000000..13b7c156c --- /dev/null +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/Views.java @@ -0,0 +1,54 @@ +package nl.vpro.jackson3; + +import com.google.common.annotations.Beta; + +/** + * Several setups at VPRO and at NPO involve a backend system that publishes JSON to ElasticSearch. + * In some cases this published json must be somewhat adapted, in contrast to when it is not yet published. + * @author Michiel Meeuwissen + * @since 1.72 + */ +public class Views { + + public interface Normal { + } + + /** + * Forward compatible view + */ + public interface Forward extends Normal { + } + + public interface Publisher extends Normal { + } + + /** + * A 'model' related view of the json. + *

+ * This would e.g. imply that some extra fields are present which would otherwise calculable, but it may be useful for the receiving end to + * receive such a value evaluated. + * @since 2.33 + */ + @Beta + public interface Model { + } + + /** + * + * @since 2.33 + */ + @Beta + public interface ModelAndNormal extends Model, Normal { + } + + /** + * New fields may be temporary marked 'ForwardPublisher'. Which will mean that {@link Jackson3Mapper#getBackwardsPublisherInstance()} will ignore them. + *

+ * That way we can serialize for checking purposes compatible with old values in ES. + *

+ * So generally this means that a field should be present in the published json, but a full republication hasn't happened yet + */ + public interface ForwardPublisher extends Publisher, Forward { + } + +} diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/XMLDurationToJsonTimestamp.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/XMLDurationToJsonTimestamp.java new file mode 100644 index 000000000..dae469a65 --- /dev/null +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/XMLDurationToJsonTimestamp.java @@ -0,0 +1,79 @@ +package nl.vpro.jackson3; + +import lombok.extern.slf4j.Slf4j; + +import java.io.IOException; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.TimeZone; + +import javax.xml.datatype.DatatypeConfigurationException; +import javax.xml.datatype.DatatypeFactory; +import javax.xml.datatype.Duration; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import nl.vpro.util.TimeUtils; + +@Slf4j +public class XMLDurationToJsonTimestamp { + + + public static class Serializer extends JsonSerializer { + + @Override + public void serialize(Duration value, JsonGenerator jgen, SerializerProvider provider) throws IOException { + Calendar cal = new GregorianCalendar(TimeZone.getTimeZone("UTC")); + cal.setTimeInMillis(0); + jgen.writeNumber(value.getTimeInMillis(cal)); + } + } + + public static class DeserializerDate extends JsonDeserializer { + @Override + public Date deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException { + return new Date(jp.getLongValue()); + } + } + + public static class Deserializer extends JsonDeserializer { + @Override + public Duration deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException { + DatatypeFactory datatypeFactory; + try { + datatypeFactory = DatatypeFactory.newInstance(); + return datatypeFactory.newDuration(jp.getLongValue()); + } catch (DatatypeConfigurationException e) { + log.error(e.getMessage(), e); + } + return null; + } + } + + public static class SerializerString extends JsonSerializer { + + @Override + public void serialize(String value, JsonGenerator jgen, SerializerProvider provider) throws IOException { + java.time.Duration duration = TimeUtils.parseDuration(value).orElse(null); + if (duration != null) { + jgen.writeNumber(duration.toMillis()); + } else { + jgen.writeNull(); + } + } + } + + public static class DeserializerJavaDuration extends JsonDeserializer { + @Override + public java.time.Duration deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException { + return java.time.Duration.ofMillis(jp.getLongValue()); + } + } +} diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/ZonedDateTimeToJsonTimestamp.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/ZonedDateTimeToJsonTimestamp.java new file mode 100644 index 000000000..f8de09169 --- /dev/null +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/ZonedDateTimeToJsonTimestamp.java @@ -0,0 +1,53 @@ +package nl.vpro.jackson3; + +import java.io.IOException; +import java.time.Instant; +import java.time.ZonedDateTime; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import static nl.vpro.jackson3.DateModule.ZONE; + + +public class ZonedDateTimeToJsonTimestamp { + + private ZonedDateTimeToJsonTimestamp() {} + + public static class Serializer extends JsonSerializer { + + public static final Serializer INSTANCE = new Serializer(); + + @Override + public void serialize(ZonedDateTime value, JsonGenerator jgen, SerializerProvider provider) throws IOException { + if (value == null) { + jgen.writeNull(); + } else { + jgen.writeNumber(value.toInstant().toEpochMilli()); + } + } + } + + public static class Deserializer extends JsonDeserializer { + + public static final Deserializer INSTANCE = new Deserializer(); + @Override + public ZonedDateTime deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException { + try { + return Instant.ofEpochMilli(jp.getLongValue()).atZone(ZONE); + } catch (JsonParseException jps) { + String s = jp.getValueAsString(); + if (s == null) { + return null; + } + return ZonedDateTime.parse(jp.getValueAsString()); + } + } + } +} diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/rs/JacksonContextResolver.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/rs/JacksonContextResolver.java new file mode 100644 index 000000000..e816713fe --- /dev/null +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/rs/JacksonContextResolver.java @@ -0,0 +1,64 @@ +package nl.vpro.jackson3.rs; + +import jakarta.annotation.Priority; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.ext.ContextResolver; +import jakarta.ws.rs.ext.Provider; + +import java.util.function.Supplier; + +import tools.jackson.databind.ObjectMapper; +import tools.jackson.jakarta.rs.json.JacksonXmlBindJsonProvider; + +import nl.vpro.jackson3.Jackson3Mapper; + +/** + * This is used to bind our object mapper to resteasy/jaxrs. + *

+ * The exposed mapper is actually in a thread local. Some interceptor may influence it. + * + * @author Michiel Meeuwissen + * @since 2.0 + */ +@Provider +@Consumes({MediaType.APPLICATION_JSON, "application/*+json", "text/json"}) +@Produces({MediaType.APPLICATION_JSON, "application/*+json", "text/json"}) +@Priority(JacksonContextResolver.PRIORITY) +public class JacksonContextResolver extends JacksonXmlBindJsonProvider implements ContextResolver { + + static final int PRIORITY = Priorities.USER; + + private final ThreadLocal mapper; + + public JacksonContextResolver() { + this(Jackson3Mapper.getLenientInstance()); + } + public JacksonContextResolver(ObjectMapper mapper) { + this(() -> mapper); + } + + public JacksonContextResolver(Supplier mapper) { + this.mapper = ThreadLocal.withInitial(mapper); + } + + @Override + public ObjectMapper getContext(Class objectType) { + return mapper.get(); + } + + /** + * @since 4.0 + */ + public void set(ObjectMapper mapper) { + this.mapper.set(mapper); + } + + /** + * @since 4.0 + */ + public void reset() { + mapper.remove(); + } +} + diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/rs/JsonIdAdderBodyReader.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/rs/JsonIdAdderBodyReader.java new file mode 100644 index 000000000..e8e6a60c6 --- /dev/null +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/rs/JsonIdAdderBodyReader.java @@ -0,0 +1,75 @@ +package nl.vpro.jackson3.rs; + +import lombok.extern.slf4j.Slf4j; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; + +import jakarta.annotation.Priority; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.*; +import jakarta.ws.rs.ext.*; + +import org.checkerframework.checker.nullness.qual.NonNull; + +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; +import com.fasterxml.jackson.databind.jsontype.TypeIdResolver; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import nl.vpro.jackson3.Jackson3Mapper; + +/** + * Sometimes jackson/resteasy will not unmarshal a json because there is no type information, but the prototype actually specifies it fully. This message body reader will deal with that (using {@link TypeIdResolver#idFromBaseType()}, by adding the id implicitly (if it is missing) before the actual unmarshal. + * + * @author Michiel Meeuwissen + * @since 2.7 + */ +@Provider +@Consumes(MediaType.APPLICATION_JSON) +@Slf4j +@Priority(JacksonContextResolver.PRIORITY - 1) // It must be a bit higher than that, so that it goes before +public class JsonIdAdderBodyReader implements MessageBodyReader { + + @Context + Providers providers; + + @Override + public boolean isReadable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { + return mediaType.isCompatible(MediaType.APPLICATION_JSON_TYPE); + } + + @Override + public Object readFrom( + final @NonNull Class type, + Type genericType, + Annotation[] annotations, + MediaType mediaType, + MultivaluedMap httpHeaders, + InputStream entityStream) throws WebApplicationException, IOException { + ObjectMapper mapper = providers == null ? null : providers.getContextResolver(ObjectMapper.class, MediaType.APPLICATION_JSON_TYPE).getContext(type); + if (mapper == null) { + log.info("No mapper found in {}", providers); + mapper = Jackson3Mapper.getLenientInstance(); + } + final JavaType javaType = mapper.getTypeFactory().constructType(genericType); + final JsonNode jsonNode = mapper.readTree(entityStream); + if (jsonNode instanceof ObjectNode objectNode) { + final TypeDeserializer typeDeserializer = mapper.getDeserializationConfig().findTypeDeserializer(javaType); + if (typeDeserializer != null) { + final String propertyName = typeDeserializer.getPropertyName(); + final String propertyValue = typeDeserializer.getTypeIdResolver().idFromBaseType(); + if (! objectNode.has(propertyName)) { + log.debug("Implicitly setting {} = {} for {}", propertyName, propertyValue, javaType); + objectNode.put(propertyName, propertyValue); + } + } + } + return mapper.treeToValue(jsonNode, javaType.getRawClass()); + + + } +} diff --git a/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/GuavaRangeModuleTest.java b/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/GuavaRangeModuleTest.java new file mode 100644 index 000000000..448f34c57 --- /dev/null +++ b/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/GuavaRangeModuleTest.java @@ -0,0 +1,85 @@ +package nl.vpro.jackson3; + +import java.time.Instant; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.google.common.collect.Range; + +import nl.vpro.jackson3.DateModule; +import nl.vpro.jackson3.GuavaRangeModule; +import static org.assertj.core.api.Assertions.assertThat; + +class GuavaRangeModuleTest { + + ObjectMapper mapper = new ObjectMapper(); + { + mapper.registerModule(new DateModule()); + mapper.registerModule(new GuavaRangeModule()); + } + + + static class WithoutSerializer { + @JsonProperty + Range range; + @JsonProperty + int anotherField = 1; + + } + static class WithIntegerRange { + @JsonSerialize(using = GuavaRangeModule.Serializer.class) Range range; + } + + static class WithInstantRange { + @JsonSerialize(using = GuavaRangeModule.Serializer.class) + Range range; + } + + + @Test + public void without() throws JsonProcessingException { + WithoutSerializer a = new WithoutSerializer(); + a.range = Range.closedOpen(1, 2); + String example = "{\"range\":{\"lowerEndpoint\":1,\"lowerBoundType\":\"CLOSED\",\"upperEndpoint\":2,\"upperBoundType\":\"OPEN\",\"type\":\"java.lang.Integer\"},\"anotherField\":1}"; + assertThat(mapper.writeValueAsString(a)).isEqualTo(example); + + WithoutSerializer ab = mapper.readValue(example, WithoutSerializer.class); + assertThat(ab.range).isEqualTo(a.range); + + } + + + @Test + public void empty() throws JsonProcessingException { + WithIntegerRange a = new WithIntegerRange(); + assertThat(mapper.writeValueAsString(a)).isEqualTo("{\"range\":null}"); + + } + + @Test + public void filled() throws JsonProcessingException { + WithIntegerRange a = new WithIntegerRange(); + a.range = Range.closedOpen(1, 10); + + assertThat(mapper.writeValueAsString(a)).isEqualTo("{\"range\":{\"lowerEndpoint\":1,\"lowerBoundType\":\"CLOSED\",\"upperEndpoint\":10,\"upperBoundType\":\"OPEN\",\"type\":\"java.lang.Integer\"}}"); + + } + + @Test + public void instant() throws JsonProcessingException { + WithInstantRange a = new WithInstantRange(); + a.range = Range.closedOpen( + Instant.parse("2021-12-24T10:00:00Z"), + Instant.parse("2021-12-25T10:00:00Z") + ); + + assertThat(mapper.writeValueAsString(a)).isEqualTo( + "{\"range\":{\"lowerEndpoint\":1640340000000,\"lowerBoundType\":\"CLOSED\",\"upperEndpoint\":1640426400000,\"upperBoundType\":\"OPEN\",\"type\":\"java.time.Instant\"}}"); + + } + +} diff --git a/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/IterableJsonTest.java b/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/IterableJsonTest.java new file mode 100644 index 000000000..38ceb894b --- /dev/null +++ b/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/IterableJsonTest.java @@ -0,0 +1,183 @@ +package nl.vpro.jackson3; + +import java.io.StringWriter; +import java.util.*; + +import jakarta.xml.bind.annotation.XmlAccessType; +import jakarta.xml.bind.annotation.XmlAccessorType; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import org.skyscreamer.jsonassert.JSONCompareMode; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Michiel Meeuwissen + * @since 4.2 + */ +public class IterableJsonTest { + + @XmlAccessorType(XmlAccessType.FIELD) + public static class A { + String x = "X"; + + public A() { + + } + public A(String x) { + this.x = x; + } + public boolean equals(Object other){ + return other != null && other instanceof A && Objects.equals(((A) other).x, x); + + } + } + + + @XmlAccessorType(XmlAccessType.FIELD) + @JsonSerialize(using = IterableJson.Serializer.class) + @JsonDeserialize(using = AIterable.Deserializer.class) + public static class AIterable implements Iterable { + public static class Deserializer extends IterableJson.Deserializer { + Deserializer() { + super(AIterable::new, A.class); + } + } + + List values; + + public AIterable() { + + } + + public AIterable(List v) { + values = v; + } + + @NonNull + @Override + public Iterator iterator() { + return values.iterator(); + + } + + } + + + @XmlAccessorType(XmlAccessType.FIELD) + @JsonSerialize(using = IterableJson.Serializer.class) + @JsonDeserialize(using = StringIterable.Deserializer.class) + public static class StringIterable implements Iterable { + public static class Deserializer extends IterableJson.Deserializer { + Deserializer() { + super(StringIterable::new, String.class); + } + } + + List values; + + public StringIterable() { + + } + + public StringIterable(@NonNull List v) { + values = v; + } + + @Override + @NonNull + public Iterator iterator() { + return values.iterator(); + + } + + } + + @XmlAccessorType(XmlAccessType.FIELD) + static class Container { + private StringIterable testList; + + public Container() { + + } + public Container(StringIterable tl) { + this.testList = tl; + } + } + + + @XmlAccessorType(XmlAccessType.FIELD) + static class AContainer { + private AIterable testList; + + public AContainer() { + + } + + public AContainer(AIterable tl) { + this.testList = tl; + } + } + + + @Test + public void testGetAValueJson() throws Exception { + AIterable in = new AIterable(Arrays.asList(new A("x"), new A("y"))); + + AIterable out = roundTripAndSimilar(in, "[{\"x\":\"x\"},{\"x\":\"y\"}]"); + + assertThat(out.values).isEqualTo(in.values); + + } + + @Test + public void testGetAValueJsonSingleValue() throws Exception { + AIterable in = new AIterable(Collections.singletonList(new A("a"))); + + AContainer out = roundTripAndSimilar(new AContainer(in), "{\"testList\":{\"x\":\"a\"}}"); + + assertThat(out.testList.values).isEqualTo(in.values); + + } + + + @Test + public void testGetValueJson() throws Exception { + StringIterable in = new StringIterable(Arrays.asList("a", "b")); + + StringIterable out = roundTripAndSimilar(in, "[\"a\",\"b\"]"); + + assertThat(out.values).isEqualTo(in.values); + + } + + @Test + public void testGetValueJsonSingleValue() throws Exception { + StringIterable in = new StringIterable(Collections.singletonList("a")); + + Container out = roundTripAndSimilar(new Container(in), "{\"testList\":\"a\"}"); + + assertThat(out.testList.values).isEqualTo(in.values); + + } + + @SuppressWarnings("unchecked") + static T roundTripAndSimilar(T input, String expected) throws Exception { + StringWriter writer = new StringWriter(); + Jackson3Mapper.getInstance().writeValue(writer, input); + + String text = writer.toString(); + + JSONAssert.assertEquals("\n" + text + "\nis different from expected\n" + expected, expected, text, JSONCompareMode.LENIENT); + + return (T) Jackson3Mapper.getInstance().readValue(text, input.getClass()); + + } + + +} diff --git a/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/Jackson2MapperTest.java b/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/Jackson2MapperTest.java new file mode 100644 index 000000000..aeddda62e --- /dev/null +++ b/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/Jackson2MapperTest.java @@ -0,0 +1,95 @@ +package nl.vpro.jackson3; + +import lombok.extern.log4j.Log4j2; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Optional; + +import jakarta.xml.bind.annotation.*; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.exc.InvalidFormatException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@Log4j2 +public class Jackson2MapperTest { + + public enum EnumValues { + a, + b + } + + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + @XmlRootElement + public static class A { + @XmlElement + int integer; + + @XmlAttribute + EnumValues enumValue; + + @XmlElement + Optional optional; + } + + @Test + public void read() throws IOException { + String example = "/* leading comments */\n{'integer': 2 /* ignore comments */, 'optional': 3}"; + A a = Jackson3Mapper.getInstance().readValue(example, A.class); + assertThat(a.integer).isEqualTo(2); + assertThat(a.optional).isPresent(); + assertThat(a.optional.get()).isEqualTo(3); + + Jackson3Mapper.getLenientInstance().readerFor(A.class).readValue(example.getBytes(StandardCharsets.UTF_8)); + } + + @Test + public void readIntFromString() throws IOException { + A a = Jackson3Mapper.getInstance().readValue("{'integer': '2'}", A.class); + assertThat(a.integer).isEqualTo(2); + } + + @Test + public void readEnumValue() throws IOException { + A a = Jackson3Mapper.getInstance().readValue("{'enumValue': 'a'}", A.class); + assertThat(a.enumValue).isEqualTo(EnumValues.a); + } + + @Test + public void readUnknownEnumValue() throws IOException { + assertThatThrownBy(() -> { + A a = Jackson3Mapper.getInstance().readValue("{'enumValue': 'c'}", A.class); + assertThat(a.enumValue).isNull(); + }).isInstanceOf(InvalidFormatException.class); + } + + @Test + public void readUnknownEnumValueLenient() throws IOException { + A a = Jackson3Mapper.getLenientInstance().readValue("{'enumValue': 'c'}", A.class); + assertThat(a.enumValue).isNull(); + } + + @Test + public void write() throws JsonProcessingException { + A a = new A(); + a.integer = 2; + a.optional = Optional.of(3); + assertThat(Jackson3Mapper.getInstance().writeValueAsString(a)).isEqualTo("{\"integer\":2,\"optional\":3}"); + } + + @Test + public void writeWithEmptyOptional() throws JsonProcessingException { + A a = new A(); + a.integer = 2; + a.optional = Optional.empty(); + assertThat(Jackson3Mapper.getInstance().writeValueAsString(a)).isEqualTo("{\"integer\":2}"); + } + + + +} diff --git a/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/JsonArrayIteratorTest.java b/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/JsonArrayIteratorTest.java new file mode 100644 index 000000000..6fe9700fd --- /dev/null +++ b/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/JsonArrayIteratorTest.java @@ -0,0 +1,247 @@ +package nl.vpro.jackson3; + +import lombok.*; +import lombok.extern.slf4j.Slf4j; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.NoSuchElementException; +import java.util.Optional; + +import jakarta.xml.bind.annotation.XmlAccessType; +import jakarta.xml.bind.annotation.XmlAccessorType; + +import org.apache.commons.io.IOUtils; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import org.skyscreamer.jsonassert.JSONCompareMode; + +import static java.util.Objects.requireNonNull; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +@SuppressWarnings("DataFlowIssue") +@Slf4j +public class JsonArrayIteratorTest { + + @Test + public void test() throws IOException { + + //Jackson2Mapper.getInstance().writeValue(System.out, new Change("bla", false)); + try (JsonArrayIterator it = JsonArrayIterator.builder().inputStream(getClass().getResourceAsStream("/changes.json")).valueClass(Change.class).objectMapper(Jackson3Mapper.getInstance()).build()) { + assertThat(it.next().getMid()).isEqualTo("POMS_NCRV_1138990"); // 1 + assertThat(it.getCount()).isEqualTo(1); + assertThat(it.getSize()).hasValueSatisfying(size -> assertThat(size).isEqualTo(14)); + for (int i = 0; i < 9; i++) { + assertThat(it.hasNext()).isTrue(); + + Change change = it.next(); // 10 + Optional size = it.getSize(); + size.ifPresent(aLong -> + log.info(it.getCount() + "/" + aLong + " :" + change) + ); + if (!change.isDeleted()) { + assertThat(change.getMedia()).isNotNull(); + } + } + assertThat(it.hasNext()).isTrue(); // 11 + assertThat(it.next().getMid()).isEqualTo("POMS_VPRO_1139788"); + assertThat(it.hasNext()).isFalse(); + } + } + + @SuppressWarnings("ConstantConditions") + @Test + public void testEmpty() throws IOException { + try (JsonArrayIterator it = new JsonArrayIterator<>(new ByteArrayInputStream("{\"array\":[]}".getBytes()), Change.class)) { + assertThat(it.hasNext()).isFalse(); + assertThat(it.hasNext()).isFalse(); + assertThat(it.getCount()).isEqualTo(0); + assertThatThrownBy(it::next).isInstanceOf(NoSuchElementException.class); + } + } + + @Test + public void testNulls() throws IOException { + JsonArrayIterator it = new JsonArrayIterator<>(new ByteArrayInputStream("{\"array\":[null, {}, null, {}]}".getBytes()), Change.class); + assertThat(it.hasNext()).isTrue(); + it.next(); + assertThat(it.peek()).isEqualTo(new Change()); + assertThat(it.getCount()).isEqualTo(2); + assertThat(it.hasNext()).isTrue(); + it.next(); + assertThat(it.getCount()).isEqualTo(4); + assertThat(it.hasNext()).isFalse(); + assertThat(it.getCount()).isEqualTo(4); + assertThatThrownBy(it::next).isInstanceOf(NoSuchElementException.class); + + } + + @Test + public void testIncompleteJson() throws IOException { + InputStream input = getClass().getResourceAsStream("/incomplete_changes.json"); + assert input != null; + try (JsonArrayIterator it = JsonArrayIterator.builder() + .inputStream(input) + .valueClass(Change.class) + .build()) { + + assertThatExceptionOfType(RuntimeException.class).isThrownBy(() -> { + while (it.hasNext()) it.next(); + }); + assertThat(it.hasNext()).isFalse(); + assertThatThrownBy(it::next).isInstanceOf(NoSuchElementException.class); + } + } + + + @SuppressWarnings("FinalizeCalledExplicitly") + @Test + public void testZeroBytes() throws IOException { + + JsonArrayIterator it = new JsonArrayIterator<>(new ByteArrayInputStream(new byte[0]), Change.class); + + assertThat(it.hasNext()).isFalse(); + assertThatThrownBy(it::next).isInstanceOf(NoSuchElementException.class); + it.close(); + } + + @Test + public void callback() throws IOException { + Runnable callback = mock(Runnable.class); + JsonArrayIterator it = new JsonArrayIterator<>(getClass().getResourceAsStream("/changes.json"), Change.class, callback); + while (it.hasNext()) { + verify(callback, times(0)).run(); + it.next(); + } + assertThat(it.getSize()).hasValueSatisfying(size -> assertThat(size).isEqualTo(it.getCount())); + verify(callback, times(1)).run(); + } + + @Test + public void write() throws IOException, JSONException { + try (JsonArrayIterator it = JsonArrayIterator + .builder() + .inputStream(getClass().getResourceAsStream("/changes.json")) + .valueClass(Change.class) + .objectMapper(Jackson3Mapper.getInstance()) + .skipNulls(false) + .build(); + ByteArrayOutputStream out = new ByteArrayOutputStream()) { + + it.write(out, (c) -> { + log.info("{}", c); + }); + String expected = "{'array': " + IOUtils.resourceToString("/array_from_changes.json", StandardCharsets.UTF_8) + "}"; + JSONAssert.assertEquals(expected, out.toString(), JSONCompareMode.STRICT); + } + } + + @Test + public void writeArray() throws IOException, JSONException { + try (JsonArrayIterator it = JsonArrayIterator + .builder() + .inputStream(getClass().getResourceAsStream("/changes.json")) + .valueClass(Change.class) + .objectMapper(Jackson3Mapper.getInstance()) + .logger(log) + .skipNulls(false) + .build(); + ByteArrayOutputStream out = new ByteArrayOutputStream()) { + + it.writeArray(out, (c) -> { + log.info("{}", c); + }); + String expected = IOUtils.resourceToString("/array_from_changes.json", StandardCharsets.UTF_8); + JSONAssert.assertEquals(expected, out.toString(), JSONCompareMode.STRICT); + } + } + + @Test + public void illegalConstruction() { + assertThatThrownBy(() -> { + try (JsonArrayIterator js = JsonArrayIterator.builder().build()) { + log.info("{}", js); + } + }).isInstanceOf(IllegalArgumentException.class); + + assertThatThrownBy(() -> { + try (JsonArrayIterator js = JsonArrayIterator + .builder() + .inputStream(requireNonNull(getClass().getResourceAsStream("/changes.json"))) + .valueClass(Change.class) + .valueCreator((jp, on) -> null) + .build()) { + log.info("{}", js); + + } + }).isInstanceOf(IllegalArgumentException.class); + } + + + @Test + public void interrupt() throws IOException { + byte[] bytes = "[{},{},{},{},{},{}]".getBytes(StandardCharsets.UTF_8); + final String[] callback = new String[1]; + try (JsonArrayIterator i = JsonArrayIterator.builder() + .inputStream(new InputStream() { + int i = 0; + @Override + public int read() throws IOException { + if (i == 6) { + throw new InterruptedIOException(); + } + if (i < bytes.length) { + return bytes[i++]; + } else { + return -1; + } + } + }) + .callback(() -> { + callback[0] = "called"; + }) + .valueClass(Simple.class) + .build()) { + log.info("{}", i.next()); + log.info("{}", i.next()); + assertThatThrownBy(i::next) + .isInstanceOf(RuntimeException.class); + assertThat(callback[0]).isEqualTo("called"); + } + } + + @XmlAccessorType(XmlAccessType.FIELD) + @Getter + @Setter + private static class Simple { + private String value; + } + + + + @XmlAccessorType(XmlAccessType.FIELD) + @Getter + @Setter + @EqualsAndHashCode + private static class Change { + + private String mid; + private boolean deleted; + private Object media; + + public Change() { + + } + public Change(String mid, boolean deleted) { + this.mid = mid; + this.deleted = deleted; + } + + @Override + public String toString() { + return mid; + } + } +} diff --git a/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/LenientBooleanDeserializerTest.java b/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/LenientBooleanDeserializerTest.java new file mode 100644 index 000000000..65e580b83 --- /dev/null +++ b/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/LenientBooleanDeserializerTest.java @@ -0,0 +1,54 @@ +package nl.vpro.jackson3; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +import nl.vpro.jackson3.LenientBooleanDeserializer; +import static org.assertj.core.api.Assertions.assertThat; + + +/** + * @author Michiel Meeuwissen + * @since 2.3 + */ +public class LenientBooleanDeserializerTest { + + public static class A { + @JsonDeserialize(using = LenientBooleanDeserializer.class) + Boolean bool; + + @JsonDeserialize(using = LenientBooleanDeserializer.class) + boolean b; + + } + ObjectMapper mapper = new ObjectMapper(); + + @Test + public void deserialize() throws IOException { + + assertThat(mapper.readValue("{\"bool\": 1}", A.class).bool).isTrue(); + assertThat(mapper.readValue("{\"bool\": true}", A.class).bool).isTrue(); + assertThat(mapper.readValue("{\"bool\": \"1\"}", A.class).bool).isTrue(); + + assertThat(mapper.readValue("{\"bool\": 0}", A.class).bool).isFalse(); + assertThat(mapper.readValue("{\"bool\": false}", A.class).bool).isFalse(); + assertThat(mapper.readValue("{\"bool\": \"0\"}", A.class).bool).isFalse(); + + } + + @Test + public void deserializeNull() throws IOException { + ObjectMapper mapper = new ObjectMapper(); + assertThat(mapper.readValue("{\"bool\": null}", A.class).bool).isNull(); + assertThat(mapper.readValue("{\"bool\": null}", A.class).b).isFalse(); + assertThat(mapper.readValue("{\"b\": \"x\"}", A.class).b).isFalse(); + + + + + } +} diff --git a/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/StringInstantToJsonTimestampTest.java b/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/StringInstantToJsonTimestampTest.java new file mode 100644 index 000000000..a49de3caf --- /dev/null +++ b/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/StringInstantToJsonTimestampTest.java @@ -0,0 +1,48 @@ +package nl.vpro.jackson3; + +import lombok.extern.slf4j.Slf4j; + +import java.time.*; +import java.time.temporal.ChronoUnit; + +import nl.vpro.jackson3.StringInstantToJsonTimestamp; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.*; + +/** + * @author Michiel Meeuwissen + */ +@Slf4j +public class StringInstantToJsonTimestampTest { + + @Test + public void testOk() { + Instant instant = StringInstantToJsonTimestamp.parseDateTime("2017-05-24T16:30:00+02:00"); + assertThat(instant).isEqualTo( + LocalDateTime.of(2017, 5, 24, 16, 30, 0).atZone(ZoneId.of("Europe/Amsterdam")).toInstant()); + log.debug("{}", instant); + } + + @Test // natty does succeed in parsing this + public void testOdd() { + Instant instant = StringInstantToJsonTimestamp.parseDateTime("0737-05-22T14:35:55+00:19:32"); // Of course, that is a very odd timezone + + assertThat(instant).isEqualTo( + LocalDateTime.of(737, 5, 26, 14, 16, 55).atZone(ZoneId.of("UTC")).toInstant()); + log.debug("{}", instant); + } + + @Test + public void testNotOk() { + assertThatThrownBy(() -> { + StringInstantToJsonTimestamp.parseDateTime("dit is echt geen datum"); + }).isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void testNattyNow() { + assertThat(StringInstantToJsonTimestamp.parseDateTime("now")).isCloseTo(Instant.now(), within(10, ChronoUnit.SECONDS)); + } + +} diff --git a/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/StringZonedLocalDateToJsonTimestampTest.java b/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/StringZonedLocalDateToJsonTimestampTest.java new file mode 100644 index 000000000..fa8f4e2da --- /dev/null +++ b/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/StringZonedLocalDateToJsonTimestampTest.java @@ -0,0 +1,10 @@ +package nl.vpro.jackson3; + + +/** + * @author Michiel Meeuwissen + * @since 2.5 + */ +public class StringZonedLocalDateToJsonTimestampTest { + +} diff --git a/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/rs/JsonIdAdderBodyReaderTest.java b/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/rs/JsonIdAdderBodyReaderTest.java new file mode 100644 index 000000000..cdf05ada9 --- /dev/null +++ b/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/rs/JsonIdAdderBodyReaderTest.java @@ -0,0 +1,92 @@ +package nl.vpro.jackson3.rs; + +import java.io.ByteArrayInputStream; +import java.io.IOException; + +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.ext.Providers; + +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.databind.ObjectMapper; + +import nl.vpro.jackson3.rs.JacksonContextResolver; +import nl.vpro.jackson3.rs.JsonIdAdderBodyReader; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +/** + * @author Michiel Meeuwissen + * @since 2.7 + */ +public class JsonIdAdderBodyReaderTest { + + @JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "objectType" + ) + @JsonSubTypes( + { + @JsonSubTypes.Type(value = A.class, name = "a"), + @JsonSubTypes.Type(value = B.class, name = "b") + }) + public interface BaseI { + + } + + public static abstract class Base implements BaseI { + + } + + @JsonTypeName("a") + public static class A extends Base { + + } + @JsonTypeName("b") + public static class B extends Base { + + } + public static class C { + + } + final JsonIdAdderBodyReader idAdderInterceptor = new JsonIdAdderBodyReader(); + { + idAdderInterceptor.providers = Mockito.mock(Providers.class); + when(idAdderInterceptor.providers.getContextResolver(ObjectMapper.class, MediaType.APPLICATION_JSON_TYPE)).thenReturn(new JacksonContextResolver()); + } + + @Test + public void testA() throws IOException { + + Object o = idAdderInterceptor.readFrom(Object.class, + A.class, + null, + MediaType.APPLICATION_JSON_TYPE, null, new ByteArrayInputStream("{}".getBytes())); + assertThat(o).isInstanceOf(A.class); + } + + + + @Test + public void testBase() throws IOException { + + Object o = idAdderInterceptor.readFrom(Object.class, + Base.class, + null, + MediaType.APPLICATION_JSON_TYPE, null, new ByteArrayInputStream("{'objectType': 'a'}".getBytes())); + assertThat(o).isInstanceOf(A.class); + } + @Test + public void testC() throws IOException { + + Object o = idAdderInterceptor.readFrom(Object.class, + C.class, + null, + MediaType.APPLICATION_JSON_TYPE, null, new ByteArrayInputStream("{}".getBytes())); + assertThat(o).isInstanceOf(C.class); + } + +} From 59bfbe36a792697115f7e8e483957908de4c6041 Mon Sep 17 00:00:00 2001 From: Michiel Meeuwissen Date: Tue, 25 Nov 2025 20:32:29 +0100 Subject: [PATCH 02/18] Fixing all kind of changes because of jackson2 -> 3 --- vpro-shared-jackson3/pom.xml | 6 +++ .../{jackson3 => jackson}/NattySupport.java | 3 +- .../nl/vpro/{jackson3 => jackson}/Views.java | 3 +- .../jackson3/BackwardsCompatibleJsonEnum.java | 2 +- .../nl/vpro/jackson3/DateToJsonTimestamp.java | 7 +-- .../jackson3/DurationToJsonTimestamp.java | 28 ++++++------ .../DurationToSecondsFloatTimestamp.java | 19 +++----- .../nl/vpro/jackson3/GuavaRangeModule.java | 39 ++++++++-------- .../vpro/jackson3/InstantToJsonTimestamp.java | 26 +++++------ .../InstantToSecondsFloatTimestamp.java | 24 +++++----- .../java/nl/vpro/jackson3/IterableJson.java | 28 ++++++------ .../java/nl/vpro/jackson3/Jackson3Mapper.java | 44 ++++++------------- .../nl/vpro/jackson3/JsonArrayIterator.java | 8 ++-- .../jackson3/LenientBooleanDeserializer.java | 29 +++++------- .../LocalDateTimeToJsonDateWithSpace.java | 23 +++++----- .../nl/vpro/jackson3/LocalDateToJsonDate.java | 21 ++++----- .../StringDurationToJsonTimestamp.java | 21 ++++----- .../StringInstantToJsonTimestamp.java | 6 ++- .../StringZonedLocalDateToJsonTimestamp.java | 19 ++++---- .../src/main/java/nl/vpro/jackson3/Utils.java | 5 ++- .../jackson3/XMLDurationToJsonTimestamp.java | 41 +++++++---------- .../ZonedDateTimeToJsonTimestamp.java | 25 +++++------ .../jackson3/rs/JsonIdAdderBodyReader.java | 10 ++--- 23 files changed, 191 insertions(+), 246 deletions(-) rename vpro-shared-jackson3/src/main/java/nl/vpro/{jackson3 => jackson}/NattySupport.java (92%) rename vpro-shared-jackson3/src/main/java/nl/vpro/{jackson3 => jackson}/Views.java (95%) diff --git a/vpro-shared-jackson3/pom.xml b/vpro-shared-jackson3/pom.xml index 66c292a24..74b8b490f 100644 --- a/vpro-shared-jackson3/pom.xml +++ b/vpro-shared-jackson3/pom.xml @@ -77,5 +77,11 @@ commons-io true + + com.fasterxml.jackson.core + jackson-core + 2.20.1 + compile + diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/NattySupport.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson/NattySupport.java similarity index 92% rename from vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/NattySupport.java rename to vpro-shared-jackson3/src/main/java/nl/vpro/jackson/NattySupport.java index 4929391bf..ced1ca497 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/NattySupport.java +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson/NattySupport.java @@ -1,4 +1,4 @@ -package nl.vpro.jackson3; +package nl.vpro.jackson; import lombok.extern.slf4j.Slf4j; @@ -8,6 +8,7 @@ import org.natty.DateGroup; import org.natty.Parser; +import nl.vpro.jackson3.StringInstantToJsonTimestamp; import nl.vpro.util.BindingUtils; import nl.vpro.util.DateUtils; diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/Views.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson/Views.java similarity index 95% rename from vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/Views.java rename to vpro-shared-jackson3/src/main/java/nl/vpro/jackson/Views.java index 13b7c156c..459d19694 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/Views.java +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson/Views.java @@ -1,6 +1,7 @@ -package nl.vpro.jackson3; +package nl.vpro.jackson; import com.google.common.annotations.Beta; +import nl.vpro.jackson3.Jackson3Mapper; /** * Several setups at VPRO and at NPO involve a backend system that publishes JSON to ElasticSearch. diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/BackwardsCompatibleJsonEnum.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/BackwardsCompatibleJsonEnum.java index e2a6cccbe..6c1785bd3 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/BackwardsCompatibleJsonEnum.java +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/BackwardsCompatibleJsonEnum.java @@ -9,7 +9,7 @@ /** * Newer jackson version suddenly recognized @XmlEnumValue. This makes it possible to fall back to old behaviour. - * {@link nl.vpro.domain.media.support.Workflow} + * {@code nl.vpro.domain.media.support.Workflow} */ @Slf4j public class BackwardsCompatibleJsonEnum { diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/DateToJsonTimestamp.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/DateToJsonTimestamp.java index dcbc332ff..19f787198 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/DateToJsonTimestamp.java +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/DateToJsonTimestamp.java @@ -1,14 +1,11 @@ package nl.vpro.jackson3; -import java.io.IOException; -import java.io.Serial; -import java.util.Date; - import tools.jackson.core.*; import tools.jackson.databind.*; - import tools.jackson.databind.deser.std.StdDeserializer; +import java.util.Date; + public class DateToJsonTimestamp { diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/DurationToJsonTimestamp.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/DurationToJsonTimestamp.java index a0e6ec6ab..b295ed60f 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/DurationToJsonTimestamp.java +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/DurationToJsonTimestamp.java @@ -1,14 +1,11 @@ package nl.vpro.jackson3; -import java.io.IOException; +import tools.jackson.core.*; +import tools.jackson.databind.*; + import java.time.Duration; import java.util.Calendar; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonToken; -import com.fasterxml.jackson.databind.*; - import nl.vpro.util.TimeUtils; /** @@ -20,28 +17,29 @@ public class DurationToJsonTimestamp { private DurationToJsonTimestamp() {} - public static class Serializer extends JsonSerializer { + public static class Serializer extends ValueSerializer { public static final Serializer INSTANCE = new Serializer(); @Override - public void serialize(Duration value, JsonGenerator jgen, SerializerProvider provider) throws IOException { + public void serialize(Duration value, JsonGenerator jgen, SerializationContext ctxt) throws JacksonException { if (value == null) { jgen.writeNull(); } else { jgen.writeNumber(value.toMillis()); } } + } - public static class XmlSerializer extends JsonSerializer { + public static class XmlSerializer extends ValueSerializer { public static final Serializer INSTANCE = new Serializer(); @Override - public void serialize(javax.xml.datatype.Duration value, JsonGenerator jgen, SerializerProvider provider) throws IOException { + public void serialize(javax.xml.datatype.Duration value, JsonGenerator jgen, SerializationContext ctxt) throws JacksonException { if (value == null) { jgen.writeNull(); } else { @@ -51,16 +49,16 @@ public void serialize(javax.xml.datatype.Duration value, JsonGenerator jgen, Ser } - public static class Deserializer extends JsonDeserializer { + public static class Deserializer extends ValueDeserializer { public static final Deserializer INSTANCE = new Deserializer(); @Override - public Duration deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException { - if (jp.getCurrentToken() == JsonToken.VALUE_STRING) { - if (jp.getText().isEmpty() && ctxt.hasDeserializationFeatures(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT.getMask())) { + public Duration deserialize(JsonParser jp, DeserializationContext ctxt) { + if (jp.currentToken() == JsonToken.VALUE_STRING) { + if (jp.getString().isEmpty() && ctxt.hasDeserializationFeatures(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT.getMask())) { return null; } else { - return TimeUtils.parseDuration(jp.getText()).orElseThrow(); + return TimeUtils.parseDuration(jp.getString()).orElseThrow(); } } else { return Duration.ofMillis(jp.getLongValue()); diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/DurationToSecondsFloatTimestamp.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/DurationToSecondsFloatTimestamp.java index f1d455a6d..1505e6612 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/DurationToSecondsFloatTimestamp.java +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/DurationToSecondsFloatTimestamp.java @@ -1,14 +1,9 @@ package nl.vpro.jackson3; -import java.io.IOException; -import java.time.Duration; +import tools.jackson.core.*; +import tools.jackson.databind.*; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; +import java.time.Duration; /** * Default Jackson serialized Durations as seconds. In poms we used to serialize durations as Dates, and hence as _milliseconds_. @@ -19,13 +14,13 @@ public class DurationToSecondsFloatTimestamp { private DurationToSecondsFloatTimestamp() {} - public static class Serializer extends JsonSerializer { + public static class Serializer extends ValueSerializer { public static final Serializer INSTANCE = new Serializer(); @Override - public void serialize(Duration value, JsonGenerator jgen, SerializerProvider provider) throws IOException { + public void serialize(Duration value, JsonGenerator jgen, SerializationContext ctxt) throws JacksonException { if (value == null) { jgen.writeNull(); } else { @@ -35,11 +30,11 @@ public void serialize(Duration value, JsonGenerator jgen, SerializerProvider pro } - public static class Deserializer extends JsonDeserializer { + public static class Deserializer extends ValueDeserializer { public static final Deserializer INSTANCE = new Deserializer(); @Override - public Duration deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException { + public Duration deserialize(JsonParser jp, DeserializationContext ctxt) { return Duration.ofMillis((long) (Float.parseFloat(jp.getValueAsString()) * 1000)); } } diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/GuavaRangeModule.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/GuavaRangeModule.java index 11379c9fc..26fb031c2 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/GuavaRangeModule.java +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/GuavaRangeModule.java @@ -1,16 +1,17 @@ package nl.vpro.jackson3; import lombok.SneakyThrows; +import tools.jackson.core.*; +import tools.jackson.databind.*; +import tools.jackson.databind.deser.std.StdDeserializer; +import tools.jackson.databind.module.SimpleModule; +import tools.jackson.databind.ser.std.StdSerializer; +import tools.jackson.databind.type.CollectionLikeType; +import tools.jackson.databind.type.SimpleType; import java.io.IOException; import java.io.Serial; -import com.fasterxml.jackson.core.*; -import com.fasterxml.jackson.databind.*; -import com.fasterxml.jackson.databind.deser.std.StdDeserializer; -import com.fasterxml.jackson.databind.module.SimpleModule; -import com.fasterxml.jackson.databind.type.CollectionLikeType; -import com.fasterxml.jackson.databind.type.SimpleType; import com.google.common.collect.BoundType; import com.google.common.collect.Range; @@ -30,7 +31,7 @@ public GuavaRangeModule() { addDeserializer(Range.class, Deserializer.INSTANCE); } - public static class Serializer extends com.fasterxml.jackson.databind.ser.std.StdSerializer> { + public static class Serializer extends StdSerializer> { @Serial private static final long serialVersionUID = -4394016847732058088L; @@ -46,7 +47,7 @@ protected Serializer() { } @Override - public void serialize(Range value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + public void serialize(Range value, JsonGenerator gen, SerializationContext provider) throws JacksonException { if (value == null) { gen.writeNull(); } else { @@ -55,26 +56,26 @@ public void serialize(Range value, JsonGenerator gen, SerializerProvider seri if (value.hasLowerBound()) { type = value.lowerEndpoint().getClass(); - gen.writeObjectField(LOWER_ENDPOINT, value.lowerEndpoint()); - gen.writeObjectField(LOWER_BOUND_TYPE, value.lowerBoundType()); + gen.writePOJOProperty(LOWER_ENDPOINT, value.lowerEndpoint()); + gen.writePOJOProperty(LOWER_BOUND_TYPE, value.lowerBoundType()); } if (value.hasUpperBound()) { type = value.upperEndpoint().getClass(); - gen.writeObjectField(UPPER_ENDPOINT, value.upperEndpoint()); - gen.writeObjectField(UPPER_BOUND_TYPE, value.upperBoundType()); + gen.writePOJOProperty(UPPER_ENDPOINT, value.upperEndpoint()); + gen.writePOJOProperty(UPPER_BOUND_TYPE, value.upperBoundType()); } if (type != null) { - gen.writeObjectField("type", type.getName()); + gen.writeStringProperty("type", type.getName()); } gen.writeEndObject(); } } + + } public static class Deserializer extends StdDeserializer> { - @Serial - private static final long serialVersionUID = -4394016847732058088L; public static Deserializer INSTANCE = new Deserializer(); @@ -91,14 +92,14 @@ protected Deserializer() { @SneakyThrows @Override - public Range deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + public Range deserialize(JsonParser p, DeserializationContext ctxt) { if (p.currentToken() == JsonToken.START_OBJECT) { p.nextToken(); } JsonNode node = ctxt.readValue(p, JsonNode.class); if (node.has("type")) { - Class type = Class.forName(node.get("type").textValue()); + Class type = Class.forName(node.get("type").stringValue()); return of(type, p, node); } else { return Range.all(); @@ -116,11 +117,11 @@ static > Range of(Class clazz, JsonParser p, JsonN if (node.has(LOWER_ENDPOINT)) { lowerBoundType = BoundType.valueOf(node.get(LOWER_BOUND_TYPE).asText()); - lowerValue = p.getCodec().treeToValue(node.get(LOWER_ENDPOINT), clazz); + lowerValue = p.objectReadContext().readValue(p, node.get(LOWER_ENDPOINT), clazz); } if (node.has(UPPER_ENDPOINT)) { upperBoundType = BoundType.valueOf(node.get(UPPER_BOUND_TYPE).asText()); - upperValue = p.getCodec().treeToValue(node.get(UPPER_ENDPOINT), clazz); + upperValue = p.(UPPER_ENDPOINT), clazz); } if (lowerValue != null) { if (upperValue != null) { diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/InstantToJsonTimestamp.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/InstantToJsonTimestamp.java index 5b711ef5e..149114e11 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/InstantToJsonTimestamp.java +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/InstantToJsonTimestamp.java @@ -1,48 +1,44 @@ package nl.vpro.jackson3; -import java.io.IOException; +import tools.jackson.core.*; +import tools.jackson.core.exc.InputCoercionException; +import tools.jackson.databind.*; + import java.time.Instant; import java.time.ZonedDateTime; import java.time.format.DateTimeParseException; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParseException; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; - public class InstantToJsonTimestamp { private InstantToJsonTimestamp() {} - public static class Serializer extends JsonSerializer { + public static class Serializer extends ValueSerializer { public static final Serializer INSTANCE = new Serializer(); @Override - public void serialize(Instant value, JsonGenerator jgen, SerializerProvider provider) throws IOException { + public void serialize(Instant value, JsonGenerator jgen, SerializationContext ctxt) throws JacksonException { if (value == null) { jgen.writeNull(); } else { jgen.writeNumber(value.toEpochMilli()); } } + + } - public static class Deserializer extends JsonDeserializer { + public static class Deserializer extends ValueDeserializer { public static final Deserializer INSTANCE = new Deserializer(); @Override - public Instant deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException { + public Instant deserialize(JsonParser jp, DeserializationContext ctxt) { try { return Instant.ofEpochMilli(jp.getLongValue()); - } catch (JsonParseException jpe) { + } catch (InputCoercionException jpe) { try { String s = jp.getValueAsString(); if (s == null) { diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/InstantToSecondsFloatTimestamp.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/InstantToSecondsFloatTimestamp.java index bace9eee6..99fd5dc32 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/InstantToSecondsFloatTimestamp.java +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/InstantToSecondsFloatTimestamp.java @@ -5,21 +5,21 @@ import java.time.ZonedDateTime; import java.time.format.DateTimeParseException; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParseException; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; +import tools.jackson.core.JsonGenerator; +import tools.jackson.core.JsonParseException; +import tools.jackson.core.JsonParser; +import tools.jackson.core.JsonProcessingException; +import tools.jackson.databind.*; +import tools.jackson.databind.JsonDeserializer; +import tools.jackson.databind.JsonSerializer; +import tools.jackson.databind.SerializerProvider; public class InstantToSecondsFloatTimestamp { private InstantToSecondsFloatTimestamp() {} - public static class Serializer extends JsonSerializer { + public static class Serializer extends ValueSerializer { public static final Serializer INSTANCE = new Serializer(); @@ -35,14 +35,14 @@ public void serialize(Instant value, JsonGenerator jgen, SerializerProvider prov } - public static class Deserializer extends JsonDeserializer { + public static class Deserializer extends ValueDeserializer { public static final Deserializer INSTANCE = new Deserializer(); @Override - public Instant deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException { + public Instant deserialize(JsonParser jp, DeserializationContext ctxt) { try { return Instant.ofEpochMilli((long) Float.parseFloat(jp.getValueAsString()) * 1000); - } catch (JsonParseException jpe) { + } catch ( jpe) { try { String s = jp.getValueAsString(); if (s == null) { diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/IterableJson.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/IterableJson.java index c28252a48..b78d94b4e 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/IterableJson.java +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/IterableJson.java @@ -4,12 +4,12 @@ import java.util.*; import java.util.function.Function; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; +import tools.jackson.core.JsonGenerator; +import tools.jackson.core.JsonParser; +import tools.jackson.databind.*; +import tools.jackson.databind.JsonDeserializer; +import tools.jackson.databind.JsonSerializer; +import tools.jackson.databind.SerializerProvider; /** * @author Michiel Meeuwissen @@ -18,7 +18,7 @@ public class IterableJson { - public static class Serializer extends JsonSerializer> { + public static class Serializer extends ValueSerializer> { @Override public void serialize(Iterable value, JsonGenerator jgen, SerializerProvider provider) throws IOException { @@ -30,12 +30,12 @@ public void serialize(Iterable value, JsonGenerator jgen, SerializerProvider pro if (i.hasNext()) { v = i.next(); if (! i.hasNext()) { - jgen.writeObject(v); + jgen.writeEmbeddedObject(v); } else { jgen.writeStartArray(); - jgen.writeObject(v); + jgen.writeEmbeddedObject(v); while (i.hasNext()) { - jgen.writeObject(i.next()); + jgen.writeEmbeddedObject(i.next()); } jgen.writeEndArray(); } @@ -48,7 +48,7 @@ public void serialize(Iterable value, JsonGenerator jgen, SerializerProvider pro } private static final Set> simpleTypes = new HashSet<>(Arrays.asList(String.class, Character.class, Boolean.class, Integer.class, Float.class, Long.class, Double.class)); - public static abstract class Deserializer extends JsonDeserializer> { + public static abstract class Deserializer extends ValueDeserializer> { private final Function, Iterable> creator; @@ -77,16 +77,16 @@ public Deserializer(Function, Iterable> supplier) { @Override public Iterable deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException { - if (jp.getParsingContext().inObject()) { + if (jp.streamReadContext().inObject()) { if (! isSimple) { jp.clearCurrentToken(); } T rs = jp.readValueAs(memberClass); return creator.apply(Collections.singletonList(rs)); - } else if (jp.getParsingContext().inArray()) { + } else if (jp.streamReadContext().inArray()) { List list = new ArrayList<>(); jp.clearCurrentToken(); - Iterator i = jp.readValuesAs(memberClass); + Iterator i = jp.read(memberClass); while (i.hasNext()) { list.add(i.next()); } diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/Jackson3Mapper.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/Jackson3Mapper.java index 2a194f596..db6806e1d 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/Jackson3Mapper.java +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/Jackson3Mapper.java @@ -7,6 +7,18 @@ import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; +import nl.vpro.jackson.Views; + +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonParser; +import tools.jackson.core.json.JsonReadFeature; +import tools.jackson.databind.*; +import tools.jackson.databind.introspect.AnnotationIntrospectorPair; +import tools.jackson.databind.introspect.JacksonAnnotationIntrospector; +import tools.jackson.databind.ser.PropertyFilter; +import tools.jackson.databind.ser.std.SimpleFilterProvider; +import tools.jackson.module.jakarta.xmlbind.JakartaXmlBindAnnotationIntrospector; + import java.io.IOException; import java.io.Serial; import java.lang.reflect.InvocationTargetException; @@ -16,19 +28,6 @@ import org.slf4j.event.Level; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.json.JsonReadFeature; -import com.fasterxml.jackson.databind.*; -import com.fasterxml.jackson.databind.Module; -import com.fasterxml.jackson.databind.introspect.AnnotationIntrospectorPair; -import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector; -import com.fasterxml.jackson.databind.ser.PropertyFilter; -import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider; -import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import com.fasterxml.jackson.module.jakarta.xmlbind.JakartaXmlBindAnnotationIntrospector; import com.google.common.annotations.Beta; import nl.vpro.logging.simple.SimpleLogger; @@ -58,23 +57,6 @@ public class Jackson3Mapper extends ObjectMapper { - @Deprecated - public static final Jackson3Mapper INSTANCE = getInstance(); - @Deprecated - public static final Jackson3Mapper LENIENT = getLenientInstance(); - @Deprecated - public static final Jackson3Mapper STRICT = getStrictInstance(); - @Deprecated - public static final Jackson3Mapper PRETTY_STRICT = getPrettyStrictInstance(); - - @Deprecated - public static final Jackson3Mapper PRETTY = getPrettyInstance(); - @Deprecated - public static final Jackson3Mapper PUBLISHER = getPublisherInstance(); - @Deprecated - public static final Jackson3Mapper PRETTY_PUBLISHER = getPublisherInstance(); - @Deprecated - public static final Jackson3Mapper BACKWARDS_PUBLISHER = getBackwardsPublisherInstance(); private static final ThreadLocal THREAD_LOCAL = ThreadLocal.withInitial(Jackson3Mapper::getInstance); @@ -175,7 +157,7 @@ public static void setThreadLocal(Jackson3Mapper set) { } } - @SneakyThrows({JsonProcessingException.class}) + @SneakyThrows({JacksonException.class}) public static T lenientTreeToValue(JsonNode jsonNode, Class clazz) { return getLenientInstance().treeToValue(jsonNode, clazz); } diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/JsonArrayIterator.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/JsonArrayIterator.java index ecde9057b..c363cd3de 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/JsonArrayIterator.java +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/JsonArrayIterator.java @@ -11,9 +11,9 @@ import org.checkerframework.checker.nullness.qual.Nullable; import org.slf4j.Logger; -import com.fasterxml.jackson.core.*; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.NullNode; +import tools.jackson.core.*; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.node.NullNode; import com.google.common.collect.PeekingIterator; import com.google.common.collect.UnmodifiableIterator; @@ -306,7 +306,7 @@ public static void write( final Function logging) throws IOException { try (JsonGenerator jg = Jackson3Mapper.getInstance().getFactory().createGenerator(out)) { jg.writeStartObject(); - jg.writeArrayFieldStart("array"); + jg.writeArrayPropertyStart("array"); writeObjects(iterator, jg, logging); jg.writeEndArray(); jg.writeEndObject(); diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/LenientBooleanDeserializer.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/LenientBooleanDeserializer.java index 5e3ebf066..3158d3a2f 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/LenientBooleanDeserializer.java +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/LenientBooleanDeserializer.java @@ -1,18 +1,14 @@ package nl.vpro.jackson3; -import java.io.IOException; - -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.JsonToken; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; +import tools.jackson.core.*; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.ValueDeserializer; /** * @author Michiel Meeuwissen * @since 2.3 */ -public class LenientBooleanDeserializer extends JsonDeserializer { +public class LenientBooleanDeserializer extends ValueDeserializer { public static final LenientBooleanDeserializer INSTANCE = new LenientBooleanDeserializer(); @@ -22,8 +18,8 @@ private LenientBooleanDeserializer() { @Override - public Boolean deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException { - JsonToken token = jsonParser.getCurrentToken(); + public Boolean deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws JacksonException { + JsonToken token = jsonParser.currentToken(); if (jsonParser.isNaN()) { return false; } @@ -32,14 +28,11 @@ public Boolean deserialize(JsonParser jsonParser, DeserializationContext deseria } else if (token.isNumeric()) { return jsonParser.getNumberValue().longValue() != 0; } else { - String text = jsonParser.getText().toLowerCase(); - switch(text) { - case "true": - case "1": - return true; - default: - return false; - } + String text = jsonParser.getString().toLowerCase(); + return switch (text) { + case "true", "1" -> true; + default -> false; + }; } } diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/LocalDateTimeToJsonDateWithSpace.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/LocalDateTimeToJsonDateWithSpace.java index 5ded11996..8c303c775 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/LocalDateTimeToJsonDateWithSpace.java +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/LocalDateTimeToJsonDateWithSpace.java @@ -1,15 +1,10 @@ package nl.vpro.jackson3; -import java.io.IOException; import java.time.LocalDateTime; import java.time.format.DateTimeParseException; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; +import tools.jackson.core.*; +import tools.jackson.databind.*; import nl.vpro.util.TimeUtils; @@ -21,34 +16,36 @@ public class LocalDateTimeToJsonDateWithSpace { private LocalDateTimeToJsonDateWithSpace() {} - public static class Serializer extends JsonSerializer { + public static class Serializer extends ValueSerializer { public static final Serializer INSTANCE = new Serializer(); @Override - public void serialize(LocalDateTime value, JsonGenerator jgen, SerializerProvider provider) throws IOException { + public void serialize(LocalDateTime value, JsonGenerator jgen, SerializationContext ctxt) throws JacksonException { if (value == null) { jgen.writeNull(); } else { jgen.writeString(value.toString().replaceFirst("T", " ")); } } + + } - public static class Deserializer extends JsonDeserializer { + public static class Deserializer extends ValueDeserializer { public static final Deserializer INSTANCE = new Deserializer(); @Override - public LocalDateTime deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException { - String text = jp.getText(); + public LocalDateTime deserialize(JsonParser jp, DeserializationContext ctxt) { + String text = jp.getString(); if (text == null) { return null; } else { try { return LocalDateTime.parse(text.replaceFirst(" ", "T")); } catch (DateTimeParseException dtf) { - return TimeUtils.parse(text).map(i -> i.atZone(TimeUtils.ZONE_ID).toLocalDateTime()).orElseThrow(() -> new IOException("Cannot parse " + text)); + return TimeUtils.parse(text).map(i -> i.atZone(TimeUtils.ZONE_ID).toLocalDateTime()).orElseThrow(() -> JacksonException.wrapWithPath(dtf, "Cannot parse " + text)); } } } diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/LocalDateToJsonDate.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/LocalDateToJsonDate.java index 3213e5c2f..f47377b7a 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/LocalDateToJsonDate.java +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/LocalDateToJsonDate.java @@ -1,26 +1,21 @@ package nl.vpro.jackson3; -import java.io.IOException; -import java.time.LocalDate; +import tools.jackson.core.*; +import tools.jackson.databind.*; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; +import java.time.LocalDate; public class LocalDateToJsonDate { private LocalDateToJsonDate() {} - public static class Serializer extends JsonSerializer { + public static class Serializer extends ValueSerializer { public static final Serializer INSTANCE = new Serializer(); @Override - public void serialize(LocalDate value, JsonGenerator jgen, SerializerProvider provider) throws IOException { + public void serialize(LocalDate value, JsonGenerator jgen, SerializationContext ctxt) throws JacksonException { if (value == null) { jgen.writeNull(); } else { @@ -30,13 +25,13 @@ public void serialize(LocalDate value, JsonGenerator jgen, SerializerProvider pr } - public static class Deserializer extends JsonDeserializer { + public static class Deserializer extends ValueDeserializer { public static final Deserializer INSTANCE = new Deserializer(); @Override - public LocalDate deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException { - String text = jp.getText(); + public LocalDate deserialize(JsonParser jp, DeserializationContext ctxt) { + String text = jp.getString(); if (text == null) { return null; } else { diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/StringDurationToJsonTimestamp.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/StringDurationToJsonTimestamp.java index 701d210bd..cf6cf291a 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/StringDurationToJsonTimestamp.java +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/StringDurationToJsonTimestamp.java @@ -4,16 +4,10 @@ */ package nl.vpro.jackson3; -import java.io.IOException; -import java.time.Duration; +import tools.jackson.core.*; +import tools.jackson.databind.*; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; +import java.time.Duration; /** * @author rico @@ -23,26 +17,27 @@ public class StringDurationToJsonTimestamp { private StringDurationToJsonTimestamp() {} - public static class Serializer extends JsonSerializer { + public static class Serializer extends ValueSerializer { public static final StringDurationToJsonTimestamp.Serializer INSTANCE = new StringDurationToJsonTimestamp.Serializer(); @Override - public void serialize(String value, JsonGenerator jgen, SerializerProvider provider) throws IOException { + public void serialize(String value, JsonGenerator jgen, SerializationContext ctxt) throws JacksonException { if (value == null) { jgen.writeNull(); } else { jgen.writeNumber(Duration.parse(value).toMillis()); } } + } - public static class Deserializer extends JsonDeserializer { + public static class Deserializer extends ValueDeserializer { public static final StringDurationToJsonTimestamp.Deserializer INSTANCE = new StringDurationToJsonTimestamp.Deserializer(); @Override - public String deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException { + public String deserialize(JsonParser jp, DeserializationContext ctxt) { return Duration.ofMillis(jp.getLongValue()).toString(); } } diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/StringInstantToJsonTimestamp.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/StringInstantToJsonTimestamp.java index b5288b974..10b1ba1dc 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/StringInstantToJsonTimestamp.java +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/StringInstantToJsonTimestamp.java @@ -12,10 +12,12 @@ import jakarta.xml.bind.DatatypeConverter; +import nl.vpro.jackson.NattySupport; + import org.slf4j.event.Level; -import com.fasterxml.jackson.core.*; -import com.fasterxml.jackson.databind.*; +import tools.jackson.core.*; +import tools.jackson.databind.*; /** * These can be used in conjunction with InstantXmlAdapter, if you want 'millis since epoch' in JSON, but formatted date stamps in xml. diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/StringZonedLocalDateToJsonTimestamp.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/StringZonedLocalDateToJsonTimestamp.java index a3855f3c9..047e4d75f 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/StringZonedLocalDateToJsonTimestamp.java +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/StringZonedLocalDateToJsonTimestamp.java @@ -1,14 +1,13 @@ package nl.vpro.jackson3; -import java.io.IOException; +import tools.jackson.core.*; +import tools.jackson.databind.*; + import java.time.Instant; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.Locale; -import com.fasterxml.jackson.core.*; -import com.fasterxml.jackson.databind.*; - import nl.vpro.util.BindingUtils; import nl.vpro.util.TimeUtils; @@ -27,12 +26,12 @@ private StringZonedLocalDateToJsonTimestamp() {} .withLocale(Locale.US); - public static class Serializer extends JsonSerializer { + public static class Serializer extends ValueSerializer { public static final Serializer INSTANCE = new Serializer(); @Override - public void serialize(Object value, JsonGenerator jgen, SerializerProvider provider) throws IOException { + public void serialize(Object value, JsonGenerator jgen, SerializationContext ctxt) throws JacksonException { if (value == null) { jgen.writeNull(); } else { @@ -42,16 +41,18 @@ public void serialize(Object value, JsonGenerator jgen, SerializerProvider provi jgen.writeNumber(((LocalDate) value).atStartOfDay(BindingUtils.DEFAULT_ZONE).toInstant().toEpochMilli()); } } + + } - public static class Deserializer extends JsonDeserializer { + public static class Deserializer extends ValueDeserializer { public static final Deserializer INSTANCE = new Deserializer(); @Override - public LocalDate deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException { + public LocalDate deserialize(JsonParser jp, DeserializationContext ctxt) { try { return Instant.ofEpochMilli(jp.getLongValue()).atZone(ZONE).toLocalDate(); - } catch (JsonParseException jps) { + } catch (JacksonException jps) { String s = jp.getValueAsString(); if (s == null) { return null; diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/Utils.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/Utils.java index 22bfd50d4..22b79ae17 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/Utils.java +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/Utils.java @@ -1,11 +1,12 @@ package nl.vpro.jackson3; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ObjectMapper; + import java.util.*; import java.util.stream.IntStream; import java.util.stream.Stream; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.MapDifference; import com.google.common.collect.Maps; diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/XMLDurationToJsonTimestamp.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/XMLDurationToJsonTimestamp.java index dae469a65..f7a90b782 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/XMLDurationToJsonTimestamp.java +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/XMLDurationToJsonTimestamp.java @@ -1,24 +1,12 @@ package nl.vpro.jackson3; import lombok.extern.slf4j.Slf4j; +import tools.jackson.core.*; +import tools.jackson.databind.*; -import java.io.IOException; -import java.util.Calendar; -import java.util.Date; -import java.util.GregorianCalendar; -import java.util.TimeZone; +import java.util.*; -import javax.xml.datatype.DatatypeConfigurationException; -import javax.xml.datatype.DatatypeFactory; -import javax.xml.datatype.Duration; - -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; +import javax.xml.datatype.*; import nl.vpro.util.TimeUtils; @@ -26,26 +14,26 @@ public class XMLDurationToJsonTimestamp { - public static class Serializer extends JsonSerializer { + public static class Serializer extends ValueSerializer { @Override - public void serialize(Duration value, JsonGenerator jgen, SerializerProvider provider) throws IOException { + public void serialize(Duration value, JsonGenerator jgen, SerializationContext ctxt) throws JacksonException { Calendar cal = new GregorianCalendar(TimeZone.getTimeZone("UTC")); cal.setTimeInMillis(0); jgen.writeNumber(value.getTimeInMillis(cal)); } } - public static class DeserializerDate extends JsonDeserializer { + public static class DeserializerDate extends ValueDeserializer { @Override - public Date deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException { + public Date deserialize(JsonParser jp, DeserializationContext ctxt) { return new Date(jp.getLongValue()); } } - public static class Deserializer extends JsonDeserializer { + public static class Deserializer extends ValueDeserializer { @Override - public Duration deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException { + public Duration deserialize(JsonParser jp, DeserializationContext ctxt) { DatatypeFactory datatypeFactory; try { datatypeFactory = DatatypeFactory.newInstance(); @@ -57,10 +45,10 @@ public Duration deserialize(JsonParser jp, DeserializationContext ctxt) throws I } } - public static class SerializerString extends JsonSerializer { + public static class SerializerString extends ValueSerializer { @Override - public void serialize(String value, JsonGenerator jgen, SerializerProvider provider) throws IOException { + public void serialize(String value, JsonGenerator jgen, SerializationContext ctxt) throws JacksonException { java.time.Duration duration = TimeUtils.parseDuration(value).orElse(null); if (duration != null) { jgen.writeNumber(duration.toMillis()); @@ -68,11 +56,12 @@ public void serialize(String value, JsonGenerator jgen, SerializerProvider provi jgen.writeNull(); } } + } - public static class DeserializerJavaDuration extends JsonDeserializer { + public static class DeserializerJavaDuration extends ValueDeserializer { @Override - public java.time.Duration deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException { + public java.time.Duration deserialize(JsonParser jp, DeserializationContext ctxt) { return java.time.Duration.ofMillis(jp.getLongValue()); } } diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/ZonedDateTimeToJsonTimestamp.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/ZonedDateTimeToJsonTimestamp.java index f8de09169..62332d2f8 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/ZonedDateTimeToJsonTimestamp.java +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/ZonedDateTimeToJsonTimestamp.java @@ -1,18 +1,11 @@ package nl.vpro.jackson3; -import java.io.IOException; +import tools.jackson.core.*; +import tools.jackson.databind.*; + import java.time.Instant; import java.time.ZonedDateTime; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParseException; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; - import static nl.vpro.jackson3.DateModule.ZONE; @@ -20,28 +13,30 @@ public class ZonedDateTimeToJsonTimestamp { private ZonedDateTimeToJsonTimestamp() {} - public static class Serializer extends JsonSerializer { + public static class Serializer extends ValueSerializer { public static final Serializer INSTANCE = new Serializer(); @Override - public void serialize(ZonedDateTime value, JsonGenerator jgen, SerializerProvider provider) throws IOException { + public void serialize(ZonedDateTime value, JsonGenerator jgen, SerializationContext ctxt) throws JacksonException { if (value == null) { jgen.writeNull(); } else { jgen.writeNumber(value.toInstant().toEpochMilli()); } } + + } - public static class Deserializer extends JsonDeserializer { + public static class Deserializer extends ValueDeserializer { public static final Deserializer INSTANCE = new Deserializer(); @Override - public ZonedDateTime deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException { + public ZonedDateTime deserialize(JsonParser jp, DeserializationContext ctxt) throws JacksonException { try { return Instant.ofEpochMilli(jp.getLongValue()).atZone(ZONE); - } catch (JsonParseException jps) { + } catch (JacksonException jps) { String s = jp.getValueAsString(); if (s == null) { return null; diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/rs/JsonIdAdderBodyReader.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/rs/JsonIdAdderBodyReader.java index e8e6a60c6..c7679c10a 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/rs/JsonIdAdderBodyReader.java +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/rs/JsonIdAdderBodyReader.java @@ -15,10 +15,10 @@ import org.checkerframework.checker.nullness.qual.NonNull; -import com.fasterxml.jackson.databind.*; -import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; -import com.fasterxml.jackson.databind.jsontype.TypeIdResolver; -import com.fasterxml.jackson.databind.node.ObjectNode; +import tools.jackson.databind.*; +import tools.jackson.databind.jsontype.TypeDeserializer; +import tools.jackson.databind.jsontype.TypeIdResolver; +import tools.jackson.databind.node.ObjectNode; import nl.vpro.jackson3.Jackson3Mapper; @@ -58,7 +58,7 @@ public Object readFrom( final JavaType javaType = mapper.getTypeFactory().constructType(genericType); final JsonNode jsonNode = mapper.readTree(entityStream); if (jsonNode instanceof ObjectNode objectNode) { - final TypeDeserializer typeDeserializer = mapper.getDeserializationConfig().findTypeDeserializer(javaType); + final TypeDeserializer typeDeserializer = mapper.deserializationConfig().getTypeResolverProvider().findTypeDeserializer(javaType); if (typeDeserializer != null) { final String propertyName = typeDeserializer.getPropertyName(); final String propertyValue = typeDeserializer.getTypeIdResolver().idFromBaseType(); From 1973b4b3e698596c6d3050a2e4b485641563aaed Mon Sep 17 00:00:00 2001 From: Michiel Meeuwissen Date: Fri, 28 Nov 2025 13:19:19 +0100 Subject: [PATCH 03/18] More jackson2 -> 3 --- vpro-shared-jackson2/pom.xml | 5 - .../StringInstantToJsonTimestamp.java | 2 + .../src/main/java/nl/vpro/jackson2/Views.java | 54 -- vpro-shared-jackson3/pom.xml | 12 +- .../java/nl/vpro/jackson/NattySupport.java | 34 -- .../nl/vpro/jackson3/GuavaRangeModule.java | 6 +- .../InstantToSecondsFloatTimestamp.java | 19 +- .../java/nl/vpro/jackson3/IterableJson.java | 11 +- .../java/nl/vpro/jackson3/Jackson3Mapper.java | 15 +- .../nl/vpro/jackson3/JsonArrayIterator.java | 45 +- .../LocalDateTimeToJsonDateWithSpace.java | 11 +- .../StringInstantToJsonTimestamp.java | 23 +- .../ZonedDateTimeToJsonTimestamp.java | 2 - .../jackson3/rs/JsonIdAdderBodyReader.java | 9 +- vpro-shared-test/pom.xml | 6 + .../test/util/jackson3/Jackson3TestUtil.java | 480 ++++++++++++++++++ vpro-shared-util/pom.xml | 5 + .../src/main/java/nl/vpro/jackson/Views.java | 2 +- .../main/java/nl/vpro/util}/NattySupport.java | 9 +- 19 files changed, 573 insertions(+), 177 deletions(-) delete mode 100644 vpro-shared-jackson2/src/main/java/nl/vpro/jackson2/Views.java delete mode 100644 vpro-shared-jackson3/src/main/java/nl/vpro/jackson/NattySupport.java create mode 100644 vpro-shared-test/src/main/java/nl/vpro/test/util/jackson3/Jackson3TestUtil.java rename {vpro-shared-jackson3 => vpro-shared-util}/src/main/java/nl/vpro/jackson/Views.java (97%) rename {vpro-shared-jackson2/src/main/java/nl/vpro/jackson2 => vpro-shared-util/src/main/java/nl/vpro/util}/NattySupport.java (82%) diff --git a/vpro-shared-jackson2/pom.xml b/vpro-shared-jackson2/pom.xml index 07d75081d..a640fdb82 100644 --- a/vpro-shared-jackson2/pom.xml +++ b/vpro-shared-jackson2/pom.xml @@ -51,11 +51,6 @@ org.projectlombok lombok - - io.github.natty-parser - natty - true - jakarta.xml.bind jakarta.xml.bind-api diff --git a/vpro-shared-jackson2/src/main/java/nl/vpro/jackson2/StringInstantToJsonTimestamp.java b/vpro-shared-jackson2/src/main/java/nl/vpro/jackson2/StringInstantToJsonTimestamp.java index 22ee94d05..52b50b55a 100644 --- a/vpro-shared-jackson2/src/main/java/nl/vpro/jackson2/StringInstantToJsonTimestamp.java +++ b/vpro-shared-jackson2/src/main/java/nl/vpro/jackson2/StringInstantToJsonTimestamp.java @@ -17,6 +17,8 @@ import com.fasterxml.jackson.core.*; import com.fasterxml.jackson.databind.*; +import nl.vpro.util.NattySupport; + /** * These can be used in conjunction with InstantXmlAdapter, if you want 'millis since epoch' in JSON, but formatted date stamps in xml. * (this is what we normally do) diff --git a/vpro-shared-jackson2/src/main/java/nl/vpro/jackson2/Views.java b/vpro-shared-jackson2/src/main/java/nl/vpro/jackson2/Views.java deleted file mode 100644 index d35402010..000000000 --- a/vpro-shared-jackson2/src/main/java/nl/vpro/jackson2/Views.java +++ /dev/null @@ -1,54 +0,0 @@ -package nl.vpro.jackson2; - -import com.google.common.annotations.Beta; - -/** - * Several setups at VPRO and at NPO involve a backend system that publishes JSON to ElasticSearch. - * In some cases this published json must be somewhat adapted, in contrast to when it is not yet published. - * @author Michiel Meeuwissen - * @since 1.72 - */ -public class Views { - - public interface Normal { - } - - /** - * Forward compatible view - */ - public interface Forward extends Normal { - } - - public interface Publisher extends Normal { - } - - /** - * A 'model' related view of the json. - *

- * This would e.g. imply that some extra fields are present which would otherwise calculable, but it may be useful for the receiving end to - * receive such a value evaluated. - * @since 2.33 - */ - @Beta - public interface Model { - } - - /** - * - * @since 2.33 - */ - @Beta - public interface ModelAndNormal extends Model, Normal { - } - - /** - * New fields may be temporary marked 'ForwardPublisher'. Which will mean that {@link Jackson2Mapper#getBackwardsPublisherInstance()} will ignore them. - *

- * That way we can serialize for checking purposes compatible with old values in ES. - *

- * So generally this means that a field should be present in the published json, but a full republication hasn't happened yet - */ - public interface ForwardPublisher extends Publisher, Forward { - } - -} diff --git a/vpro-shared-jackson3/pom.xml b/vpro-shared-jackson3/pom.xml index 74b8b490f..7e0b9f8f9 100644 --- a/vpro-shared-jackson3/pom.xml +++ b/vpro-shared-jackson3/pom.xml @@ -43,11 +43,7 @@ org.projectlombok lombok - - io.github.natty-parser - natty - true - + jakarta.xml.bind jakarta.xml.bind-api @@ -77,11 +73,5 @@ commons-io true - - com.fasterxml.jackson.core - jackson-core - 2.20.1 - compile - diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson/NattySupport.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson/NattySupport.java deleted file mode 100644 index ced1ca497..000000000 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson/NattySupport.java +++ /dev/null @@ -1,34 +0,0 @@ -package nl.vpro.jackson; - -import lombok.extern.slf4j.Slf4j; - -import java.time.Instant; -import java.util.*; - -import org.natty.DateGroup; -import org.natty.Parser; - -import nl.vpro.jackson3.StringInstantToJsonTimestamp; -import nl.vpro.util.BindingUtils; -import nl.vpro.util.DateUtils; - -/** - * The dependency on natty in {@link StringInstantToJsonTimestamp} is optional. Put support for it in this class, so we can just catch the resulting NoClassDefFoundError. - * @author Michiel Meeuwissen - * @since 2.14 - */ - -@Slf4j -class NattySupport { - private static final Parser PARSER = new Parser(TimeZone.getTimeZone(BindingUtils.DEFAULT_ZONE)); - - - static Optional parseDate(String value) { - List groups = PARSER.parse(value); - if (groups.size() == 1) { - log.info("Parsed date '{}' to {}", value, groups.get(0).getDates()); - return Optional.ofNullable(DateUtils.toInstant(groups.get(0).getDates().get(0))); - } - return Optional.empty(); - } -} diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/GuavaRangeModule.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/GuavaRangeModule.java index 26fb031c2..0cf027513 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/GuavaRangeModule.java +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/GuavaRangeModule.java @@ -33,8 +33,6 @@ public GuavaRangeModule() { public static class Serializer extends StdSerializer> { - @Serial - private static final long serialVersionUID = -4394016847732058088L; public static Serializer INSTANCE = new Serializer(); protected Serializer() { @@ -116,11 +114,11 @@ static > Range of(Class clazz, JsonParser p, JsonN C upperValue = null; if (node.has(LOWER_ENDPOINT)) { - lowerBoundType = BoundType.valueOf(node.get(LOWER_BOUND_TYPE).asText()); + lowerBoundType = BoundType.valueOf(node.get(LOWER_BOUND_TYPE).asString()); lowerValue = p.objectReadContext().readValue(p, node.get(LOWER_ENDPOINT), clazz); } if (node.has(UPPER_ENDPOINT)) { - upperBoundType = BoundType.valueOf(node.get(UPPER_BOUND_TYPE).asText()); + upperBoundType = BoundType.valueOf(node.get(UPPER_BOUND_TYPE).asString()); upperValue = p.(UPPER_ENDPOINT), clazz); } if (lowerValue != null) { diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/InstantToSecondsFloatTimestamp.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/InstantToSecondsFloatTimestamp.java index 99fd5dc32..58c489cc0 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/InstantToSecondsFloatTimestamp.java +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/InstantToSecondsFloatTimestamp.java @@ -1,19 +1,12 @@ package nl.vpro.jackson3; -import java.io.IOException; +import tools.jackson.core.*; +import tools.jackson.databind.*; + import java.time.Instant; import java.time.ZonedDateTime; import java.time.format.DateTimeParseException; -import tools.jackson.core.JsonGenerator; -import tools.jackson.core.JsonParseException; -import tools.jackson.core.JsonParser; -import tools.jackson.core.JsonProcessingException; -import tools.jackson.databind.*; -import tools.jackson.databind.JsonDeserializer; -import tools.jackson.databind.JsonSerializer; -import tools.jackson.databind.SerializerProvider; - public class InstantToSecondsFloatTimestamp { @@ -25,13 +18,15 @@ public static class Serializer extends ValueSerializer { public static final Serializer INSTANCE = new Serializer(); @Override - public void serialize(Instant value, JsonGenerator jgen, SerializerProvider provider) throws IOException { + public void serialize(Instant value, JsonGenerator jgen, SerializationContext ctxt) throws JacksonException { if (value == null) { jgen.writeNull(); } else { jgen.writeNumber(value.toEpochMilli() / 1000f); } } + + } @@ -42,7 +37,7 @@ public static class Deserializer extends ValueDeserializer { public Instant deserialize(JsonParser jp, DeserializationContext ctxt) { try { return Instant.ofEpochMilli((long) Float.parseFloat(jp.getValueAsString()) * 1000); - } catch ( jpe) { + } catch ( Exception e) { try { String s = jp.getValueAsString(); if (s == null) { diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/IterableJson.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/IterableJson.java index b78d94b4e..10622f88b 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/IterableJson.java +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/IterableJson.java @@ -4,8 +4,7 @@ import java.util.*; import java.util.function.Function; -import tools.jackson.core.JsonGenerator; -import tools.jackson.core.JsonParser; +import tools.jackson.core.*; import tools.jackson.databind.*; import tools.jackson.databind.JsonDeserializer; import tools.jackson.databind.JsonSerializer; @@ -20,7 +19,7 @@ public class IterableJson { public static class Serializer extends ValueSerializer> { @Override - public void serialize(Iterable value, JsonGenerator jgen, SerializerProvider provider) throws IOException { + public void serialize(Iterable value, JsonGenerator jgen, SerializationContext ctxt) throws JacksonException { if (value == null) { jgen.writeNull(); @@ -45,6 +44,8 @@ public void serialize(Iterable value, JsonGenerator jgen, SerializerProvider pro } } } + + } private static final Set> simpleTypes = new HashSet<>(Arrays.asList(String.class, Character.class, Boolean.class, Integer.class, Float.class, Long.class, Double.class)); @@ -76,7 +77,7 @@ public Deserializer(Function, Iterable> supplier) { @Override - public Iterable deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException { + public Iterable deserialize(JsonParser jp, DeserializationContext ctxt) { if (jp.streamReadContext().inObject()) { if (! isSimple) { jp.clearCurrentToken(); @@ -86,7 +87,7 @@ public Iterable deserialize(JsonParser jp, DeserializationContext ctxt) throw } else if (jp.streamReadContext().inArray()) { List list = new ArrayList<>(); jp.clearCurrentToken(); - Iterator i = jp.read(memberClass); + Iterator i = jp.readValueAs(memberClass); while (i.hasNext()) { list.add(i.next()); } diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/Jackson3Mapper.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/Jackson3Mapper.java index db6806e1d..6eaa3c81f 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/Jackson3Mapper.java +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/Jackson3Mapper.java @@ -15,6 +15,7 @@ import tools.jackson.databind.*; import tools.jackson.databind.introspect.AnnotationIntrospectorPair; import tools.jackson.databind.introspect.JacksonAnnotationIntrospector; +import tools.jackson.databind.json.JsonMapper; import tools.jackson.databind.ser.PropertyFilter; import tools.jackson.databind.ser.std.SimpleFilterProvider; import tools.jackson.module.jakarta.xmlbind.JakartaXmlBindAnnotationIntrospector; @@ -45,7 +46,7 @@ */ @Slf4j -public class Jackson3Mapper extends ObjectMapper { +public class Jackson3Mapper extends JsonMapper { @Serial private static final long serialVersionUID = 8353430660109292010L; @@ -57,13 +58,25 @@ public class Jackson3Mapper extends ObjectMapper { + public static final Jackson3Mapper INSTANCE = getInstance(); + public static final Jackson3Mapper LENIENT = getLenientInstance(); + public static final Jackson3Mapper STRICT = getStrictInstance(); + public static final Jackson3Mapper PRETTY_STRICT = getPrettyStrictInstance(); + public static final Jackson3Mapper PRETTY = getPrettyInstance(); + public static final Jackson3Mapper PUBLISHER = getPublisherInstance(); + public static final Jackson3Mapper PRETTY_PUBLISHER = getPublisherInstance(); + + public static final Jackson3Mapper BACKWARDS_PUBLISHER = getBackwardsPublisherInstance(); private static final ThreadLocal THREAD_LOCAL = ThreadLocal.withInitial(Jackson3Mapper::getInstance); + public static Jackson3Mapper getInstance() { Jackson3Mapper mapper = new Jackson3Mapper("instance"); + Jackson3Mapper.builder() + .build(); mapper.setConfig(mapper.getSerializationConfig().withView(Views.Forward.class)); mapper.setConfig(mapper.getDeserializationConfig().withView(Views.Forward.class)); return mapper; diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/JsonArrayIterator.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/JsonArrayIterator.java index c363cd3de..0169b9bf7 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/JsonArrayIterator.java +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/JsonArrayIterator.java @@ -2,6 +2,9 @@ import lombok.*; import lombok.extern.slf4j.Slf4j; +import tools.jackson.core.*; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.node.NullNode; import java.io.*; import java.util.*; @@ -11,9 +14,6 @@ import org.checkerframework.checker.nullness.qual.Nullable; import org.slf4j.Logger; -import tools.jackson.core.*; -import tools.jackson.databind.ObjectMapper; -import tools.jackson.databind.node.NullNode; import com.google.common.collect.PeekingIterator; import com.google.common.collect.UnmodifiableIterator; @@ -28,6 +28,8 @@ public class JsonArrayIterator extends UnmodifiableIterator implements CloseableIterator, PeekingIterator, CountedIterator { + private final ObjectMapper mapper; + private final JsonParser jp; private T next = null; @@ -36,7 +38,7 @@ public class JsonArrayIterator extends UnmodifiableIterator private Boolean hasNext; - private final BiFunction valueCreator; + private final BiFunction valueCreator; @Getter @Setter @@ -67,7 +69,7 @@ public JsonArrayIterator(InputStream inputStream, final Class clazz, Runnable this(inputStream, null, clazz, callback, null, null, null, null, null, null); } - public JsonArrayIterator(InputStream inputStream, final BiFunction valueCreator) throws IOException { + public JsonArrayIterator(InputStream inputStream, final BiFunction valueCreator) throws IOException { this(inputStream, valueCreator, null, null, null, null, null, null, null, null); } @@ -98,7 +100,7 @@ public static class Builder { @lombok.Builder(builderClassName = "Builder", builderMethodName = "builder") private JsonArrayIterator( @NonNull InputStream inputStream, - @Nullable final BiFunction valueCreator, + @Nullable final BiFunction valueCreator, @Nullable final Class valueClass, @Nullable Runnable callback, @Nullable String sizeField, @@ -111,7 +113,8 @@ private JsonArrayIterator( if (inputStream == null) { throw new IllegalArgumentException("No inputStream given"); } - this.jp = (objectMapper == null ? Jackson3Mapper.getLenientInstance() : objectMapper).getFactory().createParser(inputStream); + this.mapper = objectMapper == null ? Jackson3Mapper.getLenientInstance() : objectMapper; + this.jp = this.mapper.createParser(inputStream); this.valueCreator = valueCreator == null ? valueCreator(valueClass) : valueCreator; if (valueCreator != null && valueClass != null) { throw new IllegalArgumentException(); @@ -136,8 +139,8 @@ private JsonArrayIterator( break; } this.eventListener.accept(new TokenEvent(token)); - if (token == JsonToken.FIELD_NAME) { - fieldName = jp.getCurrentName(); + if (token == JsonToken.PROPERTY_NAME) { + fieldName = jp.currentName(); } if (token == JsonToken.VALUE_NUMBER_INT && sizeField.equals(fieldName)) { tmpSize = jp.getLongValue(); @@ -162,11 +165,11 @@ private JsonArrayIterator( this.skipNulls = skipNulls == null || skipNulls; } - private static BiFunction valueCreator(Class clazz) { - return (jp, tree) -> { + private static BiFunction valueCreator(Class clazz) { + return (m, tree) -> { try { - return jp.getCodec().treeToValue(tree, clazz); - } catch (JsonProcessingException e) { + return m.treeToValue(tree, clazz); + } catch (JacksonException e) { throw new ValueReadException(e); } }; @@ -232,7 +235,7 @@ protected void findNext() { logger.warn("Found {} nulls. Will be skipped", foundNulls); } - next = valueCreator.apply(jp, tree); + next = valueCreator.apply(mapper, tree); eventListener.accept(new NextEvent(next)); hasNext = true; } @@ -241,8 +244,6 @@ protected void findNext() { foundNulls++; logger.warn(jme.getClass() + " " + jme.getMessage() + " for\n" + tree + "\nWill be skipped"); } - } catch (IOException e) { - callbackBeforeThrow(new RuntimeException(e)); } catch (RuntimeException rte) { callbackBeforeThrow(rte); } @@ -304,7 +305,7 @@ public static void write( final CountedIterator iterator, final OutputStream out, final Function logging) throws IOException { - try (JsonGenerator jg = Jackson3Mapper.getInstance().getFactory().createGenerator(out)) { + try (JsonGenerator jg = Jackson3Mapper.getInstance().createGenerator(out)) { jg.writeStartObject(); jg.writeArrayPropertyStart("array"); writeObjects(iterator, jg, logging); @@ -320,7 +321,7 @@ public static void write( public static void writeArray( final CountedIterator iterator, final OutputStream out, final Function logging) throws IOException { - try (JsonGenerator jg = Jackson3Mapper.getInstance().getFactory().createGenerator(out)) { + try (JsonGenerator jg = Jackson3Mapper.getInstance().createGenerator(out)) { jg.writeStartArray(); writeObjects(iterator, jg, logging); jg.writeEndArray(); @@ -341,7 +342,7 @@ public static void writeObjects( try { change = iterator.next(); if (change != null) { - jg.writeObject(change); + jg.writePOJO(change); } else { jg.writeNull(); } @@ -357,8 +358,8 @@ public static void writeObjects( cause = cause.getCause(); } - log.warn(e.getClass().getCanonicalName() + " " + e.getMessage()); - jg.writeObject(e.getMessage()); + log.warn("{} {}", e.getClass().getCanonicalName(), e.getMessage()); + jg.writePOJO(e.getMessage()); } } @@ -384,7 +385,7 @@ public static class ValueReadException extends RuntimeException { @Serial private static final long serialVersionUID = 6976771876437440576L; - public ValueReadException(JsonProcessingException e) { + public ValueReadException(JacksonException e) { super(e); } } diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/LocalDateTimeToJsonDateWithSpace.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/LocalDateTimeToJsonDateWithSpace.java index 8c303c775..4948e2a86 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/LocalDateTimeToJsonDateWithSpace.java +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/LocalDateTimeToJsonDateWithSpace.java @@ -1,11 +1,11 @@ package nl.vpro.jackson3; -import java.time.LocalDateTime; -import java.time.format.DateTimeParseException; - import tools.jackson.core.*; import tools.jackson.databind.*; +import java.time.LocalDateTime; +import java.time.format.DateTimeParseException; + import nl.vpro.util.TimeUtils; @@ -37,7 +37,7 @@ public static class Deserializer extends ValueDeserializer { public static final Deserializer INSTANCE = new Deserializer(); @Override - public LocalDateTime deserialize(JsonParser jp, DeserializationContext ctxt) { + public LocalDateTime deserialize(JsonParser jp, DeserializationContext ctxt) throws JacksonException { String text = jp.getString(); if (text == null) { return null; @@ -45,7 +45,8 @@ public LocalDateTime deserialize(JsonParser jp, DeserializationContext ctxt) { try { return LocalDateTime.parse(text.replaceFirst(" ", "T")); } catch (DateTimeParseException dtf) { - return TimeUtils.parse(text).map(i -> i.atZone(TimeUtils.ZONE_ID).toLocalDateTime()).orElseThrow(() -> JacksonException.wrapWithPath(dtf, "Cannot parse " + text)); + return TimeUtils.parse(text).map(i -> i.atZone(TimeUtils.ZONE_ID).toLocalDateTime()) + .orElseThrow(() -> new JacksonException("Cannot parse " + text) {}); } } } diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/StringInstantToJsonTimestamp.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/StringInstantToJsonTimestamp.java index 10b1ba1dc..ec84dd0cf 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/StringInstantToJsonTimestamp.java +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/StringInstantToJsonTimestamp.java @@ -5,19 +5,17 @@ package nl.vpro.jackson3; import lombok.extern.slf4j.Slf4j; +import tools.jackson.core.*; +import tools.jackson.databind.*; -import java.io.IOException; import java.time.Instant; import java.util.Optional; import jakarta.xml.bind.DatatypeConverter; -import nl.vpro.jackson.NattySupport; - import org.slf4j.event.Level; -import tools.jackson.core.*; -import tools.jackson.databind.*; +import nl.vpro.util.NattySupport; /** * These can be used in conjunction with InstantXmlAdapter, if you want 'millis since epoch' in JSON, but formatted date stamps in xml. @@ -32,11 +30,11 @@ public class StringInstantToJsonTimestamp { private StringInstantToJsonTimestamp() {} - public static class Serializer extends JsonSerializer { + public static class Serializer extends ValueSerializer { public static final StringInstantToJsonTimestamp.Serializer INSTANCE = new StringInstantToJsonTimestamp.Serializer(); - @Override - public void serialize(Object value, JsonGenerator jgen, SerializerProvider provider) throws IOException { + @Override + public void serialize(Object value, JsonGenerator jgen, SerializationContext ctxt) throws JacksonException { if (value == null) { jgen.writeNull(); } else if (value instanceof Instant) { // if no JaxbAnnotationIntrospector @@ -50,6 +48,7 @@ public void serialize(Object value, JsonGenerator jgen, SerializerProvider provi } } } + } static Instant parseDateTime(String value) { @@ -78,12 +77,12 @@ static Instant parseDateTime(String value) { } } - public static class Deserializer extends JsonDeserializer { + public static class Deserializer extends ValueDeserializer { public static final StringInstantToJsonTimestamp.Deserializer INSTANCE = new StringInstantToJsonTimestamp.Deserializer(); @Override - public Instant deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException { + public Instant deserialize(JsonParser jp, DeserializationContext ctxt) { switch (jp.currentTokenId()) { case JsonTokenId.ID_NUMBER_INT -> { return Instant.ofEpochMilli(jp.getLongValue()); @@ -93,9 +92,9 @@ public Instant deserialize(JsonParser jp, DeserializationContext ctxt) throws IO } case JsonTokenId.ID_STRING -> { try { - return parseDateTime(jp.getText()); + return parseDateTime(jp.getString()); } catch (IllegalArgumentException iae) { - log.warn("Could not parse {}. Writing null to json", jp.getText()); + log.warn("Could not parse {}. Writing null to json", jp.getString()); return null; } } diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/ZonedDateTimeToJsonTimestamp.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/ZonedDateTimeToJsonTimestamp.java index 62332d2f8..6172050e5 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/ZonedDateTimeToJsonTimestamp.java +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/ZonedDateTimeToJsonTimestamp.java @@ -25,8 +25,6 @@ public void serialize(ZonedDateTime value, JsonGenerator jgen, SerializationCont jgen.writeNumber(value.toInstant().toEpochMilli()); } } - - } public static class Deserializer extends ValueDeserializer { diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/rs/JsonIdAdderBodyReader.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/rs/JsonIdAdderBodyReader.java index c7679c10a..a138c4ea9 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/rs/JsonIdAdderBodyReader.java +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/rs/JsonIdAdderBodyReader.java @@ -2,7 +2,6 @@ import lombok.extern.slf4j.Slf4j; -import java.io.IOException; import java.io.InputStream; import java.lang.annotation.Annotation; import java.lang.reflect.Type; @@ -16,6 +15,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; import tools.jackson.databind.*; +import tools.jackson.databind.json.JsonMapper; import tools.jackson.databind.jsontype.TypeDeserializer; import tools.jackson.databind.jsontype.TypeIdResolver; import tools.jackson.databind.node.ObjectNode; @@ -49,8 +49,8 @@ public Object readFrom( Annotation[] annotations, MediaType mediaType, MultivaluedMap httpHeaders, - InputStream entityStream) throws WebApplicationException, IOException { - ObjectMapper mapper = providers == null ? null : providers.getContextResolver(ObjectMapper.class, MediaType.APPLICATION_JSON_TYPE).getContext(type); + InputStream entityStream) throws WebApplicationException { + JsonMapper mapper = providers == null ? null : providers.getContextResolver(JsonMapper.class, MediaType.APPLICATION_JSON_TYPE).getContext(type); if (mapper == null) { log.info("No mapper found in {}", providers); mapper = Jackson3Mapper.getLenientInstance(); @@ -58,7 +58,8 @@ public Object readFrom( final JavaType javaType = mapper.getTypeFactory().constructType(genericType); final JsonNode jsonNode = mapper.readTree(entityStream); if (jsonNode instanceof ObjectNode objectNode) { - final TypeDeserializer typeDeserializer = mapper.deserializationConfig().getTypeResolverProvider().findTypeDeserializer(javaType); + final TypeDeserializer typeDeserializer = mapper.deserializationConfig().getTypeResolverProvider() + .findTypeDeserializer(javaType); if (typeDeserializer != null) { final String propertyName = typeDeserializer.getPropertyName(); final String propertyValue = typeDeserializer.getTypeIdResolver().idFromBaseType(); diff --git a/vpro-shared-test/pom.xml b/vpro-shared-test/pom.xml index 7257dedd1..aaaefb7db 100644 --- a/vpro-shared-test/pom.xml +++ b/vpro-shared-test/pom.xml @@ -21,6 +21,12 @@ nl.vpro.shared vpro-shared-jackson2 + true + + + nl.vpro.shared + vpro-shared-jackson3 + true com.fasterxml.jackson.core diff --git a/vpro-shared-test/src/main/java/nl/vpro/test/util/jackson3/Jackson3TestUtil.java b/vpro-shared-test/src/main/java/nl/vpro/test/util/jackson3/Jackson3TestUtil.java new file mode 100644 index 000000000..de75bf554 --- /dev/null +++ b/vpro-shared-test/src/main/java/nl/vpro/test/util/jackson3/Jackson3TestUtil.java @@ -0,0 +1,480 @@ +/* + * Copyright (C) 2014 All rights reserved + * VPRO The Netherlands + */ +package nl.vpro.test.util.jackson3; + +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.function.Supplier; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.assertj.core.api.AbstractObjectAssert; +import org.assertj.core.api.Fail; +import org.checkerframework.checker.nullness.qual.PolyNull; +import org.skyscreamer.jsonassert.JSONAssert; +import org.skyscreamer.jsonassert.JSONCompareMode; + +import tools.jackson.core.JsonPointer; +import tools.jackson.core.StreamReadFeature; +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.node.ArrayNode; +import tools.jackson.databind.node.ObjectNode; + +import nl.vpro.jackson2.Jackson2Mapper; +import nl.vpro.test.util.TestClass; + +import static org.assertj.core.api.Assertions.assertThat; + + +/** + * @author Roelof Jan Koekoek + * @since 3.3 + */ +@SuppressWarnings("UnusedReturnValue") +@Slf4j +public class Jackson3TestUtil { + + private static final ObjectMapper MAPPER = + Jackson2Mapper.getPrettyStrictInstance(); + + private static final ObjectReader JSON_READER = MAPPER.readerFor(JsonNode.class).with(StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION); + + + public static void assertJsonEquals(String pref, CharSequence expected, CharSequence actual, JsonPointer... ignores) { + try { + if (ignores.length > 0) { + JsonNode actualJson = MAPPER.readTree(actual.toString()); + remove(actualJson, ignores); + actual = MAPPER.writeValueAsString(actualJson); + } + + JSONAssert.assertEquals(pref + "\n" + actual + "\nis different from expected\n" + expected, String.valueOf(expected), String.valueOf(actual), JSONCompareMode.STRICT); + + } catch (AssertionError fail) { + log.info(fail.getMessage()); + assertThat(prettify(actual)).isEqualTo(prettify(expected)); + } catch (Exception e) { + log.error(e.getMessage()); + assertThatJson(actual).isEqualTo(prettify(expected)); + } + } + + @SneakyThrows + public static void assertJsonEquals(String pref, CharSequence expected, JsonNode actualJson, JsonPointer... ignores) { + String actual = null; + try { + remove(actualJson, ignores); + actual = MAPPER.writeValueAsString(actualJson); + + JSONAssert.assertEquals(pref + "\n" + actual + "\nis different from expected\n" + expected, String.valueOf(expected), String.valueOf(actual), JSONCompareMode.STRICT); + + } catch (AssertionError fail) { + log.info(fail.getMessage()); + assertThat(prettify(actual)).isEqualTo(prettify(expected)); + } + } + + public static void remove(JsonNode actualJson, JsonPointer... ignores) { + for (JsonPointer ignore : ignores){ + JsonNode parent = actualJson.at(ignore.head()); + JsonNode at = actualJson.at(ignore); + if (parent instanceof ObjectNode parentNode) { + parentNode.remove(ignore.getMatchingProperty()); + } else if (parent instanceof ArrayNode parentArray) { + parentArray.remove(ignore.getMatchingIndex()); + } + } + } + + @PolyNull + public static String prettify(@PolyNull CharSequence test) { + if (test == null) { + return null; + } + try { + JsonNode jsonNode = JSON_READER.readTree(String.valueOf(test)); + String pretty = MAPPER.writeValueAsString(jsonNode); + return pretty; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + + public static void assertJsonEquals(CharSequence expected, CharSequence actual, JsonPointer... ignores) { + assertJsonEquals("", expected, actual, ignores); + } + + + public static T roundTrip(T input) throws Exception { + return roundTrip(input, ""); + } + + /** + *

Marshalls input, checks whether it contains a string, and unmarshall it.

+ * + *

Checks whether marshalling and unmarshalling happens without errors, and the return value can be checked with other tests.

+ */ + @SuppressWarnings("unchecked") + public static T roundTrip(T input, String contains) throws Exception { + StringWriter writer = new StringWriter(); + MAPPER.writeValue(writer, input); + + String text = writer.toString(); + if (StringUtils.isNotEmpty(contains)) { + assertThat(text).contains(contains); + } + + return (T) MAPPER.readValue(text, input.getClass()); + } + + /** + *

Marshalls input, checks whether it is similar to expected string, and unmarshals it, then marshall it again, and it should still be similar. + *

+ *

Checks whether marshalling and unmarshalling happens without errors, and the return value can be checked with other tests.

+ */ + public static T roundTripAndSimilar(T input, String expected) { + return roundTripAndSimilar(MAPPER, input, expected); + } + public static T roundTripAndSimilar(T input, JsonNode expected) throws Exception { + return roundTripAndSimilar(MAPPER, input, expected); + } + + public static T roundTripAndSimilar(ObjectMapper mapper, T input, String expected, boolean remarshall, JsonPointer... ignores) { + return roundTripAndSimilar( + mapper, + input, + expected, + mapper.getTypeFactory().constructType(input.getClass()), + remarshall, + ignores + ); + } + public static T roundTripAndSimilar(ObjectMapper mapper, T input, String expected) { + return roundTripAndSimilar(mapper, input, expected, true); + } + + public static T roundTripAndSimilar(ObjectMapper mapper, T input, JsonNode expected, boolean remarshall) throws Exception { + return roundTripAndSimilar(mapper, input, expected, + mapper.getTypeFactory().constructType(input.getClass()), remarshall); + } + + public static T roundTripAndSimilar(ObjectMapper mapper, T input, JsonNode expected) throws Exception { + return roundTripAndSimilar(mapper, input, expected, true); + } + + public static T roundTripAndSimilar(ObjectMapper mapper, T input, InputStream expected) throws Exception { + StringWriter write = new StringWriter(); + IOUtils.copy(expected, write, "UTF-8"); + return roundTripAndSimilar(mapper, input, write.toString()); + } + + + /** + * Marshalls input, checks whether it is similar to expected string, and unmarshall it. This unmarshalled result must be equal to the input. + *

+ * Checks whether marshalling and unmarshalling happens without errors, and the return value can be checked with other tests. + */ + public static T roundTripAndSimilarAndEquals(T input, String expected) { + T result = roundTripAndSimilar(input, expected); + assertThat(result).isEqualTo(input); + return result; + } + + public static T roundTripAndSimilarAndEquals(T input, JsonNode expected) throws Exception { + T result = roundTripAndSimilar(input, expected); + assertThat(result).isEqualTo(input); + return result; + } + + public static T roundTripAndSimilarAndEquals(ObjectMapper mapper, T input, String expected) { + T result = roundTripAndSimilar(mapper, input, expected); + assertThat(result).isEqualTo(input); + return result; + } + + public static T assertJsonEquals(String actual, String expected, Class typeReference) throws IOException { + assertJsonEquals("", expected, actual); + return objectReader(MAPPER, typeReference).readValue(actual, typeReference); + } + + + protected static T roundTripAndSimilar(T input, String expected, JavaType typeReference, boolean remarshal) { + return roundTripAndSimilar(MAPPER, input, expected, typeReference, remarshal); + } + + protected static ObjectReader objectReader(ObjectMapper mapper, JavaType typeReference) { + return mapper.readerFor(typeReference) + .with(StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION); + + } + protected static ObjectReader objectReader(ObjectMapper mapper, Class typeReference) { + return mapper.readerFor(typeReference) + .with(StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION); + + } + + @SneakyThrows + protected static T roundTripAndSimilar(ObjectMapper mapper, T input, String expected, JavaType typeReference, boolean remarshall, JsonPointer... ignores) { + String marshalled = marshallAndSimilar(mapper, input, expected, ignores); + + T unmarshalled = mapper.readValue(marshalled, typeReference); + if (remarshall) { + StringWriter remarshal = new StringWriter(); + mapper.writeValue(remarshal, unmarshalled); + String remarshalled = remarshal.toString(); + log.debug("Comparing {} with expected {}", remarshalled, expected); + + assertJsonEquals("REMARSHALLED", expected, remarshalled, ignores); + } + return unmarshalled; + } + + protected static String marshallAndSimilar(ObjectMapper mapper, T input, String expected, JsonPointer... ignores) throws IOException { + StringWriter originalWriter = new StringWriter(); + mapper.writeValue(originalWriter, input); + String marshalled = originalWriter.toString(); + + log.debug("Comparing {} with expected {}", marshalled, expected); + assertJsonEquals(expected, marshalled, ignores); + return marshalled; + } + + + protected static T roundTripAndSimilar(ObjectMapper mapper, T input, JsonNode expected, JavaType typeReference, boolean remarshall) throws Exception { + return roundTripAndSimilar(mapper, input, mapper.writeValueAsString(expected), typeReference, remarshall); + } + + /** + * Can be used if the input is not a stand alone json object. + */ + public static T roundTripAndSimilarValue(T input, String expected) { + TestClass embed = new TestClass<>(input); + JavaType type = Jackson2Mapper.getInstance().getTypeFactory() + .constructParametricType(TestClass.class, input.getClass()); + + TestClass result = roundTripAndSimilar(embed, "{\"value\": " + expected + "}", type, true); + + return result.value; + } + + public static , T> JsonObjectAssert assertThatJson(T o) { + return new JsonObjectAssert<>(o); + } + + + public static JsonStringAssert assertThatJson(byte[] o) { + return assertThatJson(new String(o, StandardCharsets.UTF_8)); + } + public static , T> JsonObjectAssert assertThatJson(ObjectMapper mapper, T o) { + return new JsonObjectAssert<>(mapper, o); + } + + public static , T> JsonObjectAssert assertThatJson(Class o, String value) { + return new JsonObjectAssert<>(o, value); + } + + public static , T> JsonObjectAssert assertThatJson(ObjectMapper mapper, Class o, String value) { + return new JsonObjectAssert<>(mapper, o, value); + } + + public static JsonStringAssert assertThatJson(String o) { + return new JsonStringAssert(o); + } + + + @SuppressWarnings("UnusedReturnValue") + public static class JsonObjectAssert, A> extends AbstractObjectAssert implements Supplier { + + A rounded; + private final ObjectMapper mapper; + + private boolean checkRemarshal = true; + + private List ignores = new ArrayList<>(); + + protected JsonObjectAssert(A actual) { + super(actual, JsonObjectAssert.class); + this.mapper = MAPPER; + } + + + protected JsonObjectAssert(Class actual, String string) { + super(read(MAPPER, actual, string), JsonObjectAssert.class); + this.mapper = MAPPER; + } + + protected JsonObjectAssert(ObjectMapper mapper, A actual) { + super(actual, JsonObjectAssert.class); + this.mapper = mapper; + } + + + protected JsonObjectAssert(ObjectMapper mapper, Class actual, String string) { + super(read(mapper, actual, string), JsonObjectAssert.class); + this.mapper = mapper; + } + + public JsonObjectAssert ignore(JsonPointer... jsonPointers) { + ignores.addAll(Arrays.asList(jsonPointers)); + return this; + } + + public JsonObjectAssert ignore(String... jsonPointers) { + Arrays.stream(jsonPointers).map(JsonPointer::compile).forEach(jp -> ignores.add(jp)); + return this; + } + + + + protected static A read(ObjectMapper mapper, Class actual, String string) { + try { + return mapper.readValue(string, actual); + } catch (IOException e) { + Fail.fail(e.getMessage(), e); + return null; + } + } + + @SuppressWarnings({"CatchMayIgnoreException"}) + public S isSimilarTo(String expected) { + try { + rounded = roundTripAndSimilar(mapper, actual, expected, checkRemarshal, ignores.toArray(i -> new JsonPointer[i])); + } catch (Exception e) { + Fail.fail(e.getMessage(), e); + } + return myself; + } + + public JsonObjectAssert withoutRemarshalling() { + JsonObjectAssert copy = new JsonObjectAssert<>(mapper, actual); + copy.checkRemarshal = false; + return copy; + } + public JsonMarshallAssert withoutUnmarshalling() { + return new JsonMarshallAssert<>(mapper, actual); + } + + @SuppressWarnings({"CatchMayIgnoreException"}) + public S isSimilarTo(JsonNode expected) { + try { + rounded = roundTripAndSimilar(mapper, actual, expected, checkRemarshal); + } catch (Exception e) { + Fail.fail(e.getMessage(), e); + } + return myself; + } + + public AbstractObjectAssert andRounded() { + if (rounded == null) { + throw new IllegalStateException("No similation was done already."); + } + return assertThat(rounded); + } + public A get() { + if (rounded == null) { + throw new IllegalStateException("No similation was done already."); + } + return rounded; + } + + } + public static class JsonMarshallAssert + extends AbstractObjectAssert, A> implements Supplier { + + final ObjectMapper mapper; + String marshalled; + + protected JsonMarshallAssert(A a) { + this(MAPPER, a); + } + + protected JsonMarshallAssert(ObjectMapper mapper, A a) { + super(a, JsonMarshallAssert.class); + this.mapper = mapper; + } + + @SneakyThrows + public JsonMarshallAssert isSimilarTo(String expected) { + marshalled = marshallAndSimilar(mapper, actual, expected); + return myself; + } + + @Override + public String get() { + if (marshalled == null) { + throw new IllegalStateException("No similation was done already."); + } + return marshalled; + } + } + + + public static class JsonStringAssert extends AbstractObjectAssert { + + private JsonNode actualJson; + + protected JsonStringAssert(CharSequence actual) { + super(actual, JsonStringAssert.class); + } + + public JsonStringAssert isSimilarTo(String expected, JsonPointer... ignores) { + assertJsonEquals("", expected, actual, ignores); + return myself; + } + + public JsonStringAssert containsKeys(String... keys) { + actualJson(); + List notFound = new ArrayList<>(); + for (String key : keys) { + if (actualJson.get(key) == null) { + notFound.add(key); + } + } + assertThat(notFound).withFailMessage("Keys " + notFound + " found (in " + actualJson + ")").isEmpty(); + return myself; + } + + public JsonStringAssert doesNotContainKeys(String... keys) { + actualJson(); + List found = new ArrayList<>(); + + for (String key : keys) { + if (actualJson.get(key) != null) { + found.add(key); + } + + } + assertThat(found).withFailMessage("Unexpected keys" + found + " found (in "+ actualJson + ")").isEmpty(); + return myself; + } + + @SneakyThrows + protected void actualJson() { + if (actualJson == null) { + actualJson = MAPPER.readTree(actual.toString()); + } + } + + public JsonStringAssert isSimilarToResource(String resource) { + try { + InputStream resourceAsStream = getClass().getResourceAsStream(resource); + if (resourceAsStream == null) { + throw new IllegalArgumentException("No such resource " + resource); + } + String expected = IOUtils.toString(resourceAsStream, StandardCharsets.UTF_8); + return isSimilarTo(expected); + } catch (IOException ioe) { + throw new RuntimeException(ioe); + } + + } + } + +} diff --git a/vpro-shared-util/pom.xml b/vpro-shared-util/pom.xml index 69e496904..f8d5cef49 100644 --- a/vpro-shared-util/pom.xml +++ b/vpro-shared-util/pom.xml @@ -101,5 +101,10 @@ junit-platform-launcher provided + + io.github.natty-parser + natty + true + diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson/Views.java b/vpro-shared-util/src/main/java/nl/vpro/jackson/Views.java similarity index 97% rename from vpro-shared-jackson3/src/main/java/nl/vpro/jackson/Views.java rename to vpro-shared-util/src/main/java/nl/vpro/jackson/Views.java index 459d19694..ba475af70 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson/Views.java +++ b/vpro-shared-util/src/main/java/nl/vpro/jackson/Views.java @@ -1,7 +1,7 @@ package nl.vpro.jackson; import com.google.common.annotations.Beta; -import nl.vpro.jackson3.Jackson3Mapper; + /** * Several setups at VPRO and at NPO involve a backend system that publishes JSON to ElasticSearch. diff --git a/vpro-shared-jackson2/src/main/java/nl/vpro/jackson2/NattySupport.java b/vpro-shared-util/src/main/java/nl/vpro/util/NattySupport.java similarity index 82% rename from vpro-shared-jackson2/src/main/java/nl/vpro/jackson2/NattySupport.java rename to vpro-shared-util/src/main/java/nl/vpro/util/NattySupport.java index 5c911cc4a..f8b53d5e8 100644 --- a/vpro-shared-jackson2/src/main/java/nl/vpro/jackson2/NattySupport.java +++ b/vpro-shared-util/src/main/java/nl/vpro/util/NattySupport.java @@ -1,4 +1,4 @@ -package nl.vpro.jackson2; +package nl.vpro.util; import lombok.extern.slf4j.Slf4j; @@ -8,8 +8,7 @@ import org.natty.DateGroup; import org.natty.Parser; -import nl.vpro.util.BindingUtils; -import nl.vpro.util.DateUtils; + /** * The dependency on natty in {@link StringInstantToJsonTimestamp} is optional. Put support for it in this class, so we can just catch the resulting NoClassDefFoundError. @@ -18,11 +17,11 @@ */ @Slf4j -class NattySupport { +public class NattySupport { private static final Parser PARSER = new Parser(TimeZone.getTimeZone(BindingUtils.DEFAULT_ZONE)); - static Optional parseDate(String value) { + public static Optional parseDate(String value) { List groups = PARSER.parse(value); if (groups.size() == 1) { log.info("Parsed date '{}' to {}", value, groups.get(0).getDates()); From 5d377c69ef1ba1cebcc18fc9efc7fe1795eb2173 Mon Sep 17 00:00:00 2001 From: Michiel Meeuwissen Date: Wed, 3 Dec 2025 10:13:52 +0100 Subject: [PATCH 04/18] added dep -jackson3 --- pom.xml | 2 +- vpro-shared-bom/pom.xml | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 4476d4b7b..2e0360020 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ 6.2.13 6.5.6 2.20.1 - 3.0.0 + 3.0.3 6.0.1 5.20.0 diff --git a/vpro-shared-bom/pom.xml b/vpro-shared-bom/pom.xml index 6d6d15955..38ff74802 100644 --- a/vpro-shared-bom/pom.xml +++ b/vpro-shared-bom/pom.xml @@ -106,6 +106,11 @@ vpro-shared-jackson2 ${project.version} + + nl.vpro.shared + vpro-shared-jackson3 + ${project.version} + nl.vpro.shared vpro-shared-logging From 23cc0fb68d0624b12412ef56fb513c2c63ed02be Mon Sep 17 00:00:00 2001 From: Michiel Meeuwissen Date: Fri, 5 Dec 2025 14:37:03 +0100 Subject: [PATCH 05/18] More jackson3 support work. --- vpro-shared-jackson2/README.md | 4 - vpro-shared-jackson3/README.adoc | 16 +++ vpro-shared-jackson3/README.md | 19 ---- .../jackson3/BackwardsCompatibleJsonEnum.java | 6 +- .../java/nl/vpro/jackson3/DateModule.java | 8 +- .../nl/vpro/jackson3/GuavaRangeModule.java | 10 +- .../java/nl/vpro/jackson3/IterableJson.java | 4 +- .../java/nl/vpro/jackson3/Jackson3Mapper.java | 98 +++++++++++-------- .../jackson3/rs/JacksonContextResolver.java | 21 ++-- .../jackson3/rs/JsonIdAdderBodyReader.java | 5 +- .../vpro/jackson3/GuavaRangeModuleTest.java | 20 ++-- .../nl/vpro/jackson3/IterableJsonTest.java | 6 +- ...apperTest.java => Jackson3MapperTest.java} | 11 +-- .../LenientBooleanDeserializerTest.java | 11 +-- .../rs/JsonIdAdderBodyReaderTest.java | 7 +- 15 files changed, 124 insertions(+), 122 deletions(-) create mode 100644 vpro-shared-jackson3/README.adoc delete mode 100644 vpro-shared-jackson3/README.md rename vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/{Jackson2MapperTest.java => Jackson3MapperTest.java} (89%) diff --git a/vpro-shared-jackson2/README.md b/vpro-shared-jackson2/README.md index 7f0d0aa00..ea276ea72 100644 --- a/vpro-shared-jackson2/README.md +++ b/vpro-shared-jackson2/README.md @@ -13,7 +13,3 @@ Also `nl.vpro.jackson2.Views` is provided which defines a few classes which can This package also contains `nl.vpro.jackson2.JsonArrayIterator`. A tool to wrap, using jackson, a stream of json object into an iterator of java objects. - -## AfterUnmarshallDeserializer - -Jackson lacks support for `#afterUnmarshall'. The 'nl.vpro.jackson2.AfterUnmarshalDeserializer' deserializer adds it. This can e.g. be used to add references to parent objects. diff --git a/vpro-shared-jackson3/README.adoc b/vpro-shared-jackson3/README.adoc new file mode 100644 index 000000000..5fc23e138 --- /dev/null +++ b/vpro-shared-jackson3/README.adoc @@ -0,0 +1,16 @@ += Jackson3 utilities + +image:http://www.javadoc.io/badge/nl.vpro.shared/vpro-shared-jackson3.svg?color=blue[javadoc,link=http://www.javadoc.io/doc/nl.vpro.shared/vpro-shared-jackson3] + + + +We collect some generic Jackson3 utilities. Mainly `tools.jackson.databind.ValueSerializer`s and `tools.jackson.databind.ValueDeserializer`s. + +Some of them are bundled in modules. E.g. a `nl.vpro.jackson3.DateModule`, which can will make a `tools.jackson.databind.ObjectMapper` recognize `java.time` classes +(but a bit differently then `com.fasterxml.jackson.datatype.jsr310.JavaTimeModule` does, which it predates). + +Also `nl.vpro.jackson2.Views` is provided which defines a few classes which can be used with `@com.fasterxml.jackson.annotation.JsonView`. + +== JsonArrayIterator + +This package also contains `nl.vpro.jackson3.JsonArrayIterator`. A tool to wrap, using jackson, a stream of json object into an iterator of java objects. diff --git a/vpro-shared-jackson3/README.md b/vpro-shared-jackson3/README.md deleted file mode 100644 index 7f0d0aa00..000000000 --- a/vpro-shared-jackson3/README.md +++ /dev/null @@ -1,19 +0,0 @@ -[![javadoc](http://www.javadoc.io/badge/nl.vpro.shared/vpro-shared-jackson2.svg?color=blue)](http://www.javadoc.io/doc/nl.vpro.shared/vpro-shared-jackson2) - -# Jackson2 utilities - -We collect some generic Jackson2 utilities. Mainly `com.fasterxml.jackson.databind.JsonSerializer`s and `com.fasterxml.jackson.databind.JsonDeserializer`s. - -Some of them are bundled in modules. E.g. a `nl.vpro.jackson2.DateModule`, which can will make a `com.fasterxml.jackson.databind.ObjectMapper` recognize `java.time` classes -(but a bit differently then `com.fasterxml.jackson.datatype.jsr310.JavaTimeModule` does, which it predates). - -Also `nl.vpro.jackson2.Views` is provided which defines a few classes which can be used with `@com.fasterxml.jackson.annotation.JsonView`. - -## JsonArrayIterator - -This package also contains `nl.vpro.jackson2.JsonArrayIterator`. A tool to wrap, using jackson, a stream of json object into an iterator of java objects. - - -## AfterUnmarshallDeserializer - -Jackson lacks support for `#afterUnmarshall'. The 'nl.vpro.jackson2.AfterUnmarshalDeserializer' deserializer adds it. This can e.g. be used to add references to parent objects. diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/BackwardsCompatibleJsonEnum.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/BackwardsCompatibleJsonEnum.java index 6c1785bd3..b3799c6a3 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/BackwardsCompatibleJsonEnum.java +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/BackwardsCompatibleJsonEnum.java @@ -1,11 +1,9 @@ package nl.vpro.jackson3; import lombok.extern.slf4j.Slf4j; - -import java.io.IOException; - import tools.jackson.core.*; import tools.jackson.databind.*; +import tools.jackson.databind.cfg.EnumFeature; /** * Newer jackson version suddenly recognized @XmlEnumValue. This makes it possible to fall back to old behaviour. @@ -43,7 +41,7 @@ public T deserialize(JsonParser jp, DeserializationContext ctxt) { try { return Enum.valueOf(enumClass, jp.getValueAsString().toUpperCase()); } catch (IllegalArgumentException iaeu) { - if (ctxt.getConfig().hasDeserializationFeatures(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL.getMask())) { + if (ctxt.getConfig().hasDeserializationFeatures(EnumFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL. getMask())) { return null; } else { throw iae; diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/DateModule.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/DateModule.java index 1c25cce27..53d604629 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/DateModule.java +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/DateModule.java @@ -1,12 +1,12 @@ package nl.vpro.jackson3; +import tools.jackson.core.Version; +import tools.jackson.databind.module.SimpleModule; + import java.io.Serial; import java.time.*; import java.util.Date; -import tools.jackson.core.Version; -import tools.jackson.databind.module.SimpleModule; - /** * Work around for JACKSON-920 @@ -22,7 +22,7 @@ public class DateModule extends SimpleModule { private static final long serialVersionUID = 1L; public DateModule() { - super(new Version(0, 31, 0, "", "nl.vpro.shared", "vpro-jackson2")); + super(new Version(0, 31, 0, "", "nl.vpro.shared", "vpro-jackson3")); // first deserializers addDeserializer(Date.class, DateToJsonTimestamp.Deserializer.INSTANCE); diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/GuavaRangeModule.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/GuavaRangeModule.java index 0cf027513..4e7cd8014 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/GuavaRangeModule.java +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/GuavaRangeModule.java @@ -98,7 +98,7 @@ public Range deserialize(JsonParser p, DeserializationContext ctxt) { JsonNode node = ctxt.readValue(p, JsonNode.class); if (node.has("type")) { Class type = Class.forName(node.get("type").stringValue()); - return of(type, p, node); + return of(type, ctxt, node); } else { return Range.all(); } @@ -106,7 +106,7 @@ public Range deserialize(JsonParser p, DeserializationContext ctxt) { } } - static > Range of(Class clazz, JsonParser p, JsonNode node) throws IOException { + static > Range of(Class clazz, DeserializationContext context, JsonNode node) throws IOException { BoundType lowerBoundType = null; C lowerValue = null; @@ -115,11 +115,12 @@ static > Range of(Class clazz, JsonParser p, JsonN if (node.has(LOWER_ENDPOINT)) { lowerBoundType = BoundType.valueOf(node.get(LOWER_BOUND_TYPE).asString()); - lowerValue = p.objectReadContext().readValue(p, node.get(LOWER_ENDPOINT), clazz); + JsonNode jsonNode = node.get(LOWER_ENDPOINT); + lowerValue = context.readTreeAsValue(node.get(LOWER_ENDPOINT), clazz); } if (node.has(UPPER_ENDPOINT)) { upperBoundType = BoundType.valueOf(node.get(UPPER_BOUND_TYPE).asString()); - upperValue = p.(UPPER_ENDPOINT), clazz); + upperValue = context.readTreeAsValue(node.get(UPPER_ENDPOINT), clazz); } if (lowerValue != null) { if (upperValue != null) { @@ -137,5 +138,4 @@ static > Range of(Class clazz, JsonParser p, JsonN } - } diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/IterableJson.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/IterableJson.java index 10622f88b..bb904f58f 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/IterableJson.java +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/IterableJson.java @@ -6,9 +6,7 @@ import tools.jackson.core.*; import tools.jackson.databind.*; -import tools.jackson.databind.JsonDeserializer; -import tools.jackson.databind.JsonSerializer; -import tools.jackson.databind.SerializerProvider; + /** * @author Michiel Meeuwissen diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/Jackson3Mapper.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/Jackson3Mapper.java index 6eaa3c81f..5bce1fdf2 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/Jackson3Mapper.java +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/Jackson3Mapper.java @@ -13,6 +13,7 @@ import tools.jackson.core.JsonParser; import tools.jackson.core.json.JsonReadFeature; import tools.jackson.databind.*; +import tools.jackson.databind.cfg.EnumFeature; import tools.jackson.databind.introspect.AnnotationIntrospectorPair; import tools.jackson.databind.introspect.JacksonAnnotationIntrospector; import tools.jackson.databind.json.JsonMapper; @@ -46,7 +47,7 @@ */ @Slf4j -public class Jackson3Mapper extends JsonMapper { +public class Jackson3Mapper { @Serial private static final long serialVersionUID = 8353430660109292010L; @@ -58,34 +59,52 @@ public class Jackson3Mapper extends JsonMapper { - public static final Jackson3Mapper INSTANCE = getInstance(); - public static final Jackson3Mapper LENIENT = getLenientInstance(); - public static final Jackson3Mapper STRICT = getStrictInstance(); - public static final Jackson3Mapper PRETTY_STRICT = getPrettyStrictInstance(); - public static final Jackson3Mapper PRETTY = getPrettyInstance(); - public static final Jackson3Mapper PUBLISHER = getPublisherInstance(); - public static final Jackson3Mapper PRETTY_PUBLISHER = getPublisherInstance(); + public static final JsonMapper INSTANCE = getInstance(); + public static final JsonMapper LENIENT = getLenientInstance(); + public static final JsonMapper STRICT = getStrictInstance(); + public static final JsonMapper PRETTY_STRICT = getPrettyStrictInstance(); + public static final JsonMapper PRETTY = getPrettyInstance(); + public static final JsonMapper PUBLISHER = getPublisherInstance(); + public static final JsonMapper PRETTY_PUBLISHER = getPublisherInstance(); - public static final Jackson3Mapper BACKWARDS_PUBLISHER = getBackwardsPublisherInstance(); + public static final JsonMapper BACKWARDS_PUBLISHER = getBackwardsPublisherInstance(); private static final ThreadLocal THREAD_LOCAL = ThreadLocal.withInitial(Jackson3Mapper::getInstance); - public static Jackson3Mapper getInstance() { - Jackson3Mapper mapper = new Jackson3Mapper("instance"); - Jackson3Mapper.builder() + public static JsonMapper getInstance() { + + var mapper = JsonMapper.builder() .build(); - mapper.setConfig(mapper.getSerializationConfig().withView(Views.Forward.class)); - mapper.setConfig(mapper.getDeserializationConfig().withView(Views.Forward.class)); + + var config = mapper.serializationConfig().withView(Views.Forward.class); + mapper.rebuild() + .buildSerializationConfig(config, ) + config. + ; + + .writerWithView() + + .buildSerializationConfig() + + + + mapper.setConfig(mapper.serializationConfig().withView(Views.Forward.class)); + mapper.setConfig(mapper.deserializationConfig().withView(Views.Forward.class)); + .wi(c -> { + + }) + .build(); + return mapper; } - public static Jackson3Mapper getLenientInstance() { + public static JsonMapper getLenientInstance() { Jackson3Mapper lenient = new Jackson3Mapper("lenient"); - lenient.enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL); + lenient.enable(EnumFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL); lenient.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); lenient.enable(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES); lenient.enable(JsonParser.Feature.ALLOW_SINGLE_QUOTES); @@ -94,13 +113,13 @@ public static Jackson3Mapper getLenientInstance() { return lenient; } - public static Jackson3Mapper getPrettyInstance() { + public static JsonMapper getPrettyInstance() { Jackson3Mapper pretty = new Jackson3Mapper("pretty"); pretty.enable(SerializationFeature.INDENT_OUTPUT); return pretty; } - public static Jackson3Mapper getPrettyStrictInstance() { + public static JsonMapper getPrettyStrictInstance() { Jackson3Mapper pretty_and_strict = new Jackson3Mapper("pretty_strict"); pretty_and_strict.enable(SerializationFeature.INDENT_OUTPUT); @@ -112,7 +131,7 @@ public static Jackson3Mapper getPrettyStrictInstance() { } - public static Jackson3Mapper getStrictInstance() { + public static JsonMapper getStrictInstance() { Jackson3Mapper strict = new Jackson3Mapper("strict"); strict.enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); strict.setConfig(strict.getSerializationConfig().withView(Views.Forward.class)); @@ -121,7 +140,7 @@ public static Jackson3Mapper getStrictInstance() { return strict; } - public static Jackson3Mapper getPublisherInstance() { + public static JsonMapper getPublisherInstance() { Jackson3Mapper publisher = new Jackson3Mapper("publisher"); publisher.setConfig(publisher.getSerializationConfig().withView(Views.ForwardPublisher.class)); publisher.setConfig(publisher.getDeserializationConfig().withView(Views.Forward.class)); @@ -129,7 +148,7 @@ public static Jackson3Mapper getPublisherInstance() { return publisher; } - public static Jackson3Mapper getPrettyPublisherInstance() { + public static JsonMapper getPrettyPublisherInstance() { Jackson3Mapper prettyPublisher = new Jackson3Mapper("pretty_publisher"); prettyPublisher.setConfig(prettyPublisher.getSerializationConfig().withView(Views.ForwardPublisher.class)); prettyPublisher.setConfig(prettyPublisher.getDeserializationConfig().withView(Views.Forward.class)); @@ -137,7 +156,7 @@ public static Jackson3Mapper getPrettyPublisherInstance() { return prettyPublisher; } - public static Jackson3Mapper getBackwardsPublisherInstance() { + public static JsonMapper getBackwardsPublisherInstance() { Jackson3Mapper backwardsPublisher = new Jackson3Mapper("backwards_publisher"); backwardsPublisher.setConfig(backwardsPublisher.getSerializationConfig().withView(Views.Publisher.class)); backwardsPublisher.setConfig(backwardsPublisher.getDeserializationConfig().withView(Views.Normal.class)); @@ -146,20 +165,20 @@ public static Jackson3Mapper getBackwardsPublisherInstance() { } @Beta - public static Jackson3Mapper getModelInstance() { + public static JsonMapper getModelInstance() { Jackson3Mapper model = new Jackson3Mapper("model"); model.setConfig(model.getSerializationConfig().withView(Views.Model.class)); return model; } @Beta - public static Jackson3Mapper getModelAndNormalInstance() { + public static JsonMapper getModelAndNormalInstance() { Jackson3Mapper modalAndNormal = new Jackson3Mapper("model_and_normal"); modalAndNormal.setConfig(modalAndNormal.getSerializationConfig().withView(Views.ModelAndNormal.class)); return modalAndNormal; } - public static Jackson3Mapper getThreadLocal() { + public static JsonMapper getThreadLocal() { return THREAD_LOCAL.get(); } public static void setThreadLocal(Jackson3Mapper set) { @@ -177,7 +196,7 @@ public static T lenientTreeToValue(JsonNode jsonNode, Class clazz) { private final String toString; - private Jackson3Mapper(String toString, Predicate predicate) { + private Jackson3Mapper(String toString, Predicate predicate) { configureMapper(this, predicate); this.toString = toString; } @@ -201,7 +220,7 @@ public static void configureMapper(ObjectMapper mapper) { configureMapper(mapper, m -> true); } - public static void configureMapper(ObjectMapper mapper, Predicate filter) { + public static void configureMapper(ObjectMapper mapper, Predicate filter) { mapper.setFilterProvider(FILTER_PROVIDER); AnnotationIntrospector introspector = new AnnotationIntrospectorPair( @@ -242,8 +261,8 @@ public static void configureMapper(ObjectMapper mapper, Predicate filter // jdk8Module.configureAbsentsAsNulls(true); This I think it covered by com.fasterxml.jackson.annotation.JsonInclude.Include.NON_ABSENT register(mapper, filter, jdk8Module); - mapper.setConfig(mapper.getSerializationConfig().withView(Views.Normal.class)); - mapper.setConfig(mapper.getDeserializationConfig().withView(Views.Normal.class)); + mapper.setConfig(mapper.serializationConfig().withView(Views.Normal.class)); + mapper.setConfig(mapper.serializationConfig().withView(Views.Normal.class)); //SimpleModule module = new SimpleModule(); @@ -251,8 +270,8 @@ public static void configureMapper(ObjectMapper mapper, Predicate filter //mapper.registerModule(module); try { - Class avro = Class.forName("nl.vpro.jackson2.SerializeAvroModule"); - register(mapper, filter, (com.fasterxml.jackson.databind.Module) avro.getDeclaredConstructor().newInstance()); + Class avro = Class.forName("nl.vpro.jackson3.SerializeAvroModule"); + register(mapper, filter, (tools.jackson.databind.Module) avro.getDeclaredConstructor().newInstance()); } catch (ClassNotFoundException ncdfe) { if (! loggedAboutAvro) { log.debug("SerializeAvroModule could not be registered because: " + ncdfe.getClass().getName() + " " + ncdfe.getMessage()); @@ -278,7 +297,7 @@ public static void addFilter(String key, PropertyFilter filter) { log.info("Installed filter {} -> {}", key, filter); } - private static void register(ObjectMapper mapper, Predicate predicate, Module module) { + private static void register(ObjectMapper mapper, Predicate predicate, JacksonModule module) { if (predicate.test(module)) { mapper.registerModule(module); } @@ -310,17 +329,12 @@ public HttpResponse.BodySubscriber apply(HttpResponse.ResponseInfo responseIn return HttpResponse.BodySubscribers.mapping( HttpResponse.BodySubscribers.ofInputStream(), body -> { - try { - SimpleLogger simple = slf4j(log); - if (simple.isEnabled(level)) { - body = new LoggingInputStream(simple, body, level); - } - return readValue(body, type); - - } catch (IOException e) { - log.warn(e.getMessage(), e); - return null; + SimpleLogger simple = slf4j(log); + if (simple.isEnabled(level)) { + body = new LoggingInputStream(simple, body, level); } + return readValue(body, type); + }); } }; diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/rs/JacksonContextResolver.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/rs/JacksonContextResolver.java index e816713fe..2a0b55481 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/rs/JacksonContextResolver.java +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/rs/JacksonContextResolver.java @@ -1,16 +1,17 @@ package nl.vpro.jackson3.rs; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.jakarta.rs.json.JacksonXmlBindJsonProvider; + +import java.util.function.Supplier; + import jakarta.annotation.Priority; import jakarta.ws.rs.*; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.ext.ContextResolver; import jakarta.ws.rs.ext.Provider; -import java.util.function.Supplier; - -import tools.jackson.databind.ObjectMapper; -import tools.jackson.jakarta.rs.json.JacksonXmlBindJsonProvider; - import nl.vpro.jackson3.Jackson3Mapper; /** @@ -25,20 +26,20 @@ @Consumes({MediaType.APPLICATION_JSON, "application/*+json", "text/json"}) @Produces({MediaType.APPLICATION_JSON, "application/*+json", "text/json"}) @Priority(JacksonContextResolver.PRIORITY) -public class JacksonContextResolver extends JacksonXmlBindJsonProvider implements ContextResolver { +public class JacksonContextResolver extends JacksonXmlBindJsonProvider implements ContextResolver { static final int PRIORITY = Priorities.USER; - private final ThreadLocal mapper; + private final ThreadLocal mapper; public JacksonContextResolver() { this(Jackson3Mapper.getLenientInstance()); } - public JacksonContextResolver(ObjectMapper mapper) { + public JacksonContextResolver(JsonMapper mapper) { this(() -> mapper); } - public JacksonContextResolver(Supplier mapper) { + public JacksonContextResolver(Supplier mapper) { this.mapper = ThreadLocal.withInitial(mapper); } @@ -50,7 +51,7 @@ public ObjectMapper getContext(Class objectType) { /** * @since 4.0 */ - public void set(ObjectMapper mapper) { + public void set(JsonMapper mapper) { this.mapper.set(mapper); } diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/rs/JsonIdAdderBodyReader.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/rs/JsonIdAdderBodyReader.java index a138c4ea9..2c8ed4264 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/rs/JsonIdAdderBodyReader.java +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/rs/JsonIdAdderBodyReader.java @@ -58,8 +58,9 @@ public Object readFrom( final JavaType javaType = mapper.getTypeFactory().constructType(genericType); final JsonNode jsonNode = mapper.readTree(entityStream); if (jsonNode instanceof ObjectNode objectNode) { - final TypeDeserializer typeDeserializer = mapper.deserializationConfig().getTypeResolverProvider() - .findTypeDeserializer(javaType); + final TypeDeserializer typeDeserializer = mapper.deserializationConfig() + .getTypeResolverProvider() + .findTypeDeserializer(mapper.d, javaType); if (typeDeserializer != null) { final String propertyName = typeDeserializer.getPropertyName(); final String propertyValue = typeDeserializer.getTypeIdResolver().idFromBaseType(); diff --git a/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/GuavaRangeModuleTest.java b/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/GuavaRangeModuleTest.java index 448f34c57..10a23abe8 100644 --- a/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/GuavaRangeModuleTest.java +++ b/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/GuavaRangeModuleTest.java @@ -1,22 +1,22 @@ package nl.vpro.jackson3; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.annotation.JsonSerialize; +import tools.jackson.databind.json.JsonMapper; + import java.time.Instant; import org.junit.jupiter.api.Test; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.google.common.collect.Range; -import nl.vpro.jackson3.DateModule; -import nl.vpro.jackson3.GuavaRangeModule; import static org.assertj.core.api.Assertions.assertThat; class GuavaRangeModuleTest { - ObjectMapper mapper = new ObjectMapper(); + JsonMapper mapper = JsonMapper.builder() + .withModules(new DateModule()); { mapper.registerModule(new DateModule()); mapper.registerModule(new GuavaRangeModule()); @@ -41,7 +41,7 @@ static class WithInstantRange { @Test - public void without() throws JsonProcessingException { + public void without() throws JacksonException { WithoutSerializer a = new WithoutSerializer(); a.range = Range.closedOpen(1, 2); String example = "{\"range\":{\"lowerEndpoint\":1,\"lowerBoundType\":\"CLOSED\",\"upperEndpoint\":2,\"upperBoundType\":\"OPEN\",\"type\":\"java.lang.Integer\"},\"anotherField\":1}"; @@ -54,14 +54,14 @@ public void without() throws JsonProcessingException { @Test - public void empty() throws JsonProcessingException { + public void empty() throws JacksonException { WithIntegerRange a = new WithIntegerRange(); assertThat(mapper.writeValueAsString(a)).isEqualTo("{\"range\":null}"); } @Test - public void filled() throws JsonProcessingException { + public void filled() throws JacksonException { WithIntegerRange a = new WithIntegerRange(); a.range = Range.closedOpen(1, 10); @@ -70,7 +70,7 @@ public void filled() throws JsonProcessingException { } @Test - public void instant() throws JsonProcessingException { + public void instant() throws JacksonException { WithInstantRange a = new WithInstantRange(); a.range = Range.closedOpen( Instant.parse("2021-12-24T10:00:00Z"), diff --git a/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/IterableJsonTest.java b/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/IterableJsonTest.java index 38ceb894b..919cdfce2 100644 --- a/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/IterableJsonTest.java +++ b/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/IterableJsonTest.java @@ -1,5 +1,8 @@ package nl.vpro.jackson3; +import tools.jackson.databind.annotation.JsonDeserialize; +import tools.jackson.databind.annotation.JsonSerialize; + import java.io.StringWriter; import java.util.*; @@ -11,9 +14,6 @@ import org.skyscreamer.jsonassert.JSONAssert; import org.skyscreamer.jsonassert.JSONCompareMode; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; - import static org.assertj.core.api.Assertions.assertThat; /** diff --git a/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/Jackson2MapperTest.java b/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/Jackson3MapperTest.java similarity index 89% rename from vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/Jackson2MapperTest.java rename to vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/Jackson3MapperTest.java index aeddda62e..82cfb0c76 100644 --- a/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/Jackson2MapperTest.java +++ b/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/Jackson3MapperTest.java @@ -1,6 +1,8 @@ package nl.vpro.jackson3; import lombok.extern.log4j.Log4j2; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.exc.InvalidFormatException; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -10,14 +12,11 @@ import org.junit.jupiter.api.Test; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.exc.InvalidFormatException; - import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @Log4j2 -public class Jackson2MapperTest { +public class Jackson3MapperTest { public enum EnumValues { a, @@ -75,7 +74,7 @@ public void readUnknownEnumValueLenient() throws IOException { } @Test - public void write() throws JsonProcessingException { + public void write() throws JacksonException { A a = new A(); a.integer = 2; a.optional = Optional.of(3); @@ -83,7 +82,7 @@ public void write() throws JsonProcessingException { } @Test - public void writeWithEmptyOptional() throws JsonProcessingException { + public void writeWithEmptyOptional() throws JacksonException { A a = new A(); a.integer = 2; a.optional = Optional.empty(); diff --git a/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/LenientBooleanDeserializerTest.java b/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/LenientBooleanDeserializerTest.java index 65e580b83..a0cd30a77 100644 --- a/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/LenientBooleanDeserializerTest.java +++ b/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/LenientBooleanDeserializerTest.java @@ -1,13 +1,12 @@ package nl.vpro.jackson3; +import tools.jackson.databind.annotation.JsonDeserialize; +import tools.jackson.databind.json.JsonMapper; + import java.io.IOException; import org.junit.jupiter.api.Test; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; - -import nl.vpro.jackson3.LenientBooleanDeserializer; import static org.assertj.core.api.Assertions.assertThat; @@ -25,7 +24,7 @@ public static class A { boolean b; } - ObjectMapper mapper = new ObjectMapper(); + JsonMapper mapper = new JsonMapper(); @Test public void deserialize() throws IOException { @@ -42,7 +41,7 @@ public void deserialize() throws IOException { @Test public void deserializeNull() throws IOException { - ObjectMapper mapper = new ObjectMapper(); + JsonMapper mapper = new JsonMapper(); assertThat(mapper.readValue("{\"bool\": null}", A.class).bool).isNull(); assertThat(mapper.readValue("{\"bool\": null}", A.class).b).isFalse(); assertThat(mapper.readValue("{\"b\": \"x\"}", A.class).b).isFalse(); diff --git a/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/rs/JsonIdAdderBodyReaderTest.java b/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/rs/JsonIdAdderBodyReaderTest.java index cdf05ada9..7a42d5fff 100644 --- a/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/rs/JsonIdAdderBodyReaderTest.java +++ b/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/rs/JsonIdAdderBodyReaderTest.java @@ -1,5 +1,7 @@ package nl.vpro.jackson3.rs; +import tools.jackson.databind.json.JsonMapper; + import java.io.ByteArrayInputStream; import java.io.IOException; @@ -10,10 +12,7 @@ import org.mockito.Mockito; import com.fasterxml.jackson.annotation.*; -import com.fasterxml.jackson.databind.ObjectMapper; -import nl.vpro.jackson3.rs.JacksonContextResolver; -import nl.vpro.jackson3.rs.JsonIdAdderBodyReader; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.when; @@ -55,7 +54,7 @@ public static class C { final JsonIdAdderBodyReader idAdderInterceptor = new JsonIdAdderBodyReader(); { idAdderInterceptor.providers = Mockito.mock(Providers.class); - when(idAdderInterceptor.providers.getContextResolver(ObjectMapper.class, MediaType.APPLICATION_JSON_TYPE)).thenReturn(new JacksonContextResolver()); + when(idAdderInterceptor.providers.getContextResolver(JsonMapper.class, MediaType.APPLICATION_JSON_TYPE)).thenReturn(new JacksonContextResolver()); } @Test From 72ccf69c9d9af182693bc35c985257151e122c39 Mon Sep 17 00:00:00 2001 From: Michiel Meeuwissen Date: Wed, 7 Jan 2026 13:44:50 +0100 Subject: [PATCH 06/18] Made it compile. --- .../nl/vpro/jackson2/GuavaRangeModule.java | 6 +- .../java/nl/vpro/jackson2/Jackson2Mapper.java | 1 + .../jackson3/BackwardsCompatibleJsonEnum.java | 2 - .../java/nl/vpro/jackson3/DateModule.java | 2 - .../nl/vpro/jackson3/DateToJsonTimestamp.java | 3 - .../jackson3/DurationToJsonTimestamp.java | 4 +- .../DurationToSecondsFloatTimestamp.java | 1 - .../vpro/jackson3/InstantToJsonTimestamp.java | 4 - .../InstantToSecondsFloatTimestamp.java | 3 - .../java/nl/vpro/jackson3/IterableJson.java | 12 +- .../java/nl/vpro/jackson3/Jackson3Mapper.java | 339 +++++++++--------- .../nl/vpro/jackson3/JsonArrayIterator.java | 26 +- .../src/main/java/nl/vpro/jackson3/Utils.java | 2 +- .../ZonedDateTimeToJsonTimestamp.java | 1 + .../jackson3/rs/JacksonContextResolver.java | 15 +- .../jackson3/rs/JsonIdAdderBodyReader.java | 35 +- .../vpro/jackson3/GuavaRangeModuleTest.java | 9 +- .../nl/vpro/jackson3/IterableJsonTest.java | 4 +- .../nl/vpro/jackson3/Jackson3MapperTest.java | 30 +- .../vpro/jackson3/JsonArrayIteratorTest.java | 21 +- .../main/java/nl/vpro/util/BindingUtils.java | 1 - .../vpro/util/CountedPeekingIteratorImpl.java | 5 - .../nl/vpro/util/FileCachingInputStream.java | 11 +- .../java/nl/vpro/util/FileInputStreamTee.java | 1 - .../java/nl/vpro/util/FileSizeFormatter.java | 2 +- .../main/java/nl/vpro/util/HTMLStripper.java | 2 +- 26 files changed, 261 insertions(+), 281 deletions(-) diff --git a/vpro-shared-jackson2/src/main/java/nl/vpro/jackson2/GuavaRangeModule.java b/vpro-shared-jackson2/src/main/java/nl/vpro/jackson2/GuavaRangeModule.java index 296dd8dd0..be548eb95 100644 --- a/vpro-shared-jackson2/src/main/java/nl/vpro/jackson2/GuavaRangeModule.java +++ b/vpro-shared-jackson2/src/main/java/nl/vpro/jackson2/GuavaRangeModule.java @@ -16,9 +16,9 @@ public class GuavaRangeModule extends SimpleModule { - public static final String LOWER_ENDPOINT = "lowerEndpoint"; + public static final String LOWER_ENDPOINT = "lowerEndpoint"; public static final String LOWER_BOUND_TYPE = "lowerBoundType"; - public static final String UPPER_ENDPOINT = "upperEndpoint"; + public static final String UPPER_ENDPOINT = "upperEndpoint"; public static final String UPPER_BOUND_TYPE = "upperBoundType"; @Serial @@ -137,6 +137,4 @@ static > Range of(Class clazz, JsonParser p, JsonN } } - - } diff --git a/vpro-shared-jackson2/src/main/java/nl/vpro/jackson2/Jackson2Mapper.java b/vpro-shared-jackson2/src/main/java/nl/vpro/jackson2/Jackson2Mapper.java index 3920eae89..8f9d84c05 100644 --- a/vpro-shared-jackson2/src/main/java/nl/vpro/jackson2/Jackson2Mapper.java +++ b/vpro-shared-jackson2/src/main/java/nl/vpro/jackson2/Jackson2Mapper.java @@ -31,6 +31,7 @@ import com.fasterxml.jackson.module.jakarta.xmlbind.JakartaXmlBindAnnotationIntrospector; import com.google.common.annotations.Beta; +import nl.vpro.jackson.Views; import nl.vpro.logging.simple.SimpleLogger; import nl.vpro.util.LoggingInputStream; diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/BackwardsCompatibleJsonEnum.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/BackwardsCompatibleJsonEnum.java index b3799c6a3..cb64fda6e 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/BackwardsCompatibleJsonEnum.java +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/BackwardsCompatibleJsonEnum.java @@ -23,7 +23,6 @@ public void serialize(Enum value, JsonGenerator jgen, SerializationContext ct jgen.writeString(value.name()); } } - } public static abstract class Deserializer> extends ValueDeserializer { @@ -50,6 +49,5 @@ public T deserialize(JsonParser jp, DeserializationContext ctxt) { } } - } } diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/DateModule.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/DateModule.java index 53d604629..f3c9ecc2a 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/DateModule.java +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/DateModule.java @@ -38,7 +38,5 @@ public DateModule() { addSerializer(ZonedDateTime.class, ZonedDateTimeToJsonTimestamp.Serializer.INSTANCE); addSerializer(LocalDate.class, LocalDateToJsonDate.Serializer.INSTANCE); addSerializer(Duration.class, DurationToJsonTimestamp.Serializer.INSTANCE); - - } } diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/DateToJsonTimestamp.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/DateToJsonTimestamp.java index 19f787198..e1f536fd8 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/DateToJsonTimestamp.java +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/DateToJsonTimestamp.java @@ -22,11 +22,8 @@ public void serialize(Date value, JsonGenerator jgen, SerializationContext ctxt) jgen.writeNumber(value.getTime()); } } - - } - public static class Deserializer extends StdDeserializer { public static final Deserializer INSTANCE = new Deserializer(Date.class); diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/DurationToJsonTimestamp.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/DurationToJsonTimestamp.java index b295ed60f..74ded4063 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/DurationToJsonTimestamp.java +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/DurationToJsonTimestamp.java @@ -19,7 +19,6 @@ private DurationToJsonTimestamp() {} public static class Serializer extends ValueSerializer { - public static final Serializer INSTANCE = new Serializer(); @Override @@ -30,12 +29,10 @@ public void serialize(Duration value, JsonGenerator jgen, SerializationContext c jgen.writeNumber(value.toMillis()); } } - } public static class XmlSerializer extends ValueSerializer { - public static final Serializer INSTANCE = new Serializer(); @Override @@ -52,6 +49,7 @@ public void serialize(javax.xml.datatype.Duration value, JsonGenerator jgen, Ser public static class Deserializer extends ValueDeserializer { public static final Deserializer INSTANCE = new Deserializer(); + @Override public Duration deserialize(JsonParser jp, DeserializationContext ctxt) { if (jp.currentToken() == JsonToken.VALUE_STRING) { diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/DurationToSecondsFloatTimestamp.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/DurationToSecondsFloatTimestamp.java index 1505e6612..9cc2092b4 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/DurationToSecondsFloatTimestamp.java +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/DurationToSecondsFloatTimestamp.java @@ -16,7 +16,6 @@ private DurationToSecondsFloatTimestamp() {} public static class Serializer extends ValueSerializer { - public static final Serializer INSTANCE = new Serializer(); @Override diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/InstantToJsonTimestamp.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/InstantToJsonTimestamp.java index 149114e11..bbfd29b6a 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/InstantToJsonTimestamp.java +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/InstantToJsonTimestamp.java @@ -15,7 +15,6 @@ private InstantToJsonTimestamp() {} public static class Serializer extends ValueSerializer { - public static final Serializer INSTANCE = new Serializer(); @Override @@ -26,11 +25,8 @@ public void serialize(Instant value, JsonGenerator jgen, SerializationContext ct jgen.writeNumber(value.toEpochMilli()); } } - - } - public static class Deserializer extends ValueDeserializer { public static final Deserializer INSTANCE = new Deserializer(); diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/InstantToSecondsFloatTimestamp.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/InstantToSecondsFloatTimestamp.java index 58c489cc0..d0a30366a 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/InstantToSecondsFloatTimestamp.java +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/InstantToSecondsFloatTimestamp.java @@ -25,11 +25,8 @@ public void serialize(Instant value, JsonGenerator jgen, SerializationContext ct jgen.writeNumber(value.toEpochMilli() / 1000f); } } - - } - public static class Deserializer extends ValueDeserializer { public static final Deserializer INSTANCE = new Deserializer(); diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/IterableJson.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/IterableJson.java index bb904f58f..f5e4c63be 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/IterableJson.java +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/IterableJson.java @@ -1,12 +1,11 @@ package nl.vpro.jackson3; -import java.io.IOException; -import java.util.*; -import java.util.function.Function; - import tools.jackson.core.*; import tools.jackson.databind.*; +import java.util.*; +import java.util.function.Function; + /** * @author Michiel Meeuwissen @@ -85,9 +84,8 @@ public Iterable deserialize(JsonParser jp, DeserializationContext ctxt) { } else if (jp.streamReadContext().inArray()) { List list = new ArrayList<>(); jp.clearCurrentToken(); - Iterator i = jp.readValueAs(memberClass); - while (i.hasNext()) { - list.add(i.next()); + while (jp.nextToken() != JsonToken.END_ARRAY) { + list.add(jp.readValueAs(memberClass)); } return creator.apply(list); } else { diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/Jackson3Mapper.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/Jackson3Mapper.java index 5bce1fdf2..284cdcc35 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/Jackson3Mapper.java +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/Jackson3Mapper.java @@ -4,14 +4,10 @@ */ package nl.vpro.jackson3; +import lombok.AccessLevel; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; - -import nl.vpro.jackson.Views; - import tools.jackson.core.JacksonException; -import tools.jackson.core.JsonParser; -import tools.jackson.core.json.JsonReadFeature; import tools.jackson.databind.*; import tools.jackson.databind.cfg.EnumFeature; import tools.jackson.databind.introspect.AnnotationIntrospectorPair; @@ -21,164 +17,157 @@ import tools.jackson.databind.ser.std.SimpleFilterProvider; import tools.jackson.module.jakarta.xmlbind.JakartaXmlBindAnnotationIntrospector; -import java.io.IOException; -import java.io.Serial; import java.lang.reflect.InvocationTargetException; import java.net.http.HttpResponse; import java.util.function.Consumer; import java.util.function.Predicate; -import org.slf4j.event.Level; - +import com.fasterxml.jackson.annotation.JsonInclude; import com.google.common.annotations.Beta; +import nl.vpro.jackson.Views; import nl.vpro.logging.simple.SimpleLogger; import nl.vpro.util.LoggingInputStream; import static nl.vpro.logging.simple.Slf4jSimpleLogger.slf4j; +import static tools.jackson.core.json.JsonReadFeature.*; +import static tools.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES; +import static tools.jackson.databind.MapperFeature.USE_WRAPPER_NAME_AS_PROPERTY_NAME; +import static tools.jackson.databind.SerializationFeature.INDENT_OUTPUT; /** - * TODO: Many static public members that are not unmodifiable (e.g. {@link #INSTANCE}). + * A wrapper around a Jackson {@link ObjectMapper} with one {@link ObjectReader} and one {@link ObjectWriter} configured with specific views. *

- * Please use the static getters (like {@link #getInstance()}, so we could change that. + * In the jackson2 version this was itself an ObjectMapper which could have been configured with default views and so on. + * In jackson3 you can actually define multiple sets of configuration in the mapper itself (with serializationContexts) , but that is a bit more cumbersome to use, and deviates + * more from the original idea. * * @author Rico * @author Michiel Meeuwissen - + * @since 5.14 */ @Slf4j -public class Jackson3Mapper { - @Serial - private static final long serialVersionUID = 8353430660109292010L; +public class Jackson3Mapper { private static boolean loggedAboutAvro = false; private static boolean loggedAboutFallback = false; private static final SimpleFilterProvider FILTER_PROVIDER = new SimpleFilterProvider(); + public static final Jackson3Mapper INSTANCE = Jackson3Mapper.builder("instance") + .configure(Jackson3Mapper::lenient) + .build(); + public static final Jackson3Mapper LENIENT = getLenientInstance(); + public static final Jackson3Mapper STRICT = getStrictInstance(); + public static final Jackson3Mapper PRETTY_STRICT = getPrettyStrictInstance(); + public static final Jackson3Mapper PRETTY = getPrettyInstance(); + public static final Jackson3Mapper PUBLISHER = getPublisherInstance(); + public static final Jackson3Mapper PRETTY_PUBLISHER = getPublisherInstance(); + public static final Jackson3Mapper BACKWARDS_PUBLISHER = getBackwardsPublisherInstance(); - public static final JsonMapper INSTANCE = getInstance(); - public static final JsonMapper LENIENT = getLenientInstance(); - public static final JsonMapper STRICT = getStrictInstance(); - public static final JsonMapper PRETTY_STRICT = getPrettyStrictInstance(); - public static final JsonMapper PRETTY = getPrettyInstance(); - public static final JsonMapper PUBLISHER = getPublisherInstance(); - public static final JsonMapper PRETTY_PUBLISHER = getPublisherInstance(); - - public static final JsonMapper BACKWARDS_PUBLISHER = getBackwardsPublisherInstance(); + private static final ThreadLocal THREAD_LOCAL = ThreadLocal.withInitial(() -> INSTANCE); - private static final ThreadLocal THREAD_LOCAL = ThreadLocal.withInitial(Jackson3Mapper::getInstance); - public static JsonMapper getInstance() { - - var mapper = JsonMapper.builder() - .build(); - - var config = mapper.serializationConfig().withView(Views.Forward.class); - mapper.rebuild() - .buildSerializationConfig(config, ) - config. - ; - - .writerWithView() - - .buildSerializationConfig() - - - - mapper.setConfig(mapper.serializationConfig().withView(Views.Forward.class)); - mapper.setConfig(mapper.deserializationConfig().withView(Views.Forward.class)); - .wi(c -> { - - }) + private static Jackson3Mapper getLenientInstance() { + return Jackson3Mapper.builder("lenient") + .forward() + .configure(Jackson3Mapper::lenient) .build(); + } - return mapper; + private final void allow() { } - public static JsonMapper getLenientInstance() { - Jackson3Mapper lenient = new Jackson3Mapper("lenient"); - lenient.enable(EnumFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL); - lenient.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); - lenient.enable(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES); - lenient.enable(JsonParser.Feature.ALLOW_SINGLE_QUOTES); - lenient.setConfig(lenient.getSerializationConfig().withView(Views.Forward.class)); - lenient.setConfig(lenient.getDeserializationConfig().withView(Views.Forward.class)); - return lenient; + private static void lenient(JsonMapper.Builder builder) { + builder.enable(EnumFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL); + builder.enable(EnumFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL); + builder.disable(FAIL_ON_UNKNOWN_PROPERTIES); + builder.enable(ALLOW_UNQUOTED_PROPERTY_NAMES); + builder.enable(ALLOW_SINGLE_QUOTES); } - - public static JsonMapper getPrettyInstance() { - Jackson3Mapper pretty = new Jackson3Mapper("pretty"); - pretty.enable(SerializationFeature.INDENT_OUTPUT); - return pretty; + private static void strict(JsonMapper.Builder builder) { + builder.enable(FAIL_ON_UNKNOWN_PROPERTIES); } - public static JsonMapper getPrettyStrictInstance() { - Jackson3Mapper pretty_and_strict = new Jackson3Mapper("pretty_strict"); - pretty_and_strict.enable(SerializationFeature.INDENT_OUTPUT); + private static Jackson3Mapper getPrettyInstance() { + Jackson3Mapper.Builder pretty = Jackson3Mapper + .builder("pretty") + .configure(b -> { + lenient(b); + b.enable(INDENT_OUTPUT); + }); + return pretty.build(); + } - pretty_and_strict.setConfig(pretty_and_strict.getSerializationConfig().withView(Views.Forward.class)); - pretty_and_strict.setConfig(pretty_and_strict.getDeserializationConfig().withView(Views.Forward.class)); - pretty_and_strict.enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); // This gives quite a lot of troubles. Though I'd like it to be set, especially because PRETTY is used in tests. + private static Jackson3Mapper getPrettyStrictInstance() { - return pretty_and_strict; + return Jackson3Mapper + .builder("pretty_strict") + .configure(b -> { + strict(b); + b.enable(INDENT_OUTPUT); + b.enable(FAIL_ON_UNKNOWN_PROPERTIES); + }) + .forward() + .build() + ; } - public static JsonMapper getStrictInstance() { - Jackson3Mapper strict = new Jackson3Mapper("strict"); - strict.enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); - strict.setConfig(strict.getSerializationConfig().withView(Views.Forward.class)); - strict.setConfig(strict.getDeserializationConfig().withView(Views.Forward.class)); - - return strict; + private static Jackson3Mapper getStrictInstance() { + return Jackson3Mapper.builder("strict") + .configure(b -> { + b.enable(FAIL_ON_UNKNOWN_PROPERTIES); + } + ).forward() + .build(); } - public static JsonMapper getPublisherInstance() { - Jackson3Mapper publisher = new Jackson3Mapper("publisher"); - publisher.setConfig(publisher.getSerializationConfig().withView(Views.ForwardPublisher.class)); - publisher.setConfig(publisher.getDeserializationConfig().withView(Views.Forward.class)); - - return publisher; + private static Jackson3Mapper getPublisherInstance() { + return Jackson3Mapper.builder("publisher") + .serializationView(Views.ForwardPublisher.class) + .deserializationView(Views.Forward.class) + .build(); } - public static JsonMapper getPrettyPublisherInstance() { - Jackson3Mapper prettyPublisher = new Jackson3Mapper("pretty_publisher"); - prettyPublisher.setConfig(prettyPublisher.getSerializationConfig().withView(Views.ForwardPublisher.class)); - prettyPublisher.setConfig(prettyPublisher.getDeserializationConfig().withView(Views.Forward.class)); - prettyPublisher.enable(SerializationFeature.INDENT_OUTPUT); - return prettyPublisher; + private static Jackson3Mapper getPrettyPublisherInstance() { + return Jackson3Mapper.builder("pretty_publisher") + .serializationView(Views.ForwardPublisher.class) + .deserializationView(Views.Forward.class) + .configure(b -> b.enable(INDENT_OUTPUT)) + .build(); } - public static JsonMapper getBackwardsPublisherInstance() { - Jackson3Mapper backwardsPublisher = new Jackson3Mapper("backwards_publisher"); - backwardsPublisher.setConfig(backwardsPublisher.getSerializationConfig().withView(Views.Publisher.class)); - backwardsPublisher.setConfig(backwardsPublisher.getDeserializationConfig().withView(Views.Normal.class)); - backwardsPublisher.enable(SerializationFeature.INDENT_OUTPUT); - return backwardsPublisher; + public static Jackson3Mapper getBackwardsPublisherInstance() { + return Jackson3Mapper.builder("backwards_publisher") + .serializationView(Views.Publisher.class) + .deserializationView(Views.Normal.class) + .configure(b -> b.enable(INDENT_OUTPUT)) + .build(); } @Beta - public static JsonMapper getModelInstance() { - Jackson3Mapper model = new Jackson3Mapper("model"); - model.setConfig(model.getSerializationConfig().withView(Views.Model.class)); - return model; + public static Jackson3Mapper getModelInstance() { + return Jackson3Mapper.builder("model") + .serializationView(Views.Model.class) + .build(); } @Beta - public static JsonMapper getModelAndNormalInstance() { - Jackson3Mapper modalAndNormal = new Jackson3Mapper("model_and_normal"); - modalAndNormal.setConfig(modalAndNormal.getSerializationConfig().withView(Views.ModelAndNormal.class)); - return modalAndNormal; + public static Jackson3Mapper getModelAndNormalInstance() { + return Jackson3Mapper.builder("model_and_normal") + .serializationView(Views.ModelAndNormal.class) + .build(); + } - public static JsonMapper getThreadLocal() { + public static Jackson3Mapper getThreadLocal() { return THREAD_LOCAL.get(); } public static void setThreadLocal(Jackson3Mapper set) { @@ -191,78 +180,55 @@ public static void setThreadLocal(Jackson3Mapper set) { @SneakyThrows({JacksonException.class}) public static T lenientTreeToValue(JsonNode jsonNode, Class clazz) { - return getLenientInstance().treeToValue(jsonNode, clazz); + return getLenientInstance().reader().treeToValue(jsonNode, clazz); } + private final JsonMapper mapper; + private final ObjectWriter writer; + private final ObjectReader reader; private final String toString; - private Jackson3Mapper(String toString, Predicate predicate) { - configureMapper(this, predicate); + @lombok.Builder( + builderMethodName = "_builder", + buildMethodName = "_build", + access = AccessLevel.PRIVATE) + private Jackson3Mapper( + String toString, + JsonMapper mapper, + Class serializationView, + Class deserializationView) { + this.mapper = mapper; + this.writer = mapper.writerWithView(serializationView == null ? Views.Normal.class : serializationView); + this.reader = mapper.readerWithView(deserializationView == null ? Views.Normal.class : deserializationView); this.toString = toString; } - private Jackson3Mapper(String toString) { - configureMapper(this); - this.toString = toString; - } - - - @SafeVarargs - public static Jackson3Mapper create(String toString, Predicate module, Consumer... consumer) { - Jackson3Mapper result = new Jackson3Mapper(toString, module); - for (Consumer c : consumer){ - c.accept(result); - } - return result; - } - - public static void configureMapper(ObjectMapper mapper) { + public static void configureMapper(Jackson3Mapper.Builder mapper) { configureMapper(mapper, m -> true); } - public static void configureMapper(ObjectMapper mapper, Predicate filter) { - mapper.setFilterProvider(FILTER_PROVIDER); + public static void configureMapper(Jackson3Mapper.Builder builder, Predicate filter) { - AnnotationIntrospector introspector = new AnnotationIntrospectorPair( - new JacksonAnnotationIntrospector(), - new JakartaXmlBindAnnotationIntrospector(mapper.getTypeFactory()) - ); + builder.mapperBuilder.filterProvider(FILTER_PROVIDER); - mapper.setDefaultPropertyInclusion(JsonInclude.Include.NON_EMPTY); - mapper.setAnnotationIntrospector(introspector); + AnnotationIntrospector introspector = new AnnotationIntrospectorPair( + new JacksonAnnotationIntrospector(), + new JakartaXmlBindAnnotationIntrospector(false) + ); - mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); // This seems a good idea when reading from couchdb or so, but when reading user supplied forms, it is confusing not getting errors. + builder.mapperBuilder.changeDefaultPropertyInclusion(v -> v.withContentInclusion(JsonInclude.Include.NON_EMPTY)); + builder.mapperBuilder.annotationIntrospector(introspector); - mapper.enable(JsonParser.Feature.ALLOW_SINGLE_QUOTES); - mapper.enable(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES); - mapper.enable(MapperFeature.USE_WRAPPER_NAME_AS_PROPERTY_NAME); + builder.mapperBuilder.disable(FAIL_ON_UNKNOWN_PROPERTIES); // This seems a good idea when reading from couchdb or so, but when reading user supplied forms, it is confusing not getting errors. - try { - // this should nbe needed, but if I don't do this, resteasy still doesn't allow comments - mapper.enable(JsonParser.Feature.ALLOW_COMMENTS); - - mapper.setConfig(mapper.getDeserializationConfig().with(JsonReadFeature.ALLOW_LEADING_ZEROS_FOR_NUMBERS)); - mapper.setConfig(mapper.getDeserializationConfig().with(JsonReadFeature.ALLOW_JAVA_COMMENTS)); + builder.mapperBuilder.enable(ALLOW_SINGLE_QUOTES); + builder.mapperBuilder.enable(ALLOW_UNQUOTED_PROPERTY_NAMES); + builder.mapperBuilder.enable(USE_WRAPPER_NAME_AS_PROPERTY_NAME); + builder.mapperBuilder.enable(ALLOW_JAVA_COMMENTS); + builder.mapperBuilder.enable(ALLOW_LEADING_ZEROS_FOR_NUMBERS); - } catch (NoClassDefFoundError noClassDefFoundError) { - log.atLevel( loggedAboutFallback ? Level.DEBUG : Level.WARN).log( noClassDefFoundError.getMessage() + " temporary falling back. Please upgrade jackson"); - loggedAboutFallback = true; - - mapper.enable(JsonParser.Feature.ALLOW_COMMENTS); - //noinspection deprecation - mapper.enable(JsonParser.Feature.ALLOW_NUMERIC_LEADING_ZEROS); - - } - - register(mapper, filter, new JavaTimeModule()); - register(mapper, filter, new DateModule()); - // For example normal support for Optional. - Jdk8Module jdk8Module = new Jdk8Module(); - // jdk8Module.configureAbsentsAsNulls(true); This I think it covered by com.fasterxml.jackson.annotation.JsonInclude.Include.NON_ABSENT - register(mapper, filter, jdk8Module); - - mapper.setConfig(mapper.serializationConfig().withView(Views.Normal.class)); - mapper.setConfig(mapper.serializationConfig().withView(Views.Normal.class)); + //register(builder.mapperBuilder, filter, new JavaTimeModule()); + register(builder, filter, new DateModule()); //SimpleModule module = new SimpleModule(); @@ -271,7 +237,7 @@ public static void configureMapper(ObjectMapper mapper, Predicate try { Class avro = Class.forName("nl.vpro.jackson3.SerializeAvroModule"); - register(mapper, filter, (tools.jackson.databind.Module) avro.getDeclaredConstructor().newInstance()); + register(builder, filter, (tools.jackson.databind.JacksonModule) avro.getDeclaredConstructor().newInstance()); } catch (ClassNotFoundException ncdfe) { if (! loggedAboutAvro) { log.debug("SerializeAvroModule could not be registered because: " + ncdfe.getClass().getName() + " " + ncdfe.getMessage()); @@ -284,7 +250,7 @@ public static void configureMapper(ObjectMapper mapper, Predicate try { Class guava = Class.forName("nl.vpro.jackson3.GuavaRangeModule"); - register(mapper, filter, (com.fasterxml.jackson.databind.Module) guava.getDeclaredConstructor().newInstance()); + register(builder, filter, (tools.jackson.databind.JacksonModule) guava.getDeclaredConstructor().newInstance()); } catch (ClassNotFoundException ncdfe) { log.debug(ncdfe.getMessage()); } catch (IllegalAccessException | InstantiationException | InvocationTargetException | NoSuchMethodException e) { @@ -297,9 +263,9 @@ public static void addFilter(String key, PropertyFilter filter) { log.info("Installed filter {} -> {}", key, filter); } - private static void register(ObjectMapper mapper, Predicate predicate, JacksonModule module) { + private static void register(Jackson3Mapper.Builder mapper, Predicate predicate, JacksonModule module) { if (predicate.test(module)) { - mapper.registerModule(module); + mapper.mapperBuilder.addModule(module); } } @@ -333,12 +299,57 @@ public HttpResponse.BodySubscriber apply(HttpResponse.ResponseInfo responseIn if (simple.isEnabled(level)) { body = new LoggingInputStream(simple, body, level); } - return readValue(body, type); + return mapper.readValue(body, type); }); } }; } + + public JsonMapper mapper() { + return mapper; + } + + public ObjectWriter writer() { + return writer; + } + + public ObjectReader reader() { + return reader; + } + public ObjectReader readerFor(Class clazz) { + return reader.forType(clazz); + } + + public static Builder builder(String toString) { + return _builder().toString(toString); + } + + public static class Builder { + private final JsonMapper.Builder mapperBuilder = JsonMapper + .builder(); + { + configureMapper(this); + } + + public Jackson3Mapper.Builder forward() { + return serializationView(Views.Forward.class) + .deserializationView(Views.Forward.class); + } + + + public Jackson3Mapper build() { + mapper(mapperBuilder.build()); + return _build(); + } + + public Builder configure(Consumer consumer) { + consumer.accept(mapperBuilder); + return this; + } + + + } } diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/JsonArrayIterator.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/JsonArrayIterator.java index 0169b9bf7..bbd6ec473 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/JsonArrayIterator.java +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/JsonArrayIterator.java @@ -3,7 +3,7 @@ import lombok.*; import lombok.extern.slf4j.Slf4j; import tools.jackson.core.*; -import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectReader; import tools.jackson.databind.node.NullNode; import java.io.*; @@ -28,7 +28,7 @@ public class JsonArrayIterator extends UnmodifiableIterator implements CloseableIterator, PeekingIterator, CountedIterator { - private final ObjectMapper mapper; + private final ObjectReader reader; private final JsonParser jp; @@ -38,7 +38,7 @@ public class JsonArrayIterator extends UnmodifiableIterator private Boolean hasNext; - private final BiFunction valueCreator; + private final BiFunction valueCreator; @Getter @Setter @@ -69,7 +69,7 @@ public JsonArrayIterator(InputStream inputStream, final Class clazz, Runnable this(inputStream, null, clazz, callback, null, null, null, null, null, null); } - public JsonArrayIterator(InputStream inputStream, final BiFunction valueCreator) throws IOException { + public JsonArrayIterator(InputStream inputStream, final BiFunction valueCreator) throws IOException { this(inputStream, valueCreator, null, null, null, null, null, null, null, null); } @@ -89,7 +89,7 @@ public static class Builder { * @param sizeField The size of the iterator, i.e. the size of the array represented in the json stream * @param totalSizeField Sometimes the array is part of something bigger, e.g. a page in a search result. The size * of the 'complete' result can be in the beginning of the json in this field. - * @param objectMapper Default the objectMapper {@link Jackson3Mapper#getLenientInstance()} will be used (in + * @param objectMapper Default the objectMapper {@link Jackson3Mapper#LENIENT} will be used (in * conjunction with valueClass, but you may specify another one * @param logger Default this is logging to nl.vpro.jackson2.JsonArrayIterator, but you may override that. * @param skipNulls Whether to skip nulls in the array. Default true. @@ -100,12 +100,12 @@ public static class Builder { @lombok.Builder(builderClassName = "Builder", builderMethodName = "builder") private JsonArrayIterator( @NonNull InputStream inputStream, - @Nullable final BiFunction valueCreator, + @Nullable final BiFunction valueCreator, @Nullable final Class valueClass, @Nullable Runnable callback, @Nullable String sizeField, @Nullable String totalSizeField, - @Nullable ObjectMapper objectMapper, + @Nullable Jackson3Mapper objectMapper, @Nullable Logger logger, @Nullable Boolean skipNulls, @Nullable Listener eventListener @@ -113,8 +113,8 @@ private JsonArrayIterator( if (inputStream == null) { throw new IllegalArgumentException("No inputStream given"); } - this.mapper = objectMapper == null ? Jackson3Mapper.getLenientInstance() : objectMapper; - this.jp = this.mapper.createParser(inputStream); + this.reader = objectMapper == null ? Jackson3Mapper.LENIENT.reader() : objectMapper.reader(); + this.jp = this.reader.createParser(inputStream); this.valueCreator = valueCreator == null ? valueCreator(valueClass) : valueCreator; if (valueCreator != null && valueClass != null) { throw new IllegalArgumentException(); @@ -165,7 +165,7 @@ private JsonArrayIterator( this.skipNulls = skipNulls == null || skipNulls; } - private static BiFunction valueCreator(Class clazz) { + private static BiFunction valueCreator(Class clazz) { return (m, tree) -> { try { return m.treeToValue(tree, clazz); @@ -235,7 +235,7 @@ protected void findNext() { logger.warn("Found {} nulls. Will be skipped", foundNulls); } - next = valueCreator.apply(mapper, tree); + next = valueCreator.apply(reader, tree); eventListener.accept(new NextEvent(next)); hasNext = true; } @@ -305,7 +305,7 @@ public static void write( final CountedIterator iterator, final OutputStream out, final Function logging) throws IOException { - try (JsonGenerator jg = Jackson3Mapper.getInstance().createGenerator(out)) { + try (JsonGenerator jg = Jackson3Mapper.INSTANCE.mapper().createGenerator(out)) { jg.writeStartObject(); jg.writeArrayPropertyStart("array"); writeObjects(iterator, jg, logging); @@ -321,7 +321,7 @@ public static void write( public static void writeArray( final CountedIterator iterator, final OutputStream out, final Function logging) throws IOException { - try (JsonGenerator jg = Jackson3Mapper.getInstance().createGenerator(out)) { + try (JsonGenerator jg = Jackson3Mapper.INSTANCE.mapper().createGenerator(out)) { jg.writeStartArray(); writeObjects(iterator, jg, logging); jg.writeEndArray(); diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/Utils.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/Utils.java index 22b79ae17..8e675174f 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/Utils.java +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/Utils.java @@ -20,7 +20,7 @@ public class Utils { @SuppressWarnings("unchecked") public static MapDifference flattenedDifference( JsonNode j1, JsonNode j2) { - ObjectMapper mapper = Jackson3Mapper.getPublisherInstance(); + ObjectMapper mapper = Jackson3Mapper.PUBLISHER.mapper(); Map map1 = mapper.convertValue(j1, Map.class); Map flatten1= flatten(map1); Map map2 = mapper.convertValue(j2, Map.class); diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/ZonedDateTimeToJsonTimestamp.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/ZonedDateTimeToJsonTimestamp.java index 6172050e5..2e9ad1920 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/ZonedDateTimeToJsonTimestamp.java +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/ZonedDateTimeToJsonTimestamp.java @@ -30,6 +30,7 @@ public void serialize(ZonedDateTime value, JsonGenerator jgen, SerializationCont public static class Deserializer extends ValueDeserializer { public static final Deserializer INSTANCE = new Deserializer(); + @Override public ZonedDateTime deserialize(JsonParser jp, DeserializationContext ctxt) throws JacksonException { try { diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/rs/JacksonContextResolver.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/rs/JacksonContextResolver.java index 2a0b55481..69d0167fc 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/rs/JacksonContextResolver.java +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/rs/JacksonContextResolver.java @@ -1,6 +1,5 @@ package nl.vpro.jackson3.rs; -import tools.jackson.databind.ObjectMapper; import tools.jackson.databind.json.JsonMapper; import tools.jackson.jakarta.rs.json.JacksonXmlBindJsonProvider; @@ -30,28 +29,28 @@ public class JacksonContextResolver extends JacksonXmlBindJsonProvider implement static final int PRIORITY = Priorities.USER; - private final ThreadLocal mapper; + private final ThreadLocal mapper; public JacksonContextResolver() { - this(Jackson3Mapper.getLenientInstance()); + this(Jackson3Mapper.LENIENT); } - public JacksonContextResolver(JsonMapper mapper) { + public JacksonContextResolver(Jackson3Mapper mapper) { this(() -> mapper); } - public JacksonContextResolver(Supplier mapper) { + public JacksonContextResolver(Supplier mapper) { this.mapper = ThreadLocal.withInitial(mapper); } @Override - public ObjectMapper getContext(Class objectType) { - return mapper.get(); + public JsonMapper getContext(Class objectType) { + return mapper.get().mapper(); } /** * @since 4.0 */ - public void set(JsonMapper mapper) { + public void set(Jackson3Mapper mapper) { this.mapper.set(mapper); } diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/rs/JsonIdAdderBodyReader.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/rs/JsonIdAdderBodyReader.java index 2c8ed4264..fbfacf9f2 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/rs/JsonIdAdderBodyReader.java +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/rs/JsonIdAdderBodyReader.java @@ -1,6 +1,11 @@ package nl.vpro.jackson3.rs; import lombok.extern.slf4j.Slf4j; +import tools.jackson.databind.*; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.databind.jsontype.TypeDeserializer; +import tools.jackson.databind.jsontype.TypeIdResolver; +import tools.jackson.databind.node.ObjectNode; import java.io.InputStream; import java.lang.annotation.Annotation; @@ -14,16 +19,10 @@ import org.checkerframework.checker.nullness.qual.NonNull; -import tools.jackson.databind.*; -import tools.jackson.databind.json.JsonMapper; -import tools.jackson.databind.jsontype.TypeDeserializer; -import tools.jackson.databind.jsontype.TypeIdResolver; -import tools.jackson.databind.node.ObjectNode; - import nl.vpro.jackson3.Jackson3Mapper; /** - * Sometimes jackson/resteasy will not unmarshal a json because there is no type information, but the prototype actually specifies it fully. This message body reader will deal with that (using {@link TypeIdResolver#idFromBaseType()}, by adding the id implicitly (if it is missing) before the actual unmarshal. + * Sometimes jackson/resteasy will not unmarshal a json because there is no type information, but the prototype actually specifies it fully. This message body reader will deal with that (using {@link TypeIdResolver#idFromBaseType(tools.jackson.databind.DatabindContext)}, by adding the id implicitly (if it is missing) before the actual unmarshal. * * @author Michiel Meeuwissen * @since 2.7 @@ -50,20 +49,30 @@ public Object readFrom( MediaType mediaType, MultivaluedMap httpHeaders, InputStream entityStream) throws WebApplicationException { + final ObjectReader reader; + JsonMapper mapper = providers == null ? null : providers.getContextResolver(JsonMapper.class, MediaType.APPLICATION_JSON_TYPE).getContext(type); if (mapper == null) { + mapper = Jackson3Mapper.LENIENT.mapper(); + reader = mapper.reader(); log.info("No mapper found in {}", providers); - mapper = Jackson3Mapper.getLenientInstance(); + } else { + reader = mapper.readerFor(type); } - final JavaType javaType = mapper.getTypeFactory().constructType(genericType); - final JsonNode jsonNode = mapper.readTree(entityStream); + + final JavaType javaType = reader.typeFactory().constructType(genericType); + final JsonNode jsonNode = reader.readTree(entityStream); if (jsonNode instanceof ObjectNode objectNode) { - final TypeDeserializer typeDeserializer = mapper.deserializationConfig() + + ObjectReader objectReader = reader.forType(javaType); + objectReader.readValue(objectNode); // just to ensure t + final TypeDeserializer typeDeserializer = mapper + .deserializationConfig() .getTypeResolverProvider() - .findTypeDeserializer(mapper.d, javaType); + .findTypeDeserializer(null, javaType, null); if (typeDeserializer != null) { final String propertyName = typeDeserializer.getPropertyName(); - final String propertyValue = typeDeserializer.getTypeIdResolver().idFromBaseType(); + final String propertyValue = typeDeserializer.getTypeIdResolver().idFromBaseType(null); if (! objectNode.has(propertyName)) { log.debug("Implicitly setting {} = {} for {}", propertyName, propertyValue, javaType); objectNode.put(propertyName, propertyValue); diff --git a/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/GuavaRangeModuleTest.java b/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/GuavaRangeModuleTest.java index 10a23abe8..b5681ede8 100644 --- a/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/GuavaRangeModuleTest.java +++ b/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/GuavaRangeModuleTest.java @@ -16,12 +16,9 @@ class GuavaRangeModuleTest { JsonMapper mapper = JsonMapper.builder() - .withModules(new DateModule()); - { - mapper.registerModule(new DateModule()); - mapper.registerModule(new GuavaRangeModule()); - } - + .addModule(new DateModule()) + .addModule(new GuavaRangeModule()) + .build(); static class WithoutSerializer { @JsonProperty diff --git a/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/IterableJsonTest.java b/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/IterableJsonTest.java index 919cdfce2..06c4badc6 100644 --- a/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/IterableJsonTest.java +++ b/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/IterableJsonTest.java @@ -169,13 +169,13 @@ public void testGetValueJsonSingleValue() throws Exception { @SuppressWarnings("unchecked") static T roundTripAndSimilar(T input, String expected) throws Exception { StringWriter writer = new StringWriter(); - Jackson3Mapper.getInstance().writeValue(writer, input); + Jackson3Mapper.INSTANCE.writer().writeValue(writer, input); String text = writer.toString(); JSONAssert.assertEquals("\n" + text + "\nis different from expected\n" + expected, expected, text, JSONCompareMode.LENIENT); - return (T) Jackson3Mapper.getInstance().readValue(text, input.getClass()); + return (T) Jackson3Mapper.INSTANCE.readerFor(input.getClass()).readValue(text); } diff --git a/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/Jackson3MapperTest.java b/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/Jackson3MapperTest.java index 82cfb0c76..671f2f29f 100644 --- a/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/Jackson3MapperTest.java +++ b/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/Jackson3MapperTest.java @@ -4,7 +4,6 @@ import tools.jackson.core.JacksonException; import tools.jackson.databind.exc.InvalidFormatException; -import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Optional; @@ -37,39 +36,38 @@ public static class A { } @Test - public void read() throws IOException { + public void read() { String example = "/* leading comments */\n{'integer': 2 /* ignore comments */, 'optional': 3}"; - A a = Jackson3Mapper.getInstance().readValue(example, A.class); + A a = Jackson3Mapper.INSTANCE.readerFor(A.class).readValue(example); assertThat(a.integer).isEqualTo(2); assertThat(a.optional).isPresent(); assertThat(a.optional.get()).isEqualTo(3); - Jackson3Mapper.getLenientInstance().readerFor(A.class).readValue(example.getBytes(StandardCharsets.UTF_8)); + Jackson3Mapper.LENIENT.readerFor(A.class).readValue(example.getBytes(StandardCharsets.UTF_8)); } @Test - public void readIntFromString() throws IOException { - A a = Jackson3Mapper.getInstance().readValue("{'integer': '2'}", A.class); + public void readIntFromString() { + A a = Jackson3Mapper.INSTANCE.readerFor(A.class).readValue("{'integer': '2'}"); assertThat(a.integer).isEqualTo(2); } @Test - public void readEnumValue() throws IOException { - A a = Jackson3Mapper.getInstance().readValue("{'enumValue': 'a'}", A.class); + public void readEnumValue() { + A a = Jackson3Mapper.INSTANCE.readerFor(A.class).readValue("{'enumValue': 'a'}"); assertThat(a.enumValue).isEqualTo(EnumValues.a); } @Test - public void readUnknownEnumValue() throws IOException { + public void readUnknownEnumValue() { assertThatThrownBy(() -> { - A a = Jackson3Mapper.getInstance().readValue("{'enumValue': 'c'}", A.class); - assertThat(a.enumValue).isNull(); + A a = Jackson3Mapper.INSTANCE.readerFor(A.class).readValue("{'enumValue': 'c'}"); }).isInstanceOf(InvalidFormatException.class); } @Test - public void readUnknownEnumValueLenient() throws IOException { - A a = Jackson3Mapper.getLenientInstance().readValue("{'enumValue': 'c'}", A.class); + public void readUnknownEnumValueLenient() { + A a = Jackson3Mapper.LENIENT.readerFor(A.class).readValue("{'enumValue': 'c'}"); assertThat(a.enumValue).isNull(); } @@ -78,7 +76,7 @@ public void write() throws JacksonException { A a = new A(); a.integer = 2; a.optional = Optional.of(3); - assertThat(Jackson3Mapper.getInstance().writeValueAsString(a)).isEqualTo("{\"integer\":2,\"optional\":3}"); + assertThat(Jackson3Mapper.INSTANCE.writer().writeValueAsString(a)).isEqualTo("{\"integer\":2,\"optional\":3}"); } @Test @@ -86,9 +84,7 @@ public void writeWithEmptyOptional() throws JacksonException { A a = new A(); a.integer = 2; a.optional = Optional.empty(); - assertThat(Jackson3Mapper.getInstance().writeValueAsString(a)).isEqualTo("{\"integer\":2}"); + assertThat(Jackson3Mapper.INSTANCE.writer().writeValueAsString(a)).isEqualTo("{\"integer\":2}"); } - - } diff --git a/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/JsonArrayIteratorTest.java b/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/JsonArrayIteratorTest.java index 6fe9700fd..bcd4ad1b8 100644 --- a/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/JsonArrayIteratorTest.java +++ b/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/JsonArrayIteratorTest.java @@ -29,7 +29,7 @@ public class JsonArrayIteratorTest { public void test() throws IOException { //Jackson2Mapper.getInstance().writeValue(System.out, new Change("bla", false)); - try (JsonArrayIterator it = JsonArrayIterator.builder().inputStream(getClass().getResourceAsStream("/changes.json")).valueClass(Change.class).objectMapper(Jackson3Mapper.getInstance()).build()) { + try (JsonArrayIterator it = JsonArrayIterator.builder().inputStream(getClass().getResourceAsStream("/changes.json")).valueClass(Change.class).objectMapper(Jackson3Mapper.INSTANCE).build()) { assertThat(it.next().getMid()).isEqualTo("POMS_NCRV_1138990"); // 1 assertThat(it.getCount()).isEqualTo(1); assertThat(it.getSize()).hasValueSatisfying(size -> assertThat(size).isEqualTo(14)); @@ -96,7 +96,6 @@ public void testIncompleteJson() throws IOException { } - @SuppressWarnings("FinalizeCalledExplicitly") @Test public void testZeroBytes() throws IOException { @@ -125,7 +124,7 @@ public void write() throws IOException, JSONException { .builder() .inputStream(getClass().getResourceAsStream("/changes.json")) .valueClass(Change.class) - .objectMapper(Jackson3Mapper.getInstance()) + .objectMapper(Jackson3Mapper.INSTANCE) .skipNulls(false) .build(); ByteArrayOutputStream out = new ByteArrayOutputStream()) { @@ -144,15 +143,15 @@ public void writeArray() throws IOException, JSONException { .builder() .inputStream(getClass().getResourceAsStream("/changes.json")) .valueClass(Change.class) - .objectMapper(Jackson3Mapper.getInstance()) + .objectMapper(Jackson3Mapper.INSTANCE) .logger(log) .skipNulls(false) .build(); ByteArrayOutputStream out = new ByteArrayOutputStream()) { - it.writeArray(out, (c) -> { - log.info("{}", c); - }); + it.writeArray(out, (c) -> + log.info("{}", c) + ); String expected = IOUtils.resourceToString("/array_from_changes.json", StandardCharsets.UTF_8); JSONAssert.assertEquals(expected, out.toString(), JSONCompareMode.STRICT); } @@ -193,15 +192,15 @@ public int read() throws IOException { throw new InterruptedIOException(); } if (i < bytes.length) { - return bytes[i++]; + return (int) bytes[i++]; } else { return -1; } } }) - .callback(() -> { - callback[0] = "called"; - }) + .callback(() -> + callback[0] = "called" + ) .valueClass(Simple.class) .build()) { log.info("{}", i.next()); diff --git a/vpro-shared-util/src/main/java/nl/vpro/util/BindingUtils.java b/vpro-shared-util/src/main/java/nl/vpro/util/BindingUtils.java index ce8a75d3d..d9d3eb802 100644 --- a/vpro-shared-util/src/main/java/nl/vpro/util/BindingUtils.java +++ b/vpro-shared-util/src/main/java/nl/vpro/util/BindingUtils.java @@ -9,7 +9,6 @@ public class BindingUtils { private BindingUtils() { - } public static final ZoneId DEFAULT_ZONE = ZoneId.of("Europe/Amsterdam"); diff --git a/vpro-shared-util/src/main/java/nl/vpro/util/CountedPeekingIteratorImpl.java b/vpro-shared-util/src/main/java/nl/vpro/util/CountedPeekingIteratorImpl.java index ea4a3db2b..73fcf976f 100644 --- a/vpro-shared-util/src/main/java/nl/vpro/util/CountedPeekingIteratorImpl.java +++ b/vpro-shared-util/src/main/java/nl/vpro/util/CountedPeekingIteratorImpl.java @@ -37,11 +37,6 @@ public Optional getTotalSize() { return wrapped.getTotalSize(); } - @Override - public void close() throws Exception { - iterator.close(); - } - @Override public CountedPeekingIterator peeking() { return this; diff --git a/vpro-shared-util/src/main/java/nl/vpro/util/FileCachingInputStream.java b/vpro-shared-util/src/main/java/nl/vpro/util/FileCachingInputStream.java index 79bcd5634..0dac191fc 100644 --- a/vpro-shared-util/src/main/java/nl/vpro/util/FileCachingInputStream.java +++ b/vpro-shared-util/src/main/java/nl/vpro/util/FileCachingInputStream.java @@ -7,8 +7,7 @@ import java.net.URI; import java.nio.file.*; import java.time.Duration; -import java.util.Arrays; -import java.util.Optional; +import java.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; @@ -202,12 +201,8 @@ private Copier createToFileCopier( final ExecutorService executorService ) throws ExecutionException, InterruptedException { - final boolean effectiveProgressLogging; - if (progressLogging == null) { - effectiveProgressLogging = ! this.deleteTempFile; - } else { - effectiveProgressLogging = progressLogging; - } + final boolean effectiveProgressLogging = Objects.requireNonNullElseGet(progressLogging, () -> !this.deleteTempFile); + return Copier.builder() .input(input) .expectedCount(expectedCount) diff --git a/vpro-shared-util/src/main/java/nl/vpro/util/FileInputStreamTee.java b/vpro-shared-util/src/main/java/nl/vpro/util/FileInputStreamTee.java index c008a2323..046700e4a 100644 --- a/vpro-shared-util/src/main/java/nl/vpro/util/FileInputStreamTee.java +++ b/vpro-shared-util/src/main/java/nl/vpro/util/FileInputStreamTee.java @@ -23,7 +23,6 @@ public FileInputStreamTee(OutputStream fileOutputStream, InputStream wrapped) { @Override void write(byte[] buffer, int offset, int effectiveLength) throws IOException { fileOutputStream.write(buffer, offset, effectiveLength); - } @Override diff --git a/vpro-shared-util/src/main/java/nl/vpro/util/FileSizeFormatter.java b/vpro-shared-util/src/main/java/nl/vpro/util/FileSizeFormatter.java index 06fb273ac..f626e91a3 100644 --- a/vpro-shared-util/src/main/java/nl/vpro/util/FileSizeFormatter.java +++ b/vpro-shared-util/src/main/java/nl/vpro/util/FileSizeFormatter.java @@ -91,7 +91,7 @@ public String formatSpeed(@Nullable Number numberOfBytes, Duration duration) { return format(null, false) + "/s"; } if (duration.isZero()) { - return "\u221E B/s"; + return "∞ B/s"; } Float perSecond = 1000f * numberOfBytes.floatValue() / duration.toMillis(); return format(perSecond, false) + "/s"; diff --git a/vpro-shared-util/src/main/java/nl/vpro/util/HTMLStripper.java b/vpro-shared-util/src/main/java/nl/vpro/util/HTMLStripper.java index 69cc36511..75db71f58 100644 --- a/vpro-shared-util/src/main/java/nl/vpro/util/HTMLStripper.java +++ b/vpro-shared-util/src/main/java/nl/vpro/util/HTMLStripper.java @@ -10,7 +10,7 @@ import javax.swing.text.html.HTMLEditorKit; /** - * Swing contains a nice html parser, which can be used to clean existing html. + * Swing contains a nice HTML parser, which can be used to clean existing HTML. * */ public class HTMLStripper { From a8e6b655006776d7cd75b85924129299de7595c4 Mon Sep 17 00:00:00 2001 From: Michiel Meeuwissen Date: Wed, 7 Jan 2026 16:21:44 +0100 Subject: [PATCH 07/18] Made it compile. --- vpro-shared-i18n/pom.xml | 5 ++ .../java/nl/vpro/jackson3/Jackson3Mapper.java | 13 ++++- .../nl/vpro/swagger/OpenAPIApplication.java | 2 +- .../test/util/jackson3/Jackson3TestUtil.java | 49 +++++++++---------- 4 files changed, 40 insertions(+), 29 deletions(-) diff --git a/vpro-shared-i18n/pom.xml b/vpro-shared-i18n/pom.xml index e57ed9b2e..50a6ab86f 100644 --- a/vpro-shared-i18n/pom.xml +++ b/vpro-shared-i18n/pom.xml @@ -55,5 +55,10 @@ swagger-annotations-jakarta provided + + nl.vpro.shared + vpro-shared-logging + test + diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/Jackson3Mapper.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/Jackson3Mapper.java index 284cdcc35..ce8a78e26 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/Jackson3Mapper.java +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/Jackson3Mapper.java @@ -321,17 +321,28 @@ public ObjectReader readerFor(Class clazz) { return reader.forType(clazz); } + public Builder rebuild() { + Builder builder = builder(toString); + builder.serializationView(writer.getConfig().getActiveView()); + builder.deserializationView(reader.getConfig().getActiveView()); + builder.mapperBuilder = mapper.rebuild(); + return builder; + } + public static Builder builder(String toString) { return _builder().toString(toString); } public static class Builder { - private final JsonMapper.Builder mapperBuilder = JsonMapper + private JsonMapper.Builder mapperBuilder = JsonMapper .builder(); { configureMapper(this); } + + + public Jackson3Mapper.Builder forward() { return serializationView(Views.Forward.class) .deserializationView(Views.Forward.class); diff --git a/vpro-shared-swagger3/src/main/java/nl/vpro/swagger/OpenAPIApplication.java b/vpro-shared-swagger3/src/main/java/nl/vpro/swagger/OpenAPIApplication.java index da4762773..2a00d0ada 100644 --- a/vpro-shared-swagger3/src/main/java/nl/vpro/swagger/OpenAPIApplication.java +++ b/vpro-shared-swagger3/src/main/java/nl/vpro/swagger/OpenAPIApplication.java @@ -36,8 +36,8 @@ import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.fasterxml.jackson.module.jakarta.xmlbind.JakartaXmlBindAnnotationModule; +import nl.vpro.jackson.Views; import nl.vpro.jackson2.Jackson2Mapper; -import nl.vpro.jackson2.Views; import nl.vpro.rs.ResteasyApplication; import nl.vpro.swagger.model.*; import nl.vpro.util.ThreadPools; diff --git a/vpro-shared-test/src/main/java/nl/vpro/test/util/jackson3/Jackson3TestUtil.java b/vpro-shared-test/src/main/java/nl/vpro/test/util/jackson3/Jackson3TestUtil.java index de75bf554..4db1fde74 100644 --- a/vpro-shared-test/src/main/java/nl/vpro/test/util/jackson3/Jackson3TestUtil.java +++ b/vpro-shared-test/src/main/java/nl/vpro/test/util/jackson3/Jackson3TestUtil.java @@ -6,6 +6,11 @@ import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; +import tools.jackson.core.JsonPointer; +import tools.jackson.core.StreamReadFeature; +import tools.jackson.databind.*; +import tools.jackson.databind.node.ArrayNode; +import tools.jackson.databind.node.ObjectNode; import java.io.*; import java.nio.charset.StandardCharsets; @@ -20,13 +25,7 @@ import org.skyscreamer.jsonassert.JSONAssert; import org.skyscreamer.jsonassert.JSONCompareMode; -import tools.jackson.core.JsonPointer; -import tools.jackson.core.StreamReadFeature; -import com.fasterxml.jackson.databind.*; -import com.fasterxml.jackson.databind.node.ArrayNode; -import tools.jackson.databind.node.ObjectNode; - -import nl.vpro.jackson2.Jackson2Mapper; +import nl.vpro.jackson3.Jackson3Mapper; import nl.vpro.test.util.TestClass; import static org.assertj.core.api.Assertions.assertThat; @@ -40,18 +39,24 @@ @Slf4j public class Jackson3TestUtil { - private static final ObjectMapper MAPPER = - Jackson2Mapper.getPrettyStrictInstance(); + private static final Jackson3Mapper JACKSON_3_MAPPER = + Jackson3Mapper.PRETTY + .rebuild() + .configure(b -> + b.enable(StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION) + ) + .build(); - private static final ObjectReader JSON_READER = MAPPER.readerFor(JsonNode.class).with(StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION); + private static final ObjectReader READER = JACKSON_3_MAPPER.reader(); + private static final ObjectMapper MAPPER = JACKSON_3_MAPPER.mapper(); public static void assertJsonEquals(String pref, CharSequence expected, CharSequence actual, JsonPointer... ignores) { try { if (ignores.length > 0) { - JsonNode actualJson = MAPPER.readTree(actual.toString()); + JsonNode actualJson = READER.readTree(actual.toString()); remove(actualJson, ignores); - actual = MAPPER.writeValueAsString(actualJson); + actual = MAPPER.writer().writeValueAsString(actualJson); } JSONAssert.assertEquals(pref + "\n" + actual + "\nis different from expected\n" + expected, String.valueOf(expected), String.valueOf(actual), JSONCompareMode.STRICT); @@ -97,13 +102,8 @@ public static String prettify(@PolyNull CharSequence test) { if (test == null) { return null; } - try { - JsonNode jsonNode = JSON_READER.readTree(String.valueOf(test)); - String pretty = MAPPER.writeValueAsString(jsonNode); - return pretty; - } catch (IOException e) { - throw new RuntimeException(e); - } + JsonNode jsonNode = READER.readTree(String.valueOf(test)); + return MAPPER.writeValueAsString(jsonNode); } @@ -201,7 +201,7 @@ public static T roundTripAndSimilarAndEquals(ObjectMapper mapper, T input, S public static T assertJsonEquals(String actual, String expected, Class typeReference) throws IOException { assertJsonEquals("", expected, actual); - return objectReader(MAPPER, typeReference).readValue(actual, typeReference); + return objectReader(MAPPER, typeReference).readValue(actual); } @@ -256,7 +256,7 @@ protected static T roundTripAndSimilar(ObjectMapper mapper, T input, JsonNod */ public static T roundTripAndSimilarValue(T input, String expected) { TestClass embed = new TestClass<>(input); - JavaType type = Jackson2Mapper.getInstance().getTypeFactory() + JavaType type = Jackson3Mapper.INSTANCE.mapper().getTypeFactory() .constructParametricType(TestClass.class, input.getClass()); TestClass result = roundTripAndSimilar(embed, "{\"value\": " + expected + "}", type, true); @@ -334,12 +334,7 @@ public JsonObjectAssert ignore(String... jsonPointers) { protected static A read(ObjectMapper mapper, Class actual, String string) { - try { - return mapper.readValue(string, actual); - } catch (IOException e) { - Fail.fail(e.getMessage(), e); - return null; - } + return mapper.readValue(string, actual); } @SuppressWarnings({"CatchMayIgnoreException"}) From a54ca621332c54b0d46a7341f40968c0daccd057 Mon Sep 17 00:00:00 2001 From: Michiel Meeuwissen Date: Wed, 7 Jan 2026 20:09:06 +0100 Subject: [PATCH 08/18] Try with snapshot. --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 56a213a11..99f324d19 100644 --- a/pom.xml +++ b/pom.xml @@ -749,7 +749,7 @@ org.meeuw.i18n i18n-iso-639 - 3.12-SNAPSHOT + 4.0-SNAPSHOT io.micrometer From 6185c278929058820ff0d39c1aef9aca05b36d8f Mon Sep 17 00:00:00 2001 From: Michiel Meeuwissen Date: Wed, 7 Jan 2026 20:22:51 +0100 Subject: [PATCH 09/18] Made it compile. --- vpro-shared-i18n/src/main/java/nl/vpro/i18n/Locales.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/vpro-shared-i18n/src/main/java/nl/vpro/i18n/Locales.java b/vpro-shared-i18n/src/main/java/nl/vpro/i18n/Locales.java index a922ee56d..535ce6de0 100644 --- a/vpro-shared-i18n/src/main/java/nl/vpro/i18n/Locales.java +++ b/vpro-shared-i18n/src/main/java/nl/vpro/i18n/Locales.java @@ -5,8 +5,7 @@ import org.meeuw.i18n.countries.Country; import org.meeuw.i18n.countries.codes.CountryCode; -import org.meeuw.i18n.languages.ISO_639; -import org.meeuw.i18n.languages.ISO_639_Code; +import org.meeuw.i18n.languages.*; import org.meeuw.i18n.regions.Region; import static org.meeuw.i18n.countries.codes.CountryCode.*; @@ -54,9 +53,9 @@ private Locales() { public static final Locale FLEMISH = of(nl, BE); /** - * The locale representing the 'undetermined' language {@link ISO_639#UND} + * The locale representing the 'undetermined' language {@link LanguageCode#UND} */ - public static final Locale UNDETERMINED = of(ISO_639.UND); + public static final Locale UNDETERMINED = of(LanguageCode.UND); private static final ThreadLocal DEFAULT = ThreadLocal.withInitial(Locale::getDefault); From f4f6b43a6d2bb9224f708be57368f82fa4bfdd32 Mon Sep 17 00:00:00 2001 From: Michiel Meeuwissen Date: Wed, 7 Jan 2026 20:30:35 +0100 Subject: [PATCH 10/18] Small new feature, that makes log4j2-test superflous. --- .../logging/log4j2/CaptureListFromLogger.java | 33 +++++++++++++ .../log4j2/CaptureListFromLoggerTest.java | 46 +++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 vpro-shared-logging/src/main/java/nl/vpro/logging/log4j2/CaptureListFromLogger.java create mode 100644 vpro-shared-logging/src/test/java/nl/vpro/logging/log4j2/CaptureListFromLoggerTest.java diff --git a/vpro-shared-logging/src/main/java/nl/vpro/logging/log4j2/CaptureListFromLogger.java b/vpro-shared-logging/src/main/java/nl/vpro/logging/log4j2/CaptureListFromLogger.java new file mode 100644 index 000000000..8c8f92e28 --- /dev/null +++ b/vpro-shared-logging/src/main/java/nl/vpro/logging/log4j2/CaptureListFromLogger.java @@ -0,0 +1,33 @@ +package nl.vpro.logging.log4j2; + +import lombok.Getter; + +import java.util.*; +import java.util.function.Supplier; + +import org.apache.logging.log4j.core.LogEvent; + +/** + * + * @author Michiel Meeuwissen + * @since 5.14 + */ +public class CaptureListFromLogger extends AbstractCaptureLogger implements Supplier> { + + @Getter + private final List events = new ArrayList<>(); + + CaptureListFromLogger(UUID uuid, boolean currentThreadOnly) { + super(uuid, currentThreadOnly); + } + + @Override + public List get() { + return getEvents(); + } + + @Override + protected void accept(LogEvent event) { + events.add(event.toImmutable()); + } +} diff --git a/vpro-shared-logging/src/test/java/nl/vpro/logging/log4j2/CaptureListFromLoggerTest.java b/vpro-shared-logging/src/test/java/nl/vpro/logging/log4j2/CaptureListFromLoggerTest.java new file mode 100644 index 000000000..0173d2361 --- /dev/null +++ b/vpro-shared-logging/src/test/java/nl/vpro/logging/log4j2/CaptureListFromLoggerTest.java @@ -0,0 +1,46 @@ +package nl.vpro.logging.log4j2; + +import lombok.extern.log4j.Log4j2; + +import java.util.*; +import java.util.concurrent.*; + +import org.apache.logging.log4j.Level; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +@Log4j2 + +class CaptureListFromLoggerTest { + ExecutorService service = Executors.newCachedThreadPool(); + + @Test + public void log() { + List> futures = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + final int j = i; + futures.add(service.submit(() -> { + try (CaptureListFromLogger capture = new CaptureListFromLogger(UUID.randomUUID(), true)) { + log.info("foo" + j); + log.info("bar" + j); + + assertThat(capture.getEvents()).hasSize(2); + assertThat(capture.getEvents().get(0).getLevel()).isEqualTo(Level.INFO); + + } + })); + } + futures.forEach(f -> { + try { + f.get(); + } catch (Exception e) { + throw new AssertionError(e); + } + }); + service.shutdown(); + + + } + +} From 0fa93572dddf48bf4bc1f2fc528b915f5833d498 Mon Sep 17 00:00:00 2001 From: Michiel Meeuwissen Date: Fri, 9 Jan 2026 12:24:19 +0100 Subject: [PATCH 11/18] Trying to fix test cases. --- vpro-shared-jackson2/pom.xml | 5 + .../nl/vpro/jackson2/JsonArrayIterator.java | 10 +- .../nl/vpro/jackson2/Jackson2MapperTest.java | 28 + .../vpro/jackson2/JsonArrayIteratorTest.java | 30 +- vpro-shared-jackson3/pom.xml | 30 +- .../java/nl/vpro/jackson3/IterableJson.java | 6 +- .../java/nl/vpro/jackson3/Jackson3Mapper.java | 102 ++- .../nl/vpro/jackson3/JsonArrayIterator.java | 90 +- .../StringInstantToJsonTimestamp.java | 5 +- .../nl/vpro/jackson3/Jackson3MapperTest.java | 69 +- .../vpro/jackson3/JsonArrayIteratorTest.java | 43 +- .../StringInstantToJsonTimestampTest.java | 4 +- .../src/test/resources/changes.json | 780 ++++++++++++++++++ .../src/test/resources/log4j2-test.xml | 14 + .../java/nl/vpro/util/DirectoryWatcher.java | 12 +- 15 files changed, 1133 insertions(+), 95 deletions(-) create mode 100644 vpro-shared-jackson3/src/test/resources/changes.json create mode 100644 vpro-shared-jackson3/src/test/resources/log4j2-test.xml diff --git a/vpro-shared-jackson2/pom.xml b/vpro-shared-jackson2/pom.xml index a640fdb82..28852a1ba 100644 --- a/vpro-shared-jackson2/pom.xml +++ b/vpro-shared-jackson2/pom.xml @@ -56,6 +56,11 @@ jakarta.xml.bind-api true + + io.github.natty-parser + natty + test + jakarta.ws.rs jakarta.ws.rs-api diff --git a/vpro-shared-jackson2/src/main/java/nl/vpro/jackson2/JsonArrayIterator.java b/vpro-shared-jackson2/src/main/java/nl/vpro/jackson2/JsonArrayIterator.java index e468bcfad..81118deb6 100644 --- a/vpro-shared-jackson2/src/main/java/nl/vpro/jackson2/JsonArrayIterator.java +++ b/vpro-shared-jackson2/src/main/java/nl/vpro/jackson2/JsonArrayIterator.java @@ -137,7 +137,7 @@ private JsonArrayIterator( } this.eventListener.accept(new TokenEvent(token)); if (token == JsonToken.FIELD_NAME) { - fieldName = jp.getCurrentName(); + fieldName = jp.currentName(); } if (token == JsonToken.VALUE_NUMBER_INT && sizeField.equals(fieldName)) { tmpSize = jp.getLongValue(); @@ -145,7 +145,7 @@ private JsonArrayIterator( } if (token == JsonToken.VALUE_NUMBER_INT && totalSizeField.equals(fieldName)) { tmpTotalSize = jp.getLongValue(); - this.eventListener.accept(new TotalSizeEvent(tmpSize)); + this.eventListener.accept(new TotalSizeEvent(tmpTotalSize)); } if (token == JsonToken.START_ARRAY) { @@ -211,8 +211,12 @@ protected void findNext() { if(needsFindNext) { while(true) { try { + var lastToken = jp.getLastClearedToken(); + TreeNode tree = jp.readValueAsTree(); - this.eventListener.accept(new TokenEvent(jp.getLastClearedToken())); + var newLastToken = jp.getLastClearedToken(); + assert lastToken != newLastToken; + this.eventListener.accept(new TokenEvent(newLastToken)); if (jp.getLastClearedToken() == JsonToken.END_ARRAY) { tree = null; diff --git a/vpro-shared-jackson2/src/test/java/nl/vpro/jackson2/Jackson2MapperTest.java b/vpro-shared-jackson2/src/test/java/nl/vpro/jackson2/Jackson2MapperTest.java index 254ae9911..6b73b7f95 100644 --- a/vpro-shared-jackson2/src/test/java/nl/vpro/jackson2/Jackson2MapperTest.java +++ b/vpro-shared-jackson2/src/test/java/nl/vpro/jackson2/Jackson2MapperTest.java @@ -12,6 +12,13 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.exc.InvalidFormatException; +import com.fasterxml.jackson.databind.introspect.AnnotationIntrospectorPair; +import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.module.jakarta.xmlbind.JakartaXmlBindAnnotationIntrospector; + +import nl.vpro.jackson.Views; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -37,6 +44,27 @@ public static class A { Optional optional; } + + @Test + + public void basicJackson2() throws JsonProcessingException { + JsonMapper mapper = new JsonMapper(); + mapper.setAnnotationIntrospector(new AnnotationIntrospectorPair( + new JacksonAnnotationIntrospector(), + new JakartaXmlBindAnnotationIntrospector(mapper.getTypeFactory())) + ); + mapper.registerModule(new Jdk8Module()); + + A a = mapper.readerWithView(Views.Normal.class).forType(A.class) + .readValue(""" + {"integer": 2, "optional": 3} + """); + assertThat(a.integer).isEqualTo(2); + assertThat(a.optional).contains(3); + + } + + @Test public void read() throws IOException { String example = "/* leading comments */\n{'integer': 2 /* ignore comments */, 'optional': 3}"; diff --git a/vpro-shared-jackson2/src/test/java/nl/vpro/jackson2/JsonArrayIteratorTest.java b/vpro-shared-jackson2/src/test/java/nl/vpro/jackson2/JsonArrayIteratorTest.java index c4f603fc1..e11c165fd 100644 --- a/vpro-shared-jackson2/src/test/java/nl/vpro/jackson2/JsonArrayIteratorTest.java +++ b/vpro-shared-jackson2/src/test/java/nl/vpro/jackson2/JsonArrayIteratorTest.java @@ -5,8 +5,7 @@ import java.io.*; import java.nio.charset.StandardCharsets; -import java.util.NoSuchElementException; -import java.util.Optional; +import java.util.*; import jakarta.xml.bind.annotation.XmlAccessType; import jakarta.xml.bind.annotation.XmlAccessorType; @@ -25,8 +24,32 @@ @Slf4j public class JsonArrayIteratorTest { + + @Test + public void simple() throws IOException { + try (JsonArrayIterator i = JsonArrayIterator.builder().valueClass(Change.class) + .inputStream( + new ByteArrayInputStream(""" + { + "size": 1, + "changes": [ + { + "sequence": 724, + "revision": 2, + "mid": "POMS_NCRV_1138990", + "deleted": true + } + } + } + """.getBytes(StandardCharsets.UTF_8))).build()) { + Change change = i.next(); + log.info("{}", change); + + } + } + @Test - public void test() throws IOException { + public void chanages() throws IOException { //Jackson2Mapper.getInstance().writeValue(System.out, new Change("bla", false)); try (JsonArrayIterator it = JsonArrayIterator.builder().inputStream(getClass().getResourceAsStream("/changes.json")).valueClass(Change.class).objectMapper(Jackson2Mapper.getInstance()).build()) { @@ -43,6 +66,7 @@ public void test() throws IOException { ); if (!change.isDeleted()) { assertThat(change.getMedia()).isNotNull(); + assertThat(change.getMedia()).isInstanceOf(Map.class); } } assertThat(it.hasNext()).isTrue(); // 11 diff --git a/vpro-shared-jackson3/pom.xml b/vpro-shared-jackson3/pom.xml index 7e0b9f8f9..7b747f38c 100644 --- a/vpro-shared-jackson3/pom.xml +++ b/vpro-shared-jackson3/pom.xml @@ -44,25 +44,23 @@ lombok - - jakarta.xml.bind - jakarta.xml.bind-api - true - jakarta.ws.rs jakarta.ws.rs-api - - org.glassfish.jaxb - jaxb-runtime - test - + jakarta.annotation jakarta.annotation-api provided + + + jakarta.xml.bind + jakarta.xml.bind-api + true + + com.google.guava guava @@ -73,5 +71,17 @@ commons-io true + + + org.glassfish.jaxb + jaxb-runtime + test + + + io.github.natty-parser + natty + test + + diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/IterableJson.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/IterableJson.java index f5e4c63be..2b0cb5aa9 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/IterableJson.java +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/IterableJson.java @@ -26,12 +26,12 @@ public void serialize(Iterable value, JsonGenerator jgen, SerializationContex if (i.hasNext()) { v = i.next(); if (! i.hasNext()) { - jgen.writeEmbeddedObject(v); + jgen.writePOJO(v); } else { jgen.writeStartArray(); - jgen.writeEmbeddedObject(v); + jgen.writePOJO(v); while (i.hasNext()) { - jgen.writeEmbeddedObject(i.next()); + jgen.writePOJO(i.next()); } jgen.writeEndArray(); } diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/Jackson3Mapper.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/Jackson3Mapper.java index ce8a78e26..ba9fe4772 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/Jackson3Mapper.java +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/Jackson3Mapper.java @@ -32,6 +32,7 @@ import static nl.vpro.logging.simple.Slf4jSimpleLogger.slf4j; import static tools.jackson.core.json.JsonReadFeature.*; import static tools.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES; +import static tools.jackson.databind.MapperFeature.DEFAULT_VIEW_INCLUSION; import static tools.jackson.databind.MapperFeature.USE_WRAPPER_NAME_AS_PROPERTY_NAME; import static tools.jackson.databind.SerializationFeature.INDENT_OUTPUT; @@ -51,41 +52,75 @@ public class Jackson3Mapper { private static boolean loggedAboutAvro = false; - private static boolean loggedAboutFallback = false; private static final SimpleFilterProvider FILTER_PROVIDER = new SimpleFilterProvider(); - public static final Jackson3Mapper INSTANCE = Jackson3Mapper.builder("instance") - .configure(Jackson3Mapper::lenient) - .build(); - public static final Jackson3Mapper LENIENT = getLenientInstance(); - public static final Jackson3Mapper STRICT = getStrictInstance(); - public static final Jackson3Mapper PRETTY_STRICT = getPrettyStrictInstance(); - public static final Jackson3Mapper PRETTY = getPrettyInstance(); - public static final Jackson3Mapper PUBLISHER = getPublisherInstance(); - public static final Jackson3Mapper PRETTY_PUBLISHER = getPublisherInstance(); + public static final Jackson3Mapper INSTANCE = Jackson3Mapper.builder("instance").build(); + public static final Jackson3Mapper LENIENT = buildLenientInstance(); + public static final Jackson3Mapper STRICT = buildStrictInstance(); + public static final Jackson3Mapper PRETTY_STRICT = buildPrettyStrictInstance(); + public static final Jackson3Mapper PRETTY = buildPrettyInstance(); + public static final Jackson3Mapper PUBLISHER = buildPublisherInstance(); + public static final Jackson3Mapper PRETTY_PUBLISHER = buildPublisherInstance(); + public static final Jackson3Mapper BACKWARDS_PUBLISHER = buildBackwardsPublisherInstance(); - public static final Jackson3Mapper BACKWARDS_PUBLISHER = getBackwardsPublisherInstance(); + public static final Jackson3Mapper MODEL = buildModelInstance(); + public static final Jackson3Mapper MODEL_AND_NORMAL = buildModelAndNormalInstance(); - private static final ThreadLocal THREAD_LOCAL = ThreadLocal.withInitial(() -> INSTANCE); + public static Jackson3Mapper getInstance() { + return INSTANCE; + } + public static Jackson3Mapper getLenientInstance() { + return LENIENT; + } + public static Jackson3Mapper getPrettyInstance() { + return PRETTY; + } + + public static Jackson3Mapper getPrettyStrictInstance() { + return PRETTY_STRICT; + } + public static Jackson3Mapper getStrictInstance() { + return STRICT; + } + public static Jackson3Mapper getPublisherInstance() { + return PUBLISHER; + } + public static Jackson3Mapper getPrettyPublisherInstance() { + return PRETTY_PUBLISHER; + } - private static Jackson3Mapper getLenientInstance() { + public static Jackson3Mapper getBackwardsPublisherInstance() { + return BACKWARDS_PUBLISHER; + } + + @Beta + public static Jackson3Mapper getModelInstance() { + return MODEL; + } + + @Beta + public static Jackson3Mapper getModelAndNormalInstance() { + return MODEL_AND_NORMAL; + } + + + + private static final ThreadLocal THREAD_LOCAL = ThreadLocal.withInitial(() -> INSTANCE); + + + private static Jackson3Mapper buildLenientInstance() { return Jackson3Mapper.builder("lenient") .forward() .configure(Jackson3Mapper::lenient) .build(); } - private final void allow() { - - } - private static void lenient(JsonMapper.Builder builder) { - builder.enable(EnumFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL); builder.enable(EnumFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL); builder.disable(FAIL_ON_UNKNOWN_PROPERTIES); builder.enable(ALLOW_UNQUOTED_PROPERTY_NAMES); @@ -95,7 +130,7 @@ private static void strict(JsonMapper.Builder builder) { builder.enable(FAIL_ON_UNKNOWN_PROPERTIES); } - private static Jackson3Mapper getPrettyInstance() { + private static Jackson3Mapper buildPrettyInstance() { Jackson3Mapper.Builder pretty = Jackson3Mapper .builder("pretty") .configure(b -> { @@ -105,7 +140,7 @@ private static Jackson3Mapper getPrettyInstance() { return pretty.build(); } - private static Jackson3Mapper getPrettyStrictInstance() { + private static Jackson3Mapper buildPrettyStrictInstance() { return Jackson3Mapper .builder("pretty_strict") @@ -120,7 +155,7 @@ private static Jackson3Mapper getPrettyStrictInstance() { } - private static Jackson3Mapper getStrictInstance() { + private static Jackson3Mapper buildStrictInstance() { return Jackson3Mapper.builder("strict") .configure(b -> { b.enable(FAIL_ON_UNKNOWN_PROPERTIES); @@ -129,14 +164,14 @@ private static Jackson3Mapper getStrictInstance() { .build(); } - private static Jackson3Mapper getPublisherInstance() { + private static Jackson3Mapper buildPublisherInstance() { return Jackson3Mapper.builder("publisher") .serializationView(Views.ForwardPublisher.class) .deserializationView(Views.Forward.class) .build(); } - private static Jackson3Mapper getPrettyPublisherInstance() { + private static Jackson3Mapper buildPrettyPublisherInstance() { return Jackson3Mapper.builder("pretty_publisher") .serializationView(Views.ForwardPublisher.class) .deserializationView(Views.Forward.class) @@ -144,7 +179,7 @@ private static Jackson3Mapper getPrettyPublisherInstance() { .build(); } - public static Jackson3Mapper getBackwardsPublisherInstance() { + private static Jackson3Mapper buildBackwardsPublisherInstance() { return Jackson3Mapper.builder("backwards_publisher") .serializationView(Views.Publisher.class) .deserializationView(Views.Normal.class) @@ -152,15 +187,13 @@ public static Jackson3Mapper getBackwardsPublisherInstance() { .build(); } - @Beta - public static Jackson3Mapper getModelInstance() { + private static Jackson3Mapper buildModelInstance() { return Jackson3Mapper.builder("model") .serializationView(Views.Model.class) .build(); } - @Beta - public static Jackson3Mapper getModelAndNormalInstance() { + private static Jackson3Mapper buildModelAndNormalInstance() { return Jackson3Mapper.builder("model_and_normal") .serializationView(Views.ModelAndNormal.class) .build(); @@ -180,7 +213,7 @@ public static void setThreadLocal(Jackson3Mapper set) { @SneakyThrows({JacksonException.class}) public static T lenientTreeToValue(JsonNode jsonNode, Class clazz) { - return getLenientInstance().reader().treeToValue(jsonNode, clazz); + return buildLenientInstance().reader().treeToValue(jsonNode, clazz); } private final JsonMapper mapper; @@ -227,7 +260,10 @@ public static void configureMapper(Jackson3Mapper.Builder builder, Predicate consumer) { } } - diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/JsonArrayIterator.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/JsonArrayIterator.java index bbd6ec473..ccb428b76 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/JsonArrayIterator.java +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/JsonArrayIterator.java @@ -20,6 +20,8 @@ import nl.vpro.util.CloseableIterator; import nl.vpro.util.CountedIterator; +import static java.util.Objects.requireNonNull; + /** * @author Michiel Meeuwissen * @since 1.0 @@ -28,8 +30,6 @@ public class JsonArrayIterator extends UnmodifiableIterator implements CloseableIterator, PeekingIterator, CountedIterator { - private final ObjectReader reader; - private final JsonParser jp; private T next = null; @@ -38,7 +38,7 @@ public class JsonArrayIterator extends UnmodifiableIterator private Boolean hasNext; - private final BiFunction valueCreator; + private final BiFunction valueCreator; @Getter @Setter @@ -69,14 +69,21 @@ public JsonArrayIterator(InputStream inputStream, final Class clazz, Runnable this(inputStream, null, clazz, callback, null, null, null, null, null, null); } - public JsonArrayIterator(InputStream inputStream, final BiFunction valueCreator) throws IOException { + public JsonArrayIterator(InputStream inputStream, final BiFunction valueCreator) throws IOException { this(inputStream, valueCreator, null, null, null, null, null, null, null, null); } - public static class Builder { + public static class Builder implements Iterable { + @Override + public @NonNull Iterator iterator() { + JsonArrayIterator build = build(); + return build.stream() + .onClose(build::close) + .iterator(); + } } /** @@ -94,13 +101,11 @@ public static class Builder { * @param logger Default this is logging to nl.vpro.jackson2.JsonArrayIterator, but you may override that. * @param skipNulls Whether to skip nulls in the array. Default true. * @param eventListener A listener for events that happen during parsing and iteration of the array. See {@link Event} and extension classes. - * @throws IOException If the json parser could not be created or the piece until the start of the array could - * not be tokenized. */ - @lombok.Builder(builderClassName = "Builder", builderMethodName = "builder") + @lombok.Builder(builderClassName = "Builder", builderMethodName = "_builder") private JsonArrayIterator( @NonNull InputStream inputStream, - @Nullable final BiFunction valueCreator, + @Nullable final BiFunction valueCreator, @Nullable final Class valueClass, @Nullable Runnable callback, @Nullable String sizeField, @@ -109,12 +114,10 @@ private JsonArrayIterator( @Nullable Logger logger, @Nullable Boolean skipNulls, @Nullable Listener eventListener - ) throws IOException { - if (inputStream == null) { - throw new IllegalArgumentException("No inputStream given"); - } - this.reader = objectMapper == null ? Jackson3Mapper.LENIENT.reader() : objectMapper.reader(); - this.jp = this.reader.createParser(inputStream); + ) { + requireNonNull(inputStream, "No inputStream given"); + ObjectReader reader = objectMapper == null ? Jackson3Mapper.LENIENT.reader() : objectMapper.reader(); + this.jp = reader.createParser(inputStream); this.valueCreator = valueCreator == null ? valueCreator(valueClass) : valueCreator; if (valueCreator != null && valueClass != null) { throw new IllegalArgumentException(); @@ -131,7 +134,7 @@ private JsonArrayIterator( if (totalSizeField == null) { totalSizeField = "totalSize"; } - this.eventListener = eventListener == null? (e) -> {} : eventListener; + this.eventListener = eventListener == null? Listener.noop() : eventListener; // find the start of the array, where we will start iterating. while(true) { JsonToken token = jp.nextToken(); @@ -148,7 +151,7 @@ private JsonArrayIterator( } if (token == JsonToken.VALUE_NUMBER_INT && totalSizeField.equals(fieldName)) { tmpTotalSize = jp.getLongValue(); - this.eventListener.accept(new TotalSizeEvent(tmpSize)); + this.eventListener.accept(new TotalSizeEvent(tmpTotalSize)); } if (token == JsonToken.START_ARRAY) { @@ -159,16 +162,21 @@ private JsonArrayIterator( this.totalSize = tmpTotalSize; this.eventListener.accept(new StartEvent()); JsonToken token = jp.nextToken(); + this.needsFindNext = token != JsonToken.END_ARRAY; + if (! needsFindNext) { + this.hasNext = false; + } + this.eventListener.accept(new TokenEvent(token)); this.callback = callback; this.skipNulls = skipNulls == null || skipNulls; } - private static BiFunction valueCreator(Class clazz) { - return (m, tree) -> { - try { - return m.treeToValue(tree, clazz); + private static BiFunction valueCreator(Class clazz) { + return (jp, tree) -> { + try (JsonParser sub = jp.objectReadContext().treeAsTokens(tree)) { + return sub.readValueAs(clazz); } catch (JacksonException e) { throw new ValueReadException(e); } @@ -214,7 +222,12 @@ protected void findNext() { if(needsFindNext) { while(true) { try { + var lastToken = jp.getLastClearedToken(); TreeNode tree = jp.readValueAsTree(); + var newLastToken = jp.getLastClearedToken(); + assert lastToken != newLastToken; + this.eventListener.accept(new TokenEvent(newLastToken)); + this.eventListener.accept(new TokenEvent(jp.getLastClearedToken())); if (jp.getLastClearedToken() == JsonToken.END_ARRAY) { @@ -222,6 +235,7 @@ protected void findNext() { } else { if (tree instanceof NullNode && skipNulls) { foundNulls++; + jp.nextToken(); continue; } } @@ -235,14 +249,14 @@ protected void findNext() { logger.warn("Found {} nulls. Will be skipped", foundNulls); } - next = valueCreator.apply(reader, tree); + next = valueCreator.apply(jp, tree); eventListener.accept(new NextEvent(next)); hasNext = true; } break; } catch (ValueReadException jme) { foundNulls++; - logger.warn(jme.getClass() + " " + jme.getMessage() + " for\n" + tree + "\nWill be skipped"); + logger.warn("{} {} for\n{}\nWill be skipped", jme.getClass(), jme.getMessage(), tree); } } catch (RuntimeException rte) { callbackBeforeThrow(rte); @@ -262,7 +276,7 @@ private void callbackBeforeThrow(RuntimeException e) { } @Override - public void close() throws IOException { + public void close() { callback(); this.jp.close(); @@ -336,7 +350,7 @@ public static void writeArray( public static void writeObjects( final CountedIterator iterator, final JsonGenerator jg, - final Function logging) throws IOException { + final Function logging) { while (iterator.hasNext()) { T change; try { @@ -378,6 +392,16 @@ public Optional getTotalSize() { return Optional.ofNullable(totalSize); } + public static Builder builder() { + return JsonArrayIterator._builder(); + } + + + public static Builder builder(Class valueClass) { + return JsonArrayIterator._builder() + .valueClass(valueClass); + } + public static class ValueReadException extends RuntimeException { @@ -444,7 +468,21 @@ public NextEvent(T next) { public interface Listener extends EventListener, Consumer.Event> { - } + static Listener noop() { + return new Listener() { + + @Override + public void accept(JsonArrayIterator.Event event) { + } + + @Override + public String toString() { + return "noop"; + + } + }; + } + } } diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/StringInstantToJsonTimestamp.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/StringInstantToJsonTimestamp.java index ec84dd0cf..2ce7fefbe 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/StringInstantToJsonTimestamp.java +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/StringInstantToJsonTimestamp.java @@ -61,14 +61,15 @@ static Instant parseDateTime(String value) { return DatatypeConverter.parseTime(value).toInstant(); } catch (IllegalArgumentException iae) { try { - Optional natty = NattySupport.parseDate(value); + final Optional natty = NattySupport.parseDate(value); if (natty.isPresent()) { return natty.get(); } else { log.debug("Natty didn't match"); } } catch (NoClassDefFoundError classNotFoundException) { - log.atLevel(warnedNatty ? Level.DEBUG : Level.WARN).log("No natty?: " + classNotFoundException.getMessage()); + log.atLevel(warnedNatty ? Level.DEBUG : Level.WARN). + log("No natty?: " + classNotFoundException.getMessage()); warnedNatty = true; } catch (Throwable e) { log.debug("Natty couldn't parse {}: {}", value, e.getMessage()); diff --git a/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/Jackson3MapperTest.java b/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/Jackson3MapperTest.java index 671f2f29f..0e9434905 100644 --- a/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/Jackson3MapperTest.java +++ b/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/Jackson3MapperTest.java @@ -2,15 +2,23 @@ import lombok.extern.log4j.Log4j2; import tools.jackson.core.JacksonException; +import tools.jackson.databind.MapperFeature; import tools.jackson.databind.exc.InvalidFormatException; +import tools.jackson.databind.introspect.AnnotationIntrospectorPair; +import tools.jackson.databind.introspect.JacksonAnnotationIntrospector; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.module.jakarta.xmlbind.JakartaXmlBindAnnotationIntrospector; import java.nio.charset.StandardCharsets; +import java.util.Map; import java.util.Optional; import jakarta.xml.bind.annotation.*; import org.junit.jupiter.api.Test; +import nl.vpro.jackson.Views; + import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -35,10 +43,37 @@ public static class A { Optional optional; } + /** + * This is not actually testing Jackson3Mapper itself, but is used to test things out with jackson3 itself. + * So, this test can be changed without affecting coverage. + */ + @Test + public void basicJackson3() { + JsonMapper mapper = JsonMapper.builder() + .enable(MapperFeature.DEFAULT_VIEW_INCLUSION) + .annotationIntrospector(new AnnotationIntrospectorPair( + new JacksonAnnotationIntrospector(), + new JakartaXmlBindAnnotationIntrospector(false) + )).build(); + + A a = mapper.readerWithView(Views.Normal.class).forType(A.class) + .readValue(""" + {"integer": 2, "optional": 3} + """); + assertThat(a.integer).isEqualTo(2); + assertThat(a.optional).contains(3); + } + + @Test public void read() { - String example = "/* leading comments */\n{'integer': 2 /* ignore comments */, 'optional': 3}"; - A a = Jackson3Mapper.INSTANCE.readerFor(A.class).readValue(example); + //String example = "/* leading comments */\n{'integer': 2 /* ignore comments */, 'optional': 3}"; + String example =""" + {"integer": 2, "optional": 3} + """; + A a = Jackson3Mapper.INSTANCE + .readerFor(A.class) + .readValue(example); assertThat(a.integer).isEqualTo(2); assertThat(a.optional).isPresent(); assertThat(a.optional.get()).isEqualTo(3); @@ -46,6 +81,36 @@ public void read() { Jackson3Mapper.LENIENT.readerFor(A.class).readValue(example.getBytes(StandardCharsets.UTF_8)); } + @Test + public void readChange() { + String change = """ + { + "sequence": 4000, + "revision": 1, + "mid": "POMS_VPRO_1139774", + "deleted": false, + "media": { + "objectType": "program", + "mid": "POMS_VPRO_1139774", + "creationDate": 1396061002808, + "lastModified": 1398759143568, + "sortDate": 1398722400000, + "urn": "urn:vpro:media:program:39326270", + "embeddable": true, + "descriptions": [1] + + } + } + """; + JsonArrayIteratorTest.Change a = Jackson3Mapper.INSTANCE + .reader() + .forType(JsonArrayIteratorTest.Change.class) + .readValue(change); + assertThat(a.getMedia()).isNotNull(); + assertThat(a.getMedia()).isInstanceOf(Map.class); + log.info("Change media: {}", a.getMedia()); + } + @Test public void readIntFromString() { A a = Jackson3Mapper.INSTANCE.readerFor(A.class).readValue("{'integer': '2'}"); diff --git a/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/JsonArrayIteratorTest.java b/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/JsonArrayIteratorTest.java index bcd4ad1b8..73cf3c22c 100644 --- a/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/JsonArrayIteratorTest.java +++ b/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/JsonArrayIteratorTest.java @@ -5,8 +5,7 @@ import java.io.*; import java.nio.charset.StandardCharsets; -import java.util.NoSuchElementException; -import java.util.Optional; +import java.util.*; import jakarta.xml.bind.annotation.XmlAccessType; import jakarta.xml.bind.annotation.XmlAccessorType; @@ -26,10 +25,36 @@ public class JsonArrayIteratorTest { @Test - public void test() throws IOException { + public void simple() { + for (Change change : JsonArrayIterator.builder(Change.class) + .inputStream( + new ByteArrayInputStream(""" + { + "size": 1, + "changes": [ + { + "sequence": 724, + "revision": 2, + "mid": "POMS_NCRV_1138990", + "deleted": true + } + } + } + """.getBytes(StandardCharsets.UTF_8)))) { + log.info("{}", change); + + } + } + + @Test + public void changes() throws IOException { //Jackson2Mapper.getInstance().writeValue(System.out, new Change("bla", false)); - try (JsonArrayIterator it = JsonArrayIterator.builder().inputStream(getClass().getResourceAsStream("/changes.json")).valueClass(Change.class).objectMapper(Jackson3Mapper.INSTANCE).build()) { + try (JsonArrayIterator it = JsonArrayIterator.builder() + .inputStream(getClass().getResourceAsStream("/changes.json")) + .valueClass(Change.class) + .objectMapper(Jackson3Mapper.INSTANCE) + .build()) { assertThat(it.next().getMid()).isEqualTo("POMS_NCRV_1138990"); // 1 assertThat(it.getCount()).isEqualTo(1); assertThat(it.getSize()).hasValueSatisfying(size -> assertThat(size).isEqualTo(14)); @@ -39,10 +64,11 @@ public void test() throws IOException { Change change = it.next(); // 10 Optional size = it.getSize(); size.ifPresent(aLong -> - log.info(it.getCount() + "/" + aLong + " :" + change) + log.info("{}/{} :{}", it.getCount(), aLong, change) ); if (!change.isDeleted()) { assertThat(change.getMedia()).isNotNull(); + assertThat(change.getMedia()).isInstanceOf(Map.class); } } assertThat(it.hasNext()).isTrue(); // 11 @@ -64,7 +90,10 @@ public void testEmpty() throws IOException { @Test public void testNulls() throws IOException { - JsonArrayIterator it = new JsonArrayIterator<>(new ByteArrayInputStream("{\"array\":[null, {}, null, {}]}".getBytes()), Change.class); + JsonArrayIterator it = new JsonArrayIterator<>( + new ByteArrayInputStream("{\"array\":[null, {}, null, {}]}".getBytes()), + Change.class + ); assertThat(it.hasNext()).isTrue(); it.next(); assertThat(it.peek()).isEqualTo(new Change()); @@ -224,7 +253,7 @@ private static class Simple { @Getter @Setter @EqualsAndHashCode - private static class Change { + public static class Change { private String mid; private boolean deleted; diff --git a/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/StringInstantToJsonTimestampTest.java b/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/StringInstantToJsonTimestampTest.java index a49de3caf..b3e1f9743 100644 --- a/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/StringInstantToJsonTimestampTest.java +++ b/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/StringInstantToJsonTimestampTest.java @@ -5,7 +5,6 @@ import java.time.*; import java.time.temporal.ChronoUnit; -import nl.vpro.jackson3.StringInstantToJsonTimestamp; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.*; @@ -42,7 +41,8 @@ public void testNotOk() { @Test public void testNattyNow() { - assertThat(StringInstantToJsonTimestamp.parseDateTime("now")).isCloseTo(Instant.now(), within(10, ChronoUnit.SECONDS)); + assertThat(StringInstantToJsonTimestamp.parseDateTime("now")) + .isCloseTo(Instant.now(), within(10, ChronoUnit.SECONDS)); } } diff --git a/vpro-shared-jackson3/src/test/resources/changes.json b/vpro-shared-jackson3/src/test/resources/changes.json new file mode 100644 index 000000000..65e090f30 --- /dev/null +++ b/vpro-shared-jackson3/src/test/resources/changes.json @@ -0,0 +1,780 @@ +{ + "size": 14, + "changes": [ + { + "sequence": 724, + "revision": 2, + "mid": "POMS_NCRV_1138990", + "deleted": true + }, + "Een string", + null, + { + "sequence": 3660, + "revision": 2, + "mid": "POMS_AVRO_1138559", + "deleted": true + }, + "some error", + { + "sequence": 3661, + "revision": 2, + "mid": "POMS_AVRO_1138560", + "deleted": true + }, + { + "sequence": 4000, + "revision": 1, + "mid": "POMS_VPRO_1139774", + "deleted": false, + "media": { + "objectType": "program", + "mid": "POMS_VPRO_1139774", + "creationDate": 1396061002808, + "lastModified": 1398759143568, + "sortDate": 1398722400000, + "urn": "urn:vpro:media:program:39326270", + "embeddable": true, + "descriptions": [1] + + } + }, + { + "sequence": 5419, + "revision": 1, + "mid": "POMS_VPRO_1139774", + "deleted": false, + "media": { + "objectType": "program", + "mid": "POMS_VPRO_1139774", + "creationDate": 1396061002808, + "lastModified": 1398759143568, + "sortDate": 1398722400000, + "urn": "urn:vpro:media:program:39326270", + "embeddable": true, + "episodeOf": [ + { + "midRef": "POMS_S_VPRO_449041", + "urnRef": "urn:vpro:media:group:33644950", + "type": "SERIES", + "index": 1, + "highlighted": false, + "added": 1396061004445 + } + ], + "crids": ["crid://broadcast.radiobox2/278049"], + "broadcasters": [ + { + "id": "VPRO", + "value": "VPRO" + } + ], + "titles": [ + { + "value": "Nooit Meer Slapen", + "owner": "RADIOBOX", + "type": "MAIN" + } + ], + "descriptions": [ + { + "value": "Actrice, columniste en schrijfster Tosca Niterink is in het eerste uur te gast om te vertellen over haar boek 'De Vergeetclub'. En Jungle by Night presenteerde afgelopen weekend hun tweede plaat 'The Hunt', en trapte een internationale tournee af. Botte Jellema bezocht het optreden en sprak daar met de band. Verder reageert schrijfster Saskia de Coster met fictie op het nieuws van de dag en correspondent Eva de Valk bericht vanuit San Francisco over de muurschilderingen in Mission District. Ook onderzoeken we het fenomeen 'indie games' met journalist Niels 't Hooft en game-ontwikkelaar Rami Ismail.", + "owner": "RADIOBOX", + "type": "MAIN" + } + ], + "genres": [], + "countries": [], + "languages": [], + "duration": 7200000, + "descendantOf": [ + { + "midRef": "POMS_S_VPRO_449041", + "urnRef": "urn:vpro:media:group:33644950", + "type": "SERIES" + } + ], + "email": ["nooitmeerslapen@vpro.nl"], + "websites": [ + { + "value": "http://www.radio1.nl/nooitmeerslapen" + } + ], + "locations": [ + { + "programUrl": "http://download.omroep.nl/audiologging/r1/2014/04/29/0000_0200_nooit_meer_slapen_20140429.mp3", + "avAttributes": { + "avFileFormat": "MP3" + }, + "duration": 7200000, + "creationDate": 1398759137893, + "lastModified": 1398759143567, + "workflow": "PUBLISHED", + "owner": "RADIOBOX", + "urn": "urn:vpro:media:location:40652422" + } + ], + "scheduleEvents": [ + { + "start": 1398722400000, + "duration": 7200000, + "poProgID": "POMS_VPRO_1139774", + "channel": "RAD1", + "urnRef": "urn:vpro:media:program:39326270", + "midRef": "POMS_VPRO_1139774" + } + ], + "images": [ + { + "title": "nms_itunes_profiel.jpg", + "description": "Nooit Meer Slapen", + "imageUri": "urn:vpro:image:226081", + "creationDate": 1398757654108, + "lastModified": 1398757656461, + "workflow": "PUBLISHED", + "owner": "RADIOBOX", + "type": "PICTURE", + "highlighted": false, + "urn": "urn:vpro:media:image:40644427" + } + ], + "workflow": "PUBLISHED", + "type": "BROADCAST", + "avType": "AUDIO" + } + }, + { + "sequence": 6629, + "revision": 4, + "mid": "POMS_VPRO_1139787", + "deleted": false, + "media": { + "objectType": "program", + "mid": "POMS_VPRO_1139787", + "creationDate": 1396061002808, + "lastModified": 1398760426214, + "sortDate": 1398722400000, + "urn": "urn:vpro:media:program:39326270", + "embeddable": true, + "episodeOf": [ + { + "midRef": "POMS_S_VPRO_449041", + "urnRef": "urn:vpro:media:group:33644950", + "type": "SERIES", + "index": 1, + "highlighted": false, + "added": 1396061004445 + } + ], + "crids": ["crid://broadcast.radiobox2/278049"], + "broadcasters": [ + { + "id": "VPRO", + "value": "VPRO" + } + ], + "titles": [ + { + "value": "Nooit Meer Slapen", + "owner": "RADIOBOX", + "type": "MAIN" + } + ], + "descriptions": [ + { + "value": "Actrice, columniste en schrijfster Tosca Niterink is in het eerste uur te gast om te vertellen over haar boek 'De Vergeetclub'. En Jungle by Night presenteerde afgelopen weekend hun tweede plaat 'The Hunt', en trapte een internationale tournee af. Botte Jellema bezocht het optreden en sprak daar met de band. Verder reageert schrijfster Saskia de Coster met fictie op het nieuws van de dag en correspondent Eva de Valk bericht vanuit San Francisco over de muurschilderingen in Mission District. Ook onderzoeken we het fenomeen 'indie games' met journalist Niels 't Hooft en game-ontwikkelaar Rami Ismail.", + "owner": "RADIOBOX", + "type": "MAIN" + } + ], + "genres": [], + "countries": [], + "languages": [], + "duration": 7200000, + "descendantOf": [ + { + "midRef": "POMS_S_VPRO_449041", + "urnRef": "urn:vpro:media:group:33644950", + "type": "SERIES" + } + ], + "email": ["nooitmeerslapen@vpro.nl"], + "websites": [ + { + "value": "http://www.radio1.nl/nooitmeerslapen" + } + ], + "locations": [ + { + "programUrl": "http://download.omroep.nl/audiologging/r1/2014/04/29/0000_0200_nooit_meer_slapen_20140429.mp3", + "avAttributes": { + "avFileFormat": "MP3" + }, + "duration": 7200000, + "creationDate": 1398759137893, + "lastModified": 1398759143567, + "workflow": "PUBLISHED", + "owner": "RADIOBOX", + "urn": "urn:vpro:media:location:40652422" + } + ], + "scheduleEvents": [ + { + "start": 1398722400000, + "duration": 7200000, + "poProgID": "POMS_VPRO_1139787", + "channel": "RAD1", + "urnRef": "urn:vpro:media:program:39326270", + "midRef": "POMS_VPRO_1139787" + } + ], + "images": [ + { + "title": "nms_itunes_profiel.jpg", + "description": "Nooit Meer Slapen", + "imageUri": "urn:vpro:image:226081", + "creationDate": 1398757654108, + "lastModified": 1398757656461, + "workflow": "PUBLISHED", + "owner": "RADIOBOX", + "type": "PICTURE", + "highlighted": false, + "urn": "urn:vpro:media:image:40644427" + } + ], + "workflow": "PUBLISHED", + "segments": [ + { + "objectType": "segment", + "mid": "POMS_VPRO_1139788", + "creationDate": 1398760359009, + "lastModified": 1398760426214, + "sortDate": 1398722400000, + "urn": "urn:vpro:media:segment:40658898", + "embeddable": true, + "crids": ["crid://fragment.radiobox2/142297"], + "broadcasters": [ + { + "id": "VPRO", + "value": "VPRO" + } + ], + "titles": [ + { + "value": "Tosca Niterink's nieuwe boek: De Vergeetclub", + "owner": "RADIOBOX", + "type": "MAIN" + } + ], + "descriptions": [ + { + "value": "De dementerende moeder van Tosca Niterink doet samen met acht andere dames op leeftijd aan kleinschalig wonen achter een cijferslot, voor hun eigen veiligheid. In het boek 'De Vergeetclub' beschrijft de actrice, die vooral bekend werd door haar aandeel in 'Theo en Thea', 'Kreatief met Kurk' en 'Borreltijd', met oog voor het absurde, de uitgesproken karakters van de dames en de verwikkelingen die daaruit voortkomen. Actrice, columniste en schrijfster Tosca Niterink vertelt over haar boek.", + "owner": "RADIOBOX", + "type": "MAIN" + } + ], + "genres": [], + "countries": [], + "languages": [], + "duration": 3365000, + "descendantOf": [ + { + "midRef": "POMS_S_VPRO_449041", + "urnRef": "urn:vpro:media:group:33644950", + "type": "SERIES" + }, + { + "midRef": "POMS_VPRO_1139787", + "urnRef": "urn:vpro:media:program:39326270", + "type": "BROADCAST" + } + ], + "locations": [ + { + "programUrl": "http://radiobox2.omroep.nl/audiofragment/file/142297/fragment.mp3", + "avAttributes": { + "avFileFormat": "MP3" + }, + "duration": 3365000, + "creationDate": 1398760346008, + "lastModified": 1398760359138, + "workflow": "PUBLISHED", + "owner": "RADIOBOX", + "urn": "urn:vpro:media:location:40658902" + } + ], + "images": [ + { + "title": "devergeetclub.jpg", + "description": "Tosca Niterink's nieuwe boek: De Vergeetclub", + "imageUri": "urn:vpro:image:251307", + "creationDate": 1398760357411, + "lastModified": 1398760359137, + "workflow": "PUBLISHED", + "owner": "RADIOBOX", + "type": "PICTURE", + "highlighted": false, + "urn": "urn:vpro:media:image:40658900" + } + ], + "workflow": "PUBLISHED", + "start": 159000, + "urnRef": "urn:vpro:media:program:39326270", + "midRef": "POMS_VPRO_1139787", + "type": "SEGMENT", + "avType": "AUDIO" + }, + { + "objectType": "segment", + "mid": "POMS_VPRO_1139789", + "creationDate": 1398760397576, + "lastModified": 1398760426214, + "sortDate": 1398722400000, + "urn": "urn:vpro:media:segment:40658921", + "embeddable": true, + "crids": ["crid://fragment.radiobox2/142299"], + "broadcasters": [ + { + "id": "VPRO", + "value": "VPRO" + } + ], + "titles": [ + { + "value": "De kleinschalige kant van de game industrie: 'indie games'", + "owner": "RADIOBOX", + "type": "MAIN" + } + ], + "descriptions": [ + { + "value": "Videogames zijn een miljardenindustrie geworden. Maar deze industrie heeft ook een meer kleinschalige kant: 'indie games'. Nederland is sterk vertegenwoordigd in dit genre. Het zijn video games die door individuen of kleine teams worden ontwikkeld, meestal zonder financiële steun van een uitgever. We onderzoeken het fenomeen met journalist Niels 't Hooft en game-ontwikkelaar Rami Ismail.", + "owner": "RADIOBOX", + "type": "MAIN" + } + ], + "genres": [], + "countries": [], + "languages": [], + "duration": 971000, + "descendantOf": [ + { + "midRef": "POMS_S_VPRO_449041", + "urnRef": "urn:vpro:media:group:33644950", + "type": "SERIES" + }, + { + "midRef": "POMS_VPRO_1139787", + "urnRef": "urn:vpro:media:program:39326270", + "type": "BROADCAST" + } + ], + "locations": [ + { + "programUrl": "http://radiobox2.omroep.nl/audiofragment/file/142299/fragment.mp3", + "avAttributes": { + "avFileFormat": "MP3" + }, + "duration": 971000, + "creationDate": 1398760371060, + "lastModified": 1398760397659, + "workflow": "PUBLISHED", + "owner": "RADIOBOX", + "urn": "urn:vpro:media:location:40658925" + } + ], + "images": [ + { + "title": "indiegames.jpg", + "description": "De kleinschalige kant van de game industrie: 'indie games'", + "imageUri": "urn:vpro:image:251308", + "creationDate": 1398760386849, + "lastModified": 1398760397658, + "workflow": "PUBLISHED", + "owner": "RADIOBOX", + "type": "PICTURE", + "highlighted": false, + "urn": "urn:vpro:media:image:40658923" + } + ], + "workflow": "PUBLISHED", + "start": 4336000, + "urnRef": "urn:vpro:media:program:39326270", + "midRef": "POMS_VPRO_1139787", + "type": "SEGMENT", + "avType": "AUDIO" + }, + { + "objectType": "segment", + "mid": "POMS_VPRO_1139790", + "creationDate": 1398760397577, + "lastModified": 1398760426214, + "sortDate": 1398722400000, + "urn": "urn:vpro:media:segment:40658927", + "embeddable": true, + "crids": ["crid://fragment.radiobox2/142298"], + "broadcasters": [ + { + "id": "VPRO", + "value": "VPRO" + } + ], + "titles": [ + { + "value": "Jungle by Night's tweede album The Hunt", + "owner": "RADIOBOX", + "type": "MAIN" + } + ], + "descriptions": [ + { + "value": "Negen Amsterdamse jongens bestormden drie jaar geleden alle festivalpodia van Nederland en ver daarbuiten. Ze waren een opvallend succes met hun Afrobeat. Jungle by Night presenteerde afgelopen weekend hun tweede plaat, The Hunt, en trapte een internationale tournee af. Botte Jellema bezocht het optreden en sprak daar met de band.", + "owner": "RADIOBOX", + "type": "MAIN" + } + ], + "genres": [], + "countries": [], + "languages": [], + "duration": 921000, + "descendantOf": [ + { + "midRef": "POMS_S_VPRO_449041", + "urnRef": "urn:vpro:media:group:33644950", + "type": "SERIES" + }, + { + "midRef": "POMS_VPRO_1139787", + "urnRef": "urn:vpro:media:program:39326270", + "type": "BROADCAST" + } + ], + "locations": [ + { + "programUrl": "http://radiobox2.omroep.nl/audiofragment/file/142298/fragment.mp3", + "avAttributes": { + "avFileFormat": "MP3" + }, + "duration": 921000, + "creationDate": 1398760371060, + "lastModified": 1398760397662, + "workflow": "PUBLISHED", + "owner": "RADIOBOX", + "urn": "urn:vpro:media:location:40658931" + } + ], + "images": [ + { + "title": "junglebynight.jpg", + "description": "Jungle by Night's tweede album The Hunt", + "imageUri": "urn:vpro:image:251309", + "creationDate": 1398760395326, + "lastModified": 1398760397661, + "workflow": "PUBLISHED", + "owner": "RADIOBOX", + "type": "PICTURE", + "highlighted": false, + "urn": "urn:vpro:media:image:40658929" + } + ], + "workflow": "PUBLISHED", + "start": 6230000, + "urnRef": "urn:vpro:media:program:39326270", + "midRef": "POMS_VPRO_1139787", + "type": "SEGMENT", + "avType": "AUDIO" + } + ], + "type": "BROADCAST", + "avType": "AUDIO" + } + }, + { + "sequence": 10553, + "revision": 2, + "mid": "POW_00039706", + "deleted": false, + "media": { + "objectType": "program", + "mid": "POW_00039706", + "creationDate": 1279283049485, + "lastModified": 1398764908532, + "sortDate": 1140732900000, + "urn": "urn:vpro:media:program:3030437", + "embeddable": true, + "episodeOf": [ + { + "midRef": "POW_00039674", + "urnRef": "urn:vpro:media:group:19690815", + "type": "SEASON", + "index": 3, + "highlighted": false, + "added": 1359627207372 + } + ], + "crids": ["crid://tmp.program.omroep.nl/2621725"], + "broadcasters": [ + { + "id": "VPRO", + "value": "VPRO" + }, + { + "id": "IKON", + "value": "IKON" + } + ], + "titles": [ + { + "value": "De Donderdag Documentaire", + "owner": "BROADCASTER", + "type": "MAIN" + }, + { + "value": "De Donderdag Documentaire: Dorpsbelangen", + "owner": "NEBO", + "type": "MAIN" + }, + { + "value": "Dorpsbelangen", + "owner": "BROADCASTER", + "type": "SUB" + } + ], + "descriptions": [ + { + "value": "In de gemeentelijke politiek gelden andere regels en wetten dan in de landelijke. Aan de vooravond van de gemeenteraadsverkiezingen in 2006 portretteert filmmaakster Marieke van der Winden vier gemeenteraadsleden in Maasdriel. ", + "owner": "BROADCASTER", + "type": "MAIN" + }, + { + "value": "Documentaire aan de vooravond van de gemeenteraadsverkiezingen van 2006. 'Als ik hier de baas was, zou ik de boel al lang hebben volgebouwd! We moeten niet van heel Nederland een natuurgebied willen maken.' Eric van der Veen, gemeenteraadslid van Maasdriel, zit op de bank bij collega-gemeenteraadslid Ko Hooijmans. Het uitzicht is adembenemend: natuur zo ver je kijken kunt. Hooijmans haalt wat gegeneerd zijn schouders op. Gaat zijn collega niet wat ver voor het oog van de camera? In de gemeentelijke politiek gelden andere regels en wetten dan in de landelijke. Filmmaakster Marieke van der Winden dompelde zich een jaar lang onder in de gemeentelijke politiek van Maasdriel en portretteerde vier gemeenteraadsleden. Ondernemer Eric van der Veen heeft als fractievoorzitter van Gemeentebelangen Maasdriel een flinke vinger in de pap van de plaatselijke politiek. Hij is onder meer eigenaar van de apparatuur van de plaatselijke omroep. Zijn nieuwste plan is de plaatsing van een zendmast. Hij realiseert zich maar al te goed hoe belangrijk de media zijn voor zijn positie. Van der Veen is een echte lobbyist en netwerker. Net als veel andere gemeenteraadsleden én de burgemeester doet hij bij de plaatselijke kapper de laatste dorpsnieuwtjes en -roddels op. Aannemer Jan van Boxtel van het CDA weet heel goed hoe hij zijn achterban aan zich kan binden: via de kerk. In het parochiebestuur én in de gemeenteraad beijvert hij zich onder meer voor het onderhoud van de kerk en andere monumenten. Daar vloeien vaak ook weer leuke opdrachten uit voort. Daarnaast doet hij, samen met zijn vrouw, goede werken zoals het rondbrengen van warme maaltijden. Het levert deze man van geen woorden maar daden veel goodwill én stemmen op. Naast de bouwsector is de agrarische sector een belangrijke economische tak van sport in dit gebied. Ook Ko Hooijmans van de VVD verdient er als champignonkweker een goede boterham in. Op de plek van het huis van zijn ouders heeft hij onlangs een villa laten neerzetten. Hooijmans laat", + "owner": "NEBO", + "type": "MAIN" + }, + { + "value": "In de gemeentelijke politiek gelden andere regels en wetten dan in de landelijke. Aan de vooravond van de gemeenteraadsverkiezingen in 2006 portretteert filmmaakster Marieke van der Winden vier gemeenteraadsleden in Maasdriel. ", + "owner": "BROADCASTER", + "type": "SHORT" + }, + { + "value": "Documentaire over een jaar lang gemeentelijke politiek in Maasdriel. Met portretten van vier gemeenteraadsleden. Hier gelden andere, meer persoonlijke belangen. Als je een invloedrijke familie hebt, eigenaar bent van een bedrijf of aan het hoofd staat van de kerk, heb je een streepje voor.", + "owner": "NEBO", + "type": "SHORT" + }, + { + "value": "Titel: Dorpsbelangen - Documentaire aan de vooravond van de gemeenteraadsverkiezingen van 2006. 'Als ik hier de baas was, zou ik de boel al lang hebben volgebouwd! We moeten niet van heel Nederland een natuurgebied willen maken.' Eric van der Veen, gemeenteraadslid van Maasdriel, zit op de bank bij collega-gemeenteraadslid Ko Hooijmans. Het uitzicht is adembenemend: natuur zo ver je kijken kunt. Hooijmans haalt wat gegeneerd zijn schouders op. Gaat zijn collega niet wat ver voor het oog van de camera? In de gemeentelijke politiek gelden andere regels en wetten dan in de landelijke. Filmmaakster Marieke van der Winden dompelde zich een jaar lang onder in de gemeentelijke politiek van Maasdriel en portretteerde vier gemeenteraadsleden.", + "owner": "BROADCASTER", + "type": "EPISODE" + }, + { + "value": "Documentaire aan de vooravond van de gemeenteraadsverkiezingen van 2006. 'Als ik hier de baas was, zou ik de boel al lang hebben volgebouwd! We moeten niet van heel Nederland een natuurgebied willen maken.' Eric van der Veen, gemeenteraadslid van Maasdriel, zit op de bank bij collega-gemeenteraadslid Ko Hooijmans. Het uitzicht is adembenemend: natuur zo ver je kijken kunt. Hooijmans haalt wat gegeneerd zijn schouders op. Gaat zijn collega niet wat ver voor het oog van de camera? In de gemeentelijke politiek gelden andere regels en wetten dan in de landelijke. Filmmaakster Marieke van der Winden dompelde zich een jaar lang onder in de gemeentelijke politiek van Maasdriel en portretteerde vier gemeenteraadsleden. Ondernemer Eric van der Veen heeft als fractievoorzitter van Gemeentebelangen Maasdriel een flinke vinger in de pap van de plaatselijke politiek. Hij is onder meer eigenaar van de apparatuur van de plaatselijke omroep. Zijn nieuwste plan is de plaatsing van een zendmast. Hij realiseert zich maar al te goed hoe belangrijk de media zijn voor zijn positie. Van der Veen is een echte lobbyist en netwerker. Net als veel andere gemeenteraadsleden én de burgemeester doet hij bij de plaatselijke kapper de laatste dorpsnieuwtjes en -roddels op. Aannemer Jan van Boxtel van het CDA weet heel goed hoe hij zijn achterban aan zich kan binden: via de kerk. In het parochiebestuur én in de gemeenteraad beijvert hij zich onder meer voor het onderhoud van de kerk en andere monumenten. Daar vloeien vaak ook weer leuke opdrachten uit voort. Daarnaast doet hij, samen met zijn vrouw, goede werken zoals het rondbrengen van warme maaltijden. Het levert deze man van geen woorden maar daden veel goodwill én stemmen op. Naast de bouwsector is de agrarische sector een belangrijke economische tak van sport in dit gebied. Ook Ko Hooijmans van de VVD verdient er als champignonkweker een goede boterham in. Op de plek van het huis van zijn ouders heeft hij onlangs een villa laten neerzetten. Hooijmans laat Regie: Marieke van der Winden.", + "owner": "NEBO", + "type": "EPISODE" + } + ], + "genres": [ + { + "id": "3.0.1.8", + "terms": ["Documentaire"] + } + ], + "countries": [], + "languages": [], + "avAttributes": { + "videoAttributes": { + "aspectRatio": "4:3" + } + }, + "duration": 3300000, + "descendantOf": [ + { + "midRef": "POW_00039674", + "urnRef": "urn:vpro:media:group:19690815", + "type": "SEASON" + }, + { + "midRef": "POMS_S_HUMAN_105498", + "urnRef": "urn:vpro:media:group:3029741", + "type": "SERIES" + } + ], + "ageRating": "ALL", + "predictions": [ + { + "state": "REALIZED", + "publishStart": 1136070000000, + "platform": "INTERNETVOD" + } + ], + "locations": [ + { + "programUrl": "http://cgi.omroep.nl/cgi-bin/streams?/tv/ikon/dedonderdagdocumentaire/sb.20060223.asf", + "avAttributes": { + "bitrate": 204800, + "avFileFormat": "WM", + "videoAttributes": { + "videoCoding": "WM", + "aspectRatio": "4:3" + } + }, + "creationDate": 1279283049534, + "lastModified": 1359627207340, + "publishStart": 1136070000000, + "workflow": "PUBLISHED", + "owner": "NEBO", + "urn": "urn:vpro:media:location:3030448" + }, + { + "programUrl": "http://cgi.omroep.nl/cgi-bin/streams?/tv/ikon/dedonderdagdocumentaire/bb.20060223.asf", + "avAttributes": { + "bitrate": 512000, + "avFileFormat": "WM", + "videoAttributes": { + "videoCoding": "WM", + "aspectRatio": "4:3" + } + }, + "creationDate": 1279283049546, + "lastModified": 1359627207340, + "publishStart": 1136070000000, + "workflow": "PUBLISHED", + "owner": "NEBO", + "urn": "urn:vpro:media:location:3030451" + } + ], + "scheduleEvents": [ + { + "avAttributes": { + "bitrate": 0, + "avFileFormat": "WM", + "videoAttributes": { + "videoCoding": "WM", + "aspectRatio": "4:3" + } + }, + "textSubtitles": "Teletekst ondertitels", + "start": 1140732900000, + "duration": 3300000, + "poProgID": "POW_00039706", + "channel": "NED1", + "urnRef": "urn:vpro:media:program:3030437", + "midRef": "POW_00039706" + } + ], + "images": [ + { + "title": "Dorpsbelangen", + "imageUri": "urn:vpro.image:7012", + "height": 266, + "width": 400, + "creationDate": 1279800000433, + "lastModified": 1306252549650, + "workflow": "PUBLISHED", + "owner": "BROADCASTER", + "type": "PICTURE", + "highlighted": false, + "urn": "urn:vpro:media:image:3213689" + }, + { + "title": "Dorpsbelangen", + "description": "In de gemeentelijke politiek gelden andere regels en wetten dan in de landelijke. Aan de vooravond van de gemeenteraadsverkiezingen in 2006 portretteert filmmaakster Marieke van der Winden vier gemeenteraadsleden in Maasdriel. ", + "imageUri": "urn:vpro:image:70792", + "creationDate": 1335362453307, + "lastModified": 1335362453790, + "workflow": "PUBLISHED", + "owner": "NEBO", + "type": "STILL", + "highlighted": false, + "urn": "urn:vpro:media:image:13886727" + }, + { + "title": "Dorpsbelangen", + "description": "In de gemeentelijke politiek gelden andere regels en wetten dan in de landelijke. Aan de vooravond van de gemeenteraadsverkiezingen in 2006 portretteert filmmaakster Marieke van der Winden vier gemeenteraadsleden in Maasdriel. ", + "imageUri": "urn:vpro:image:70793", + "creationDate": 1335362453536, + "lastModified": 1335362453790, + "workflow": "PUBLISHED", + "owner": "NEBO", + "type": "STILL", + "highlighted": false, + "urn": "urn:vpro:media:image:13886728" + }, + { + "title": "Dorpsbelangen", + "description": "In de gemeentelijke politiek gelden andere regels en wetten dan in de landelijke. Aan de vooravond van de gemeenteraadsverkiezingen in 2006 portretteert filmmaakster Marieke van der Winden vier gemeenteraadsleden in Maasdriel. ", + "imageUri": "urn:vpro:image:70794", + "creationDate": 1335362453788, + "lastModified": 1335362453791, + "workflow": "PUBLISHED", + "owner": "NEBO", + "type": "STILL", + "highlighted": false, + "urn": "urn:vpro:media:image:13886729" + }, + { + "title": "De Donderdag Documentaire", + "description": "In de gemeentelijke politiek gelden andere regels en wetten dan in de landelijke. Aan de vooravond van de gemeenteraadsverkiezingen in 2006 portretteert filmmaakster Marieke van der Winden vier gemeenteraadsleden in Maasdriel. ", + "imageUri": "urn:vpro:image:328108", + "creationDate": 1389030464312, + "lastModified": 1389030498441, + "workflow": "PUBLISHED", + "owner": "NEBO", + "type": "STILL", + "highlighted": false, + "urn": "urn:vpro:media:image:35057598" + }, + { + "title": "De Donderdag Documentaire", + "description": "In de gemeentelijke politiek gelden andere regels en wetten dan in de landelijke. Aan de vooravond van de gemeenteraadsverkiezingen in 2006 portretteert filmmaakster Marieke van der Winden vier gemeenteraadsleden in Maasdriel. ", + "imageUri": "urn:vpro:image:328109", + "creationDate": 1389030481417, + "lastModified": 1389030498442, + "workflow": "PUBLISHED", + "owner": "NEBO", + "type": "STILL", + "highlighted": false, + "urn": "urn:vpro:media:image:35057599" + }, + { + "title": "De Donderdag Documentaire", + "description": "In de gemeentelijke politiek gelden andere regels en wetten dan in de landelijke. Aan de vooravond van de gemeenteraadsverkiezingen in 2006 portretteert filmmaakster Marieke van der Winden vier gemeenteraadsleden in Maasdriel. ", + "imageUri": "urn:vpro:image:328110", + "creationDate": 1389030498300, + "lastModified": 1389030498443, + "workflow": "PUBLISHED", + "owner": "NEBO", + "type": "STILL", + "highlighted": false, + "urn": "urn:vpro:media:image:35057600" + } + ], + "workflow": "PUBLISHED", + "type": "BROADCAST", + "avType": "VIDEO" + } + }, + { + "sequence": 10687, + "revision": 2, + "mid": "POMS_NCRV_1141275", + "deleted": true + }, + { + "sequence": 10730, + "revision": 2, + "mid": "POMS_KRO_1140821", + "deleted": true + }, + { + "sequence": 10738, + "revision": 2, + "mid": "POMS_EO_1140308", + "deleted": true + }, + { + "sequence": 10748, + "revision": 2, + "mid": "POMS_VPRO_1139788", + "deleted": true + } +]} diff --git a/vpro-shared-jackson3/src/test/resources/log4j2-test.xml b/vpro-shared-jackson3/src/test/resources/log4j2-test.xml new file mode 100644 index 000000000..4965d8719 --- /dev/null +++ b/vpro-shared-jackson3/src/test/resources/log4j2-test.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/vpro-shared-util/src/main/java/nl/vpro/util/DirectoryWatcher.java b/vpro-shared-util/src/main/java/nl/vpro/util/DirectoryWatcher.java index 6f1bc1c91..113173b6b 100644 --- a/vpro-shared-util/src/main/java/nl/vpro/util/DirectoryWatcher.java +++ b/vpro-shared-util/src/main/java/nl/vpro/util/DirectoryWatcher.java @@ -101,7 +101,8 @@ private WatchKey register(Path path, WatchService watcher) throws IOException { @SneakyThrows private Future watchService() { - register(directory, watcher); + WatchKey key = register(directory, watcher); + log.debug("Registered watch key {} for directory {}", key, directory); // check initial set of file for existing symlinks to follow the targets of. try (Stream p = Files.list(directory).filter(filter)) { @@ -162,6 +163,7 @@ public enum WatcherEventType { DELETE, RELINKED; + @NonNull public static WatcherEventType of(WatchEvent.Kind type) { if (type == ENTRY_CREATE) { return CREATE; @@ -169,8 +171,10 @@ public static WatcherEventType of(WatchEvent.Kind type) { return MODIFY; } else if (type == ENTRY_DELETE) { return DELETE; + } else if (type == OVERFLOW) { + return MODIFY; } else { - return null; + throw new IllegalArgumentException("Unknown WatchEvent.Kind " + type); } } } @@ -247,7 +251,7 @@ private void watchLoop() { key.reset(); } } catch (InterruptedException e) { - log.info("Interrupted watcher for " + directory); + log.info("Interrupted watcher for {}", directory); Thread.currentThread().interrupt(); break; } @@ -290,7 +294,7 @@ private Optional checkSymlink(Path file, WatchEvent.Kind kind) { watchedTargetFiles.put(pathToKey(resolve), file); if (!watchedTargetDirectories.contains(pathToKey(resolve.getParent()))) { - register(resolve.getParent(), watcher); + WatchKey key = register(resolve.getParent(), watcher); watchedTargetDirectories.add(pathToKey(resolve.getParent())); } return Optional.of(resolve); From e0860bc06507597f091c3d37af5ccd23ecb36b27 Mon Sep 17 00:00:00 2001 From: Michiel Meeuwissen Date: Fri, 9 Jan 2026 13:42:37 +0100 Subject: [PATCH 12/18] Trying to fix test cases. --- .../java/nl/vpro/jackson3/Jackson3Mapper.java | 15 +- .../nl/vpro/jackson3/JsonArrayIterator.java | 20 +- .../vpro/jackson3/GuavaRangeModuleTest.java | 12 +- .../nl/vpro/jackson3/Jackson3MapperTest.java | 9 +- .../vpro/jackson3/JsonArrayIteratorTest.java | 16 +- .../test/resources/array_from_changes.json | 608 ++++++++++++++++++ .../test/resources/incomplete_changes.json | 31 + .../test/util/jackson3/Jackson3TestUtil.java | 10 +- 8 files changed, 691 insertions(+), 30 deletions(-) create mode 100644 vpro-shared-jackson3/src/test/resources/array_from_changes.json create mode 100644 vpro-shared-jackson3/src/test/resources/incomplete_changes.json diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/Jackson3Mapper.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/Jackson3Mapper.java index ba9fe4772..3cdb61b96 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/Jackson3Mapper.java +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/Jackson3Mapper.java @@ -8,6 +8,7 @@ import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import tools.jackson.core.JacksonException; +import tools.jackson.core.StreamReadFeature; import tools.jackson.databind.*; import tools.jackson.databind.cfg.EnumFeature; import tools.jackson.databind.introspect.AnnotationIntrospectorPair; @@ -249,7 +250,7 @@ public static void configureMapper(Jackson3Mapper.Builder builder, Predicate v.withContentInclusion(JsonInclude.Include.NON_EMPTY)); + builder.mapperBuilder.changeDefaultPropertyInclusion(v -> v.withValueInclusion(JsonInclude.Include.NON_EMPTY)); builder.mapperBuilder.annotationIntrospector(introspector); builder.mapperBuilder.disable(FAIL_ON_UNKNOWN_PROPERTIES); // This seems a good idea when reading from couchdb or so, but when reading user supplied forms, it is confusing not getting errors. @@ -358,7 +359,7 @@ public ObjectReader readerFor(Class clazz) { } public Builder rebuild() { - Builder builder = builder(toString); + Builder builder = builder(toString + "'"); builder.serializationView(writer.getConfig().getActiveView()); builder.deserializationView(reader.getConfig().getActiveView()); builder.mapperBuilder = mapper.rebuild(); @@ -370,6 +371,16 @@ public static Builder builder(String toString) { .toString(toString); } + public Jackson3Mapper withSourceInLocation() { + return rebuild() + .toString(toString + " with source in location") + .configure(b -> + b.enable(StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION) + ).build(); + } + + + public static class Builder { private JsonMapper.Builder mapperBuilder = JsonMapper .builder(); diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/JsonArrayIterator.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/JsonArrayIterator.java index ccb428b76..771848f1f 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/JsonArrayIterator.java +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/JsonArrayIterator.java @@ -222,15 +222,19 @@ protected void findNext() { if(needsFindNext) { while(true) { try { - var lastToken = jp.getLastClearedToken(); - TreeNode tree = jp.readValueAsTree(); - var newLastToken = jp.getLastClearedToken(); - assert lastToken != newLastToken; - this.eventListener.accept(new TokenEvent(newLastToken)); - - this.eventListener.accept(new TokenEvent(jp.getLastClearedToken())); + var currentToken = jp.currentToken(); + TreeNode tree; + if (currentToken == JsonToken.START_OBJECT) { + tree = jp.readValueAsTree(); + } else { + tree = null; + log.debug("Expected START_OBJECT token but got {}", currentToken); + } + jp.nextToken(); + var lastToken = jp.currentToken(); - if (jp.getLastClearedToken() == JsonToken.END_ARRAY) { + this.eventListener.accept(new TokenEvent(lastToken)); + if (lastToken == JsonToken.END_ARRAY) { tree = null; } else { if (tree instanceof NullNode && skipNulls) { diff --git a/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/GuavaRangeModuleTest.java b/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/GuavaRangeModuleTest.java index b5681ede8..2097c6430 100644 --- a/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/GuavaRangeModuleTest.java +++ b/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/GuavaRangeModuleTest.java @@ -6,7 +6,10 @@ import java.time.Instant; +import org.json.JSONException; import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import org.skyscreamer.jsonassert.JSONCompareMode; import com.fasterxml.jackson.annotation.JsonProperty; import com.google.common.collect.Range; @@ -38,13 +41,14 @@ static class WithInstantRange { @Test - public void without() throws JacksonException { + public void without() throws JacksonException, JSONException { WithoutSerializer a = new WithoutSerializer(); a.range = Range.closedOpen(1, 2); - String example = "{\"range\":{\"lowerEndpoint\":1,\"lowerBoundType\":\"CLOSED\",\"upperEndpoint\":2,\"upperBoundType\":\"OPEN\",\"type\":\"java.lang.Integer\"},\"anotherField\":1}"; - assertThat(mapper.writeValueAsString(a)).isEqualTo(example); + String expected = "{\"range\":{\"lowerEndpoint\":1,\"lowerBoundType\":\"CLOSED\",\"upperEndpoint\":2,\"upperBoundType\":\"OPEN\",\"type\":\"java.lang.Integer\"},\"anotherField\":1}"; + String result = mapper.writeValueAsString(a); + JSONAssert.assertEquals(result + "\nis different from expected\n" + expected, expected, result, JSONCompareMode.STRICT); - WithoutSerializer ab = mapper.readValue(example, WithoutSerializer.class); + WithoutSerializer ab = mapper.readValue(expected, WithoutSerializer.class); assertThat(ab.range).isEqualTo(a.range); } diff --git a/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/Jackson3MapperTest.java b/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/Jackson3MapperTest.java index 0e9434905..c4b64aefd 100644 --- a/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/Jackson3MapperTest.java +++ b/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/Jackson3MapperTest.java @@ -17,6 +17,8 @@ import org.junit.jupiter.api.Test; +import com.fasterxml.jackson.annotation.JsonInclude; + import nl.vpro.jackson.Views; import static org.assertj.core.api.Assertions.assertThat; @@ -51,10 +53,13 @@ public static class A { public void basicJackson3() { JsonMapper mapper = JsonMapper.builder() .enable(MapperFeature.DEFAULT_VIEW_INCLUSION) + .annotationIntrospector(new AnnotationIntrospectorPair( new JacksonAnnotationIntrospector(), new JakartaXmlBindAnnotationIntrospector(false) - )).build(); + )) + .changeDefaultPropertyInclusion(v -> v.withValueInclusion(JsonInclude.Include.NON_EMPTY)) + .build(); A a = mapper.readerWithView(Views.Normal.class).forType(A.class) .readValue(""" @@ -62,6 +67,8 @@ public void basicJackson3() { """); assertThat(a.integer).isEqualTo(2); assertThat(a.optional).contains(3); + + assertThat(mapper.writer().writeValueAsString(a)).isEqualTo("{\"integer\":2,\"optional\":3}"); } diff --git a/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/JsonArrayIteratorTest.java b/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/JsonArrayIteratorTest.java index 73cf3c22c..fa135079d 100644 --- a/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/JsonArrayIteratorTest.java +++ b/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/JsonArrayIteratorTest.java @@ -27,6 +27,7 @@ public class JsonArrayIteratorTest { @Test public void simple() { for (Change change : JsonArrayIterator.builder(Change.class) + .objectMapper(Jackson3Mapper.INSTANCE.withSourceInLocation()) .inputStream( new ByteArrayInputStream(""" { @@ -38,7 +39,7 @@ public void simple() { "mid": "POMS_NCRV_1138990", "deleted": true } - } + ] } """.getBytes(StandardCharsets.UTF_8)))) { log.info("{}", change); @@ -47,13 +48,13 @@ public void simple() { } @Test - public void changes() throws IOException { + public void changes() { //Jackson2Mapper.getInstance().writeValue(System.out, new Change("bla", false)); try (JsonArrayIterator it = JsonArrayIterator.builder() .inputStream(getClass().getResourceAsStream("/changes.json")) .valueClass(Change.class) - .objectMapper(Jackson3Mapper.INSTANCE) + .objectMapper(Jackson3Mapper.INSTANCE.withSourceInLocation()) .build()) { assertThat(it.next().getMid()).isEqualTo("POMS_NCRV_1138990"); // 1 assertThat(it.getCount()).isEqualTo(1); @@ -108,7 +109,7 @@ public void testNulls() throws IOException { } @Test - public void testIncompleteJson() throws IOException { + public void testIncompleteJson() { InputStream input = getClass().getResourceAsStream("/incomplete_changes.json"); assert input != null; try (JsonArrayIterator it = JsonArrayIterator.builder() @@ -143,7 +144,8 @@ public void callback() throws IOException { verify(callback, times(0)).run(); it.next(); } - assertThat(it.getSize()).hasValueSatisfying(size -> assertThat(size).isEqualTo(it.getCount())); + assertThat(it.getSize()) + .hasValueSatisfying(size -> assertThat(size).isEqualTo(it.getCount())); verify(callback, times(1)).run(); } @@ -192,7 +194,7 @@ public void illegalConstruction() { try (JsonArrayIterator js = JsonArrayIterator.builder().build()) { log.info("{}", js); } - }).isInstanceOf(IllegalArgumentException.class); + }).isInstanceOf(NullPointerException.class); assertThatThrownBy(() -> { try (JsonArrayIterator js = JsonArrayIterator @@ -209,7 +211,7 @@ public void illegalConstruction() { @Test - public void interrupt() throws IOException { + public void interrupt() { byte[] bytes = "[{},{},{},{},{},{}]".getBytes(StandardCharsets.UTF_8); final String[] callback = new String[1]; try (JsonArrayIterator i = JsonArrayIterator.builder() diff --git a/vpro-shared-jackson3/src/test/resources/array_from_changes.json b/vpro-shared-jackson3/src/test/resources/array_from_changes.json new file mode 100644 index 000000000..334bab23f --- /dev/null +++ b/vpro-shared-jackson3/src/test/resources/array_from_changes.json @@ -0,0 +1,608 @@ +[{ + "mid" : "POMS_NCRV_1138990", + "deleted" : true + }, null, { + "mid" : "POMS_AVRO_1138559", + "deleted" : true + }, { + "mid" : "POMS_AVRO_1138560", + "deleted" : true + }, { + "mid" : "POMS_VPRO_1139774", + "deleted" : false, + "media" : { + "objectType" : "program", + "mid" : "POMS_VPRO_1139774", + "creationDate" : 1396061002808, + "lastModified" : 1398759143568, + "sortDate" : 1398722400000, + "urn" : "urn:vpro:media:program:39326270", + "embeddable" : true, + "descriptions" : [ 1 ] + } + }, { + "mid" : "POMS_VPRO_1139774", + "deleted" : false, + "media" : { + "objectType" : "program", + "mid" : "POMS_VPRO_1139774", + "creationDate" : 1396061002808, + "lastModified" : 1398759143568, + "sortDate" : 1398722400000, + "urn" : "urn:vpro:media:program:39326270", + "embeddable" : true, + "episodeOf" : [ { + "midRef" : "POMS_S_VPRO_449041", + "urnRef" : "urn:vpro:media:group:33644950", + "type" : "SERIES", + "index" : 1, + "highlighted" : false, + "added" : 1396061004445 + } ], + "crids" : [ "crid://broadcast.radiobox2/278049" ], + "broadcasters" : [ { + "id" : "VPRO", + "value" : "VPRO" + } ], + "titles" : [ { + "value" : "Nooit Meer Slapen", + "owner" : "RADIOBOX", + "type" : "MAIN" + } ], + "descriptions" : [ { + "value" : "Actrice, columniste en schrijfster Tosca Niterink is in het eerste uur te gast om te vertellen over haar boek 'De Vergeetclub'. En Jungle by Night presenteerde afgelopen weekend hun tweede plaat 'The Hunt', en trapte een internationale tournee af. Botte Jellema bezocht het optreden en sprak daar met de band. Verder reageert schrijfster Saskia de Coster met fictie op het nieuws van de dag en correspondent Eva de Valk bericht vanuit San Francisco over de muurschilderingen in Mission District. Ook onderzoeken we het fenomeen 'indie games' met journalist Niels 't Hooft en game-ontwikkelaar Rami Ismail.", + "owner" : "RADIOBOX", + "type" : "MAIN" + } ], + "duration" : 7200000, + "descendantOf" : [ { + "midRef" : "POMS_S_VPRO_449041", + "urnRef" : "urn:vpro:media:group:33644950", + "type" : "SERIES" + } ], + "email" : [ "nooitmeerslapen@vpro.nl" ], + "websites" : [ { + "value" : "http://www.radio1.nl/nooitmeerslapen" + } ], + "locations" : [ { + "programUrl" : "http://download.omroep.nl/audiologging/r1/2014/04/29/0000_0200_nooit_meer_slapen_20140429.mp3", + "avAttributes" : { + "avFileFormat" : "MP3" + }, + "duration" : 7200000, + "creationDate" : 1398759137893, + "lastModified" : 1398759143567, + "workflow" : "PUBLISHED", + "owner" : "RADIOBOX", + "urn" : "urn:vpro:media:location:40652422" + } ], + "scheduleEvents" : [ { + "start" : 1398722400000, + "duration" : 7200000, + "poProgID" : "POMS_VPRO_1139774", + "channel" : "RAD1", + "urnRef" : "urn:vpro:media:program:39326270", + "midRef" : "POMS_VPRO_1139774" + } ], + "images" : [ { + "title" : "nms_itunes_profiel.jpg", + "description" : "Nooit Meer Slapen", + "imageUri" : "urn:vpro:image:226081", + "creationDate" : 1398757654108, + "lastModified" : 1398757656461, + "workflow" : "PUBLISHED", + "owner" : "RADIOBOX", + "type" : "PICTURE", + "highlighted" : false, + "urn" : "urn:vpro:media:image:40644427" + } ], + "workflow" : "PUBLISHED", + "type" : "BROADCAST", + "avType" : "AUDIO" + } + }, { + "mid" : "POMS_VPRO_1139787", + "deleted" : false, + "media" : { + "objectType" : "program", + "mid" : "POMS_VPRO_1139787", + "creationDate" : 1396061002808, + "lastModified" : 1398760426214, + "sortDate" : 1398722400000, + "urn" : "urn:vpro:media:program:39326270", + "embeddable" : true, + "episodeOf" : [ { + "midRef" : "POMS_S_VPRO_449041", + "urnRef" : "urn:vpro:media:group:33644950", + "type" : "SERIES", + "index" : 1, + "highlighted" : false, + "added" : 1396061004445 + } ], + "crids" : [ "crid://broadcast.radiobox2/278049" ], + "broadcasters" : [ { + "id" : "VPRO", + "value" : "VPRO" + } ], + "titles" : [ { + "value" : "Nooit Meer Slapen", + "owner" : "RADIOBOX", + "type" : "MAIN" + } ], + "descriptions" : [ { + "value" : "Actrice, columniste en schrijfster Tosca Niterink is in het eerste uur te gast om te vertellen over haar boek 'De Vergeetclub'. En Jungle by Night presenteerde afgelopen weekend hun tweede plaat 'The Hunt', en trapte een internationale tournee af. Botte Jellema bezocht het optreden en sprak daar met de band. Verder reageert schrijfster Saskia de Coster met fictie op het nieuws van de dag en correspondent Eva de Valk bericht vanuit San Francisco over de muurschilderingen in Mission District. Ook onderzoeken we het fenomeen 'indie games' met journalist Niels 't Hooft en game-ontwikkelaar Rami Ismail.", + "owner" : "RADIOBOX", + "type" : "MAIN" + } ], + "duration" : 7200000, + "descendantOf" : [ { + "midRef" : "POMS_S_VPRO_449041", + "urnRef" : "urn:vpro:media:group:33644950", + "type" : "SERIES" + } ], + "email" : [ "nooitmeerslapen@vpro.nl" ], + "websites" : [ { + "value" : "http://www.radio1.nl/nooitmeerslapen" + } ], + "locations" : [ { + "programUrl" : "http://download.omroep.nl/audiologging/r1/2014/04/29/0000_0200_nooit_meer_slapen_20140429.mp3", + "avAttributes" : { + "avFileFormat" : "MP3" + }, + "duration" : 7200000, + "creationDate" : 1398759137893, + "lastModified" : 1398759143567, + "workflow" : "PUBLISHED", + "owner" : "RADIOBOX", + "urn" : "urn:vpro:media:location:40652422" + } ], + "scheduleEvents" : [ { + "start" : 1398722400000, + "duration" : 7200000, + "poProgID" : "POMS_VPRO_1139787", + "channel" : "RAD1", + "urnRef" : "urn:vpro:media:program:39326270", + "midRef" : "POMS_VPRO_1139787" + } ], + "images" : [ { + "title" : "nms_itunes_profiel.jpg", + "description" : "Nooit Meer Slapen", + "imageUri" : "urn:vpro:image:226081", + "creationDate" : 1398757654108, + "lastModified" : 1398757656461, + "workflow" : "PUBLISHED", + "owner" : "RADIOBOX", + "type" : "PICTURE", + "highlighted" : false, + "urn" : "urn:vpro:media:image:40644427" + } ], + "workflow" : "PUBLISHED", + "segments" : [ { + "objectType" : "segment", + "mid" : "POMS_VPRO_1139788", + "creationDate" : 1398760359009, + "lastModified" : 1398760426214, + "sortDate" : 1398722400000, + "urn" : "urn:vpro:media:segment:40658898", + "embeddable" : true, + "crids" : [ "crid://fragment.radiobox2/142297" ], + "broadcasters" : [ { + "id" : "VPRO", + "value" : "VPRO" + } ], + "titles" : [ { + "value" : "Tosca Niterink's nieuwe boek: De Vergeetclub", + "owner" : "RADIOBOX", + "type" : "MAIN" + } ], + "descriptions" : [ { + "value" : "De dementerende moeder van Tosca Niterink doet samen met acht andere dames op leeftijd aan kleinschalig wonen achter een cijferslot, voor hun eigen veiligheid. In het boek 'De Vergeetclub' beschrijft de actrice, die vooral bekend werd door haar aandeel in 'Theo en Thea', 'Kreatief met Kurk' en 'Borreltijd', met oog voor het absurde, de uitgesproken karakters van de dames en de verwikkelingen die daaruit voortkomen. Actrice, columniste en schrijfster Tosca Niterink vertelt over haar boek.", + "owner" : "RADIOBOX", + "type" : "MAIN" + } ], + "duration" : 3365000, + "descendantOf" : [ { + "midRef" : "POMS_S_VPRO_449041", + "urnRef" : "urn:vpro:media:group:33644950", + "type" : "SERIES" + }, { + "midRef" : "POMS_VPRO_1139787", + "urnRef" : "urn:vpro:media:program:39326270", + "type" : "BROADCAST" + } ], + "locations" : [ { + "programUrl" : "http://radiobox2.omroep.nl/audiofragment/file/142297/fragment.mp3", + "avAttributes" : { + "avFileFormat" : "MP3" + }, + "duration" : 3365000, + "creationDate" : 1398760346008, + "lastModified" : 1398760359138, + "workflow" : "PUBLISHED", + "owner" : "RADIOBOX", + "urn" : "urn:vpro:media:location:40658902" + } ], + "images" : [ { + "title" : "devergeetclub.jpg", + "description" : "Tosca Niterink's nieuwe boek: De Vergeetclub", + "imageUri" : "urn:vpro:image:251307", + "creationDate" : 1398760357411, + "lastModified" : 1398760359137, + "workflow" : "PUBLISHED", + "owner" : "RADIOBOX", + "type" : "PICTURE", + "highlighted" : false, + "urn" : "urn:vpro:media:image:40658900" + } ], + "workflow" : "PUBLISHED", + "start" : 159000, + "urnRef" : "urn:vpro:media:program:39326270", + "midRef" : "POMS_VPRO_1139787", + "type" : "SEGMENT", + "avType" : "AUDIO" + }, { + "objectType" : "segment", + "mid" : "POMS_VPRO_1139789", + "creationDate" : 1398760397576, + "lastModified" : 1398760426214, + "sortDate" : 1398722400000, + "urn" : "urn:vpro:media:segment:40658921", + "embeddable" : true, + "crids" : [ "crid://fragment.radiobox2/142299" ], + "broadcasters" : [ { + "id" : "VPRO", + "value" : "VPRO" + } ], + "titles" : [ { + "value" : "De kleinschalige kant van de game industrie: 'indie games'", + "owner" : "RADIOBOX", + "type" : "MAIN" + } ], + "descriptions" : [ { + "value" : "Videogames zijn een miljardenindustrie geworden. Maar deze industrie heeft ook een meer kleinschalige kant: 'indie games'. Nederland is sterk vertegenwoordigd in dit genre. Het zijn video games die door individuen of kleine teams worden ontwikkeld, meestal zonder financiële steun van een uitgever. We onderzoeken het fenomeen met journalist Niels 't Hooft en game-ontwikkelaar Rami Ismail.", + "owner" : "RADIOBOX", + "type" : "MAIN" + } ], + "duration" : 971000, + "descendantOf" : [ { + "midRef" : "POMS_S_VPRO_449041", + "urnRef" : "urn:vpro:media:group:33644950", + "type" : "SERIES" + }, { + "midRef" : "POMS_VPRO_1139787", + "urnRef" : "urn:vpro:media:program:39326270", + "type" : "BROADCAST" + } ], + "locations" : [ { + "programUrl" : "http://radiobox2.omroep.nl/audiofragment/file/142299/fragment.mp3", + "avAttributes" : { + "avFileFormat" : "MP3" + }, + "duration" : 971000, + "creationDate" : 1398760371060, + "lastModified" : 1398760397659, + "workflow" : "PUBLISHED", + "owner" : "RADIOBOX", + "urn" : "urn:vpro:media:location:40658925" + } ], + "images" : [ { + "title" : "indiegames.jpg", + "description" : "De kleinschalige kant van de game industrie: 'indie games'", + "imageUri" : "urn:vpro:image:251308", + "creationDate" : 1398760386849, + "lastModified" : 1398760397658, + "workflow" : "PUBLISHED", + "owner" : "RADIOBOX", + "type" : "PICTURE", + "highlighted" : false, + "urn" : "urn:vpro:media:image:40658923" + } ], + "workflow" : "PUBLISHED", + "start" : 4336000, + "urnRef" : "urn:vpro:media:program:39326270", + "midRef" : "POMS_VPRO_1139787", + "type" : "SEGMENT", + "avType" : "AUDIO" + }, { + "objectType" : "segment", + "mid" : "POMS_VPRO_1139790", + "creationDate" : 1398760397577, + "lastModified" : 1398760426214, + "sortDate" : 1398722400000, + "urn" : "urn:vpro:media:segment:40658927", + "embeddable" : true, + "crids" : [ "crid://fragment.radiobox2/142298" ], + "broadcasters" : [ { + "id" : "VPRO", + "value" : "VPRO" + } ], + "titles" : [ { + "value" : "Jungle by Night's tweede album The Hunt", + "owner" : "RADIOBOX", + "type" : "MAIN" + } ], + "descriptions" : [ { + "value" : "Negen Amsterdamse jongens bestormden drie jaar geleden alle festivalpodia van Nederland en ver daarbuiten. Ze waren een opvallend succes met hun Afrobeat. Jungle by Night presenteerde afgelopen weekend hun tweede plaat, The Hunt, en trapte een internationale tournee af. Botte Jellema bezocht het optreden en sprak daar met de band.", + "owner" : "RADIOBOX", + "type" : "MAIN" + } ], + "duration" : 921000, + "descendantOf" : [ { + "midRef" : "POMS_S_VPRO_449041", + "urnRef" : "urn:vpro:media:group:33644950", + "type" : "SERIES" + }, { + "midRef" : "POMS_VPRO_1139787", + "urnRef" : "urn:vpro:media:program:39326270", + "type" : "BROADCAST" + } ], + "locations" : [ { + "programUrl" : "http://radiobox2.omroep.nl/audiofragment/file/142298/fragment.mp3", + "avAttributes" : { + "avFileFormat" : "MP3" + }, + "duration" : 921000, + "creationDate" : 1398760371060, + "lastModified" : 1398760397662, + "workflow" : "PUBLISHED", + "owner" : "RADIOBOX", + "urn" : "urn:vpro:media:location:40658931" + } ], + "images" : [ { + "title" : "junglebynight.jpg", + "description" : "Jungle by Night's tweede album The Hunt", + "imageUri" : "urn:vpro:image:251309", + "creationDate" : 1398760395326, + "lastModified" : 1398760397661, + "workflow" : "PUBLISHED", + "owner" : "RADIOBOX", + "type" : "PICTURE", + "highlighted" : false, + "urn" : "urn:vpro:media:image:40658929" + } ], + "workflow" : "PUBLISHED", + "start" : 6230000, + "urnRef" : "urn:vpro:media:program:39326270", + "midRef" : "POMS_VPRO_1139787", + "type" : "SEGMENT", + "avType" : "AUDIO" + } ], + "type" : "BROADCAST", + "avType" : "AUDIO" + } + }, { + "mid" : "POW_00039706", + "deleted" : false, + "media" : { + "objectType" : "program", + "mid" : "POW_00039706", + "creationDate" : 1279283049485, + "lastModified" : 1398764908532, + "sortDate" : 1140732900000, + "urn" : "urn:vpro:media:program:3030437", + "embeddable" : true, + "episodeOf" : [ { + "midRef" : "POW_00039674", + "urnRef" : "urn:vpro:media:group:19690815", + "type" : "SEASON", + "index" : 3, + "highlighted" : false, + "added" : 1359627207372 + } ], + "crids" : [ "crid://tmp.program.omroep.nl/2621725" ], + "broadcasters" : [ { + "id" : "VPRO", + "value" : "VPRO" + }, { + "id" : "IKON", + "value" : "IKON" + } ], + "titles" : [ { + "value" : "De Donderdag Documentaire", + "owner" : "BROADCASTER", + "type" : "MAIN" + }, { + "value" : "De Donderdag Documentaire: Dorpsbelangen", + "owner" : "NEBO", + "type" : "MAIN" + }, { + "value" : "Dorpsbelangen", + "owner" : "BROADCASTER", + "type" : "SUB" + } ], + "descriptions" : [ { + "value" : "In de gemeentelijke politiek gelden andere regels en wetten dan in de landelijke. Aan de vooravond van de gemeenteraadsverkiezingen in 2006 portretteert filmmaakster Marieke van der Winden vier gemeenteraadsleden in Maasdriel. ", + "owner" : "BROADCASTER", + "type" : "MAIN" + }, { + "value" : "Documentaire aan de vooravond van de gemeenteraadsverkiezingen van 2006. 'Als ik hier de baas was, zou ik de boel al lang hebben volgebouwd! We moeten niet van heel Nederland een natuurgebied willen maken.' Eric van der Veen, gemeenteraadslid van Maasdriel, zit op de bank bij collega-gemeenteraadslid Ko Hooijmans. Het uitzicht is adembenemend: natuur zo ver je kijken kunt. Hooijmans haalt wat gegeneerd zijn schouders op. Gaat zijn collega niet wat ver voor het oog van de camera? In de gemeentelijke politiek gelden andere regels en wetten dan in de landelijke. Filmmaakster Marieke van der Winden dompelde zich een jaar lang onder in de gemeentelijke politiek van Maasdriel en portretteerde vier gemeenteraadsleden. Ondernemer Eric van der Veen heeft als fractievoorzitter van Gemeentebelangen Maasdriel een flinke vinger in de pap van de plaatselijke politiek. Hij is onder meer eigenaar van de apparatuur van de plaatselijke omroep. Zijn nieuwste plan is de plaatsing van een zendmast. Hij realiseert zich maar al te goed hoe belangrijk de media zijn voor zijn positie. Van der Veen is een echte lobbyist en netwerker. Net als veel andere gemeenteraadsleden én de burgemeester doet hij bij de plaatselijke kapper de laatste dorpsnieuwtjes en -roddels op. Aannemer Jan van Boxtel van het CDA weet heel goed hoe hij zijn achterban aan zich kan binden: via de kerk. In het parochiebestuur én in de gemeenteraad beijvert hij zich onder meer voor het onderhoud van de kerk en andere monumenten. Daar vloeien vaak ook weer leuke opdrachten uit voort. Daarnaast doet hij, samen met zijn vrouw, goede werken zoals het rondbrengen van warme maaltijden. Het levert deze man van geen woorden maar daden veel goodwill én stemmen op. Naast de bouwsector is de agrarische sector een belangrijke economische tak van sport in dit gebied. Ook Ko Hooijmans van de VVD verdient er als champignonkweker een goede boterham in. Op de plek van het huis van zijn ouders heeft hij onlangs een villa laten neerzetten. Hooijmans laat", + "owner" : "NEBO", + "type" : "MAIN" + }, { + "value" : "In de gemeentelijke politiek gelden andere regels en wetten dan in de landelijke. Aan de vooravond van de gemeenteraadsverkiezingen in 2006 portretteert filmmaakster Marieke van der Winden vier gemeenteraadsleden in Maasdriel. ", + "owner" : "BROADCASTER", + "type" : "SHORT" + }, { + "value" : "Documentaire over een jaar lang gemeentelijke politiek in Maasdriel. Met portretten van vier gemeenteraadsleden. Hier gelden andere, meer persoonlijke belangen. Als je een invloedrijke familie hebt, eigenaar bent van een bedrijf of aan het hoofd staat van de kerk, heb je een streepje voor.", + "owner" : "NEBO", + "type" : "SHORT" + }, { + "value" : "Titel: Dorpsbelangen - Documentaire aan de vooravond van de gemeenteraadsverkiezingen van 2006. 'Als ik hier de baas was, zou ik de boel al lang hebben volgebouwd! We moeten niet van heel Nederland een natuurgebied willen maken.' Eric van der Veen, gemeenteraadslid van Maasdriel, zit op de bank bij collega-gemeenteraadslid Ko Hooijmans. Het uitzicht is adembenemend: natuur zo ver je kijken kunt. Hooijmans haalt wat gegeneerd zijn schouders op. Gaat zijn collega niet wat ver voor het oog van de camera? In de gemeentelijke politiek gelden andere regels en wetten dan in de landelijke. Filmmaakster Marieke van der Winden dompelde zich een jaar lang onder in de gemeentelijke politiek van Maasdriel en portretteerde vier gemeenteraadsleden.", + "owner" : "BROADCASTER", + "type" : "EPISODE" + }, { + "value" : "Documentaire aan de vooravond van de gemeenteraadsverkiezingen van 2006. 'Als ik hier de baas was, zou ik de boel al lang hebben volgebouwd! We moeten niet van heel Nederland een natuurgebied willen maken.' Eric van der Veen, gemeenteraadslid van Maasdriel, zit op de bank bij collega-gemeenteraadslid Ko Hooijmans. Het uitzicht is adembenemend: natuur zo ver je kijken kunt. Hooijmans haalt wat gegeneerd zijn schouders op. Gaat zijn collega niet wat ver voor het oog van de camera? In de gemeentelijke politiek gelden andere regels en wetten dan in de landelijke. Filmmaakster Marieke van der Winden dompelde zich een jaar lang onder in de gemeentelijke politiek van Maasdriel en portretteerde vier gemeenteraadsleden. Ondernemer Eric van der Veen heeft als fractievoorzitter van Gemeentebelangen Maasdriel een flinke vinger in de pap van de plaatselijke politiek. Hij is onder meer eigenaar van de apparatuur van de plaatselijke omroep. Zijn nieuwste plan is de plaatsing van een zendmast. Hij realiseert zich maar al te goed hoe belangrijk de media zijn voor zijn positie. Van der Veen is een echte lobbyist en netwerker. Net als veel andere gemeenteraadsleden én de burgemeester doet hij bij de plaatselijke kapper de laatste dorpsnieuwtjes en -roddels op. Aannemer Jan van Boxtel van het CDA weet heel goed hoe hij zijn achterban aan zich kan binden: via de kerk. In het parochiebestuur én in de gemeenteraad beijvert hij zich onder meer voor het onderhoud van de kerk en andere monumenten. Daar vloeien vaak ook weer leuke opdrachten uit voort. Daarnaast doet hij, samen met zijn vrouw, goede werken zoals het rondbrengen van warme maaltijden. Het levert deze man van geen woorden maar daden veel goodwill én stemmen op. Naast de bouwsector is de agrarische sector een belangrijke economische tak van sport in dit gebied. Ook Ko Hooijmans van de VVD verdient er als champignonkweker een goede boterham in. Op de plek van het huis van zijn ouders heeft hij onlangs een villa laten neerzetten. Hooijmans laat Regie: Marieke van der Winden.", + "owner" : "NEBO", + "type" : "EPISODE" + } ], + "genres" : [ { + "id" : "3.0.1.8", + "terms" : [ "Documentaire" ] + } ], + "avAttributes" : { + "videoAttributes" : { + "aspectRatio" : "4:3" + } + }, + "duration" : 3300000, + "descendantOf" : [ { + "midRef" : "POW_00039674", + "urnRef" : "urn:vpro:media:group:19690815", + "type" : "SEASON" + }, { + "midRef" : "POMS_S_HUMAN_105498", + "urnRef" : "urn:vpro:media:group:3029741", + "type" : "SERIES" + } ], + "ageRating" : "ALL", + "predictions" : [ { + "state" : "REALIZED", + "publishStart" : 1136070000000, + "platform" : "INTERNETVOD" + } ], + "locations" : [ { + "programUrl" : "http://cgi.omroep.nl/cgi-bin/streams?/tv/ikon/dedonderdagdocumentaire/sb.20060223.asf", + "avAttributes" : { + "bitrate" : 204800, + "avFileFormat" : "WM", + "videoAttributes" : { + "videoCoding" : "WM", + "aspectRatio" : "4:3" + } + }, + "creationDate" : 1279283049534, + "lastModified" : 1359627207340, + "publishStart" : 1136070000000, + "workflow" : "PUBLISHED", + "owner" : "NEBO", + "urn" : "urn:vpro:media:location:3030448" + }, { + "programUrl" : "http://cgi.omroep.nl/cgi-bin/streams?/tv/ikon/dedonderdagdocumentaire/bb.20060223.asf", + "avAttributes" : { + "bitrate" : 512000, + "avFileFormat" : "WM", + "videoAttributes" : { + "videoCoding" : "WM", + "aspectRatio" : "4:3" + } + }, + "creationDate" : 1279283049546, + "lastModified" : 1359627207340, + "publishStart" : 1136070000000, + "workflow" : "PUBLISHED", + "owner" : "NEBO", + "urn" : "urn:vpro:media:location:3030451" + } ], + "scheduleEvents" : [ { + "avAttributes" : { + "bitrate" : 0, + "avFileFormat" : "WM", + "videoAttributes" : { + "videoCoding" : "WM", + "aspectRatio" : "4:3" + } + }, + "textSubtitles" : "Teletekst ondertitels", + "start" : 1140732900000, + "duration" : 3300000, + "poProgID" : "POW_00039706", + "channel" : "NED1", + "urnRef" : "urn:vpro:media:program:3030437", + "midRef" : "POW_00039706" + } ], + "images" : [ { + "title" : "Dorpsbelangen", + "imageUri" : "urn:vpro.image:7012", + "height" : 266, + "width" : 400, + "creationDate" : 1279800000433, + "lastModified" : 1306252549650, + "workflow" : "PUBLISHED", + "owner" : "BROADCASTER", + "type" : "PICTURE", + "highlighted" : false, + "urn" : "urn:vpro:media:image:3213689" + }, { + "title" : "Dorpsbelangen", + "description" : "In de gemeentelijke politiek gelden andere regels en wetten dan in de landelijke. Aan de vooravond van de gemeenteraadsverkiezingen in 2006 portretteert filmmaakster Marieke van der Winden vier gemeenteraadsleden in Maasdriel. ", + "imageUri" : "urn:vpro:image:70792", + "creationDate" : 1335362453307, + "lastModified" : 1335362453790, + "workflow" : "PUBLISHED", + "owner" : "NEBO", + "type" : "STILL", + "highlighted" : false, + "urn" : "urn:vpro:media:image:13886727" + }, { + "title" : "Dorpsbelangen", + "description" : "In de gemeentelijke politiek gelden andere regels en wetten dan in de landelijke. Aan de vooravond van de gemeenteraadsverkiezingen in 2006 portretteert filmmaakster Marieke van der Winden vier gemeenteraadsleden in Maasdriel. ", + "imageUri" : "urn:vpro:image:70793", + "creationDate" : 1335362453536, + "lastModified" : 1335362453790, + "workflow" : "PUBLISHED", + "owner" : "NEBO", + "type" : "STILL", + "highlighted" : false, + "urn" : "urn:vpro:media:image:13886728" + }, { + "title" : "Dorpsbelangen", + "description" : "In de gemeentelijke politiek gelden andere regels en wetten dan in de landelijke. Aan de vooravond van de gemeenteraadsverkiezingen in 2006 portretteert filmmaakster Marieke van der Winden vier gemeenteraadsleden in Maasdriel. ", + "imageUri" : "urn:vpro:image:70794", + "creationDate" : 1335362453788, + "lastModified" : 1335362453791, + "workflow" : "PUBLISHED", + "owner" : "NEBO", + "type" : "STILL", + "highlighted" : false, + "urn" : "urn:vpro:media:image:13886729" + }, { + "title" : "De Donderdag Documentaire", + "description" : "In de gemeentelijke politiek gelden andere regels en wetten dan in de landelijke. Aan de vooravond van de gemeenteraadsverkiezingen in 2006 portretteert filmmaakster Marieke van der Winden vier gemeenteraadsleden in Maasdriel. ", + "imageUri" : "urn:vpro:image:328108", + "creationDate" : 1389030464312, + "lastModified" : 1389030498441, + "workflow" : "PUBLISHED", + "owner" : "NEBO", + "type" : "STILL", + "highlighted" : false, + "urn" : "urn:vpro:media:image:35057598" + }, { + "title" : "De Donderdag Documentaire", + "description" : "In de gemeentelijke politiek gelden andere regels en wetten dan in de landelijke. Aan de vooravond van de gemeenteraadsverkiezingen in 2006 portretteert filmmaakster Marieke van der Winden vier gemeenteraadsleden in Maasdriel. ", + "imageUri" : "urn:vpro:image:328109", + "creationDate" : 1389030481417, + "lastModified" : 1389030498442, + "workflow" : "PUBLISHED", + "owner" : "NEBO", + "type" : "STILL", + "highlighted" : false, + "urn" : "urn:vpro:media:image:35057599" + }, { + "title" : "De Donderdag Documentaire", + "description" : "In de gemeentelijke politiek gelden andere regels en wetten dan in de landelijke. Aan de vooravond van de gemeenteraadsverkiezingen in 2006 portretteert filmmaakster Marieke van der Winden vier gemeenteraadsleden in Maasdriel. ", + "imageUri" : "urn:vpro:image:328110", + "creationDate" : 1389030498300, + "lastModified" : 1389030498443, + "workflow" : "PUBLISHED", + "owner" : "NEBO", + "type" : "STILL", + "highlighted" : false, + "urn" : "urn:vpro:media:image:35057600" + } ], + "workflow" : "PUBLISHED", + "type" : "BROADCAST", + "avType" : "VIDEO" + } + }, { + "mid" : "POMS_NCRV_1141275", + "deleted" : true + }, { + "mid" : "POMS_KRO_1140821", + "deleted" : true + }, { + "mid" : "POMS_EO_1140308", + "deleted" : true + }, { + "mid" : "POMS_VPRO_1139788", + "deleted" : true + }] diff --git a/vpro-shared-jackson3/src/test/resources/incomplete_changes.json b/vpro-shared-jackson3/src/test/resources/incomplete_changes.json new file mode 100644 index 000000000..e74082b53 --- /dev/null +++ b/vpro-shared-jackson3/src/test/resources/incomplete_changes.json @@ -0,0 +1,31 @@ +{ + "size": 14, + "changes": [ + { + "sequence": 724, + "revision": 2, + "mid": "POMS_NCRV_1138990", + "deleted": true + }, + "Een string", + null, + { + "sequence": 3660, + "revision": 2, + "mid": "POMS_AVRO_1138559", + "deleted": true + }, + "some error", + { + "sequence": 3661, + "revision": 2, + "mid": "POMS_AVRO_1138560", + "deleted": true + }, + { + "sequence": 4000, + "revision": 1, + "mid": "POMS_VPRO_1139774", + "deleted": false, + "media": { + "objectType": "program", \ No newline at end of file diff --git a/vpro-shared-test/src/main/java/nl/vpro/test/util/jackson3/Jackson3TestUtil.java b/vpro-shared-test/src/main/java/nl/vpro/test/util/jackson3/Jackson3TestUtil.java index 4db1fde74..4b26597f0 100644 --- a/vpro-shared-test/src/main/java/nl/vpro/test/util/jackson3/Jackson3TestUtil.java +++ b/vpro-shared-test/src/main/java/nl/vpro/test/util/jackson3/Jackson3TestUtil.java @@ -39,14 +39,8 @@ @Slf4j public class Jackson3TestUtil { - private static final Jackson3Mapper JACKSON_3_MAPPER = - Jackson3Mapper.PRETTY - .rebuild() - .configure(b -> - b.enable(StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION) - ) - .build(); - + private static final Jackson3Mapper JACKSON_3_MAPPER = Jackson3Mapper.PRETTY.withSourceInLocation(); + private static final ObjectReader READER = JACKSON_3_MAPPER.reader(); private static final ObjectMapper MAPPER = JACKSON_3_MAPPER.mapper(); From 4b3ad1d05c800004c701c0b4208ef84dc8dbfb5c Mon Sep 17 00:00:00 2001 From: Michiel Meeuwissen Date: Fri, 9 Jan 2026 13:55:54 +0100 Subject: [PATCH 13/18] Trying to fix test cases. --- .github/workflows/pull.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull.yml b/.github/workflows/pull.yml index f6ee73cfd..15f85808b 100644 --- a/.github/workflows/pull.yml +++ b/.github/workflows/pull.yml @@ -4,6 +4,7 @@ name: build pull request on: pull_request: branches: [main] + types: [opened, synchronize, reopened] concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} @@ -16,7 +17,6 @@ jobs: env: MAVEN_ARGS: '--no-transfer-progress' steps: - - uses: actions/checkout@v6 - name: Set up JDK uses: actions/setup-java@v5 From 59d58beb1acacd6cd135a0cc5ad69a8d2a67d2ef Mon Sep 17 00:00:00 2001 From: Michiel Meeuwissen Date: Fri, 9 Jan 2026 14:07:59 +0100 Subject: [PATCH 14/18] Upped some dependencies. --- pom.xml | 32 ++++++++++++++++---------------- vpro-shared-swagger3/pom.xml | 4 +--- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/pom.xml b/pom.xml index 8222c59dc..f25df23ba 100644 --- a/pom.xml +++ b/pom.xml @@ -22,8 +22,8 @@ 2.20.1 3.0.3 - 6.0.1 - 5.20.0 + 6.0.2 + 5.21.0 3.27.6 7.0.0.Final @@ -33,9 +33,9 @@ --> 3.6.1.Final - 1.18.1 + 1.18.3 2.0.17 - 2.25.2 + 2.25.3 33.5.0-jre 3.30.2-GA 2.3.2 @@ -43,7 +43,7 @@ 3.0 0.18.1 1.15 - 3.52.0 + 3.53.0 4.5.14 4.4.16 @@ -61,7 +61,7 @@ 9.0.1.Final 4.16.0 - 2.2.40 + 2.2.41 1.18.42 ${project.build.directory}/delombok @@ -73,10 +73,10 @@ 7.13.4 8.8.2 - 1.16.0 - 11.16.0 + 1.16.1 + 11.20.1 - 2.0.2 + 2.0.3 --add-opens java.base/java.lang=ALL-UNNAMED OVERRIDE @@ -472,17 +472,17 @@ org.apache.commons commons-lang3 - 3.19.0 + 3.20.0 org.apache.commons commons-text - 1.14.0 + 1.15.0 org.apache.commons commons-configuration2 - 2.12.0 + 2.13.0 io.github.natty-parser @@ -648,7 +648,7 @@ org.jsoup jsoup - 1.21.2 + 1.22.1 org.mockito @@ -724,7 +724,7 @@ org.wiremock wiremock - 3.13.1 + 3.13.2 test @@ -761,7 +761,7 @@ org.apache.groovy groovy - 5.0.2 + 5.0.3 org.javassist @@ -793,7 +793,7 @@ org.meeuw mihxil-json-grep - 0.13 + 1.0 org.ehcache diff --git a/vpro-shared-swagger3/pom.xml b/vpro-shared-swagger3/pom.xml index 4d14d2625..c60b958f8 100644 --- a/vpro-shared-swagger3/pom.xml +++ b/vpro-shared-swagger3/pom.xml @@ -10,10 +10,8 @@ vpro-shared-swagger3 vpro-shared-swagger3 - 5.14-SNAPSHOT - - 5.30.2 + 5.31.0 swagger From be848cd419679ea615c016ccf7be70768596c2a7 Mon Sep 17 00:00:00 2001 From: Michiel Meeuwissen Date: Fri, 9 Jan 2026 14:09:30 +0100 Subject: [PATCH 15/18] No need to do that for something else. --- .github/workflows/touch-javadocio.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/touch-javadocio.yml b/.github/workflows/touch-javadocio.yml index b8ffecf9f..636e1abcd 100644 --- a/.github/workflows/touch-javadocio.yml +++ b/.github/workflows/touch-javadocio.yml @@ -5,7 +5,7 @@ on: push: paths: - pom.xml - branches: ['**'] + branches: ['main', 'REL-*'] workflow_dispatch: From 3fcf53730078d616b85cee184087d98e0309b767 Mon Sep 17 00:00:00 2001 From: Michiel Meeuwissen Date: Fri, 9 Jan 2026 14:12:20 +0100 Subject: [PATCH 16/18] Test case failures. --- .../src/main/java/nl/vpro/jackson2/JsonArrayIterator.java | 4 +--- .../src/test/java/nl/vpro/jackson2/JsonArrayIteratorTest.java | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/vpro-shared-jackson2/src/main/java/nl/vpro/jackson2/JsonArrayIterator.java b/vpro-shared-jackson2/src/main/java/nl/vpro/jackson2/JsonArrayIterator.java index 81118deb6..e80f6612a 100644 --- a/vpro-shared-jackson2/src/main/java/nl/vpro/jackson2/JsonArrayIterator.java +++ b/vpro-shared-jackson2/src/main/java/nl/vpro/jackson2/JsonArrayIterator.java @@ -211,11 +211,9 @@ protected void findNext() { if(needsFindNext) { while(true) { try { - var lastToken = jp.getLastClearedToken(); - TreeNode tree = jp.readValueAsTree(); var newLastToken = jp.getLastClearedToken(); - assert lastToken != newLastToken; + this.eventListener.accept(new TokenEvent(newLastToken)); if (jp.getLastClearedToken() == JsonToken.END_ARRAY) { diff --git a/vpro-shared-jackson2/src/test/java/nl/vpro/jackson2/JsonArrayIteratorTest.java b/vpro-shared-jackson2/src/test/java/nl/vpro/jackson2/JsonArrayIteratorTest.java index e11c165fd..5c6bec36d 100644 --- a/vpro-shared-jackson2/src/test/java/nl/vpro/jackson2/JsonArrayIteratorTest.java +++ b/vpro-shared-jackson2/src/test/java/nl/vpro/jackson2/JsonArrayIteratorTest.java @@ -49,7 +49,7 @@ public void simple() throws IOException { } @Test - public void chanages() throws IOException { + public void changes() throws IOException { //Jackson2Mapper.getInstance().writeValue(System.out, new Change("bla", false)); try (JsonArrayIterator it = JsonArrayIterator.builder().inputStream(getClass().getResourceAsStream("/changes.json")).valueClass(Change.class).objectMapper(Jackson2Mapper.getInstance()).build()) { From 4a225be9d4aba9902c3d27edd69d72564025ec64 Mon Sep 17 00:00:00 2001 From: Michiel Meeuwissen Date: Fri, 9 Jan 2026 16:31:03 +0100 Subject: [PATCH 17/18] Fixing test cases. --- .../nl/vpro/jackson3/JsonArrayIterator.java | 106 ++++++------------ .../jackson3/rs/JsonIdAdderBodyReader.java | 24 ++-- .../vpro/jackson3/JsonArrayIteratorTest.java | 30 +++-- .../rs/JsonIdAdderBodyReaderTest.java | 7 +- 4 files changed, 70 insertions(+), 97 deletions(-) diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/JsonArrayIterator.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/JsonArrayIterator.java index 771848f1f..7faa71e3d 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/JsonArrayIterator.java +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/JsonArrayIterator.java @@ -4,6 +4,7 @@ import lombok.extern.slf4j.Slf4j; import tools.jackson.core.*; import tools.jackson.databind.ObjectReader; +import tools.jackson.databind.exc.MismatchedInputException; import tools.jackson.databind.node.NullNode; import java.io.*; @@ -32,13 +33,16 @@ public class JsonArrayIterator extends UnmodifiableIterator private final JsonParser jp; + private final ObjectReader reader; + + private T next = null; private boolean needsFindNext = true; private Boolean hasNext; - private final BiFunction valueCreator; + private final BiFunction valueCreator; @Getter @Setter @@ -69,7 +73,7 @@ public JsonArrayIterator(InputStream inputStream, final Class clazz, Runnable this(inputStream, null, clazz, callback, null, null, null, null, null, null); } - public JsonArrayIterator(InputStream inputStream, final BiFunction valueCreator) throws IOException { + public JsonArrayIterator(InputStream inputStream, final BiFunction valueCreator) throws IOException { this(inputStream, valueCreator, null, null, null, null, null, null, null, null); } @@ -105,7 +109,7 @@ public static class Builder implements Iterable { @lombok.Builder(builderClassName = "Builder", builderMethodName = "_builder") private JsonArrayIterator( @NonNull InputStream inputStream, - @Nullable final BiFunction valueCreator, + @Nullable final BiFunction valueCreator, @Nullable final Class valueClass, @Nullable Runnable callback, @Nullable String sizeField, @@ -116,8 +120,8 @@ private JsonArrayIterator( @Nullable Listener eventListener ) { requireNonNull(inputStream, "No inputStream given"); - ObjectReader reader = objectMapper == null ? Jackson3Mapper.LENIENT.reader() : objectMapper.reader(); - this.jp = reader.createParser(inputStream); + this.reader = objectMapper == null ? Jackson3Mapper.LENIENT.reader() : objectMapper.reader(); + this.jp = this.reader.createParser(inputStream); this.valueCreator = valueCreator == null ? valueCreator(valueClass) : valueCreator; if (valueCreator != null && valueClass != null) { throw new IllegalArgumentException(); @@ -161,25 +165,13 @@ private JsonArrayIterator( this.size = tmpSize; this.totalSize = tmpTotalSize; this.eventListener.accept(new StartEvent()); - JsonToken token = jp.nextToken(); - this.needsFindNext = token != JsonToken.END_ARRAY; - if (! needsFindNext) { - this.hasNext = false; - } - - this.eventListener.accept(new TokenEvent(token)); - this.callback = callback; this.skipNulls = skipNulls == null || skipNulls; } - private static BiFunction valueCreator(Class clazz) { - return (jp, tree) -> { - try (JsonParser sub = jp.objectReadContext().treeAsTokens(tree)) { - return sub.readValueAs(clazz); - } catch (JacksonException e) { - throw new ValueReadException(e); - } + private static BiFunction valueCreator(Class clazz) { + return (m, tree) -> { + return m.treeToValue(tree, clazz); }; } @@ -222,43 +214,32 @@ protected void findNext() { if(needsFindNext) { while(true) { try { + jp.nextToken(); var currentToken = jp.currentToken(); - TreeNode tree; - if (currentToken == JsonToken.START_OBJECT) { - tree = jp.readValueAsTree(); - } else { - tree = null; - log.debug("Expected START_OBJECT token but got {}", currentToken); + this.eventListener.accept(new TokenEvent(currentToken)); + + if (currentToken == JsonToken.END_ARRAY) { + callback(); + next = null; + hasNext = false; + break; } - jp.nextToken(); - var lastToken = jp.currentToken(); - - this.eventListener.accept(new TokenEvent(lastToken)); - if (lastToken == JsonToken.END_ARRAY) { - tree = null; - } else { - if (tree instanceof NullNode && skipNulls) { - foundNulls++; - jp.nextToken(); - continue; - } + TreeNode tree = jp.readValueAsTree(); // read the next token. + if (tree == null) { + next = null; + hasNext = false; + break; + } + if (tree instanceof NullNode && skipNulls) { + foundNulls++; + continue; } - try { - if (tree == null) { - callback(); - hasNext = false; - } else { - if (foundNulls > 0) { - logger.warn("Found {} nulls. Will be skipped", foundNulls); - } - - next = valueCreator.apply(jp, tree); - eventListener.accept(new NextEvent(next)); - hasNext = true; - } + next = valueCreator.apply(reader, tree); + eventListener.accept(new NextEvent(next)); + hasNext = true; break; - } catch (ValueReadException jme) { + } catch (MismatchedInputException jme) { foundNulls++; logger.warn("{} {} for\n{}\nWill be skipped", jme.getClass(), jme.getMessage(), tree); } @@ -298,7 +279,7 @@ protected void callback() { /** * Write the entire stream to an output stream */ - public void write(OutputStream out, final Consumer logging) throws IOException { + public void write(OutputStream out, final Consumer logging) { write(this, out, logging == null ? null : (c) -> { logging.accept(c); return null;}); } @@ -307,14 +288,6 @@ public void writeArray(OutputStream out, final Consumer logging) throws IOExc } - /** - * Write the entire stream to an output stream - * @deprecated Use {@link #write(OutputStream, Consumer)} - */ - @Deprecated - public void write(OutputStream out, final Function logging) throws IOException { - write(this, out, logging); - } /** * Write the entire stream to an output stream @@ -322,7 +295,7 @@ public void write(OutputStream out, final Function logging) throws IOEx public static void write( final CountedIterator iterator, final OutputStream out, - final Function logging) throws IOException { + final Function logging) { try (JsonGenerator jg = Jackson3Mapper.INSTANCE.mapper().createGenerator(out)) { jg.writeStartObject(); jg.writeArrayPropertyStart("array"); @@ -408,15 +381,6 @@ public static Builder builder(Class valueClass) { - public static class ValueReadException extends RuntimeException { - - @Serial - private static final long serialVersionUID = 6976771876437440576L; - - public ValueReadException(JacksonException e) { - super(e); - } - } public class Event { diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/rs/JsonIdAdderBodyReader.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/rs/JsonIdAdderBodyReader.java index fbfacf9f2..918aa03b8 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/rs/JsonIdAdderBodyReader.java +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/rs/JsonIdAdderBodyReader.java @@ -1,6 +1,8 @@ package nl.vpro.jackson3.rs; +import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; +import tools.jackson.core.JsonParser; import tools.jackson.databind.*; import tools.jackson.databind.json.JsonMapper; import tools.jackson.databind.jsontype.TypeDeserializer; @@ -41,6 +43,7 @@ public boolean isReadable(Class type, Type genericType, Annotation[] annotati return mediaType.isCompatible(MediaType.APPLICATION_JSON_TYPE); } + @SneakyThrows @Override public Object readFrom( final @NonNull Class type, @@ -49,27 +52,24 @@ public Object readFrom( MediaType mediaType, MultivaluedMap httpHeaders, InputStream entityStream) throws WebApplicationException { - final ObjectReader reader; + final ObjectReader reader; JsonMapper mapper = providers == null ? null : providers.getContextResolver(JsonMapper.class, MediaType.APPLICATION_JSON_TYPE).getContext(type); if (mapper == null) { mapper = Jackson3Mapper.LENIENT.mapper(); - reader = mapper.reader(); - log.info("No mapper found in {}", providers); - } else { - reader = mapper.readerFor(type); } + reader = mapper.reader(); + ObjectReader objectReader = reader.forType(type); + final JsonParser parser = reader.createParser(entityStream); - final JavaType javaType = reader.typeFactory().constructType(genericType); - final JsonNode jsonNode = reader.readTree(entityStream); + final JsonNode jsonNode = parser.readValueAsTree(); if (jsonNode instanceof ObjectNode objectNode) { - ObjectReader objectReader = reader.forType(javaType); - objectReader.readValue(objectNode); // just to ensure t + final JavaType javaType = reader.typeFactory().constructType(genericType); final TypeDeserializer typeDeserializer = mapper .deserializationConfig() .getTypeResolverProvider() - .findTypeDeserializer(null, javaType, null); + .findTypeDeserializer(null, javaType, null); if (typeDeserializer != null) { final String propertyName = typeDeserializer.getPropertyName(); final String propertyValue = typeDeserializer.getTypeIdResolver().idFromBaseType(null); @@ -79,8 +79,8 @@ public Object readFrom( } } } - return mapper.treeToValue(jsonNode, javaType.getRawClass()); + return mapper.treeToValue(jsonNode, type); - } + } } diff --git a/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/JsonArrayIteratorTest.java b/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/JsonArrayIteratorTest.java index fa135079d..a7732b769 100644 --- a/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/JsonArrayIteratorTest.java +++ b/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/JsonArrayIteratorTest.java @@ -26,7 +26,7 @@ public class JsonArrayIteratorTest { @Test public void simple() { - for (Change change : JsonArrayIterator.builder(Change.class) + try (JsonArrayIterator iterator = JsonArrayIterator.builder(Change.class) .objectMapper(Jackson3Mapper.INSTANCE.withSourceInLocation()) .inputStream( new ByteArrayInputStream(""" @@ -41,27 +41,37 @@ public void simple() { } ] } - """.getBytes(StandardCharsets.UTF_8)))) { - log.info("{}", change); + """.getBytes(StandardCharsets.UTF_8))).build()) { + assertThat(iterator.hasNext()).isTrue(); + assertThat(iterator.getSize()).hasValue(1L); + Change change = iterator.next(); + log.info("{}", change); + assertThat(iterator.hasNext()).isFalse(); } } @Test public void changes() { - + String[] mids = new String[] { + "POMS_NCRV_1138990", + null, + null, + "POMS_AVRO_1138559" + }; //Jackson2Mapper.getInstance().writeValue(System.out, new Change("bla", false)); try (JsonArrayIterator it = JsonArrayIterator.builder() .inputStream(getClass().getResourceAsStream("/changes.json")) .valueClass(Change.class) .objectMapper(Jackson3Mapper.INSTANCE.withSourceInLocation()) .build()) { - assertThat(it.next().getMid()).isEqualTo("POMS_NCRV_1138990"); // 1 + assertThat(it.next().getMid()).isEqualTo( + mids[0] + ); // 1 assertThat(it.getCount()).isEqualTo(1); assertThat(it.getSize()).hasValueSatisfying(size -> assertThat(size).isEqualTo(14)); for (int i = 0; i < 9; i++) { assertThat(it.hasNext()).isTrue(); - Change change = it.next(); // 10 Optional size = it.getSize(); size.ifPresent(aLong -> @@ -164,7 +174,7 @@ public void write() throws IOException, JSONException { log.info("{}", c); }); String expected = "{'array': " + IOUtils.resourceToString("/array_from_changes.json", StandardCharsets.UTF_8) + "}"; - JSONAssert.assertEquals(expected, out.toString(), JSONCompareMode.STRICT); + JSONAssert.assertEquals(expected, out.toString(), JSONCompareMode.LENIENT); } } @@ -184,7 +194,7 @@ public void writeArray() throws IOException, JSONException { log.info("{}", c) ); String expected = IOUtils.resourceToString("/array_from_changes.json", StandardCharsets.UTF_8); - JSONAssert.assertEquals(expected, out.toString(), JSONCompareMode.STRICT); + JSONAssert.assertEquals(expected, out.toString(), JSONCompareMode.LENIENT); } } @@ -220,10 +230,10 @@ public void interrupt() { @Override public int read() throws IOException { if (i == 6) { - throw new InterruptedIOException(); + throw new InterruptedIOException("INterrupted at 6 bytes, so that would have complete 2 objects"); } if (i < bytes.length) { - return (int) bytes[i++]; + return bytes[i++]; } else { return -1; } diff --git a/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/rs/JsonIdAdderBodyReaderTest.java b/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/rs/JsonIdAdderBodyReaderTest.java index 7a42d5fff..cb64fe21f 100644 --- a/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/rs/JsonIdAdderBodyReaderTest.java +++ b/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/rs/JsonIdAdderBodyReaderTest.java @@ -3,7 +3,6 @@ import tools.jackson.databind.json.JsonMapper; import java.io.ByteArrayInputStream; -import java.io.IOException; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.ext.Providers; @@ -58,7 +57,7 @@ public static class C { } @Test - public void testA() throws IOException { + public void testA() { Object o = idAdderInterceptor.readFrom(Object.class, A.class, @@ -70,7 +69,7 @@ public void testA() throws IOException { @Test - public void testBase() throws IOException { + public void testBase() { Object o = idAdderInterceptor.readFrom(Object.class, Base.class, @@ -79,7 +78,7 @@ public void testBase() throws IOException { assertThat(o).isInstanceOf(A.class); } @Test - public void testC() throws IOException { + public void testC() { Object o = idAdderInterceptor.readFrom(Object.class, C.class, From c8f68ec7b669e3502cd1f19cf47d324894962bf8 Mon Sep 17 00:00:00 2001 From: Michiel Meeuwissen Date: Fri, 9 Jan 2026 16:56:20 +0100 Subject: [PATCH 18/18] Finally figured it out. --- .../jackson3/rs/JsonIdAdderBodyReader.java | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/rs/JsonIdAdderBodyReader.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/rs/JsonIdAdderBodyReader.java index 918aa03b8..1cd6ab9f3 100644 --- a/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/rs/JsonIdAdderBodyReader.java +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/rs/JsonIdAdderBodyReader.java @@ -4,6 +4,7 @@ import lombok.extern.slf4j.Slf4j; import tools.jackson.core.JsonParser; import tools.jackson.databind.*; +import tools.jackson.databind.deser.DeserializationContextExt; import tools.jackson.databind.json.JsonMapper; import tools.jackson.databind.jsontype.TypeDeserializer; import tools.jackson.databind.jsontype.TypeIdResolver; @@ -53,34 +54,37 @@ public Object readFrom( MultivaluedMap httpHeaders, InputStream entityStream) throws WebApplicationException { - final ObjectReader reader; JsonMapper mapper = providers == null ? null : providers.getContextResolver(JsonMapper.class, MediaType.APPLICATION_JSON_TYPE).getContext(type); if (mapper == null) { mapper = Jackson3Mapper.LENIENT.mapper(); } - reader = mapper.reader(); + + // Create the reader first (so we can get the TypeFactory from it) + final ObjectReader reader = mapper.reader(); ObjectReader objectReader = reader.forType(type); final JsonParser parser = reader.createParser(entityStream); final JsonNode jsonNode = parser.readValueAsTree(); + // Construct the JavaType for the declared generic type and ask Jackson for the TypeDeserializer + final JavaType javaType = reader.typeFactory().constructType(genericType); if (jsonNode instanceof ObjectNode objectNode) { - final JavaType javaType = reader.typeFactory().constructType(genericType); - final TypeDeserializer typeDeserializer = mapper - .deserializationConfig() - .getTypeResolverProvider() - .findTypeDeserializer(null, javaType, null); + + + DeserializationContextExt deserializationContextExt = mapper._deserializationContext(); + final TypeDeserializer typeDeserializer = deserializationContextExt.findTypeDeserializer(javaType); + if (typeDeserializer != null) { final String propertyName = typeDeserializer.getPropertyName(); - final String propertyValue = typeDeserializer.getTypeIdResolver().idFromBaseType(null); + final String propertyValue = typeDeserializer.getTypeIdResolver().idFromBaseType(deserializationContextExt); if (! objectNode.has(propertyName)) { log.debug("Implicitly setting {} = {} for {}", propertyName, propertyValue, javaType); objectNode.put(propertyName, propertyValue); } } } - return mapper.treeToValue(jsonNode, type); + return mapper.treeToValue(jsonNode, javaType.getRawClass()); } }