Skip to content

Commit fd1b321

Browse files
committed
Fix JsonFileItemWriter to produce valid JSON when append allowed
Closes GH-5272 Signed-off-by: Yanming Zhou <zhouyanming@gmail.com>
1 parent c121edd commit fd1b321

File tree

2 files changed

+113
-3
lines changed

2 files changed

+113
-3
lines changed

spring-batch-infrastructure/src/main/java/org/springframework/batch/infrastructure/item/json/JsonFileItemWriter.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,14 @@
1616

1717
package org.springframework.batch.infrastructure.item.json;
1818

19+
import java.io.File;
20+
import java.io.IOException;
21+
import java.io.RandomAccessFile;
1922
import java.util.Iterator;
2023

2124
import org.springframework.batch.infrastructure.item.Chunk;
25+
import org.springframework.batch.infrastructure.item.ExecutionContext;
26+
import org.springframework.batch.infrastructure.item.ItemStreamException;
2227
import org.springframework.batch.infrastructure.item.support.AbstractFileItemWriter;
2328
import org.springframework.core.io.WritableResource;
2429
import org.springframework.util.Assert;
@@ -46,6 +51,7 @@
4651
* @param <T> type of object to write as json representation
4752
* @author Mahmoud Ben Hassine
4853
* @author Jimmy Praet
54+
* @author Yanming Zhou
4955
* @since 4.1
5056
*/
5157
public class JsonFileItemWriter<T> extends AbstractFileItemWriter<T> {
@@ -58,6 +64,8 @@ public class JsonFileItemWriter<T> extends AbstractFileItemWriter<T> {
5864

5965
private JsonObjectMarshaller<T> jsonObjectMarshaller;
6066

67+
private boolean hasExistingItems;
68+
6169
/**
6270
* Create a new {@link JsonFileItemWriter} instance.
6371
* @param resource to write json data to
@@ -91,10 +99,27 @@ public void setJsonObjectMarshaller(JsonObjectMarshaller<T> jsonObjectMarshaller
9199
this.jsonObjectMarshaller = jsonObjectMarshaller;
92100
}
93101

102+
@Override
103+
public void open(ExecutionContext executionContext) throws ItemStreamException {
104+
try {
105+
if (this.append && this.resource != null && this.resource.exists() && this.resource.contentLength() > 0) {
106+
this.hasExistingItems = reopen(this.resource.getFile());
107+
}
108+
}
109+
catch (IOException ex) {
110+
throw new ItemStreamException(ex.getMessage(), ex);
111+
}
112+
super.open(executionContext);
113+
}
114+
94115
@SuppressWarnings("DataFlowIssue")
95116
@Override
96117
public String doWrite(Chunk<? extends T> items) {
97118
StringBuilder lines = new StringBuilder();
119+
if (this.hasExistingItems) {
120+
lines.append(JSON_OBJECT_SEPARATOR).append(this.lineSeparator);
121+
this.hasExistingItems = false;
122+
}
98123
Iterator<? extends T> iterator = items.iterator();
99124
if (!items.isEmpty() && state.getLinesWritten() > 0) {
100125
lines.append(JSON_OBJECT_SEPARATOR).append(this.lineSeparator);
@@ -109,4 +134,19 @@ public String doWrite(Chunk<? extends T> items) {
109134
return lines.toString();
110135
}
111136

137+
private boolean reopen(File file) throws IOException {
138+
long length = file.length();
139+
// try to delete lineSeparator + JSON_ARRAY_STOP + lineSeparator
140+
long pos = length - (1 + 2L * this.lineSeparator.length());
141+
if (pos <= 0) {
142+
return false;
143+
}
144+
try (RandomAccessFile raf = new RandomAccessFile(file, "rw")) {
145+
raf.setLength(pos);
146+
// file content is not empty or empty JSON array
147+
// (JSON_ARRAY_START + 2 * lineSeparator + JSON_ARRAY_STOP + lineSeparator)
148+
return length > 2 + 3L * this.lineSeparator.length();
149+
}
150+
}
151+
112152
}

spring-batch-infrastructure/src/test/java/org/springframework/batch/infrastructure/item/json/JsonFileItemWriterTests.java

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,18 +25,18 @@
2525
import org.mockito.Mock;
2626
import org.mockito.Mockito;
2727
import org.mockito.junit.jupiter.MockitoExtension;
28+
import tools.jackson.databind.json.JsonMapper;
2829

2930
import org.springframework.batch.infrastructure.item.Chunk;
3031
import org.springframework.batch.infrastructure.item.ExecutionContext;
31-
import org.springframework.batch.infrastructure.item.json.JsonFileItemWriter;
32-
import org.springframework.batch.infrastructure.item.json.JsonObjectMarshaller;
3332
import org.springframework.core.io.FileSystemResource;
3433
import org.springframework.core.io.WritableResource;
3534

36-
import static org.junit.jupiter.api.Assertions.assertThrows;
35+
import static org.junit.jupiter.api.Assertions.*;
3736

3837
/**
3938
* @author Mahmoud Ben Hassine
39+
* @author Yanming Zhou
4040
*/
4141
@ExtendWith(MockitoExtension.class)
4242
class JsonFileItemWriterTests {
@@ -49,6 +49,7 @@ class JsonFileItemWriterTests {
4949
@BeforeEach
5050
void setUp() throws Exception {
5151
File file = Files.createTempFile("test", "json").toFile();
52+
file.deleteOnExit();
5253
this.resource = new FileSystemResource(file);
5354
}
5455

@@ -72,4 +73,73 @@ void itemsShouldBeMarshalledToJsonWithTheJsonObjectMarshaller() throws Exception
7273
Mockito.verify(this.jsonObjectMarshaller).marshal("bar");
7374
}
7475

76+
@Test
77+
void appendAllowed() throws Exception {
78+
JsonFileItemWriter<String> writer = new JsonFileItemWriter<>(this.resource,
79+
new JacksonJsonObjectMarshaller<>());
80+
writer.setAppendAllowed(true);
81+
82+
writer.open(new ExecutionContext());
83+
writer.close();
84+
85+
resourceShouldContains();
86+
87+
writer.open(new ExecutionContext());
88+
writer.write(Chunk.of("aaa"));
89+
writer.write(Chunk.of("bbb"));
90+
writer.close();
91+
92+
resourceShouldContains("aaa", "bbb");
93+
94+
writer.open(new ExecutionContext());
95+
writer.close();
96+
97+
resourceShouldContains("aaa", "bbb");
98+
99+
writer.open(new ExecutionContext());
100+
writer.write(Chunk.of("ccc"));
101+
writer.close();
102+
103+
resourceShouldContains("aaa", "bbb", "ccc");
104+
}
105+
106+
@Test
107+
void appendNotAllowed() throws Exception {
108+
JsonFileItemWriter<String> writer = new JsonFileItemWriter<>(this.resource,
109+
new JacksonJsonObjectMarshaller<>());
110+
111+
writer.open(new ExecutionContext());
112+
writer.close();
113+
114+
resourceShouldContains();
115+
116+
writer.open(new ExecutionContext());
117+
writer.write(Chunk.of("aaa"));
118+
writer.write(Chunk.of("bbb"));
119+
writer.close();
120+
121+
resourceShouldContains("aaa", "bbb");
122+
123+
writer.open(new ExecutionContext());
124+
writer.close();
125+
126+
resourceShouldContains();
127+
128+
writer.open(new ExecutionContext());
129+
writer.write(Chunk.of("ccc"));
130+
writer.close();
131+
132+
resourceShouldContains("ccc");
133+
134+
writer.open(new ExecutionContext());
135+
writer.write(Chunk.of("ddd"));
136+
writer.close();
137+
138+
resourceShouldContains("ddd");
139+
}
140+
141+
private void resourceShouldContains(String... array) throws Exception {
142+
assertArrayEquals(array, new JsonMapper().readValue(this.resource.getContentAsByteArray(), String[].class));
143+
}
144+
75145
}

0 commit comments

Comments
 (0)