You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@james.apache.org by bt...@apache.org on 2023/01/06 03:02:49 UTC
[james-project] 10/12: JAMES-3771 Prevent SPI lookups on the HTTP eventloop
This is an automated email from the ASF dual-hosted git repository.
btellier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git
commit 49c358a39dad08f3c238716595d7795447152aa5
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Thu Dec 15 13:59:24 2022 +0700
JAMES-3771 Prevent SPI lookups on the HTTP eventloop
---
.../james/backends/opensearch/ClientProvider.java | 2 +-
.../opensearch/json/JsonpDeserializerBase.java | 397 +++++++++++++++++++++
.../backends/opensearch/json/JsonpMapperBase.java | 102 ++++++
.../james/backends/opensearch/json/JsonpUtils.java | 207 +++++++++++
.../json/UnexpectedJsonEventException.java | 40 +++
.../json/jackson/JacksonJsonProvider.java | 298 ++++++++++++++++
.../json/jackson/JacksonJsonpGenerator.java | 374 +++++++++++++++++++
.../json/jackson/JacksonJsonpLocation.java | 54 +++
.../json/jackson/JacksonJsonpMapper.java | 123 +++++++
.../json/jackson/JacksonJsonpParser.java | 313 ++++++++++++++++
.../opensearch/json/jackson/JacksonUtils.java | 43 +++
.../opensearch/json/jackson/JsonValueParser.java | 108 ++++++
.../backends/opensearch/json/package-info.java | 29 ++
13 files changed, 2089 insertions(+), 1 deletion(-)
diff --git a/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/ClientProvider.java b/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/ClientProvider.java
index 89a104bb8b..abd0fc9945 100644
--- a/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/ClientProvider.java
+++ b/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/ClientProvider.java
@@ -44,9 +44,9 @@ import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.nio.client.HttpAsyncClientBuilder;
import org.apache.http.ssl.SSLContextBuilder;
import org.apache.http.ssl.TrustStrategy;
+import org.apache.james.backends.opensearch.json.jackson.JacksonJsonpMapper;
import org.apache.james.util.concurrent.NamedThreadFactory;
import org.opensearch.client.RestClient;
-import org.opensearch.client.json.jackson.JacksonJsonpMapper;
import org.opensearch.client.opensearch.OpenSearchAsyncClient;
import org.opensearch.client.transport.rest_client.RestClientTransport;
import org.slf4j.Logger;
diff --git a/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/JsonpDeserializerBase.java b/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/JsonpDeserializerBase.java
new file mode 100644
index 0000000000..c790e4ab2c
--- /dev/null
+++ b/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/JsonpDeserializerBase.java
@@ -0,0 +1,397 @@
+/****************************************************************
+ * 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.james.backends.opensearch.json;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.opensearch.client.json.JsonpDeserializer;
+import org.opensearch.client.json.JsonpMapper;
+
+import jakarta.json.JsonNumber;
+import jakarta.json.JsonValue;
+import jakarta.json.stream.JsonParser;
+import jakarta.json.stream.JsonParser.Event;
+import jakarta.json.stream.JsonParsingException;
+
+/**
+ * Base class for {@link JsonpDeserializer} implementations that accept a set of JSON events known at instanciation time.
+ */
+public abstract class JsonpDeserializerBase<V> implements JsonpDeserializer<V> {
+
+ private final EnumSet<Event> acceptedEvents;
+ private final EnumSet<Event> nativeEvents;
+
+ protected JsonpDeserializerBase(EnumSet<Event> acceptedEvents) {
+ this(acceptedEvents, acceptedEvents);
+ }
+
+ protected JsonpDeserializerBase(EnumSet<Event> acceptedEvents, EnumSet<Event> nativeEvents) {
+ this.acceptedEvents = acceptedEvents;
+ this.nativeEvents = nativeEvents;
+ }
+
+ /** Combines accepted events from a number of deserializers */
+ protected static EnumSet<Event> allAcceptedEvents(JsonpDeserializer<?>... deserializers) {
+ EnumSet<Event> result = EnumSet.noneOf(Event.class);
+ for (JsonpDeserializer<?> deserializer: deserializers) {
+
+ EnumSet<Event> set = deserializer.acceptedEvents();
+ // Disabled for now. Only happens with the experimental Union2 and is caused by string and number
+ // parsers leniency. Need to be replaced with a check on a preferred event type.
+ //if (!Collections.disjoint(result, set)) {
+ // throw new IllegalArgumentException("Deserializer accepted events are not disjoint");
+ //}
+
+ result.addAll(set);
+ }
+ return result;
+ }
+
+ @Override
+ public EnumSet<Event> nativeEvents() {
+ return nativeEvents;
+ }
+
+ /**
+ * The JSON events this deserializer accepts as a starting point
+ */
+ public final EnumSet<Event> acceptedEvents() {
+ return acceptedEvents;
+ }
+
+ /**
+ * Convenience method for {@code acceptedEvents.contains(event)}
+ */
+ public final boolean accepts(Event event) {
+ return acceptedEvents.contains(event);
+ }
+
+ //---------------------------------------------------------------------------------------------
+
+ //----- Builtin types
+
+ static final JsonpDeserializer<String> STRING =
+ // String parsing is lenient and accepts any other primitive type
+ new JsonpDeserializerBase<String>(EnumSet.of(
+ Event.KEY_NAME, Event.VALUE_STRING, Event.VALUE_NUMBER,
+ Event.VALUE_FALSE, Event.VALUE_TRUE
+ ),
+ EnumSet.of(Event.VALUE_STRING)
+ ) {
+ @Override
+ public String deserialize(JsonParser parser, JsonpMapper mapper, Event event) {
+ if (event == Event.VALUE_TRUE) {
+ return "true";
+ }
+ if (event == Event.VALUE_FALSE) {
+ return "false";
+ }
+ return parser.getString(); // also accepts numbers
+ }
+ };
+
+ static final JsonpDeserializer<Integer> INTEGER =
+ new JsonpDeserializerBase<Integer>(
+ EnumSet.of(Event.VALUE_NUMBER, Event.VALUE_STRING),
+ EnumSet.of(Event.VALUE_NUMBER)
+ ) {
+ @Override
+ public Integer deserialize(JsonParser parser, JsonpMapper mapper, Event event) {
+ if (event == Event.VALUE_STRING) {
+ return Integer.valueOf(parser.getString());
+ }
+ return parser.getInt();
+ }
+ };
+
+ static final JsonpDeserializer<Boolean> BOOLEAN =
+ new JsonpDeserializerBase<Boolean>(
+ EnumSet.of(Event.VALUE_FALSE, Event.VALUE_TRUE, Event.VALUE_STRING),
+ EnumSet.of(Event.VALUE_FALSE, Event.VALUE_TRUE)
+ ) {
+ @Override
+ public Boolean deserialize(JsonParser parser, JsonpMapper mapper, Event event) {
+ if (event == Event.VALUE_STRING) {
+ return Boolean.parseBoolean(parser.getString());
+ } else {
+ return event == Event.VALUE_TRUE;
+ }
+ }
+ };
+
+ static final JsonpDeserializer<Long> LONG =
+ new JsonpDeserializerBase<Long>(
+ EnumSet.of(Event.VALUE_NUMBER, Event.VALUE_STRING),
+ EnumSet.of(Event.VALUE_NUMBER)
+ ) {
+ @Override
+ public Long deserialize(JsonParser parser, JsonpMapper mapper, Event event) {
+ if (event == Event.VALUE_STRING) {
+ return Long.valueOf(parser.getString());
+ }
+ return parser.getLong();
+ }
+ };
+
+ static final JsonpDeserializer<Float> FLOAT =
+ new JsonpDeserializerBase<Float>(
+ EnumSet.of(Event.VALUE_NUMBER, Event.VALUE_STRING),
+ EnumSet.of(Event.VALUE_NUMBER)
+
+ ) {
+ @Override
+ public Float deserialize(JsonParser parser, JsonpMapper mapper, Event event) {
+ if (event == Event.VALUE_STRING) {
+ return Float.valueOf(parser.getString());
+ }
+ return parser.getBigDecimal().floatValue();
+ }
+ };
+
+ static final JsonpDeserializer<Double> DOUBLE =
+ new JsonpDeserializerBase<Double>(
+ EnumSet.of(Event.VALUE_NUMBER, Event.VALUE_STRING),
+ EnumSet.of(Event.VALUE_NUMBER)
+ ) {
+ @Override
+ public Double deserialize(JsonParser parser, JsonpMapper mapper, Event event) {
+ if (event == Event.VALUE_STRING) {
+ return Double.valueOf(parser.getString());
+ }
+ return parser.getBigDecimal().doubleValue();
+ }
+ };
+
+ static final class DoubleOrNullDeserializer extends JsonpDeserializerBase<Double> {
+ static final EnumSet<Event> nativeEvents = EnumSet.of(Event.VALUE_NUMBER, Event.VALUE_NULL);
+ static final EnumSet<Event> acceptedEvents = EnumSet.of(Event.VALUE_STRING, Event.VALUE_NUMBER, Event.VALUE_NULL);
+ private final double defaultValue;
+
+ DoubleOrNullDeserializer(double defaultValue) {
+ super(acceptedEvents, nativeEvents);
+ this.defaultValue = defaultValue;
+ }
+
+ @Override
+ public Double deserialize(JsonParser parser, JsonpMapper mapper, Event event) {
+ if (event == Event.VALUE_NULL) {
+ return defaultValue;
+ }
+ if (event == Event.VALUE_STRING) {
+ return Double.valueOf(parser.getString());
+ }
+ return parser.getBigDecimal().doubleValue();
+ }
+ }
+
+ static final class IntOrNullDeserializer extends JsonpDeserializerBase<Integer> {
+ static final EnumSet<Event> nativeEvents = EnumSet.of(Event.VALUE_NUMBER, Event.VALUE_NULL);
+ static final EnumSet<Event> acceptedEvents = EnumSet.of(Event.VALUE_STRING, Event.VALUE_NUMBER, Event.VALUE_NULL);
+ private final int defaultValue;
+
+ IntOrNullDeserializer(int defaultValue) {
+ super(acceptedEvents, nativeEvents);
+ this.defaultValue = defaultValue;
+ }
+
+ @Override
+ public Integer deserialize(JsonParser parser, JsonpMapper mapper, Event event) {
+ if (event == Event.VALUE_NULL) {
+ return defaultValue;
+ }
+ if (event == Event.VALUE_STRING) {
+ return Integer.valueOf(parser.getString());
+ }
+ return parser.getInt();
+ }
+ }
+
+ static final class StringOrNullDeserializer extends JsonpDeserializerBase<String> {
+ static final EnumSet<Event> nativeEvents = EnumSet.of(Event.VALUE_STRING, Event.VALUE_NULL);
+ static final EnumSet<Event> acceptedEvents = EnumSet.of(Event.KEY_NAME, Event.VALUE_STRING,
+ Event.VALUE_NUMBER, Event.VALUE_FALSE, Event.VALUE_TRUE, Event.VALUE_NULL);
+
+ StringOrNullDeserializer() {
+ super(acceptedEvents, nativeEvents);
+ }
+
+ @Override
+ public String deserialize(JsonParser parser, JsonpMapper mapper, Event event) {
+ if (event == Event.VALUE_NULL) {
+ return null;
+ }
+ if (event == Event.VALUE_TRUE) {
+ return "true";
+ }
+ if (event == Event.VALUE_FALSE) {
+ return "false";
+ }
+ return parser.getString();
+ }
+ }
+
+ static final JsonpDeserializer<Double> DOUBLE_OR_NAN =
+ new JsonpDeserializerBase<Double>(
+ EnumSet.of(Event.VALUE_NUMBER, Event.VALUE_STRING, Event.VALUE_NULL),
+ EnumSet.of(Event.VALUE_NUMBER, Event.VALUE_NULL)
+ ) {
+ @Override
+ public Double deserialize(JsonParser parser, JsonpMapper mapper, Event event) {
+ if (event == Event.VALUE_NULL) {
+ return Double.NaN;
+ }
+ if (event == Event.VALUE_STRING) {
+ return Double.valueOf(parser.getString());
+ }
+ return parser.getBigDecimal().doubleValue();
+ }
+ };
+
+ static final JsonpDeserializer<Number> NUMBER =
+ new JsonpDeserializerBase<Number>(
+ EnumSet.of(Event.VALUE_NUMBER, Event.VALUE_STRING),
+ EnumSet.of(Event.VALUE_NUMBER)
+ ) {
+ @Override
+ public Number deserialize(JsonParser parser, JsonpMapper mapper, Event event) {
+ if (event == Event.VALUE_STRING) {
+ return Double.valueOf(parser.getString());
+ }
+ return ((JsonNumber)parser.getValue()).numberValue();
+ }
+ };
+
+ static final JsonpDeserializer<JsonValue> JSON_VALUE =
+ new JsonpDeserializerBase<JsonValue>(
+ EnumSet.allOf(Event.class)
+ ) {
+ @Override
+ public JsonValue deserialize(JsonParser parser, JsonpMapper mapper, Event event) {
+ return parser.getValue();
+ }
+ };
+
+ static final JsonpDeserializer<Void> VOID = new JsonpDeserializerBase<Void>(
+ EnumSet.noneOf(Event.class)
+ ) {
+ @Override
+ public Void deserialize(JsonParser parser, JsonpMapper mapper) {
+ throw new JsonParsingException("Void types should not have any value", parser.getLocation());
+ }
+
+ @Override
+ public Void deserialize(JsonParser parser, JsonpMapper mapper, Event event) {
+ return deserialize(parser, mapper);
+ }
+ };
+
+ //----- Collections
+
+ static class ArrayDeserializer<T> implements JsonpDeserializer<List<T>> {
+ private final JsonpDeserializer<T> itemDeserializer;
+ private EnumSet<Event> acceptedEvents;
+ private static final EnumSet<Event> nativeEvents = EnumSet.of(Event.START_ARRAY);
+
+ protected ArrayDeserializer(JsonpDeserializer<T> itemDeserializer) {
+ this.itemDeserializer = itemDeserializer;
+ }
+
+ @Override
+ public EnumSet<Event> nativeEvents() {
+ return nativeEvents;
+ }
+
+ @Override
+ public EnumSet<Event> acceptedEvents() {
+ // Accepted events is computed lazily
+ // no need for double-checked lock, we don't care about computing it several times
+ if (acceptedEvents == null) {
+ acceptedEvents = EnumSet.of(Event.START_ARRAY);
+ acceptedEvents.addAll(itemDeserializer.acceptedEvents());
+ }
+ return acceptedEvents;
+ }
+
+ @Override
+ public List<T> deserialize(JsonParser parser, JsonpMapper mapper, Event event) {
+ if (event == Event.START_ARRAY) {
+ List<T> result = new ArrayList<>();
+ while ((event = parser.next()) != Event.END_ARRAY) {
+ JsonpUtils.ensureAccepts(itemDeserializer, parser, event);
+ result.add(itemDeserializer.deserialize(parser, mapper, event));
+ }
+ return result;
+ } else {
+ // Single-value mode
+ JsonpUtils.ensureAccepts(itemDeserializer, parser, event);
+ return Collections.singletonList(itemDeserializer.deserialize(parser, mapper, event));
+ }
+ }
+ }
+
+ static class StringMapDeserializer<T> extends JsonpDeserializerBase<Map<String, T>> {
+ private final JsonpDeserializer<T> itemDeserializer;
+
+ protected StringMapDeserializer(JsonpDeserializer<T> itemDeserializer) {
+ super(EnumSet.of(Event.START_OBJECT));
+ this.itemDeserializer = itemDeserializer;
+ }
+
+ @Override
+ public Map<String, T> deserialize(JsonParser parser, JsonpMapper mapper, Event event) {
+ Map<String, T> result = new HashMap<>();
+ while ((event = parser.next()) != Event.END_OBJECT) {
+ JsonpUtils.expectEvent(parser, Event.KEY_NAME, event);
+ String key = parser.getString();
+ T value = itemDeserializer.deserialize(parser, mapper);
+ result.put(key, value);
+ }
+ return result;
+ }
+ }
+
+ static class EnumMapDeserializer<K, V> extends JsonpDeserializerBase<Map<K, V>> {
+ private final JsonpDeserializer<K> keyDeserializer;
+ private final JsonpDeserializer<V> valueDeserializer;
+
+ protected EnumMapDeserializer(JsonpDeserializer<K> keyDeserializer, JsonpDeserializer<V> valueDeserializer) {
+ super(EnumSet.of(Event.START_OBJECT));
+ this.keyDeserializer = keyDeserializer;
+ this.valueDeserializer = valueDeserializer;
+ }
+
+ @Override
+ public Map<K, V> deserialize(JsonParser parser, JsonpMapper mapper, Event event) {
+ Map<K, V> result = new HashMap<>();
+ while ((event = parser.next()) != Event.END_OBJECT) {
+ JsonpUtils.expectEvent(parser, Event.KEY_NAME, event);
+ K key = keyDeserializer.deserialize(parser, mapper, event);
+ V value = valueDeserializer.deserialize(parser, mapper);
+ result.put(key, value);
+ }
+ return result;
+ }
+ }
+}
diff --git a/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/JsonpMapperBase.java b/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/JsonpMapperBase.java
new file mode 100644
index 0000000000..5a710795b8
--- /dev/null
+++ b/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/JsonpMapperBase.java
@@ -0,0 +1,102 @@
+/****************************************************************
+ * 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.james.backends.opensearch.json;
+
+import java.lang.reflect.Field;
+
+import javax.annotation.Nullable;
+
+import org.opensearch.client.json.JsonpDeserializable;
+import org.opensearch.client.json.JsonpDeserializer;
+import org.opensearch.client.json.JsonpMapper;
+import org.opensearch.client.json.JsonpSerializable;
+import org.opensearch.client.json.JsonpSerializer;
+
+import jakarta.json.JsonValue;
+import jakarta.json.stream.JsonGenerator;
+import jakarta.json.stream.JsonParser;
+
+public abstract class JsonpMapperBase implements JsonpMapper {
+
+ /** Get a serializer when none of the builtin ones are applicable */
+ protected abstract <T> JsonpDeserializer<T> getDefaultDeserializer(Class<T> clazz);
+
+ @Override
+ public <T> T deserialize(JsonParser parser, Class<T> clazz) {
+ JsonpDeserializer<T> deserializer = findDeserializer(clazz);
+ if (deserializer != null) {
+ return deserializer.deserialize(parser, this);
+ }
+
+ return getDefaultDeserializer(clazz).deserialize(parser, this);
+ }
+
+ @Nullable
+ @SuppressWarnings("unchecked")
+ public static <T> JsonpDeserializer<T> findDeserializer(Class<T> clazz) {
+ JsonpDeserializable annotation = clazz.getAnnotation(JsonpDeserializable.class);
+ if (annotation != null) {
+ try {
+ Field field = clazz.getDeclaredField(annotation.field());
+ return (JsonpDeserializer<T>)field.get(null);
+ } catch (Exception e) {
+ throw new RuntimeException("No deserializer found in '" + clazz.getName() + "." + annotation.field() + "'");
+ }
+ }
+
+ return null;
+ }
+
+ @Nullable
+ @SuppressWarnings("unchecked")
+ public static <T> JsonpSerializer<T> findSerializer(T value) {
+ Class<?> valueClass = value.getClass();
+ if (JsonpSerializable.class.isAssignableFrom(valueClass)) {
+ return (JsonpSerializer<T>) JsonpSerializableSerializer.INSTANCE;
+ }
+
+ if (JsonValue.class.isAssignableFrom(valueClass)) {
+ return (JsonpSerializer<T>) JsonpValueSerializer.INSTANCE;
+ }
+
+ return null;
+ }
+
+ protected static class JsonpSerializableSerializer<T extends JsonpSerializable> implements JsonpSerializer<T> {
+ @Override
+ public void serialize(T value, JsonGenerator generator, JsonpMapper mapper) {
+ value.serialize(generator, mapper);
+ }
+
+ protected static final JsonpSerializer<?> INSTANCE = new JsonpSerializableSerializer<>();
+
+ }
+
+ protected static class JsonpValueSerializer implements JsonpSerializer<JsonValue> {
+ @Override
+ public void serialize(JsonValue value, JsonGenerator generator, JsonpMapper mapper) {
+ generator.write(value);
+ }
+
+ protected static final JsonpSerializer<?> INSTANCE = new JsonpValueSerializer();
+
+ }
+
+}
diff --git a/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/JsonpUtils.java b/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/JsonpUtils.java
new file mode 100644
index 0000000000..5694a5cbee
--- /dev/null
+++ b/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/JsonpUtils.java
@@ -0,0 +1,207 @@
+/****************************************************************
+ * 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.james.backends.opensearch.json;
+
+import java.io.StringReader;
+import java.util.AbstractMap;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nullable;
+
+import org.opensearch.client.json.JsonpDeserializer;
+import org.opensearch.client.json.JsonpMapper;
+import org.opensearch.client.json.JsonpSerializable;
+import org.opensearch.client.json.JsonpSerializer;
+import org.opensearch.client.util.ObjectBuilder;
+
+import jakarta.json.JsonObject;
+import jakarta.json.JsonString;
+import jakarta.json.JsonValue;
+import jakarta.json.stream.JsonGenerator;
+import jakarta.json.stream.JsonParser;
+import jakarta.json.stream.JsonParser.Event;
+import jakarta.json.stream.JsonParsingException;
+
+public class JsonpUtils {
+
+ /**
+ * Advances the parser to the next event and checks that this even is the expected one.
+ *
+ * @return the expected event
+ *
+ * @throws jakarta.json.JsonException if an i/o error occurs (IOException would be cause of JsonException)
+ * @throws JsonParsingException if the event is not the expected one, or if the parser encounters invalid
+ * JSON when advancing to next state.
+ * @throws java.util.NoSuchElementException if there are no more parsing states.
+ */
+ public static Event expectNextEvent(JsonParser parser, Event expected) {
+ Event event = parser.next();
+ expectEvent(parser, expected, event);
+ return event;
+ }
+
+ public static void expectEvent(JsonParser parser, Event expected, Event event) {
+ if (event != expected) {
+ throw new UnexpectedJsonEventException(parser, event, expected);
+ }
+ }
+
+ public static String expectKeyName(JsonParser parser, Event event) {
+ JsonpUtils.expectEvent(parser, Event.KEY_NAME, event);
+ return parser.getString();
+ }
+
+ public static void ensureAccepts(JsonpDeserializer<?> deserializer, JsonParser parser, Event event) {
+ if (!deserializer.acceptedEvents().contains(event)) {
+ throw new UnexpectedJsonEventException(parser, event, deserializer.acceptedEvents());
+ }
+ }
+
+ /**
+ * Skip the value at the next position of the parser.
+ */
+ public static void skipValue(JsonParser parser) {
+ skipValue(parser, parser.next());
+ }
+
+ /**
+ * Skip the value at the current position of the parser.
+ */
+ public static void skipValue(JsonParser parser, Event event) {
+ switch (event) {
+ case START_OBJECT:
+ parser.skipObject();
+ break;
+
+ case START_ARRAY:
+ parser.skipArray();
+ break;
+
+ default:
+ // Not a structure, no additional skipping needed
+ break;
+ }
+ }
+
+ public static <T> T buildVariant(JsonParser parser, ObjectBuilder<T> builder) {
+ if (builder == null) {
+ throw new JsonParsingException("No variant found", parser.getLocation());
+ }
+ return builder.build();
+ }
+
+ public static <T> void serialize(T value, JsonGenerator generator, @Nullable JsonpSerializer<T> serializer, JsonpMapper mapper) {
+ if (serializer != null) {
+ serializer.serialize(value, generator, mapper);
+ } else if (value instanceof JsonpSerializable) {
+ ((JsonpSerializable) value).serialize(generator, mapper);
+ } else {
+ mapper.serialize(value, generator);
+ }
+ }
+
+ /**
+ * Looks ahead a field value in the Json object from the upcoming object in a parser, which should be on the
+ * START_OBJECT event.
+ *
+ * Returns a pair containing that value and a parser that should be used to actually parse the object
+ * (the object has been consumed from the original one).
+ */
+ public static Map.Entry<String, JsonParser> lookAheadFieldValue(
+ String name, String defaultValue, JsonParser parser, JsonpMapper mapper
+ ) {
+ // FIXME: need a buffering parser wrapper so that we don't roundtrip through a JsonObject and a String
+ // FIXME: resulting parser should return locations that are offset with the original parser's location
+ JsonObject object = parser.getObject();
+ String result = object.getString(name, null);
+
+ if (result == null) {
+ result = defaultValue;
+ }
+
+ if (result == null) {
+ throw new JsonParsingException("Property '" + name + "' not found", parser.getLocation());
+ }
+
+ return new AbstractMap.SimpleImmutableEntry<>(result, objectParser(object, mapper));
+ }
+
+ /**
+ * Create a parser that traverses a JSON object
+ */
+ public static JsonParser objectParser(JsonObject object, JsonpMapper mapper) {
+ // FIXME: we should have used createParser(object), but this doesn't work as it creates a
+ // org.glassfish.json.JsonStructureParser that doesn't implement the JsonP 1.0.1 features, in particular
+ // parser.getObject(). So deserializing recursive internally-tagged union would fail with UnsupportedOperationException
+ // While glassfish has this issue or until we write our own, we roundtrip through a string.
+
+ String strObject = object.toString();
+ return mapper.jsonProvider().createParser(new StringReader(strObject));
+ }
+
+ public static String toString(JsonValue value) {
+ switch (value.getValueType()) {
+ case OBJECT:
+ throw new IllegalArgumentException("Json objects cannot be used as string");
+
+ case ARRAY:
+ return value.asJsonArray().stream()
+ .map(JsonpUtils::toString)
+ .collect(Collectors.joining(","));
+
+ case STRING:
+ return ((JsonString)value).getString();
+
+ case TRUE:
+ return "true";
+
+ case FALSE:
+ return "false";
+
+ case NULL:
+ return "null";
+
+ case NUMBER:
+ return value.toString();
+
+ default:
+ throw new IllegalArgumentException("Unknown JSON value type: '" + value + "'");
+ }
+ }
+
+ public static void serializeDoubleOrNull(JsonGenerator generator, double value, double defaultValue) {
+ // Only output null if the default value isn't finite, which cannot be represented as JSON
+ if (value == defaultValue && !Double.isFinite(defaultValue)) {
+ generator.writeNull();
+ } else {
+ generator.write(value);
+ }
+ }
+
+ public static void serializeIntOrNull(JsonGenerator generator, int value, int defaultValue) {
+ // Only output null if the default value isn't finite, which cannot be represented as JSON
+ if (value == defaultValue && defaultValue == Integer.MAX_VALUE || defaultValue == Integer.MIN_VALUE) {
+ generator.writeNull();
+ } else {
+ generator.write(value);
+ }
+ }
+}
diff --git a/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/UnexpectedJsonEventException.java b/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/UnexpectedJsonEventException.java
new file mode 100644
index 0000000000..faf7d5fe85
--- /dev/null
+++ b/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/UnexpectedJsonEventException.java
@@ -0,0 +1,40 @@
+/****************************************************************
+ * 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.james.backends.opensearch.json;
+
+import java.util.EnumSet;
+
+import jakarta.json.stream.JsonParser;
+import jakarta.json.stream.JsonParser.Event;
+import jakarta.json.stream.JsonParsingException;
+
+public class UnexpectedJsonEventException extends JsonParsingException {
+ public UnexpectedJsonEventException(JsonParser parser, Event event) {
+ super("Unexpected JSON event '" + event + "'", parser.getLocation());
+ }
+
+ public UnexpectedJsonEventException(JsonParser parser, Event event, Event expected) {
+ super("Unexpected JSON event '" + event + "' instead of '" + expected + "'", parser.getLocation());
+ }
+
+ public UnexpectedJsonEventException(JsonParser parser, Event event, EnumSet<Event> expected) {
+ super("Unexpected JSON event '" + event + "' instead of '" + expected + "'", parser.getLocation());
+ }
+}
diff --git a/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/jackson/JacksonJsonProvider.java b/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/jackson/JacksonJsonProvider.java
new file mode 100644
index 0000000000..e7d128977f
--- /dev/null
+++ b/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/jackson/JacksonJsonProvider.java
@@ -0,0 +1,298 @@
+/****************************************************************
+ * 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.james.backends.opensearch.json.jackson;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.io.Writer;
+import java.nio.charset.Charset;
+import java.util.Collections;
+import java.util.Map;
+
+import com.fasterxml.jackson.core.JsonFactory;
+
+import jakarta.json.JsonArray;
+import jakarta.json.JsonArrayBuilder;
+import jakarta.json.JsonBuilderFactory;
+import jakarta.json.JsonObject;
+import jakarta.json.JsonObjectBuilder;
+import jakarta.json.JsonReader;
+import jakarta.json.JsonReaderFactory;
+import jakarta.json.JsonWriter;
+import jakarta.json.JsonWriterFactory;
+import jakarta.json.spi.JsonProvider;
+import jakarta.json.stream.JsonGenerator;
+import jakarta.json.stream.JsonGeneratorFactory;
+import jakarta.json.stream.JsonParser;
+import jakarta.json.stream.JsonParserFactory;
+
+/**
+ * A partial implementation of JSONP's SPI on top of Jackson.
+ */
+public class JacksonJsonProvider extends JsonProvider {
+
+ private final JsonFactory jsonFactory;
+
+ public JacksonJsonProvider(JsonFactory jsonFactory) {
+ this.jsonFactory = jsonFactory;
+ }
+
+ public JacksonJsonProvider() {
+ this(new JsonFactory());
+ }
+
+ /**
+ * Return the underlying Jackson {@link JsonFactory}.
+ */
+ public JsonFactory jacksonJsonFactory() {
+ return this.jsonFactory;
+ }
+
+ //---------------------------------------------------------------------------------------------
+ // Parser
+
+ private final ParserFactory defaultParserFactory = new ParserFactory(null);
+
+ @Override
+ public JsonParserFactory createParserFactory(Map<String, ?> config) {
+ if (config == null || config.isEmpty()) {
+ return defaultParserFactory;
+ } else {
+ // TODO: handle specific configuration
+ return defaultParserFactory;
+ }
+ }
+
+ @Override
+ public JsonParser createParser(Reader reader) {
+ return defaultParserFactory.createParser(reader);
+ }
+
+ @Override
+ public JsonParser createParser(InputStream in) {
+ return defaultParserFactory.createParser(in);
+ }
+
+ private class ParserFactory implements JsonParserFactory {
+
+ private final Map<String, ?> config;
+
+ ParserFactory(Map<String, ?> config) {
+ this.config = config == null ? Collections.emptyMap() : config;
+ }
+
+ @Override
+ public JsonParser createParser(Reader reader) {
+ try {
+ return new JacksonJsonpParser(jsonFactory.createParser(reader));
+ } catch (IOException ioe) {
+ throw JacksonUtils.convertException(ioe);
+ }
+ }
+
+ @Override
+ public JsonParser createParser(InputStream in) {
+ try {
+ return new JacksonJsonpParser(jsonFactory.createParser(in));
+ } catch (IOException ioe) {
+ throw JacksonUtils.convertException(ioe);
+ }
+ }
+
+ @Override
+ public JsonParser createParser(InputStream in, Charset charset) {
+ try {
+ return new JacksonJsonpParser(jsonFactory.createParser(new InputStreamReader(in, charset)));
+ } catch (IOException ioe) {
+ throw JacksonUtils.convertException(ioe);
+ }
+ }
+
+ /**
+ * Not implemented.
+ */
+ @Override
+ public JsonParser createParser(JsonObject obj) {
+ return JsonProvider.provider().createParserFactory(null).createParser(obj);
+ }
+
+ /**
+ * Not implemented.
+ */
+ @Override
+ public JsonParser createParser(JsonArray array) {
+ return JsonProvider.provider().createParserFactory(null).createParser(array);
+ }
+
+ /**
+ * Not implemented.
+ */
+ @Override
+ public Map<String, ?> getConfigInUse() {
+ return config;
+ }
+ }
+
+ //---------------------------------------------------------------------------------------------
+ // Generator
+
+ private final JsonGeneratorFactory defaultGeneratorFactory = new GeneratorFactory(null);
+
+ @Override
+ public JsonGeneratorFactory createGeneratorFactory(Map<String, ?> config) {
+ if (config == null || config.isEmpty()) {
+ return defaultGeneratorFactory;
+ } else {
+ // TODO: handle specific configuration
+ return defaultGeneratorFactory;
+ }
+ }
+
+ @Override
+ public JsonGenerator createGenerator(Writer writer) {
+ return defaultGeneratorFactory.createGenerator(writer);
+ }
+
+ @Override
+ public JsonGenerator createGenerator(OutputStream out) {
+ return defaultGeneratorFactory.createGenerator(out);
+ }
+
+ private class GeneratorFactory implements JsonGeneratorFactory {
+
+ private final Map<String, ?> config;
+
+ GeneratorFactory(Map<String, ?> config) {
+ this.config = config == null ? Collections.emptyMap() : config;
+ }
+
+ @Override
+ public JsonGenerator createGenerator(Writer writer) {
+ try {
+ return new JacksonJsonpGenerator(jsonFactory.createGenerator(writer));
+ } catch (IOException ioe) {
+ throw JacksonUtils.convertException(ioe);
+ }
+ }
+
+ @Override
+ public JsonGenerator createGenerator(OutputStream out) {
+ try {
+ return new JacksonJsonpGenerator(jsonFactory.createGenerator(out));
+ } catch (IOException ioe) {
+ throw JacksonUtils.convertException(ioe);
+ }
+ }
+
+ @Override
+ public JsonGenerator createGenerator(OutputStream out, Charset charset) {
+ try {
+ return new JacksonJsonpGenerator(jsonFactory.createGenerator(new OutputStreamWriter(out, charset)));
+ } catch (IOException ioe) {
+ throw JacksonUtils.convertException(ioe);
+ }
+
+ }
+
+ @Override
+ public Map<String, ?> getConfigInUse() {
+ return config;
+ }
+ }
+
+ //---------------------------------------------------------------------------------------------
+ // Unsupported operations
+
+ /**
+ * Not implemented.
+ */
+ @Override
+ public JsonReader createReader(Reader reader) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Not implemented.
+ */
+ @Override
+ public JsonReader createReader(InputStream in) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Not implemented.
+ */
+ @Override
+ public JsonWriter createWriter(Writer writer) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Not implemented.
+ */
+ @Override
+ public JsonWriter createWriter(OutputStream out) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Not implemented.
+ */
+ @Override
+ public JsonWriterFactory createWriterFactory(Map<String, ?> config) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Not implemented.
+ */
+ @Override
+ public JsonReaderFactory createReaderFactory(Map<String, ?> config) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Not implemented.
+ */
+ @Override
+ public JsonObjectBuilder createObjectBuilder() {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Not implemented.
+ */
+ @Override
+ public JsonArrayBuilder createArrayBuilder() {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Not implemented.
+ */
+ @Override
+ public JsonBuilderFactory createBuilderFactory(Map<String, ?> config) {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/jackson/JacksonJsonpGenerator.java b/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/jackson/JacksonJsonpGenerator.java
new file mode 100644
index 0000000000..c8758d11d8
--- /dev/null
+++ b/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/jackson/JacksonJsonpGenerator.java
@@ -0,0 +1,374 @@
+/****************************************************************
+ * 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.james.backends.opensearch.json.jackson;
+
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.Map;
+
+import com.fasterxml.jackson.core.JsonStreamContext;
+
+import jakarta.json.JsonNumber;
+import jakarta.json.JsonString;
+import jakarta.json.JsonValue;
+import jakarta.json.stream.JsonGenerationException;
+import jakarta.json.stream.JsonGenerator;
+
+/**
+ * A JSONP generator implementation on top of Jackson.
+ */
+public class JacksonJsonpGenerator implements JsonGenerator {
+
+ private final com.fasterxml.jackson.core.JsonGenerator generator;
+
+ public JacksonJsonpGenerator(com.fasterxml.jackson.core.JsonGenerator generator) {
+ this.generator = generator;
+ }
+
+ /**
+ * Returns the underlying Jackson generator.
+ */
+ public com.fasterxml.jackson.core.JsonGenerator jacksonGenerator() {
+ return generator;
+ }
+
+ @Override
+ public JsonGenerator writeStartObject() {
+ try {
+ generator.writeStartObject();
+ } catch (IOException e) {
+ throw JacksonUtils.convertException(e);
+ }
+ return this;
+ }
+
+ @Override
+ public JsonGenerator writeStartObject(String name) {
+ try {
+ generator.writeFieldName(name);
+ generator.writeStartObject();
+ } catch (IOException e) {
+ throw JacksonUtils.convertException(e);
+ }
+ return this;
+ }
+
+ @Override
+ public JsonGenerator writeStartArray() {
+ try {
+ generator.writeStartArray();
+ } catch (IOException e) {
+ throw JacksonUtils.convertException(e);
+ }
+ return this;
+ }
+
+ @Override
+ public JsonGenerator writeStartArray(String name) {
+ try {
+ generator.writeFieldName(name);
+ generator.writeStartArray();
+ } catch (IOException e) {
+ throw JacksonUtils.convertException(e);
+ }
+ return this;
+ }
+
+ @Override
+ public JsonGenerator writeKey(String name) {
+ try {
+ generator.writeFieldName(name);
+ } catch (IOException e) {
+ throw JacksonUtils.convertException(e);
+ }
+ return this;
+ }
+
+ @Override
+ public JsonGenerator write(String name, JsonValue value) {
+ try {
+ generator.writeFieldName(name);
+ writeValue(value);
+ } catch (IOException e) {
+ throw JacksonUtils.convertException(e);
+ }
+ return this;
+ }
+
+ @Override
+ public JsonGenerator write(String name, String value) {
+ try {
+ generator.writeFieldName(name);
+ generator.writeString(value);
+ } catch (IOException e) {
+ throw JacksonUtils.convertException(e);
+ }
+ return this;
+ }
+
+ @Override
+ public JsonGenerator write(String name, BigInteger value) {
+ try {
+ generator.writeFieldName(name);
+ generator.writeNumber(value);
+ } catch (IOException e) {
+ throw JacksonUtils.convertException(e);
+ }
+ return this;
+ }
+
+ @Override
+ public JsonGenerator write(String name, BigDecimal value) {
+ try {
+ generator.writeFieldName(name);
+ generator.writeNumber(value);
+ } catch (IOException e) {
+ throw JacksonUtils.convertException(e);
+ }
+ return this;
+ }
+
+ @Override
+ public JsonGenerator write(String name, int value) {
+ try {
+ generator.writeFieldName(name);
+ generator.writeNumber(value);
+ } catch (IOException e) {
+ throw JacksonUtils.convertException(e);
+ }
+ return this;
+ }
+
+ @Override
+ public JsonGenerator write(String name, long value) {
+ try {
+ generator.writeFieldName(name);
+ generator.writeNumber(value);
+ } catch (IOException e) {
+ throw JacksonUtils.convertException(e);
+ }
+ return this;
+ }
+
+ @Override
+ public JsonGenerator write(String name, double value) {
+ try {
+ generator.writeFieldName(name);
+ generator.writeNumber(value);
+ } catch (IOException e) {
+ throw JacksonUtils.convertException(e);
+ }
+ return this;
+ }
+
+ @Override
+ public JsonGenerator write(String name, boolean value) {
+ try {
+ generator.writeFieldName(name);
+ generator.writeBooleanField(name, value);
+ } catch (IOException e) {
+ throw JacksonUtils.convertException(e);
+ }
+ return this;
+ }
+
+ @Override
+ public JsonGenerator writeNull(String name) {
+ try {
+ generator.writeFieldName(name);
+ generator.writeNull();
+ } catch (IOException e) {
+ throw JacksonUtils.convertException(e);
+ }
+ return this;
+ }
+
+ @Override
+ public JsonGenerator writeEnd() {
+ try {
+ JsonStreamContext ctx = generator.getOutputContext();
+ if (ctx.inObject()) {
+ generator.writeEndObject();
+ } else if (ctx.inArray()) {
+ generator.writeEndArray();
+ } else {
+ throw new JsonGenerationException("Unexpected context: '" + ctx.typeDesc() + "'");
+ }
+ } catch (IOException e) {
+ throw JacksonUtils.convertException(e);
+ }
+ return this;
+ }
+
+ @Override
+ public JsonGenerator write(JsonValue value) {
+ try {
+ writeValue(value);
+ } catch (IOException e) {
+ throw JacksonUtils.convertException(e);
+ }
+ return this;
+ }
+
+ @Override
+ public JsonGenerator write(String value) {
+ try {
+ generator.writeString(value);
+ } catch (IOException e) {
+ throw JacksonUtils.convertException(e);
+ }
+ return this;
+ }
+
+ @Override
+ public JsonGenerator write(BigDecimal value) {
+ try {
+ generator.writeNumber(value);
+ } catch (IOException e) {
+ throw JacksonUtils.convertException(e);
+ }
+ return this;
+ }
+
+ @Override
+ public JsonGenerator write(BigInteger value) {
+ try {
+ generator.writeNumber(value);
+ } catch (IOException e) {
+ throw JacksonUtils.convertException(e);
+ }
+ return this;
+ }
+
+ @Override
+ public JsonGenerator write(int value) {
+ try {
+ generator.writeNumber(value);
+ } catch (IOException e) {
+ throw JacksonUtils.convertException(e);
+ }
+ return this;
+ }
+
+ @Override
+ public JsonGenerator write(long value) {
+ try {
+ generator.writeNumber(value);
+ } catch (IOException e) {
+ throw JacksonUtils.convertException(e);
+ }
+ return this;
+ }
+
+ @Override
+ public JsonGenerator write(double value) {
+ try {
+ generator.writeNumber(value);
+ } catch (IOException e) {
+ throw JacksonUtils.convertException(e);
+ }
+ return this;
+ }
+
+ @Override
+ public JsonGenerator write(boolean value) {
+ try {
+ generator.writeBoolean(value);
+ } catch (IOException e) {
+ throw JacksonUtils.convertException(e);
+ }
+ return this;
+ }
+
+ @Override
+ public JsonGenerator writeNull() {
+ try {
+ generator.writeNull();
+ } catch (IOException e) {
+ throw JacksonUtils.convertException(e);
+ }
+ return this;
+ }
+
+ @Override
+ public void close() {
+ try {
+ generator.close();
+ } catch (IOException e) {
+ throw JacksonUtils.convertException(e);
+ }
+ }
+
+ @Override
+ public void flush() {
+ try {
+ generator.flush();
+ } catch (IOException e) {
+ throw JacksonUtils.convertException(e);
+ }
+ }
+
+ private void writeValue(JsonValue value) throws IOException {
+ switch (value.getValueType()) {
+ case OBJECT:
+ generator.writeStartObject();
+ for (Map.Entry<String, JsonValue> entry: value.asJsonObject().entrySet()) {
+ generator.writeFieldName(entry.getKey());
+ writeValue(entry.getValue());
+ }
+ generator.writeEndObject();
+ break;
+
+ case ARRAY:
+ generator.writeStartArray();
+ for (JsonValue item: value.asJsonArray()) {
+ writeValue(item);
+ }
+ generator.writeEndArray();
+ break;
+
+ case STRING:
+ generator.writeString(((JsonString)value).getString());
+ break;
+
+ case FALSE:
+ generator.writeBoolean(false);
+ break;
+
+ case TRUE:
+ generator.writeBoolean(true);
+ break;
+
+ case NULL:
+ generator.writeNull();
+ break;
+
+ case NUMBER:
+ JsonNumber n = (JsonNumber) value;
+ if (n.isIntegral()) {
+ generator.writeNumber(n.longValue());
+ } else {
+ generator.writeNumber(n.doubleValue());
+ }
+ break;
+ }
+ }
+}
diff --git a/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/jackson/JacksonJsonpLocation.java b/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/jackson/JacksonJsonpLocation.java
new file mode 100644
index 0000000000..132f40365e
--- /dev/null
+++ b/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/jackson/JacksonJsonpLocation.java
@@ -0,0 +1,54 @@
+/****************************************************************
+ * 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.james.backends.opensearch.json.jackson;
+
+import jakarta.json.stream.JsonLocation;
+
+/**
+ * Translate a Jackson location to a JSONP location.
+ */
+public class JacksonJsonpLocation implements JsonLocation {
+
+ private final com.fasterxml.jackson.core.JsonLocation location;
+
+ JacksonJsonpLocation(com.fasterxml.jackson.core.JsonLocation location) {
+ this.location = location;
+ }
+
+ JacksonJsonpLocation(com.fasterxml.jackson.core.JsonParser parser) {
+ this(parser.getTokenLocation());
+ }
+
+ @Override
+ public long getLineNumber() {
+ return location.getLineNr();
+ }
+
+ @Override
+ public long getColumnNumber() {
+ return location.getColumnNr();
+ }
+
+ @Override
+ public long getStreamOffset() {
+ long charOffset = location.getCharOffset();
+ return charOffset == -1 ? location.getByteOffset() : charOffset;
+ }
+}
diff --git a/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/jackson/JacksonJsonpMapper.java b/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/jackson/JacksonJsonpMapper.java
new file mode 100644
index 0000000000..692b0e937c
--- /dev/null
+++ b/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/jackson/JacksonJsonpMapper.java
@@ -0,0 +1,123 @@
+/****************************************************************
+ * 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.james.backends.opensearch.json.jackson;
+
+import java.io.IOException;
+import java.util.EnumSet;
+
+import org.apache.james.backends.opensearch.json.JsonpDeserializerBase;
+import org.apache.james.backends.opensearch.json.JsonpMapperBase;
+import org.opensearch.client.json.JsonpDeserializer;
+import org.opensearch.client.json.JsonpMapper;
+import org.opensearch.client.json.JsonpSerializer;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+
+import jakarta.json.spi.JsonProvider;
+import jakarta.json.stream.JsonGenerator;
+import jakarta.json.stream.JsonParser;
+
+public class JacksonJsonpMapper extends JsonpMapperBase {
+
+ private final JacksonJsonProvider provider;
+ private final ObjectMapper objectMapper;
+
+ public JacksonJsonpMapper(ObjectMapper objectMapper) {
+ this(objectMapper, new JsonFactory());
+ }
+
+ public JacksonJsonpMapper(ObjectMapper objectMapper, JsonFactory jsonFactory) {
+ this.provider = new JacksonJsonProvider(jsonFactory);
+ this.objectMapper = objectMapper
+ .configure(SerializationFeature.INDENT_OUTPUT, false)
+ .setSerializationInclusion(JsonInclude.Include.NON_NULL);
+ }
+
+ public JacksonJsonpMapper() {
+ this(new ObjectMapper());
+ }
+
+ /**
+ * Returns the underlying Jackson mapper.
+ */
+ public ObjectMapper objectMapper() {
+ return this.objectMapper;
+ }
+
+ @Override
+ public JsonProvider jsonProvider() {
+ return provider;
+ }
+
+ @Override
+ protected <T> JsonpDeserializer<T> getDefaultDeserializer(Class<T> clazz) {
+ return new JacksonValueParser<>(clazz);
+ }
+
+ @Override
+ public <T> void serialize(T value, JsonGenerator generator) {
+
+ if (!(generator instanceof JacksonJsonpGenerator)) {
+ throw new IllegalArgumentException("Jackson's ObjectMapper can only be used with the JacksonJsonpProvider");
+ }
+
+ JsonpSerializer<T> serializer = findSerializer(value);
+ if (serializer != null) {
+ serializer.serialize(value, generator, this);
+ return;
+ }
+
+ com.fasterxml.jackson.core.JsonGenerator jkGenerator = ((JacksonJsonpGenerator)generator).jacksonGenerator();
+ try {
+ objectMapper.writeValue(jkGenerator, value);
+ } catch (IOException ioe) {
+ throw JacksonUtils.convertException(ioe);
+ }
+ }
+
+ private class JacksonValueParser<T> extends JsonpDeserializerBase<T> {
+
+ private final Class<T> clazz;
+
+ protected JacksonValueParser(Class<T> clazz) {
+ super(EnumSet.allOf(JsonParser.Event.class));
+ this.clazz = clazz;
+ }
+
+ @Override
+ public T deserialize(JsonParser parser, JsonpMapper mapper, JsonParser.Event event) {
+
+ if (!(parser instanceof JacksonJsonpParser)) {
+ throw new IllegalArgumentException("Jackson's ObjectMapper can only be used with the JacksonJsonpProvider");
+ }
+
+ com.fasterxml.jackson.core.JsonParser jkParser = ((JacksonJsonpParser)parser).jacksonParser();
+
+ try {
+ return objectMapper.readValue(jkParser, clazz);
+ } catch (IOException ioe) {
+ throw JacksonUtils.convertException(ioe);
+ }
+ }
+ }
+}
diff --git a/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/jackson/JacksonJsonpParser.java b/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/jackson/JacksonJsonpParser.java
new file mode 100644
index 0000000000..b1dcacccb2
--- /dev/null
+++ b/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/jackson/JacksonJsonpParser.java
@@ -0,0 +1,313 @@
+/****************************************************************
+ * 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.james.backends.opensearch.json.jackson;
+
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.util.EnumMap;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.stream.Stream;
+
+import com.fasterxml.jackson.core.JsonToken;
+
+import jakarta.json.JsonArray;
+import jakarta.json.JsonObject;
+import jakarta.json.JsonValue;
+import jakarta.json.stream.JsonLocation;
+import jakarta.json.stream.JsonParser;
+import jakarta.json.stream.JsonParsingException;
+
+/**
+ * A JSONP parser implementation on top of Jackson.
+ * <p>
+ * <b>Warning:</b> this implementation isn't fully compliant with the JSONP specification: calling {@link #hasNext()}
+ * moves forward the underlying Jackson parser as Jackson doesn't provide an equivalent method. This means no value
+ * getter method (e.g. {@link #getInt()} or {@link #getString()} should be called until the next call to {@link #next()}.
+ * Such calls will throw an {@code IllegalStateException}.
+ */
+public class JacksonJsonpParser implements JsonParser {
+
+ private final com.fasterxml.jackson.core.JsonParser parser;
+
+ private boolean hasNextWasCalled = false;
+
+ private static final EnumMap<JsonToken, Event> tokenToEvent;
+
+ static {
+ tokenToEvent = new EnumMap<>(JsonToken.class);
+ tokenToEvent.put(JsonToken.END_ARRAY, Event.END_ARRAY);
+ tokenToEvent.put(JsonToken.END_OBJECT, Event.END_OBJECT);
+ tokenToEvent.put(JsonToken.FIELD_NAME, Event.KEY_NAME);
+ tokenToEvent.put(JsonToken.START_ARRAY, Event.START_ARRAY);
+ tokenToEvent.put(JsonToken.START_OBJECT, Event.START_OBJECT);
+ tokenToEvent.put(JsonToken.VALUE_FALSE, Event.VALUE_FALSE);
+ tokenToEvent.put(JsonToken.VALUE_NULL, Event.VALUE_NULL);
+ tokenToEvent.put(JsonToken.VALUE_NUMBER_FLOAT, Event.VALUE_NUMBER);
+ tokenToEvent.put(JsonToken.VALUE_NUMBER_INT, Event.VALUE_NUMBER);
+ tokenToEvent.put(JsonToken.VALUE_STRING, Event.VALUE_STRING);
+ tokenToEvent.put(JsonToken.VALUE_TRUE, Event.VALUE_TRUE);
+
+ // No equivalent for
+ // - VALUE_EMBEDDED_OBJECT
+ // - NOT_AVAILABLE
+ }
+
+ public JacksonJsonpParser(com.fasterxml.jackson.core.JsonParser parser) {
+ this.parser = parser;
+ }
+
+ /**
+ * Returns the underlying Jackson parser.
+ */
+ public com.fasterxml.jackson.core.JsonParser jacksonParser() {
+ return this.parser;
+ }
+
+ private JsonParsingException convertException(IOException ioe) {
+ return new JsonParsingException("Jackson exception: " + ioe.getMessage(), ioe, getLocation());
+ }
+
+ private JsonToken fetchNextToken() {
+ try {
+ return parser.nextToken();
+ } catch (IOException e) {
+ throw convertException(e);
+ }
+ }
+
+ private void ensureTokenIsCurrent() {
+ if (hasNextWasCalled) {
+ throw new IllegalStateException("Cannot get event data as parser as already been moved to the next event");
+ }
+ }
+
+ @Override
+ public boolean hasNext() {
+ if (hasNextWasCalled) {
+ return parser.currentToken() != null;
+ } else {
+ hasNextWasCalled = true;
+ return fetchNextToken() != null;
+ }
+ }
+
+ @Override
+ public Event next() {
+ JsonToken token;
+ if (hasNextWasCalled) {
+ token = parser.getCurrentToken();
+ hasNextWasCalled = false;
+ } else {
+ token = fetchNextToken();
+ }
+
+ if (token == null) {
+ throw new NoSuchElementException();
+ }
+
+ Event result = tokenToEvent.get(token);
+ if (result == null) {
+ throw new JsonParsingException("Unsupported Jackson event type '" + token + "'", getLocation());
+ }
+
+ return result;
+ }
+
+ @Override
+ public String getString() {
+ ensureTokenIsCurrent();
+ try {
+ return parser.getValueAsString();
+ } catch (IOException e) {
+ throw convertException(e);
+ }
+ }
+
+ @Override
+ public boolean isIntegralNumber() {
+ ensureTokenIsCurrent();
+ return parser.isExpectedNumberIntToken();
+ }
+
+ @Override
+ public int getInt() {
+ ensureTokenIsCurrent();
+ try {
+ return parser.getIntValue();
+ } catch (IOException e) {
+ throw convertException(e);
+ }
+ }
+
+ @Override
+ public long getLong() {
+ ensureTokenIsCurrent();
+ try {
+ return parser.getLongValue();
+ } catch (IOException e) {
+ throw convertException(e);
+ }
+ }
+
+ @Override
+ public BigDecimal getBigDecimal() {
+ ensureTokenIsCurrent();
+ try {
+ return parser.getDecimalValue();
+ } catch (IOException e) {
+ throw convertException(e);
+ }
+ }
+
+ @Override
+ public JsonLocation getLocation() {
+ return new JacksonJsonpLocation(parser.getCurrentLocation());
+ }
+
+ @Override
+ public void close() {
+ try {
+ parser.close();
+ } catch (IOException e) {
+ throw convertException(e);
+ }
+ }
+
+ private JsonValueParser valueParser;
+
+ @Override
+ public JsonObject getObject() {
+ ensureTokenIsCurrent();
+ if (parser.currentToken() != JsonToken.START_OBJECT) {
+ throw new IllegalStateException("Unexpected event '" + parser.currentToken() +
+ "' at " + parser.getTokenLocation());
+ }
+ if (valueParser == null) {
+ valueParser = new JsonValueParser();
+ }
+ try {
+ return valueParser.parseObject(parser);
+ } catch (IOException e) {
+ throw convertException(e);
+ }
+ }
+
+ @Override
+ public JsonArray getArray() {
+ ensureTokenIsCurrent();
+ if (valueParser == null) {
+ valueParser = new JsonValueParser();
+ }
+ if (parser.currentToken() != JsonToken.START_ARRAY) {
+ throw new IllegalStateException("Unexpected event '" + parser.currentToken() +
+ "' at " + parser.getTokenLocation());
+ }
+ try {
+ return valueParser.parseArray(parser);
+ } catch (IOException e) {
+ throw convertException(e);
+ }
+ }
+
+ @Override
+ public JsonValue getValue() {
+ ensureTokenIsCurrent();
+ if (valueParser == null) {
+ valueParser = new JsonValueParser();
+ }
+ try {
+ return valueParser.parseValue(parser);
+ } catch (IOException e) {
+ throw convertException(e);
+ }
+ }
+
+ @Override
+ public void skipObject() {
+ ensureTokenIsCurrent();
+ if (parser.currentToken() != JsonToken.START_OBJECT) {
+ return;
+ }
+
+ try {
+ int depth = 1;
+ JsonToken token;
+ do {
+ token = parser.nextToken();
+ switch (token) {
+ case START_OBJECT:
+ depth++;
+ break;
+ case END_OBJECT:
+ depth--;
+ break;
+ }
+ } while (!(token == JsonToken.END_OBJECT && depth == 0));
+ } catch (IOException e) {
+ throw convertException(e);
+ }
+ }
+
+ @Override
+ public void skipArray() {
+ ensureTokenIsCurrent();
+ if (parser.currentToken() != JsonToken.START_ARRAY) {
+ return;
+ }
+
+ try {
+ int depth = 1;
+ JsonToken token;
+ do {
+ token = parser.nextToken();
+ switch (token) {
+ case START_ARRAY:
+ depth++;
+ break;
+ case END_ARRAY:
+ depth--;
+ break;
+ }
+ } while (!(token == JsonToken.END_ARRAY && depth == 0));
+ } catch (IOException e) {
+ throw convertException(e);
+ }
+ }
+
+ @Override
+ public Stream<Map.Entry<String, JsonValue>> getObjectStream() {
+ return getObject().entrySet().stream();
+ }
+
+ @Override
+ public Stream<JsonValue> getArrayStream() {
+ return getArray().stream();
+ }
+
+ /**
+ * Not implemented.
+ */
+ @Override
+ public Stream<JsonValue> getValueStream() {
+ return JsonParser.super.getValueStream();
+ }
+}
+
diff --git a/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/jackson/JacksonUtils.java b/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/jackson/JacksonUtils.java
new file mode 100644
index 0000000000..d5e7782395
--- /dev/null
+++ b/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/jackson/JacksonUtils.java
@@ -0,0 +1,43 @@
+/****************************************************************
+ * 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.james.backends.opensearch.json.jackson;
+
+import java.io.IOException;
+
+import com.fasterxml.jackson.core.JsonParseException;
+
+import jakarta.json.JsonException;
+import jakarta.json.stream.JsonGenerationException;
+import jakarta.json.stream.JsonParsingException;
+
+class JacksonUtils {
+ public static JsonException convertException(IOException ioe) {
+ if (ioe instanceof com.fasterxml.jackson.core.JsonGenerationException) {
+ return new JsonGenerationException(ioe.getMessage(), ioe);
+
+ } else if (ioe instanceof JsonParseException) {
+ JsonParseException jpe = (JsonParseException) ioe;
+ return new JsonParsingException(ioe.getMessage(), jpe, new JacksonJsonpLocation(jpe.getLocation()));
+
+ } else {
+ return new JsonException("Jackson exception", ioe);
+ }
+ }
+}
diff --git a/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/jackson/JsonValueParser.java b/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/jackson/JsonValueParser.java
new file mode 100644
index 0000000000..a66bbd3edb
--- /dev/null
+++ b/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/jackson/JsonValueParser.java
@@ -0,0 +1,108 @@
+/****************************************************************
+ * 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.james.backends.opensearch.json.jackson;
+
+import java.io.IOException;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonToken;
+
+import jakarta.json.JsonArray;
+import jakarta.json.JsonArrayBuilder;
+import jakarta.json.JsonObject;
+import jakarta.json.JsonObjectBuilder;
+import jakarta.json.JsonValue;
+import jakarta.json.spi.JsonProvider;
+import jakarta.json.stream.JsonParsingException;
+
+/**
+ * Reads a Jsonp value/object/array from a Jackson parser. The parser's current token should be the start of the
+ * object (e.g. START_OBJECT, VALUE_NUMBER, etc).
+ */
+class JsonValueParser {
+ private static final JsonProvider provider = JsonProvider.provider();
+
+ public JsonObject parseObject(JsonParser parser) throws IOException {
+
+ JsonObjectBuilder ob = provider.createObjectBuilder();
+
+ JsonToken token;
+ while ((token = parser.nextToken()) != JsonToken.END_OBJECT) {
+ if (token != JsonToken.FIELD_NAME) {
+ throw new JsonParsingException("Expected a property name", new JacksonJsonpLocation(parser));
+ }
+ String name = parser.getCurrentName();
+ parser.nextToken();
+ ob.add(name, parseValue(parser));
+ }
+ return ob.build();
+ }
+
+ public JsonArray parseArray(JsonParser parser) throws IOException {
+ JsonArrayBuilder ab = provider.createArrayBuilder();
+
+ while (parser.nextToken() != JsonToken.END_ARRAY) {
+ ab.add(parseValue(parser));
+ }
+ return ab.build();
+ }
+
+ public JsonValue parseValue(JsonParser parser) throws IOException {
+ switch (parser.currentToken()) {
+ case START_OBJECT:
+ return parseObject(parser);
+
+ case START_ARRAY:
+ return parseArray(parser);
+
+ case VALUE_TRUE:
+ return JsonValue.TRUE;
+
+ case VALUE_FALSE:
+ return JsonValue.FALSE;
+
+ case VALUE_NULL:
+ return JsonValue.NULL;
+
+ case VALUE_STRING:
+ return provider.createValue(parser.getText());
+
+ case VALUE_NUMBER_FLOAT:
+ case VALUE_NUMBER_INT:
+ switch (parser.getNumberType()) {
+ case INT:
+ return provider.createValue(parser.getIntValue());
+ case LONG:
+ return provider.createValue(parser.getLongValue());
+ case FLOAT:
+ case DOUBLE:
+ return provider.createValue(parser.getDoubleValue());
+ case BIG_DECIMAL:
+ return provider.createValue(parser.getDecimalValue());
+ case BIG_INTEGER:
+ return provider.createValue(parser.getBigIntegerValue());
+ }
+
+ default:
+ throw new JsonParsingException("Unexpected token '" + parser.currentToken() + "'", new JacksonJsonpLocation(parser));
+
+ }
+ }
+}
diff --git a/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/package-info.java b/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/package-info.java
new file mode 100644
index 0000000000..db5d3f4c13
--- /dev/null
+++ b/backends-common/opensearch/src/main/java/org/apache/james/backends/opensearch/json/package-info.java
@@ -0,0 +1,29 @@
+/****************************************************************
+ * 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.james.backends.opensearch.json;
+
+/**
+ * See {@link https://github.com/opensearch-project/opensearch-java/issues/292}
+ *
+ * The Jackson related code was copied from {@link https://github.com/opensearch-project/opensearch-java} in order to get rid of a SPI lookup for each deserialized request,
+ * that was happening on the event loop.
+ *
+ * The only modified class is {@link jackson.JsonValueParser} where the SPI lookup was made static.
+ */
\ No newline at end of file
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org