Skip to content

Commit a187a1f

Browse files
quafffmbenhassine
authored andcommitted
Fix JsonFileItemWriter to produce valid JSON when append allowed
Closes GH-5272 Signed-off-by: Yanming Zhou <zhouyanming@gmail.com> --- Cherry-picked from 550c65f and adapted to v5.2.x: - Make WritableResource protected in AbstractFileItemWriter - Update test to use Jackson 2 APIs
1 parent 5a089da commit a187a1f

3 files changed

Lines changed: 137 additions & 2 deletions

File tree

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

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

1717
package org.springframework.batch.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.item.Chunk;
25+
import org.springframework.batch.item.ExecutionContext;
26+
import org.springframework.batch.item.ItemStreamException;
2227
import org.springframework.batch.item.support.AbstractFileItemWriter;
2328
import org.springframework.core.io.WritableResource;
2429
import org.springframework.util.Assert;
@@ -46,6 +51,7 @@
4651
* @see JacksonJsonObjectMarshaller
4752
* @param <T> type of object to write as json representation
4853
* @author Mahmoud Ben Hassine
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
@@ -93,9 +101,27 @@ public void setJsonObjectMarshaller(JsonObjectMarshaller<T> jsonObjectMarshaller
93101
this.jsonObjectMarshaller = jsonObjectMarshaller;
94102
}
95103

