Repository: metron Updated Branches: refs/heads/feature/METRON-1554-pcap-query-panel 6c90724d8 -> 7dff4def2
METRON-1606 Add a 'wrap' to incoming messages in the metron json parser (ottobackwards) closes apache/metron#1054 Project: http://git-wip-us.apache.org/repos/asf/metron/repo Commit: http://git-wip-us.apache.org/repos/asf/metron/commit/6bac842d Tree: http://git-wip-us.apache.org/repos/asf/metron/tree/6bac842d Diff: http://git-wip-us.apache.org/repos/asf/metron/diff/6bac842d Branch: refs/heads/feature/METRON-1554-pcap-query-panel Commit: 6bac842d7ad2819c56412837d8befb603ef15f35 Parents: aceca46 Author: ottobackwards <ottobackwa...@gmail.com> Authored: Fri Jul 20 17:44:35 2018 -0400 Committer: otto <o...@apache.org> Committed: Fri Jul 20 17:44:35 2018 -0400 ---------------------------------------------------------------------- .../docker/rpm-docker/SPECS/metron.spec | 1 + .../parsed/jsonMapExampleParsed | 6 + .../raw/jsonMapExampleOutput | 3 + metron-platform/metron-parsers/README.md | 5 + .../zookeeper/parsers/jsonMapWrappedQuery.json | 5 + .../metron/parsers/json/JSONMapParser.java | 40 +++- .../JSONMapWrappedQueryIntegrationTest.java | 37 ++++ .../integration/ParserIntegrationTest.java | 22 +- .../json/JSONMapParserWrappedQueryTest.java | 199 +++++++++++++++++++ 9 files changed, 299 insertions(+), 19 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/metron/blob/6bac842d/metron-deployment/packaging/docker/rpm-docker/SPECS/metron.spec ---------------------------------------------------------------------- diff --git a/metron-deployment/packaging/docker/rpm-docker/SPECS/metron.spec b/metron-deployment/packaging/docker/rpm-docker/SPECS/metron.spec index 4b88fd0..b308908 100644 --- a/metron-deployment/packaging/docker/rpm-docker/SPECS/metron.spec +++ b/metron-deployment/packaging/docker/rpm-docker/SPECS/metron.spec @@ -151,6 +151,7 @@ This package installs the Metron Parser files %{metron_home}/config/zookeeper/parsers/bro.json %{metron_home}/config/zookeeper/parsers/jsonMap.json %{metron_home}/config/zookeeper/parsers/jsonMapQuery.json +%{metron_home}/config/zookeeper/parsers/jsonMapWrappedQuery.json %{metron_home}/config/zookeeper/parsers/snort.json %{metron_home}/config/zookeeper/parsers/squid.json %{metron_home}/config/zookeeper/parsers/websphere.json http://git-wip-us.apache.org/repos/asf/metron/blob/6bac842d/metron-platform/metron-integration-test/src/main/sample/data/jsonMapWrappedQuery/parsed/jsonMapExampleParsed ---------------------------------------------------------------------- diff --git a/metron-platform/metron-integration-test/src/main/sample/data/jsonMapWrappedQuery/parsed/jsonMapExampleParsed b/metron-platform/metron-integration-test/src/main/sample/data/jsonMapWrappedQuery/parsed/jsonMapExampleParsed new file mode 100644 index 0000000..c6aac78 --- /dev/null +++ b/metron-platform/metron-integration-test/src/main/sample/data/jsonMapWrappedQuery/parsed/jsonMapExampleParsed @@ -0,0 +1,6 @@ +{ "string" : "foo", "number" : 1, "ignored" : [ "blah" ], "original_string":"{ \"string\" : \"foo\", \"number\" : 1, \"ignored\" : [ \"blah\" ] }","timestamp":1000000000000, "source.type":"jsonMapWrappedQuery","guid":"this-is-random-uuid-will-be-36-chars" } +{ "number" : 4 , "original_string" : "{ \"number\" : 4 }", "source.type":"jsonMapWrappedQuery","timestamp":1000000000000,"guid":"this-is-random-uuid-will-be-36-chars"} +{ "string" : "bar", "number" : 2, "ignored" : [ "blah" ], "original_string":"{ \"string\" : \"bar\", \"number\" : 2, \"ignored\" : [ \"blah\" ] }","timestamp":1000000000000, "source.type":"jsonMapWrappedQuery","guid":"this-is-random-uuid-will-be-36-chars" } +{ "number" : 5 , "original_string" : "{ \"number\" : 5 }", "source.type":"jsonMapWrappedQuery","timestamp":1000000000000,"guid":"this-is-random-uuid-will-be-36-chars"} +{ "string" : "baz", "number" : 3, "ignored" : [ "blah" ], "original_string":"{ \"string\" : \"baz\", \"number\" : 3, \"ignored\" : [ \"blah\" ] }","timestamp":1000000000000, "source.type":"jsonMapWrappedQuery","guid":"this-is-random-uuid-will-be-36-chars" } +{ "number" : 6 , "original_string" : "{ \"number\" : 6 }", "source.type":"jsonMapWrappedQuery","timestamp":1000000000000,"guid":"this-is-random-uuid-will-be-36-chars"} http://git-wip-us.apache.org/repos/asf/metron/blob/6bac842d/metron-platform/metron-integration-test/src/main/sample/data/jsonMapWrappedQuery/raw/jsonMapExampleOutput ---------------------------------------------------------------------- diff --git a/metron-platform/metron-integration-test/src/main/sample/data/jsonMapWrappedQuery/raw/jsonMapExampleOutput b/metron-platform/metron-integration-test/src/main/sample/data/jsonMapWrappedQuery/raw/jsonMapExampleOutput new file mode 100644 index 0000000..f142a55 --- /dev/null +++ b/metron-platform/metron-integration-test/src/main/sample/data/jsonMapWrappedQuery/raw/jsonMapExampleOutput @@ -0,0 +1,3 @@ +{ "string" : "foo", "number" : 1, "ignored" : [ "blah" ] },{ "number" : 4 }, +{ "string" : "bar", "number" : 2, "ignored" : [ "blah" ] },{ "number" : 5 }, +{ "string" : "baz", "number" : 3, "ignored" : [ "blah" ] },{ "number" : 6 } http://git-wip-us.apache.org/repos/asf/metron/blob/6bac842d/metron-platform/metron-parsers/README.md ---------------------------------------------------------------------- diff --git a/metron-platform/metron-parsers/README.md b/metron-platform/metron-parsers/README.md index 0e428e3..fd6b470 100644 --- a/metron-platform/metron-parsers/README.md +++ b/metron-platform/metron-parsers/README.md @@ -44,6 +44,11 @@ There are two general types types of parsers: * `ALLOW` : Allow multidimensional maps * `ERROR` : Throw an error when a multidimensional map is encountered * `jsonpQuery` : A [JSON Path](#json_path) query string. If present, the result of the JSON Path query should be a list of messages. This is useful if you have a JSON document which contains a list or array of messages embedded in it, and you do not have another means of splitting the message. + * `wrapInEntityArray` : `"true" or "false"`. If `jsonQuery` is present and this flag is present and set to `"true"`, the incoming message will be wrapped in a JSON entity and array. + for example: + `{"name":"value"},{"name2","value2}` will be wrapped as `{"message" : [{"name":"value"},{"name2","value2}]}`. + This is using the default value for `wrapEntityName` if that property is not set. + * `wrapEntityName` : Sets the name to use when wrapping JSON using `wrapInEntityArray`. The `jsonpQuery` should reference this name. * A field called `timestamp` is expected to exist and, if it does not, then current time is inserted. ## Parser Error Routing http://git-wip-us.apache.org/repos/asf/metron/blob/6bac842d/metron-platform/metron-parsers/src/main/config/zookeeper/parsers/jsonMapWrappedQuery.json ---------------------------------------------------------------------- diff --git a/metron-platform/metron-parsers/src/main/config/zookeeper/parsers/jsonMapWrappedQuery.json b/metron-platform/metron-parsers/src/main/config/zookeeper/parsers/jsonMapWrappedQuery.json new file mode 100644 index 0000000..3e960e0 --- /dev/null +++ b/metron-platform/metron-parsers/src/main/config/zookeeper/parsers/jsonMapWrappedQuery.json @@ -0,0 +1,5 @@ +{ + "parserClassName":"org.apache.metron.parsers.json.JSONMapParser", + "sensorTopic":"jsonMapWrappedQuery", + "parserConfig": {"jsonpQuery":"$.foo","wrapInEntityArray":"true","wrapEntityName":"foo"} +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/metron/blob/6bac842d/metron-platform/metron-parsers/src/main/java/org/apache/metron/parsers/json/JSONMapParser.java ---------------------------------------------------------------------- diff --git a/metron-platform/metron-parsers/src/main/java/org/apache/metron/parsers/json/JSONMapParser.java b/metron-platform/metron-parsers/src/main/java/org/apache/metron/parsers/json/JSONMapParser.java index f5d67f9..0acc96c 100644 --- a/metron-platform/metron-parsers/src/main/java/org/apache/metron/parsers/json/JSONMapParser.java +++ b/metron-platform/metron-parsers/src/main/java/org/apache/metron/parsers/json/JSONMapParser.java @@ -35,6 +35,7 @@ import java.util.EnumSet; import java.util.List; import java.util.Map; import java.util.Set; + import org.apache.commons.lang3.StringUtils; import org.apache.metron.common.utils.JSONUtils; import org.apache.metron.parsers.BasicParser; @@ -87,10 +88,18 @@ public class JSONMapParser extends BasicParser { public static final String MAP_STRATEGY_CONFIG = "mapStrategy"; public static final String JSONP_QUERY = "jsonpQuery"; + public static final String WRAP_JSON = "wrapInEntityArray"; + public static final String WRAP_ENTITY_NAME = "wrapEntityName"; + public static final String DEFAULT_WRAP_ENTITY_NAME = "messages"; + + private static final String WRAP_START_FMT = "{ \"%s\" : ["; + private static final String WRAP_END = "]}"; private MapStrategy mapStrategy = MapStrategy.DROP; private transient TypeRef<List<Map<String, Object>>> typeRef = null; private String jsonpQuery = null; + private String wrapEntityName = DEFAULT_WRAP_ENTITY_NAME; + private boolean wrapJson = false; @Override @@ -100,6 +109,20 @@ public class JSONMapParser extends BasicParser { if (config.containsKey(JSONP_QUERY)) { typeRef = new TypeRef<List<Map<String, Object>>>() { }; jsonpQuery = (String) config.get(JSONP_QUERY); + + if (!StringUtils.isBlank(jsonpQuery) && config.containsKey(WRAP_JSON)) { + Object wrapObject = config.get(WRAP_JSON); + if (wrapObject instanceof String) { + wrapJson = Boolean.valueOf((String)wrapObject); + } else if (wrapObject instanceof Boolean) { + wrapJson = (Boolean) config.get(WRAP_JSON); + } + String entityName = (String)config.get(WRAP_ENTITY_NAME); + if (!StringUtils.isBlank(entityName)) { + wrapEntityName = entityName; + } + } + Configuration.setDefaults(new Configuration.Defaults() { private final JsonProvider jsonProvider = new JacksonJsonProvider(); @@ -147,9 +170,14 @@ public class JSONMapParser extends BasicParser { String originalString = new String(rawMessage); List<Map<String, Object>> messages = new ArrayList<>(); + // if configured, wrap the json in an entity and array + if (wrapJson) { + originalString = wrapMessageJson(originalString); + } + if (!StringUtils.isEmpty(jsonpQuery)) { - Object parsedObject = JsonPath.parse(new String(rawMessage)).read(jsonpQuery, typeRef); - if(parsedObject != null) { + Object parsedObject = JsonPath.parse(originalString).read(jsonpQuery, typeRef); + if (parsedObject != null) { messages.addAll((List<Map<String,Object>>)parsedObject); } } else { @@ -192,4 +220,12 @@ public class JSONMapParser extends BasicParser { return ret; } + private String wrapMessageJson(String jsonMessage) { + String base = new StringBuilder(String.format(WRAP_START_FMT,wrapEntityName)) + .append(jsonMessage).toString().trim(); + if (base.endsWith(",")) { + base = base.substring(0, base.length() - 1); + } + return base + WRAP_END; + } } http://git-wip-us.apache.org/repos/asf/metron/blob/6bac842d/metron-platform/metron-parsers/src/test/java/org/apache/metron/parsers/integration/JSONMapWrappedQueryIntegrationTest.java ---------------------------------------------------------------------- diff --git a/metron-platform/metron-parsers/src/test/java/org/apache/metron/parsers/integration/JSONMapWrappedQueryIntegrationTest.java b/metron-platform/metron-parsers/src/test/java/org/apache/metron/parsers/integration/JSONMapWrappedQueryIntegrationTest.java new file mode 100644 index 0000000..569e175 --- /dev/null +++ b/metron-platform/metron-parsers/src/test/java/org/apache/metron/parsers/integration/JSONMapWrappedQueryIntegrationTest.java @@ -0,0 +1,37 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.metron.parsers.integration; + +import org.apache.metron.parsers.integration.validation.SampleDataValidation; + +import java.util.ArrayList; +import java.util.List; + +public class JSONMapWrappedQueryIntegrationTest extends ParserIntegrationTest { + @Override + String getSensorType() { + return "jsonMapWrappedQuery"; + } + + @Override + List<ParserValidation> getValidations() { + return new ArrayList<ParserValidation>() {{ + add(new SampleDataValidation()); + }}; + } +} http://git-wip-us.apache.org/repos/asf/metron/blob/6bac842d/metron-platform/metron-parsers/src/test/java/org/apache/metron/parsers/integration/ParserIntegrationTest.java ---------------------------------------------------------------------- diff --git a/metron-platform/metron-parsers/src/test/java/org/apache/metron/parsers/integration/ParserIntegrationTest.java b/metron-platform/metron-parsers/src/test/java/org/apache/metron/parsers/integration/ParserIntegrationTest.java index cd3d005..6b00a2b 100644 --- a/metron-platform/metron-parsers/src/test/java/org/apache/metron/parsers/integration/ParserIntegrationTest.java +++ b/metron-platform/metron-parsers/src/test/java/org/apache/metron/parsers/integration/ParserIntegrationTest.java @@ -1,4 +1,4 @@ -/** +/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information @@ -17,31 +17,19 @@ */ package org.apache.metron.parsers.integration; -import com.google.common.base.Function; import org.apache.metron.TestConstants; -import org.apache.metron.common.Constants; -import org.apache.metron.enrichment.integration.components.ConfigUploadComponent; -import org.apache.metron.integration.*; -import org.apache.metron.integration.components.KafkaComponent; -import org.apache.metron.integration.processors.KafkaMessageSet; -import org.apache.metron.integration.components.ZKServerComponent; -import org.apache.metron.integration.processors.KafkaProcessor; +import org.apache.metron.integration.BaseIntegrationTest; +import org.apache.metron.integration.ProcessorResult; import org.apache.metron.integration.utils.TestUtils; -import org.apache.metron.parsers.integration.components.ParserTopologyComponent; import org.apache.metron.test.TestDataType; import org.apache.metron.test.utils.SampleDataUtils; -import org.junit.After; -import org.junit.AfterClass; import org.junit.Assert; -import org.junit.Before; -import org.junit.BeforeClass; import org.junit.Test; -import javax.annotation.Nullable; import java.io.File; import java.io.IOException; import java.nio.file.Files; -import java.util.*; +import java.util.List; public abstract class ParserIntegrationTest extends BaseIntegrationTest { protected List<byte[]> inputMessages; @@ -74,7 +62,7 @@ public abstract class ParserIntegrationTest extends BaseIntegrationTest { } else { List<ParserValidation> validations = getValidations(); if (validations == null || validations.isEmpty()) { - buffer.append("No validations configured for sensorType " + sensorType + ". Dumping parsed messages").append("\n"); + buffer.append("No validations configured for sensorType ").append(sensorType).append(". Dumping parsed messages").append("\n"); dumpParsedMessages(outputMessages,buffer); Assert.fail(buffer.toString()); } else { http://git-wip-us.apache.org/repos/asf/metron/blob/6bac842d/metron-platform/metron-parsers/src/test/java/org/apache/metron/parsers/json/JSONMapParserWrappedQueryTest.java ---------------------------------------------------------------------- diff --git a/metron-platform/metron-parsers/src/test/java/org/apache/metron/parsers/json/JSONMapParserWrappedQueryTest.java b/metron-platform/metron-parsers/src/test/java/org/apache/metron/parsers/json/JSONMapParserWrappedQueryTest.java new file mode 100644 index 0000000..0da45dd --- /dev/null +++ b/metron-platform/metron-parsers/src/test/java/org/apache/metron/parsers/json/JSONMapParserWrappedQueryTest.java @@ -0,0 +1,199 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.metron.parsers.json; + +import com.google.common.collect.ImmutableMap; +import org.adrianwalker.multilinestring.Multiline; +import org.apache.log4j.Level; +import org.apache.metron.parsers.BasicParser; +import org.apache.metron.test.utils.UnitTestHelper; +import org.json.simple.JSONObject; +import org.junit.Assert; +import org.junit.Test; + +import java.util.HashMap; +import java.util.List; + +public class JSONMapParserWrappedQueryTest { + + /** + * { "name" : "foo1", "value" : "bar", "number" : 1.0 }, + * { "name" : "foo2", "value" : "baz", "number" : 2.0 } + */ + @Multiline + static String JSON_LIST; + + /** + * { "name" : "foo1", "value" : "bar", "number" : 1.0 } + */ + @Multiline + static String JSON_SINGLE; + + /** + * { "name" : "foo2", "value" : "baz", "number" : 2.0 } + */ + @Multiline + static String JSON_SINGLE2; + + @Test + public void testHappyPath() { + JSONMapParser parser = new JSONMapParser(); + parser.configure(new HashMap<String, Object>() {{ + put(JSONMapParser.WRAP_JSON,true); + put(JSONMapParser.WRAP_ENTITY_NAME,"foo"); + put(JSONMapParser.JSONP_QUERY, "$.foo"); + }}); + List<JSONObject> output = parser.parse(JSON_LIST.getBytes()); + Assert.assertEquals(output.size(), 2); + //don't forget the timestamp field! + Assert.assertEquals(output.get(0).size(), 5); + JSONObject message = output.get(0); + Assert.assertEquals("foo1", message.get("name")); + Assert.assertEquals("bar", message.get("value")); + Assert.assertEquals(1.0, message.get("number")); + Assert.assertNotNull(message.get("timestamp")); + Assert.assertTrue(message.get("timestamp") instanceof Number); + Assert.assertNotNull(message.get("number")); + Assert.assertTrue(message.get("number") instanceof Number); + + message = output.get(1); + Assert.assertEquals("foo2", message.get("name")); + Assert.assertEquals("baz", message.get("value")); + Assert.assertEquals(2.0, message.get("number")); + Assert.assertNotNull(message.get("timestamp")); + Assert.assertTrue(message.get("timestamp") instanceof Number); + Assert.assertNotNull(message.get("number")); + Assert.assertTrue(message.get("number") instanceof Number); + + } + + @Test(expected = IllegalStateException.class) + public void testInvalidJSONPathThrows() { + JSONMapParser parser = new JSONMapParser(); + parser.configure(new HashMap<String, Object>() {{ + put(JSONMapParser.JSONP_QUERY, "$$..$$SDSE$#$#."); + }}); + List<JSONObject> output = parser.parse(JSON_LIST.getBytes()); + + } + + @Test + public void testNoMatchesNoExceptions() { + JSONMapParser parser = new JSONMapParser(); + parser.configure(new HashMap<String, Object>() {{ + put(JSONMapParser.JSONP_QUERY, "$.foo"); + }}); + List<JSONObject> output = parser.parse(JSON_SINGLE.getBytes()); + Assert.assertEquals(0, output.size()); + } + + /** + * { + * "foo" : + * [ + * { + * "collection" : { "blah" : 7, "blah2" : "foo", "bigblah" : { "innerBlah" : "baz", "reallyInnerBlah" : { "color" : "grey" }}} + * }, + * { + * "collection" : { "blah" : 8, "blah2" : "bar", "bigblah" : { "innerBlah" : "baz2", "reallyInnerBlah" : { "color" : "blue" }}} + * } + * ] + * } + */ + @Multiline + static String collectionHandlingJSON; + + @Test + public void testCollectionHandlingDrop() { + JSONMapParser parser = new JSONMapParser(); + parser.configure(new HashMap<String, Object>() {{ + put(JSONMapParser.JSONP_QUERY, "$.foo"); + }}); + List<JSONObject> output = parser.parse(collectionHandlingJSON.getBytes()); + Assert.assertEquals(output.size(), 2); + + //don't forget the timestamp field! + Assert.assertEquals(output.get(0).size(), 2); + + JSONObject message = output.get(0); + Assert.assertNotNull(message.get("timestamp")); + Assert.assertTrue(message.get("timestamp") instanceof Number); + + message = output.get(1); + Assert.assertNotNull(message.get("timestamp")); + Assert.assertTrue(message.get("timestamp") instanceof Number); + } + + @Test(expected = IllegalStateException.class) + public void testCollectionHandlingError() { + JSONMapParser parser = new JSONMapParser(); + parser.configure(ImmutableMap + .of(JSONMapParser.MAP_STRATEGY_CONFIG, JSONMapParser.MapStrategy.ERROR.name(), + JSONMapParser.JSONP_QUERY, "$.foo")); + UnitTestHelper.setLog4jLevel(BasicParser.class, Level.FATAL); + parser.parse(collectionHandlingJSON.getBytes()); + UnitTestHelper.setLog4jLevel(BasicParser.class, Level.ERROR); + } + + + @Test + public void testCollectionHandlingAllow() { + JSONMapParser parser = new JSONMapParser(); + parser.configure(ImmutableMap + .of(JSONMapParser.MAP_STRATEGY_CONFIG, JSONMapParser.MapStrategy.ALLOW.name(), + JSONMapParser.JSONP_QUERY, "$.foo")); + List<JSONObject> output = parser.parse(collectionHandlingJSON.getBytes()); + Assert.assertEquals(output.size(), 2); + Assert.assertEquals(output.get(0).size(), 3); + JSONObject message = output.get(0); + Assert.assertNotNull(message.get("timestamp")); + Assert.assertTrue(message.get("timestamp") instanceof Number); + + Assert.assertEquals(output.get(1).size(), 3); + message = output.get(1); + Assert.assertNotNull(message.get("timestamp")); + Assert.assertTrue(message.get("timestamp") instanceof Number); + } + + @Test + public void testCollectionHandlingUnfold() { + JSONMapParser parser = new JSONMapParser(); + parser.configure(ImmutableMap + .of(JSONMapParser.MAP_STRATEGY_CONFIG, JSONMapParser.MapStrategy.UNFOLD.name(), + JSONMapParser.JSONP_QUERY, "$.foo")); + List<JSONObject> output = parser.parse(collectionHandlingJSON.getBytes()); + Assert.assertEquals(output.size(), 2); + Assert.assertEquals(output.get(0).size(), 6); + JSONObject message = output.get(0); + Assert.assertEquals(message.get("collection.blah"), 7); + Assert.assertEquals(message.get("collection.blah2"), "foo"); + Assert.assertEquals(message.get("collection.bigblah.innerBlah"), "baz"); + Assert.assertEquals(message.get("collection.bigblah.reallyInnerBlah.color"), "grey"); + Assert.assertNotNull(message.get("timestamp")); + Assert.assertTrue(message.get("timestamp") instanceof Number); + + Assert.assertEquals(output.get(1).size(), 6); + message = output.get(1); + Assert.assertEquals(message.get("collection.blah"), 8); + Assert.assertEquals(message.get("collection.blah2"), "bar"); + Assert.assertEquals(message.get("collection.bigblah.innerBlah"), "baz2"); + Assert.assertEquals(message.get("collection.bigblah.reallyInnerBlah.color"), "blue"); + Assert.assertNotNull(message.get("timestamp")); + Assert.assertTrue(message.get("timestamp") instanceof Number); + } +}