View Javadoc

1   /*
2    * Copyright 2006-2007 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.file;
18  
19  import static org.junit.Assert.assertEquals;
20  import static org.junit.Assert.assertFalse;
21  import static org.junit.Assert.assertNotNull;
22  import static org.junit.Assert.assertNull;
23  import static org.junit.Assert.assertTrue;
24  import static org.junit.Assert.fail;
25  
26  import java.io.BufferedReader;
27  import java.io.File;
28  import java.io.FileReader;
29  import java.io.IOException;
30  import java.io.Writer;
31  import java.nio.charset.UnsupportedCharsetException;
32  import java.util.ArrayList;
33  import java.util.Arrays;
34  import java.util.Collections;
35  import java.util.List;
36  
37  import org.junit.After;
38  import org.junit.Before;
39  import org.junit.Test;
40  import org.springframework.batch.item.ExecutionContext;
41  import org.springframework.batch.item.ItemStreamException;
42  import org.springframework.batch.item.UnexpectedInputException;
43  import org.springframework.batch.item.file.transform.LineAggregator;
44  import org.springframework.batch.item.file.transform.PassThroughLineAggregator;
45  import org.springframework.batch.support.transaction.ResourcelessTransactionManager;
46  import org.springframework.core.io.FileSystemResource;
47  import org.springframework.core.io.Resource;
48  import org.springframework.transaction.PlatformTransactionManager;
49  import org.springframework.transaction.TransactionStatus;
50  import org.springframework.transaction.support.TransactionCallback;
51  import org.springframework.transaction.support.TransactionTemplate;
52  import org.springframework.util.ClassUtils;
53  
54  /**
55   * Tests of regular usage for {@link FlatFileItemWriter} Exception cases will be in separate TestCase classes with
56   * different <code>setUp</code> and <code>tearDown</code> methods
57   * 
58   * @author Robert Kasanicky
59   * @author Dave Syer
60   * 
61   */
62  public class FlatFileItemWriterTests {
63  
64  	// object under test
65  	private FlatFileItemWriter<String> writer = new FlatFileItemWriter<String>();
66  
67  	// String to be written into file by the FlatFileInputTemplate
68  	private static final String TEST_STRING = "FlatFileOutputTemplateTest-OutputData";
69  
70  	// temporary output file
71  	private File outputFile;
72  
73  	// reads the output file to check the result
74  	private BufferedReader reader;
75  
76  	private ExecutionContext executionContext;
77  
78  	/**
79  	 * Create temporary output file, define mock behaviour, set dependencies and initialize the object under test
80  	 */
81  	@Before
82  	public void setUp() throws Exception {
83  
84  		outputFile = File.createTempFile("flatfile-test-output-", ".tmp");
85  
86  		writer.setResource(new FileSystemResource(outputFile));
87  		writer.setLineSeparator("\n");
88  		writer.setLineAggregator(new PassThroughLineAggregator<String>());
89  		writer.afterPropertiesSet();
90  		writer.setSaveState(true);
91  		executionContext = new ExecutionContext();
92  	}
93  
94  	/**
95  	 * Release resources and delete the temporary output file
96  	 */
97  	@After
98  	public void tearDown() throws Exception {
99  		if (reader != null) {
100 			reader.close();
101 		}
102 		writer.close();
103 		outputFile.delete();
104 	}
105 
106 	/*
107 	 * Read a line from the output file, if the reader has not been created, recreate. This method is only necessary
108 	 * because running the tests in a UNIX environment locks the file if it's open for writing.
109 	 */
110 	private String readLine() throws IOException {
111 
112 		if (reader == null) {
113 			reader = new BufferedReader(new FileReader(outputFile));
114 		}
115 
116 		return reader.readLine();
117 	}
118 
119 	@Test
120 	public void testWriteWithMultipleOpen() throws Exception {
121 		writer.open(executionContext);
122 		writer.write(Collections.singletonList("test1"));
123 		writer.open(executionContext);
124 		writer.write(Collections.singletonList("test2"));
125 		assertEquals("test1", readLine());
126 		assertEquals("test2", readLine());
127 	}
128 
129 	@Test
130 	public void testWriteWithDelete() throws Exception {
131 		writer.open(executionContext);
132 		writer.write(Collections.singletonList("test1"));
133 		writer.close();
134 		assertEquals("test1", readLine());
135 		reader = null;
136 		writer.setShouldDeleteIfExists(true);
137 		writer.open(executionContext);
138 		writer.write(Collections.singletonList("test2"));
139 		assertEquals("test2", readLine());
140 	}
141 
142 	@Test
143 	public void testWriteWithAppend() throws Exception {
144 		writer.setAppendAllowed(true);
145 		writer.open(executionContext);
146 		writer.write(Collections.singletonList("test1"));
147 		writer.close();
148 		assertEquals("test1", readLine());
149 		reader = null;
150 		writer.open(executionContext);
151 		writer.write(Collections.singletonList("test2"));
152 		assertEquals("test1", readLine());
153 		assertEquals("test2", readLine());
154 	}
155 
156 	@Test
157 	public void testWriteWithAppendRestartOnSecondChunk() throws Exception {
158 		writer.setAppendAllowed(true);
159 		writer.open(executionContext);
160 		writer.write(Collections.singletonList("test1"));
161 		writer.close();
162 		assertEquals("test1", readLine());
163 		reader = null;
164 		writer.open(executionContext);
165 		writer.write(Collections.singletonList(TEST_STRING));
166 		writer.update(executionContext);
167 		writer.write(Collections.singletonList(TEST_STRING));
168 		writer.close();
169 		assertEquals("test1", readLine());
170 		assertEquals(TEST_STRING, readLine());
171 		assertEquals(TEST_STRING, readLine());
172 		assertEquals(null, readLine());
173 		writer.open(executionContext);
174 		writer.write(Collections.singletonList(TEST_STRING));
175 		writer.close();
176 		reader = null;
177 		assertEquals("test1", readLine());
178 		assertEquals(TEST_STRING, readLine());
179 		assertEquals(TEST_STRING, readLine());
180 		assertEquals(null, readLine());
181 	}
182 
183 	@Test
184 	public void testOpenTwice() {
185 		// opening the writer twice should cause no issues
186 		writer.open(executionContext);
187 		writer.open(executionContext);
188 	}
189 
190 	/**
191 	 * Regular usage of <code>write(String)</code> method
192 	 * 
193 	 * @throws Exception
194 	 */
195 	@Test
196 	public void testWriteString() throws Exception {
197 		writer.open(executionContext);
198 		writer.write(Collections.singletonList(TEST_STRING));
199 		writer.close();
200 		String lineFromFile = readLine();
201 
202 		assertEquals(TEST_STRING, lineFromFile);
203 	}
204 
205 	@Test
206 	public void testForcedWriteString() throws Exception {
207 		writer.setForceSync(true);
208 		writer.open(executionContext);
209 		writer.write(Collections.singletonList(TEST_STRING));
210 		writer.close();
211 		String lineFromFile = readLine();
212 
213 		assertEquals(TEST_STRING, lineFromFile);
214 	}
215 
216 	/**
217 	 * Regular usage of <code>write(String)</code> method
218 	 * 
219 	 * @throws Exception
220 	 */
221 	@Test
222 	public void testWriteWithConverter() throws Exception {
223 		writer.setLineAggregator(new LineAggregator<String>() {
224 			public String aggregate(String item) {
225 				return "FOO:" + item;
226 			}
227 		});
228 		String data = "string";
229 		writer.open(executionContext);
230 		writer.write(Collections.singletonList(data));
231 		String lineFromFile = readLine();
232 		// converter not used if input is String
233 		assertEquals("FOO:" + data, lineFromFile);
234 	}
235 
236 	/**
237 	 * Regular usage of <code>write(String)</code> method
238 	 * 
239 	 * @throws Exception
240 	 */
241 	@Test
242 	public void testWriteWithConverterAndString() throws Exception {
243 		writer.setLineAggregator(new LineAggregator<String>() {
244 			public String aggregate(String item) {
245 				return "FOO:" + item;
246 			}
247 		});
248 		writer.open(executionContext);
249 		writer.write(Collections.singletonList(TEST_STRING));
250 		String lineFromFile = readLine();
251 		assertEquals("FOO:" + TEST_STRING, lineFromFile);
252 	}
253 
254 	/**
255 	 * Regular usage of <code>write(String[], LineDescriptor)</code> method
256 	 * 
257 	 * @throws Exception
258 	 */
259 	@Test
260 	public void testWriteRecord() throws Exception {
261 		writer.open(executionContext);
262 		writer.write(Collections.singletonList("1"));
263 		String lineFromFile = readLine();
264 		assertEquals("1", lineFromFile);
265 	}
266 
267 	@Test
268 	public void testWriteRecordWithrecordSeparator() throws Exception {
269 		writer.setLineSeparator("|");
270 		writer.open(executionContext);
271 		writer.write(Arrays.asList(new String[] { "1", "2" }));
272 		String lineFromFile = readLine();
273 		assertEquals("1|2|", lineFromFile);
274 	}
275 
276 	@Test
277 	public void testRestart() throws Exception {
278 
279 		writer.setFooterCallback(new FlatFileFooterCallback() {
280 
281 			public void writeFooter(Writer writer) throws IOException {
282 				writer.write("footer");
283 			}
284 
285 		});
286 
287 		writer.open(executionContext);
288 		// write some lines
289 		writer.write(Arrays.asList(new String[] { "testLine1", "testLine2", "testLine3" }));
290 		// write more lines
291 		writer.write(Arrays.asList(new String[] { "testLine4", "testLine5" }));
292 		// get restart data
293 		writer.update(executionContext);
294 		// close template
295 		writer.close();
296 
297 		// init with correct data
298 		writer.open(executionContext);
299 		// write more lines
300 		writer.write(Arrays.asList(new String[] { "testLine6", "testLine7", "testLine8" }));
301 		// get statistics
302 		writer.update(executionContext);
303 		// close template
304 		writer.close();
305 
306 		// verify what was written to the file
307 		for (int i = 1; i <= 8; i++) {
308 			assertEquals("testLine" + i, readLine());
309 		}
310 
311 		assertEquals("footer", readLine());
312 
313 		// 3 lines were written to the file after restart
314 		assertEquals(3, executionContext.getLong(ClassUtils.getShortName(FlatFileItemWriter.class) + ".written"));
315 
316 	}
317 
318 	@Test
319 	public void testWriteStringTransactional() throws Exception {
320 		writeStringTransactionCheck(null);
321 		assertEquals(TEST_STRING, readLine());
322 	}
323 
324 	@Test
325 	public void testWriteStringNotTransactional() throws Exception {
326 		writer.setTransactional(false);
327 		writeStringTransactionCheck(TEST_STRING);
328 	}
329 
330 	private void writeStringTransactionCheck(final String expectedInTransaction) {
331 		PlatformTransactionManager transactionManager = new ResourcelessTransactionManager();
332 
333 		writer.open(executionContext);
334 		new TransactionTemplate(transactionManager).execute(new TransactionCallback() {
335 			public Object doInTransaction(TransactionStatus status) {
336 				try {
337 					writer.write(Collections.singletonList(TEST_STRING));
338 					assertEquals(expectedInTransaction, readLine());
339 				}
340 				catch (Exception e) {
341 					throw new UnexpectedInputException("Could not write data", e);
342 				}
343 
344 				return null;
345 			}
346 		});
347 		writer.close();
348 	}
349 
350 	@Test
351 	public void testTransactionalRestart() throws Exception {
352 
353 		writer.setFooterCallback(new FlatFileFooterCallback() {
354 
355 			public void writeFooter(Writer writer) throws IOException {
356 				writer.write("footer");
357 			}
358 
359 		});
360 
361 		writer.open(executionContext);
362 
363 		PlatformTransactionManager transactionManager = new ResourcelessTransactionManager();
364 
365 		new TransactionTemplate(transactionManager).execute(new TransactionCallback() {
366 			public Object doInTransaction(TransactionStatus status) {
367 				try {
368 					// write some lines
369 					writer.write(Arrays.asList(new String[] { "testLine1", "testLine2", "testLine3" }));
370 					// write more lines
371 					writer.write(Arrays.asList(new String[] { "testLine4", "testLine5" }));
372 				}
373 				catch (Exception e) {
374 					throw new UnexpectedInputException("Could not write data", e);
375 				}
376 				// get restart data
377 				writer.update(executionContext);
378 				return null;
379 			}
380 		});
381 		// close template
382 		writer.close();
383 
384 		// init with correct data
385 		writer.open(executionContext);
386 
387 		new TransactionTemplate(transactionManager).execute(new TransactionCallback() {
388 			public Object doInTransaction(TransactionStatus status) {
389 				try {
390 					// write more lines
391 					writer.write(Arrays.asList(new String[] { "testLine6", "testLine7", "testLine8" }));
392 				}
393 				catch (Exception e) {
394 					throw new UnexpectedInputException("Could not write data", e);
395 				}
396 				// get restart data
397 				writer.update(executionContext);
398 				return null;
399 			}
400 		});
401 		// close template
402 		writer.close();
403 
404 		// verify what was written to the file
405 		for (int i = 1; i <= 8; i++) {
406 			assertEquals("testLine" + i, readLine());
407 		}
408 
409 		assertEquals("footer", readLine());
410 
411 		// 3 lines were written to the file after restart
412 		assertEquals(3, executionContext.getLong(ClassUtils.getShortName(FlatFileItemWriter.class) + ".written"));
413 
414 	}
415 
416 	@Test
417 	public void testOpenWithNonWritableFile() throws Exception {
418 		writer = new FlatFileItemWriter<String>();
419 		writer.setLineAggregator(new PassThroughLineAggregator<String>());
420 		FileSystemResource file = new FileSystemResource("target/no-such-file.foo");
421 		writer.setResource(file);
422 		new File(file.getFile().getParent()).mkdirs();
423 		file.getFile().createNewFile();
424 		assertTrue("Test file must exist: " + file, file.exists());
425 		assertTrue("Test file set to read-only: " + file, file.getFile().setReadOnly());
426 		assertFalse("Should be readonly file: " + file, file.getFile().canWrite());
427 		writer.afterPropertiesSet();
428 		try {
429 			writer.open(executionContext);
430 			fail("Expected IllegalStateException");
431 		}
432 		catch (IllegalStateException e) {
433 			String message = e.getMessage();
434 			assertTrue("Message does not contain 'writable': " + message, message.indexOf("writable") >= 0);
435 		}
436 	}
437 
438 	@Test
439 	public void testAfterPropertiesSetChecksMandatory() throws Exception {
440 		writer = new FlatFileItemWriter<String>();
441 		try {
442 			writer.afterPropertiesSet();
443 			fail("Expected IllegalArgumentException");
444 		}
445 		catch (IllegalArgumentException e) {
446 			// expected
447 		}
448 	}
449 
450 	@Test
451 	public void testDefaultStreamContext() throws Exception {
452 		writer = new FlatFileItemWriter<String>();
453 		writer.setResource(new FileSystemResource(outputFile));
454 		writer.setLineAggregator(new PassThroughLineAggregator<String>());
455 		writer.afterPropertiesSet();
456 		writer.setSaveState(true);
457 		writer.open(executionContext);
458 		writer.update(executionContext);
459 		assertNotNull(executionContext);
460 		assertEquals(2, executionContext.entrySet().size());
461 		assertEquals(0, executionContext.getLong(ClassUtils.getShortName(FlatFileItemWriter.class) + ".current.count"));
462 	}
463 
464 	@Test
465 	public void testWriteStringWithBogusEncoding() throws Exception {
466 		writer.setTransactional(false);
467 		writer.setEncoding("BOGUS");
468 		// writer.setShouldDeleteIfEmpty(true);
469 		try {
470 			writer.open(executionContext);
471 			fail("Expected ItemStreamException");
472 		}
473 		catch (ItemStreamException e) {
474 			assertTrue(e.getCause() instanceof UnsupportedCharsetException);
475 		}
476 		writer.close();
477 		// Try and write after the exception on open:
478 		writer.setEncoding("UTF-8");
479 		writer.open(executionContext);
480 		writer.write(Collections.singletonList(TEST_STRING));
481 	}
482 
483 	@Test
484 	public void testWriteStringWithEncodingAfterClose() throws Exception {
485 		writer.open(executionContext);
486 		writer.write(Collections.singletonList(TEST_STRING));
487 		writer.close();
488 		writer.setEncoding("UTF-8");
489 		writer.open(executionContext);
490 		writer.write(Collections.singletonList(TEST_STRING));
491 		String lineFromFile = readLine();
492 
493 		assertEquals(TEST_STRING, lineFromFile);
494 	}
495 
496 	@Test
497 	public void testWriteFooter() throws Exception {
498 		writer.setFooterCallback(new FlatFileFooterCallback() {
499 
500 			public void writeFooter(Writer writer) throws IOException {
501 				writer.write("a\nb");
502 			}
503 
504 		});
505 		writer.open(executionContext);
506 		writer.write(Collections.singletonList(TEST_STRING));
507 		writer.close();
508 		assertEquals(TEST_STRING, readLine());
509 		assertEquals("a", readLine());
510 		assertEquals("b", readLine());
511 	}
512 
513 	@Test
514 	public void testWriteHeader() throws Exception {
515 		writer.setHeaderCallback(new FlatFileHeaderCallback() {
516 
517 			public void writeHeader(Writer writer) throws IOException {
518 				writer.write("a\nb");
519 			}
520 
521 		});
522 		writer.open(executionContext);
523 		writer.write(Collections.singletonList(TEST_STRING));
524 		writer.close();
525 		String lineFromFile = readLine();
526 		assertEquals("a", lineFromFile);
527 		lineFromFile = readLine();
528 		assertEquals("b", lineFromFile);
529 		lineFromFile = readLine();
530 		assertEquals(TEST_STRING, lineFromFile);
531 	}
532 
533 	@Test
534 	public void testWriteWithAppendAfterHeaders() throws Exception {
535 		writer.setHeaderCallback(new FlatFileHeaderCallback() {
536 			public void writeHeader(Writer writer) throws IOException {
537 				writer.write("a\nb");
538 			}
539 
540 		});
541 		writer.setAppendAllowed(true);
542 		writer.open(executionContext);
543 		writer.write(Collections.singletonList("test1"));
544 		writer.close();
545 		assertEquals("a", readLine());
546 		assertEquals("b", readLine());
547 		assertEquals("test1", readLine());
548 		reader = null;
549 		writer.open(executionContext);
550 		writer.write(Collections.singletonList("test2"));
551 		assertEquals("a", readLine());
552 		assertEquals("b", readLine());
553 		assertEquals("test1", readLine());
554 		assertEquals("test2", readLine());
555 	}
556 
557 	@Test
558 	public void testWriteHeaderAndDeleteOnExit() throws Exception {
559 		writer.setHeaderCallback(new FlatFileHeaderCallback() {
560 
561 			public void writeHeader(Writer writer) throws IOException {
562 				writer.write("a\nb");
563 			}
564 
565 		});
566 		writer.setShouldDeleteIfEmpty(true);
567 		writer.open(executionContext);
568 		assertTrue(outputFile.exists());
569 		writer.close();
570 		assertFalse(outputFile.exists());
571 	}
572 
573 	@Test
574 	public void testDeleteOnExitReopen() throws Exception {
575 		writer.setShouldDeleteIfEmpty(true);
576 		writer.open(executionContext);
577 		assertTrue(outputFile.exists());
578 		writer.close();
579 		assertFalse(outputFile.exists());
580 		writer.open(executionContext);
581 		writer.write(Collections.singletonList("test2"));
582 		assertEquals("test2", readLine());
583 	}
584 
585 	@Test
586 	public void testWriteHeaderAfterRestartOnFirstChunk() throws Exception {
587 		writer.setHeaderCallback(new FlatFileHeaderCallback() {
588 
589 			public void writeHeader(Writer writer) throws IOException {
590 				writer.write("a\nb");
591 			}
592 
593 		});
594 		writer.open(executionContext);
595 		writer.write(Collections.singletonList(TEST_STRING));
596 		writer.close();
597 		writer.open(executionContext);
598 		writer.write(Collections.singletonList(TEST_STRING));
599 		writer.close();
600 		String lineFromFile = readLine();
601 		assertEquals("a", lineFromFile);
602 		lineFromFile = readLine();
603 		assertEquals("b", lineFromFile);
604 		lineFromFile = readLine();
605 		assertEquals(TEST_STRING, lineFromFile);
606 		lineFromFile = readLine();
607 		assertEquals(null, lineFromFile);
608 	}
609 
610 	@Test
611 	public void testWriteHeaderAfterRestartOnSecondChunk() throws Exception {
612 		writer.setHeaderCallback(new FlatFileHeaderCallback() {
613 
614 			public void writeHeader(Writer writer) throws IOException {
615 				writer.write("a\nb");
616 			}
617 
618 		});
619 		writer.open(executionContext);
620 		writer.write(Collections.singletonList(TEST_STRING));
621 		writer.update(executionContext);
622 		writer.write(Collections.singletonList(TEST_STRING));
623 		writer.close();
624 		String lineFromFile = readLine();
625 		assertEquals("a", lineFromFile);
626 		lineFromFile = readLine();
627 		assertEquals("b", lineFromFile);
628 		lineFromFile = readLine();
629 		assertEquals(TEST_STRING, lineFromFile);
630 		writer.open(executionContext);
631 		writer.write(Collections.singletonList(TEST_STRING));
632 		writer.close();
633 		reader = null;
634 		lineFromFile = readLine();
635 		assertEquals("a", lineFromFile);
636 		lineFromFile = readLine();
637 		assertEquals("b", lineFromFile);
638 		lineFromFile = readLine();
639 		assertEquals(TEST_STRING, lineFromFile);
640 		lineFromFile = readLine();
641 		assertEquals(TEST_STRING, lineFromFile);
642 	}
643 
644 	@Test
645 	/*
646 	 * Nothing gets written to file if line aggregation fails.
647 	 */
648 	public void testLineAggregatorFailure() throws Exception {
649 
650 		writer.setLineAggregator(new LineAggregator<String>() {
651 
652 			public String aggregate(String item) {
653 				if (item.equals("2")) {
654 					throw new RuntimeException("aggregation failed on " + item);
655 				}
656 				return item;
657 			}
658 		});
659 		List<String> items = new ArrayList<String>() {
660 			{
661 				add("1");
662 				add("2");
663 				add("3");
664 			}
665 		};
666 
667 		writer.open(executionContext);
668 		try {
669 			writer.write(items);
670 			fail();
671 		}
672 		catch (RuntimeException expected) {
673 			assertEquals("aggregation failed on 2", expected.getMessage());
674 		}
675 
676 		// nothing was written to output
677 		assertNull(readLine());
678 	}
679 
680 	@Test
681 	/**
682 	 * If append=true a new output file should still be created on the first run (not restart).
683 	 */
684 	public void testAppendToNotYetExistingFile() throws Exception {
685 		Resource toBeCreated = new FileSystemResource("target/FlatFileItemWriterTests.out");
686 		
687 		outputFile = toBeCreated.getFile(); //enable easy content reading and auto-delete the file 
688 		
689 		assertFalse("output file does not exist yet", toBeCreated.exists());
690 		writer.setResource(toBeCreated);
691 		writer.setAppendAllowed(true);
692 		writer.afterPropertiesSet();
693 
694 		writer.open(executionContext);
695 		assertTrue("output file was created", toBeCreated.exists());
696 
697 		writer.write(Collections.singletonList("test1"));
698 		writer.close();
699 		assertEquals("test1", readLine());
700 	}
701 }