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..ca5e5f23dc --- /dev/null +++ b/spring-batch-infrastructure-tests/src/test/java/org/springframework/batch/item/json/JsonItemReaderFunctionalTests.java @@ -0,0 +1,118 @@ +/* + * 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.hamcrest.Matchers; +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.ParseException; +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(ParseException.class); + this.expectedException.expectCause(Matchers.instanceOf(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..c591fafaae --- /dev/null +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/json/GsonJsonObjectReader.java @@ -0,0 +1,97 @@ +/* + * 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.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; + +/** + * 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 { + 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; + } + + @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..a32ad009f4 --- /dev/null +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/json/JacksonJsonObjectReader.java @@ -0,0 +1,93 @@ +/* + * 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.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; + +/** + * 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 { + 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; + } + + @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..9dd8bdc982 --- /dev/null +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/json/JsonItemReader.java @@ -0,0 +1,126 @@ +/* + * 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.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 { + + private static final Log LOGGER = LogFactory.getLog(JsonItemReader.class); + + private Resource resource; + + private JsonObjectReader jsonObjectReader; + + 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 + */ + 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(); + } + +} 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..41b7914e03 --- /dev/null +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/json/builder/JsonItemReaderBuilder.java @@ -0,0 +1,157 @@ +/* + * 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<>(this.resource, this.jsonObjectReader); + 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..7f02e8e59f --- /dev/null +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/json/JsonItemReaderCommonTests.java @@ -0,0 +1,57 @@ +/* + * 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() { + ByteArrayResource resource = new ByteArrayResource(FOOS.getBytes()); + JsonObjectReader jsonObjectReader = getJsonObjectReader(); + JsonItemReader itemReader = new JsonItemReader<>(resource, jsonObjectReader); + itemReader.setName("fooJsonItemReader"); + return itemReader; + } + + @Override + protected void pointToEmptyInput(ItemReader tested) { + JsonItemReader reader = (JsonItemReader) tested; + reader.setResource(new ByteArrayResource("[]".getBytes())); + + 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..12cd58697a --- /dev/null +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/json/JsonItemReaderTests.java @@ -0,0 +1,144 @@ +/* + * 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.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(); + + @Mock + private JsonObjectReader jsonObjectReader; + + private JsonItemReader itemReader; + + @Test + 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()); + } + + 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() { + // given + this.expectedException.expect(ItemStreamException.class); + this.expectedException.expectMessage("Failed to initialize the reader"); + this.expectedException.expectCause(Matchers.instanceOf(IllegalStateException.class)); + this.itemReader = new JsonItemReader<>(new NonExistentResource(), this.jsonObjectReader); + + // when + this.itemReader.open(new ExecutionContext()); + + // then + // expected exception + } + + @Test + 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 = new JsonItemReader<>(new NonReadableResource(), this.jsonObjectReader); + + // when + this.itemReader.open(new ExecutionContext()); + + // then + // expected exception + } + + 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..57e2fa38da --- /dev/null +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/json/builder/JsonItemReaderBuilderTest.java @@ -0,0 +1,105 @@ +/* + * 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) + .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")); + } +}