Skip to content

Commit 996a5eb

Browse files
jstewart148jyemin
authored andcommitted
Add getEmbedded methods for extracting values in embedded documents
JAVA-2532
1 parent dc9e69b commit 996a5eb

File tree

2 files changed

+168
-0
lines changed

2 files changed

+168
-0
lines changed

bson/src/main/org/bson/Document.java

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,14 @@
3232
import java.io.StringWriter;
3333
import java.util.Collection;
3434
import java.util.Date;
35+
import java.util.Iterator;
3536
import java.util.LinkedHashMap;
3637
import java.util.List;
3738
import java.util.Map;
3839
import java.util.Set;
3940

4041
import static java.lang.String.format;
42+
import static org.bson.assertions.Assertions.isTrue;
4143
import static org.bson.assertions.Assertions.notNull;
4244

4345
/**
@@ -160,6 +162,76 @@ public <T> T get(final Object key, final T defaultValue) {
160162
return value == null ? defaultValue : (T) value;
161163
}
162164

165+
/**
166+
* Gets the value in an embedded document, casting it to the given {@code Class<T>}. The list of keys represents a path to the
167+
* embedded value, drilling down into an embedded document for each key. This is useful to avoid having casts in
168+
* client code, though the effect is the same.
169+
*
170+
* The generic type of the keys list is {@code ?} to be consistent with the corresponding {@code get} methods, but in practice
171+
* the actual type of the argument should be {@code List<String>}. So to get the embedded value of a key list that is of type String,
172+
* you would write {@code String name = doc.getEmbedded(List.of("employee", "manager", "name"), String.class)} instead of
173+
* {@code String name = (String) doc.get("employee", Document.class).get("manager", Document.class).get("name") }.
174+
*
175+
* @param keys the list of keys
176+
* @param clazz the non-null class to cast the value to
177+
* @param <T> the type of the class
178+
* @return the value of the given embedded key, or null if the instance does not contain this embedded key.
179+
* @throws ClassCastException if the value of the given embedded key is not of type T
180+
* @since 3.10
181+
*/
182+
public <T> T getEmbedded(final List<?> keys, final Class<T> clazz) {
183+
notNull("keys", keys);
184+
isTrue("keys", !keys.isEmpty());
185+
notNull("clazz", clazz);
186+
return getEmbeddedValue(keys, clazz, null);
187+
}
188+
189+
/**
190+
* Gets the value in an embedded document, casting it to the given {@code Class<T>} or returning the default value if null.
191+
* The list of keys represents a path to the embedded value, drilling down into an embedded document for each key.
192+
* This is useful to avoid having casts in client code, though the effect is the same.
193+
*
194+
* The generic type of the keys list is {@code ?} to be consistent with the corresponding {@code get} methods, but in practice
195+
* the actual type of the argument should be {@code List<String>}. So to get the embedded value of a key list that is of type String,
196+
* you would write {@code String name = doc.getEmbedded(List.of("employee", "manager", "name"), "John Smith")} instead of
197+
* {@code String name = doc.get("employee", Document.class).get("manager", Document.class).get("name", "John Smith") }.
198+
*
199+
* @param keys the list of keys
200+
* @param defaultValue what to return if the value is null
201+
* @param <T> the type of the class
202+
* @return the value of the given key, or null if the instance does not contain this key.
203+
* @throws ClassCastException if the value of the given key is not of type T
204+
* @since 3.10
205+
*/
206+
public <T> T getEmbedded(final List<?> keys, final T defaultValue) {
207+
notNull("keys", keys);
208+
isTrue("keys", !keys.isEmpty());
209+
notNull("defaultValue", defaultValue);
210+
return getEmbeddedValue(keys, null, defaultValue);
211+
}
212+
213+
214+
// Gets the embedded value of the given list of keys, casting it to {@code Class<T>} or returning the default value if null.
215+
// Throws ClassCastException if any of the intermediate embedded values is not a Document.
216+
@SuppressWarnings("unchecked")
217+
private <T> T getEmbeddedValue(final List<?> keys, final Class<T> clazz, final T defaultValue) {
218+
Object value = this;
219+
Iterator<?> keyIterator = keys.iterator();
220+
while (keyIterator.hasNext()) {
221+
Object key = keyIterator.next();
222+
value = ((Document) value).get(key);
223+
if (!(value instanceof Document)) {
224+
if (value == null) {
225+
return defaultValue;
226+
} else if (keyIterator.hasNext()) {
227+
throw new ClassCastException(format("At key %s, the value is not a Document (%s)",
228+
key, value.getClass().getName()));
229+
}
230+
}
231+
}
232+
return clazz != null ? clazz.cast(value) : (T) value;
233+
}
234+
163235
/**
164236
* Gets the value of the given key as an Integer.
165237
*

bson/src/test/unit/org/bson/types/DocumentSpecification.groovy

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,102 @@ class DocumentSpecification extends Specification {
126126
thrown(ClassCastException)
127127
}
128128

129+
def 'should return null when getting embedded value'() {
130+
when:
131+
Document document = Document.parse("{a: 1, b: {x: [2, 3, 4], y: {m: 'one', len: 3}}, 'a.b': 'two'}")
132+
133+
then:
134+
document.getEmbedded(List.of('notAKey'), String) == null
135+
document.getEmbedded(List.of('b', 'y', 'notAKey'), String) == null
136+
document.getEmbedded(List.of('b', 'b', 'm'), String) == null
137+
Document.parse('{}').getEmbedded(List.of('a', 'b'), Integer) == null
138+
Document.parse('{b: 1}').getEmbedded(['a'], Integer) == null
139+
Document.parse('{b: 1}').getEmbedded(['a', 'b'], Integer) == null
140+
Document.parse('{a: {c: 1}}').getEmbedded(['a', 'b'], Integer) == null
141+
Document.parse('{a: {c: 1}}').getEmbedded(['a', 'b', 'c'], Integer) == null
142+
}
143+
144+
def 'should return embedded value'() {
145+
given:
146+
Date date = new Date();
147+
ObjectId objectId = new ObjectId();
148+
149+
when:
150+
Document document = Document.parse("{a: 1, b: {x: [2, 3, 4], y: {m: 'one', len: 3}}, 'a.b': 'two'}")
151+
.append('l', new Document('long', 2L))
152+
.append('d', new Document('double', 3.0 as double))
153+
.append('t', new Document('boolean', true))
154+
.append('o', new Document('objectId', objectId))
155+
.append('n', new Document('date', date))
156+
157+
then:
158+
document.getEmbedded(List.of('a'), Integer) == 1
159+
document.getEmbedded(List.of('b', 'x'), List).get(0) == 2
160+
document.getEmbedded(List.of('b', 'x'), List).get(1) == 3
161+
document.getEmbedded(List.of('b', 'x'), List).get(2) == 4
162+
document.getEmbedded(List.of('b', 'y', 'm'), String) == 'one'
163+
document.getEmbedded(List.of('b', 'y', 'len'), Integer) == 3
164+
document.getEmbedded(List.of('a.b'), String) == 'two'
165+
document.getEmbedded(List.of('b', 'y'), Document).getString('m') == 'one'
166+
document.getEmbedded(List.of('b', 'y'), Document).getInteger('len') == 3
167+
168+
document.getEmbedded(Arrays.asList('l', 'long'), Long) == 2L
169+
document.getEmbedded(Arrays.asList('d', 'double'), Double) == 3.0d
170+
document.getEmbedded(Arrays.asList('l', 'long'), Number) == 2L
171+
document.getEmbedded(Arrays.asList('d', 'double'), Number) == 3.0d
172+
document.getEmbedded(Arrays.asList('t', 'boolean'), Boolean) == true
173+
document.getEmbedded(Arrays.asList('t', 'x'), false) == false
174+
document.getEmbedded(Arrays.asList('o', 'objectId'), ObjectId) == objectId
175+
document.getEmbedded(Arrays.asList('n', 'date'), Date) == date
176+
}
177+
178+
def 'should throw an exception getting an embedded value'() {
179+
given:
180+
Document document = Document.parse("{a: 1, b: {x: [2, 3, 4], y: {m: 'one', len: 3}}, 'a.b': 'two'}")
181+
182+
when:
183+
document.getEmbedded(null, String) == null
184+
185+
then:
186+
thrown(IllegalArgumentException)
187+
188+
when:
189+
document.getEmbedded(List.of(), String) == null
190+
191+
then:
192+
thrown(IllegalStateException)
193+
194+
when:
195+
document.getEmbedded(['a', 'b'], Integer)
196+
197+
then:
198+
thrown(ClassCastException)
199+
200+
when:
201+
document.getEmbedded(List.of('b', 'y', 'm'), Integer)
202+
203+
then:
204+
thrown(ClassCastException)
205+
206+
when:
207+
document.getEmbedded(List.of('b', 'x'), Document)
208+
209+
then:
210+
thrown(ClassCastException)
211+
212+
when:
213+
document.getEmbedded(List.of('b', 'x', 'm'), String)
214+
215+
then:
216+
thrown(ClassCastException)
217+
218+
when:
219+
document.getEmbedded(Arrays.asList('b', 'x', 'm'), 'invalid')
220+
221+
then:
222+
thrown(ClassCastException)
223+
}
224+
129225
def 'should parse a valid JSON string to a Document'() {
130226
when:
131227
Document document = Document.parse("{ 'int' : 1, 'string' : 'abc' }");

0 commit comments

Comments
 (0)