// Copyright 2010-2011 Michel Kraemer
//
// Licensed 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 de.undercouch.bson4jackson;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;
import org.bson.BSONEncoder;
import org.bson.BSONObject;
import org.bson.BasicBSONEncoder;
import org.bson.BasicBSONObject;
import org.bson.types.BSONTimestamp;
import org.bson.types.Binary;
import org.bson.types.Code;
import org.bson.types.CodeWScope;
import org.bson.types.Symbol;
import org.junit.Test;
import com.fasterxml.jackson.core.JsonGenerationException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.MappingIterator;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
import de.undercouch.bson4jackson.types.JavaScript;
import de.undercouch.bson4jackson.types.ObjectId;
import de.undercouch.bson4jackson.types.Timestamp;
/**
* Tests {@link BsonParser}
* @author Michel Kraemer
*/
public class BsonParserTest {
/**
* Simple test class for {@link BsonParserTest#parseRootObjectArray()}
*/
public static class SimpleClass {
public String name;
}
/**
* Simple test class for {@link BsonParserTest#parseBinaryObject()}
*/
public static class BinaryClass {
public byte[] barr;
}
/**
* Simple test class for {@link BsonParserTest#parseObjectId()}
*/
public static class ObjectIdClass {
public org.bson.types.ObjectId oid;
}
private <T> T parseBsonObject(BSONObject o, Class<T> cls,
Module... modules) throws IOException {
BSONEncoder enc = new BasicBSONEncoder();
byte[] b = enc.encode(o);
ByteArrayInputStream bais = new ByteArrayInputStream(b);
BsonFactory fac = new BsonFactory();
ObjectMapper mapper = new ObjectMapper(fac);
if (modules != null) {
for (Module mod : modules) {
mapper.registerModule(mod);
}
}
fac.setCodec(mapper);
return mapper.readValue(bais, cls);
}
private Map<?, ?> parseBsonObject(BSONObject o) throws IOException {
return parseBsonObject(o, Map.class);
}
@Test
public void parsePrimitives() throws Exception {
BSONObject o = new BasicBSONObject();
o.put("Double", 5.0);
o.put("Float", 10.0f);
o.put("String", "Hello World");
o.put("Null", null);
o.put("Bool1", true);
o.put("Bool2", false);
o.put("Int32", 1234);
o.put("Int64", 1234L);
Map<?, ?> data = parseBsonObject(o);
assertEquals(5.0, data.get("Double"));
assertEquals(10.0, data.get("Float"));
assertEquals("Hello World", data.get("String"));
assertNull(data.get("Null"));
assertEquals(true, data.get("Bool1"));
assertEquals(false, data.get("Bool2"));
assertEquals(1234, data.get("Int32"));
assertEquals(1234L, data.get("Int64"));
}
/**
* Tests reading a very large string. Refers issue #18
* @throws Exception if something went wrong
* @author endasb
*/
@Test
public void parseBigString() throws Exception {
BSONObject o = new BasicBSONObject();
StringBuilder bigStr = new StringBuilder();
for (int i = 0; i < 80000; i++) {
bigStr.append("abc");
}
o.put("String", bigStr.toString());
Map<?, ?> data = parseBsonObject(o);
assertEquals(240000, data.get("String").toString().length());
}
/**
* Tests reading a very large string using multiple threads. Refers
* issue #19. Does not fail reproducibly, but with very high probability.
* You may have to run unit tests several times though to really rule out
* multi-threading issues.
* @throws Exception if something went wrong
* @author endasb
*/
@Test
public void parseBigStringInThreads() throws Exception {
final BSONObject o = new BasicBSONObject();
final AtomicInteger fails = new AtomicInteger(0);
StringBuilder bigStr = new StringBuilder();
for (int i = 0; i < 80000; i++) {
bigStr.append("abc");
}
o.put("String", bigStr.toString());
ArrayList<Thread> threads = new ArrayList<Thread>();
for (int i = 0; i < 50; i++) {
threads.add(new Thread(new Runnable() {
@Override
public void run() {
try {
Map<?, ?> data = parseBsonObject(o);
data = parseBsonObject(o);
assertNotNull(data);
} catch (Exception e) {
fail("Threading issue " + fails.incrementAndGet());
}
}
}));
}
for (Thread thread:threads) {
thread.start();
}
for (Thread thread:threads) {
thread.join();
}
assertEquals(0, fails.get());
}
@Test
public void parseBig() throws Exception {
BSONObject o = new BasicBSONObject();
o.put("Double", 5.0);
o.put("Int32", 1234);
BSONEncoder enc = new BasicBSONEncoder();
byte[] b = enc.encode(o);
ByteArrayInputStream bais = new ByteArrayInputStream(b);
ObjectMapper mapper = new ObjectMapper(new BsonFactory());
mapper.configure(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS, true);
mapper.configure(DeserializationFeature.USE_BIG_INTEGER_FOR_INTS, true);
Map<?, ?> data = mapper.readValue(bais, Map.class);
assertEquals(BigDecimal.class, data.get("Double").getClass());
assertEquals(BigInteger.class, data.get("Int32").getClass());
}
@Test
public void parseComplex() throws Exception {
BSONObject o = new BasicBSONObject();
o.put("Timestamp", new BSONTimestamp(0xAABB, 0xCCDD));
o.put("Symbol", new Symbol("Test"));
o.put("ObjectId", new org.bson.types.ObjectId(Integer.MAX_VALUE, -2, Integer.MIN_VALUE));
Pattern p = Pattern.compile(".*", Pattern.CASE_INSENSITIVE |
Pattern.DOTALL | Pattern.MULTILINE | Pattern.UNICODE_CASE);
o.put("Regex", p);
Map<?, ?> data = parseBsonObject(o);
assertEquals(new Timestamp(0xAABB, 0xCCDD), data.get("Timestamp"));
assertEquals(new de.undercouch.bson4jackson.types.Symbol("Test"), data.get("Symbol"));
ObjectId oid = (ObjectId)data.get("ObjectId");
assertEquals(Integer.MAX_VALUE, oid.getTime());
assertEquals(-2, oid.getMachine());
assertEquals(Integer.MIN_VALUE, oid.getInc());
Pattern p2 = (Pattern)data.get("Regex");
assertEquals(p.flags(), p2.flags());
assertEquals(p.pattern(), p2.pattern());
}
@Test
public void parseUndefined() throws Exception {
BSONObject o = new BasicBSONObject();
o.put("Undefined", new Object());
o.put("Int32", 5);
BSONEncoder enc = new BasicBSONEncoder() {
@Override
protected boolean putSpecial(String name, Object o) {
putUndefined(name);
return true;
}
};
byte[] b = enc.encode(o);
ByteArrayInputStream bais = new ByteArrayInputStream(b);
ObjectMapper mapper = new ObjectMapper(new BsonFactory());
Map<?, ?> data = mapper.readValue(bais, Map.class);
assertEquals(1, data.size());
assertEquals(5, data.get("Int32"));
}
@Test
public void parseEmbeddedDocument() throws Exception {
BSONObject o1 = new BasicBSONObject();
o1.put("Int32", 5);
BSONObject o2 = new BasicBSONObject();
o2.put("Int64", 10L);
o1.put("Obj", o2);
o1.put("String", "Hello");
Map<?, ?> data = parseBsonObject(o1);
assertEquals(3, data.size());
assertEquals(5, data.get("Int32"));
Map<?, ?> data2 = (Map<?, ?>)data.get("Obj");
assertEquals(1, data2.size());
assertEquals(10L, data2.get("Int64"));
assertEquals("Hello", data.get("String"));
}
@Test
public void parseEmbeddedArray() throws Exception {
List<Integer> i = new ArrayList<Integer>();
i.add(5);
i.add(6);
BSONObject o = new BasicBSONObject();
o.put("Int32", 5);
o.put("Arr", i);
o.put("String", "Hello");
Map<?, ?> data = parseBsonObject(o);
assertEquals(3, data.size());
assertEquals(5, data.get("Int32"));
}
/**
* Tests reading an embedded document through
* {@link BsonParser#readValueAsTree()}. Refers issue #9
* @throws Exception if something went wrong
* @author audistard
*/
@Test
public void parseEmbeddedDocumentAsTree() throws Exception {
BSONObject o2 = new BasicBSONObject();
o2.put("Int64", 10L);
BSONObject o3 = new BasicBSONObject();
o3.put("Int64", 11L);
BSONObject o1 = new BasicBSONObject();
o1.put("Obj2", o2);
o1.put("Obj3", o3);
BSONEncoder enc = new BasicBSONEncoder();
byte[] b = enc.encode(o1);
ByteArrayInputStream bais = new ByteArrayInputStream(b);
BsonFactory fac = new BsonFactory();
ObjectMapper mapper = new ObjectMapper(fac);
fac.setCodec(mapper);
BsonParser dec = fac.createParser(bais);
assertEquals(JsonToken.START_OBJECT, dec.nextToken());
assertEquals(JsonToken.FIELD_NAME, dec.nextToken());
assertEquals("Obj2", dec.getCurrentName());
assertEquals(JsonToken.START_OBJECT, dec.nextToken());
JsonNode obj2 = dec.readValueAsTree();
assertEquals(1, obj2.size());
assertNotNull(obj2.get("Int64"));
assertEquals(10L, obj2.get("Int64").longValue());
assertEquals(JsonToken.FIELD_NAME, dec.nextToken());
assertEquals("Obj3", dec.getCurrentName());
assertEquals(JsonToken.START_OBJECT, dec.nextToken());
assertEquals(JsonToken.FIELD_NAME, dec.nextToken());
assertEquals("Int64", dec.getCurrentName());
assertEquals(JsonToken.VALUE_NUMBER_INT, dec.nextToken());
assertEquals(11L, dec.getLongValue());
assertEquals(JsonToken.END_OBJECT, dec.nextToken());
assertEquals(JsonToken.END_OBJECT, dec.nextToken());
}
@Test
public void parseCode() throws Exception {
BSONObject scope = new BasicBSONObject();
scope.put("Int32", 5);
BSONObject o = new BasicBSONObject();
o.put("Code1", new CodeWScope("alert('test');", scope));
o.put("Code2", new Code("alert('Hello');"));
Map<?, ?> data = parseBsonObject(o);
assertEquals(2, data.size());
JavaScript c1 = (JavaScript)data.get("Code1");
JavaScript c2 = (JavaScript)data.get("Code2");
assertEquals("alert('test');", c1.getCode());
assertEquals("alert('Hello');", c2.getCode());
Map<String, Object> c1scope = c1.getScope();
assertEquals(5, c1scope.get("Int32"));
}
@Test
public void parseBinary() throws Exception {
byte[] b = new byte[] { 1, 2, 3, 4, 5 };
BSONObject o = new BasicBSONObject();
o.put("b1", b);
o.put("b2", new Binary(BsonConstants.SUBTYPE_BINARY, b));
o.put("uuid", new UUID(1L, 2L));
Map<?, ?> data = parseBsonObject(o);
assertEquals(3, data.size());
assertArrayEquals(b, (byte[])data.get("b1"));
assertArrayEquals(b, (byte[])data.get("b2"));
assertEquals(new UUID(1L, 2L), data.get("uuid"));
}
@Test
public void parseBinaryObject() throws Exception {
byte[] b = new byte[] { 1, 2, 3, 4, 5 };
BSONObject o = new BasicBSONObject();
o.put("barr", b);
BinaryClass data = parseBsonObject(o, BinaryClass.class);
assertArrayEquals(b, data.barr);
}
/**
* Test if {@link BsonParser#nextToken()} returns null if there
* is no more input. Refers issue #10.
* @throws Exception if something went wrong
* @author hertzsprung
* @author Michel Kraemer
*/
@Test
public void parseBeyondEnd() throws Exception {
ByteArrayOutputStream out = new ByteArrayOutputStream();
BsonFactory bsonFactory = new BsonFactory();
BsonGenerator generator = bsonFactory.createGenerator(out);
generator.writeStartObject();
generator.writeStringField("myField", "myValue");
generator.writeEndObject();
generator.close();
BsonParser parser = bsonFactory.createJsonParser(out.toByteArray());
//the following loop shall throw no exception and end after 4 iterations
int i = 0;
while (parser.nextToken() != null) {
++i;
assertTrue(i <= 4);
}
assertEquals(4, i);
}
/**
* Make sure we honor the length of the document if requested
* @throws Exception if something went wrong
*/
@Test
public void honorLength() throws Exception {
ByteArrayOutputStream out = new ByteArrayOutputStream();
BsonFactory bsonFactory = new BsonFactory();
bsonFactory.enable(BsonParser.Feature.HONOR_DOCUMENT_LENGTH);
BsonGenerator generator = bsonFactory.createGenerator(out);
generator.writeStartObject();
generator.writeStringField("myField", "myValue");
generator.writeEndObject();
generator.close();
out.write(new String("Hello world!\n").getBytes());
InputStream is = new ByteArrayInputStream(out.toByteArray());
ObjectMapper mapper = new ObjectMapper(bsonFactory);
bsonFactory.setCodec(mapper);
Map<?, ?> result = mapper.readValue(is, Map.class);
assertEquals("myValue", result.get("myField"));
// Now check that we can read the extra string we put at the end
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
assertEquals("Hello world!", reader.readLine());
}
/**
* Checks if the parser returns a textual representation of arbitrary
* tokens. See issue #23.
* @throws Exception if something went wrong
*/
@Test
public void parseAsText() throws Exception {
BSONObject o = new BasicBSONObject();
o.put("Float", 5.0f);
o.put("Int32", 1234);
BSONEncoder enc = new BasicBSONEncoder();
byte[] b = enc.encode(o);
ByteArrayInputStream bais = new ByteArrayInputStream(b);
BsonFactory fac = new BsonFactory();
BsonParser dec = fac.createParser(bais);
assertEquals(JsonToken.START_OBJECT, dec.nextToken());
assertEquals(JsonToken.FIELD_NAME, dec.nextToken());
assertEquals("Float", dec.getCurrentName());
assertEquals(JsonToken.VALUE_NUMBER_FLOAT, dec.nextToken());
assertEquals(5.0f, dec.getFloatValue(), 0.00001);
assertEquals("5.0", dec.getText());
assertEquals(JsonToken.FIELD_NAME, dec.nextToken());
assertEquals("Int32", dec.getCurrentName());
assertEquals(JsonToken.VALUE_NUMBER_INT, dec.nextToken());
assertEquals(1234, dec.getIntValue());
assertEquals("1234", dec.getText());
assertEquals(JsonToken.END_OBJECT, dec.nextToken());
}
/**
* Tests if a simple BSON file can be read successfully
* @throws Exception if something went wrong
*/
@Test
public void readBSONFile() throws Exception {
InputStream is = getClass().getResourceAsStream("test.bson");
try {
ObjectMapper mapper = new ObjectMapper(new BsonFactory());
MappingIterator<BSONObject> iterator =
mapper.reader(BasicBSONObject.class).readValues(is);
BSONObject o = null;
while (iterator.hasNext()) {
assertNull(o);
BSONObject object = iterator.next();
assertNotNull(object);
o = object;
}
assertEquals("Hello world", o.get("message"));
assertEquals(10.0, o.get("size"));
assertTrue(o.keySet().contains("_id"));
assertEquals(3, o.keySet().size());
} finally {
is.close();
}
}
/**
* Tests if a root-level array can be read correctly. Fixes issue #31
* @throws Exception if something went wrong
*/
@Test
public void parseRootArray() throws Exception {
ByteArrayOutputStream out = new ByteArrayOutputStream();
BsonFactory bsonFactory = new BsonFactory();
BsonGenerator generator = bsonFactory.createGenerator(out);
generator.writeStartArray();
generator.writeString("first");
generator.writeString("second");
generator.writeString("third");
generator.writeEndArray();
generator.close();
InputStream is = new ByteArrayInputStream(out.toByteArray());
ObjectMapper mapper = new ObjectMapper(bsonFactory);
bsonFactory.setCodec(mapper);
String[] result = mapper.readValue(is, String[].class);
assertEquals("first", result[0]);
assertEquals("second", result[1]);
assertEquals("third", result[2]);
}
/**
* Tests if an empty root array can be parsed correctly
* @throws Exception if something went wrong
*/
@Test
public void parseEmptyRootArray() throws Exception {
ByteArrayOutputStream out = new ByteArrayOutputStream();
BsonFactory bsonFactory = new BsonFactory();
BsonGenerator generator = bsonFactory.createGenerator(out);
generator.writeStartArray();
generator.writeEndArray();
generator.close();
InputStream is = new ByteArrayInputStream(out.toByteArray());
ObjectMapper mapper = new ObjectMapper(bsonFactory);
bsonFactory.setCodec(mapper);
String[] result = mapper.readValue(is, String[].class);
assertEquals(0, result.length);
}
/**
* Tests if a root object is not accidentally parsed as an array
* @throws Exception if something went wrong
*/
@Test(expected = JsonMappingException.class)
public void parseRootObjectAsArray() throws Exception {
ByteArrayOutputStream out = new ByteArrayOutputStream();
BsonFactory bsonFactory = new BsonFactory();
BsonGenerator generator = bsonFactory.createGenerator(out);
generator.writeStartObject();
generator.writeStringField("myField", "myValue");
generator.writeEndObject();
generator.close();
InputStream is = new ByteArrayInputStream(out.toByteArray());
ObjectMapper mapper = new ObjectMapper(bsonFactory);
bsonFactory.setCodec(mapper);
mapper.readValue(is, String[].class);
}
/**
* Creates a root array consisting of two simple objects and tries to
* deserialize them
* @throws Exception if something goes wrong
*/
@Test
public void parseRootObjectArray() throws Exception {
ByteArrayOutputStream out = new ByteArrayOutputStream();
BsonFactory bsonFactory = new BsonFactory();
BsonGenerator generator = bsonFactory.createGenerator(out);
generator.writeStartArray();
generator.writeStartObject();
generator.writeStringField("name", "test");
generator.writeEndObject();
generator.writeStartObject();
generator.writeStringField("name", "test2");
generator.writeEndObject();
generator.writeEndArray();
generator.close();
InputStream is = new ByteArrayInputStream(out.toByteArray());
ObjectMapper mapper = new ObjectMapper(bsonFactory);
bsonFactory.setCodec(mapper);
SimpleClass[] result = mapper.readValue(is, SimpleClass[].class);
assertEquals("test", result[0].name);
assertEquals("test2", result[1].name);
}
/**
* Check if org.bson.types.ObjectId can be serialized and deserialized as
* a byte array. See issue #38
* @throws Exception if something goes wrong
*/
@Test
public void parseObjectId() throws Exception {
class ObjectIdDeserializer extends StdDeserializer<org.bson.types.ObjectId> {
private static final long serialVersionUID = 6934309887169924897L;
protected ObjectIdDeserializer() {
super(org.bson.types.ObjectId.class);
}
@Override
public org.bson.types.ObjectId deserialize(JsonParser jp,
DeserializationContext ctxt) throws IOException,
JsonGenerationException {
return new org.bson.types.ObjectId(jp.getBinaryValue());
}
}
org.bson.types.ObjectId oid = new org.bson.types.ObjectId();
BSONObject o = new BasicBSONObject();
o.put("oid", oid.toByteArray());
SimpleModule mod = new SimpleModule();
mod.addDeserializer(org.bson.types.ObjectId.class, new ObjectIdDeserializer());
ObjectIdClass res = parseBsonObject(o, ObjectIdClass.class, mod);
assertEquals(oid, res.oid);
}
}