JAMES-2578 Add Serializers
Project: http://git-wip-us.apache.org/repos/asf/james-project/repo Commit: http://git-wip-us.apache.org/repos/asf/james-project/commit/46ba928f Tree: http://git-wip-us.apache.org/repos/asf/james-project/tree/46ba928f Diff: http://git-wip-us.apache.org/repos/asf/james-project/diff/46ba928f Branch: refs/heads/master Commit: 46ba928f398e69c14fd5a6980ba80f6ac8de9e2b Parents: c97bc75 Author: Gautier DI FOLCO <gdifo...@linagora.com> Authored: Fri Oct 26 11:53:08 2018 +0200 Committer: Benoit Tellier <btell...@linagora.com> Committed: Thu Nov 1 11:02:43 2018 +0700 ---------------------------------------------------------------------- .../apache/mailet/ArbitrarySerializable.java | 48 ++ .../java/org/apache/mailet/AttributeValue.java | 146 +++++ .../main/java/org/apache/mailet/Serializer.java | 445 +++++++++++++++ .../org/apache/mailet/AttributeValueTest.java | 555 +++++++++++++++++++ 4 files changed, 1194 insertions(+) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/james-project/blob/46ba928f/mailet/api/src/main/java/org/apache/mailet/ArbitrarySerializable.java ---------------------------------------------------------------------- diff --git a/mailet/api/src/main/java/org/apache/mailet/ArbitrarySerializable.java b/mailet/api/src/main/java/org/apache/mailet/ArbitrarySerializable.java new file mode 100644 index 0000000..2bab770 --- /dev/null +++ b/mailet/api/src/main/java/org/apache/mailet/ArbitrarySerializable.java @@ -0,0 +1,48 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.mailet; + +import java.util.Optional; + +public interface ArbitrarySerializable { + public class Serializable { + private final AttributeValue<?> value; + private final Class<? extends Factory> factory; + + public Serializable(AttributeValue<?> value, Class<? extends Factory> factory) { + this.value = value; + this.factory = factory; + } + + public Class<? extends Factory> getFactory() { + return factory; + } + + public AttributeValue<?> getValue() { + return value; + } + } + + public interface Factory { + Optional<ArbitrarySerializable> deserialize(Serializable serializable); + } + + Serializable serialize(); +} http://git-wip-us.apache.org/repos/asf/james-project/blob/46ba928f/mailet/api/src/main/java/org/apache/mailet/AttributeValue.java ---------------------------------------------------------------------- diff --git a/mailet/api/src/main/java/org/apache/mailet/AttributeValue.java b/mailet/api/src/main/java/org/apache/mailet/AttributeValue.java index 82e520d..fd997c4 100644 --- a/mailet/api/src/main/java/org/apache/mailet/AttributeValue.java +++ b/mailet/api/src/main/java/org/apache/mailet/AttributeValue.java @@ -19,11 +19,23 @@ package org.apache.mailet; +import java.io.IOException; +import java.io.Serializable; +import java.net.URL; +import java.util.Collection; +import java.util.Map; import java.util.Objects; +import java.util.Optional; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.MoreObjects; /** * Strong typing for attribute value, which represents the value of an attribute stored in a mail. @@ -31,6 +43,127 @@ import com.fasterxml.jackson.databind.node.ObjectNode; * @since Mailet API v3.2 */ public class AttributeValue<T> { + private static final Logger LOGGER = LoggerFactory.getLogger(AttributeValue.class); + + public static AttributeValue<Boolean> of(Boolean value) { + return new AttributeValue<>(value, Serializer.BOOLEAN_SERIALIZER); + } + + public static AttributeValue<String> of(String value) { + return new AttributeValue<>(value, Serializer.STRING_SERIALIZER); + } + + public static AttributeValue<Integer> of(Integer value) { + return new AttributeValue<>(value, Serializer.INT_SERIALIZER); + } + + public static AttributeValue<Long> of(Long value) { + return new AttributeValue<>(value, Serializer.LONG_SERIALIZER); + } + + public static AttributeValue<Float> of(Float value) { + return new AttributeValue<>(value, Serializer.FLOAT_SERIALIZER); + } + + public static AttributeValue<Double> of(Double value) { + return new AttributeValue<>(value, Serializer.DOUBLE_SERIALIZER); + } + + public static AttributeValue<ArbitrarySerializable> of(ArbitrarySerializable value) { + return new AttributeValue<>(value, Serializer.ARIBITRARY_SERIALIZABLE_SERIALIZER); + } + + public static AttributeValue<URL> of(URL value) { + return new AttributeValue<>(value, Serializer.URL_SERIALIZER); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + public static AttributeValue<Collection<AttributeValue<?>>> of(Collection<AttributeValue<?>> value) { + return new AttributeValue<>(value, new Serializer.CollectionSerializer()); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + public static AttributeValue<Map<String, AttributeValue<?>>> of(Map<String, AttributeValue<?>> value) { + return new AttributeValue<>(value, new Serializer.MapSerializer()); + } + + public static AttributeValue<Serializable> ofSerializable(Serializable value) { + return new AttributeValue<>(value, new Serializer.FSTSerializer()); + } + + @SuppressWarnings("unchecked") + public static AttributeValue<?> ofAny(Object value) { + if (value instanceof Boolean) { + return of((Boolean) value); + } + if (value instanceof String) { + return of((String) value); + } + if (value instanceof Integer) { + return of((Integer) value); + } + if (value instanceof Long) { + return of((Long) value); + } + if (value instanceof Float) { + return of((Float) value); + } + if (value instanceof Double) { + return of((Double) value); + } + if (value instanceof Collection<?>) { + return of(((Collection<AttributeValue<?>>) value)); + } + if (value instanceof Map<?,?>) { + return of(((Map<String, AttributeValue<?>>) value)); + } + if (value instanceof ArbitrarySerializable) { + return of((ArbitrarySerializable) value); + } + if (value instanceof URL) { + return of((URL) value); + } + if (value instanceof Serializable) { + return ofSerializable((Serializable) value); + } + throw new IllegalArgumentException(value.getClass().toString() + " should at least be Serializable"); + } + + public static AttributeValue<?> fromJsonString(String json) throws IOException { + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode tree = objectMapper.readTree(json); + return fromJson(tree); + } + + public static Optional<AttributeValue<?>> optionalFromJsonString(String json) { + try { + return Optional.of(fromJsonString(json)); + } catch (IOException e) { + LOGGER.error("Error while deserializing '" + json + "'", e); + return Optional.empty(); + } + } + + @VisibleForTesting + static AttributeValue<?> fromJson(JsonNode input) { + return Optional.ofNullable(input) + .filter(ObjectNode.class::isInstance) + .map(ObjectNode.class::cast) + .flatMap(AttributeValue::deserialize) + .map(AttributeValue::ofAny) + .orElseThrow(() -> new IllegalStateException("unable to deserialize " + input.toString())); + } + + public static Optional<?> deserialize(ObjectNode fields) { + return Optional.ofNullable(fields.get("serializer")) + .flatMap(serializer -> Optional.ofNullable(fields.get("value")) + .flatMap(value -> findSerializerAndDeserialize(serializer, value))); + } + + public static Optional<?> findSerializerAndDeserialize(JsonNode serializer, JsonNode value) { + return Serializer.Registry.find(serializer.asText()) + .flatMap(s -> s.deserialize(value)); + } private final T value; private final Serializer<T> serializer; @@ -44,6 +177,11 @@ public class AttributeValue<T> { return value; } + //FIXME : poor performance + public AttributeValue<T> duplicate() { + return (AttributeValue<T>) fromJson(toJson()); + } + public JsonNode toJson() { ObjectNode serialized = JsonNodeFactory.instance.objectNode(); serialized.put("serializer", serializer.getName()); @@ -70,4 +208,12 @@ public class AttributeValue<T> { public final int hashCode() { return Objects.hash(value, serializer); } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("value", value) + .add("serializer", serializer.getName()) + .toString(); + } } http://git-wip-us.apache.org/repos/asf/james-project/blob/46ba928f/mailet/api/src/main/java/org/apache/mailet/Serializer.java ---------------------------------------------------------------------- diff --git a/mailet/api/src/main/java/org/apache/mailet/Serializer.java b/mailet/api/src/main/java/org/apache/mailet/Serializer.java new file mode 100644 index 0000000..d71a01c --- /dev/null +++ b/mailet/api/src/main/java/org/apache/mailet/Serializer.java @@ -0,0 +1,445 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.mailet; + +import java.io.IOException; +import java.io.Serializable; +import java.io.UncheckedIOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Stream; + +import org.apache.james.util.streams.Iterators; +import org.nustaq.serialization.FSTConfiguration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.BooleanNode; +import com.fasterxml.jackson.databind.node.DoubleNode; +import com.fasterxml.jackson.databind.node.FloatNode; +import com.fasterxml.jackson.databind.node.IntNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.LongNode; +import com.fasterxml.jackson.databind.node.NumericNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; + +/** + * Controlled Json serialization/deserialization + * + * @since Mailet API v3.2 + */ +public interface Serializer<T> { + JsonNode serialize(T object); + + Optional<T> deserialize(JsonNode json); + + String getName(); + + class Registry { + + private static ImmutableMap<String, Serializer<?>> serializers; + + static { + serializers = Stream + .<Serializer<?>>of( + BOOLEAN_SERIALIZER, + STRING_SERIALIZER, + INT_SERIALIZER, + LONG_SERIALIZER, + FLOAT_SERIALIZER, + DOUBLE_SERIALIZER, + ARIBITRARY_SERIALIZABLE_SERIALIZER, + URL_SERIALIZER, + new CollectionSerializer<>(), + new MapSerializer<>(), + new FSTSerializer()) + .collect(ImmutableMap.toImmutableMap(Serializer::getName, Function.identity())); + } + + static Optional<Serializer<?>> find(String name) { + return Optional.ofNullable(serializers.get(name)); + } + } + + class BooleanSerializer implements Serializer<Boolean> { + @Override + public JsonNode serialize(Boolean object) { + return BooleanNode.valueOf(object); + } + + @Override + public Optional<Boolean> deserialize(JsonNode json) { + if (json instanceof BooleanNode) { + return Optional.of(json.asBoolean()); + } else { + return Optional.empty(); + } + } + + @Override + public String getName() { + return "BooleanSerializer"; + } + + @Override + public boolean equals(Object other) { + return this.getClass() == other.getClass(); + } + } + + Serializer<Boolean> BOOLEAN_SERIALIZER = new BooleanSerializer(); + + class StringSerializer implements Serializer<String> { + @Override + public JsonNode serialize(String object) { + return TextNode.valueOf(object); + } + + @Override + public Optional<String> deserialize(JsonNode json) { + if (json instanceof TextNode) { + return Optional.of(json.asText()); + } else { + return Optional.empty(); + } + } + + @Override + public String getName() { + return "StringSerializer"; + } + + @Override + public boolean equals(Object other) { + return this.getClass() == other.getClass(); + } + } + + Serializer<String> STRING_SERIALIZER = new StringSerializer(); + + class IntSerializer implements Serializer<Integer> { + @Override + public JsonNode serialize(Integer object) { + return IntNode.valueOf(object); + } + + @Override + public Optional<Integer> deserialize(JsonNode json) { + if (json instanceof NumericNode) { + return Optional.of(json.asInt()); + } else { + return Optional.empty(); + } + } + + @Override + public String getName() { + return "IntSerializer"; + } + + @Override + public boolean equals(Object other) { + return this.getClass() == other.getClass(); + } + } + + Serializer<Integer> INT_SERIALIZER = new IntSerializer(); + + class LongSerializer implements Serializer<Long> { + @Override + public JsonNode serialize(Long object) { + return LongNode.valueOf(object); + } + + @Override + public Optional<Long> deserialize(JsonNode json) { + if (json instanceof NumericNode) { + return Optional.of(json.asLong()); + } else { + return Optional.empty(); + } + } + + @Override + public String getName() { + return "LongSerializer"; + } + + @Override + public boolean equals(Object other) { + return this.getClass() == other.getClass(); + } + } + + Serializer<Long> LONG_SERIALIZER = new LongSerializer(); + + class FloatSerializer implements Serializer<Float> { + @Override + public JsonNode serialize(Float object) { + return FloatNode.valueOf(object); + } + + @Override + public Optional<Float> deserialize(JsonNode json) { + if (json instanceof NumericNode) { + return Optional.of(json.floatValue()); + } else { + return Optional.empty(); + } + } + + @Override + public String getName() { + return "FloatSerializer"; + } + + @Override + public boolean equals(Object other) { + return this.getClass() == other.getClass(); + } + } + + Serializer<Float> FLOAT_SERIALIZER = new FloatSerializer(); + + class DoubleSerializer implements Serializer<Double> { + @Override + public JsonNode serialize(Double object) { + return DoubleNode.valueOf(object); + } + + @Override + public Optional<Double> deserialize(JsonNode json) { + if (json instanceof NumericNode) { + return Optional.of(json.asDouble()); + } else { + return Optional.empty(); + } + } + + @Override + public String getName() { + return "DoubleSerializer"; + } + + @Override + public boolean equals(Object other) { + return this.getClass() == other.getClass(); + } + } + + Serializer<Double> DOUBLE_SERIALIZER = new DoubleSerializer(); + + class ArbitrarySerializableSerializer implements Serializer<ArbitrarySerializable> { + private static final Logger LOGGER = LoggerFactory.getLogger(ArbitrarySerializableSerializer.class); + + @Override + public JsonNode serialize(ArbitrarySerializable serializable) { + ArbitrarySerializable.Serializable serialized = serializable.serialize(); + ObjectNode serializedJson = JsonNodeFactory.instance.objectNode(); + serializedJson.put("factory", serialized.getFactory().getName()); + serializedJson.replace("value", serialized.getValue().toJson()); + return serializedJson; + } + + @Override + public Optional<ArbitrarySerializable> deserialize(JsonNode json) { + return Optional.of(json) + .filter(ObjectNode.class::isInstance) + .map(ObjectNode.class::cast) + .flatMap(this::instantiate); + } + + public Optional<ArbitrarySerializable> instantiate(ObjectNode fields) { + return Optional.ofNullable(fields.get("factory")) + .flatMap(serializer -> + Optional.ofNullable(fields.get("value")) + .flatMap(value -> deserialize(serializer.asText(), AttributeValue.fromJson(value)))); + } + + @SuppressWarnings("unchecked") + private Optional<ArbitrarySerializable> deserialize(String serializer, AttributeValue<?> value) { + try { + Class<?> factoryClass = Class.forName(serializer); + if (ArbitrarySerializable.Factory.class.isAssignableFrom(factoryClass)) { + ArbitrarySerializable.Factory factory = (ArbitrarySerializable.Factory) factoryClass.newInstance(); + return factory.deserialize(new ArbitrarySerializable.Serializable(value, (Class<ArbitrarySerializable.Factory>) factoryClass)); + } + } catch (Exception e) { + LOGGER.error("Error while deserializing", e); + } + + return Optional.empty(); + } + + @Override + public String getName() { + return "ArbitrarySerializableSerializer"; + } + + @Override + public boolean equals(Object other) { + return this.getClass() == other.getClass(); + } + } + + Serializer<ArbitrarySerializable> ARIBITRARY_SERIALIZABLE_SERIALIZER = new ArbitrarySerializableSerializer(); + + class UrlSerializer implements Serializer<URL> { + @Override + public JsonNode serialize(URL object) { + return STRING_SERIALIZER.serialize(object.toString()); + } + + @Override + public Optional<URL> deserialize(JsonNode json) { + return STRING_SERIALIZER.deserialize(json).flatMap(url -> { + try { + return Optional.of(new URL(url)); + } catch (MalformedURLException e) { + return Optional.empty(); + } + }); + } + + @Override + public String getName() { + return "UrlSerializer"; + } + + @Override + public boolean equals(Object other) { + return this.getClass() == other.getClass(); + } + } + + Serializer<URL> URL_SERIALIZER = new UrlSerializer(); + + class CollectionSerializer<U> implements Serializer<Collection<AttributeValue<U>>> { + @Override + public JsonNode serialize(Collection<AttributeValue<U>> object) { + List<JsonNode> jsons = object.stream() + .map(AttributeValue::toJson) + .collect(ImmutableList.toImmutableList()); + return new ArrayNode(JsonNodeFactory.instance, jsons); + } + + @Override + public Optional<Collection<AttributeValue<U>>> deserialize(JsonNode json) { + if (json instanceof ArrayNode) { + return Optional.of(Iterators.toStream(json.elements()) + .map(value -> (AttributeValue<U>) AttributeValue.fromJson(value)) + .collect(ImmutableList.toImmutableList())); + } else { + return Optional.empty(); + } + } + + @Override + public String getName() { + return "CollectionSerializer"; + } + + @Override + public boolean equals(Object other) { + return this.getClass() == other.getClass(); + } + } + + class MapSerializer<U> implements Serializer<Map<String, AttributeValue<U>>> { + @Override + public JsonNode serialize(Map<String, AttributeValue<U>> object) { + Map<String, JsonNode> jsonMap = object.entrySet().stream() + .collect(ImmutableMap.toImmutableMap(Entry::getKey, entry -> entry.getValue().toJson())); + return new ObjectNode(JsonNodeFactory.instance, jsonMap); + } + + @Override + public Optional<Map<String, AttributeValue<U>>> deserialize(JsonNode json) { + if (json instanceof ObjectNode) { + return Optional.of(Iterators.toStream(json.fields()) + .collect(ImmutableMap.toImmutableMap( + Map.Entry::getKey, + entry -> (AttributeValue<U>) AttributeValue.fromJson(entry.getValue()) + ))); + } else { + return Optional.empty(); + } + } + + @Override + public String getName() { + return "MapSerializer"; + } + + @Override + public boolean equals(Object other) { + return this.getClass() == other.getClass(); + } + } + + class FSTSerializer implements Serializer<Serializable> { + static final FSTConfiguration CONFIGURATION = FSTConfiguration.createJsonConfiguration(); + + @Override + public JsonNode serialize(Serializable object) { + FSTConfiguration conf = CONFIGURATION; + String json = conf.asJsonString(object); + try { + return new ObjectMapper().reader().readTree(json); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @Override + public Optional<Serializable> deserialize(JsonNode json) { + FSTConfiguration conf = FSTConfiguration.createJsonConfiguration(); + try { + return Optional.of((Serializable) conf.asObject(new ObjectMapper().writer().writeValueAsBytes(json))); + } catch (JsonProcessingException e) { + throw new UncheckedIOException(e); + } + } + + @Override + public String getName() { + return "FSTSerializer"; + } + + @Override + public boolean equals(Object other) { + return this.getClass() == other.getClass(); + } + } + +} http://git-wip-us.apache.org/repos/asf/james-project/blob/46ba928f/mailet/api/src/test/java/org/apache/mailet/AttributeValueTest.java ---------------------------------------------------------------------- diff --git a/mailet/api/src/test/java/org/apache/mailet/AttributeValueTest.java b/mailet/api/src/test/java/org/apache/mailet/AttributeValueTest.java new file mode 100644 index 0000000..f741d05 --- /dev/null +++ b/mailet/api/src/test/java/org/apache/mailet/AttributeValueTest.java @@ -0,0 +1,555 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ +package org.apache.mailet; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.core.io.JsonEOFException; +import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; + +import nl.jqno.equalsverifier.EqualsVerifier; + +class AttributeValueTest { + @Test + void shouldRespectBeanContract() { + EqualsVerifier.forClass(AttributeValue.class).verify(); + } + + @Test + void ofShouldAcceptNullValue() { + AttributeValue<String> attributeValue = AttributeValue.of((String) null); + + assertThat(attributeValue.getValue()).isNull(); + } + + @Nested + class StringSerialization { + @Test + void stringShouldBeSerializedAndBack() { + AttributeValue<String> expected = AttributeValue.of("value"); + + JsonNode json = expected.toJson(); + AttributeValue<?> actual = AttributeValue.fromJson(json); + + assertThat(actual).isEqualTo(expected); + } + + @Test + void emptyStringShouldBeSerializedAndBack() { + AttributeValue<String> expected = AttributeValue.of(""); + + JsonNode json = expected.toJson(); + AttributeValue<?> actual = AttributeValue.fromJson(json); + + assertThat(actual).isEqualTo(expected); + } + + @Disabled("Failing!") + @Test + void nullStringShouldBeSerializedAndBack() { + AttributeValue<String> expected = AttributeValue.of((String) null); + + JsonNode json = expected.toJson(); + AttributeValue<?> actual = AttributeValue.fromJson(json); + + assertThat(actual).isEqualTo(expected); + } + + @Test + void fromJsonStringShouldReturnStringAttributeValueWhenString() throws Exception { + AttributeValue<String> expected = AttributeValue.of("value"); + + AttributeValue<?> actual = AttributeValue.fromJsonString("{\"serializer\":\"StringSerializer\",\"value\": \"value\"}"); + + assertThat(actual).isEqualTo(expected); + } + + @Test + void fromJsonStringShouldThrowOnMalformedFormattedJson() { + assertThatIllegalStateException() + .isThrownBy(() -> AttributeValue.fromJsonString("{\"serializer\":\"StringSerializer\",\"value\": []}")); + } + } + + @Nested + class BooleanSerialization { + @Test + void trueShouldBeSerializedAndBack() { + AttributeValue<Boolean> expected = AttributeValue.of(true); + + JsonNode json = expected.toJson(); + AttributeValue<?> actual = AttributeValue.fromJson(json); + + assertThat(actual).isEqualTo(expected); + } + + @Test + void falseShouldBeSerializedAndBack() { + AttributeValue<Boolean> expected = AttributeValue.of(true); + + JsonNode json = expected.toJson(); + AttributeValue<?> actual = AttributeValue.fromJson(json); + + assertThat(actual).isEqualTo(expected); + } + + @Disabled("Failing") + @Test + void nullBooleanShouldBeSerializedAndBack() { + AttributeValue<Boolean> expected = AttributeValue.of((Boolean) null); + + JsonNode json = expected.toJson(); + AttributeValue<?> actual = AttributeValue.fromJson(json); + + assertThat(actual).isEqualTo(expected); + } + + @Test + void fromJsonStringShouldReturnBooleanAttributeValueWhenBoolean() throws Exception { + AttributeValue<Boolean> expected = AttributeValue.of(true); + + AttributeValue<?> actual = AttributeValue.fromJsonString("{\"serializer\":\"BooleanSerializer\",\"value\": true}"); + + assertThat(actual).isEqualTo(expected); + } + + @Test + void fromJsonStringShouldThrowOnMalformedFormattedJson() { + assertThatIllegalStateException() + .isThrownBy(() -> AttributeValue.fromJsonString("{\"serializer\":\"BooleanSerializer\",\"value\": []}")); + } + } + + @Nested + class IntegerSerialization { + @Test + void intShouldBeSerializedAndBack() { + AttributeValue<Integer> expected = AttributeValue.of(42); + + JsonNode json = expected.toJson(); + AttributeValue<?> actual = AttributeValue.fromJson(json); + + assertThat(actual).isEqualTo(expected); + } + + @Disabled("Failing") + @Test + void nullIntShouldBeSerializedAndBack() { + AttributeValue<Integer> expected = AttributeValue.of((Integer) null); + + JsonNode json = expected.toJson(); + AttributeValue<?> actual = AttributeValue.fromJson(json); + + assertThat(actual).isEqualTo(expected); + } + + @Test + void fromJsonStringShouldReturnIntAttributeValueWhenInt() throws Exception { + AttributeValue<Integer> expected = AttributeValue.of(42); + + AttributeValue<?> actual = AttributeValue.fromJsonString("{\"serializer\":\"IntSerializer\",\"value\": 42}"); + + assertThat(actual).isEqualTo(expected); + } + + @Test + void fromJsonStringShouldThrowOnMalformedFormattedJson() { + assertThatIllegalStateException() + .isThrownBy(() -> AttributeValue.fromJsonString("{\"serializer\":\"IntSerializer\",\"value\": []}")); + } + } + + @Nested + class LongSerialization { + @Test + void longShouldBeSerializedAndBack() { + AttributeValue<Long> expected = AttributeValue.of(42L); + + JsonNode json = expected.toJson(); + AttributeValue<?> actual = AttributeValue.fromJson(json); + + assertThat(actual).isEqualTo(expected); + } + + @Disabled("Failing") + @Test + void nullLongShouldBeSerializedAndBack() { + AttributeValue<Long> expected = AttributeValue.of((Long) null); + + JsonNode json = expected.toJson(); + AttributeValue<?> actual = AttributeValue.fromJson(json); + + assertThat(actual).isEqualTo(expected); + } + + @Test + void fromJsonStringShouldReturnLongAttributeValueWhenLong() throws Exception { + AttributeValue<Long> expected = AttributeValue.of(42L); + + AttributeValue<?> actual = AttributeValue.fromJsonString("{\"serializer\":\"LongSerializer\",\"value\":42}"); + + assertThat(actual).isEqualTo(expected); + } + + @Test + void fromJsonStringShouldThrowOnMalformedFormattedJson() { + assertThatIllegalStateException() + .isThrownBy(() -> AttributeValue.fromJsonString("{\"serializer\":\"LongSerializer\",\"value\": []}")); + } + } + + @Nested + class FloatSerialization { + @Test + void floatShouldBeSerializedAndBack() { + AttributeValue<Float> expected = AttributeValue.of(1.0f); + + JsonNode json = expected.toJson(); + AttributeValue<?> actual = AttributeValue.fromJson(json); + + assertThat(actual).isEqualTo(expected); + } + + @Disabled("Failing") + @Test + void nullFloatShouldBeSerializedAndBack() { + AttributeValue<Float> expected = AttributeValue.of((Float) null); + + JsonNode json = expected.toJson(); + AttributeValue<?> actual = AttributeValue.fromJson(json); + + assertThat(actual).isEqualTo(expected); + } + + @Test + void fromJsonStringShouldReturnFloatAttributeValueWhenFloat() throws Exception { + AttributeValue<Float> expected = AttributeValue.of(1.0f); + + AttributeValue<?> actual = AttributeValue.fromJsonString("{\"serializer\":\"FloatSerializer\",\"value\":1.0}"); + + assertThat(actual).isEqualTo(expected); + } + + @Test + void fromJsonStringShouldThrowOnMalformedFormattedJson() { + assertThatIllegalStateException() + .isThrownBy(() -> AttributeValue.fromJsonString("{\"serializer\":\"FloatSerializer\",\"value\": []}")); + } + } + + @Nested + class DoubleSerialization { + @Test + void doubleShouldBeSerializedAndBack() { + AttributeValue<Double> expected = AttributeValue.of(1.0d); + + JsonNode json = expected.toJson(); + AttributeValue<?> actual = AttributeValue.fromJson(json); + + assertThat(actual).isEqualTo(expected); + } + + @Disabled("Failing") + @Test + void nullDoubleShouldBeSerializedAndBack() { + AttributeValue<Double> expected = AttributeValue.of((Double) null); + + JsonNode json = expected.toJson(); + AttributeValue<?> actual = AttributeValue.fromJson(json); + + assertThat(actual).isEqualTo(expected); + } + + @Test + void fromJsonStringShouldReturnDoubleAttributeValueWhenDouble() throws Exception { + AttributeValue<Double> expected = AttributeValue.of(1.0); + + AttributeValue<?> actual = AttributeValue.fromJsonString("{\"serializer\":\"DoubleSerializer\",\"value\":1.0}"); + + assertThat(actual).isEqualTo(expected); + } + + @Test + void fromJsonStringShouldThrowOnMalformedFormattedJson() { + assertThatIllegalStateException() + .isThrownBy(() -> AttributeValue.fromJsonString("{\"serializer\":\"DoubleSerializer\",\"value\": []}")); + } + } + + @Nested + class QueueSerializableTest { + @Test + void queueSerializableShouldBeSerializedAndBack() { + AttributeValue<ArbitrarySerializable> expected = AttributeValue.of(new TestArbitrarySerializable(42)); + + JsonNode json = expected.toJson(); + AttributeValue<?> actual = AttributeValue.fromJson(json); + + assertThat(actual).isEqualTo(expected); + } + + + @Test + void fromJsonStringShouldReturnQueueSerializableAttributeValueWhenQueueSerializable() throws Exception { + AttributeValue<ArbitrarySerializable> expected = AttributeValue.of(new TestArbitrarySerializable(42)); + + AttributeValue<?> actual = AttributeValue.fromJsonString("{\"serializer\":\"ArbitrarySerializableSerializer\",\"value\":{\"factory\":\"org.apache.mailet.AttributeValueTest$TestArbitrarySerializable$Factory\",\"value\":{\"serializer\":\"IntSerializer\",\"value\":42}}}"); + + assertThat(actual).isEqualTo(expected); + } + } + + @Nested + class UrlSerialization { + @Test + void urlShouldBeSerializedAndBack() throws MalformedURLException { + AttributeValue<URL> expected = AttributeValue.of(new URL("https://james.apache.org/")); + + JsonNode json = expected.toJson(); + AttributeValue<?> actual = AttributeValue.fromJson(json); + + assertThat(actual).isEqualTo(expected); + } + + @Disabled("Failing!") + @Test + void nullURLShouldBeSerializedAndBack() { + AttributeValue<URL> expected = AttributeValue.of((URL) null); + + JsonNode json = expected.toJson(); + AttributeValue<?> actual = AttributeValue.fromJson(json); + + assertThat(actual).isEqualTo(expected); + } + + @Test + void fromJsonStringShouldReturnUrlAttributeValueWhenUrl() throws Exception { + AttributeValue<URL> expected = AttributeValue.of(new URL("https://james.apache.org/")); + + AttributeValue<?> actual = AttributeValue.fromJsonString("{\"serializer\":\"UrlSerializer\",\"value\": \"https://james.apache.org/\"}"); + + assertThat(actual).isEqualTo(expected); + } + + @Test + void fromJsonStringShouldThrowOnMalformedUrl() { + assertThatIllegalStateException() + .isThrownBy(() -> AttributeValue.fromJsonString("{\"serializer\":\"UrlSerializer\",\"value\": \"htps://bad/\"}")); + } + + @Test + void fromJsonStringShouldThrowOnMalformedFormattedJson() { + assertThatIllegalStateException() + .isThrownBy(() -> AttributeValue.fromJsonString("{\"serializer\":\"UrlSerializer\",\"value\": {}}")); + } + } + + @Nested + class ListSerialization { + @Disabled("Failing!") + @Test + void nullStringListShouldBeSerializedAndBack() { + AttributeValue<?> expected = AttributeValue.ofAny((List<String>) null); + + JsonNode json = expected.toJson(); + AttributeValue<?> actual = AttributeValue.fromJson(json); + + assertThat(actual).isEqualTo(expected); + } + + @Test + void emptyStringListShouldBeSerializedAndBack() { + AttributeValue<?> expected = AttributeValue.ofAny(ImmutableList.<String>of()); + + JsonNode json = expected.toJson(); + AttributeValue<?> actual = AttributeValue.fromJson(json); + + assertThat(actual).isEqualTo(expected); + } + + @Test + void listShouldBeSerializedAndBack() { + AttributeValue<?> expected = AttributeValue.of(ImmutableList.of(AttributeValue.of("first"), AttributeValue.of("second"))); + + JsonNode json = expected.toJson(); + AttributeValue<?> actual = AttributeValue.fromJson(json); + + assertThat(actual).isEqualTo(expected); + } + + @Test + void fromJsonStringShouldReturnEmptyListAttributeValueWhenEmptyArray() throws Exception { + AttributeValue<?> expected = AttributeValue.of(ImmutableList.of()); + + AttributeValue<?> actual = AttributeValue.fromJsonString("{\"serializer\":\"CollectionSerializer\",\"value\": []}"); + + assertThat(actual).isEqualTo(expected); + } + + @Test + void fromJsonStringShouldReturnListAttributeValueWhenArray() throws Exception { + AttributeValue<?> expected = AttributeValue.of(ImmutableList.of(AttributeValue.of("first"), AttributeValue.of("second"))); + + AttributeValue<?> actual = AttributeValue.fromJsonString("{\"serializer\":\"CollectionSerializer\",\"value\":[{\"serializer\":\"StringSerializer\",\"value\":\"first\"},{\"serializer\":\"StringSerializer\",\"value\":\"second\"}]}"); + + assertThat(actual).isEqualTo(expected); + } + + @Test + void fromJsonStringShouldThrowOnMalformedFormattedJson() { + assertThatIllegalStateException() + .isThrownBy(() -> AttributeValue.fromJsonString("{\"serializer\":\"CollectionSerializer\",\"value\": {}}")); + } + } + + @Nested + class MapSerialization { + @Disabled("Failing!") + @Test + void nullMapShouldBeSerializedAndBack() { + AttributeValue<?> expected = AttributeValue.of((Map) null); + JsonNode json = expected.toJson(); + AttributeValue<?> actual = AttributeValue.fromJson(json); + + assertThat(actual).isEqualTo(expected); + } + + @Test + void emptyMapShouldBeSerializedAndBack() { + AttributeValue<?> expected = AttributeValue.of(ImmutableMap.of()); + JsonNode json = expected.toJson(); + AttributeValue<?> actual = AttributeValue.fromJson(json); + + assertThat(actual).isEqualTo(expected); + } + + @Test + void mapWithPrimitiveTypesShouldBeSerializedAndBack() { + AttributeValue<?> expected = AttributeValue.of(ImmutableMap.of("a", AttributeValue.of("value"), "b", AttributeValue.of(12))); + JsonNode json = expected.toJson(); + AttributeValue<?> actual = AttributeValue.fromJson(json); + + assertThat(actual).isEqualTo(expected); + } + + @Test + void fromJsonStringShouldReturnEmptyMapWhenEmptyMap() throws Exception { + AttributeValue<Map<String, AttributeValue<?>>> expected = AttributeValue.of(ImmutableMap.of()); + + AttributeValue<?> actual = AttributeValue.fromJsonString("{\"serializer\":\"MapSerializer\",\"value\":{}}"); + + assertThat(actual).isEqualTo(expected); + } + + @Test + void fromJsonStringShouldReturnMapWhenMap() throws Exception { + AttributeValue<Map<String, AttributeValue<?>>> expected = AttributeValue.of( + ImmutableMap.of( + "a", AttributeValue.of(1), + "b", AttributeValue.of(2))); + + AttributeValue<?> actual = AttributeValue.fromJsonString("{\"serializer\":\"MapSerializer\",\"value\":{\"a\":{\"serializer\":\"IntSerializer\",\"value\":1},\"b\":{\"serializer\":\"IntSerializer\",\"value\":2}}}"); + + assertThat(actual).isEqualTo(expected); + } + + @Test + void fromJsonStringShouldThrowOnMalformedFormattedJson() { + assertThatIllegalStateException() + .isThrownBy(() -> AttributeValue.fromJsonString("{\"serializer\":\"MapSerializer\",\"value\": []}")); + } + } + + @Test + void fromJsonStringShouldThrowOnUnknownSerializer() { + assertThatIllegalStateException() + .isThrownBy(() -> AttributeValue.fromJsonString("{\"serializer\":\"unknown\",\"value\": \"value\"}")); + } + + @Test + void fromJsonStringShouldThrowOnBrokenJson() { + assertThatThrownBy(() ->AttributeValue.fromJsonString("{\"serializer\":\"StringSerializer\",\"value\": \"Missing closing bracket\"")) + .isInstanceOf(JsonEOFException.class); + } + + @Test + void fromJsonStringShouldThrowOnMissingSerializerField() { + assertThatIllegalStateException() + .isThrownBy(() -> AttributeValue.fromJsonString("{\"value\": \"value\"}")); + } + + @Test + void fromJsonStringShouldThrowOnMissingValueField() { + assertThatIllegalStateException() + .isThrownBy(() -> AttributeValue.fromJsonString("{\"serializer\":\"MapSerializer\"}")); + } + + private static class TestArbitrarySerializable implements ArbitrarySerializable { + public static class Factory implements ArbitrarySerializable.Factory { + @Override + public Optional<ArbitrarySerializable> deserialize(Serializable serializable) { + return Optional.of(serializable.getValue().value()) + .filter(Integer.class::isInstance) + .map(Integer.class::cast) + .map(TestArbitrarySerializable::new); + } + } + + private final Integer value; + + public TestArbitrarySerializable(Integer value) { + this.value = value; + } + + @Override + public Serializable serialize() { + return new Serializable(AttributeValue.of(value), Factory.class); + } + + @Override + public final boolean equals(Object o) { + if (o instanceof TestArbitrarySerializable) { + TestArbitrarySerializable that = (TestArbitrarySerializable) o; + + return Objects.equals(this.value, that.value); + } + return false; + } + + @Override + public final int hashCode() { + return Objects.hash(value); + } + } +} --------------------------------------------------------------------- To unsubscribe, e-mail: server-dev-unsubscr...@james.apache.org For additional commands, e-mail: server-dev-h...@james.apache.org