Skip to content

Commit 7eafb4f

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 7eafb4f

File tree

2 files changed

+77
-3
lines changed

2 files changed

+77
-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+
super.open(executionContext);
105+
if (this.append && this.resource != null && this.resource.exists()) {
106+
try {
107+
this.hasExistingItems = reopen(this.resource.getFile());
108+
}
109+
catch (IOException ex) {
110+
throw new ItemStreamException(ex.getMessage(), ex);
111+
}
112+
}
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 JSON_OBJECT_SEPARATOR + lineSeparator + JSON_ARRAY_STOP
140+
long pos = length - (2 + 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: 37 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 {
@@ -72,4 +72,38 @@ void itemsShouldBeMarshalledToJsonWithTheJsonObjectMarshaller() throws Exception
7272
Mockito.verify(this.jsonObjectMarshaller).marshal("bar");
7373
}
7474

75+
@Test
76+
void appendAllowed() throws Exception {
77+
JsonFileItemWriter<String> writer = new JsonFileItemWriter<>(this.resource,
78+
new JacksonJsonObjectMarshaller<>());
79+
writer.setAppendAllowed(true);
80+
81+
writer.open(new ExecutionContext());
82+
writer.close();
83+
84+
resourceShouldContains();
85+
86+
writer.open(new ExecutionContext());
87+
writer.write(Chunk.of("aaa"));
88+
writer.write(Chunk.of("bbb"));
89+
writer.close();
90+
91+
resourceShouldContains("aaa", "bbb");
92+
93+
writer.open(new ExecutionContext());
94+
writer.close();
95+
96+
resourceShouldContains("aaa", "bbb");
97+
98+
writer.open(new ExecutionContext());
99+
writer.write(Chunk.of("ccc"));
100+
writer.close();
101+
102+
resourceShouldContains("aaa", "bbb", "ccc");
103+
}
104+
105+
private void resourceShouldContains(String... array) throws Exception {
106+
assertArrayEquals(array, new JsonMapper().readValue(this.resource.getContentAsByteArray(), String[].class));
107+
}
108+
75109
}

0 commit comments

Comments
 (0)