You are viewing a plain text version of this content. The canonical link for it is here.
Posted to server-dev@james.apache.org by bt...@apache.org on 2018/11/01 04:07:43 UTC
[08/17] james-project git commit: JAMES-2578 Add Serializers
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 <gd...@linagora.com>
Authored: Fri Oct 26 11:53:08 2018 +0200
Committer: Benoit Tellier <bt...@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-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org