Skip to content

Commit 63ef61d

Browse files
author
Alexandre Dutra
committed
JAVA-742: Codec support for JSON.
This commit introduces two codecs: - a new JacksonJsonCodec that automatically maps JSON structures to CQL varchar types leveraging the popular Jackson framework. - a new Jsr353JsonCodec that automatically maps JSON structures to CQL varchar types using JSR 353.
1 parent f34c86e commit 63ef61d

8 files changed

Lines changed: 496 additions & 84 deletions

File tree

changelog/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
- [improvement] JAVA-917: Document SSL configuration.
1616
- [improvement] JAVA-936: Adapt schema metadata parsing logic to new storage format of CQL types in C* 3.0.
1717
- [new feature] JAVA-846: Provide custom codecs library as an extra module.
18+
- [new feature] JAVA-742: Codec Support for JSON.
1819

1920
Merged from 2.1 branch:
2021

driver-core/src/test/java/com/datastax/driver/core/TypeCodecTest.java

Lines changed: 1 addition & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,8 @@
1818
import java.nio.ByteBuffer;
1919
import java.util.*;
2020

21-
import com.fasterxml.jackson.annotation.JsonCreator;
22-
import com.fasterxml.jackson.annotation.JsonProperty;
2321
import com.google.common.base.Function;
2422
import com.google.common.base.Joiner;
25-
import com.google.common.base.Objects;
2623
import com.google.common.base.Strings;
2724
import com.google.common.collect.Lists;
2825
import com.google.common.reflect.TypeToken;
@@ -90,18 +87,6 @@ public void collectionElementTooLargeTest() throws Exception {
9087
codec.serialize(list, ProtocolVersion.V2);
9188
}
9289

