View Javadoc

1   package org.springframework.batch.item.xml;
2   
3   import static org.easymock.EasyMock.createMock;
4   import static org.easymock.EasyMock.expect;
5   import static org.easymock.EasyMock.replay;
6   import static org.junit.Assert.assertEquals;
7   import static org.junit.Assert.assertFalse;
8   import static org.junit.Assert.assertTrue;
9   import static org.junit.Assert.fail;
10  
11  import java.io.File;
12  import java.io.IOException;
13  import java.util.Collections;
14  import java.util.List;
15  
16  import javax.xml.stream.XMLEventFactory;
17  import javax.xml.stream.XMLEventWriter;
18  import javax.xml.stream.XMLStreamException;
19  import javax.xml.transform.Result;
20  
21  import org.apache.commons.io.FileUtils;
22  import org.junit.Before;
23  import org.junit.Test;
24  import org.springframework.batch.item.ExecutionContext;
25  import org.springframework.batch.item.UnexpectedInputException;
26  import org.springframework.batch.support.transaction.ResourcelessTransactionManager;
27  import org.springframework.core.io.FileSystemResource;
28  import org.springframework.core.io.Resource;
29  import org.springframework.oxm.Marshaller;
30  import org.springframework.oxm.XmlMappingException;
31  import org.springframework.transaction.PlatformTransactionManager;
32  import org.springframework.transaction.TransactionStatus;
33  import org.springframework.transaction.support.TransactionCallback;
34  import org.springframework.transaction.support.TransactionTemplate;
35  import org.springframework.util.Assert;
36  import org.springframework.util.ClassUtils;
37  import org.springframework.util.StringUtils;
38  
39  /**
40   * Tests for {@link StaxEventItemWriter}.
41   */
42  public class StaxEventItemWriterTests {
43  
44  	// object under test
45  	private StaxEventItemWriter<Object> writer;
46  
47  	// output file
48  	private Resource resource;
49  
50  	private ExecutionContext executionContext;
51  
52  	// test item for writing to output
53  	private Object item = new Object() {
54  		public String toString() {
55  			return ClassUtils.getShortName(StaxEventItemWriter.class) + "-testString";
56  		}
57  	};
58  
59  	private List<? extends Object> items = Collections.singletonList(item);
60  
61  	private static final String TEST_STRING = "<" + ClassUtils.getShortName(StaxEventItemWriter.class)
62  			+ "-testString/>";
63  
64  	private static final String NS_TEST_STRING = "<ns:" + ClassUtils.getShortName(StaxEventItemWriter.class)
65  			+ "-testString/>";
66  
67  	private static final String FOO_TEST_STRING = "<foo:" + ClassUtils.getShortName(StaxEventItemWriter.class)
68  			+ "-testString/>";
69  
70  	private SimpleMarshaller marshaller;
71  
72  	@Before
73  	public void setUp() throws Exception {
74  		File directory = new File("target/data");
75  		directory.mkdirs();
76  		resource = new FileSystemResource(File.createTempFile("StaxEventWriterOutputSourceTests", ".xml", directory));
77  		writer = createItemWriter();
78  		executionContext = new ExecutionContext();
79  	}
80  
81  	/**
82  	 * Item is written to the output file only after flush.
83  	 */
84  	@Test
85  	public void testWriteAndFlush() throws Exception {
86  		writer.open(executionContext);
87  		writer.write(items);
88  		writer.close();
89  		String content = getOutputFileContent();
90  		assertTrue("Wrong content: " + content, content.contains(TEST_STRING));
91  	}
92  
93  	@Test
94  	public void testWriteAndForceFlush() throws Exception {
95  		writer.setForceSync(true);
96  		writer.open(executionContext);
97  		writer.write(items);
98  		writer.close();
99  		String content = getOutputFileContent();
100 		assertTrue("Wrong content: " + content, content.contains(TEST_STRING));
101 	}
102 
103 	/**
104 	 * Restart scenario - content is appended to the output file after restart.
105 	 */
106 	@Test
107 	public void testRestart() throws Exception {
108 		writer.open(executionContext);
109 		// write item
110 		writer.write(items);
111 		writer.update(executionContext);
112 		writer.close();
113 
114 		// create new writer from saved restart data and continue writing
115 		writer = createItemWriter();
116 		writer.open(executionContext);
117 		writer.write(items);
118 		writer.write(items);
119 		writer.close();
120 
121 		// check the output is concatenation of 'before restart' and 'after
122 		// restart' writes.
123 		String outputFile = getOutputFileContent();
124 		assertEquals(3, StringUtils.countOccurrencesOf(outputFile, TEST_STRING));
125 		assertEquals("<root>" + TEST_STRING + TEST_STRING + TEST_STRING + "</root>", outputFile.replace(" ", ""));
126 	}
127 
128 	@Test
129 	public void testTransactionalRestart() throws Exception {
130 		writer.open(executionContext);
131 
132 		PlatformTransactionManager transactionManager = new ResourcelessTransactionManager();
133 
134 		new TransactionTemplate(transactionManager).execute(new TransactionCallback() {
135 			public Object doInTransaction(TransactionStatus status) {
136 				try {
137 					// write item
138 					writer.write(items);
139 				}
140 				catch (Exception e) {
141 					throw new UnexpectedInputException("Could not write data", e);
142 				}
143 				// get restart data
144 				writer.update(executionContext);
145 				return null;
146 			}
147 		});
148 		writer.close();
149 
150 		// create new writer from saved restart data and continue writing
151 		writer = createItemWriter();
152 		writer.open(executionContext);
153 		new TransactionTemplate(transactionManager).execute(new TransactionCallback() {
154 			public Object doInTransaction(TransactionStatus status) {
155 				try {
156 					writer.write(items);
157 				}
158 				catch (Exception e) {
159 					throw new UnexpectedInputException("Could not write data", e);
160 				}
161 				// get restart data
162 				writer.update(executionContext);
163 				return null;
164 			}
165 		});
166 		writer.close();
167 
168 		// check the output is concatenation of 'before restart' and 'after
169 		// restart' writes.
170 		String outputFile = getOutputFileContent();
171 		assertEquals(2, StringUtils.countOccurrencesOf(outputFile, TEST_STRING));
172 		assertTrue(outputFile.contains("<root>" + TEST_STRING + TEST_STRING + "</root>"));
173 	}
174 
175 	@Test
176 	public void testTransactionalRestartFailOnFirstWrite() throws Exception {
177 
178 		PlatformTransactionManager transactionManager = new ResourcelessTransactionManager();
179 
180 		writer.open(executionContext);
181 		try {
182 			new TransactionTemplate(transactionManager).execute(new TransactionCallback() {
183 				public Object doInTransaction(TransactionStatus status) {
184 					try {
185 						writer.write(items);
186 					}
187 					catch (Exception e) {
188 						throw new IllegalStateException("Could not write data", e);
189 					}
190 					throw new UnexpectedInputException("Could not write data");
191 				}
192 			});
193 		}
194 		catch (UnexpectedInputException e) {
195 			// expected
196 		}
197 		writer.close();
198 		System.err.println(getOutputFileContent());
199 		String outputFile = getOutputFileContent();
200 		assertEquals("<root></root>", outputFile);
201 
202 		// create new writer from saved restart data and continue writing
203 		writer = createItemWriter();
204 		new TransactionTemplate(transactionManager).execute(new TransactionCallback() {
205 			public Object doInTransaction(TransactionStatus status) {
206 				writer.open(executionContext);
207 				try {
208 					writer.write(items);
209 				}
210 				catch (Exception e) {
211 					throw new UnexpectedInputException("Could not write data", e);
212 				}
213 				// get restart data
214 				writer.update(executionContext);
215 				return null;
216 			}
217 		});
218 		writer.close();
219 
220 		// check the output is concatenation of 'before restart' and 'after
221 		// restart' writes.
222 		outputFile = getOutputFileContent();
223 		System.err.println(getOutputFileContent());
224 		assertEquals(1, StringUtils.countOccurrencesOf(outputFile, TEST_STRING));
225 		assertTrue(outputFile.contains("<root>" + TEST_STRING + "</root>"));
226 		assertEquals("<root><StaxEventItemWriter-testString/></root>", outputFile);
227 	}
228 
229 	/**
230 	 * Item is written to the output file only after flush.
231 	 */
232 	@Test
233 	public void testWriteWithHeader() throws Exception {
234 
235 		writer.setHeaderCallback(new StaxWriterCallback() {
236 
237 			public void write(XMLEventWriter writer) throws IOException {
238 				XMLEventFactory factory = XMLEventFactory.newInstance();
239 				try {
240 					writer.add(factory.createStartElement("", "", "header"));
241 					writer.add(factory.createEndElement("", "", "header"));
242 				}
243 				catch (XMLStreamException e) {
244 					throw new RuntimeException(e);
245 				}
246 
247 			}
248 
249 		});
250 		writer.open(executionContext);
251 		writer.write(items);
252 		String content = getOutputFileContent();
253 		assertTrue("Wrong content: " + content, content.contains(("<header/>")));
254 		assertTrue("Wrong content: " + content, content.contains(TEST_STRING));
255 	}
256 
257 	/**
258 	 * Count of 'records written so far' is returned as statistics.
259 	 */
260 	@Test
261 	public void testStreamContext() throws Exception {
262 		writer.open(executionContext);
263 		final int NUMBER_OF_RECORDS = 10;
264 		assertFalse(executionContext.containsKey(ClassUtils.getShortName(StaxEventItemWriter.class) + ".record.count"));
265 		for (int i = 1; i <= NUMBER_OF_RECORDS; i++) {
266 			writer.write(items);
267 			writer.update(executionContext);
268 			long writeStatistics = executionContext.getLong(ClassUtils.getShortName(StaxEventItemWriter.class)
269 					+ ".record.count");
270 
271 			assertEquals(i, writeStatistics);
272 		}
273 	}
274 
275 	/**
276 	 * Open method writes the root tag, close method adds corresponding end tag.
277 	 */
278 	@Test
279 	public void testOpenAndClose() throws Exception {
280 		writer.setHeaderCallback(new StaxWriterCallback() {
281 
282 			public void write(XMLEventWriter writer) throws IOException {
283 				XMLEventFactory factory = XMLEventFactory.newInstance();
284 				try {
285 					writer.add(factory.createStartElement("", "", "header"));
286 					writer.add(factory.createEndElement("", "", "header"));
287 				}
288 				catch (XMLStreamException e) {
289 					throw new RuntimeException(e);
290 				}
291 
292 			}
293 
294 		});
295 		writer.setFooterCallback(new StaxWriterCallback() {
296 
297 			public void write(XMLEventWriter writer) throws IOException {
298 				XMLEventFactory factory = XMLEventFactory.newInstance();
299 				try {
300 					writer.add(factory.createStartElement("", "", "footer"));
301 					writer.add(factory.createEndElement("", "", "footer"));
302 				}
303 				catch (XMLStreamException e) {
304 					throw new RuntimeException(e);
305 				}
306 
307 			}
308 
309 		});
310 		writer.setRootTagName("testroot");
311 		writer.setRootElementAttributes(Collections.<String, String> singletonMap("attribute", "value"));
312 		writer.open(executionContext);
313 		writer.close();
314 		String content = getOutputFileContent();
315 
316 		assertTrue(content.contains("<testroot attribute=\"value\">"));
317 		assertTrue(content.contains("<header/>"));
318 		assertTrue(content.contains("<footer/>"));
319 		assertTrue(content.endsWith("</testroot>"));
320 	}
321 
322 	@Test
323 	public void testNonExistantResource() throws Exception {
324 		Resource doesntExist = createMock(Resource.class);
325 		expect(doesntExist.getFile()).andReturn(File.createTempFile("arbitrary", null));
326 		expect(doesntExist.exists()).andReturn(false);
327 		replay(doesntExist);
328 
329 		writer.setResource(doesntExist);
330 
331 		try {
332 			writer.open(executionContext);
333 			fail();
334 		}
335 		catch (IllegalStateException e) {
336 			assertEquals("Output resource must exist", e.getMessage());
337 		}
338 	}
339 
340 	/**
341 	 * Item is written to the output file with namespace.
342 	 */
343 	@Test
344 	public void testWriteRootTagWithNamespace() throws Exception {
345 		writer.setRootTagName("{http://www.springframework.org/test}root");
346 		writer.afterPropertiesSet();
347 		writer.open(executionContext);
348 		writer.write(items);
349 		writer.close();
350 		String content = getOutputFileContent();
351 		assertTrue("Wrong content: " + content, content
352 				.contains(("<root xmlns=\"http://www.springframework.org/test\">")));
353 		assertTrue("Wrong content: " + content, content.contains(TEST_STRING));
354 		assertTrue("Wrong content: " + content, content.contains(("</root>")));
355 	}
356 
357 	/**
358 	 * Item is written to the output file with namespace and prefix.
359 	 */
360 	@Test
361 	public void testWriteRootTagWithNamespaceAndPrefix() throws Exception {
362 		writer.setRootTagName("{http://www.springframework.org/test}ns:root");
363 		writer.afterPropertiesSet();
364 		marshaller.setNamespace(writer.getRootTagNamespace());
365 		marshaller.setNamespacePrefix(writer.getRootTagNamespacePrefix());
366 		writer.open(executionContext);
367 		writer.write(items);
368 		writer.close();
369 		String content = getOutputFileContent();
370 		assertTrue("Wrong content: " + content, content
371 				.contains(("<ns:root xmlns:ns=\"http://www.springframework.org/test\">")));
372 		assertTrue("Wrong content: " + content, content.contains(NS_TEST_STRING));
373 		assertTrue("Wrong content: " + content, content.contains(("</ns:root>")));
374 		assertTrue("Wrong content: " + content, content.contains(("<ns:root")));
375 	}
376 
377 	/**
378 	 * Item is written to the output file with additional namespaces and prefix.
379 	 */
380 	@Test
381 	public void testWriteRootTagWithAdditionalNamespace() throws Exception {
382 		writer.setRootTagName("{http://www.springframework.org/test}ns:root");
383 		marshaller.setNamespace("urn:org.test.foo");
384 		marshaller.setNamespacePrefix("foo");
385 		writer.setRootElementAttributes(Collections.singletonMap("xmlns:foo", "urn:org.test.foo"));
386 		writer.afterPropertiesSet();
387 		writer.open(executionContext);
388 		writer.write(items);
389 		writer.close();
390 		String content = getOutputFileContent();
391 		assertTrue("Wrong content: " + content, content
392 				.contains(("<ns:root xmlns:ns=\"http://www.springframework.org/test\" "
393 						+ "xmlns:foo=\"urn:org.test.foo\">")));
394 		assertTrue("Wrong content: " + content, content.contains(FOO_TEST_STRING));
395 		assertTrue("Wrong content: " + content, content.contains(("</ns:root>")));
396 		assertTrue("Wrong content: " + content, content.contains(("<ns:root")));
397 	}
398 
399 	/**
400 	 * Writes object's toString representation as XML comment.
401 	 */
402 	private static class SimpleMarshaller implements Marshaller {
403 
404 		private String namespacePrefix = "";
405 
406 		private String namespace = "";
407 
408 		public void setNamespace(String namespace) {
409 			this.namespace = namespace;
410 		}
411 
412 		public void setNamespacePrefix(String namespacePrefix) {
413 			this.namespacePrefix = namespacePrefix;
414 		}
415 
416 		public void marshal(Object graph, Result result) throws XmlMappingException, IOException {
417  	 	 Assert.isInstanceOf( Result.class, result);
418 			try {
419 				StaxUtils.getXmlEventWriter( result ).add( XMLEventFactory.newInstance().createStartElement(namespacePrefix, namespace, graph.toString()));
420 				StaxUtils.getXmlEventWriter( result ).add( XMLEventFactory.newInstance().createEndElement(namespacePrefix, namespace, graph.toString()));
421 			}
422 			catch ( Exception e) {
423 				throw new RuntimeException("Exception while writing to output file", e);
424 			}
425 		}
426 
427 		@SuppressWarnings("rawtypes")
428 		public boolean supports(Class clazz) {
429 			return true;
430 		}
431 	}
432 
433 	/**
434 	 * @return output file content as String
435 	 */
436 	private String getOutputFileContent() throws IOException {
437 		String value = FileUtils.readFileToString(resource.getFile(), null);
438 		value = value.replace("<?xml version='1.0' encoding='UTF-8'?>", "");
439 		return value;
440 	}
441 
442 	/**
443 	 * @return new instance of fully configured writer
444 	 */
445 	private StaxEventItemWriter<Object> createItemWriter() throws Exception {
446 		StaxEventItemWriter<Object> source = new StaxEventItemWriter<Object>();
447 		source.setResource(resource);
448 
449 		marshaller = new SimpleMarshaller();
450 		source.setMarshaller(marshaller);
451 
452 		source.setEncoding("UTF-8");
453 		source.setRootTagName("root");
454 		source.setVersion("1.0");
455 		source.setOverwriteOutput(true);
456 		source.setSaveState(true);
457 
458 		source.afterPropertiesSet();
459 
460 		return source;
461 	}
462 
463 }