diff --git a/.circleci/config.yml b/.circleci/config.yml index 3ad66ad46..33bc358ab 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -535,8 +535,8 @@ workflows: matrix: parameters: args: - - '-Dadb.jackson.version=2.18.0' - - '-Dadb.jackson.version=2.17.2' + - '-Dadb.jackson.version=2.18.2' + - '-Dadb.jackson.version=2.17.3' - '-Dadb.jackson.version=2.16.2' - '-Dadb.jackson.version=2.15.4' - '-Dadb.jackson.version=2.14.3' @@ -551,7 +551,7 @@ workflows: only: - main - test: - name: test-native-ssl=<> + name: test-native-ssl=<>-<> matrix: parameters: native: @@ -571,7 +571,7 @@ workflows: only: - main - test-shaded: - name: test-native-shaded-ssl=<> + name: test-native-shaded-ssl=<>-<> matrix: parameters: native: diff --git a/core/pom.xml b/core/pom.xml index 7e768fc1f..587102e05 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -8,7 +8,7 @@ ../release-parent com.arangodb release-parent - 7.14.0 + 7.15.0-SNAPSHOT core diff --git a/core/src/main/java/com/arangodb/entity/MultiDocumentEntity.java b/core/src/main/java/com/arangodb/entity/MultiDocumentEntity.java index 95803226f..6bdbe2873 100644 --- a/core/src/main/java/com/arangodb/entity/MultiDocumentEntity.java +++ b/core/src/main/java/com/arangodb/entity/MultiDocumentEntity.java @@ -20,6 +20,7 @@ package com.arangodb.entity; +import java.util.ArrayList; import java.util.List; /** @@ -27,9 +28,9 @@ */ public final class MultiDocumentEntity { - private List documents; - private List errors; - private List documentsAndErrors; + private List documents = new ArrayList<>(); + private List errors = new ArrayList<>(); + private List documentsAndErrors = new ArrayList<>(); private boolean isPotentialDirtyRead = false; public MultiDocumentEntity() { diff --git a/core/src/main/java/com/arangodb/internal/InternalArangoCollection.java b/core/src/main/java/com/arangodb/internal/InternalArangoCollection.java index 6f054cf5e..f794bcd31 100644 --- a/core/src/main/java/com/arangodb/internal/InternalArangoCollection.java +++ b/core/src/main/java/com/arangodb/internal/InternalArangoCollection.java @@ -32,7 +32,6 @@ import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Collection; -import java.util.List; import static com.arangodb.internal.serde.SerdeUtils.constructParametricType; @@ -111,28 +110,9 @@ private InternalRequest createInsertDocumentRequest(final DocumentCreateOptions protected ResponseDeserializer>> insertDocumentsResponseDeserializer(Class userDataClass) { return (response) -> { - final MultiDocumentEntity> multiDocument = new MultiDocumentEntity<>(); - final List> docs = new ArrayList<>(); - final List errors = new ArrayList<>(); - final List documentsAndErrors = new ArrayList<>(); - final JsonNode body = getSerde().parse(response.getBody()); - for (final JsonNode next : body) { - JsonNode isError = next.get(ArangoResponseField.ERROR_FIELD_NAME); - if (isError != null && isError.booleanValue()) { - final ErrorEntity error = getSerde().deserialize(next, ErrorEntity.class); - errors.add(error); - documentsAndErrors.add(error); - } else { - Type type = constructParametricType(DocumentCreateEntity.class, userDataClass); - final DocumentCreateEntity doc = getSerde().deserialize(next, type); - docs.add(doc); - documentsAndErrors.add(doc); - } - } - multiDocument.setDocuments(docs); - multiDocument.setErrors(errors); - multiDocument.setDocumentsAndErrors(documentsAndErrors); - return multiDocument; + Type type = constructParametricType(MultiDocumentEntity.class, + constructParametricType(DocumentCreateEntity.class, userDataClass)); + return getSerde().deserialize(response.getBody(), type); }; } @@ -184,31 +164,12 @@ protected InternalRequest getDocumentsRequest(final Iterable keys, final return request; } - protected ResponseDeserializer> getDocumentsResponseDeserializer( - final Class type) { + protected ResponseDeserializer> getDocumentsResponseDeserializer(final Class type) { return (response) -> { - final MultiDocumentEntity multiDocument = new MultiDocumentEntity<>(); + MultiDocumentEntity multiDocument = getSerde().deserialize(response.getBody(), + constructParametricType(MultiDocumentEntity.class, type)); boolean potentialDirtyRead = Boolean.parseBoolean(response.getMeta("X-Arango-Potential-Dirty-Read")); multiDocument.setPotentialDirtyRead(potentialDirtyRead); - final List docs = new ArrayList<>(); - final List errors = new ArrayList<>(); - final List documentsAndErrors = new ArrayList<>(); - final JsonNode body = getSerde().parse(response.getBody()); - for (final JsonNode next : body) { - JsonNode isError = next.get(ArangoResponseField.ERROR_FIELD_NAME); - if (isError != null && isError.booleanValue()) { - final ErrorEntity error = getSerde().deserialize(next, ErrorEntity.class); - errors.add(error); - documentsAndErrors.add(error); - } else { - final T doc = getSerde().deserializeUserData(getSerde().serialize(next), type); - docs.add(doc); - documentsAndErrors.add(doc); - } - } - multiDocument.setDocuments(docs); - multiDocument.setErrors(errors); - multiDocument.setDocumentsAndErrors(documentsAndErrors); return multiDocument; }; } @@ -250,28 +211,9 @@ private InternalRequest createReplaceDocumentRequest(final DocumentReplaceOption protected ResponseDeserializer>> replaceDocumentsResponseDeserializer( final Class returnType) { return (response) -> { - final MultiDocumentEntity> multiDocument = new MultiDocumentEntity<>(); - final List> docs = new ArrayList<>(); - final List errors = new ArrayList<>(); - final List documentsAndErrors = new ArrayList<>(); - final JsonNode body = getSerde().parse(response.getBody()); - for (final JsonNode next : body) { - JsonNode isError = next.get(ArangoResponseField.ERROR_FIELD_NAME); - if (isError != null && isError.booleanValue()) { - final ErrorEntity error = getSerde().deserialize(next, ErrorEntity.class); - errors.add(error); - documentsAndErrors.add(error); - } else { - Type type = constructParametricType(DocumentUpdateEntity.class, returnType); - final DocumentUpdateEntity doc = getSerde().deserialize(next, type); - docs.add(doc); - documentsAndErrors.add(doc); - } - } - multiDocument.setDocuments(docs); - multiDocument.setErrors(errors); - multiDocument.setDocumentsAndErrors(documentsAndErrors); - return multiDocument; + Type type = constructParametricType(MultiDocumentEntity.class, + constructParametricType(DocumentUpdateEntity.class, returnType)); + return getSerde().deserialize(response.getBody(), type); }; } @@ -313,28 +255,9 @@ private InternalRequest createUpdateDocumentRequest(final DocumentUpdateOptions protected ResponseDeserializer>> updateDocumentsResponseDeserializer( final Class returnType) { return (response) -> { - final MultiDocumentEntity> multiDocument = new MultiDocumentEntity<>(); - final List> docs = new ArrayList<>(); - final List errors = new ArrayList<>(); - final List documentsAndErrors = new ArrayList<>(); - final JsonNode body = getSerde().parse(response.getBody()); - for (final JsonNode next : body) { - JsonNode isError = next.get(ArangoResponseField.ERROR_FIELD_NAME); - if (isError != null && isError.booleanValue()) { - final ErrorEntity error = getSerde().deserialize(next, ErrorEntity.class); - errors.add(error); - documentsAndErrors.add(error); - } else { - Type type = constructParametricType(DocumentUpdateEntity.class, returnType); - final DocumentUpdateEntity doc = getSerde().deserialize(next, type); - docs.add(doc); - documentsAndErrors.add(doc); - } - } - multiDocument.setDocuments(docs); - multiDocument.setErrors(errors); - multiDocument.setDocumentsAndErrors(documentsAndErrors); - return multiDocument; + Type type = constructParametricType(MultiDocumentEntity.class, + constructParametricType(DocumentUpdateEntity.class, returnType)); + return getSerde().deserialize(response.getBody(), type); }; } @@ -370,28 +293,9 @@ private InternalRequest createDeleteDocumentRequest(final DocumentDeleteOptions protected ResponseDeserializer>> deleteDocumentsResponseDeserializer( final Class userDataClass) { return (response) -> { - final MultiDocumentEntity> multiDocument = new MultiDocumentEntity<>(); - final List> docs = new ArrayList<>(); - final List errors = new ArrayList<>(); - final List documentsAndErrors = new ArrayList<>(); - final JsonNode body = getSerde().parse(response.getBody()); - for (final JsonNode next : body) { - JsonNode isError = next.get(ArangoResponseField.ERROR_FIELD_NAME); - if (isError != null && isError.booleanValue()) { - final ErrorEntity error = getSerde().deserialize(next, ErrorEntity.class); - errors.add(error); - documentsAndErrors.add(error); - } else { - Type type = constructParametricType(DocumentDeleteEntity.class, userDataClass); - final DocumentDeleteEntity doc = getSerde().deserialize(next, type); - docs.add(doc); - documentsAndErrors.add(doc); - } - } - multiDocument.setDocuments(docs); - multiDocument.setErrors(errors); - multiDocument.setDocumentsAndErrors(documentsAndErrors); - return multiDocument; + Type type = constructParametricType(MultiDocumentEntity.class, + constructParametricType(DocumentDeleteEntity.class, userDataClass)); + return getSerde().deserialize(response.getBody(), type); }; } diff --git a/core/src/main/java/com/arangodb/internal/net/Communication.java b/core/src/main/java/com/arangodb/internal/net/Communication.java index b390ecee1..0309cd04c 100644 --- a/core/src/main/java/com/arangodb/internal/net/Communication.java +++ b/core/src/main/java/com/arangodb/internal/net/Communication.java @@ -23,8 +23,6 @@ import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicLong; -import static com.arangodb.internal.util.SerdeUtils.toJsonString; - @UsedInApi public abstract class Communication implements Closeable { private static final Logger LOGGER = LoggerFactory.getLogger(Communication.class); @@ -59,7 +57,7 @@ private CompletableFuture doExecuteAsync( final InternalRequest request, final HostHandle hostHandle, final Host host, final int attemptCount, Connection connection, long reqId ) { if (LOGGER.isDebugEnabled()) { - LOGGER.debug("Send Request [id={}]: {} {}", reqId, request, toJsonString(serde, request.getBody())); + LOGGER.debug("Send Request [id={}]: {} {}", reqId, request, serde.toJsonString(request.getBody())); } final CompletableFuture rfuture = new CompletableFuture<>(); try { @@ -85,7 +83,7 @@ private CompletableFuture doExecuteAsync( handleException(isSafe(request), e, hostHandle, request, host, reqId, attemptCount, rfuture); } else { if (LOGGER.isDebugEnabled()) { - LOGGER.debug("Received Response [id={}]: {} {}", reqId, response, toJsonString(serde, response.getBody())); + LOGGER.debug("Received Response [id={}]: {} {}", reqId, response, serde.toJsonString(response.getBody())); } ArangoDBException errorEntityEx = ResponseUtils.translateError(serde, response); if (errorEntityEx instanceof ArangoDBRedirectException) { diff --git a/core/src/main/java/com/arangodb/internal/serde/InternalDeserializers.java b/core/src/main/java/com/arangodb/internal/serde/InternalDeserializers.java index d56807af9..20b3ce3b7 100644 --- a/core/src/main/java/com/arangodb/internal/serde/InternalDeserializers.java +++ b/core/src/main/java/com/arangodb/internal/serde/InternalDeserializers.java @@ -6,8 +6,11 @@ import com.arangodb.entity.ReplicationFactor; import com.arangodb.entity.arangosearch.CollectionLink; import com.arangodb.entity.arangosearch.FieldLink; +import com.arangodb.util.RawBytes; import com.arangodb.util.RawJson; import com.arangodb.internal.InternalResponse; +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.TreeNode; import com.fasterxml.jackson.databind.DeserializationContext; @@ -16,6 +19,8 @@ import com.fasterxml.jackson.databind.node.*; import java.io.IOException; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; @@ -26,7 +31,23 @@ public final class InternalDeserializers { static final JsonDeserializer RAW_JSON_DESERIALIZER = new JsonDeserializer() { @Override public RawJson deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { - return RawJson.of(SerdeUtils.INSTANCE.writeJson(p.readValueAsTree())); + if (JsonFactory.FORMAT_NAME_JSON.equals(p.getCodec().getFactory().getFormatName())) { + return RawJson.of(new String(SerdeUtils.extractBytes(p), StandardCharsets.UTF_8)); + } else { + StringWriter w = new StringWriter(); + try (JsonGenerator gen = SerdeUtils.INSTANCE.getJsonMapper().getFactory().createGenerator(w)) { + gen.copyCurrentStructure(p); + gen.flush(); + } + return RawJson.of(w.toString()); + } + } + }; + + static final JsonDeserializer RAW_BYTES_DESERIALIZER = new JsonDeserializer() { + @Override + public RawBytes deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + return RawBytes.of(SerdeUtils.extractBytes(p)); } }; @@ -134,5 +155,4 @@ public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOEx } } - } diff --git a/core/src/main/java/com/arangodb/internal/serde/InternalModule.java b/core/src/main/java/com/arangodb/internal/serde/InternalModule.java index eefa65759..392a9c334 100644 --- a/core/src/main/java/com/arangodb/internal/serde/InternalModule.java +++ b/core/src/main/java/com/arangodb/internal/serde/InternalModule.java @@ -3,38 +3,34 @@ import com.arangodb.entity.CollectionStatus; import com.arangodb.entity.CollectionType; import com.arangodb.entity.InvertedIndexPrimarySort; +import com.arangodb.entity.MultiDocumentEntity; import com.arangodb.entity.ReplicationFactor; +import com.arangodb.util.RawBytes; import com.arangodb.util.RawJson; import com.arangodb.internal.InternalRequest; import com.arangodb.internal.InternalResponse; import com.fasterxml.jackson.databind.Module; import com.fasterxml.jackson.databind.module.SimpleModule; -import java.util.function.Supplier; +class InternalModule { -enum InternalModule implements Supplier { - INSTANCE; + static Module get(InternalSerde serde) { + SimpleModule module = new SimpleModule(); - private final SimpleModule module; - - InternalModule() { - module = new SimpleModule(); + module.addDeserializer(MultiDocumentEntity.class, new MultiDocumentEntityDeserializer(serde)); module.addSerializer(RawJson.class, InternalSerializers.RAW_JSON_SERIALIZER); module.addSerializer(InternalRequest.class, InternalSerializers.REQUEST); module.addSerializer(CollectionType.class, InternalSerializers.COLLECTION_TYPE); module.addDeserializer(RawJson.class, InternalDeserializers.RAW_JSON_DESERIALIZER); + module.addDeserializer(RawBytes.class, InternalDeserializers.RAW_BYTES_DESERIALIZER); module.addDeserializer(CollectionStatus.class, InternalDeserializers.COLLECTION_STATUS); module.addDeserializer(CollectionType.class, InternalDeserializers.COLLECTION_TYPE); module.addDeserializer(ReplicationFactor.class, InternalDeserializers.REPLICATION_FACTOR); module.addDeserializer(InternalResponse.class, InternalDeserializers.RESPONSE); module.addDeserializer(InvertedIndexPrimarySort.Field.class, InternalDeserializers.INVERTED_INDEX_PRIMARY_SORT_FIELD); - } - @Override - public Module get() { return module; } - } diff --git a/core/src/main/java/com/arangodb/internal/serde/InternalSerde.java b/core/src/main/java/com/arangodb/internal/serde/InternalSerde.java index 758cc550a..d9ca65ef1 100644 --- a/core/src/main/java/com/arangodb/internal/serde/InternalSerde.java +++ b/core/src/main/java/com/arangodb/internal/serde/InternalSerde.java @@ -3,6 +3,8 @@ import com.arangodb.arch.UsedInApi; import com.arangodb.serde.ArangoSerde; import com.arangodb.ContentType; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.JsonNode; import java.lang.reflect.Type; @@ -15,6 +17,7 @@ public interface InternalSerde extends ArangoSerde { * * @param content byte array * @return JSON string + * @implSpec return {@code "[Unparsable data]"} in case of parsing exception */ String toJsonString(byte[] content); @@ -58,14 +61,6 @@ default T deserialize(JsonNode node, Class clazz) { */ T deserialize(JsonNode node, Type type); - /** - * Parses the content. - * - * @param content VPack or byte encoded JSON string - * @return root of the parsed tree - */ - JsonNode parse(byte[] content); - /** * Parses the content at json pointer. * @@ -98,7 +93,7 @@ default T deserialize(byte[] content, String jsonPointer, Class clazz) { * @return deserialized object */ default T deserialize(byte[] content, String jsonPointer, Type type) { - return deserialize(parse(content, jsonPointer), type); + return deserialize(extract(content, jsonPointer), type); } /** @@ -130,30 +125,26 @@ default T deserialize(byte[] content, String jsonPointer, Type type) { * Deserializes the content and binds it to the target data type, using the user serde. * * @param content byte array to deserialize - * @param type target data type + * @param clazz class of target data type * @return deserialized object */ - T deserializeUserData(byte[] content, Type type); + T deserializeUserData(byte[] content, JavaType clazz); /** - * Deserializes the parsed json node and binds it to the target data type, using the user serde. + * Deserializes the parsed json node and binds it to the target data type. + * The parser is not closed. * - * @param node parsed json node - * @param clazz class of target data type + * @param parser json parser + * @param clazz class of target data type * @return deserialized object */ - default T deserializeUserData(JsonNode node, Class clazz) { - return deserializeUserData(node, (Type) clazz); - } + T deserializeUserData(JsonParser parser, JavaType clazz); /** - * Deserializes the parsed json node and binds it to the target data type, using the user serde. - * - * @param node parsed json node - * @param type target data type - * @return deserialized object + * @param content byte array to deserialize + * @return whether the content represents a document (i.e. it has at least one field name equal to _id, _key, _rev) */ - T deserializeUserData(JsonNode node, Type type); + boolean isDocument(byte[] content); /** * @return the user serde diff --git a/core/src/main/java/com/arangodb/internal/serde/InternalSerdeImpl.java b/core/src/main/java/com/arangodb/internal/serde/InternalSerdeImpl.java index c51e50ab6..1cb1412b2 100644 --- a/core/src/main/java/com/arangodb/internal/serde/InternalSerdeImpl.java +++ b/core/src/main/java/com/arangodb/internal/serde/InternalSerdeImpl.java @@ -1,28 +1,30 @@ package com.arangodb.internal.serde; import com.arangodb.ArangoDBException; -import com.arangodb.entity.BaseDocument; -import com.arangodb.entity.BaseEdgeDocument; import com.arangodb.internal.RequestContextHolder; import com.arangodb.serde.ArangoSerde; import com.arangodb.util.RawBytes; import com.arangodb.util.RawJson; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.JsonToken; import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.Module; import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.lang.reflect.Type; import java.nio.charset.StandardCharsets; -import java.util.List; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; import static com.arangodb.internal.serde.SerdeUtils.checkSupportedJacksonVersion; +import static com.arangodb.internal.serde.SerdeUtils.extractBytes; final class InternalSerdeImpl implements InternalSerde { @@ -38,7 +40,8 @@ final class InternalSerdeImpl implements InternalSerde { this.userSerde = userSerde; mapper.deactivateDefaultTyping(); mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - mapper.registerModule(InternalModule.INSTANCE.get()); + mapper.enable(JsonParser.Feature.INCLUDE_SOURCE_IN_LOCATION); + mapper.registerModule(InternalModule.get(this)); if (protocolModule != null) { mapper.registerModule(protocolModule); } @@ -65,27 +68,48 @@ public T deserialize(byte[] content, Class clazz) { @Override public String toJsonString(final byte[] content) { + if (content == null) { + return ""; + } try { return SerdeUtils.INSTANCE.writeJson(mapper.readTree(content)); - } catch (IOException e) { - throw ArangoDBException.of(e); + } catch (Exception e) { + return "[Unparsable data]"; } } @Override public byte[] extract(final byte[] content, final String jsonPointer) { - try { - JsonNode target = parse(content).at(jsonPointer); - return mapper.writeValueAsBytes(target); - } catch (IOException e) { - throw ArangoDBException.of(e); + if (!jsonPointer.startsWith("/")) { + throw new ArangoDBException("Unsupported JSON pointer: " + jsonPointer); } - } - - @Override - public JsonNode parse(byte[] content) { - try { - return mapper.readTree(content); + String[] parts = jsonPointer.substring(1).split("/"); + try (JsonParser parser = mapper.getFactory().createParser(content)) { + int match = 0; + int level = 0; + JsonToken token = parser.nextToken(); + if (token != JsonToken.START_OBJECT) { + throw new ArangoDBException("Unable to parse token: " + token); + } + while (true) { + token = parser.nextToken(); + if (token == JsonToken.START_OBJECT) { + level++; + } + if (token == JsonToken.END_OBJECT) { + level--; + } + if (token == null || level < match) { + throw new ArangoDBException("Unable to parse JSON pointer: " + jsonPointer); + } + if (token == JsonToken.FIELD_NAME && match == level && parts[match].equals(parser.getText())) { + match++; + if (match == parts.length) { + parser.nextToken(); + return extractBytes(parser); + } + } + } } catch (IOException e) { throw ArangoDBException.of(e); } @@ -110,7 +134,7 @@ public byte[] serializeUserData(Object value) { return ((RawBytes) value).get(); } else if (RawJson.class.equals(clazz) && JsonFactory.FORMAT_NAME_JSON.equals(mapper.getFactory().getFormatName())) { return ((RawJson) value).get().getBytes(StandardCharsets.UTF_8); - } else if (isManagedClass(clazz)) { + } else if (SerdeUtils.isManagedClass(clazz)) { return serialize(value); } else { return userSerde.serialize(value); @@ -119,21 +143,23 @@ public byte[] serializeUserData(Object value) { @Override public byte[] serializeCollectionUserData(Iterable value) { - List jsonNodeCollection = StreamSupport.stream(value.spliterator(), false) - .map(this::serializeUserData) - .map(this::parse) - .collect(Collectors.toList()); - return serialize(jsonNodeCollection); + ByteArrayOutputStream os = new ByteArrayOutputStream(); + try (JsonGenerator gen = mapper.getFactory().createGenerator(os)) { + gen.writeStartArray(); + for (Object o : value) { + gen.writeRawValue(new RawUserDataValue(serializeUserData(o))); + } + gen.writeEndArray(); + gen.flush(); + } catch (IOException e) { + throw ArangoDBException.of(e); + } + return os.toByteArray(); } @Override - @SuppressWarnings("unchecked") public T deserializeUserData(byte[] content, Class clazz) { - if (RawBytes.class.equals(clazz)) { - return (T) RawBytes.of(content); - } else if (RawJson.class.equals(clazz) && JsonFactory.FORMAT_NAME_JSON.equals(mapper.getFactory().getFormatName())) { - return (T) RawJson.of(new String(content, StandardCharsets.UTF_8)); - } else if (isManagedClass(clazz)) { + if (SerdeUtils.isManagedClass(clazz)) { return deserialize(content, clazz); } else { return userSerde.deserialize(content, clazz, RequestContextHolder.INSTANCE.getCtx()); @@ -142,17 +168,61 @@ public T deserializeUserData(byte[] content, Class clazz) { @Override @SuppressWarnings("unchecked") - public T deserializeUserData(byte[] content, Type type) { - if (type instanceof Class) { - return deserializeUserData(content, (Class) type); - } else { - throw new UnsupportedOperationException(); + public T deserializeUserData(byte[] content, JavaType clazz) { + try { + if (SerdeUtils.isManagedClass(clazz.getRawClass())) { + return mapper.readerFor(clazz).readValue(content); + } else { + return deserializeUserData(content, (Class) clazz.getRawClass()); + } + } catch (IOException e) { + throw ArangoDBException.of(e); } } @Override - public T deserializeUserData(JsonNode node, Type type) { - return deserializeUserData(serialize(node), type); + public T deserializeUserData(JsonParser parser, JavaType clazz) { + try { + if (SerdeUtils.isManagedClass(clazz.getRawClass())) { + return mapper.readerFor(clazz).readValue(parser); + } else { + return deserializeUserData(extractBytes(parser), clazz); + } + } catch (IOException e) { + throw ArangoDBException.of(e); + } + } + + @Override + public boolean isDocument(byte[] content) { + try (JsonParser p = mapper.getFactory().createParser(content)) { + if (p.nextToken() != JsonToken.START_OBJECT) { + return false; + } + + int level = 1; + while (level >= 1) { + JsonToken t = p.nextToken(); + if (level == 1 && t == JsonToken.FIELD_NAME) { + String fieldName = p.getText(); + if (fieldName.equals("_id") || fieldName.equals("_key") || fieldName.equals("_rev")) { + return true; + } + } + if (t.isStructStart()) { + level++; + } else if (t.isStructEnd()) { + level--; + } + } + + if (p.currentToken() != JsonToken.END_OBJECT) { + throw new JsonMappingException(p, "Expected END_OBJECT but got " + p.currentToken()); + } + } catch (IOException e) { + throw ArangoDBException.of(e); + } + return false; } @Override @@ -181,20 +251,4 @@ public T deserialize(final byte[] content, final Type type) { } } - private boolean isManagedClass(Class clazz) { - return JsonNode.class.isAssignableFrom(clazz) || - RawJson.class.equals(clazz) || - RawBytes.class.equals(clazz) || - BaseDocument.class.equals(clazz) || - BaseEdgeDocument.class.equals(clazz) || - isEntityClass(clazz); - } - - private boolean isEntityClass(Class clazz) { - Package pkg = clazz.getPackage(); - if (pkg == null) { - return false; - } - return pkg.getName().startsWith("com.arangodb.entity"); - } } diff --git a/core/src/main/java/com/arangodb/internal/serde/InternalSerializers.java b/core/src/main/java/com/arangodb/internal/serde/InternalSerializers.java index 31c87ae77..db156dff8 100644 --- a/core/src/main/java/com/arangodb/internal/serde/InternalSerializers.java +++ b/core/src/main/java/com/arangodb/internal/serde/InternalSerializers.java @@ -6,11 +6,14 @@ import com.arangodb.internal.ArangoRequestParam; import com.arangodb.util.RawJson; import com.arangodb.internal.InternalRequest; +import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.Collection; import java.util.HashMap; import java.util.Map; @@ -21,7 +24,14 @@ public final class InternalSerializers { static final JsonSerializer RAW_JSON_SERIALIZER = new JsonSerializer() { @Override public void serialize(RawJson value, JsonGenerator gen, SerializerProvider serializers) throws IOException { - gen.writeTree(SerdeUtils.INSTANCE.parseJson(value.get())); + if (JsonFactory.FORMAT_NAME_JSON.equals(gen.getCodec().getFactory().getFormatName())) { + gen.writeRawValue(new RawUserDataValue(value.get().getBytes(StandardCharsets.UTF_8))); + } else { + try (JsonParser parser = SerdeUtils.INSTANCE.getJsonMapper().getFactory().createParser(value.get())) { + parser.nextToken(); + gen.copyCurrentStructure(parser); + } + } } }; static final JsonSerializer REQUEST = new JsonSerializer() { diff --git a/core/src/main/java/com/arangodb/internal/serde/MultiDocumentEntityDeserializer.java b/core/src/main/java/com/arangodb/internal/serde/MultiDocumentEntityDeserializer.java new file mode 100644 index 000000000..ca650569d --- /dev/null +++ b/core/src/main/java/com/arangodb/internal/serde/MultiDocumentEntityDeserializer.java @@ -0,0 +1,69 @@ +package com.arangodb.internal.serde; + +import com.arangodb.entity.ErrorEntity; +import com.arangodb.entity.MultiDocumentEntity; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.BeanProperty; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.deser.ContextualDeserializer; + +import java.io.IOException; + +public class MultiDocumentEntityDeserializer extends JsonDeserializer> implements ContextualDeserializer { + private final JavaType containedType; + private final InternalSerde serde; + + MultiDocumentEntityDeserializer(InternalSerde serde) { + this(serde, null); + } + + MultiDocumentEntityDeserializer(InternalSerde serde, JavaType containedType) { + this.serde = serde; + this.containedType = containedType; + } + + @Override + public MultiDocumentEntity deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + MultiDocumentEntity multiDocument = new MultiDocumentEntity<>(); + + // silent=true returns an empty object + if (p.currentToken() == JsonToken.START_OBJECT) { + if (p.nextToken() == JsonToken.END_OBJECT) { + return multiDocument; + } else { + throw new JsonMappingException(p, "Unexpected token sequence: START_OBJECT, " + p.currentToken()); + } + } + + if (p.currentToken() != JsonToken.START_ARRAY) { + throw new JsonMappingException(p, "Expected START_ARRAY but got " + p.currentToken()); + } + p.nextToken(); + while (p.currentToken() != JsonToken.END_ARRAY) { + if (p.currentToken() != JsonToken.START_OBJECT) { + throw new JsonMappingException(p, "Expected START_OBJECT but got " + p.currentToken()); + } + byte[] element = SerdeUtils.extractBytes(p); + if (serde.isDocument(element)) { + Object d = serde.deserializeUserData(element, containedType); + multiDocument.getDocuments().add(d); + multiDocument.getDocumentsAndErrors().add(d); + } else { + ErrorEntity e = serde.deserialize(element, ErrorEntity.class); + multiDocument.getErrors().add(e); + multiDocument.getDocumentsAndErrors().add(e); + } + p.nextToken(); // END_OBJECT + } + return multiDocument; + } + + @Override + public JsonDeserializer createContextual(DeserializationContext ctxt, BeanProperty property) { + return new MultiDocumentEntityDeserializer(serde, ctxt.getContextualType().containedType(0)); + } +} diff --git a/core/src/main/java/com/arangodb/internal/serde/RawUserDataValue.java b/core/src/main/java/com/arangodb/internal/serde/RawUserDataValue.java new file mode 100644 index 000000000..4bfde90f2 --- /dev/null +++ b/core/src/main/java/com/arangodb/internal/serde/RawUserDataValue.java @@ -0,0 +1,92 @@ +package com.arangodb.internal.serde; + +import com.fasterxml.jackson.core.SerializableString; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; + +class RawUserDataValue implements SerializableString { + private final byte[] data; + + RawUserDataValue(byte[] data) { + this.data = data; + } + + @Override + public String getValue() { + throw new UnsupportedOperationException(); + } + + @Override + public int charLength() { + throw new UnsupportedOperationException(); + } + + @Override + public char[] asQuotedChars() { + throw new UnsupportedOperationException(); + } + + @Override + public byte[] asUnquotedUTF8() { + return data; + } + + @Override + public byte[] asQuotedUTF8() { + throw new UnsupportedOperationException(); + } + + @Override + public int appendQuotedUTF8(byte[] buffer, int offset) { + throw new UnsupportedOperationException(); + } + + @Override + public int appendQuoted(char[] buffer, int offset) { + throw new UnsupportedOperationException(); + } + + @Override + public int appendUnquotedUTF8(byte[] buffer, int offset) { + final int length = data.length; + if ((offset + length) > buffer.length) { + return -1; + } + System.arraycopy(data, 0, buffer, offset, length); + return length; + } + + @Override + public int appendUnquoted(char[] buffer, int offset) { + throw new UnsupportedOperationException(); + } + + @Override + public int writeQuotedUTF8(OutputStream out) { + throw new UnsupportedOperationException(); + } + + @Override + public int writeUnquotedUTF8(OutputStream out) throws IOException { + final int length = data.length; + out.write(data, 0, length); + return length; + } + + @Override + public int putQuotedUTF8(ByteBuffer buffer) { + throw new UnsupportedOperationException(); + } + + @Override + public int putUnquotedUTF8(ByteBuffer buffer) { + final int length = data.length; + if (length > buffer.remaining()) { + return -1; + } + buffer.put(data, 0, length); + return length; + } +} diff --git a/core/src/main/java/com/arangodb/internal/serde/SerdeUtils.java b/core/src/main/java/com/arangodb/internal/serde/SerdeUtils.java index 299d2ff69..9783386c6 100644 --- a/core/src/main/java/com/arangodb/internal/serde/SerdeUtils.java +++ b/core/src/main/java/com/arangodb/internal/serde/SerdeUtils.java @@ -1,7 +1,14 @@ package com.arangodb.internal.serde; import com.arangodb.ArangoDBException; +import com.arangodb.entity.BaseDocument; +import com.arangodb.entity.BaseEdgeDocument; +import com.arangodb.util.RawBytes; +import com.arangodb.util.RawJson; +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.JsonToken; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -9,6 +16,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.IOException; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Arrays; @@ -55,6 +63,10 @@ static void checkSupportedJacksonVersion() { }); } + public ObjectMapper getJsonMapper() { + return jsonMapper; + } + /** * Parse a JSON string. * @@ -81,4 +93,53 @@ public String writeJson(final JsonNode data) { } } + /** + * Extract raw bytes for the current JSON (or VPACK) node + * + * @param parser JsonParser with current token pointing to the node to extract + * @return byte array + */ + @SuppressWarnings("deprecation") + public static byte[] extractBytes(JsonParser parser) throws IOException { + JsonToken t = parser.currentToken(); + if (t.isStructEnd() || t == JsonToken.FIELD_NAME) { + throw new ArangoDBException("Unexpected token: " + t); + } + byte[] data = (byte[]) parser.getTokenLocation().getSourceRef(); + int start = (int) parser.getTokenLocation().getByteOffset(); + int end = (int) parser.getCurrentLocation().getByteOffset(); + if (t.isStructStart()) { + int open = 1; + while (open > 0) { + t = parser.nextToken(); + if (t.isStructStart()) { + open++; + } else if (t.isStructEnd()) { + open--; + } + } + } + parser.finishToken(); + if (JsonFactory.FORMAT_NAME_JSON.equals(parser.getCodec().getFactory().getFormatName())) { + end = (int) parser.getCurrentLocation().getByteOffset(); + } + return Arrays.copyOfRange(data, start, end); + } + + public static boolean isManagedClass(Class clazz) { + return JsonNode.class.isAssignableFrom(clazz) || + RawJson.class.equals(clazz) || + RawBytes.class.equals(clazz) || + BaseDocument.class.equals(clazz) || + BaseEdgeDocument.class.equals(clazz) || + isEntityClass(clazz); + } + + private static boolean isEntityClass(Class clazz) { + Package pkg = clazz.getPackage(); + if (pkg == null) { + return false; + } + return pkg.getName().startsWith("com.arangodb.entity"); + } } diff --git a/core/src/main/java/com/arangodb/internal/serde/UserDataDeserializer.java b/core/src/main/java/com/arangodb/internal/serde/UserDataDeserializer.java index 91220088b..ecb8c83f3 100644 --- a/core/src/main/java/com/arangodb/internal/serde/UserDataDeserializer.java +++ b/core/src/main/java/com/arangodb/internal/serde/UserDataDeserializer.java @@ -29,7 +29,12 @@ private UserDataDeserializer(final JavaType targetType, final InternalSerde serd @Override public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { - return serde.deserializeUserData(p.readValueAsTree(), targetType); + Class clazz = (Class) targetType; + if (SerdeUtils.isManagedClass(clazz)) { + return p.readValueAs(clazz); + } else { + return serde.deserializeUserData(SerdeUtils.extractBytes(p), clazz); + } } @Override @@ -41,4 +46,5 @@ public Object deserializeWithType(JsonParser p, DeserializationContext ctxt, Typ public JsonDeserializer createContextual(DeserializationContext ctxt, BeanProperty property) { return new UserDataDeserializer(ctxt.getContextualType(), serde); } + } diff --git a/core/src/main/java/com/arangodb/internal/serde/UserDataSerializer.java b/core/src/main/java/com/arangodb/internal/serde/UserDataSerializer.java index d9a6acb30..501998da4 100644 --- a/core/src/main/java/com/arangodb/internal/serde/UserDataSerializer.java +++ b/core/src/main/java/com/arangodb/internal/serde/UserDataSerializer.java @@ -1,7 +1,7 @@ package com.arangodb.internal.serde; import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; @@ -16,10 +16,10 @@ class UserDataSerializer extends JsonSerializer { @Override public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException { - // TODO: find a way to append raw bytes directly - // see https://github.com/FasterXML/jackson-core/issues/914 - try (JsonParser parser = gen.getCodec().getFactory().createParser(serde.serializeUserData(value))) { - gen.writeTree(parser.readValueAsTree()); + if (value != null && JsonNode.class.isAssignableFrom(value.getClass())) { + gen.writeTree((JsonNode) value); + } else { + gen.writeRawValue(new RawUserDataValue(serde.serializeUserData(value))); } } } diff --git a/core/src/main/java/com/arangodb/internal/util/SerdeUtils.java b/core/src/main/java/com/arangodb/internal/util/SerdeUtils.java deleted file mode 100644 index 06e5edbb9..000000000 --- a/core/src/main/java/com/arangodb/internal/util/SerdeUtils.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.arangodb.internal.util; - -import com.arangodb.internal.serde.InternalSerde; - -public class SerdeUtils { - private SerdeUtils() { - } - - public static String toJsonString(InternalSerde serde, byte[] data) { - if (data == null) { - return ""; - } - try { - return serde.toJsonString(data); - } catch (Exception e) { - return "[Unparsable data]"; - } - } -} diff --git a/driver/pom.xml b/driver/pom.xml index a2791cf21..bc2c2e293 100644 --- a/driver/pom.xml +++ b/driver/pom.xml @@ -8,7 +8,7 @@ ../release-parent com.arangodb release-parent - 7.14.0 + 7.15.0-SNAPSHOT arangodb-java-driver diff --git a/http-protocol/pom.xml b/http-protocol/pom.xml index 2a783fba1..a7e2e10e3 100644 --- a/http-protocol/pom.xml +++ b/http-protocol/pom.xml @@ -8,7 +8,7 @@ ../release-parent com.arangodb release-parent - 7.14.0 + 7.15.0-SNAPSHOT http-protocol diff --git a/jackson-serde-json/pom.xml b/jackson-serde-json/pom.xml index fb6ed0929..417c3b619 100644 --- a/jackson-serde-json/pom.xml +++ b/jackson-serde-json/pom.xml @@ -8,7 +8,7 @@ ../release-parent com.arangodb release-parent - 7.14.0 + 7.15.0-SNAPSHOT jackson-serde-json diff --git a/jackson-serde-vpack/pom.xml b/jackson-serde-vpack/pom.xml index 83e1a0edf..7d258b789 100644 --- a/jackson-serde-vpack/pom.xml +++ b/jackson-serde-vpack/pom.xml @@ -8,7 +8,7 @@ ../release-parent com.arangodb release-parent - 7.14.0 + 7.15.0-SNAPSHOT jackson-serde-vpack diff --git a/jsonb-serde/pom.xml b/jsonb-serde/pom.xml index 6f883f06f..d8dffbe49 100644 --- a/jsonb-serde/pom.xml +++ b/jsonb-serde/pom.xml @@ -8,7 +8,7 @@ ../release-parent com.arangodb release-parent - 7.14.0 + 7.15.0-SNAPSHOT jsonb-serde diff --git a/pom.xml b/pom.xml index efcc9fef4..727b4f4bb 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ com.arangodb arangodb-java-driver-parent - 7.14.0 + 7.15.0-SNAPSHOT 2016 release-parent @@ -93,7 +93,7 @@ com.fasterxml.jackson jackson-bom - 2.18.0 + 2.18.2 import pom @@ -140,7 +140,7 @@ com.arangodb jackson-dataformat-velocypack - 4.4.0 + 4.5.0 com.arangodb @@ -325,6 +325,19 @@ + + + oss.sonatype.org-snapshot + https://oss.sonatype.org/content/repositories/snapshots + + false + + + true + + + + https://github.com/arangodb/arangodb-java-driver scm:git:git://github.com/arangodb/arangodb-java-driver.git diff --git a/release-parent/pom.xml b/release-parent/pom.xml index ecb3a61f4..c674c6f35 100644 --- a/release-parent/pom.xml +++ b/release-parent/pom.xml @@ -6,7 +6,7 @@ com.arangodb arangodb-java-driver-parent - 7.14.0 + 7.15.0-SNAPSHOT pom diff --git a/shaded/pom.xml b/shaded/pom.xml index 887edb7bc..858269964 100644 --- a/shaded/pom.xml +++ b/shaded/pom.xml @@ -8,7 +8,7 @@ ../release-parent com.arangodb release-parent - 7.14.0 + 7.15.0-SNAPSHOT arangodb-java-driver-shaded diff --git a/test-functional/pom.xml b/test-functional/pom.xml index b53baa7c6..3eaecde1d 100644 --- a/test-functional/pom.xml +++ b/test-functional/pom.xml @@ -8,7 +8,7 @@ ../test-parent com.arangodb test-parent - 7.14.0 + 7.15.0-SNAPSHOT test-functional diff --git a/test-functional/src/test/java/com/arangodb/ArangoCollectionTest.java b/test-functional/src/test/java/com/arangodb/ArangoCollectionTest.java index b70da4d4d..ea21964db 100644 --- a/test-functional/src/test/java/com/arangodb/ArangoCollectionTest.java +++ b/test-functional/src/test/java/com/arangodb/ArangoCollectionTest.java @@ -754,6 +754,34 @@ void getDocuments(ArangoCollection collection) { } } + @ParameterizedTest + @MethodSource("cols") + void getDocumentsUserData(ArangoCollection collection) { + Cat a = new Cat(); + a.setKey(UUID.randomUUID().toString()); + a.setName("a"); + + Cat b = new Cat(); + b.setKey(UUID.randomUUID().toString()); + b.setName("b"); + + final List values = Arrays.asList(a, b); + collection.insertDocuments(values); + final MultiDocumentEntity documents = collection.getDocuments(Arrays.asList(a.getKey(), b.getKey()), + Cat.class); + assertThat(documents).isNotNull(); + assertThat(documents.getDocuments()) + .hasSize(2) + .anySatisfy(d -> { + assertThat(d.getKey()).isEqualTo(a.getKey()); + assertThat(d.getName()).isEqualTo(a.getName()); + }) + .anySatisfy(d -> { + assertThat(d.getKey()).isEqualTo(b.getKey()); + assertThat(d.getName()).isEqualTo(b.getName()); + }); + } + @ParameterizedTest @MethodSource("cols") void getDocumentsWithCustomShardingKey(ArangoCollection c) { @@ -2413,6 +2441,33 @@ void insertDocuments(ArangoCollection collection) { assertThat(docs.getErrors()).isEmpty(); } + @ParameterizedTest + @MethodSource("cols") + void insertDocumentsReturnNewUserData(ArangoCollection collection) { + Cat a = new Cat(); + a.setKey(UUID.randomUUID().toString()); + a.setName("a"); + + Cat b = new Cat(); + b.setKey(UUID.randomUUID().toString()); + b.setName("b"); + + final List values = Arrays.asList(a, b); + MultiDocumentEntity> res = + collection.insertDocuments(values, new DocumentCreateOptions().returnNew(true), Cat.class); + assertThat(res).isNotNull(); + assertThat(res.getDocuments()) + .hasSize(2) + .anySatisfy(d -> { + assertThat(d.getKey()).isEqualTo(a.getKey()); + assertThat(d.getNew().getName()).isEqualTo(a.getName()); + }) + .anySatisfy(d -> { + assertThat(d.getKey()).isEqualTo(b.getKey()); + assertThat(d.getNew().getName()).isEqualTo(b.getName()); + }); + } + @ParameterizedTest @MethodSource("cols") void insertDocumentsOverwriteModeUpdate(ArangoCollection collection) { @@ -2537,9 +2592,10 @@ void insertDocumentsReturnNew(ArangoCollection collection) { for (final DocumentCreateEntity doc : docs.getDocuments()) { assertThat(doc.getNew()).isNotNull(); final BaseDocument baseDocument = doc.getNew(); + assertThat(baseDocument.getId()).isNotNull(); assertThat(baseDocument.getKey()).isNotNull(); + assertThat(baseDocument.getRevision()).isNotNull(); } - } @ParameterizedTest diff --git a/test-functional/src/test/java/com/arangodb/ArangoDatabaseAsyncTest.java b/test-functional/src/test/java/com/arangodb/ArangoDatabaseAsyncTest.java index cc8aab2ad..bd6f45a2b 100644 --- a/test-functional/src/test/java/com/arangodb/ArangoDatabaseAsyncTest.java +++ b/test-functional/src/test/java/com/arangodb/ArangoDatabaseAsyncTest.java @@ -675,7 +675,7 @@ void queryRawBytes(ArangoDatabaseAsync db) throws ExecutionException, Interrupte RawBytes doc = RawBytes.of(serde.serialize(Collections.singletonMap("value", 1))); RawBytes res = db.query("RETURN @doc", RawBytes.class, Collections.singletonMap("doc", doc)).get() .getResult().get(0); - JsonNode data = serde.parse(res.get()); + JsonNode data = serde.deserialize(res.get(), JsonNode.class); assertThat(data.isObject()).isTrue(); assertThat(data.get("value").isNumber()).isTrue(); assertThat(data.get("value").numberValue()).isEqualTo(1); diff --git a/test-functional/src/test/java/com/arangodb/ArangoDatabaseTest.java b/test-functional/src/test/java/com/arangodb/ArangoDatabaseTest.java index 0cfd5b63c..ea487e07c 100644 --- a/test-functional/src/test/java/com/arangodb/ArangoDatabaseTest.java +++ b/test-functional/src/test/java/com/arangodb/ArangoDatabaseTest.java @@ -762,12 +762,45 @@ void queryRawBytes(ArangoDatabase db) { InternalSerde serde = db.getSerde(); RawBytes doc = RawBytes.of(serde.serialize(Collections.singletonMap("value", 1))); RawBytes res = db.query("RETURN @doc", RawBytes.class, Collections.singletonMap("doc", doc)).next(); - JsonNode data = serde.parse(res.get()); + JsonNode data = serde.deserialize(res.get(), JsonNode.class); assertThat(data.isObject()).isTrue(); assertThat(data.get("value").isNumber()).isTrue(); assertThat(data.get("value").numberValue()).isEqualTo(1); } + @ParameterizedTest + @MethodSource("dbs") + void queryUserDataScalar(ArangoDatabase db) { + List docs = Arrays.asList("a", "b", "c"); + ArangoCursor res = db.query("FOR d IN @docs RETURN d", String.class, + Collections.singletonMap("docs", docs), new AqlQueryOptions().batchSize(1)); + assertThat((Iterable) res).contains("a", "b", "c"); + } + + @ParameterizedTest + @MethodSource("dbs") + void queryUserDataManaged(ArangoDatabase db) { + RawJson a = RawJson.of("\"foo\""); + RawJson b = RawJson.of("{\"key\":\"value\"}"); + RawJson c = RawJson.of("[1,null,true,\"bla\",{},[],\"\"]"); + RawJson docs = RawJson.of("[" + a.get() + "," + b.get() + "," + c.get() + "]"); + ArangoCursor res = db.query("FOR d IN @docs RETURN d", RawJson.class, + Collections.singletonMap("docs", docs), new AqlQueryOptions().batchSize(1)); + assertThat((Iterable) res).containsExactly(a, b, c); + } + + @ParameterizedTest + @MethodSource("dbs") + void queryUserData(ArangoDatabase db) { + Object a = "foo"; + Object b = Collections.singletonMap("key", "value"); + Object c = Arrays.asList(1, null, true, "bla", Collections.emptyMap(), Collections.emptyList(), ""); + List docs = Arrays.asList(a, b, c); + ArangoCursor res = db.query("FOR d IN @docs RETURN d", Object.class, + Collections.singletonMap("docs", docs), new AqlQueryOptions().batchSize(1)); + assertThat((Iterable) res).containsExactly(a, b, c); + } + @ParameterizedTest @MethodSource("dbs") void changeQueryCache(ArangoDatabase db) { diff --git a/test-functional/src/test/java/com/arangodb/UserAgentTest.java b/test-functional/src/test/java/com/arangodb/UserAgentTest.java index 1aac8adf1..46458f609 100644 --- a/test-functional/src/test/java/com/arangodb/UserAgentTest.java +++ b/test-functional/src/test/java/com/arangodb/UserAgentTest.java @@ -10,7 +10,7 @@ class UserAgentTest extends BaseJunit5 { - private static final String EXPECTED_VERSION = "7.14.0"; + private static final String EXPECTED_VERSION = "7.15.0-SNAPSHOT"; private static final boolean SHADED = Boolean.parseBoolean(System.getProperty("shaded")); diff --git a/test-non-functional/pom.xml b/test-non-functional/pom.xml index 8c1da5cf6..8e33bde71 100644 --- a/test-non-functional/pom.xml +++ b/test-non-functional/pom.xml @@ -8,7 +8,7 @@ ../test-parent com.arangodb test-parent - 7.14.0 + 7.15.0-SNAPSHOT test-non-functional diff --git a/test-parent/pom.xml b/test-parent/pom.xml index 684ed7b78..3199ae192 100644 --- a/test-parent/pom.xml +++ b/test-parent/pom.xml @@ -7,7 +7,7 @@ com.arangodb arangodb-java-driver-parent - 7.14.0 + 7.15.0-SNAPSHOT pom diff --git a/test-perf/pom.xml b/test-perf/pom.xml index b6e419ca7..1479f6da5 100644 --- a/test-perf/pom.xml +++ b/test-perf/pom.xml @@ -7,7 +7,7 @@ ../test-parent com.arangodb test-parent - 7.14.0 + 7.15.0-SNAPSHOT test-perf diff --git a/test-resilience/pom.xml b/test-resilience/pom.xml index fb67d0306..8af00cdd6 100644 --- a/test-resilience/pom.xml +++ b/test-resilience/pom.xml @@ -6,7 +6,7 @@ ../test-parent com.arangodb test-parent - 7.14.0 + 7.15.0-SNAPSHOT 4.0.0 diff --git a/test-resilience/src/test/java/resilience/MockTest.java b/test-resilience/src/test/java/resilience/MockTest.java new file mode 100644 index 000000000..c75ecc27a --- /dev/null +++ b/test-resilience/src/test/java/resilience/MockTest.java @@ -0,0 +1,40 @@ +package resilience; + +import ch.qos.logback.classic.Level; +import com.arangodb.ArangoDB; +import com.arangodb.Protocol; +import com.arangodb.internal.net.Communication; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.mockserver.integration.ClientAndServer; + +import java.util.Collections; + +import static org.mockserver.integration.ClientAndServer.startClientAndServer; + +public class MockTest extends SingleServerTest { + + protected ClientAndServer mockServer; + protected ArangoDB arangoDB; + + public MockTest() { + super(Collections.singletonMap(Communication.class, Level.DEBUG)); + } + + @BeforeEach + void before() { + mockServer = startClientAndServer(getEndpoint().getHost(), getEndpoint().getPort()); + arangoDB = new ArangoDB.Builder() + .protocol(Protocol.HTTP_JSON) + .password(PASSWORD) + .host("127.0.0.1", mockServer.getPort()) + .build(); + } + + @AfterEach + void after() { + arangoDB.shutdown(); + mockServer.stop(); + } + +} diff --git a/test-resilience/src/test/java/resilience/http/MockTest.java b/test-resilience/src/test/java/resilience/mock/SerdeTest.java similarity index 57% rename from test-resilience/src/test/java/resilience/http/MockTest.java rename to test-resilience/src/test/java/resilience/mock/SerdeTest.java index 7e3e958d4..2d559c2aa 100644 --- a/test-resilience/src/test/java/resilience/http/MockTest.java +++ b/test-resilience/src/test/java/resilience/mock/SerdeTest.java @@ -1,99 +1,23 @@ -package resilience.http; +package resilience.mock; import ch.qos.logback.classic.Level; -import com.arangodb.ArangoDB; import com.arangodb.ArangoDBException; -import com.arangodb.Protocol; -import com.arangodb.internal.net.Communication; +import com.arangodb.entity.MultiDocumentEntity; import com.fasterxml.jackson.core.JsonParseException; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; +import com.fasterxml.jackson.databind.JsonNode; import org.junit.jupiter.api.Test; -import org.mockserver.integration.ClientAndServer; -import org.mockserver.matchers.Times; -import resilience.SingleServerTest; +import resilience.MockTest; -import java.util.Collections; -import java.util.concurrent.ExecutionException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; -import static org.mockserver.integration.ClientAndServer.startClientAndServer; import static org.mockserver.model.HttpRequest.request; import static org.mockserver.model.HttpResponse.response; -class MockTest extends SingleServerTest { - - private ClientAndServer mockServer; - private ArangoDB arangoDB; - - public MockTest() { - super(Collections.singletonMap(Communication.class, Level.DEBUG)); - } - - @BeforeEach - void before() { - mockServer = startClientAndServer(getEndpoint().getHost(), getEndpoint().getPort()); - arangoDB = new ArangoDB.Builder() - .protocol(Protocol.HTTP_JSON) - .password(PASSWORD) - .host("127.0.0.1", mockServer.getPort()) - .build(); - } - - @AfterEach - void after() { - arangoDB.shutdown(); - mockServer.stop(); - } - - @Test - void retryOn503() { - arangoDB.getVersion(); - - mockServer - .when( - request() - .withMethod("GET") - .withPath("/.*/_api/version"), - Times.exactly(2) - ) - .respond( - response() - .withStatusCode(503) - .withBody("{\"error\":true,\"errorNum\":503,\"errorMessage\":\"boom\",\"code\":503}") - ); - - logs.reset(); - arangoDB.getVersion(); - assertThat(logs.getLogs()) - .filteredOn(e -> e.getLevel().equals(Level.WARN)) - .anyMatch(e -> e.getFormattedMessage().contains("Could not connect to host")); - } - - @Test - void retryOn503Async() throws ExecutionException, InterruptedException { - arangoDB.async().getVersion().get(); - - mockServer - .when( - request() - .withMethod("GET") - .withPath("/.*/_api/version"), - Times.exactly(2) - ) - .respond( - response() - .withStatusCode(503) - .withBody("{\"error\":true,\"errorNum\":503,\"errorMessage\":\"boom\",\"code\":503}") - ); - - logs.reset(); - arangoDB.async().getVersion().get(); - assertThat(logs.getLogs()) - .filteredOn(e -> e.getLevel().equals(Level.WARN)) - .anyMatch(e -> e.getFormattedMessage().contains("Could not connect to host")); - } +public class SerdeTest extends MockTest { @Test void unparsableData() { @@ -178,4 +102,35 @@ void textPlainDataWithCharset() { .hasMessageContaining("upstream timed out"); } + @Test + void getDocumentsWithErrorField() { + List keys = Arrays.asList("1", "2", "3"); + + String resp = "[" + + "{\"error\":true,\"_key\":\"1\",\"_id\":\"col/1\",\"_rev\":\"_i4otI-q---\"}," + + "{\"_key\":\"2\",\"_id\":\"col/2\",\"_rev\":\"_i4otI-q--_\"}," + + "{\"_key\":\"3\",\"_id\":\"col/3\",\"_rev\":\"_i4otI-q--A\"}" + + "]"; + + mockServer + .when( + request() + .withMethod("PUT") + .withPath("/.*/_api/document/col") + .withQueryStringParameter("onlyget", "true") + ) + .respond( + response() + .withStatusCode(200) + .withHeader("Content-Type", "application/json; charset=utf-8") + .withBody(resp.getBytes(StandardCharsets.UTF_8)) + ); + + MultiDocumentEntity res = arangoDB.db().collection("col").getDocuments(keys, JsonNode.class); + assertThat(res.getErrors()).isEmpty(); + assertThat(res.getDocuments()).hasSize(3) + .anySatisfy(d -> assertThat(d.get("_key").textValue()).isEqualTo("1")) + .anySatisfy(d -> assertThat(d.get("_key").textValue()).isEqualTo("2")) + .anySatisfy(d -> assertThat(d.get("_key").textValue()).isEqualTo("3")); + } } diff --git a/test-resilience/src/test/java/resilience/mock/ServiceUnavailableTest.java b/test-resilience/src/test/java/resilience/mock/ServiceUnavailableTest.java new file mode 100644 index 000000000..358311cf4 --- /dev/null +++ b/test-resilience/src/test/java/resilience/mock/ServiceUnavailableTest.java @@ -0,0 +1,65 @@ +package resilience.mock; + +import ch.qos.logback.classic.Level; +import org.junit.jupiter.api.Test; +import org.mockserver.matchers.Times; +import resilience.MockTest; + +import java.util.concurrent.ExecutionException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockserver.model.HttpRequest.request; +import static org.mockserver.model.HttpResponse.response; + +class ServiceUnavailableTest extends MockTest { + + @Test + void retryOn503() { + arangoDB.getVersion(); + + mockServer + .when( + request() + .withMethod("GET") + .withPath("/.*/_api/version"), + Times.exactly(2) + ) + .respond( + response() + .withStatusCode(503) + .withBody("{\"error\":true,\"errorNum\":503,\"errorMessage\":\"boom\",\"code\":503}") + ); + + logs.reset(); + arangoDB.getVersion(); + assertThat(logs.getLogs()) + .filteredOn(e -> e.getLevel().equals(Level.WARN)) + .anyMatch(e -> e.getFormattedMessage().contains("Could not connect to host")); + } + + @Test + void retryOn503Async() throws ExecutionException, InterruptedException { + arangoDB.async().getVersion().get(); + + mockServer + .when( + request() + .withMethod("GET") + .withPath("/.*/_api/version"), + Times.exactly(2) + ) + .respond( + response() + .withStatusCode(503) + .withBody("{\"error\":true,\"errorNum\":503,\"errorMessage\":\"boom\",\"code\":503}") + ); + + logs.reset(); + arangoDB.async().getVersion().get(); + assertThat(logs.getLogs()) + .filteredOn(e -> e.getLevel().equals(Level.WARN)) + .anyMatch(e -> e.getFormattedMessage().contains("Could not connect to host")); + } + + +} diff --git a/tutorial/gradle/build.gradle b/tutorial/gradle/build.gradle index 1dc14f8f5..fa2c1c9cb 100644 --- a/tutorial/gradle/build.gradle +++ b/tutorial/gradle/build.gradle @@ -12,7 +12,7 @@ repositories { } dependencies { - implementation 'com.arangodb:arangodb-java-driver:7.14.0' + implementation 'com.arangodb:arangodb-java-driver:7.15.0-SNAPSHOT' } ext { diff --git a/tutorial/maven/pom.xml b/tutorial/maven/pom.xml index 236cbf5f4..9da9981dd 100644 --- a/tutorial/maven/pom.xml +++ b/tutorial/maven/pom.xml @@ -19,7 +19,7 @@ com.arangodb arangodb-java-driver - 7.14.0 + 7.15.0-SNAPSHOT diff --git a/vst-protocol/pom.xml b/vst-protocol/pom.xml index 60909d93f..0baa3e41a 100644 --- a/vst-protocol/pom.xml +++ b/vst-protocol/pom.xml @@ -8,7 +8,7 @@ ../release-parent com.arangodb release-parent - 7.14.0 + 7.15.0-SNAPSHOT vst-protocol