Skip to content

Commit 9f4c4ec

Browse files
committed
Add a streaming Json item reader
This commit adds a new Json item reader implementation based on Jackson. Resolves BATCH-2691
1 parent 977741c commit 9f4c4ec

File tree

8 files changed

+859
-0
lines changed

8 files changed

+859
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/*
2+
* Copyright 2018 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.batch.item.json;
18+
19+
import java.math.BigDecimal;
20+
21+
import com.fasterxml.jackson.core.JsonParseException;
22+
import org.junit.Assert;
23+
import org.junit.Rule;
24+
import org.junit.Test;
25+
import org.junit.rules.ExpectedException;
26+
27+
import org.springframework.batch.item.ExecutionContext;
28+
import org.springframework.batch.item.ItemStreamException;
29+
import org.springframework.batch.item.json.domain.Trade;
30+
import org.springframework.core.io.ByteArrayResource;
31+
import org.springframework.core.io.ClassPathResource;
32+
33+
import static org.hamcrest.Matchers.instanceOf;
34+
35+
/**
36+
* @author Mahmoud Ben Hassine
37+
*/
38+
public class JsonItemReaderFunctionalTests {
39+
40+
@Rule
41+
public ExpectedException expectedException = ExpectedException.none();
42+
43+
@Test
44+
public void testJsonReading() throws Exception {
45+
JsonItemReader<Trade> itemReader = new JsonItemReader<>();
46+
itemReader.setItemType(Trade.class);
47+
itemReader.setResource(new ClassPathResource("org/springframework/batch/item/json/trades.json"));
48+
itemReader.setName("tradeJsonItemReader");
49+
itemReader.afterPropertiesSet();
50+
51+
itemReader.open(new ExecutionContext());
52+
53+
Trade trade = itemReader.read();
54+
Assert.assertNotNull(trade);
55+
Assert.assertEquals("123", trade.getIsin());
56+
Assert.assertEquals("foo", trade.getCustomer());
57+
Assert.assertEquals(new BigDecimal("1.2"), trade.getPrice());
58+
Assert.assertEquals(1, trade.getQuantity());
59+
60+
trade = itemReader.read();
61+
Assert.assertNotNull(trade);
62+
Assert.assertEquals("456", trade.getIsin());
63+
Assert.assertEquals("bar", trade.getCustomer());
64+
Assert.assertEquals(new BigDecimal("1.4"), trade.getPrice());
65+
Assert.assertEquals(2, trade.getQuantity());
66+
67+
trade = itemReader.read();
68+
Assert.assertNull(trade);
69+
}
70+
71+
@Test
72+
public void testEmptyResource() throws Exception {
73+
JsonItemReader<Trade> itemReader = new JsonItemReader<>();
74+
itemReader.setItemType(Trade.class);
75+
itemReader.setResource(new ByteArrayResource("[]".getBytes()));
76+
itemReader.setName("tradeJsonItemReader");
77+
itemReader.afterPropertiesSet();
78+
79+
itemReader.open(new ExecutionContext());
80+
81+
Trade trade = itemReader.read();
82+
Assert.assertNull(trade);
83+
}
84+
85+
@Test
86+
public void testInvalidResourceFormat() throws Exception {
87+
this.expectedException.expect(ItemStreamException.class);
88+
this.expectedException.expectMessage("Failed to initialize the reader");
89+
this.expectedException.expectCause(instanceOf(IllegalStateException.class));
90+
JsonItemReader<Trade> itemReader = new JsonItemReader<>();
91+
itemReader.setItemType(Trade.class);
92+
itemReader.setResource(new ByteArrayResource("{}, {}".getBytes()));
93+
itemReader.afterPropertiesSet();
94+
95+
itemReader.open(new ExecutionContext());
96+
}
97+
98+
@Test
99+
public void testInvalidResourceContent() throws Exception {
100+
this.expectedException.expect(JsonParseException.class);
101+
JsonItemReader<Trade> itemReader = new JsonItemReader<>();
102+
itemReader.setItemType(Trade.class);
103+
itemReader.setResource(new ByteArrayResource("[{]".getBytes()));
104+
itemReader.setName("tradeJsonItemReader");
105+
itemReader.afterPropertiesSet();
106+
itemReader.open(new ExecutionContext());
107+
108+
itemReader.read();
109+
}
110+
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/*
2+
* Copyright 2018 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.batch.item.json.domain;
17+
18+
import java.math.BigDecimal;
19+
20+
/**
21+
* @author Mahmoud Ben Hassine
22+
*/
23+
public class Trade {
24+
25+
private String isin = "";
26+
27+
private long quantity = 0;
28+
29+
private BigDecimal price = new BigDecimal(0);
30+
31+
private String customer = "";
32+
33+
public Trade() {
34+
}
35+
36+
public Trade(String isin, long quantity, BigDecimal price, String customer) {
37+
this.isin = isin;
38+
this.quantity = quantity;
39+
this.price = price;
40+
this.customer = customer;
41+
}
42+
43+
public void setCustomer(String customer) {
44+
this.customer = customer;
45+
}
46+
47+
public void setIsin(String isin) {
48+
this.isin = isin;
49+
}
50+
51+
public void setPrice(BigDecimal price) {
52+
this.price = price;
53+
}
54+
55+
public void setQuantity(long quantity) {
56+
this.quantity = quantity;
57+
}
58+
59+
public String getIsin() {
60+
return isin;
61+
}
62+
63+
public BigDecimal getPrice() {
64+
return price;
65+
}
66+
67+
public long getQuantity() {
68+
return quantity;
69+
}
70+
71+
public String getCustomer() {
72+
return customer;
73+
}
74+
75+
@Override
76+
public String toString() {
77+
return "Trade: [isin=" + this.isin + ",quantity=" + this.quantity + ",price=" + this.price + ",customer="
78+
+ this.customer + "]";
79+
}
80+
81+
@Override
82+
public int hashCode() {
83+
final int prime = 31;
84+
int result = 1;
85+
result = prime * result + ((customer == null) ? 0 : customer.hashCode());
86+
result = prime * result + ((isin == null) ? 0 : isin.hashCode());
87+
result = prime * result + ((price == null) ? 0 : price.hashCode());
88+
result = prime * result + (int) (quantity ^ (quantity >>> 32));
89+
return result;
90+
}
91+
92+
@Override
93+
public boolean equals(Object obj) {
94+
if (this == obj)
95+
return true;
96+
if (obj == null)
97+
return false;
98+
if (getClass() != obj.getClass())
99+
return false;
100+
Trade other = (Trade) obj;
101+
if (customer == null) {
102+
if (other.customer != null)
103+
return false;
104+
}
105+
else if (!customer.equals(other.customer))
106+
return false;
107+
if (isin == null) {
108+
if (other.isin != null)
109+
return false;
110+
}
111+
else if (!isin.equals(other.isin))
112+
return false;
113+
if (price == null) {
114+
if (other.price != null)
115+
return false;
116+
}
117+
else if (!price.equals(other.price))
118+
return false;
119+
if (quantity != other.quantity)
120+
return false;
121+
return true;
122+
}
123+
124+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[
2+
{
3+
"isin": "123",
4+
"quantity": 1,
5+
"price": 1.2,
6+
"customer": "foo"
7+
},
8+
{
9+
"isin": "456",
10+
"quantity": 2,
11+
"price": 1.4,
12+
"customer": "bar"
13+
}
14+
]
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
/*
2+
* Copyright 2018 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.batch.item.json;
18+
19+
import java.io.InputStream;
20+
21+
import com.fasterxml.jackson.core.JsonParser;
22+
import com.fasterxml.jackson.core.JsonToken;
23+
import com.fasterxml.jackson.databind.ObjectMapper;
24+
import org.apache.commons.logging.Log;
25+
import org.apache.commons.logging.LogFactory;
26+
27+
import org.springframework.batch.item.ItemStreamReader;
28+
import org.springframework.batch.item.file.ResourceAwareItemReaderItemStream;
29+
import org.springframework.batch.item.support.AbstractItemCountingItemStreamItemReader;
30+
import org.springframework.beans.factory.InitializingBean;
31+
import org.springframework.core.io.Resource;
32+
import org.springframework.util.Assert;
33+
34+
/**
35+
* {@link ItemStreamReader} implementation that reads Json objects from a
36+
* {@link Resource} having the following format:
37+
* <p>
38+
* <code>
39+
* [
40+
* {
41+
* // JSON object
42+
* },
43+
* {
44+
* // JSON object
45+
* }
46+
* ]
47+
* </code>
48+
* <p>
49+
*
50+
* The implementation is <b>not</b> thread-safe.
51+
*
52+
* @param <T> the type of json objects to read
53+
*
54+
* @author Mahmoud Ben Hassine
55+
* @since 4.1
56+
*/
57+
public class JsonItemReader<T> extends AbstractItemCountingItemStreamItemReader<T> implements
58+
ResourceAwareItemReaderItemStream<T>, InitializingBean {
59+
60+
private static final Log LOGGER = LogFactory.getLog(JsonItemReader.class);
61+
62+
private Class<T> itemType;
63+
64+
private Resource resource;
65+
66+
private JsonParser parser;
67+
68+
private ObjectMapper mapper = new ObjectMapper();
69+
70+
private boolean strict = true;
71+
72+
/**
73+
* Set the {@link ObjectMapper} to use to map Json fragments to domain objects.
74+
* @param mapper to use
75+
*/
76+
public void setObjectMapper(ObjectMapper mapper) {
77+
this.mapper = mapper;
78+
}
79+
80+
/**
81+
* Set the target domain object type.
82+
* @param itemType the target domain object type
83+
*/
84+
public void setItemType(Class<T> itemType) {
85+
this.itemType = itemType;
86+
}
87+
88+
/**
89+
* In strict mode the reader will throw an exception on
90+
* {@link #open(org.springframework.batch.item.ExecutionContext)} if the
91+
* input resource does not exist.
92+
* @param strict true by default
93+
*/
94+
public void setStrict(boolean strict) {
95+
this.strict = strict;
96+
}
97+
98+
@Override
99+
public void setResource(Resource resource) {
100+
this.resource = resource;
101+
}
102+
103+
@Override
104+
protected T doRead() throws Exception {
105+
if (this.parser.nextToken() == JsonToken.START_OBJECT) {
106+
return this.mapper.readValue(this.parser, this.itemType);
107+
}
108+
return null;
109+
}
110+
111+
@Override
112+
protected void doOpen() throws Exception {
113+
if (!this.resource.exists()) {
114+
if (this.strict) {
115+
throw new IllegalStateException("Input resource must exist (reader is in 'strict' mode)");
116+
}
117+
LOGGER.warn("Input resource does not exist " + this.resource.getDescription());
118+
return;
119+
}
120+
if (!this.resource.isReadable()) {
121+
if (this.strict) {
122+
throw new IllegalStateException("Input resource must be readable (reader is in 'strict' mode)");
123+
}
124+
LOGGER.warn("Input resource is not readable " + this.resource.getDescription());
125+
return;
126+
}
127+
InputStream resourceInputStream = this.resource.getInputStream();
128+
this.parser = this.mapper.getFactory().createParser(resourceInputStream);
129+
Assert.state(this.parser.nextToken() == JsonToken.START_ARRAY,
130+
"The resource must start with an array of Json objects");
131+
}
132+
133+
@Override
134+
protected void doClose() throws Exception {
135+
this.parser.close();
136+
}
137+
138+
@Override
139+
public void afterPropertiesSet() throws Exception {
140+
Assert.notNull(this.itemType, "The item type must not be null.");
141+
Assert.notNull(this.resource, "The resource must not be null.");
142+
}
143+
144+
}

0 commit comments

Comments
 (0)