104+
@Override
105+
public void open(ExecutionContext executionContext) throws ItemStreamException {
106+
try {
107+
if (this.append && this.resource != null && this.resource.exists() && this.resource.contentLength() > 0) {
108+
reopen(this.resource.getFile());
109+
}
110+
}
111+
catch (IOException ex) {
112+
throw new ItemStreamException(ex.getMessage(), ex);
113+
}
114+
super.open(executionContext);
115+
}
116+
117+
@SuppressWarnings("DataFlowIssue")
96118
@Override
97119
public String doWrite(Chunk<? extends T> items) {
98120
StringBuilder lines = new StringBuilder();
121+
if (this.hasExistingItems) {
122+
lines.append(JSON_OBJECT_SEPARATOR).append(this.lineSeparator);
123+
this.hasExistingItems = false;
124+
}
99125
Iterator<? extends T> iterator = items.iterator();
100126
if (!items.isEmpty() && state.getLinesWritten() > 0) {
101127
lines.append(JSON_OBJECT_SEPARATOR).append(this.lineSeparator);
@@ -110,4 +136,23 @@ public String doWrite(Chunk<? extends T> items) {
110136
return lines.toString();
111137
}
112138

139+
private void reopen(File file) throws IOException {
140+
try (RandomAccessFile raf = new RandomAccessFile(file, "rw")) {
141+
long pos = raf.length();
142+
boolean stopFound = false;
143+
while (--pos >= 0) {
144+
raf.seek(pos);
145+
int current = raf.readByte();
146+
if (!stopFound && current == JSON_ARRAY_STOP) {
147+
stopFound = true;
148+
}
149+
else if (stopFound && !Character.isWhitespace(current)) {
150+
this.hasExistingItems = current != JSON_ARRAY_START;
151+
raf.setLength(this.hasExistingItems ? pos + 1 : pos);
152+
break;
153+
}
154+
}
155+
}
156+
}
157+
113158
}

spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/AbstractFileItemWriter.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ public abstract class AbstractFileItemWriter<T> extends AbstractItemStreamItemWr
8080

8181
private static final String RESTART_DATA_NAME = "current.count";
8282

83-
private WritableResource resource;
83+
protected WritableResource resource;
8484

8585
protected OutputState state = null;
8686

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

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.batch.item.json;
1818

1919
import java.io.File;
20+
import java.nio.charset.StandardCharsets;
2021
import java.nio.file.Files;
2122

2223
import org.junit.jupiter.api.BeforeEach;
@@ -25,16 +26,18 @@
2526
import org.mockito.Mock;
2627
import org.mockito.Mockito;
2728
import org.mockito.junit.jupiter.MockitoExtension;
29+
import com.fasterxml.jackson.databind.json.JsonMapper;
2830

2931
import org.springframework.batch.item.Chunk;
3032
import org.springframework.batch.item.ExecutionContext;
3133
import org.springframework.core.io.FileSystemResource;
3234
import org.springframework.core.io.WritableResource;
3335

34-
import static org.junit.jupiter.api.Assertions.assertThrows;
36+
import static org.junit.jupiter.api.Assertions.*;
3537

3638
/**
3739
* @author Mahmoud Ben Hassine
40+
* @author Yanming Zhou
3841
*/
3942
@ExtendWith(MockitoExtension.class)
4043
class JsonFileItemWriterTests {
@@ -47,6 +50,7 @@ class JsonFileItemWriterTests {
4750
@BeforeEach
4851
void setUp() throws Exception {
4952
File file = Files.createTempFile("test", "json").toFile();
53+
file.deleteOnExit();
5054
this.resource = new FileSystemResource(file);
5155
}
5256

@@ -75,4 +79,90 @@ void itemsShouldBeMarshalledToJsonWithTheJsonObjectMarshaller() throws Exception
7579
Mockito.verify(this.jsonObjectMarshaller).marshal("bar");
7680
}
7781

82+
@Test
83+
void appendAllowed() throws Exception {
84+
JsonFileItemWriter<String> writer = new JsonFileItemWriter<>(this.resource,
85+
new JacksonJsonObjectMarshaller<>());
86+
writer.setAppendAllowed(true);
87+
88+
writer.open(new ExecutionContext());
89+
writer.close();
90+
91+
resourceShouldContain();
92+
93+
writer.open(new ExecutionContext());
94+
writer.write(Chunk.of("aaa"));
95+
writer.write(Chunk.of("bbb"));
96+
writer.close();
97+
98+
resourceShouldContain("aaa", "bbb");
99+
100+
writer.open(new ExecutionContext());
101+
writer.close();
102+
103+
resourceShouldContain("aaa", "bbb");
104+
105+
writer.open(new ExecutionContext());
106+
writer.write(Chunk.of("ccc"));
107+
writer.close();
108+
109+
resourceShouldContain("aaa", "bbb", "ccc");
110+
}
111+
112+
@Test
113+
void appendAllowedWithUnformattedJson() throws Exception {
114+
JsonFileItemWriter<String> writer = new JsonFileItemWriter<>(this.resource,
115+
new JacksonJsonObjectMarshaller<>());
116+
writer.setAppendAllowed(true);
117+
118+
Files.writeString(this.resource.getFile().toPath(), "[\n \"foo\"]", StandardCharsets.UTF_8);
119+
120+
resourceShouldContain("foo");
121+
122+
writer.open(new ExecutionContext());
123+
writer.write(Chunk.of("bar"));
124+
writer.close();
125+
126+
resourceShouldContain("foo", "bar");
127+
}
128+
129+
@Test
130+
void appendNotAllowed() throws Exception {
131+
JsonFileItemWriter<String> writer = new JsonFileItemWriter<>(this.resource,
132+
new JacksonJsonObjectMarshaller<>());
133+
134+
writer.open(new ExecutionContext());
135+
writer.close();
136+
137+
resourceShouldContain();
138+
139+
writer.open(new ExecutionContext());
140+
writer.write(Chunk.of("aaa"));
141+
writer.write(Chunk.of("bbb"));
142+
writer.close();
143+
144+
resourceShouldContain("aaa", "bbb");
145+
146+
writer.open(new ExecutionContext());
147+
writer.close();
148+
149+
resourceShouldContain();
150+
151+
writer.open(new ExecutionContext());
152+
writer.write(Chunk.of("ccc"));
153+
writer.close();
154+
155+
resourceShouldContain("ccc");
156+
157+
writer.open(new ExecutionContext());
158+
writer.write(Chunk.of("ddd"));
159+
writer.close();
160+
161+
resourceShouldContain("ddd");
162+
}
163+
164+
private void resourceShouldContain(String... array) throws Exception {
165+
assertArrayEquals(array, new JsonMapper().readValue(this.resource.getContentAsByteArray(), String[].class));
166+
}
167+
78168
}

0 commit comments

Comments
 (0)