93-
@Test(groups = "unit")
94-
public void test_cql_text_to_json() {
95-
JsonCodec<User> codec = new JsonCodec<User>(User.class);
96-
// the codec is expected to format json objects as json strings enclosed in single quotes,
97-
// as it is required for CQL literals of varchar type.
98-
String json = "'{\"id\":1,\"name\":\"John Doe\"}'";
99-
User user = new User(1, "John Doe");
100-
assertThat(codec.format(user)).isEqualTo(json);
101-
assertThat(codec.parse(json)).isEqualToComparingFieldByField(user);
102-
assertThat(codec).canSerialize(user);
103-
}
104-
10590
@Test(groups = "unit")
10691
public void test_cql_list_varchar_to_list_list_integer() {
10792
ListVarcharToListListInteger codec = new ListVarcharToListListInteger();
@@ -309,53 +294,7 @@ public String format(List<List<Integer>> value) {
309294
throw new UnsupportedOperationException();
310295
}
311296
}
312-
313-
@SuppressWarnings("unused")
314-
public static class User {
315-
316-
private int id;
317-
318-
private String name;
319-
320-
@JsonCreator
321-
public User(@JsonProperty("id") int id, @JsonProperty("name") String name) {
322-
this.id = id;
323-
this.name = name;
324-
}
325-
326-
public int getId() {
327-
return id;
328-
}
329-
330-
public void setId(int id) {
331-
this.id = id;
332-
}
333-
334-
public String getName() {
335-
return name;
336-
}
337-
338-
public void setName(String name) {
339-
this.name = name;
340-
}
341-
342-
@Override
343-
public boolean equals(Object o) {
344-
if (this == o)
345-
return true;
346-
if (o == null || getClass() != o.getClass())
347-
return false;
348-
User user = (User)o;
349-
return Objects.equal(id, user.id) &&
350-
Objects.equal(name, user.name);
351-
}
352-
353-
@Override
354-
public int hashCode() {
355-
return Objects.hashCode(id, name);
356-
}
357-
}
358-
297+
359298
class A {
360299

361300
int i = 0;

driver-extras/pom.xml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,13 @@
6565
<optional>true</optional>
6666
</dependency>
6767

68+
<dependency>
69+
<groupId>javax.json</groupId>
70+
<artifactId>javax.json-api</artifactId>
71+
<version>1.0</version>
72+
<optional>true</optional>
73+
</dependency>
74+
6875
<dependency>
6976
<groupId>com.datastax.cassandra</groupId>
7077
<artifactId>cassandra-driver-core</artifactId>
@@ -108,6 +115,13 @@
108115
<scope>test</scope>
109116
</dependency>
110117

118+
<dependency>
119+
<groupId>org.glassfish</groupId>
120+
<artifactId>javax.json</artifactId>
121+
<version>1.0.4</version>
122+
<scope>test</scope>
123+
</dependency>
124+
111125
</dependencies>
112126

113127
<build>

driver-core/src/test/java/com/datastax/driver/core/JsonCodec.java renamed to driver-extras/src/main/java/com/datastax/driver/extras/codecs/json/JacksonJsonCodec.java

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
package com.datastax.driver.core;
16+
package com.datastax.driver.extras.codecs.json;
1717

1818
import java.io.IOException;
1919
import java.nio.ByteBuffer;
@@ -23,17 +23,26 @@
2323
import com.fasterxml.jackson.databind.ObjectMapper;
2424
import com.fasterxml.jackson.databind.type.TypeFactory;
2525

26+
import com.datastax.driver.core.DataType;
27+
import com.datastax.driver.core.ParseUtils;
28+
import com.datastax.driver.core.ProtocolVersion;
29+
import com.datastax.driver.core.TypeCodec;
2630
import com.datastax.driver.core.exceptions.InvalidTypeException;
2731
import com.datastax.driver.core.utils.Bytes;
2832

2933
/**
30-
* A simple Json codec for TypeCodec tests that uses Jackson to perform serialization and deserialization.
34+
* A JSON codec that uses the <a href="http://wiki.fasterxml.com/JacksonHome">Jackson</a>
35+
* library to perform serialization and deserialization of JSON objects.
36+
* <p>
37+
* Note that this codec maps a single object to a single JSON structure at a time;
38+
* mapping of arrays or collections to root-level JSON arrays is not supported,
39+
* but such a codec can be easily crafted after this one.
3140
*/
32-
public class JsonCodec<T> extends TypeCodec<T> {
41+
public class JacksonJsonCodec<T> extends TypeCodec<T> {
3342

3443
private final ObjectMapper objectMapper = new ObjectMapper();
3544

36-
public JsonCodec(Class<T> javaType) {
45+
public JacksonJsonCodec(Class<T> javaType) {
3746
super(DataType.varchar(), javaType);
3847
}
3948

@@ -70,24 +79,29 @@ public String format(T value) throws InvalidTypeException {
7079
} catch (IOException e) {
7180
throw new InvalidTypeException(e.getMessage(), e);
7281
}
73-
return '\'' + json.replace("\'", "''") + '\'';
82+
return ParseUtils.quote(json);
7483
}
7584

7685
@Override
7786
@SuppressWarnings("unchecked")
7887
public T parse(String value) throws InvalidTypeException {
7988
if (value == null || value.isEmpty() || value.equalsIgnoreCase("NULL"))
8089
return null;
81-
if (value.charAt(0) != '\'' || value.charAt(value.length() - 1) != '\'')
90+
if (!ParseUtils.isQuoted(value))
8291
throw new InvalidTypeException("JSON strings must be enclosed by single quotes");
83-
String json = value.substring(1, value.length() - 1).replace("''", "'");
92+
String json = ParseUtils.unquote(value);
8493
try {
8594
return (T)objectMapper.readValue(json, toJacksonJavaType());
8695
} catch (IOException e) {
8796
throw new InvalidTypeException(e.getMessage(), e);
8897
}
8998
}
9099

100+
/**
101+
* This method acts as a bridge between Guava's {@link com.google.common.reflect.TypeToken TypeToken} API, which is used
102+
* by the driver, and Jackson's {@link JavaType} API.
103+
* @return A {@link JavaType} instance corresponding to the codec's {@link #getJavaType() Java type}.
104+
*/
91105
protected JavaType toJacksonJavaType() {
92106
return TypeFactory.defaultInstance().constructType(getJavaType().getType());
93107
}
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/*
2+
* Copyright (C) 2012-2015 DataStax Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.datastax.driver.extras.codecs.json;
17+
18+
import java.io.*;
19+
import java.nio.ByteBuffer;
20+
import java.util.Map;
21+
22+
import javax.json.*;
23+
24+
import com.datastax.driver.core.DataType;
25+
import com.datastax.driver.core.ParseUtils;
26+
import com.datastax.driver.core.ProtocolVersion;
27+
import com.datastax.driver.core.TypeCodec;
28+
import com.datastax.driver.core.exceptions.InvalidTypeException;
29+
import com.datastax.driver.core.utils.Bytes;
30+
31+
/**
32+
* A JSON codec that uses the
33+
* <a href="https://jcp.org/en/jsr/detail?id=353">Java API for JSON processing</a>
34+
* to perform serialization and deserialization of JSON structures.
35+
* <p>
36+
* More specifically, this codec maps an arbitrary {@link JsonStructure} to
37+
* a CQL {@code varchar} column.
38+
* <p>
39+
* Note that this codec handles the Java type {@link JsonStructure}.
40+
* It is therefore required that values are set and retrieved using that exact Java type;
41+
* users should manually downcast to either {@link JsonObject} or {@link JsonArray},
42+
* as in the example below:
43+
* <pre>{@code
44+
*
45+
* // setting values
46+
* JsonObject myObject = ...
47+
* PreparedStatement ps = ...
48+
* // set values using JsonStructure
49+
* BoundStatement bs = ps.bind().set(1, myObject, JsonStructure.class);
50+
*
51+
* // retrieving values
52+
* Row row = session.execute(bs).one();
53+
* // use JsonStructure to retrieve values
54+
* JsonStructure json = row.get(0, JsonStructure.class);
55+
* if (json instanceof JsonObject) {
56+
* myObject = (JsonObject) json;
57+
* ...
58+
* }
59+
* }</pre>
60+
*
61+
*/
62+
public class Jsr353JsonCodec extends TypeCodec<JsonStructure> {
63+
64+
private final JsonReaderFactory readerFactory;
65+
66+
private final JsonWriterFactory writerFactory;
67+
68+
public Jsr353JsonCodec() {
69+
this(null);
70+
}
71+
72+
public Jsr353JsonCodec(Map<String, ?> config) {
73+
super(DataType.varchar(), JsonStructure.class);
74+
readerFactory = Json.createReaderFactory(config);
75+
writerFactory = Json.createWriterFactory(config);
76+
}
77+
78+
@Override
79+
public ByteBuffer serialize(JsonStructure value, ProtocolVersion protocolVersion) throws InvalidTypeException {
80+
if (value == null)
81+
return null;
82+
ByteArrayOutputStream baos = new ByteArrayOutputStream();
83+
try {
84+
JsonWriter writer = writerFactory.createWriter(baos);
85+
writer.write(value);
86+
return ByteBuffer.wrap(baos.toByteArray());
87+
} catch (JsonException e) {
88+
throw new InvalidTypeException(e.getMessage(), e);
89+
} finally {
90+
try {
91+
baos.close();
92+
} catch (IOException e) {
93+
// cannot happen
94+
throw new InvalidTypeException(e.getMessage(), e);
95+
}
96+
}
97+
}
98+
99+
@Override
100+
@SuppressWarnings("unchecked")
101+
public JsonStructure deserialize(ByteBuffer bytes, ProtocolVersion protocolVersion) throws InvalidTypeException {
102+
if (bytes == null)
103+
return null;
104+
ByteArrayInputStream bais = new ByteArrayInputStream(Bytes.getArray(bytes));
105+
try {
106+
JsonReader reader = readerFactory.createReader(bais);
107+
return reader.read();
108+
} catch (JsonException e) {
109+
throw new InvalidTypeException(e.getMessage(), e);
110+
} finally {
111+
try {
112+
bais.close();
113+
} catch (IOException e) {
114+
// cannot happen
115+
throw new InvalidTypeException(e.getMessage(), e);
116+
}
117+
}
118+
}
119+
120+
@Override
121+
public String format(JsonStructure value) throws InvalidTypeException {
122+
if (value == null)
123+
return "NULL";
124+
String json;
125+
StringWriter sw = new StringWriter();
126+
try {
127+
JsonWriter writer = writerFactory.createWriter(sw);
128+
writer.write(value);
129+
json = sw.toString();
130+
} catch (JsonException e) {
131+
throw new InvalidTypeException(e.getMessage(), e);
132+
} finally {
133+
try {
134+
sw.close();
135+
} catch (IOException e) {
136+
// cannot happen
137+
throw new InvalidTypeException(e.getMessage(), e);
138+
}
139+
}
140+
return ParseUtils.quote(json);
141+
}
142+
143+
@Override
144+
@SuppressWarnings("unchecked")
145+
public JsonStructure parse(String value) throws InvalidTypeException {
146+
if (value == null || value.isEmpty() || value.equalsIgnoreCase("NULL"))
147+
return null;
148+
if (!ParseUtils.isQuoted(value))
149+
throw new InvalidTypeException("JSON strings must be enclosed by single quotes");
150+
String json = ParseUtils.unquote(value);
151+
StringReader sr = new StringReader(json);
152+
try {
153+
JsonReader reader = readerFactory.createReader(sr);
154+
return reader.read();
155+
} catch (JsonException e) {
156+
throw new InvalidTypeException(e.getMessage(), e);
157+
} finally {
158+
sr.close();
159+
}
160+
}
161+
162+
}

0 commit comments

Comments
 (0)