diff --git a/spring-batch-infrastructure/pom.xml b/spring-batch-infrastructure/pom.xml index b7cd74891..8b3567eef 100644 --- a/spring-batch-infrastructure/pom.xml +++ b/spring-batch-infrastructure/pom.xml @@ -1,5 +1,6 @@ - + 4.0.0 spring-batch-infrastructure 2.1.0.CI-SNAPSHOT @@ -100,6 +101,12 @@ commons-io test + + org.codehaus.jackson + jackson-mapper-asl + 1.0.1 + true + hsqldb hsqldb diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/separator/DefaultRecordSeparatorPolicy.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/separator/DefaultRecordSeparatorPolicy.java index 2392fdbe1..e535777d2 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/separator/DefaultRecordSeparatorPolicy.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/separator/DefaultRecordSeparatorPolicy.java @@ -119,8 +119,8 @@ public class DefaultRecordSeparatorPolicy extends SimpleRecordSeparatorPolicy { } /** - * Determine if the current line (or buffered concatenation of lines) - * contains an unterminated quote, indicating that the record is continuing + * Determine if the current line (or buffered concatenation of lines) ends + * with the continuation marker, indicating that the record is continuing * onto the next line. * * @param result diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/separator/JsonRecordSeparatorPolicy.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/separator/JsonRecordSeparatorPolicy.java new file mode 100644 index 000000000..e64aa3064 --- /dev/null +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/separator/JsonRecordSeparatorPolicy.java @@ -0,0 +1,49 @@ +/* + * Copyright 2006-2007 the original author or authors. + * + * 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 org.springframework.batch.item.file.separator; + +import org.springframework.util.StringUtils; + +/** + * JSON-based record separator. Waits for a valid JSON object before returning a + * complete line. A valid object has balanced braces ({}), possibly nested, and + * ends with a closing brace. This separator can be used to split a stream into + * JSON objects, even if those objects are spread over multiple lines, e.g. + * + *
+ * {"foo": "bar",
+ *  "value": { "spam": 2 }}
+ *  {"foo": "rab",
+ *  "value": { "spam": 3, "foo": "bar" }}
+ * 
+ * + * @author Dave Syer + * + */ +public class JsonRecordSeparatorPolicy extends SimpleRecordSeparatorPolicy { + + /** + * True if the line can be parsed to a JSON object. + * + * @see RecordSeparatorPolicy#isEndOfRecord(String) + */ + public boolean isEndOfRecord(String line) { + return StringUtils.countOccurrencesOf(line, "{") == StringUtils.countOccurrencesOf(line, "}") + && line.trim().endsWith("}"); + } + +} diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/transform/JsonLineMapper.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/transform/JsonLineMapper.java new file mode 100644 index 000000000..8fb1b6ca0 --- /dev/null +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/transform/JsonLineMapper.java @@ -0,0 +1,52 @@ +package org.springframework.batch.item.file.transform; + +import java.util.Map; + +import org.codehaus.jackson.JsonParser; +import org.codehaus.jackson.map.MappingJsonFactory; +import org.springframework.batch.item.file.FlatFileParseException; +import org.springframework.batch.item.file.LineMapper; + +/** + * Interpret a line as a Json object and parse it up to a Map. The line should + * be a standard Json object, starting with "{" and ending with "}" and composed + * of name:value pairs separated by commas. Whitespace is ignored, + * e.g. + * + *
+ * { "foo" : "bar", "value" : 123 }
+ * 
+ * + * The values can also be Json objects (which are converted to maps): + * + *
+ * { "foo": "bar", "map": { "one": 1, "two": 2}}
+ * 
+ * + * @author Dave Syer + * + */ +public class JsonLineMapper implements LineMapper> { + + private MappingJsonFactory factory = new MappingJsonFactory(); + + /** + * Interpret the line as a Json object and create a Map from it. + * + * @see LineMapper#mapLine(String, int) + */ + public Map mapLine(String line, int lineNumber) throws Exception { + Map result; + try { + JsonParser parser = factory.createJsonParser(line); + @SuppressWarnings("unchecked") + Map token = parser.readValueAs(Map.class); + result = token; + } + catch (Exception e) { + throw new FlatFileParseException("Cannot parse line to JSON", e, line, lineNumber); + } + return result; + } + +} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/separator/JsonRecordSeparatorPolicyTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/separator/JsonRecordSeparatorPolicyTests.java new file mode 100644 index 000000000..9ec34e591 --- /dev/null +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/separator/JsonRecordSeparatorPolicyTests.java @@ -0,0 +1,23 @@ +package org.springframework.batch.item.file.separator; + +import static org.junit.Assert.*; + +import org.junit.Test; + +public class JsonRecordSeparatorPolicyTests { + + private JsonRecordSeparatorPolicy policy = new JsonRecordSeparatorPolicy(); + + @Test + public void testIsEndOfRecord() { + assertFalse(policy.isEndOfRecord("{\"a\":\"b\"")); + assertTrue(policy.isEndOfRecord("{\"a\":\"b\"} ")); + } + + @Test + public void testNestedObject() { + assertFalse(policy.isEndOfRecord("{\"a\": {\"b\": 2}")); + assertTrue(policy.isEndOfRecord("{\"a\": {\"b\": 2}} ")); + } + +} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/transform/JsonLineMapperTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/transform/JsonLineMapperTests.java new file mode 100644 index 000000000..73b03e820 --- /dev/null +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/transform/JsonLineMapperTests.java @@ -0,0 +1,34 @@ +package org.springframework.batch.item.file.transform; + +import static org.junit.Assert.*; + +import java.util.Map; + +import org.junit.Test; +import org.springframework.batch.item.file.FlatFileParseException; + +public class JsonLineMapperTests { + + private JsonLineMapper mapper = new JsonLineMapper(); + + @Test + public void testMapLine() throws Exception { + Map map = mapper.mapLine("{\"foo\": 1}", 1); + assertEquals(1, map.get("foo")); + } + + @SuppressWarnings("unchecked") + @Test + public void testMapNested() throws Exception { + Map map = mapper.mapLine("{\"foo\": 1, \"bar\" : {\"foo\": 2}}", 1); + assertEquals(1, map.get("foo")); + assertEquals(2, ((Map) map.get("bar")).get("foo")); + } + + @Test(expected=FlatFileParseException.class) + public void testMappingError() throws Exception { + Map map = mapper.mapLine("{\"foo\": 1", 1); + assertEquals(1, map.get("foo")); + } + +}