/* * Copyright 2006-2007 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.emonocot.job.io; import java.io.BufferedWriter; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.UnsupportedEncodingException; import java.io.Writer; import java.nio.channels.FileChannel; import java.util.List; import java.util.Map; import javax.xml.stream.XMLEventFactory; import javax.xml.stream.XMLEventWriter; import javax.xml.stream.XMLOutputFactory; import javax.xml.stream.XMLStreamException; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.batch.item.ExecutionContext; import org.springframework.batch.item.ItemStreamException; import org.springframework.batch.item.ItemWriter; import org.springframework.batch.item.WriteFailedException; import org.springframework.batch.item.file.ResourceAwareItemWriterItemStream; import org.springframework.batch.item.util.ExecutionContextUserSupport; import org.springframework.batch.item.util.FileUtils; import org.springframework.batch.item.xml.StaxWriterCallback; import org.springframework.batch.item.xml.stax.NoStartEndDocumentStreamWriter; import org.springframework.batch.support.transaction.TransactionAwareBufferedWriter; import org.springframework.beans.factory.InitializingBean; import org.springframework.core.io.Resource; import org.springframework.dao.DataAccessResourceFailureException; import org.springframework.oxm.Marshaller; import org.springframework.oxm.XmlMappingException; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import org.springframework.util.xml.StaxUtils; /** * An implementation of {@link ItemWriter} which uses StAX and * {@link Marshaller} for serializing object to XML. * * This item writer also provides restart, statistics and transaction features * by implementing corresponding interfaces. * * The implementation is *not* thread-safe. * * @author Peter Zozom * @author Robert Kasanicky * @param <T> the type of item written */ public class StaxEventItemWriter<T> extends ExecutionContextUserSupport implements ResourceAwareItemWriterItemStream<T>, InitializingBean { /** * */ private static Log log = LogFactory.getLog(StaxEventItemWriter.class); /** * default encoding. */ private static final String DEFAULT_ENCODING = "UTF-8"; /** * default encoding. */ private static final String DEFAULT_XML_VERSION = "1.0"; /** * default root tag name. */ private static final String DEFAULT_ROOT_TAG_NAME = "root"; /** * restart data property name. */ private static final String RESTART_DATA_NAME = "position"; /** * restart data property name. */ private static final String WRITE_STATISTICS_NAME = "record.count"; /** * file system resource. */ private Resource resource; /** * xml marshaller. */ private Marshaller marshaller; /** * encoding to be used while reading from the resource. */ private String encoding = DEFAULT_ENCODING; /** * XML version. */ private String version = DEFAULT_XML_VERSION; /** * name of the root tag. */ private String rootTagName = DEFAULT_ROOT_TAG_NAME; /** * namespace prefix of the root tag. */ private String rootTagNamespacePrefix = ""; /** * namespace of the root tag. */ private String rootTagNamespace = ""; /** * root element attributes. */ private Map<String, String> rootElementAttributes = null; /** * TRUE means, that output file will be overwritten if exists - default is * TRUE. */ private boolean overwriteOutput = true; /** * file channel. */ private FileChannel channel; /** * wrapper for XML event writer that swallows StartDocument and EndDocument * events. */ private XMLEventWriter eventWriter; /** * XML event writer. */ private XMLEventWriter delegateEventWriter; /** * current count of processed records. */ private long currentRecordCount = 0; /** * */ private boolean saveState = true; /** * */ private StaxWriterCallback headerCallback; /** * */ private StaxWriterCallback footerCallback; /** * */ private Writer bufferedWriter; /** * */ private boolean transactional = true; /** * */ public StaxEventItemWriter() { setName(ClassUtils.getShortName(StaxEventItemWriter.class)); } /** * Set output file. * * @param newResource * the output file */ public final void setResource(final Resource newResource) { this.resource = newResource; } /** * Set Object to XML marshaller. * * @param newMarshaller * the Object to XML marshaller */ public final void setMarshaller(final Marshaller newMarshaller) { this.marshaller = newMarshaller; } /** * @param newHeaderCallback is called before writing any items. */ public final void setHeaderCallback( final StaxWriterCallback newHeaderCallback) { this.headerCallback = newHeaderCallback; } /** * @param newFooterCallback * is called after writing all items but before closing the file */ public final void setFooterCallback( final StaxWriterCallback newFooterCallback) { this.footerCallback = newFooterCallback; } /** * Flag to indicate that writes should be deferred to the end of a * transaction if present. Defaults to true. * * @param isTransactional * the flag to set */ public final void setTransactional(final boolean isTransactional) { this.transactional = isTransactional; } /** * Get used encoding. * * @return the encoding used */ public final String getEncoding() { return encoding; } /** * Set encoding to be used for output file. * * @param newEncoding * the encoding to be used */ public final void setEncoding(final String newEncoding) { this.encoding = newEncoding; } /** * Get XML version. * * @return the XML version used */ public final String getVersion() { return version; } /** * Set XML version to be used for output XML. * * @param newVersion * the XML version to be used */ public final void setVersion(final String newVersion) { this.version = newVersion; } /** * Get the tag name of the root element. * * @return the root element tag name */ public final String getRootTagName() { return rootTagName; } /** * Set the tag name of the root element. If not set, default name is used * ("root"). Namespace URI and prefix can also be set optionally using the * notation: * * <pre> * {uri}prefix:root * </pre> * * The prefix is optional (defaults to empty), but if it is specified then * the uri must be provided. In addition you might want to declare other * namespaces using the {@link #setRootElementAttributes(Map) root * attributes}. * * @param newRootTagName * the tag name to be used for the root element */ public final void setRootTagName(final String newRootTagName) { this.rootTagName = newRootTagName; } /** * Get the namespace prefix of the root element. Empty by default. * * @return the rootTagNamespacePrefix */ public final String getRootTagNamespacePrefix() { return rootTagNamespacePrefix; } /** * Get the namespace of the root element. * * @return the rootTagNamespace */ public final String getRootTagNamespace() { return rootTagNamespace; } /** * Get attributes of the root element. * * @return attributes of the root element */ public final Map<String, String> getRootElementAttributes() { return rootElementAttributes; } /** * Set the root element attributes to be written. If any of the key names * begin with "xmlns:" then they are treated as namespace declarations. * * @param newRootElementAttributes * attributes of the root element */ public final void setRootElementAttributes( final Map<String, String> newRootElementAttributes) { this.rootElementAttributes = newRootElementAttributes; } /** * Set "overwrite" flag for the output file. Flag is ignored when output * file processing is restarted. * * @param doOverwriteOutput Overwrite the output */ public final void setOverwriteOutput(final boolean doOverwriteOutput) { this.overwriteOutput = doOverwriteOutput; } /** * * @param doSaveState Save the state */ public final void setSaveState(final boolean doSaveState) { this.saveState = doSaveState; } /** * @throws Exception if there is a problem initializing the writer */ public final void afterPropertiesSet() throws Exception { Assert.notNull(marshaller); if (rootTagName.contains("{")) { rootTagNamespace = rootTagName.replaceAll("\\{(.*)\\}.*", "$1"); rootTagName = rootTagName.replaceAll("\\{.*\\}(.*)", "$1"); if (rootTagName.contains(":")) { rootTagNamespacePrefix = rootTagName .replaceAll("(.*):.*", "$1"); rootTagName = rootTagName.replaceAll(".*:(.*)", "$1"); } } } /** * Open the output source. * @param newExecutionContext Set the execution context * @see org.springframework.batch.item.ItemStream#open(ExecutionContext) */ public final void open(final ExecutionContext newExecutionContext) { Assert.notNull(resource, "The resource must be set"); long startAtPosition = 0; boolean restarted = false; // if restart data is provided, restart from provided offset // otherwise start from beginning if (newExecutionContext.containsKey(getKey(RESTART_DATA_NAME))) { startAtPosition = newExecutionContext .getLong(getKey(RESTART_DATA_NAME)); restarted = true; } open(startAtPosition, restarted); if (startAtPosition == 0) { try { if (headerCallback != null) { headerCallback.write(delegateEventWriter); } } catch (IOException e) { throw new ItemStreamException("Failed to write headerItems", e); } } } /** * Helper method for opening output source at given file position. * @param position Set the position * @param restarted Is this execution being restarted */ private void open(final long position, final boolean restarted) { File file; FileOutputStream os = null; try { file = resource.getFile(); FileUtils.setUpOutputFile(file, restarted, overwriteOutput); Assert.state(resource.exists(), "Output resource must exist"); os = new FileOutputStream(file, true); channel = os.getChannel(); setPosition(position); } catch (IOException ioe) { throw new DataAccessResourceFailureException( "Unable to write to file resource: [" + resource + "]", ioe); } XMLOutputFactory outputFactory = XMLOutputFactory.newInstance(); if (outputFactory .isPropertySupported("com.ctc.wstx.automaticEndElements")) { // If the current XMLOutputFactory implementation is supplied by // Woodstox >= 3.2.9 we want to disable its // automatic end element feature (see: // http://jira.codehaus.org/browse/WSTX-165) per // http://jira.springframework.org/browse/BATCH-761. outputFactory.setProperty("com.ctc.wstx.automaticEndElements", Boolean.FALSE); } try { if (transactional) { bufferedWriter = new TransactionAwareBufferedWriter( new OutputStreamWriter(os, encoding), new Runnable() { public void run() { closeStream(); } }); } else { bufferedWriter = new BufferedWriter(new OutputStreamWriter(os, encoding)); } delegateEventWriter = outputFactory .createXMLEventWriter(bufferedWriter); eventWriter = new NoStartEndDocumentStreamWriter( delegateEventWriter); if (!restarted) { startDocument(delegateEventWriter); } } catch (XMLStreamException xse) { throw new DataAccessResourceFailureException( "Unable to write to file resource: [" + resource + "]", xse); } catch (UnsupportedEncodingException e) { throw new DataAccessResourceFailureException( "Unable to write to file resource: [" + resource + "] with encoding=[" + encoding + "]", e); } } /** * Writes simple XML header containing: * <ul> * <li>xml declaration - defines encoding and XML version</li> * <li>opening tag of the root element and its attributes</li> * </ul> * If this is not sufficient for you, simply override this method. Encoding, * version and root tag name can be retrieved with corresponding getters. * * @param writer * XML event writer * @throws XMLStreamException if there is a problem starting the document */ protected final void startDocument(final XMLEventWriter writer) throws XMLStreamException { XMLEventFactory factory = XMLEventFactory.newInstance(); // write start document writer.add(factory.createStartDocument(getEncoding(), getVersion())); // write root tag writer.add(factory.createStartElement(getRootTagNamespacePrefix(), getRootTagNamespace(), getRootTagName())); if (StringUtils.hasText(getRootTagNamespace())) { if (StringUtils.hasText(getRootTagNamespacePrefix())) { writer.add(factory.createNamespace(getRootTagNamespacePrefix(), getRootTagNamespace())); } else { writer.add(factory.createNamespace(getRootTagNamespace())); } } // write root tag attributes if (!CollectionUtils.isEmpty(getRootElementAttributes())) { for (Map.Entry<String, String> entry : getRootElementAttributes() .entrySet()) { String key = entry.getKey(); if (key.startsWith("xmlns")) { String prefix = ""; if (key.contains(":")) { prefix = key.substring(key.indexOf(":") + 1); } writer.add( factory.createNamespace(prefix, entry.getValue())); } else { writer.add(factory.createAttribute(key, entry.getValue())); } } } /* * This forces the flush to write the end of the root element and avoids * an off-by-one error on restart. */ writer.add(factory.createIgnorableSpace("")); writer.flush(); } /** * Writes the EndDocument tag manually. * * @param writer * XML event writer * @throws XMLStreamException if there is a problem ending the document */ protected final void endDocument(final XMLEventWriter writer) throws XMLStreamException { // writer.writeEndDocument(); <- this doesn't work after restart // we need to write end tag of the root element manually String nsPrefix = null; if (!StringUtils.hasText(getRootTagNamespacePrefix())) { nsPrefix = ""; } else { nsPrefix = getRootTagNamespacePrefix() + ":"; } try { bufferedWriter.write("</" + nsPrefix + getRootTagName() + ">"); } catch (IOException ioe) { throw new DataAccessResourceFailureException( "Unable to close file resource: [" + resource + "]", ioe); } } /** * Flush and close the output source. * * @see org.springframework.batch.item.ItemStream#close() */ public final void close() { // harmless event to close the root tag if there were no items XMLEventFactory factory = XMLEventFactory.newInstance(); try { delegateEventWriter.add(factory.createCharacters("")); } catch (XMLStreamException e) { log.error(e); } try { if (footerCallback != null) { footerCallback.write(delegateEventWriter); } delegateEventWriter.flush(); endDocument(delegateEventWriter); } catch (IOException e) { throw new ItemStreamException("Failed to write footer items", e); } catch (XMLStreamException e) { throw new ItemStreamException( "Failed to write end document tag", e); } finally { try { eventWriter.close(); } catch (XMLStreamException e) { log.error("Unable to close file resource: [" + resource + "] " + e); } finally { try { bufferedWriter.close(); } catch (IOException e) { log.error("Unable to close file resource: [" + resource + "] " + e); } finally { if (!transactional) { closeStream(); } } } } } /** * */ private void closeStream() { try { channel.close(); } catch (IOException ioe) { log.error("Unable to close file resource: [" + resource + "] " + ioe); } } /** * Write the value objects and flush them to the file. * * @param items * the value object * @throws IOException * if there is a problem writing to the resource */ public final void write(final List<? extends T> items) throws IOException { currentRecordCount += items.size(); for (Object object : items) { Assert.state(marshaller.supports(object.getClass()), "Marshaller must support the class of the marshalled object"); try { marshaller.marshal(object, StaxUtils.createStaxResult(eventWriter)); } catch (XmlMappingException e) { throw new IOException(e.getMessage()); } catch (XMLStreamException e) { throw new IOException(e.getMessage()); } } try { eventWriter.flush(); } catch (XMLStreamException e) { throw new WriteFailedException("Failed to flush the events", e); } } /** * @param executionContext Set the execution context * Get the restart data. * * @see org.springframework.batch.item.ItemStream#update(ExecutionContext) */ public final void update(final ExecutionContext executionContext) { if (saveState) { Assert.notNull(executionContext, "ExecutionContext must not be null"); executionContext.putLong(getKey(RESTART_DATA_NAME), getPosition()); executionContext.putLong(getKey(WRITE_STATISTICS_NAME), currentRecordCount); } } /** * Get the actual position in file channel. This method flushes any buffered * data before position is read. * * @return byte offset in file channel */ private long getPosition() { long position; try { eventWriter.flush(); position = channel.position(); if (bufferedWriter instanceof TransactionAwareBufferedWriter) { position += ((TransactionAwareBufferedWriter) bufferedWriter) .getBufferSize(); } } catch (Exception e) { throw new DataAccessResourceFailureException( "Unable to write to file resource: [" + resource + "]", e); } return position; } /** * Set the file channel position. * * @param newPosition * new file channel position */ private void setPosition(final long newPosition) { try { channel.truncate(newPosition); channel.position(newPosition); } catch (IOException e) { throw new DataAccessResourceFailureException( "Unable to write to file resource: [" + resource + "]", e); } } }