diff --git a/.github/workflows/pull.yml b/.github/workflows/pull.yml index 907892d73..15f85808b 100644 --- a/.github/workflows/pull.yml +++ b/.github/workflows/pull.yml @@ -4,8 +4,7 @@ name: build pull request on: pull_request: branches: [main] - types: [opened, synchronize, reopened ] - + types: [opened, synchronize, reopened] concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} @@ -18,7 +17,6 @@ jobs: env: MAVEN_ARGS: '--no-transfer-progress' steps: - - uses: actions/checkout@v6 - name: Set up JDK uses: actions/setup-java@v5 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: diff --git a/pom.xml b/pom.xml index f091e9dd7..f25df23ba 100644 --- a/pom.xml +++ b/pom.xml @@ -20,9 +20,10 @@ 6.2.13 6.5.6 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 @@ -32,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 @@ -42,7 +43,7 @@ 3.0 0.18.1 1.15 - 3.52.0 + 3.53.0 4.5.14 4.4.16 @@ -60,7 +61,7 @@ 9.0.1.Final 4.16.0 - 2.2.40 + 2.2.41 1.18.42 ${project.build.directory}/delombok @@ -72,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 @@ -136,6 +137,8 @@ vpro-shared-hibernate-search6 vpro-shared-jackson2 + vpro-shared-jackson3 + vpro-shared-logging vpro-shared-log4j2 vpro-shared-monitoring @@ -469,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 @@ -587,6 +590,13 @@ pom import + + tools.jackson + jackson-bom + ${jackson3.version} + pom + import + org.apache.httpcomponents httpclient @@ -638,7 +648,7 @@ org.jsoup jsoup - 1.21.2 + 1.22.1 org.mockito @@ -714,7 +724,7 @@ org.wiremock wiremock - 3.13.1 + 3.13.2 test @@ -751,7 +761,7 @@ org.apache.groovy groovy - 5.0.2 + 5.0.3 org.javassist @@ -783,7 +793,7 @@ org.meeuw mihxil-json-grep - 0.13 + 1.0 org.ehcache 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 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-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-jackson2/pom.xml b/vpro-shared-jackson2/pom.xml index 07d75081d..28852a1ba 100644 --- a/vpro-shared-jackson2/pom.xml +++ b/vpro-shared-jackson2/pom.xml @@ -51,16 +51,16 @@ org.projectlombok lombok - - io.github.natty-parser - natty - true - jakarta.xml.bind 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/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-jackson2/src/main/java/nl/vpro/jackson2/JsonArrayIterator.java b/vpro-shared-jackson2/src/main/java/nl/vpro/jackson2/JsonArrayIterator.java index e468bcfad..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 @@ -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) { @@ -212,7 +212,9 @@ protected void findNext() { while(true) { try { TreeNode tree = jp.readValueAsTree(); - this.eventListener.accept(new TokenEvent(jp.getLastClearedToken())); + var newLastToken = jp.getLastClearedToken(); + + this.eventListener.accept(new TokenEvent(newLastToken)); if (jp.getLastClearedToken() == JsonToken.END_ARRAY) { tree = null; 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/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..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 @@ -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 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()) { @@ -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/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/pom.xml b/vpro-shared-jackson3/pom.xml new file mode 100644 index 000000000..7b747f38c --- /dev/null +++ b/vpro-shared-jackson3/pom.xml @@ -0,0 +1,87 @@ + + + 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 + + + + jakarta.ws.rs + jakarta.ws.rs-api + + + + jakarta.annotation + jakarta.annotation-api + provided + + + + jakarta.xml.bind + jakarta.xml.bind-api + true + + + + com.google.guava + guava + true + + + commons-io + 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/BackwardsCompatibleJsonEnum.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/BackwardsCompatibleJsonEnum.java new file mode 100644 index 000000000..cb64fda6e --- /dev/null +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/BackwardsCompatibleJsonEnum.java @@ -0,0 +1,53 @@ +package nl.vpro.jackson3; + +import lombok.extern.slf4j.Slf4j; +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. + * {@code 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(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 new file mode 100644 index 000000000..f3c9ecc2a --- /dev/null +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/DateModule.java @@ -0,0 +1,42 @@ +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; + + +/** + * 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-jackson3")); + + // 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..e1f536fd8 --- /dev/null +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/DateToJsonTimestamp.java @@ -0,0 +1,40 @@ +package nl.vpro.jackson3; + +import tools.jackson.core.*; +import tools.jackson.databind.*; +import tools.jackson.databind.deser.std.StdDeserializer; + +import java.util.Date; + + +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..74ded4063 --- /dev/null +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/DurationToJsonTimestamp.java @@ -0,0 +1,66 @@ +package nl.vpro.jackson3; + +import tools.jackson.core.*; +import tools.jackson.databind.*; + +import java.time.Duration; +import java.util.Calendar; + +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 ValueSerializer { + + public static final Serializer INSTANCE = new Serializer(); + + @Override + 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 ValueSerializer { + + public static final Serializer INSTANCE = new Serializer(); + + @Override + public void serialize(javax.xml.datatype.Duration value, JsonGenerator jgen, SerializationContext ctxt) throws JacksonException { + if (value == null) { + jgen.writeNull(); + } else { + jgen.writeNumber(value.getTimeInMillis(Calendar.getInstance())); + } + } + } + + + 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) { + if (jp.getString().isEmpty() && ctxt.hasDeserializationFeatures(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT.getMask())) { + return null; + } else { + 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 new file mode 100644 index 000000000..9cc2092b4 --- /dev/null +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/DurationToSecondsFloatTimestamp.java @@ -0,0 +1,40 @@ +package nl.vpro.jackson3; + +import tools.jackson.core.*; +import tools.jackson.databind.*; + +import java.time.Duration; + +/** + * 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 ValueSerializer { + + public static final Serializer INSTANCE = new Serializer(); + + @Override + public void serialize(Duration value, JsonGenerator jgen, SerializationContext ctxt) throws JacksonException { + if (value == null) { + jgen.writeNull(); + } else { + jgen.writeNumber(value.toMillis() / 1000f); + } + } + } + + + public static class Deserializer extends ValueDeserializer { + + public static final Deserializer INSTANCE = new Deserializer(); + @Override + 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 new file mode 100644 index 000000000..4e7cd8014 --- /dev/null +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/GuavaRangeModule.java @@ -0,0 +1,141 @@ +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.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 StdSerializer> { + + 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, SerializationContext provider) throws JacksonException { + if (value == null) { + gen.writeNull(); + } else { + gen.writeStartObject(); + Class type = null; + + if (value.hasLowerBound()) { + type = value.lowerEndpoint().getClass(); + gen.writePOJOProperty(LOWER_ENDPOINT, value.lowerEndpoint()); + gen.writePOJOProperty(LOWER_BOUND_TYPE, value.lowerBoundType()); + } + if (value.hasUpperBound()) { + type = value.upperEndpoint().getClass(); + gen.writePOJOProperty(UPPER_ENDPOINT, value.upperEndpoint()); + gen.writePOJOProperty(UPPER_BOUND_TYPE, value.upperBoundType()); + } + if (type != null) { + gen.writeStringProperty("type", type.getName()); + } + gen.writeEndObject(); + } + } + + + } + + public static class Deserializer extends StdDeserializer> { + + 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) { + + 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").stringValue()); + return of(type, ctxt, node); + } else { + return Range.all(); + } + + } + } + + static > Range of(Class clazz, DeserializationContext context, 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).asString()); + 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 = context.readTreeAsValue(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..bbfd29b6a --- /dev/null +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/InstantToJsonTimestamp.java @@ -0,0 +1,50 @@ +package nl.vpro.jackson3; + +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; + + +public class InstantToJsonTimestamp { + + private InstantToJsonTimestamp() {} + + public static class Serializer extends ValueSerializer { + + public static final Serializer INSTANCE = new Serializer(); + + @Override + 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 ValueDeserializer { + + public static final Deserializer INSTANCE = new Deserializer(); + @Override + public Instant deserialize(JsonParser jp, DeserializationContext ctxt) { + try { + return Instant.ofEpochMilli(jp.getLongValue()); + } catch (InputCoercionException 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..d0a30366a --- /dev/null +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/InstantToSecondsFloatTimestamp.java @@ -0,0 +1,52 @@ +package nl.vpro.jackson3; + +import tools.jackson.core.*; +import tools.jackson.databind.*; + +import java.time.Instant; +import java.time.ZonedDateTime; +import java.time.format.DateTimeParseException; + + +public class InstantToSecondsFloatTimestamp { + + private InstantToSecondsFloatTimestamp() {} + + public static class Serializer extends ValueSerializer { + + + public static final Serializer INSTANCE = new Serializer(); + + @Override + public void serialize(Instant value, JsonGenerator jgen, SerializationContext ctxt) throws JacksonException { + if (value == null) { + jgen.writeNull(); + } else { + jgen.writeNumber(value.toEpochMilli() / 1000f); + } + } + } + + public static class Deserializer extends ValueDeserializer { + + public static final Deserializer INSTANCE = new Deserializer(); + @Override + public Instant deserialize(JsonParser jp, DeserializationContext ctxt) { + try { + return Instant.ofEpochMilli((long) Float.parseFloat(jp.getValueAsString()) * 1000); + } catch ( Exception e) { + 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..2b0cb5aa9 --- /dev/null +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/IterableJson.java @@ -0,0 +1,96 @@ +package nl.vpro.jackson3; + +import tools.jackson.core.*; +import tools.jackson.databind.*; + +import java.util.*; +import java.util.function.Function; + + +/** + * @author Michiel Meeuwissen + * @since 0.32 + */ +public class IterableJson { + + + public static class Serializer extends ValueSerializer> { + @Override + public void serialize(Iterable value, JsonGenerator jgen, SerializationContext ctxt) throws JacksonException { + + if (value == null) { + jgen.writeNull(); + } else { + Iterator i = value.iterator(); + Object v; + if (i.hasNext()) { + v = i.next(); + if (! i.hasNext()) { + jgen.writePOJO(v); + } else { + jgen.writeStartArray(); + jgen.writePOJO(v); + while (i.hasNext()) { + jgen.writePOJO(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 ValueDeserializer> { + + 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) { + if (jp.streamReadContext().inObject()) { + if (! isSimple) { + jp.clearCurrentToken(); + } + T rs = jp.readValueAs(memberClass); + return creator.apply(Collections.singletonList(rs)); + } else if (jp.streamReadContext().inArray()) { + List list = new ArrayList<>(); + jp.clearCurrentToken(); + while (jp.nextToken() != JsonToken.END_ARRAY) { + list.add(jp.readValueAs(memberClass)); + } + 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..3cdb61b96 --- /dev/null +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/Jackson3Mapper.java @@ -0,0 +1,413 @@ +/* + * Copyright (C) 2012 All rights reserved + * VPRO The Netherlands + */ +package nl.vpro.jackson3; + +import lombok.AccessLevel; +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; +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; + +import java.lang.reflect.InvocationTargetException; +import java.net.http.HttpResponse; +import java.util.function.Consumer; +import java.util.function.Predicate; + +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.DEFAULT_VIEW_INCLUSION; +import static tools.jackson.databind.MapperFeature.USE_WRAPPER_NAME_AS_PROPERTY_NAME; +import static tools.jackson.databind.SerializationFeature.INDENT_OUTPUT; + +/** + * A wrapper around a Jackson {@link ObjectMapper} with one {@link ObjectReader} and one {@link ObjectWriter} configured with specific views. + *

+ * 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 { + + private static boolean loggedAboutAvro = false; + + private static final SimpleFilterProvider FILTER_PROVIDER = new SimpleFilterProvider(); + + 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 MODEL = buildModelInstance(); + public static final Jackson3Mapper MODEL_AND_NORMAL = buildModelAndNormalInstance(); + + + 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; + } + + 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 static void lenient(JsonMapper.Builder builder) { + 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); + } + private static void strict(JsonMapper.Builder builder) { + builder.enable(FAIL_ON_UNKNOWN_PROPERTIES); + } + + private static Jackson3Mapper buildPrettyInstance() { + Jackson3Mapper.Builder pretty = Jackson3Mapper + .builder("pretty") + .configure(b -> { + lenient(b); + b.enable(INDENT_OUTPUT); + }); + return pretty.build(); + } + + private static Jackson3Mapper buildPrettyStrictInstance() { + + return Jackson3Mapper + .builder("pretty_strict") + .configure(b -> { + strict(b); + b.enable(INDENT_OUTPUT); + b.enable(FAIL_ON_UNKNOWN_PROPERTIES); + }) + .forward() + .build() + ; + } + + + private static Jackson3Mapper buildStrictInstance() { + return Jackson3Mapper.builder("strict") + .configure(b -> { + b.enable(FAIL_ON_UNKNOWN_PROPERTIES); + } + ).forward() + .build(); + } + + private static Jackson3Mapper buildPublisherInstance() { + return Jackson3Mapper.builder("publisher") + .serializationView(Views.ForwardPublisher.class) + .deserializationView(Views.Forward.class) + .build(); + } + + private static Jackson3Mapper buildPrettyPublisherInstance() { + return Jackson3Mapper.builder("pretty_publisher") + .serializationView(Views.ForwardPublisher.class) + .deserializationView(Views.Forward.class) + .configure(b -> b.enable(INDENT_OUTPUT)) + .build(); + } + + private static Jackson3Mapper buildBackwardsPublisherInstance() { + return Jackson3Mapper.builder("backwards_publisher") + .serializationView(Views.Publisher.class) + .deserializationView(Views.Normal.class) + .configure(b -> b.enable(INDENT_OUTPUT)) + .build(); + } + + private static Jackson3Mapper buildModelInstance() { + return Jackson3Mapper.builder("model") + .serializationView(Views.Model.class) + .build(); + } + + private static Jackson3Mapper buildModelAndNormalInstance() { + return Jackson3Mapper.builder("model_and_normal") + .serializationView(Views.ModelAndNormal.class) + .build(); + + } + + 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({JacksonException.class}) + public static T lenientTreeToValue(JsonNode jsonNode, Class clazz) { + return buildLenientInstance().reader().treeToValue(jsonNode, clazz); + } + + private final JsonMapper mapper; + private final ObjectWriter writer; + private final ObjectReader reader; + private final String toString; + + @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; + } + + public static void configureMapper(Jackson3Mapper.Builder mapper) { + configureMapper(mapper, m -> true); + } + + public static void configureMapper(Jackson3Mapper.Builder builder, Predicate filter) { + + builder.mapperBuilder.filterProvider(FILTER_PROVIDER); + + AnnotationIntrospector introspector = new AnnotationIntrospectorPair( + new JacksonAnnotationIntrospector(), + new JakartaXmlBindAnnotationIntrospector(false) + ); + + 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. + + 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); + + + // actually a breaking change in jackson3, this defaults to false now. + builder.mapperBuilder.enable(DEFAULT_VIEW_INCLUSION); + + register(builder, filter, new DateModule()); + + + //SimpleModule module = new SimpleModule(); + //module.setDeserializerModifier(new AfterUnmarshalModifier()); + //mapper.registerModule(module); + + try { + Class avro = Class.forName("nl.vpro.jackson3.SerializeAvroModule"); + 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()); + } + 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(builder, filter, (tools.jackson.databind.JacksonModule) 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(Jackson3Mapper.Builder mapper, Predicate predicate, JacksonModule module) { + if (predicate.test(module)) { + mapper.mapperBuilder.addModule(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 -> { + SimpleLogger simple = slf4j(log); + if (simple.isEnabled(level)) { + body = new LoggingInputStream(simple, body, level); + } + 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 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 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(); + { + 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 new file mode 100644 index 000000000..7faa71e3d --- /dev/null +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/JsonArrayIterator.java @@ -0,0 +1,456 @@ +package nl.vpro.jackson3; + +import lombok.*; +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.*; +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.google.common.collect.PeekingIterator; +import com.google.common.collect.UnmodifiableIterator; + +import nl.vpro.util.CloseableIterator; +import nl.vpro.util.CountedIterator; + +import static java.util.Objects.requireNonNull; + +/** + * @author Michiel Meeuwissen + * @since 1.0 + */ +@Slf4j +public class JsonArrayIterator extends UnmodifiableIterator + implements CloseableIterator, PeekingIterator, CountedIterator { + + private final JsonParser jp; + + private final ObjectReader reader; + + + 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 implements Iterable { + + + @Override + public @NonNull Iterator iterator() { + JsonArrayIterator build = build(); + return build.stream() + .onClose(build::close) + .iterator(); + } + } + + /** + * + * @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#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. + * @param eventListener A listener for events that happen during parsing and iteration of the array. See {@link Event} and extension classes. + */ + @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 Jackson3Mapper objectMapper, + @Nullable Logger logger, + @Nullable Boolean skipNulls, + @Nullable Listener eventListener + ) { + requireNonNull(inputStream, "No inputStream given"); + 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(); + } + 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? Listener.noop() : 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.PROPERTY_NAME) { + fieldName = jp.currentName(); + } + 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(tmpTotalSize)); + + } + if (token == JsonToken.START_ARRAY) { + break; + } + } + this.size = tmpSize; + this.totalSize = tmpTotalSize; + this.eventListener.accept(new StartEvent()); + this.callback = callback; + this.skipNulls = skipNulls == null || skipNulls; + } + + private static BiFunction valueCreator(Class clazz) { + return (m, tree) -> { + return m.treeToValue(tree, clazz); + }; + + } + + @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 { + jp.nextToken(); + var currentToken = jp.currentToken(); + this.eventListener.accept(new TokenEvent(currentToken)); + + if (currentToken == JsonToken.END_ARRAY) { + callback(); + next = null; + hasNext = false; + break; + } + TreeNode tree = jp.readValueAsTree(); // read the next token. + if (tree == null) { + next = null; + hasNext = false; + break; + } + if (tree instanceof NullNode && skipNulls) { + foundNulls++; + continue; + } + try { + next = valueCreator.apply(reader, tree); + eventListener.accept(new NextEvent(next)); + hasNext = true; + break; + } catch (MismatchedInputException jme) { + foundNulls++; + logger.warn("{} {} for\n{}\nWill be skipped", jme.getClass(), jme.getMessage(), tree); + } + } 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() { + 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) { + 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 + */ + public static void write( + final CountedIterator iterator, + final OutputStream out, + final Function logging) { + try (JsonGenerator jg = Jackson3Mapper.INSTANCE.mapper().createGenerator(out)) { + jg.writeStartObject(); + jg.writeArrayPropertyStart("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.INSTANCE.mapper().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) { + while (iterator.hasNext()) { + T change; + try { + change = iterator.next(); + if (change != null) { + jg.writePOJO(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.writePOJO(e.getMessage()); + } + + } + } + + + @Override + @NonNull + public Optional getSize() { + return Optional.ofNullable(size); + } + + @Override + @NonNull + 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 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> { + + + 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/LenientBooleanDeserializer.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/LenientBooleanDeserializer.java new file mode 100644 index 000000000..3158d3a2f --- /dev/null +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/LenientBooleanDeserializer.java @@ -0,0 +1,39 @@ +package nl.vpro.jackson3; + +import tools.jackson.core.*; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.ValueDeserializer; + +/** + * @author Michiel Meeuwissen + * @since 2.3 + */ +public class LenientBooleanDeserializer extends ValueDeserializer { + + public static final LenientBooleanDeserializer INSTANCE = new LenientBooleanDeserializer(); + + private LenientBooleanDeserializer() { + + } + + + @Override + public Boolean deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws JacksonException { + JsonToken token = jsonParser.currentToken(); + if (jsonParser.isNaN()) { + return false; + } + if (token.isBoolean()) { + return jsonParser.getBooleanValue(); + } else if (token.isNumeric()) { + return jsonParser.getNumberValue().longValue() != 0; + } else { + 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 new file mode 100644 index 000000000..4948e2a86 --- /dev/null +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/LocalDateTimeToJsonDateWithSpace.java @@ -0,0 +1,54 @@ +package nl.vpro.jackson3; + +import tools.jackson.core.*; +import tools.jackson.databind.*; + +import java.time.LocalDateTime; +import java.time.format.DateTimeParseException; + +import nl.vpro.util.TimeUtils; + + +/** + * @since 2.0 + */ +public class LocalDateTimeToJsonDateWithSpace { + + private LocalDateTimeToJsonDateWithSpace() {} + + public static class Serializer extends ValueSerializer { + + public static final Serializer INSTANCE = new Serializer(); + + @Override + 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 ValueDeserializer { + + public static final Deserializer INSTANCE = new Deserializer(); + + @Override + public LocalDateTime deserialize(JsonParser jp, DeserializationContext ctxt) throws JacksonException { + 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 JacksonException("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..f47377b7a --- /dev/null +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/LocalDateToJsonDate.java @@ -0,0 +1,42 @@ +package nl.vpro.jackson3; + +import tools.jackson.core.*; +import tools.jackson.databind.*; + +import java.time.LocalDate; + + +public class LocalDateToJsonDate { + + private LocalDateToJsonDate() {} + + public static class Serializer extends ValueSerializer { + + public static final Serializer INSTANCE = new Serializer(); + + @Override + public void serialize(LocalDate value, JsonGenerator jgen, SerializationContext ctxt) throws JacksonException { + if (value == null) { + jgen.writeNull(); + } else { + jgen.writeString(value.toString()); + } + } + } + + + public static class Deserializer extends ValueDeserializer { + + public static final Deserializer INSTANCE = new Deserializer(); + + @Override + public LocalDate deserialize(JsonParser jp, DeserializationContext ctxt) { + String text = jp.getString(); + if (text == null) { + return null; + } else { + return LocalDate.parse(text); + } + } + } +} 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..cf6cf291a --- /dev/null +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/StringDurationToJsonTimestamp.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2016 All rights reserved + * VPRO The Netherlands + */ +package nl.vpro.jackson3; + +import tools.jackson.core.*; +import tools.jackson.databind.*; + +import java.time.Duration; + +/** + * @author rico + * @since 0.37 + */ +public class StringDurationToJsonTimestamp { + + private StringDurationToJsonTimestamp() {} + + public static class Serializer extends ValueSerializer { + public static final StringDurationToJsonTimestamp.Serializer INSTANCE = new StringDurationToJsonTimestamp.Serializer(); + + @Override + 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 ValueDeserializer { + + public static final StringDurationToJsonTimestamp.Deserializer INSTANCE = new StringDurationToJsonTimestamp.Deserializer(); + + @Override + 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 new file mode 100644 index 000000000..2ce7fefbe --- /dev/null +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/StringInstantToJsonTimestamp.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2016 All rights reserved + * VPRO The Netherlands + */ +package nl.vpro.jackson3; + +import lombok.extern.slf4j.Slf4j; +import tools.jackson.core.*; +import tools.jackson.databind.*; + +import java.time.Instant; +import java.util.Optional; + +import jakarta.xml.bind.DatatypeConverter; + +import org.slf4j.event.Level; + +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) + * @author Michiel Meeuwissen + * @since 0.39 + */ +@Slf4j +public class StringInstantToJsonTimestamp { + + private static boolean warnedNatty = false; + + private StringInstantToJsonTimestamp() {} + + public static class Serializer extends ValueSerializer { + public static final StringInstantToJsonTimestamp.Serializer INSTANCE = new StringInstantToJsonTimestamp.Serializer(); + + @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 + 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 { + 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()); + warnedNatty = true; + } catch (Throwable e) { + log.debug("Natty couldn't parse {}: {}", value, e.getMessage()); + } + throw iae; + } + } + + public static class Deserializer extends ValueDeserializer { + + public static final StringInstantToJsonTimestamp.Deserializer INSTANCE = new StringInstantToJsonTimestamp.Deserializer(); + + @Override + public Instant deserialize(JsonParser jp, DeserializationContext ctxt) { + 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.getString()); + } catch (IllegalArgumentException iae) { + log.warn("Could not parse {}. Writing null to json", jp.getString()); + 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..047e4d75f --- /dev/null +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/StringZonedLocalDateToJsonTimestamp.java @@ -0,0 +1,65 @@ +package nl.vpro.jackson3; + +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 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 ValueSerializer { + + public static final Serializer INSTANCE = new Serializer(); + + @Override + public void serialize(Object value, JsonGenerator jgen, SerializationContext ctxt) throws JacksonException { + 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 ValueDeserializer { + + public static final Deserializer INSTANCE = new Deserializer(); + @Override + public LocalDate deserialize(JsonParser jp, DeserializationContext ctxt) { + try { + return Instant.ofEpochMilli(jp.getLongValue()).atZone(ZONE).toLocalDate(); + } catch (JacksonException 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..8e675174f --- /dev/null +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/Utils.java @@ -0,0 +1,61 @@ +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.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.PUBLISHER.mapper(); + 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/XMLDurationToJsonTimestamp.java b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/XMLDurationToJsonTimestamp.java new file mode 100644 index 000000000..f7a90b782 --- /dev/null +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/XMLDurationToJsonTimestamp.java @@ -0,0 +1,68 @@ +package nl.vpro.jackson3; + +import lombok.extern.slf4j.Slf4j; +import tools.jackson.core.*; +import tools.jackson.databind.*; + +import java.util.*; + +import javax.xml.datatype.*; + +import nl.vpro.util.TimeUtils; + +@Slf4j +public class XMLDurationToJsonTimestamp { + + + public static class Serializer extends ValueSerializer { + + @Override + 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 ValueDeserializer { + @Override + public Date deserialize(JsonParser jp, DeserializationContext ctxt) { + return new Date(jp.getLongValue()); + } + } + + public static class Deserializer extends ValueDeserializer { + @Override + public Duration deserialize(JsonParser jp, DeserializationContext ctxt) { + 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 ValueSerializer { + + @Override + 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()); + } else { + jgen.writeNull(); + } + } + + } + + public static class DeserializerJavaDuration extends ValueDeserializer { + @Override + 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 new file mode 100644 index 000000000..2e9ad1920 --- /dev/null +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/ZonedDateTimeToJsonTimestamp.java @@ -0,0 +1,47 @@ +package nl.vpro.jackson3; + +import tools.jackson.core.*; +import tools.jackson.databind.*; + +import java.time.Instant; +import java.time.ZonedDateTime; + +import static nl.vpro.jackson3.DateModule.ZONE; + + +public class ZonedDateTimeToJsonTimestamp { + + private ZonedDateTimeToJsonTimestamp() {} + + public static class Serializer extends ValueSerializer { + + public static final Serializer INSTANCE = new Serializer(); + + @Override + 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 ValueDeserializer { + + public static final Deserializer INSTANCE = new Deserializer(); + + @Override + public ZonedDateTime deserialize(JsonParser jp, DeserializationContext ctxt) throws JacksonException { + try { + return Instant.ofEpochMilli(jp.getLongValue()).atZone(ZONE); + } catch (JacksonException 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..69d0167fc --- /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 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 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.LENIENT); + } + public JacksonContextResolver(Jackson3Mapper mapper) { + this(() -> mapper); + } + + public JacksonContextResolver(Supplier mapper) { + this.mapper = ThreadLocal.withInitial(mapper); + } + + @Override + public JsonMapper getContext(Class objectType) { + return mapper.get().mapper(); + } + + /** + * @since 4.0 + */ + public void set(Jackson3Mapper 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..1cd6ab9f3 --- /dev/null +++ b/vpro-shared-jackson3/src/main/java/nl/vpro/jackson3/rs/JsonIdAdderBodyReader.java @@ -0,0 +1,90 @@ +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.deser.DeserializationContextExt; +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; +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 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(tools.jackson.databind.DatabindContext)}, 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); + } + + @SneakyThrows + @Override + public Object readFrom( + final @NonNull Class type, + Type genericType, + Annotation[] annotations, + MediaType mediaType, + MultivaluedMap httpHeaders, + InputStream entityStream) throws WebApplicationException { + + JsonMapper mapper = providers == null ? null : providers.getContextResolver(JsonMapper.class, MediaType.APPLICATION_JSON_TYPE).getContext(type); + if (mapper == null) { + mapper = Jackson3Mapper.LENIENT.mapper(); + } + + // 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) { + + + + DeserializationContextExt deserializationContextExt = mapper._deserializationContext(); + final TypeDeserializer typeDeserializer = deserializationContextExt.findTypeDeserializer(javaType); + + if (typeDeserializer != null) { + final String propertyName = typeDeserializer.getPropertyName(); + 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, 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..2097c6430 --- /dev/null +++ b/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/GuavaRangeModuleTest.java @@ -0,0 +1,86 @@ +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.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; + +import static org.assertj.core.api.Assertions.assertThat; + +class GuavaRangeModuleTest { + + JsonMapper mapper = JsonMapper.builder() + .addModule(new DateModule()) + .addModule(new GuavaRangeModule()) + .build(); + + 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 JacksonException, JSONException { + WithoutSerializer a = new WithoutSerializer(); + a.range = Range.closedOpen(1, 2); + 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(expected, WithoutSerializer.class); + assertThat(ab.range).isEqualTo(a.range); + + } + + + @Test + public void empty() throws JacksonException { + WithIntegerRange a = new WithIntegerRange(); + assertThat(mapper.writeValueAsString(a)).isEqualTo("{\"range\":null}"); + + } + + @Test + public void filled() throws JacksonException { + 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 JacksonException { + 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..06c4badc6 --- /dev/null +++ b/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/IterableJsonTest.java @@ -0,0 +1,183 @@ +package nl.vpro.jackson3; + +import tools.jackson.databind.annotation.JsonDeserialize; +import tools.jackson.databind.annotation.JsonSerialize; + +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 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.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.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 new file mode 100644 index 000000000..c4b64aefd --- /dev/null +++ b/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/Jackson3MapperTest.java @@ -0,0 +1,162 @@ +package nl.vpro.jackson3; + +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 com.fasterxml.jackson.annotation.JsonInclude; + +import nl.vpro.jackson.Views; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@Log4j2 +public class Jackson3MapperTest { + + public enum EnumValues { + a, + b + } + + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + @XmlRootElement + public static class A { + @XmlElement + int integer; + + @XmlAttribute + EnumValues enumValue; + + @XmlElement + 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) + )) + .changeDefaultPropertyInclusion(v -> v.withValueInclusion(JsonInclude.Include.NON_EMPTY)) + .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); + + assertThat(mapper.writer().writeValueAsString(a)).isEqualTo("{\"integer\":2,\"optional\":3}"); + } + + + @Test + public void read() { + //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); + + 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'}"); + assertThat(a.integer).isEqualTo(2); + } + + @Test + public void readEnumValue() { + A a = Jackson3Mapper.INSTANCE.readerFor(A.class).readValue("{'enumValue': 'a'}"); + assertThat(a.enumValue).isEqualTo(EnumValues.a); + } + + @Test + public void readUnknownEnumValue() { + assertThatThrownBy(() -> { + A a = Jackson3Mapper.INSTANCE.readerFor(A.class).readValue("{'enumValue': 'c'}"); + }).isInstanceOf(InvalidFormatException.class); + } + + @Test + public void readUnknownEnumValueLenient() { + A a = Jackson3Mapper.LENIENT.readerFor(A.class).readValue("{'enumValue': 'c'}"); + assertThat(a.enumValue).isNull(); + } + + @Test + public void write() throws JacksonException { + A a = new A(); + a.integer = 2; + a.optional = Optional.of(3); + assertThat(Jackson3Mapper.INSTANCE.writer().writeValueAsString(a)).isEqualTo("{\"integer\":2,\"optional\":3}"); + } + + @Test + public void writeWithEmptyOptional() throws JacksonException { + A a = new A(); + a.integer = 2; + a.optional = Optional.empty(); + 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 new file mode 100644 index 000000000..a7732b769 --- /dev/null +++ b/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/JsonArrayIteratorTest.java @@ -0,0 +1,287 @@ +package nl.vpro.jackson3; + +import lombok.*; +import lombok.extern.slf4j.Slf4j; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.*; + +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 simple() { + try (JsonArrayIterator iterator = JsonArrayIterator.builder(Change.class) + .objectMapper(Jackson3Mapper.INSTANCE.withSourceInLocation()) + .inputStream( + new ByteArrayInputStream(""" + { + "size": 1, + "changes": [ + { + "sequence": 724, + "revision": 2, + "mid": "POMS_NCRV_1138990", + "deleted": true + } + ] + } + """.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( + 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 -> + log.info("{}/{} :{}", it.getCount(), aLong, change) + ); + if (!change.isDeleted()) { + assertThat(change.getMedia()).isNotNull(); + assertThat(change.getMedia()).isInstanceOf(Map.class); + } + } + 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() { + 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); + } + } + + + @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.INSTANCE) + .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.LENIENT); + } + } + + @Test + public void writeArray() throws IOException, JSONException { + try (JsonArrayIterator it = JsonArrayIterator + .builder() + .inputStream(getClass().getResourceAsStream("/changes.json")) + .valueClass(Change.class) + .objectMapper(Jackson3Mapper.INSTANCE) + .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.LENIENT); + } + } + + @Test + public void illegalConstruction() { + assertThatThrownBy(() -> { + try (JsonArrayIterator js = JsonArrayIterator.builder().build()) { + log.info("{}", js); + } + }).isInstanceOf(NullPointerException.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() { + 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("INterrupted at 6 bytes, so that would have complete 2 objects"); + } + 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 + public 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..a0cd30a77 --- /dev/null +++ b/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/LenientBooleanDeserializerTest.java @@ -0,0 +1,53 @@ +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 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; + + } + JsonMapper mapper = new JsonMapper(); + + @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 { + 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/StringInstantToJsonTimestampTest.java b/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/StringInstantToJsonTimestampTest.java new file mode 100644 index 000000000..b3e1f9743 --- /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 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..cb64fe21f --- /dev/null +++ b/vpro-shared-jackson3/src/test/java/nl/vpro/jackson3/rs/JsonIdAdderBodyReaderTest.java @@ -0,0 +1,90 @@ +package nl.vpro.jackson3.rs; + +import tools.jackson.databind.json.JsonMapper; + +import java.io.ByteArrayInputStream; + +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 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(JsonMapper.class, MediaType.APPLICATION_JSON_TYPE)).thenReturn(new JacksonContextResolver()); + } + + @Test + public void testA() { + + 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() { + + 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() { + + Object o = idAdderInterceptor.readFrom(Object.class, + C.class, + null, + MediaType.APPLICATION_JSON_TYPE, null, new ByteArrayInputStream("{}".getBytes())); + assertThat(o).isInstanceOf(C.class); + } + +} 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/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/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-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-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 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/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..4b26597f0 --- /dev/null +++ b/vpro-shared-test/src/main/java/nl/vpro/test/util/jackson3/Jackson3TestUtil.java @@ -0,0 +1,469 @@ +/* + * Copyright (C) 2014 All rights reserved + * VPRO The Netherlands + */ +package nl.vpro.test.util.jackson3; + +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; +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 nl.vpro.jackson3.Jackson3Mapper; +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 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(); + + + public static void assertJsonEquals(String pref, CharSequence expected, CharSequence actual, JsonPointer... ignores) { + try { + if (ignores.length > 0) { + JsonNode actualJson = READER.readTree(actual.toString()); + remove(actualJson, ignores); + actual = MAPPER.writer().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; + } + JsonNode jsonNode = READER.readTree(String.valueOf(test)); + return MAPPER.writeValueAsString(jsonNode); + } + + + 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); + } + + + 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 = Jackson3Mapper.INSTANCE.mapper().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) { + return mapper.readValue(string, actual); + } + + @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-jackson2/src/main/java/nl/vpro/jackson2/Views.java b/vpro-shared-util/src/main/java/nl/vpro/jackson/Views.java similarity index 93% rename from vpro-shared-jackson2/src/main/java/nl/vpro/jackson2/Views.java rename to vpro-shared-util/src/main/java/nl/vpro/jackson/Views.java index d35402010..ba475af70 100644 --- a/vpro-shared-jackson2/src/main/java/nl/vpro/jackson2/Views.java +++ b/vpro-shared-util/src/main/java/nl/vpro/jackson/Views.java @@ -1,7 +1,8 @@ -package nl.vpro.jackson2; +package nl.vpro.jackson; 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. @@ -42,7 +43,7 @@ public interface ModelAndNormal extends Model, Normal { } /** - * New fields may be temporary marked 'ForwardPublisher'. Which will mean that {@link Jackson2Mapper#getBackwardsPublisherInstance()} will ignore them. + * 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. *

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/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); 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 { 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());