Skip to content

Commit ece6667

Browse files
committed
fixed deserialization of multiple documents
1 parent a77c69a commit ece6667

File tree

7 files changed

+204
-123
lines changed

7 files changed

+204
-123
lines changed

core/src/main/java/com/arangodb/internal/serde/InternalSerde.java

+8-2
Original file line numberDiff line numberDiff line change
@@ -125,10 +125,10 @@ default <T> T deserialize(byte[] content, String jsonPointer, Type type) {
125125
* Deserializes the content and binds it to the target data type, using the user serde.
126126
*
127127
* @param content byte array to deserialize
128-
* @param type target data type
128+
* @param clazz class of target data type
129129
* @return deserialized object
130130
*/
131-
<T> T deserializeUserData(byte[] content, Type type);
131+
<T> T deserializeUserData(byte[] content, JavaType clazz);
132132

133133
/**
134134
* Deserializes the parsed json node and binds it to the target data type.
@@ -140,6 +140,12 @@ default <T> T deserialize(byte[] content, String jsonPointer, Type type) {
140140
*/
141141
<T> T deserializeUserData(JsonParser parser, JavaType clazz);
142142

143+
/**
144+
* @param content byte array to deserialize
145+
* @return whether the content represents a document (i.e. it has at least one field name equal to _id, _key, _rev)
146+
*/
147+
boolean isDocument(byte[] content);
148+
143149
/**
144150
* @return the user serde
145151
*/

core/src/main/java/com/arangodb/internal/serde/InternalSerdeImpl.java

+42-6
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import com.fasterxml.jackson.core.JsonToken;
1414
import com.fasterxml.jackson.databind.DeserializationFeature;
1515
import com.fasterxml.jackson.databind.JavaType;
16+
import com.fasterxml.jackson.databind.JsonMappingException;
1617
import com.fasterxml.jackson.databind.JsonNode;
1718
import com.fasterxml.jackson.databind.Module;
1819
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -166,12 +167,15 @@ public <T> T deserializeUserData(byte[] content, Class<T> clazz) {
166167
}
167168

168169
@Override
169-
@SuppressWarnings("unchecked")
170-
public <T> T deserializeUserData(byte[] content, Type type) {
171-
if (type instanceof Class) {
172-
return deserializeUserData(content, (Class<T>) type);
173-
} else {
174-
throw new UnsupportedOperationException();
170+
public <T> T deserializeUserData(byte[] content, JavaType clazz) {
171+
try {
172+
if (SerdeUtils.isManagedClass(clazz.getRawClass())) {
173+
return mapper.readerFor(clazz).readValue(content);
174+
} else {
175+
return deserializeUserData(content, clazz);
176+
}
177+
} catch (IOException e) {
178+
throw ArangoDBException.of(e);
175179
}
176180
}
177181

@@ -188,6 +192,38 @@ public <T> T deserializeUserData(JsonParser parser, JavaType clazz) {
188192
}
189193
}
190194

195+
@Override
196+
public boolean isDocument(byte[] content) {
197+
try (JsonParser p = mapper.createParser(content)) {
198+
if (p.nextToken() != JsonToken.START_OBJECT) {
199+
return false;
200+
}
201+
202+
int level = 1;
203+
while (level >= 1) {
204+
JsonToken t = p.nextToken();
205+
if (level == 1 && t == JsonToken.FIELD_NAME) {
206+
String fieldName = p.getText();
207+
if (fieldName.equals("_id") || fieldName.equals("_key") || fieldName.equals("_rev")) {
208+
return true;
209+
}
210+
}
211+
if (t.isStructStart()) {
212+
level++;
213+
} else if (t.isStructEnd()) {
214+
level--;
215+
}
216+
}
217+
218+
if (p.currentToken() != JsonToken.END_OBJECT) {
219+
throw new JsonMappingException(p, "Expected END_OBJECT but got " + p.currentToken());
220+
}
221+
} catch (IOException e) {
222+
throw ArangoDBException.of(e);
223+
}
224+
return false;
225+
}
226+
191227
@Override
192228
public ArangoSerde getUserSerde() {
193229
return userSerde;

core/src/main/java/com/arangodb/internal/serde/MultiDocumentEntityDeserializer.java

+9-30
Original file line numberDiff line numberDiff line change
@@ -47,36 +47,15 @@ public MultiDocumentEntity<?> deserialize(JsonParser p, DeserializationContext c
4747
if (p.currentToken() != JsonToken.START_OBJECT) {
4848
throw new JsonMappingException(p, "Expected START_OBJECT but got " + p.currentToken());
4949
}
50-
p.nextToken();
51-
if (p.currentToken() != JsonToken.FIELD_NAME) {
52-
throw new JsonMappingException(p, "Expected FIELD_NAME but got " + p.currentToken());
53-
}
54-
String fieldName = p.getText();
55-
// FIXME: this can potentially fail for: MultiDocumentEntity<T> getDocuments()
56-
// fix by scanning the 1st level field names and checking if any matches:
57-
// - "_id"
58-
// - "_key"
59-
// - "_rev"
60-
switch (fieldName) {
61-
case "_id":
62-
case "_key":
63-
case "_rev":
64-
case "_oldRev":
65-
case "new":
66-
case "old":
67-
Object d = serde.deserializeUserData(p, containedType);
68-
multiDocument.getDocuments().add(d);
69-
multiDocument.getDocumentsAndErrors().add(d);
70-
break;
71-
case "error":
72-
case "errorNum":
73-
case "errorMessage":
74-
ErrorEntity e = ctxt.readValue(p, ErrorEntity.class);
75-
multiDocument.getErrors().add(e);
76-
multiDocument.getDocumentsAndErrors().add(e);
77-
break;
78-
default:
79-
throw new JsonMappingException(p, "Unrecognized field '" + fieldName + "'");
50+
byte[] element = SerdeUtils.extractBytes(p);
51+
if (serde.isDocument(element)) {
52+
Object d = serde.deserializeUserData(element, containedType);
53+
multiDocument.getDocuments().add(d);
54+
multiDocument.getDocumentsAndErrors().add(d);
55+
} else {
56+
ErrorEntity e = serde.deserialize(element, ErrorEntity.class);
57+
multiDocument.getErrors().add(e);
58+
multiDocument.getDocumentsAndErrors().add(e);
8059
}
8160
p.nextToken(); // END_OBJECT
8261
}

core/src/main/java/com/arangodb/internal/serde/UserDataDeserializer.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOEx
3333
if (SerdeUtils.isManagedClass(clazz)) {
3434
return p.readValueAs(clazz);
3535
} else {
36-
return serde.deserializeUserData(SerdeUtils.extractBytes(p), targetType);
36+
return serde.deserializeUserData(SerdeUtils.extractBytes(p), clazz);
3737
}
3838
}
3939

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package resilience;
2+
3+
import ch.qos.logback.classic.Level;
4+
import com.arangodb.ArangoDB;
5+
import com.arangodb.Protocol;
6+
import com.arangodb.internal.net.Communication;
7+
import org.junit.jupiter.api.AfterEach;
8+
import org.junit.jupiter.api.BeforeEach;
9+
import org.mockserver.integration.ClientAndServer;
10+
11+
import java.util.Collections;
12+
13+
import static org.mockserver.integration.ClientAndServer.startClientAndServer;
14+
15+
public class MockTest extends SingleServerTest {
16+
17+
protected ClientAndServer mockServer;
18+
protected ArangoDB arangoDB;
19+
20+
public MockTest() {
21+
super(Collections.singletonMap(Communication.class, Level.DEBUG));
22+
}
23+
24+
@BeforeEach
25+
void before() {
26+
mockServer = startClientAndServer(getEndpoint().getHost(), getEndpoint().getPort());
27+
arangoDB = new ArangoDB.Builder()
28+
.protocol(Protocol.HTTP_JSON)
29+
.password(PASSWORD)
30+
.host("127.0.0.1", mockServer.getPort())
31+
.build();
32+
}
33+
34+
@AfterEach
35+
void after() {
36+
arangoDB.shutdown();
37+
mockServer.stop();
38+
}
39+
40+
}
Original file line numberDiff line numberDiff line change
@@ -1,99 +1,23 @@
1-
package resilience.http;
1+
package resilience.mock;
22

33
import ch.qos.logback.classic.Level;
4-
import com.arangodb.ArangoDB;
54
import com.arangodb.ArangoDBException;
6-
import com.arangodb.Protocol;
7-
import com.arangodb.internal.net.Communication;
5+
import com.arangodb.entity.MultiDocumentEntity;
86
import com.fasterxml.jackson.core.JsonParseException;
9-
import org.junit.jupiter.api.AfterEach;
10-
import org.junit.jupiter.api.BeforeEach;
7+
import com.fasterxml.jackson.databind.JsonNode;
118
import org.junit.jupiter.api.Test;
12-
import org.mockserver.integration.ClientAndServer;
13-
import org.mockserver.matchers.Times;
14-
import resilience.SingleServerTest;
9+
import resilience.MockTest;
1510

16-
import java.util.Collections;
17-
import java.util.concurrent.ExecutionException;
11+
import java.nio.charset.StandardCharsets;
12+
import java.util.Arrays;
13+
import java.util.List;
1814

1915
import static org.assertj.core.api.Assertions.assertThat;
2016
import static org.assertj.core.api.Assertions.catchThrowable;
21-
import static org.mockserver.integration.ClientAndServer.startClientAndServer;
2217
import static org.mockserver.model.HttpRequest.request;
2318
import static org.mockserver.model.HttpResponse.response;
2419

25-
class MockTest extends SingleServerTest {
26-
27-
private ClientAndServer mockServer;
28-
private ArangoDB arangoDB;
29-
30-
public MockTest() {
31-
super(Collections.singletonMap(Communication.class, Level.DEBUG));
32-
}
33-
34-
@BeforeEach
35-
void before() {
36-
mockServer = startClientAndServer(getEndpoint().getHost(), getEndpoint().getPort());
37-
arangoDB = new ArangoDB.Builder()
38-
.protocol(Protocol.HTTP_JSON)
39-
.password(PASSWORD)
40-
.host("127.0.0.1", mockServer.getPort())
41-
.build();
42-
}
43-
44-
@AfterEach
45-
void after() {
46-
arangoDB.shutdown();
47-
mockServer.stop();
48-
}
49-
50-
@Test
51-
void retryOn503() {
52-
arangoDB.getVersion();
53-
54-
mockServer
55-
.when(
56-
request()
57-
.withMethod("GET")
58-
.withPath("/.*/_api/version"),
59-
Times.exactly(2)
60-
)
61-
.respond(
62-
response()
63-
.withStatusCode(503)
64-
.withBody("{\"error\":true,\"errorNum\":503,\"errorMessage\":\"boom\",\"code\":503}")
65-
);
66-
67-
logs.reset();
68-
arangoDB.getVersion();
69-
assertThat(logs.getLogs())
70-
.filteredOn(e -> e.getLevel().equals(Level.WARN))
71-
.anyMatch(e -> e.getFormattedMessage().contains("Could not connect to host"));
72-
}
73-
74-
@Test
75-
void retryOn503Async() throws ExecutionException, InterruptedException {
76-
arangoDB.async().getVersion().get();
77-
78-
mockServer
79-
.when(
80-
request()
81-
.withMethod("GET")
82-
.withPath("/.*/_api/version"),
83-
Times.exactly(2)
84-
)
85-
.respond(
86-
response()
87-
.withStatusCode(503)
88-
.withBody("{\"error\":true,\"errorNum\":503,\"errorMessage\":\"boom\",\"code\":503}")
89-
);
90-
91-
logs.reset();
92-
arangoDB.async().getVersion().get();
93-
assertThat(logs.getLogs())
94-
.filteredOn(e -> e.getLevel().equals(Level.WARN))
95-
.anyMatch(e -> e.getFormattedMessage().contains("Could not connect to host"));
96-
}
20+
public class SerdeTest extends MockTest {
9721

9822
@Test
9923
void unparsableData() {
@@ -178,4 +102,35 @@ void textPlainDataWithCharset() {
178102
.hasMessageContaining("upstream timed out");
179103
}
180104

105+
@Test
106+
void getDocumentsWithErrorField() {
107+
List<String> keys = Arrays.asList("1", "2", "3");
108+
109+
String resp = "[" +
110+
"{\"error\":true,\"_key\":\"1\",\"_id\":\"col/1\",\"_rev\":\"_i4otI-q---\"}," +
111+
"{\"_key\":\"2\",\"_id\":\"col/2\",\"_rev\":\"_i4otI-q--_\"}," +
112+
"{\"_key\":\"3\",\"_id\":\"col/3\",\"_rev\":\"_i4otI-q--A\"}" +
113+
"]";
114+
115+
mockServer
116+
.when(
117+
request()
118+
.withMethod("PUT")
119+
.withPath("/.*/_api/document/col")
120+
.withQueryStringParameter("onlyget", "true")
121+
)
122+
.respond(
123+
response()
124+
.withStatusCode(200)
125+
.withHeader("Content-Type", "application/json; charset=utf-8")
126+
.withBody(resp.getBytes(StandardCharsets.UTF_8))
127+
);
128+
129+
MultiDocumentEntity<JsonNode> res = arangoDB.db().collection("col").getDocuments(keys, JsonNode.class);
130+
assertThat(res.getErrors()).isEmpty();
131+
assertThat(res.getDocuments()).hasSize(3)
132+
.anySatisfy(d -> assertThat(d.get("_key").textValue()).isEqualTo("1"))
133+
.anySatisfy(d -> assertThat(d.get("_key").textValue()).isEqualTo("2"))
134+
.anySatisfy(d -> assertThat(d.get("_key").textValue()).isEqualTo("3"));
135+
}
181136
}

0 commit comments

Comments
 (0)