From d1b9998b3bd5daa2e40779e4097da7bb0489159c Mon Sep 17 00:00:00 2001 From: Mahmoud Ben Hassine Date: Mon, 21 May 2018 18:59:16 +0200 Subject: [PATCH 1/4] Add a streaming Json item reader This commit adds a new Json item reader with two implementations based on Jackson and Gson. Resolves BATCH-2691 --- build.gradle | 2 + .../GsonJsonItemReaderFunctionalTests.java | 38 +++++ .../JacksonJsonItemReaderFunctionalTests.java | 38 +++++ .../json/JsonItemReaderFunctionalTests.java | 115 +++++++++++++ .../batch/item/json/domain/Trade.java | 124 ++++++++++++++ .../batch/item/json/trades.json | 14 ++ .../batch/item/json/GsonJsonObjectReader.java | 89 ++++++++++ .../item/json/JacksonJsonObjectReader.java | 87 ++++++++++ .../batch/item/json/JsonItemReader.java | 121 +++++++++++++ .../batch/item/json/JsonObjectReader.java | 55 ++++++ .../json/builder/JsonItemReaderBuilder.java | 159 ++++++++++++++++++ .../json/GsonJsonItemReaderCommonTests.java | 31 ++++ .../JacksonJsonItemReaderCommonTests.java | 31 ++++ .../item/json/JsonItemReaderCommonTests.java | 60 +++++++ .../batch/item/json/JsonItemReaderTests.java | 127 ++++++++++++++ .../builder/JsonItemReaderBuilderTest.java | 106 ++++++++++++ 16 files changed, 1197 insertions(+) create mode 100644 spring-batch-infrastructure-tests/src/test/java/org/springframework/batch/item/json/GsonJsonItemReaderFunctionalTests.java create mode 100644 spring-batch-infrastructure-tests/src/test/java/org/springframework/batch/item/json/JacksonJsonItemReaderFunctionalTests.java create mode 100644 spring-batch-infrastructure-tests/src/test/java/org/springframework/batch/item/json/JsonItemReaderFunctionalTests.java create mode 100644 spring-batch-infrastructure-tests/src/test/java/org/springframework/batch/item/json/domain/Trade.java create mode 100644 spring-batch-infrastructure-tests/src/test/resources/org/springframework/batch/item/json/trades.json create mode 100644 spring-batch-infrastructure/src/main/java/org/springframework/batch/item/json/GsonJsonObjectReader.java create mode 100644 spring-batch-infrastructure/src/main/java/org/springframework/batch/item/json/JacksonJsonObjectReader.java create mode 100644 spring-batch-infrastructure/src/main/java/org/springframework/batch/item/json/JsonItemReader.java create mode 100644 spring-batch-infrastructure/src/main/java/org/springframework/batch/item/json/JsonObjectReader.java create mode 100644 spring-batch-infrastructure/src/main/java/org/springframework/batch/item/json/builder/JsonItemReaderBuilder.java create mode 100644 spring-batch-infrastructure/src/test/java/org/springframework/batch/item/json/GsonJsonItemReaderCommonTests.java create mode 100644 spring-batch-infrastructure/src/test/java/org/springframework/batch/item/json/JacksonJsonItemReaderCommonTests.java create mode 100644 spring-batch-infrastructure/src/test/java/org/springframework/batch/item/json/JsonItemReaderCommonTests.java create mode 100644 spring-batch-infrastructure/src/test/java/org/springframework/batch/item/json/JsonItemReaderTests.java create mode 100644 spring-batch-infrastructure/src/test/java/org/springframework/batch/item/json/builder/JsonItemReaderBuilderTest.java diff --git a/build.gradle b/build.gradle index 84fc6a9f37..cca04f07c4 100644 --- a/build.gradle +++ b/build.gradle @@ -74,6 +74,7 @@ allprojects { hibernateValidatorVersion = '6.0.4.Final' hsqldbVersion = '2.4.0' jackson2Version = '2.9.2' + gsonVersion = '2.8.5' javaMailVersion = '1.6.0' javaxBatchApiVersion = '1.0' javaxInjectVersion = '1' @@ -326,6 +327,7 @@ project('spring-batch-infrastructure') { optional "javax.jms:javax.jms-api:$jmsVersion" optional "com.fasterxml.jackson.core:jackson-databind:${jackson2Version}" + optional "com.google.code.gson:gson:${gsonVersion}" compile("org.hibernate:hibernate-core:$hibernateVersion") { dep -> optional dep exclude group: 'org.jboss.spec.javax.transaction', module: 'jboss-transaction-api_1.1_spec' diff --git a/spring-batch-infrastructure-tests/src/test/java/org/springframework/batch/item/json/GsonJsonItemReaderFunctionalTests.java b/spring-batch-infrastructure-tests/src/test/java/org/springframework/batch/item/json/GsonJsonItemReaderFunctionalTests.java new file mode 100644 index 0000000000..05821e36dc --- /dev/null +++ b/spring-batch-infrastructure-tests/src/test/java/org/springframework/batch/item/json/GsonJsonItemReaderFunctionalTests.java @@ -0,0 +1,38 @@ +/* + * Copyright 2018 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.json; + +import com.google.gson.JsonSyntaxException; + +import org.springframework.batch.item.json.domain.Trade; + +/** + * @author Mahmoud Ben Hassine + */ +public class GsonJsonItemReaderFunctionalTests extends JsonItemReaderFunctionalTests { + + @Override + protected JsonObjectReader getJsonObjectReader() { + return new GsonJsonObjectReader<>(Trade.class); + } + + @Override + protected Class getJsonParsingException() { + return JsonSyntaxException.class; + } + +} diff --git a/spring-batch-infrastructure-tests/src/test/java/org/springframework/batch/item/json/JacksonJsonItemReaderFunctionalTests.java b/spring-batch-infrastructure-tests/src/test/java/org/springframework/batch/item/json/JacksonJsonItemReaderFunctionalTests.java new file mode 100644 index 0000000000..d09079f385 --- /dev/null +++ b/spring-batch-infrastructure-tests/src/test/java/org/springframework/batch/item/json/JacksonJsonItemReaderFunctionalTests.java @@ -0,0 +1,38 @@ +/* + * Copyright 2018 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.json; + +import com.fasterxml.jackson.core.JsonParseException; + +import org.springframework.batch.item.json.domain.Trade; + +/** + * @author Mahmoud Ben Hassine + */ +public class JacksonJsonItemReaderFunctionalTests extends JsonItemReaderFunctionalTests { + + @Override + protected JsonObjectReader getJsonObjectReader() { + return new JacksonJsonObjectReader<>(Trade.class); + } + + @Override + protected Class getJsonParsingException() { + return JsonParseException.class; + } + +} diff --git a/spring-batch-infrastructure-tests/src/test/java/org/springframework/batch/item/json/JsonItemReaderFunctionalTests.java b/spring-batch-infrastructure-tests/src/test/java/org/springframework/batch/item/json/JsonItemReaderFunctionalTests.java new file mode 100644 index 0000000000..1a2110bb35 --- /dev/null +++ b/spring-batch-infrastructure-tests/src/test/java/org/springframework/batch/item/json/JsonItemReaderFunctionalTests.java @@ -0,0 +1,115 @@ +/* + * Copyright 2018 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.json; + +import java.math.BigDecimal; + +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.batch.item.ExecutionContext; +import org.springframework.batch.item.ItemStreamException; +import org.springframework.batch.item.json.builder.JsonItemReaderBuilder; +import org.springframework.batch.item.json.domain.Trade; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.ClassPathResource; + +import static org.hamcrest.Matchers.instanceOf; + +/** + * @author Mahmoud Ben Hassine + */ +public abstract class JsonItemReaderFunctionalTests { + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + protected abstract JsonObjectReader getJsonObjectReader(); + + protected abstract Class getJsonParsingException(); + + @Test + public void testJsonReading() throws Exception { + JsonItemReader itemReader = new JsonItemReaderBuilder() + .jsonObjectReader(getJsonObjectReader()) + .resource(new ClassPathResource("org/springframework/batch/item/json/trades.json")) + .name("tradeJsonItemReader") + .build(); + + itemReader.open(new ExecutionContext()); + + Trade trade = itemReader.read(); + Assert.assertNotNull(trade); + Assert.assertEquals("123", trade.getIsin()); + Assert.assertEquals("foo", trade.getCustomer()); + Assert.assertEquals(new BigDecimal("1.2"), trade.getPrice()); + Assert.assertEquals(1, trade.getQuantity()); + + trade = itemReader.read(); + Assert.assertNotNull(trade); + Assert.assertEquals("456", trade.getIsin()); + Assert.assertEquals("bar", trade.getCustomer()); + Assert.assertEquals(new BigDecimal("1.4"), trade.getPrice()); + Assert.assertEquals(2, trade.getQuantity()); + + trade = itemReader.read(); + Assert.assertNull(trade); + } + + @Test + public void testEmptyResource() throws Exception { + JsonItemReader itemReader = new JsonItemReaderBuilder() + .jsonObjectReader(getJsonObjectReader()) + .resource(new ByteArrayResource("[]".getBytes())) + .name("tradeJsonItemReader") + .build(); + + itemReader.open(new ExecutionContext()); + + Trade trade = itemReader.read(); + Assert.assertNull(trade); + } + + @Test + public void testInvalidResourceFormat() { + this.expectedException.expect(ItemStreamException.class); + this.expectedException.expectMessage("Failed to initialize the reader"); + this.expectedException.expectCause(instanceOf(IllegalStateException.class)); + JsonItemReader itemReader = new JsonItemReaderBuilder() + .jsonObjectReader(getJsonObjectReader()) + .resource(new ByteArrayResource("{}, {}".getBytes())) + .name("tradeJsonItemReader") + .build(); + + itemReader.open(new ExecutionContext()); + } + + @Test + public void testInvalidResourceContent() throws Exception { + this.expectedException.expect(getJsonParsingException()); + JsonItemReader itemReader = new JsonItemReaderBuilder() + .jsonObjectReader(getJsonObjectReader()) + .resource(new ByteArrayResource("[{]".getBytes())) + .name("tradeJsonItemReader") + .build(); + itemReader.open(new ExecutionContext()); + + itemReader.read(); + } +} diff --git a/spring-batch-infrastructure-tests/src/test/java/org/springframework/batch/item/json/domain/Trade.java b/spring-batch-infrastructure-tests/src/test/java/org/springframework/batch/item/json/domain/Trade.java new file mode 100644 index 0000000000..78e14b8621 --- /dev/null +++ b/spring-batch-infrastructure-tests/src/test/java/org/springframework/batch/item/json/domain/Trade.java @@ -0,0 +1,124 @@ +/* + * Copyright 2018 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.json.domain; + +import java.math.BigDecimal; + +/** + * @author Mahmoud Ben Hassine + */ +public class Trade { + + private String isin = ""; + + private long quantity = 0; + + private BigDecimal price = new BigDecimal(0); + + private String customer = ""; + + public Trade() { + } + + public Trade(String isin, long quantity, BigDecimal price, String customer) { + this.isin = isin; + this.quantity = quantity; + this.price = price; + this.customer = customer; + } + + public void setCustomer(String customer) { + this.customer = customer; + } + + public void setIsin(String isin) { + this.isin = isin; + } + + public void setPrice(BigDecimal price) { + this.price = price; + } + + public void setQuantity(long quantity) { + this.quantity = quantity; + } + + public String getIsin() { + return isin; + } + + public BigDecimal getPrice() { + return price; + } + + public long getQuantity() { + return quantity; + } + + public String getCustomer() { + return customer; + } + + @Override + public String toString() { + return "Trade: [isin=" + this.isin + ",quantity=" + this.quantity + ",price=" + this.price + ",customer=" + + this.customer + "]"; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((customer == null) ? 0 : customer.hashCode()); + result = prime * result + ((isin == null) ? 0 : isin.hashCode()); + result = prime * result + ((price == null) ? 0 : price.hashCode()); + result = prime * result + (int) (quantity ^ (quantity >>> 32)); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Trade other = (Trade) obj; + if (customer == null) { + if (other.customer != null) + return false; + } + else if (!customer.equals(other.customer)) + return false; + if (isin == null) { + if (other.isin != null) + return false; + } + else if (!isin.equals(other.isin)) + return false; + if (price == null) { + if (other.price != null) + return false; + } + else if (!price.equals(other.price)) + return false; + if (quantity != other.quantity) + return false; + return true; + } + +} diff --git a/spring-batch-infrastructure-tests/src/test/resources/org/springframework/batch/item/json/trades.json b/spring-batch-infrastructure-tests/src/test/resources/org/springframework/batch/item/json/trades.json new file mode 100644 index 0000000000..fd4cca1d80 --- /dev/null +++ b/spring-batch-infrastructure-tests/src/test/resources/org/springframework/batch/item/json/trades.json @@ -0,0 +1,14 @@ +[ + { + "isin": "123", + "quantity": 1, + "price": 1.2, + "customer": "foo" + }, + { + "isin": "456", + "quantity": 2, + "price": 1.4, + "customer": "bar" + } +] diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/json/GsonJsonObjectReader.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/json/GsonJsonObjectReader.java new file mode 100644 index 0000000000..3278bf03d6 --- /dev/null +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/json/GsonJsonObjectReader.java @@ -0,0 +1,89 @@ +/* + * Copyright 2018 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.json; + +import java.io.InputStream; +import java.io.InputStreamReader; + +import com.google.gson.Gson; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; + +import org.springframework.core.io.Resource; +import org.springframework.util.Assert; + +/** + * Implementation of {@link JsonObjectReader} based on + * Google Gson. + * + * @param type of the target object + * + * @author Mahmoud Ben Hassine + * @since 4.1 + */ +public class GsonJsonObjectReader implements JsonObjectReader { + + private Class itemType; + + private JsonReader jsonReader; + + private Gson mapper = new Gson(); + + private InputStream inputStream; + + /** + * Create a new {@link GsonJsonObjectReader} instance. + * @param itemType the target item type + */ + public GsonJsonObjectReader(Class itemType) { + this.itemType = itemType; + } + + /** + * Set the object mapper to use to map Json objects to domain objects. + * @param mapper the object mapper to use + */ + public void setMapper(Gson mapper) { + Assert.notNull(mapper, "The mapper must not be null"); + this.mapper = mapper; + } + + @Override + public void open(Resource resource) throws Exception { + Assert.notNull(resource, "The resource must not be null"); + this.inputStream = resource.getInputStream(); + this.jsonReader = this.mapper.newJsonReader(new InputStreamReader(this.inputStream)); + Assert.state(this.jsonReader.peek() == JsonToken.BEGIN_ARRAY, + "The Json input stream must start with an array of Json objects"); + this.jsonReader.beginArray(); + } + + @Override + public T read() throws Exception { + if (this.jsonReader.hasNext()) { + return this.mapper.fromJson(this.jsonReader, this.itemType); + } + return null; + } + + @Override + public void close() throws Exception { + this.inputStream.close(); + this.jsonReader.close(); + } + +} diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/json/JacksonJsonObjectReader.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/json/JacksonJsonObjectReader.java new file mode 100644 index 0000000000..8eb2d6a778 --- /dev/null +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/json/JacksonJsonObjectReader.java @@ -0,0 +1,87 @@ +/* + * Copyright 2018 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.json; + +import java.io.InputStream; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.springframework.core.io.Resource; +import org.springframework.util.Assert; + +/** + * Implementation of {@link JsonObjectReader} based on + * Jackson. + * + * @param type of the target object + * + * @author Mahmoud Ben Hassine + * @since 4.1 + */ +public class JacksonJsonObjectReader implements JsonObjectReader { + + private Class itemType; + + private JsonParser jsonParser; + + private ObjectMapper mapper = new ObjectMapper(); + + private InputStream inputStream; + + /** + * Create a new {@link JacksonJsonObjectReader} instance. + * @param itemType the target item type + */ + public JacksonJsonObjectReader(Class itemType) { + this.itemType = itemType; + } + + /** + * Set the object mapper to use to map Json objects to domain objects. + * @param mapper the object mapper to use + */ + public void setMapper(ObjectMapper mapper) { + Assert.notNull(mapper, "The mapper must not be null"); + this.mapper = mapper; + } + + @Override + public void open(Resource resource) throws Exception { + Assert.notNull(resource, "The resource must not be null"); + this.inputStream = resource.getInputStream(); + this.jsonParser = this.mapper.getFactory().createParser(this.inputStream); + Assert.state(this.jsonParser.nextToken() == JsonToken.START_ARRAY, + "The Json input stream must start with an array of Json objects"); + } + + @Override + public T read() throws Exception { + if (this.jsonParser.nextToken() == JsonToken.START_OBJECT) { + return this.mapper.readValue(this.jsonParser, this.itemType); + } + return null; + } + + @Override + public void close() throws Exception { + this.inputStream.close(); + this.jsonParser.close(); + } + +} diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/json/JsonItemReader.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/json/JsonItemReader.java new file mode 100644 index 0000000000..16d980f298 --- /dev/null +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/json/JsonItemReader.java @@ -0,0 +1,121 @@ +/* + * Copyright 2018 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.json; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.batch.item.ItemStreamReader; +import org.springframework.batch.item.file.ResourceAwareItemReaderItemStream; +import org.springframework.batch.item.support.AbstractItemCountingItemStreamItemReader; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.io.Resource; +import org.springframework.util.Assert; + +/** + * {@link ItemStreamReader} implementation that reads Json objects from a + * {@link Resource} having the following format: + *

+ * + * [ + * { + * // JSON object + * }, + * { + * // JSON object + * } + * ] + * + *

+ * + * The implementation is not thread-safe. + * + * @param the type of json objects to read + * + * @author Mahmoud Ben Hassine + * @since 4.1 + */ +public class JsonItemReader extends AbstractItemCountingItemStreamItemReader implements + ResourceAwareItemReaderItemStream, InitializingBean { + + private static final Log LOGGER = LogFactory.getLog(JsonItemReader.class); + + private Resource resource; + + private JsonObjectReader jsonObjectReader; + + private boolean strict = true; + + /** + * Set the {@link JsonObjectReader} to use to read and map Json fragments to domain objects. + * @param jsonObjectReader the json object reader to use + */ + public void setJsonObjectReader(JsonObjectReader jsonObjectReader) { + this.jsonObjectReader = jsonObjectReader; + } + + /** + * In strict mode the reader will throw an exception on + * {@link #open(org.springframework.batch.item.ExecutionContext)} if the + * input resource does not exist. + * @param strict true by default + */ + public void setStrict(boolean strict) { + this.strict = strict; + } + + @Override + public void setResource(Resource resource) { + this.resource = resource; + } + + @Override + protected T doRead() throws Exception { + return jsonObjectReader.read(); + } + + @Override + protected void doOpen() throws Exception { + if (!this.resource.exists()) { + if (this.strict) { + throw new IllegalStateException("Input resource must exist (reader is in 'strict' mode)"); + } + LOGGER.warn("Input resource does not exist " + this.resource.getDescription()); + return; + } + if (!this.resource.isReadable()) { + if (this.strict) { + throw new IllegalStateException("Input resource must be readable (reader is in 'strict' mode)"); + } + LOGGER.warn("Input resource is not readable " + this.resource.getDescription()); + return; + } + this.jsonObjectReader.open(this.resource); + } + + @Override + protected void doClose() throws Exception { + this.jsonObjectReader.close(); + } + + @Override + public void afterPropertiesSet() throws Exception { + Assert.notNull(this.jsonObjectReader, "The json object reader must not be null."); + Assert.notNull(this.resource, "The resource must not be null."); + } + +} diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/json/JsonObjectReader.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/json/JsonObjectReader.java new file mode 100644 index 0000000000..42b54f87c6 --- /dev/null +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/json/JsonObjectReader.java @@ -0,0 +1,55 @@ +/* + * Copyright 2018 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.json; + +import org.springframework.core.io.Resource; + +/** + * Strategy interface for Json readers. Implementations are expected to use + * a streaming API in order to read Json objects one at a time. + * + * @param type of the target object + * + * @author Mahmoud Ben Hassine + * @since 4.1 + */ +public interface JsonObjectReader { + + /** + * Open the Json resource for reading. + * @param resource the input resource + * @throws Exception if unable to open the resource + */ + default void open(Resource resource) throws Exception { + + } + + /** + * Read the next object in the Json resource if any. + * @return the next object or null if the resource is exhausted + * @throws Exception if unable to read the next object + */ + T read() throws Exception; + + /** + * Close the input resource. + * @throws Exception if unable to close the input resource + */ + default void close() throws Exception { + + } +} diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/json/builder/JsonItemReaderBuilder.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/json/builder/JsonItemReaderBuilder.java new file mode 100644 index 0000000000..fd3f26eac9 --- /dev/null +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/json/builder/JsonItemReaderBuilder.java @@ -0,0 +1,159 @@ +/* + * Copyright 2018 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.json.builder; + +import org.springframework.batch.item.json.JsonItemReader; +import org.springframework.batch.item.json.JsonObjectReader; +import org.springframework.core.io.Resource; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * A builder for {@link JsonItemReader}. + * + * @param type of the target item + * + * @author Mahmoud Ben Hassine + * @since 4.1 + */ +public class JsonItemReaderBuilder { + + private JsonObjectReader jsonObjectReader; + + private Resource resource; + + private String name; + + private boolean strict = true; + + private boolean saveState = true; + + private int maxItemCount = Integer.MAX_VALUE; + + private int currentItemCount; + + /** + * Set the {@link JsonObjectReader} to use to read and map Json objects to domain objects. + * @param jsonObjectReader to use + * @return The current instance of the builder. + * @see JsonItemReader#setJsonObjectReader(JsonObjectReader) + */ + public JsonItemReaderBuilder jsonObjectReader(JsonObjectReader jsonObjectReader) { + this.jsonObjectReader = jsonObjectReader; + + return this; + } + + /** + * The {@link Resource} to be used as input. + * @param resource the input to the reader. + * @return The current instance of the builder. + * @see JsonItemReader#setResource(Resource) + */ + public JsonItemReaderBuilder resource(Resource resource) { + this.resource = resource; + + return this; + } + + /** + * The name used to calculate the key within the + * {@link org.springframework.batch.item.ExecutionContext}. Required if + * {@link #saveState(boolean)} is set to true. + * @param name name of the reader instance + * @return The current instance of the builder. + * @see org.springframework.batch.item.ItemStreamSupport#setName(String) + */ + public JsonItemReaderBuilder name(String name) { + this.name = name; + + return this; + } + + /** + * Setting this value to true indicates that it is an error if the input + * does not exist and an exception will be thrown. Defaults to true. + * @param strict indicates the input resource must exist + * @return The current instance of the builder. + * @see JsonItemReader#setStrict(boolean) + */ + public JsonItemReaderBuilder strict(boolean strict) { + this.strict = strict; + + return this; + } + + /** + * Configure if the state of the {@link org.springframework.batch.item.ItemStreamSupport} + * should be persisted within the {@link org.springframework.batch.item.ExecutionContext} + * for restart purposes. + * @param saveState defaults to true + * @return The current instance of the builder. + */ + public JsonItemReaderBuilder saveState(boolean saveState) { + this.saveState = saveState; + + return this; + } + + /** + * Configure the max number of items to be read. + * @param maxItemCount the max items to be read + * @return The current instance of the builder. + * @see org.springframework.batch.item.support.AbstractItemCountingItemStreamItemReader#setMaxItemCount(int) + */ + public JsonItemReaderBuilder maxItemCount(int maxItemCount) { + this.maxItemCount = maxItemCount; + + return this; + } + + /** + * Index for the current item. Used on restarts to indicate where to start from. + * @param currentItemCount current index + * @return The current instance of the builder. + * @see org.springframework.batch.item.support.AbstractItemCountingItemStreamItemReader#setCurrentItemCount(int) + */ + public JsonItemReaderBuilder currentItemCount(int currentItemCount) { + this.currentItemCount = currentItemCount; + + return this; + } + + /** + * Validate the configuration and build a new {@link JsonItemReader}. + * @return a new instance of the {@link JsonItemReader} + */ + public JsonItemReader build() { + Assert.notNull(this.jsonObjectReader, "A json object reader is required."); + Assert.notNull(this.resource, "A resource is required."); + if (this.saveState) { + Assert.state(StringUtils.hasText(this.name), "A name is required when saveState is set to true."); + } + + JsonItemReader reader = new JsonItemReader<>(); + reader.setJsonObjectReader(this.jsonObjectReader); + reader.setResource(this.resource); + reader.setName(this.name); + reader.setStrict(this.strict); + reader.setSaveState(this.saveState); + reader.setMaxItemCount(this.maxItemCount); + reader.setCurrentItemCount(this.currentItemCount); + + return reader; + } +} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/json/GsonJsonItemReaderCommonTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/json/GsonJsonItemReaderCommonTests.java new file mode 100644 index 0000000000..2a10165a96 --- /dev/null +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/json/GsonJsonItemReaderCommonTests.java @@ -0,0 +1,31 @@ +/* + * Copyright 2018 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.json; + +import org.springframework.batch.item.sample.Foo; + +/** + * @author Mahmoud Ben Hassine + */ +public class GsonJsonItemReaderCommonTests extends JsonItemReaderCommonTests { + + @Override + protected JsonObjectReader getJsonObjectReader() { + return new GsonJsonObjectReader<>(Foo.class); + } + +} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/json/JacksonJsonItemReaderCommonTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/json/JacksonJsonItemReaderCommonTests.java new file mode 100644 index 0000000000..5be8cac795 --- /dev/null +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/json/JacksonJsonItemReaderCommonTests.java @@ -0,0 +1,31 @@ +/* + * Copyright 2018 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.json; + +import org.springframework.batch.item.sample.Foo; + +/** + * @author Mahmoud Ben Hassine + */ +public class JacksonJsonItemReaderCommonTests extends JsonItemReaderCommonTests { + + @Override + protected JsonObjectReader getJsonObjectReader() { + return new JacksonJsonObjectReader<>(Foo.class); + } + +} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/json/JsonItemReaderCommonTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/json/JsonItemReaderCommonTests.java new file mode 100644 index 0000000000..5a135bacbf --- /dev/null +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/json/JsonItemReaderCommonTests.java @@ -0,0 +1,60 @@ +/* + * Copyright 2018 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.json; + +import org.springframework.batch.item.AbstractItemStreamItemReaderTests; +import org.springframework.batch.item.ExecutionContext; +import org.springframework.batch.item.ItemReader; +import org.springframework.batch.item.sample.Foo; +import org.springframework.core.io.ByteArrayResource; + +/** + * @author Mahmoud Ben Hassine + */ +public abstract class JsonItemReaderCommonTests extends AbstractItemStreamItemReaderTests { + + private static final String FOOS = "[" + + " {\"value\":1}," + + " {\"value\":2}," + + " {\"value\":3}," + + " {\"value\":4}," + + " {\"value\":5}" + + "]"; + + protected abstract JsonObjectReader getJsonObjectReader(); + + @Override + protected ItemReader getItemReader() throws Exception { + JsonItemReader itemReader = new JsonItemReader<>(); + itemReader.setResource(new ByteArrayResource(FOOS.getBytes())); + itemReader.setName("fooJsonItemReader"); + itemReader.setSaveState(true); + itemReader.setJsonObjectReader(getJsonObjectReader()); + itemReader.afterPropertiesSet(); + return itemReader; + } + + @Override + protected void pointToEmptyInput(ItemReader tested) throws Exception { + JsonItemReader reader = (JsonItemReader) tested; + reader.setResource(new ByteArrayResource("[]".getBytes())); + reader.afterPropertiesSet(); + + reader.open(new ExecutionContext()); + } + +} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/json/JsonItemReaderTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/json/JsonItemReaderTests.java new file mode 100644 index 0000000000..db062435c9 --- /dev/null +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/json/JsonItemReaderTests.java @@ -0,0 +1,127 @@ +/* + * Copyright 2018 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.json; + +import java.io.InputStream; + +import org.hamcrest.Matchers; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.batch.item.ExecutionContext; +import org.springframework.batch.item.ItemStreamException; +import org.springframework.core.io.AbstractResource; + +/** + * @author Mahmoud Ben Hassine + */ +public class JsonItemReaderTests { + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + private JsonItemReader itemReader = new JsonItemReader<>(); + + @Test + public void testMandatoryJsonObjectReader() throws Exception { + this.expectedException.expect(IllegalArgumentException.class); + this.expectedException.expectMessage("The json object reader must not be null."); + + this.itemReader.afterPropertiesSet(); + } + + @Test + public void testMandatoryResource() throws Exception { + this.expectedException.expect(IllegalArgumentException.class); + this.expectedException.expectMessage("The resource must not be null."); + this.itemReader.setJsonObjectReader(() -> null); + + this.itemReader.afterPropertiesSet(); + } + + @Test + public void testNonExistentResource() throws Exception { + this.expectedException.expect(ItemStreamException.class); + this.expectedException.expectMessage("Failed to initialize the reader"); + this.expectedException.expectCause(Matchers.instanceOf(IllegalStateException.class)); + this.itemReader.setJsonObjectReader(() -> null); + this.itemReader.setResource(new NonExistentResource()); + this.itemReader.afterPropertiesSet(); + + this.itemReader.open(new ExecutionContext()); + } + + @Test + public void testNonReadableResource() throws Exception { + this.expectedException.expect(ItemStreamException.class); + this.expectedException.expectMessage("Failed to initialize the reader"); + this.expectedException.expectCause(Matchers.instanceOf(IllegalStateException.class)); + this.itemReader.setJsonObjectReader(() -> null); + this.itemReader.setResource(new NonReadableResource()); + this.itemReader.afterPropertiesSet(); + + this.itemReader.open(new ExecutionContext()); + } + + private static class NonExistentResource extends AbstractResource { + + NonExistentResource() { + } + + @Override + public boolean exists() { + return false; + } + + @Override + public String getDescription() { + return "NonExistentResource"; + } + + @Override + public InputStream getInputStream() { + return null; + } + } + + private static class NonReadableResource extends AbstractResource { + + NonReadableResource() { + } + + @Override + public boolean isReadable() { + return false; + } + + @Override + public boolean exists() { + return true; + } + + @Override + public String getDescription() { + return "NonReadableResource"; + } + + @Override + public InputStream getInputStream() { + return null; + } + } +} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/json/builder/JsonItemReaderBuilderTest.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/json/builder/JsonItemReaderBuilderTest.java new file mode 100644 index 0000000000..d661f65370 --- /dev/null +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/json/builder/JsonItemReaderBuilderTest.java @@ -0,0 +1,106 @@ +/* + * Copyright 2018 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.json.builder; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import org.springframework.batch.item.json.JsonItemReader; +import org.springframework.batch.item.json.JsonObjectReader; +import org.springframework.core.io.Resource; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; +import static org.springframework.test.util.ReflectionTestUtils.getField; + +/** + * @author Mahmoud Ben Hassine + */ +public class JsonItemReaderBuilderTest { + + @Mock + private Resource resource; + @Mock + private JsonObjectReader jsonObjectReader; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void testValidation() { + try { + new JsonItemReaderBuilder() + .build(); + fail("A json object reader is required."); + } + catch (IllegalArgumentException iae) { + assertEquals("A json object reader is required.", + iae.getMessage()); + } + + try { + new JsonItemReaderBuilder() + .jsonObjectReader(this.jsonObjectReader) + .build(); + fail("A resource is required."); + } + catch (IllegalArgumentException iae) { + assertEquals("A resource is required.", + iae.getMessage()); + } + + try { + new JsonItemReaderBuilder() + .jsonObjectReader(this.jsonObjectReader) + .resource(this.resource) + .saveState(true) + .build(); + fail("A name is required when saveState is set to true."); + } + catch (IllegalStateException iae) { + assertEquals("A name is required when saveState is set to true.", + iae.getMessage()); + } + } + + @Test + public void testConfiguration() { + JsonItemReader itemReader = new JsonItemReaderBuilder() + .jsonObjectReader(this.jsonObjectReader) + .resource(this.resource) + .saveState(true) + .strict(true) + .name("jsonItemReader") + .maxItemCount(100) + .currentItemCount(50) + .build(); + + Assert.assertEquals(this.jsonObjectReader, getField(itemReader, "jsonObjectReader")); + Assert.assertEquals(this.resource, getField(itemReader, "resource")); + Assert.assertEquals(100, getField(itemReader, "maxItemCount")); + Assert.assertEquals(50, getField(itemReader, "currentItemCount")); + Assert.assertTrue((Boolean) getField(itemReader, "saveState")); + Assert.assertTrue((Boolean) getField(itemReader, "strict")); + Object executionContext = getField(itemReader, "executionContextUserSupport"); + Assert.assertEquals("jsonItemReader", getField(executionContext, "name")); + } +} From 5b6e7b9ff7db438e5ef574b4d9fc1ea4b5aa5d9f Mon Sep 17 00:00:00 2001 From: Mahmoud Ben Hassine Date: Thu, 31 May 2018 00:48:30 +0200 Subject: [PATCH 2/4] BATCH-2691: remove saveState(true) since it is the default --- .../batch/item/json/builder/JsonItemReaderBuilderTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/json/builder/JsonItemReaderBuilderTest.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/json/builder/JsonItemReaderBuilderTest.java index d661f65370..57e2fa38da 100644 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/json/builder/JsonItemReaderBuilderTest.java +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/json/builder/JsonItemReaderBuilderTest.java @@ -72,7 +72,6 @@ public void testValidation() { new JsonItemReaderBuilder() .jsonObjectReader(this.jsonObjectReader) .resource(this.resource) - .saveState(true) .build(); fail("A name is required when saveState is set to true."); } From 53262e6b604b42c3fa5d193213bd1b0d19e2a77a Mon Sep 17 00:00:00 2001 From: Mahmoud Ben Hassine Date: Thu, 31 May 2018 00:53:23 +0200 Subject: [PATCH 3/4] BATCH-2691: remove leaky abstraction of json parsing exceptions by wrapping them in a ParseException --- .../item/json/JsonItemReaderFunctionalTests.java | 5 ++++- .../batch/item/json/GsonJsonObjectReader.java | 12 ++++++++++-- .../batch/item/json/JacksonJsonObjectReader.java | 10 ++++++++-- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/spring-batch-infrastructure-tests/src/test/java/org/springframework/batch/item/json/JsonItemReaderFunctionalTests.java b/spring-batch-infrastructure-tests/src/test/java/org/springframework/batch/item/json/JsonItemReaderFunctionalTests.java index 1a2110bb35..ca5e5f23dc 100644 --- a/spring-batch-infrastructure-tests/src/test/java/org/springframework/batch/item/json/JsonItemReaderFunctionalTests.java +++ b/spring-batch-infrastructure-tests/src/test/java/org/springframework/batch/item/json/JsonItemReaderFunctionalTests.java @@ -18,6 +18,7 @@ import java.math.BigDecimal; +import org.hamcrest.Matchers; import org.junit.Assert; import org.junit.Rule; import org.junit.Test; @@ -25,6 +26,7 @@ import org.springframework.batch.item.ExecutionContext; import org.springframework.batch.item.ItemStreamException; +import org.springframework.batch.item.ParseException; import org.springframework.batch.item.json.builder.JsonItemReaderBuilder; import org.springframework.batch.item.json.domain.Trade; import org.springframework.core.io.ByteArrayResource; @@ -102,7 +104,8 @@ public void testInvalidResourceFormat() { @Test public void testInvalidResourceContent() throws Exception { - this.expectedException.expect(getJsonParsingException()); + this.expectedException.expect(ParseException.class); + this.expectedException.expectCause(Matchers.instanceOf(getJsonParsingException())); JsonItemReader itemReader = new JsonItemReaderBuilder() .jsonObjectReader(getJsonObjectReader()) .resource(new ByteArrayResource("[{]".getBytes())) diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/json/GsonJsonObjectReader.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/json/GsonJsonObjectReader.java index 3278bf03d6..c591fafaae 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/json/GsonJsonObjectReader.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/json/GsonJsonObjectReader.java @@ -16,13 +16,17 @@ package org.springframework.batch.item.json; +import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import com.google.gson.Gson; +import com.google.gson.JsonIOException; +import com.google.gson.JsonSyntaxException; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonToken; +import org.springframework.batch.item.ParseException; import org.springframework.core.io.Resource; import org.springframework.util.Assert; @@ -74,8 +78,12 @@ public void open(Resource resource) throws Exception { @Override public T read() throws Exception { - if (this.jsonReader.hasNext()) { - return this.mapper.fromJson(this.jsonReader, this.itemType); + try { + if (this.jsonReader.hasNext()) { + return this.mapper.fromJson(this.jsonReader, this.itemType); + } + } catch (IOException |JsonIOException | JsonSyntaxException e) { + throw new ParseException("Unable to read next JSON object", e); } return null; } diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/json/JacksonJsonObjectReader.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/json/JacksonJsonObjectReader.java index 8eb2d6a778..a32ad009f4 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/json/JacksonJsonObjectReader.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/json/JacksonJsonObjectReader.java @@ -16,12 +16,14 @@ package org.springframework.batch.item.json; +import java.io.IOException; import java.io.InputStream; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonToken; import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.batch.item.ParseException; import org.springframework.core.io.Resource; import org.springframework.util.Assert; @@ -72,8 +74,12 @@ public void open(Resource resource) throws Exception { @Override public T read() throws Exception { - if (this.jsonParser.nextToken() == JsonToken.START_OBJECT) { - return this.mapper.readValue(this.jsonParser, this.itemType); + try { + if (this.jsonParser.nextToken() == JsonToken.START_OBJECT) { + return this.mapper.readValue(this.jsonParser, this.itemType); + } + } catch (IOException e) { + throw new ParseException("Unable to read next JSON object", e); } return null; } From 493196c39b363448b08aa6ec8a600a8517e403fa Mon Sep 17 00:00:00 2001 From: Mahmoud Ben Hassine Date: Thu, 31 May 2018 09:35:51 +0200 Subject: [PATCH 4/4] BATCH-2691: use constructor injection for required dependencies --- .../batch/item/json/JsonItemReader.java | 21 ++++--- .../json/builder/JsonItemReaderBuilder.java | 4 +- .../item/json/JsonItemReaderCommonTests.java | 13 ++-- .../batch/item/json/JsonItemReaderTests.java | 59 ++++++++++++------- 4 files changed, 57 insertions(+), 40 deletions(-) diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/json/JsonItemReader.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/json/JsonItemReader.java index 16d980f298..9dd8bdc982 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/json/JsonItemReader.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/json/JsonItemReader.java @@ -22,7 +22,6 @@ import org.springframework.batch.item.ItemStreamReader; import org.springframework.batch.item.file.ResourceAwareItemReaderItemStream; import org.springframework.batch.item.support.AbstractItemCountingItemStreamItemReader; -import org.springframework.beans.factory.InitializingBean; import org.springframework.core.io.Resource; import org.springframework.util.Assert; @@ -50,7 +49,7 @@ * @since 4.1 */ public class JsonItemReader extends AbstractItemCountingItemStreamItemReader implements - ResourceAwareItemReaderItemStream, InitializingBean { + ResourceAwareItemReaderItemStream { private static final Log LOGGER = LogFactory.getLog(JsonItemReader.class); @@ -60,6 +59,18 @@ public class JsonItemReader extends AbstractItemCountingItemStreamItemReader< private boolean strict = true; + /** + * Create a new {@link JsonItemReader} instance. + * @param resource the input json resource + * @param jsonObjectReader the json object reader to use + */ + public JsonItemReader(Resource resource, JsonObjectReader jsonObjectReader) { + Assert.notNull(resource, "The resource must not be null."); + Assert.notNull(jsonObjectReader, "The json object reader must not be null."); + this.resource = resource; + this.jsonObjectReader = jsonObjectReader; + } + /** * Set the {@link JsonObjectReader} to use to read and map Json fragments to domain objects. * @param jsonObjectReader the json object reader to use @@ -112,10 +123,4 @@ protected void doClose() throws Exception { this.jsonObjectReader.close(); } - @Override - public void afterPropertiesSet() throws Exception { - Assert.notNull(this.jsonObjectReader, "The json object reader must not be null."); - Assert.notNull(this.resource, "The resource must not be null."); - } - } diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/json/builder/JsonItemReaderBuilder.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/json/builder/JsonItemReaderBuilder.java index fd3f26eac9..41b7914e03 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/json/builder/JsonItemReaderBuilder.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/json/builder/JsonItemReaderBuilder.java @@ -145,9 +145,7 @@ public JsonItemReader build() { Assert.state(StringUtils.hasText(this.name), "A name is required when saveState is set to true."); } - JsonItemReader reader = new JsonItemReader<>(); - reader.setJsonObjectReader(this.jsonObjectReader); - reader.setResource(this.resource); + JsonItemReader reader = new JsonItemReader<>(this.resource, this.jsonObjectReader); reader.setName(this.name); reader.setStrict(this.strict); reader.setSaveState(this.saveState); diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/json/JsonItemReaderCommonTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/json/JsonItemReaderCommonTests.java index 5a135bacbf..7f02e8e59f 100644 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/json/JsonItemReaderCommonTests.java +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/json/JsonItemReaderCommonTests.java @@ -38,21 +38,18 @@ public abstract class JsonItemReaderCommonTests extends AbstractItemStreamItemRe protected abstract JsonObjectReader getJsonObjectReader(); @Override - protected ItemReader getItemReader() throws Exception { - JsonItemReader itemReader = new JsonItemReader<>(); - itemReader.setResource(new ByteArrayResource(FOOS.getBytes())); + protected ItemReader getItemReader() { + ByteArrayResource resource = new ByteArrayResource(FOOS.getBytes()); + JsonObjectReader jsonObjectReader = getJsonObjectReader(); + JsonItemReader itemReader = new JsonItemReader<>(resource, jsonObjectReader); itemReader.setName("fooJsonItemReader"); - itemReader.setSaveState(true); - itemReader.setJsonObjectReader(getJsonObjectReader()); - itemReader.afterPropertiesSet(); return itemReader; } @Override - protected void pointToEmptyInput(ItemReader tested) throws Exception { + protected void pointToEmptyInput(ItemReader tested) { JsonItemReader reader = (JsonItemReader) tested; reader.setResource(new ByteArrayResource("[]".getBytes())); - reader.afterPropertiesSet(); reader.open(new ExecutionContext()); } diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/json/JsonItemReaderTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/json/JsonItemReaderTests.java index db062435c9..12cd58697a 100644 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/json/JsonItemReaderTests.java +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/json/JsonItemReaderTests.java @@ -22,60 +22,77 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; import org.springframework.batch.item.ExecutionContext; import org.springframework.batch.item.ItemStreamException; import org.springframework.core.io.AbstractResource; +import org.springframework.core.io.ByteArrayResource; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; /** * @author Mahmoud Ben Hassine */ +@RunWith(MockitoJUnitRunner.class) public class JsonItemReaderTests { @Rule public ExpectedException expectedException = ExpectedException.none(); - private JsonItemReader itemReader = new JsonItemReader<>(); - - @Test - public void testMandatoryJsonObjectReader() throws Exception { - this.expectedException.expect(IllegalArgumentException.class); - this.expectedException.expectMessage("The json object reader must not be null."); + @Mock + private JsonObjectReader jsonObjectReader; - this.itemReader.afterPropertiesSet(); - } + private JsonItemReader itemReader; @Test - public void testMandatoryResource() throws Exception { - this.expectedException.expect(IllegalArgumentException.class); - this.expectedException.expectMessage("The resource must not be null."); - this.itemReader.setJsonObjectReader(() -> null); + public void testValidation() { + try { + new JsonItemReader<>(null, this.jsonObjectReader); + fail("A resource is required."); + } catch (IllegalArgumentException iae) { + assertEquals("The resource must not be null.", iae.getMessage()); + } - this.itemReader.afterPropertiesSet(); + try { + new JsonItemReader<>(new ByteArrayResource("[{}]".getBytes()), null); + fail("A json object reader is required."); + } catch (IllegalArgumentException iae) { + assertEquals("The json object reader must not be null.", iae.getMessage()); + } } @Test - public void testNonExistentResource() throws Exception { + public void testNonExistentResource() { + // given this.expectedException.expect(ItemStreamException.class); this.expectedException.expectMessage("Failed to initialize the reader"); this.expectedException.expectCause(Matchers.instanceOf(IllegalStateException.class)); - this.itemReader.setJsonObjectReader(() -> null); - this.itemReader.setResource(new NonExistentResource()); - this.itemReader.afterPropertiesSet(); + this.itemReader = new JsonItemReader<>(new NonExistentResource(), this.jsonObjectReader); + // when this.itemReader.open(new ExecutionContext()); + + // then + // expected exception } @Test - public void testNonReadableResource() throws Exception { + public void testNonReadableResource() { + // given this.expectedException.expect(ItemStreamException.class); this.expectedException.expectMessage("Failed to initialize the reader"); this.expectedException.expectCause(Matchers.instanceOf(IllegalStateException.class)); - this.itemReader.setJsonObjectReader(() -> null); - this.itemReader.setResource(new NonReadableResource()); - this.itemReader.afterPropertiesSet(); + this.itemReader = new JsonItemReader<>(new NonReadableResource(), this.jsonObjectReader); + // when this.itemReader.open(new ExecutionContext()); + + // then + // expected exception } private static class NonExistentResource extends AbstractResource {