Add spring-batch- to module directory names (so folks can use mvn eclipse:eclipse if they want to).

BATCH-238: Remove hibernate support for the Daos.
This commit is contained in:
dsyer
2007-12-10 21:23:48 +00:00
parent 17705f27ab
commit 8ea331bfc7
884 changed files with 956 additions and 2352 deletions

View File

@@ -0,0 +1,77 @@
/*
* 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.springframework.batch.common;
import java.util.HashMap;
import java.util.Map;
/**
* A {@link ExceptionClassifier} that has only two classes of exception.
* Provides convenient methods for setting up and querying the classification
* with boolean return type.
*
* @author Dave Syer
*
*/
public class BinaryExceptionClassifier extends ExceptionClassifierSupport {
/**
* The classifier result for a non-default exception.
*/
public static final String NON_DEFAULT = "NON_DEFAULT";
private SubclassExceptionClassifier delegate = new SubclassExceptionClassifier();
/**
* Set the special exceptions. Any exception on the list, or subclasses
* thereof, will be classified as non-default.
*
* @param exceptionClasses defaults to {@link Exception}.
*/
public final void setExceptionClasses(Class[] exceptionClasses) {
Map temp = new HashMap();
for (int i = 0; i < exceptionClasses.length; i++) {
temp.put(exceptionClasses[i], NON_DEFAULT);
}
this.delegate.setTypeMap(temp);
}
/**
* Convenience method to return boolean if the throwable is classified as
* default.
*
* @param throwable the Throwable to classify
* @return true if it is default classified (i.e. not on the list provided
* in {@link #setExceptionClasses(Class[])}.
*/
public boolean isDefault(Throwable throwable) {
return classify(throwable).equals(DEFAULT);
}
/**
* Returns either {@link ExceptionClassifierSupport#DEFAULT} or
* {@link #NON_DEFAULT} depending on the type of the throwable. If the type
* of the throwable or one of its ancestors is on the exception class list
* the classification is as {@link #NON_DEFAULT}.
*
* @see #setExceptionClasses(Class[])
* @see ExceptionClassifierSupport#classify(Throwable)
*/
public Object classify(Throwable throwable) {
return delegate.classify(throwable);
}
}

View File

@@ -0,0 +1,45 @@
/*
* 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.springframework.batch.common;
/**
* Interface for a classifier of exceptions.
*
* @author Dave Syer
*
*/
public interface ExceptionClassifier {
/**
* Get a default value, normally the same as would be returned by
* {@link #classify(Throwable)} with null argument.
*
* @return the default value.
*/
Object getDefault();
/**
* Classify the given exception and return a non-null object. The return
* type depends on the implementation but typically would be a key in a map
* which the client maintains.
*
* @param throwable the input exception. Can be null.
* @return an object.
*/
Object classify(Throwable throwable);
}

View File

@@ -0,0 +1,51 @@
/*
* 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.springframework.batch.common;
/**
* Base class for {@link ExceptionClassifier} implementations. Provides default
* behaviour and some convenience members, like constants.
*
* @author Dave Syer
*
*/
public class ExceptionClassifierSupport implements ExceptionClassifier {
/**
* Default classification key.
*/
public static final String DEFAULT = "default";
/**
* Always returns the value of {@value #DEFAULT}.
*
* @see org.springframework.batch.common.ExceptionClassifier#classify(java.lang.Throwable)
*/
public Object classify(Throwable throwable) {
return DEFAULT;
}
/**
* Wrapper for a call to {@link #classify(Throwable)} with argument null.
*
* @see org.springframework.batch.common.ExceptionClassifier#getDefault()
*/
public Object getDefault() {
return classify(null);
}
}

View File

@@ -0,0 +1,113 @@
/*
* 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.springframework.batch.common;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import org.springframework.util.Assert;
/**
*
* @author Dave Syer
*
*/
public class SubclassExceptionClassifier extends ExceptionClassifierSupport {
private Map classified = new HashMap();
/**
* Map of Throwable class types to keys for the classifier. Any subclass of
* the type provided will be classified as of the type given by the
* correspinding map entry value.
*
* @param typeMap the typeMap to set
*/
public final void setTypeMap(Map typeMap) {
Map map = new HashMap();
for (Iterator iter = typeMap.entrySet().iterator(); iter.hasNext();) {
Map.Entry entry = (Map.Entry) iter.next();
addRetryableExceptionClass(entry.getKey(), entry.getValue(), map);
}
this.classified = map;
}
/**
* Return the value from the type map whose key is the class of the given
* Throwable, or its nearest ancestor if a subclass.
*
* @see org.springframework.batch.common.ExceptionClassifierSupport#classify(java.lang.Throwable)
*/
public Object classify(Throwable throwable) {
if (throwable == null) {
return super.classify(throwable);
}
Class exceptionClass = throwable.getClass();
if (classified.containsKey(exceptionClass)) {
return classified.get(exceptionClass);
}
// check for subclasses
Set classes = new TreeSet(new ClassComparator());
classes.addAll(classified.keySet());
for (Iterator iterator = classes.iterator(); iterator.hasNext();) {
Class cls = (Class) iterator.next();
if (cls.isAssignableFrom(exceptionClass)) {
Object value = classified.get(cls);
addRetryableExceptionClass(exceptionClass, value, this.classified);
return value;
}
}
return super.classify(throwable);
}
private void addRetryableExceptionClass(Object candidateClass, Object classifiedAs, Map map) {
Assert.isAssignable(Class.class, candidateClass.getClass());
Class exceptionClass = (Class) candidateClass;
Assert.isAssignable(Throwable.class, exceptionClass);
map.put(exceptionClass, classifiedAs);
}
/**
* Comparator for classes to order by inheritance.
*
* @author Dave Syer
*
*/
private class ClassComparator implements Comparator {
/**
* @return 1 if arg0 is assignable from arg1
* @return -1 otherwise
* @see java.util.Comparator#compare(java.lang.Object, java.lang.Object)
*/
public int compare(Object arg0, Object arg1) {
Class cls0 = (Class) arg0;
Class cls1 = (Class) arg1;
if (cls0.isAssignableFrom(cls1)) {
return 1;
}
return -1;
}
}
}

View File

@@ -0,0 +1,7 @@
<html>
<body>
<p>
Infrastructure implementations of common concerns.
</p>
</body>
</html>

View File

@@ -0,0 +1,37 @@
/*
* 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.springframework.batch.io;
/**
* Basic interface for generic input operations. Class implementing this
* interface will be responsible for reading records from input stream and also
* possibly for mapping these records to objects. Generally it is responsibility
* of implementing class to decide which technology to use for mapping and how
* it should be configured.
*
* @author Dave Syer
*/
public interface InputSource {
/**
* Read record from input stream and map it to an object.
*
* @return the value object
*/
public Object read();
}

View File

@@ -0,0 +1,36 @@
/*
* 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.springframework.batch.io;
/**
* Basic interface for generic output operations. Class implementing this
* interface will be responsible for serializing objects. Generally, it is
* responsibility of implementing class to decide which technology to use for
* mapping and how it should be configured.
*
* @author Dave Syer
*/
public interface ItemWriter {
/**
* Writes provided object to an output stream or similar.
*
* @param item
* the object to write.
*/
public void write(Object item);
}

View File

@@ -0,0 +1,39 @@
/*
* 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.springframework.batch.io;
/**
* Implementation of this interface indicates to the framework that this object
* is capable of skipping a record in cases where it cannot be processed because
* it is invalid, incomplete for other non critical reasons.
*
* @author Waseem Malik
* @author Dave Syer
*
*/
public interface Skippable {
/**
* Skip the current record. This method can be invoked whenever an input
* source provides an invalid object. The implementing class should skip the
* current record the next time it is encountered in the same process (e.g.
* after a rollback and retry of a transaction).
*
*/
public void skip();
}

View File

@@ -0,0 +1,263 @@
/*
* 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.springframework.batch.io.cursor;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import org.hibernate.ScrollableResults;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.StatelessSession;
import org.springframework.batch.io.InputSource;
import org.springframework.batch.io.Skippable;
import org.springframework.batch.item.ResourceLifecycle;
import org.springframework.batch.repeat.synch.BatchTransactionSynchronizationManager;
import org.springframework.batch.restart.GenericRestartData;
import org.springframework.batch.restart.RestartData;
import org.springframework.batch.restart.Restartable;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationAdapter;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
/**
* {@link InputSource} for reading database records built on top of Hibernate.
*
* It executes the HQL {@link #queryString} when initialized and iterates over
* the result set as {@link #read()} method is called, returning an object
* corresponding to current row.
*
* Input source can be configured to use either {@link StatelessSession}
* sufficient for simple mappings without the need to cascade to associated
* objects or standard hibernate {@link Session} for more advanced mappings or
* when caching is desired.
*
* When stateful session is used it will be cleared after successful commit
* without being flushed (no inserts or updates are expected).
*
* @author Robert Kasanicky
* @author Dave Syer
*/
public class HibernateCursorInputSource implements InputSource, Restartable,
Skippable, InitializingBean, DisposableBean, ResourceLifecycle {
private static final String RESTART_DATA_ROW_NUMBER_KEY = ClassUtils
.getShortName(HibernateCursorInputSource.class)
+ ".rowNumber";
private static final String SKIPPED_ROWS = ClassUtils
.getShortName(HibernateCursorInputSource.class)
+ ".skippedRows";;
private SessionFactory sessionFactory;
private StatelessSession statelessSession;
private Session statefulSession;
private ScrollableResults cursor;
private String queryString;
private boolean useStatelessSession = true;
private int lastCommitRowNumber = 0;
private final List skippedRows = new ArrayList();
private int skipCount = 0;
/* Current count of processed records. */
private int currentProcessedRow = 0;
private boolean initialized = false;
private TransactionSynchronization synchronization = new HibernateInputSourceTransactionSynchronization();
public Object read() {
if (!initialized) {
open();
}
if (cursor.next()) {
currentProcessedRow++;
if (!skippedRows.isEmpty()) {
// while is necessary to handle successive skips.
while (skippedRows.contains(new Integer(currentProcessedRow))) {
if (!cursor.next()) {
return null;
}
currentProcessedRow++;
}
}
Object data = cursor.get(0);
return data;
}
return null;
}
/**
* Closes the result set cursor and hibernate session.
*/
public void close() {
initialized = false;
cursor.close();
currentProcessedRow = 0;
skippedRows.clear();
skipCount = 0;
if (useStatelessSession) {
statelessSession.close();
} else {
statefulSession.close();
}
}
/**
* Creates cursor for the query.
*/
public void open() {
if (useStatelessSession) {
statelessSession = sessionFactory.openStatelessSession();
cursor = statelessSession.createQuery(queryString).scroll();
} else {
statefulSession = sessionFactory.openSession();
cursor = statefulSession.createQuery(queryString).scroll();
}
BatchTransactionSynchronizationManager
.registerSynchronization(synchronization);
initialized = true;
}
/**
* @param sessionFactory
* hibernate session factory
*/
public void setSessionFactory(SessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
}
public void afterPropertiesSet() throws Exception {
Assert.notNull(sessionFactory);
Assert.hasLength(queryString);
}
public void destroy() throws Exception {
close();
}
/**
* @param queryString
* HQL query string
*/
public void setQueryString(String queryString) {
this.queryString = queryString;
}
/**
* Can be set only in uninitialized state.
*
* @param useStatelessSession
* <code>true</code> to use {@link StatelessSession}
* <code>false</code> to use standard hibernate {@link Session}
*/
public void setUseStatelessSession(boolean useStatelessSession) {
Assert.state(!initialized);
this.useStatelessSession = useStatelessSession;
}
/**
* @return the current row number wrapped as <code>RestartData</code>
*/
public RestartData getRestartData() {
Properties props = new Properties();
props.setProperty(RESTART_DATA_ROW_NUMBER_KEY, ""+currentProcessedRow);
String skipped = skippedRows.toString();
props.setProperty(SKIPPED_ROWS, skipped.substring(1,
skipped.length() - 1));
return new GenericRestartData(props);
}
/**
* Sets the cursor to the received row number.
*/
public void restoreFrom(RestartData data) {
Assert
.state(!initialized,
"Cannot restore when already intialized. Call close() first before restore()");
Properties props = data.getProperties();
if (props.getProperty(RESTART_DATA_ROW_NUMBER_KEY) == null) {
return;
}
currentProcessedRow = Integer.parseInt(props
.getProperty(RESTART_DATA_ROW_NUMBER_KEY));
open();
cursor.setRowNumber(currentProcessedRow-1);
if (!props.containsKey(SKIPPED_ROWS)) {
return;
}
String[] skipped = StringUtils.commaDelimitedListToStringArray(props
.getProperty(SKIPPED_ROWS));
for (int i = 0; i < skipped.length; i++) {
this.skippedRows.add(new Integer(skipped[i]));
}
}
/**
* Skip the current row. If the transaction is rolled back, this row will
* not be represented when read() is called. For example, if you read in row
* 2, find the data to be bad, and call skip(), then continue processing and
* find
*/
public void skip() {
skippedRows.add(new Integer(currentProcessedRow));
skipCount++;
}
/**
* Encapsulates transaction events handling.
*/
private class HibernateInputSourceTransactionSynchronization extends
TransactionSynchronizationAdapter {
public void afterCompletion(int status) {
if (status == TransactionSynchronization.STATUS_ROLLED_BACK) {
currentProcessedRow = lastCommitRowNumber;
if (lastCommitRowNumber == 0) {
cursor.beforeFirst();
} else {
// Set the cursor so that next time it is advanced it will
// come back to the committed row.
cursor.setRowNumber(lastCommitRowNumber-1);
}
} else if (status == TransactionSynchronization.STATUS_COMMITTED) {
lastCommitRowNumber = currentProcessedRow;
if (!useStatelessSession) {
statefulSession.clear();
}
}
}
}
}

View File

@@ -0,0 +1,568 @@
/*
* 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.springframework.batch.io.cursor;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.SQLWarning;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import javax.sql.DataSource;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.batch.io.InputSource;
import org.springframework.batch.io.Skippable;
import org.springframework.batch.io.support.AbstractTransactionalIoSource;
import org.springframework.batch.item.ResourceLifecycle;
import org.springframework.batch.restart.GenericRestartData;
import org.springframework.batch.restart.RestartData;
import org.springframework.batch.restart.Restartable;
import org.springframework.batch.statistics.StatisticsProvider;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.InvalidDataAccessResourceUsageException;
import org.springframework.jdbc.SQLWarningException;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.support.JdbcUtils;
import org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator;
import org.springframework.jdbc.support.SQLExceptionTranslator;
import org.springframework.jdbc.support.SQLStateSQLExceptionTranslator;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* <p>
* Simple input source that opens a Sql Cursor and continually retrieves the
* next row in the ResultSet. It is extremely important to note that the
* JdbcDriver used must be version 3.0 or higher. This is because earlier
* versions do not support holding a ResultSet open over commits.
* </p>
*
* <p>
* Each call to read() will call the provided RowMapper, (NOTE: Calling read()
* without setting a RowMapper will result in an IllegalStateException!) passing
* in the ResultSet. If this is the first call to read(), the provided query
* will be run in order to open the cursor. There is currently no wrapping of
* the ResultSet to suppress calls to next(). However, if the RowMapper
* increments the current row, the next call to read will verify that the
* current row is at the expected position and throw a DataAccessException if it
* is not. This means that, in theory, a RowMapper could read ahead, as long as
* it returns the row back to it's correct position before returning. The reason
* for such strictness on the ResultSet is due to the need to maintain strict
* control for Transactions, restartability and skippability. This ensures that
* each call to read() returns the ResultSet at the correct line, regardless of
* rollbacks, restarts, or skips.
* </p>
*
* <p>
* Restart: This implementation contains basic, simple restart. The current row
* is returned as restart data, and when restored from that same data, the
* cursor is opened and the current row set to the value within the restart
* data.
* </p>
*
* <p>
* Statistics: There are two statistics returned by this input source: the
* current line being processed and the number of lines that have been skipped.
* </p>
*
* <p>
* Transactions: At first glance, it may appear odd that Spring's
* TransactionSynchronization abstraction is used for something that is reading
* from the database, however, it is important because the same resultset is
* held open regardless of commits or roll backs. This means that when a
* transaction is committed, the input source is notified so that it can save
* it's current row number. Later, if the transaction is rolled back, the
* current row can be moved back to the same row number as it was on when commit
* was called.
* </p>
*
* <p>
* Calling skip will indicate to the input source that a record is bad and
* should not be represented to the user if the transaction is rolled back. For
* example, if row 2 is read in, and found to be bad, calling skip will inform
* the Input Source. If reading is then continued, and a rollback is necessary
* because of an error on output, the input source will be returned to row 1.
* Calling read while on row 1 will move the current row to 3, not 2, because 2
* has been marked as skipped.
* </p>
*
* <p>
* Calling close on this Input Source will cause all resources it is currently
* using to be freed. (Connection, resultset, etc). If read() is called on the
* same instance again, the cursor will simply be reopened starting at row 0.
* </p>
*
* @author Lucas Ward
* @author Peter Zozom
*/
public class JdbcCursorInputSource extends AbstractTransactionalIoSource
implements InputSource, ResourceLifecycle, DisposableBean,
InitializingBean, Restartable, StatisticsProvider, Skippable {
private static Log log = LogFactory.getLog(JdbcCursorInputSource.class);
public static final int VALUE_NOT_SET = -1;
private static final String CURRENT_PROCESSED_ROW = "sqlCursorInput.lastProcessedRowNum";
private static final String SKIPPED_ROWS = "sqlCursorInput.skippedRows";
private static final String SKIP_COUNT = "sqlCursorInput.skippedRrecordCount";
private Connection con;
private Statement stmt;
protected ResultSet rs;
private DataSource dataSource;
private String sql;
private final List skippedRows = new ArrayList();
private int skipCount = 0;
private int fetchSize = VALUE_NOT_SET;
private int maxRows = VALUE_NOT_SET;
private int queryTimeout = VALUE_NOT_SET;
private boolean ignoreWarnings = true;
private boolean verifyCursorPosition = true;
private SQLExceptionTranslator exceptionTranslator;
/* Current count of processed records. */
private int currentProcessedRow = 0;
private int lastCommittedRow = 0;
private RowMapper mapper;
private boolean initialized = false;
/**
* Assert that mandatory properties are set.
*
* @throws IllegalArgumentException
* if either data source or sql properties not set.
*/
public void afterPropertiesSet() throws Exception {
Assert.notNull(dataSource, "DataSOurce must be provided");
Assert.notNull(sql, "The SQL query must be provided");
}
/**
* Public setter for the data source for injection purposes.
*
* @param dataSource
*/
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
/**
* Increment the cursor to the next row, validating the cursor position and
* passing the resultset to the RowMapper. If read has not been called on
* this instance before, the cursor will be opened. If there are skipped
* records for this commit scope, an internal list of skipped records will
* be checked to ensure that only a valid row is given to the mapper.
*
* @returns Object returned by RowMapper
* @throws DataAccessException
* @throws IllegalStateExceptino
* if mapper is null.
*/
public Object read() {
if (!initialized) {
open();
}
Assert.state(mapper != null, "Mapper must not be null.");
try {
if (!rs.next()) {
return null;
} else {
currentProcessedRow++;
if (!skippedRows.isEmpty()) {
// while is necessary to handle successive skips.
while (skippedRows
.contains(new Integer(currentProcessedRow))) {
if (!rs.next()) {
return null;
}
currentProcessedRow++;
}
}
Object mappedResult = mapper.mapRow(rs, currentProcessedRow);
verifyCursorPosition(currentProcessedRow);
return mappedResult;
}
} catch (SQLException se) {
throw getExceptionTranslator().translate(
"Trying to process next row", sql, se);
}
}
public int getCurrentProcessedRow() {
return currentProcessedRow;
}
/**
* Mark the current row. Calling reset will cause the result set to be set
* to the current row when mark was called.
*/
protected void transactionCommitted() {
lastCommittedRow = currentProcessedRow;
skippedRows.clear();
}
/**
* Set the ResultSet's current row to the last marked position.
*
* @throws DataAccessException
*/
protected void transactionRolledBack() {
try {
currentProcessedRow = lastCommittedRow;
if (currentProcessedRow > 0) {
rs.absolute(currentProcessedRow);
} else {
rs.beforeFirst();
}
} catch (SQLException se) {
throw getExceptionTranslator().translate(
"Attempted to move ResultSet to last committed row", sql,
se);
}
}
/**
* Close this input source. The ResultSet, Statement and Connection created
* will be closed. This must be called or the connection and cursor will be
* held open indefinitely!
*
* @see org.springframework.batch.item.ResourceLifecycle#close()
*/
public void close() {
initialized = false;
JdbcUtils.closeResultSet(this.rs);
JdbcUtils.closeStatement(this.stmt);
JdbcUtils.closeConnection(this.con);
this.currentProcessedRow = 0;
skippedRows.clear();
skipCount = 0;
}
/**
* Calls close to ensure that bean factories can close and always release
* resources.
*
* @see org.springframework.beans.factory.DisposableBean#destroy()
*/
public void destroy() throws Exception {
close();
}
// Check the result set is in synch with the currentRow attribute. This is
// important
// to ensure that the user hasn't modified the current row.
private void verifyCursorPosition(int expectedCurrentRow)
throws SQLException {
if (verifyCursorPosition) {
if (expectedCurrentRow != this.rs.getRow()) {
throw new InvalidDataAccessResourceUsageException(
"Unexpected cursor position change.");
}
}
}
/*
* Executes the provided SQL query. The statement is created with
* 'READ_ONLY' and 'HOLD_CUSORS_OVER_COMMIT' set to true. This is extremely
* important, since a non read-only cursor may lock tables that shouldn't be
* locked, and not holding the cursor open over a commit would require it to
* be reopened after each commit, which would destroy performance.
*/
private void executeQuery() {
Assert.state(dataSource != null, "DataSource must not be null.");
try {
this.con = dataSource.getConnection();
this.stmt = this.con.createStatement(
ResultSet.TYPE_SCROLL_INSENSITIVE,
ResultSet.CONCUR_READ_ONLY,
ResultSet.HOLD_CURSORS_OVER_COMMIT);
applyStatementSettings(this.stmt);
this.rs = this.stmt.executeQuery(sql);
handleWarnings(this.stmt.getWarnings());
} catch (SQLException se) {
close();
throw getExceptionTranslator()
.translate("Executing query", sql, se);
}
super.registerSynchronization();
}
/*
* Prepare the given JDBC Statement (or PreparedStatement or
* CallableStatement), applying statement settings such as fetch size, max
* rows, and query timeout. @param stmt the JDBC Statement to prepare
* @throws SQLException
*
* @see #setFetchSize
* @see #setMaxRows
* @see #setQueryTimeout
*/
private void applyStatementSettings(Statement stmt) throws SQLException {
if (fetchSize != VALUE_NOT_SET) {
stmt.setFetchSize(fetchSize);
stmt.setFetchDirection(ResultSet.FETCH_FORWARD);
}
if (maxRows != VALUE_NOT_SET) {
stmt.setMaxRows(maxRows);
}
if (queryTimeout != VALUE_NOT_SET) {
stmt.setQueryTimeout(queryTimeout);
}
}
/*
* Return the exception translator for this instance. <p>Creates a default
* SQLErrorCodeSQLExceptionTranslator for the specified DataSource if none
* is set.
*/
protected SQLExceptionTranslator getExceptionTranslator() {
if (exceptionTranslator == null) {
if (dataSource != null) {
exceptionTranslator = new SQLErrorCodeSQLExceptionTranslator(
dataSource);
} else {
exceptionTranslator = new SQLStateSQLExceptionTranslator();
}
}
return exceptionTranslator;
}
/*
* Throw a SQLWarningException if we're not ignoring warnings, else log the
* warnings (at debug level).
*
* @param warning the warnings object from the current statement. May be
* <code>null</code>, in which case this method does nothing.
*
* @see org.springframework.jdbc.SQLWarningException
*/
private void handleWarnings(SQLWarning warnings) throws SQLWarningException {
if (ignoreWarnings) {
SQLWarning warningToLog = warnings;
while (warningToLog != null) {
log.debug("SQLWarning ignored: SQL state '"
+ warningToLog.getSQLState() + "', error code '"
+ warningToLog.getErrorCode() + "', message ["
+ warningToLog.getMessage() + "]");
warningToLog = warningToLog.getNextWarning();
}
} else if (warnings != null) {
throw new SQLWarningException("Warning not ignored", warnings);
}
}
/*
* (non-Javadoc)
*
* @see org.springframework.batch.restart.Restartable#getRestartData()
*/
public RestartData getRestartData() {
String skipped = skippedRows.toString();
Properties statistics = getStatistics();
statistics.setProperty(SKIPPED_ROWS, skipped.substring(1,skipped.length()-1));
return new GenericRestartData(statistics);
}
/*
* (non-Javadoc)
*
* @see org.springframework.batch.restart.Restartable#restoreFrom(org.springframework.batch.restart.RestartData)
*/
public void restoreFrom(RestartData data) {
Assert.state(!initialized);
if (data == null)
return;
open();
Properties restartProperties = data.getProperties();
if (!restartProperties.containsKey(CURRENT_PROCESSED_ROW)) {
return;
}
try {
this.currentProcessedRow = Integer.parseInt(restartProperties
.getProperty(CURRENT_PROCESSED_ROW));
rs.absolute(currentProcessedRow);
} catch (SQLException se) {
throw getExceptionTranslator().translate(
"Attempted to move ResultSet to last committed row", sql,
se);
}
if (!restartProperties.containsKey(SKIPPED_ROWS)) {
return;
}
String[] skipped = StringUtils.commaDelimitedListToStringArray(restartProperties.getProperty(SKIPPED_ROWS));
for (int i = 0; i < skipped.length; i++) {
this.skippedRows.add(new Integer(skipped[i]));
}
}
/*
* (non-Javadoc)
*
* @see org.springframework.batch.statistics.StatisticsProvider#getStatistics()
*/
public Properties getStatistics() {
Properties props = new Properties();
props.setProperty(CURRENT_PROCESSED_ROW, new Integer(
currentProcessedRow).toString());
props.setProperty(SKIP_COUNT, new Integer(skipCount).toString());
return props;
}
/**
* Skip the current row. If the transaction is rolled back, this row will
* not be represented to the RowMapper when read() is called. For example,
* if you read in row 2, find the data to be bad, and call skip(), then
* continue processing and find
*/
public void skip() {
skippedRows.add(new Integer(currentProcessedRow));
skipCount++;
}
/**
* Gives the JDBC driver a hint as to the number of rows that should be
* fetched from the database when more rows are needed for this
* <code>ResultSet</code> object. If the fetch size specified is zero, the
* JDBC driver ignores the value.
*
* @param fetchSize
* the number of rows to fetch
* @see ResultSet#setFetchSize(int)
*/
public void setFetchSize(int fetchSize) {
this.fetchSize = fetchSize;
}
/**
* Sets the limit for the maximum number of rows that any
* <code>ResultSet</code> object can contain to the given number.
*
* @param maxRows
* the new max rows limit; zero means there is no limit
* @see Statement#setMaxRows(int)
*/
public void setMaxRows(int maxRows) {
this.maxRows = maxRows;
}
/**
* Sets the number of seconds the driver will wait for a
* <code>Statement</code> object to execute to the given number of
* seconds. If the limit is exceeded, an <code>SQLException</code> is
* thrown.
*
* @param queryTimeout
* seconds the new query timeout limit in seconds; zero means
* there is no limit
* @see Statement#setQueryTimeout(int)
*/
public void setQueryTimeout(int queryTimeout) {
this.queryTimeout = queryTimeout;
}
/**
* Set whether SQLWarnings should be ignored (only logged) or exception
* should be thrown.
*
* @param ignoreWarnings
* if TRUE, warnings are ignored
*/
public void setIgnoreWarnings(boolean ignoreWarnings) {
this.ignoreWarnings = ignoreWarnings;
}
/**
* Allow verification of cursor position after current row is processed by
* RowMapper or RowCallbackHandler. Default value is TRUE.
*
* @param verifyCursorPosition
* if true, cursor position is verified
*/
public void setVerifyCursorPosition(boolean verifyCursorPosition) {
this.verifyCursorPosition = verifyCursorPosition;
}
/**
* Set the RowMapper to be used for all calls to read().
*
* @param mapper
*/
public void setMapper(RowMapper mapper) {
this.mapper = mapper;
}
/**
* Set the sql statement to be used when creating the cursor. This statement
* should be a complete and valid Sql statement, as it will be run directly
* without any modification.
*
* @param sql
*/
public void setSql(String sql) {
this.sql = sql;
}
public void open() {
Assert.isNull(rs);
executeQuery();
initialized = true;
}
}

View File

@@ -0,0 +1,8 @@
<html>
<body>
<p>
Infrastructure implementations of cursor based input sources. All input sources within this package
open a cursor against the database, and return back a mapped object for each row.
</p>
</body>
</html>

View File

@@ -0,0 +1,207 @@
/*
* 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.springframework.batch.io.driving;
import java.util.Iterator;
import java.util.List;
import org.springframework.batch.io.InputSource;
import org.springframework.batch.io.support.AbstractTransactionalIoSource;
import org.springframework.batch.item.ResourceLifecycle;
import org.springframework.batch.repeat.synch.BatchTransactionSynchronizationManager;
import org.springframework.batch.restart.RestartData;
import org.springframework.batch.restart.Restartable;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;
/**
* <p>Abstract base class for driving query input sources. Input Sources of
* this type use a 'driving query' to return back a list of keys. Upon each
* call to read, a new key is returned.</p>
*
* <p>Mutability: Because this base class cannot guarantee that the keys returned
* by subclasses are immutable, care should be taken to not modify a key value.
* Doing so would cause issues if a rollback occurs. For example, if a call
* to read() is made, and the returned key is modified, a rollback will cause
* the next call to read() to return the same object that was originally returned,
* since there is no way to create a defensive copy, and re-querying the database
* for all the keys would be too resource intensive.</p>
*
*
* @author Lucas Ward
* @since 1.0
*/
public class DrivingQueryInputSource extends AbstractTransactionalIoSource implements InputSource,
ResourceLifecycle, InitializingBean, DisposableBean, Restartable {
private boolean initialized = false;
private List keys;
private Iterator keysIterator;
private int currentIndex = 0;
private int lastCommitIndex = 0;
private KeyGenerator keyGenerator;
public DrivingQueryInputSource() {
}
/**
* Initialize the input source with the provided keys list.
*
* @param keys
*/
public DrivingQueryInputSource(List keys) {
this.keys = keys;
this.keysIterator = keys.iterator();
}
/**
* Return the next key in the List. If the InputSource has not been initialized yet,
* then {@link AbstractDrivingQueryInputSource.open()} will be called.
*
* @return next key in the list if not index is not at the last element, null otherwise.
*/
public Object read() {
if (!initialized) {
open();
}
if (keysIterator.hasNext()) {
currentIndex++;
return keysIterator.next();
}
return null;
}
/**
* Get the current key. This method will return the same
* object returned by the last read() method. If the
* InputSource hasn't been initialized yet, then null will
* be returned.
*
* @return the current key.
*/
protected Object getCurrentKey(){
if(initialized){
return keys.get(currentIndex - 1);
}
return null;
}
/**
* Close the resource by setting the list of keys to null, allowing them
* to be garbage collected.
*/
public void close() {
initialized = false;
currentIndex = 0;
lastCommitIndex = 0;
keys = null;
keysIterator = null;
}
/**
* Initialize the input source by delegating to the subclass in order to retrieve
* the keys. The input source will also be registered with the
* {@link BatchTransactionSynchronizationManager} in order to ensure it is notified
* about commits and rollbacks.
*
* @throws IllegalStateException if the keys list is null or initialized is true.
*/
public void open() {
Assert.state(keys == null || initialized, "Cannot open an already opened input source" +
", call close() first.");
keys = keyGenerator.retrieveKeys();
keysIterator = keys.listIterator();
super.registerSynchronization();
initialized = true;
}
/* (non-Javadoc)
* @see org.springframework.beans.factory.DisposableBean#destroy()
*/
public void destroy() throws Exception {
close();
}
/**
* Restore input source to previous state. If the input source has already
* been initialized before calling restore (meaning, read has been called)
* then an IllegalStateException will be thrown, since all input sources
* should be restored before being read from, otherwise already processed
* data could be returned. The RestartData attempting to be restored from
* must have been obtained from the <strong>same input source as the one
* being restored from</strong> otherwise it is invalid.
*
* @throws IllegalArgumentException if restart data or it's properties is null.
* @throws IllegalStateException if the input source has already been initialized.
*/
public final void restoreFrom(RestartData data) {
Assert.notNull(data, "RestartData must not be null.");
Assert.notNull(data.getProperties(), "RestartData properties must not be null.");
Assert.state(!initialized, "Cannot restore when already intialized. Call"
+ " close() first before restore()");
if (data.getProperties().size() == 0) {
return;
}
keys = keyGenerator.restoreKeys(data);
if(keys != null && keys.size() > 0){
keysIterator = keys.listIterator();
initialized = true;
}
}
public RestartData getRestartData() {
return keyGenerator.getKeyAsRestartData(getCurrentKey());
}
public void afterPropertiesSet() throws Exception {
Assert.notNull(keyGenerator, "The KeyGenerator must not be null.");
}
/**
* Set the key generation strategy to use for this input source.
*
* @param keyGenerator
*/
public void setKeyGenerator(
KeyGenerator keyGenerator) {
this.keyGenerator = keyGenerator;
}
protected void transactionCommitted() {
lastCommitIndex = currentIndex;
}
protected void transactionRolledBack() {
keysIterator = keys.listIterator(lastCommitIndex);
}
}

View File

@@ -0,0 +1,63 @@
/*
* 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.springframework.batch.io.driving;
import org.springframework.batch.io.driving.support.IbatisKeyGenerator;
import org.springframework.orm.ibatis.SqlMapClientTemplate;
import com.ibatis.sqlmap.client.SqlMapClient;
/**
* Extension of {@link DrivingQueryInputSource} that maps keys to
* objects. An iBatis query id must be set to map and return each 'detail record'.
*
* @author Lucas Ward
* @see IbatisKeyGenerator
*/
public class IbatisDrivingQueryInputSource extends DrivingQueryInputSource {
private String detailsQueryId;
private SqlMapClientTemplate sqlMapClientTemplate;
/**
* Overridden read() that uses the returned key as arguments to the details query.
*
* @see org.springframework.batch.io.driving.DrivingQueryInputSource#read()
*/
public Object read() {
return sqlMapClientTemplate.queryForObject(detailsQueryId, super.read());
}
/**
* @param detailsQueryId id of the iBATIS select statement that will used
* to retrieve an object for a single primary key from the list
* returned by driving query
*/
public void setDetailsQueryId(String detailsQueryId) {
this.detailsQueryId = detailsQueryId;
}
/**
* Set the {@link SqlMapClientTemplate} to use for this input source.
*
* @param sqlMapClientTemplate
*/
public void setSqlMapClient(
SqlMapClient sqlMapClient) {
this.sqlMapClientTemplate = new SqlMapClientTemplate(sqlMapClient);
}
}

View File

@@ -0,0 +1,38 @@
package org.springframework.batch.io.driving;
import java.util.List;
import org.springframework.batch.restart.RestartData;
/**
* Strategy interface used to generate keys in driving query input.
*
* @author Lucas Ward
* @since 1.0
*/
public interface KeyGenerator {
/**
* @return list of keys returned by the driving query
*/
List retrieveKeys();
/**
* Restore the keys list based on provided restart data.
*
* @param restartData, the restart data to restore the keys list from.
* @return a list of keys.
* @throws IllegalArgumentException is restartData is null.
*/
List restoreKeys(RestartData restartData);
/**
* Return the provided key as restart data.
*
* @param key to be converted to restart data.
* @return RestartData representation of the key.
* @throws IllegalArgumentException if key is null.
* @throws IllegalArgumentException if key is an incompatible type.
*/
RestartData getKeyAsRestartData(Object key);
}

View File

@@ -0,0 +1,8 @@
<html>
<body>
<p>
Infrastructure implementations of driving query based input sources. All input sources within this package
query a database to return a list of keys, and return back one key per call to read().
</p>
</body>
</html>

View File

@@ -0,0 +1,136 @@
/**
*
*/
package org.springframework.batch.io.driving.support;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Map.Entry;
import org.springframework.batch.restart.RestartData;
import org.springframework.core.CollectionFactory;
import org.springframework.jdbc.core.ColumnMapRowMapper;
import org.springframework.jdbc.core.PreparedStatementSetter;
import org.springframework.jdbc.core.SqlParameterValue;
import org.springframework.jdbc.core.SqlTypeValue;
import org.springframework.jdbc.core.StatementCreatorUtils;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/**
* </p>Extension of the ColumnMapRowMapper that converts a column map to RestartData and allows
* RestartData to be converted back as a PreparedStatementSetter. This is useful in a restart
* scenario, as it allows for the standard functionality of the ColumnMapRowMapper to be used to
* create a map representing the columns returned by a query. It should be noted that this column ordering
* is preserved in the map using a link list version of Map.
*
*
* @author Lucas Ward
* @author Dave Syer
* @see RestartDataRowMapper
*/
public class ColumnMapRestartDataRowMapper extends ColumnMapRowMapper implements RestartDataRowMapper{
static final String KEY = ClassUtils.getQualifiedName(ColumnMapRestartDataRowMapper.class) + ".KEY.";
public PreparedStatementSetter createSetter(RestartData restartData) {
ColumnMapRestartData columnData = new ColumnMapRestartData(restartData.getProperties());
List columns = new ArrayList();
for (Iterator iterator = columnData.keys.values().iterator(); iterator.hasNext();) {
Object column = (Object) iterator.next();
columns.add(column);
}
return new ArgPreparedStatementSetter(columns.toArray());
}
public RestartData createRestartData(Object key) {
Assert.isInstanceOf(Map.class, key, "Key must be of type Map.");
Map keys = (Map)key;
return new ColumnMapRestartData(keys);
}
private static class ColumnMapRestartData implements RestartData{
private final Map keys;
public ColumnMapRestartData(Map keys) {
this.keys = keys;
}
public ColumnMapRestartData(Properties props) {
keys = CollectionFactory.createLinkedCaseInsensitiveMapIfPossible(props.size());
for(int counter = 0; counter < props.size(); counter++){
String column = props.getProperty(KEY + counter);
if(column != null){
keys.put(KEY + counter, column);
}
else{
break;
}
}
}
public Properties getProperties() {
Properties props = new Properties();
int counter = 0;
for (Iterator iterator = keys.entrySet().iterator(); iterator.hasNext();) {
Entry entry = (Entry) iterator.next();
props.setProperty(KEY + counter, entry.getValue().toString());
counter++;
}
return props;
}
}
/*
* Exact duplicate of Spring class of the same name, copied because it is
* package private.
*/
private static class ArgPreparedStatementSetter implements PreparedStatementSetter{
private final Object[] args;
/**
* Create a new ArgPreparedStatementSetter for the given arguments.
* @param args the arguments to set
*/
public ArgPreparedStatementSetter(Object[] args) {
this.args = args;
}
public void setValues(PreparedStatement ps) throws SQLException {
if (this.args != null) {
for (int i = 0; i < this.args.length; i++) {
Object arg = this.args[i];
if (arg instanceof SqlParameterValue) {
SqlParameterValue paramValue = (SqlParameterValue) arg;
StatementCreatorUtils.setParameterValue(ps, i + 1, paramValue, paramValue.getValue());
}
else {
StatementCreatorUtils.setParameterValue(ps, i + 1, SqlTypeValue.TYPE_UNKNOWN, arg);
}
}
}
}
}
}

View File

@@ -0,0 +1,103 @@
package org.springframework.batch.io.driving.support;
import java.util.List;
import java.util.Properties;
import org.springframework.batch.io.driving.DrivingQueryInputSource;
import org.springframework.batch.io.driving.KeyGenerator;
import org.springframework.batch.restart.GenericRestartData;
import org.springframework.batch.restart.RestartData;
import org.springframework.orm.ibatis.SqlMapClientTemplate;
import org.springframework.util.Assert;
import com.ibatis.sqlmap.client.SqlMapClient;
/**
* {@link KeyGenerator} based on iBATIS ORM framework. It is functionally similar to
* {@link SingleColumnJdbcKeyGenerator} but does not make assumptions about the primary key
* structure.
*
* @author Robert Kasanicky
* @author Lucas Ward
* @see DrivingQueryInputSource
*/
public class IbatisKeyGenerator implements KeyGenerator {
public static final String RESTART_KEY = "IbatisDrivingQueryInputSource.keyIndex";
private SqlMapClientTemplate sqlMapClientTemplate;
private String drivingQuery;
private String restartQueryId;
/*
* Retrieve the keys using the provided driving query id.
*
* @see org.springframework.batch.io.support.AbstractDrivingQueryInputSource#retrieveKeys()
*/
public List retrieveKeys() {
return sqlMapClientTemplate.queryForList(drivingQuery);
}
/*
*
* @see org.springframework.batch.restart.Restartable#getRestartData()
*/
public RestartData getKeyAsRestartData(Object key) {
Properties props = new Properties();
props.setProperty(RESTART_KEY, key.toString());
return new GenericRestartData(props);
}
/**
* Restore the keys list given the provided restart data.
*
* @see org.springframework.batch.io.driving.DrivingQueryInputSource#restoreKeys(org.springframework.batch.restart.RestartData)
*/
public List restoreKeys(RestartData data) {
Properties props = data.getProperties();
Object key = props.getProperty(RESTART_KEY);
return sqlMapClientTemplate.queryForList(restartQueryId, key);
}
/* (non-Javadoc)
* @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
*/
public void afterPropertiesSet() throws Exception {
Assert.notNull(sqlMapClientTemplate, "SqlMaperClientTemplate must not be null.");
Assert.hasText(drivingQuery, "The DrivingQuery must not be null or empty.");
}
/**
* @param sqlMapClient configured iBATIS client
*/
public void setSqlMapClient(SqlMapClient sqlMapClient) {
this.sqlMapClientTemplate = new SqlMapClientTemplate();
this.sqlMapClientTemplate.setSqlMapClient(sqlMapClient);
}
/**
* @param drivingQueryId id of the iBATIS select statement that will be used
* to retrieve the list of primary keys
*/
public void setDrivingQueryId(String drivingQueryId) {
this.drivingQuery = drivingQueryId;
}
/**
* Set the id of the restart query.
*
* @param restartQueryId id of the iBatis select statement that will be used
* to retrieve the list of primary keys after a restart.
*/
public void setRestartQueryId(String restartQueryId) {
this.restartQueryId = restartQueryId;
}
public final SqlMapClientTemplate getSqlMapClientTemplate() {
return sqlMapClientTemplate;
}
}

View File

@@ -0,0 +1,155 @@
/*
* 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.springframework.batch.io.driving.support;
import java.util.ArrayList;
import java.util.List;
import org.springframework.batch.io.driving.DrivingQueryInputSource;
import org.springframework.batch.io.driving.KeyGenerator;
import org.springframework.batch.restart.RestartData;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* <p>Jdbc implementation of the {@link KeyGenerator} interface that works for composite keys.
* (i.e. keys represented by multiple columns) A sql query to be used to return the keys and
* a {@link RestartDataRowMapper} to map each row in the resultset to an Object must be set in
* order to work correctly.
* </p>
*
* @author Lucas Ward
* @see DrivingQueryInputSource
* @since 1.0
*/
public class MultipleColumnJdbcKeyGenerator implements
KeyGenerator {
public static final String RESTART_KEY = "CompositeKeySqlDrivingQueryInputSource.key";
private JdbcTemplate jdbcTemplate;
private RestartDataRowMapper keyMapper = new ColumnMapRestartDataRowMapper();
private String sql;
private String restartSql;
public MultipleColumnJdbcKeyGenerator() {
super();
}
/**
* Construct a new InputSource.
*
* @param jdbcTemplate
* @param sql - Sql statement that returns all keys to process.
* @param keyMapper - RowMapper that maps each row of the ResultSet to an object.
*/
public MultipleColumnJdbcKeyGenerator(JdbcTemplate jdbcTemplate,
String sql){
this();
Assert.notNull(jdbcTemplate, "The JdbcTemplate must not be null.");
Assert.hasText(sql, "The DrivingQuery must not be null or empty.");
Assert.notNull(keyMapper, "The key RowMapper must not be null.");
this.jdbcTemplate = jdbcTemplate;
this.sql = sql;
}
/* (non-Javadoc)
* @see org.springframework.batch.io.sql.scratch.AbstractDrivingQueryInputSource#retrieveKeys()
*/
public List retrieveKeys() {
return jdbcTemplate.query(sql, keyMapper);
}
/* (non-Javadoc)
* @see org.springframework.batch.io.sql.scratch.AbstractDrivingQueryInputSource#restoreKeys(org.springframework.batch.restart.RestartData)
*/
public List restoreKeys(RestartData restartData) {
Assert.state(keyMapper != null, "KeyMapper must not be null.");
Assert.state(StringUtils.hasText(restartSql), "The RestartQuery must not be null or empty" +
" in order to restart.");
if (restartData.getProperties() != null) {
return jdbcTemplate.query(restartSql, keyMapper.createSetter(restartData), keyMapper);
}
return new ArrayList();
}
/* (non-Javadoc)
* @see org.springframework.batch.restart.Restartable#getRestartData()
*/
public RestartData getKeyAsRestartData(Object key) {
Assert.state(keyMapper != null, "RestartDataConverter must not be null.");
return keyMapper.createRestartData(key);
}
/**
* Set the query to use to retrieve keys in order to restore the previous
* state for restart.
*
* @param restartQuery
*/
public void setRestartQuery(String restartQuery) {
this.restartSql = restartQuery;
}
/* (non-Javadoc)
* @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
*/
public void afterPropertiesSet() throws Exception {
Assert.notNull(jdbcTemplate, "The JdbcTemplate must not be null.");
Assert.hasText(sql, "The DrivingQuery must not be null or empty.");
Assert.notNull(keyMapper, "The key RowMapper must not be null.");
}
/**
* Set the {@link RestartDataRowMapper} to be used to map a resultset
* to keys.
*
* @param keyMapper
*/
public void setKeyMapper(RestartDataRowMapper keyMapper) {
this.keyMapper = keyMapper;
}
/**
* Set the sql statement used to generate the keys list.
*
* @param sql
*/
public void setSql(String sql) {
this.sql = sql;
}
/**
* Set the sql statement to be used to restore the keys list
* after a restart.
*
* @param restartSql
*/
public void setRestartSql(String restartSql) {
this.restartSql = restartSql;
}
public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
}

View File

@@ -0,0 +1,53 @@
/*
* 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.springframework.batch.io.driving.support;
import org.springframework.batch.restart.RestartData;
import org.springframework.jdbc.core.PreparedStatementSetter;
import org.springframework.jdbc.core.RowMapper;
/**
* RestartDataRowMapper extends the standard {@link RowMapper} interface to provide for
* converting an object returned from a RowMapper to RestartData and back again. One
* of the most common use cases for this type of functionality is the DrivingQuery approach
* to sql processing. Using a RestartDataRowMapper, developers can create each unique key
* to suite their specific needs, and also describe how such a key would be converted to
* RestartData, so that it can be serialized and stored.
*
* @author Lucas Ward
* @see RowMapper
* @since 1.0
*/
public interface RestartDataRowMapper extends RowMapper {
/**
* Given the provided composite key, return a RestartData representation.
*
* @param key
* @return ResartData representing the composite key.
* @throws IllegalArgumentException if key is null or of an unsupported type.
*/
public RestartData createRestartData(Object key);
/**
* Given the provided restart data, return a PreparedStatementSeter that can
* be used as parameters to a JdbcTemplate.
*
* @param restartData
* @return an array of objects that can be used as arguments to a JdbcTemplate.
*/
public PreparedStatementSetter createSetter(RestartData restartData);
}

View File

@@ -0,0 +1,169 @@
/*
* 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.springframework.batch.io.driving.support;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import org.apache.commons.lang.ClassUtils;
import org.springframework.batch.io.driving.KeyGenerator;
import org.springframework.batch.restart.GenericRestartData;
import org.springframework.batch.restart.RestartData;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.SingleColumnRowMapper;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* <p>Jdbc {@link KeyGenerator} implementation that only works for a single column key. A sql
* query must be passed in which will be used to return a list of keys. Each key will be mapped
* by a {@link RowMapper} that returns a mapped key. By default, the {@link SingleColumnRowMapper}
* is used, and will convert keys into well known types at runtime. It is extremely important to
* note that only one column should be mapped to an object and returned as a key. If multiple
* columns are returned as a key in this strategy, then restart will not function properly. Instead
* a strategy that supports keys comprised of multiple columns should be used.
*
* <p>Restartability: Because the key is only one column, restart is made much more simple. Before
* each commit, the last processed key is returned to be stored as restart data. Upon restart, that
* same key is given back to restore from, using a separate 'RestartQuery'. This means that only the
* keys remaining to be processed are returned, rather than returning the original list of keys and
* iterating forward to that last committed point.
* </p>
*
* @author Lucas Ward
* @since 1.0
*/
public class SingleColumnJdbcKeyGenerator implements KeyGenerator {
public static final String RESTART_KEY = ClassUtils.getShortClassName(SingleColumnJdbcKeyGenerator.class) + ".key";
private JdbcTemplate jdbcTemplate;
private String sql;
private String restartSql;
private RowMapper keyMapper = new SingleColumnRowMapper();
public SingleColumnJdbcKeyGenerator() {
super();
}
/**
* Constructs a new instance using the provided jdbcTemplate and string representing
* the sql statement that should be used to retrieve keys.
*
* @param jdbcTemplate
* @param sql
* @throws IllegalArgumentException if jdbcTemplate is null.
* @throws IllegalArgumentException if sql string is empty or null.
*/
public SingleColumnJdbcKeyGenerator(JdbcTemplate jdbcTemplate, String sql) {
this();
Assert.notNull(jdbcTemplate, "JdbcTemplate must not be null.");
Assert.hasText(sql, "The sql statement must not be null or empty.");
this.jdbcTemplate = jdbcTemplate;
this.sql = sql;
}
/* (non-Javadoc)
* @see org.springframework.batch.io.driving.KeyGenerationStrategy#retrieveKeys()
*/
public List retrieveKeys() {
return jdbcTemplate.query(sql, keyMapper);
}
/**
* Get the restart data representing the last processed key.
*
* @see KeyGenerator#getKeyAsRestartData()
* @throws IllegalArgumentException if key is null.
*/
public RestartData getKeyAsRestartData(Object key) {
Assert.notNull(key, "The key must not be null.");
Properties props = new Properties();
props.setProperty(RESTART_KEY, key.toString());
return new GenericRestartData(props);
}
/**
* Return the remaining to be processed for the provided {@link RestartData}.
* The RestartData attempting to be restored from must have been obtained from the
* <strong>same KeyGenerationStrategy as the one
* being restored from</strong> otherwise it is invalid.
*
* @param RestartData obtained by calling getRestartData during a previous
* run.
* @throws IllegalStateException if restart sql statement is null.
* @throws IllegalArgumentException if restart data is null.
* @see KeyGenerator#restoreKeys(org.springframework.batch.restart.RestartData)
*/
public List restoreKeys(RestartData restartData) {
Assert.notNull(restartData, "The restart data must not be null.");
Assert.state(StringUtils.hasText(restartSql), "The RestartQuery must not be null or empty" +
" in order to restart.");
String lastProcessedKey = restartData.getProperties().getProperty(RESTART_KEY);
if (lastProcessedKey != null) {
return jdbcTemplate.query(restartSql, new Object[] { lastProcessedKey }, keyMapper);
}
return new ArrayList();
}
/* (non-Javadoc)
* @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
*/
public void afterPropertiesSet() throws Exception {
Assert.notNull(jdbcTemplate, "JdbcTemplate must not be null.");
Assert.hasText(sql, "The DrivingQuery must not be null or empty.");
}
/**
* Set the {@link RowMapper} to be used to map each key to an object.
*
* @param keyMapper
*/
public void setKeyMapper(RowMapper keyMapper) {
this.keyMapper = keyMapper;
}
/**
* Set the SQL query to be used to return the remaining keys to be processed.
*
* @param restartSql
*/
public void setRestartSql(String restartSql) {
this.restartSql = restartSql;
}
/**
* Set the SQL statement to be used to return the keys to be processed.
*
* @param sql
*/
public void setSql(String sql) {
this.sql = sql;
}
}

View File

@@ -0,0 +1,49 @@
/*
* 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.springframework.batch.io.exception;
/**
* This exception is thrown when there is a critical configuration error and the
* current job or module execution cannot continue.
*
* @author Kerry O'Brien
*/
public class BatchConfigurationException extends BatchCriticalException {
private static final long serialVersionUID = 759498454063502984L;
/**
* @param msg
* @param ex
*/
public BatchConfigurationException(String msg, Throwable ex) {
super(msg, ex);
}
/**
* @param msg
*/
public BatchConfigurationException(String msg) {
super(msg);
}
/**
* @param nested
*/
public BatchConfigurationException(Throwable nested) {
super(nested);
}
}

View File

@@ -0,0 +1,72 @@
/*
* 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.springframework.batch.io.exception;
/**
* BatchCritcalException - Indiates to the framework that a critical error has
* occured and batch processing should immeadiately stop. However, in most cases
* status should still be persisted indicating that an error foced the job to
* terminate. Any framework code that catches a BatchCriticalException will
* rethrow the exception. This allows any code that creates a critical exception
* to be able to add an error code that will still be accesible at the very
* beginning of the call chain. (usually a launcher that kicked off the
* JobController). Error code values 0 - 2000 a reserved for framework classes.
* Anything greater than 2000 can be used by application code.
*
* @author Lucas Ward
*
*/
public class BatchCriticalException extends RuntimeException {
private static final long serialVersionUID = 8838982304219248527L;
/**
* Constructs a new instance with a default error code of 1.
*
* @param msg the exception message.
*
*/
public BatchCriticalException(String msg) {
super(msg);
}
/**
* Constructs a new instance with a default error code of 1.
*
* @param msg the exception message.
*
*/
public BatchCriticalException(String msg, Throwable nested) {
super(msg, nested);
}
/**
* Constructs a new instance with a nested exception. The error code is
* defaulted to 1 and the message is empty.
*/
public BatchCriticalException(Throwable nested) {
super(nested);
}
/**
* Constructs a new instance, the error code is defaulted to one and the
* message is empty.
*/
public BatchCriticalException() {
super();
}
}

View File

@@ -0,0 +1,50 @@
/*
* 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.springframework.batch.io.exception;
/**
* Exception that should be thrown to indicate an error in the environment.
* Excamples of such errors include file or database access errors. Because this
* class extends BatchCriticalException, throwing this error will indicate to
* the framework that processing should stop. It is vital that an error-code be
* passed as well, since this will be returned from the main method of the
* launcher.
*
* @author Lucas Ward
*/
public class BatchEnvironmentException extends BatchCriticalException {
private static final long serialVersionUID = 1382420837776529019L;
/**
* Refer to the similar constructor in the parent class
* {@link BatchCriticalException}.
*
*/
public BatchEnvironmentException(String msg, Throwable nested) {
super(msg, nested);
}
/**
* Refer to the similar constructor in the parent class
* {@link BatchCriticalException}.
*
*/
public BatchEnvironmentException(String msg) {
super(msg);
}
}

View File

@@ -0,0 +1,23 @@
package org.springframework.batch.io.exception;
import org.springframework.util.MethodInvoker;
/**
* Indicates an error has been encountered
* while trying to dynamically call a method e.g. using {@link MethodInvoker}.
*
* @author Robert Kasanicky
*/
public class DynamicMethodInvocationException extends RuntimeException {
//generated value
private static final long serialVersionUID = -6056786139731564040L;
public DynamicMethodInvocationException(Throwable cause){
super(cause);
}
public DynamicMethodInvocationException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,64 @@
/*
* 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.springframework.batch.io.exception;
/**
* Exception thrown when errors are encountered
* parsing flat files. The original input, typically
* a line, can be passed in, so that latter catches
* can write out the original input to a log, or
* an error table.
*
* @author Lucas Ward
*
*/
public class FlatFileParsingException extends ParsingException {
private static final long serialVersionUID = 2529197834044942724L;
private String input;
private int lineNumber;
public FlatFileParsingException(String message, String input) {
super(message);
this.input = input;
}
public FlatFileParsingException(String message, String input, int lineNumber) {
super(message);
this.input = input;
this.lineNumber = lineNumber;
}
public FlatFileParsingException(String message, Throwable cause, String input, int lineNumber) {
super(message, cause);
this.input = input;
this.lineNumber = lineNumber;
}
public FlatFileParsingException(Throwable cause, String input) {
super(cause);
this.input = input;
}
public String getInput() {
return input;
}
public int getLineNumber() {
return lineNumber;
}
}

View File

@@ -0,0 +1,40 @@
/*
* 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.springframework.batch.io.exception;
/**
* Exception indicating that an error has been encountered
* parsing io, typically from a file.
*
* @author Lucas Ward
*
*/
public class ParsingException extends RuntimeException {
private static final long serialVersionUID = 2953386084409312312L;
public ParsingException(String message) {
super(message);
}
public ParsingException(String message, Throwable cause) {
super(message, cause);
}
public ParsingException(Throwable cause) {
super(cause);
}
}

View File

@@ -0,0 +1,36 @@
/*
* 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.springframework.batch.io.exception;
/**
* Throwing this exception causes transaction rollback.
*/
public class TransactionInvalidException extends BatchCriticalException {
private static final long serialVersionUID = -1933213086873834098L;
public TransactionInvalidException(String msg, Throwable ex) {
super(msg, ex);
}
public TransactionInvalidException(String msg) {
super(msg);
}
public TransactionInvalidException(Throwable nested) {
super(nested);
}
}

View File

@@ -0,0 +1,37 @@
/*
* 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.springframework.batch.io.exception;
/**
* This exception indicates an error which does not require the transaction to
* be rolled back (for example when an invalid record is skipped).
*/
public class TransactionValidException extends BatchCriticalException {
private static final long serialVersionUID = 4113323182216735223L;
public TransactionValidException(String msg, Throwable ex) {
super(msg, ex);
}
public TransactionValidException(String msg) {
super(msg);
}
public TransactionValidException(Throwable nested) {
super(nested);
}
}

View File

@@ -0,0 +1,34 @@
/*
* 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.springframework.batch.io.exception;
/**
* This exception should be thrown when there are validation errors.
*/
public class ValidationException extends TransactionValidException {
private static final long serialVersionUID = 7926495144451758088L;
public ValidationException(String message) {
super(message);
}
public ValidationException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,7 @@
<html>
<body>
<p>
Infrastructure implementations of io exception concerns.
</p>
</body>
</html>

View File

@@ -0,0 +1,652 @@
/*
* 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.springframework.batch.io.file;
import java.math.BigDecimal;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Properties;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* @author Rob Harrop
* @author Dave Syer
*/
public final class FieldSet {
private final static String DEFAULT_DATE_PATTERN = "yyyy-MM-dd";
/**
* The fields wrapped by this '<code>FieldSet</code>' instance.
*/
private String[] tokens;
private List names;
public FieldSet(String[] tokens) {
this.tokens = tokens == null ? null : (String[]) tokens.clone();
}
public FieldSet(String[] tokens, String[] names) {
Assert.notNull(tokens);
Assert.notNull(names);
if (tokens.length != names.length) {
throw new IllegalArgumentException(
"Field names must be same length as values: names="
+ Arrays.asList(names) + ", values="
+ Arrays.asList(tokens));
}
this.tokens = (String[]) tokens.clone();
this.names = Arrays.asList(names);
}
/**
* Public accessor for the names property.
*
* @return the names
*
* @throws IllegalStateException if the names are not defined
*/
public String[] getNames() {
if (names == null) {
throw new IllegalStateException(
"Field names are not known");
}
return (String[]) names.toArray();
}
/**
* @return fields wrapped by this '<code>FieldSet</code>' instance as String values.
*/
public String[] getValues() {
return tokens;
}
/**
* Read the {@link String} value at index '<code>index</code>'.
*
* @param index
* the field index.
* @throws IndexOutOfBoundsException
* if the index is out of bounds.
*/
public String readString(int index) {
return readAndTrim(index);
}
/**
* Read the {@link String} value from column with given '<code>name</code>'.
*
* @param name
* the field name.
*/
public String readString(String name) {
return readString(indexOf(name));
}
/**
* Read the '<code>boolean</code>' value at index '<code>index</code>'.
*
* @param index
* the field index.
* @throws IndexOutOfBoundsException
* if the index is out of bounds.
*/
public boolean readBoolean(int index) {
return readBoolean(index, "true");
}
/**
* Read the '<code>boolean</code>' value from column with given '<code>name</code>'.
*
* @param name
* the field name.
* @throws IllegalArgumentException
* if a column with given name is not defined.
*/
public boolean readBoolean(String name) {
return readBoolean(indexOf(name));
}
/**
* Read the '<code>boolean</code>' value at index '<code>index</code>'.
*
* @param index
* the field index.
* @param trueValue
* the value that signifies {@link Boolean#TRUE true};
* case-sensitive.
* @throws IndexOutOfBoundsException
* if the index is out of bounds, or if the supplied
* <code>trueValue</code> is <code>null</code>.
*/
public boolean readBoolean(int index, String trueValue) {
Assert.notNull(trueValue, "'trueValue' cannot be null.");
String value = readAndTrim(index);
return trueValue.equals(value) ? true : false;
}
/**
* Read the '<code>boolean</code>' value from column with given '<code>name</code>'.
*
* @param name
* the field name.
* @param trueValue
* the value that signifies {@link Boolean#TRUE true};
* case-sensitive.
* @throws IllegalArgumentException
* if a column with given name is not defined, or if the
* supplied <code>trueValue</code> is <code>null</code>.
*/
public boolean readBoolean(String name, String trueValue) {
return readBoolean(indexOf(name), trueValue);
}
/**
* Read the '<code>char</code>' value at index '<code>index</code>'.
*
* @param index
* the field index.
* @throws IndexOutOfBoundsException
* if the index is out of bounds.
*/
public char readChar(int index) {
String value = readAndTrim(index);
Assert.isTrue(value.length() == 1, "Cannot convert field value '"
+ value + "' to char.");
return value.charAt(0);
}
/**
* Read the '<code>char</code>' value from column with given '<code>name</code>'.
*
* @param name
* the field name.
* @throws IllegalArgumentException
* if a column with given name is not defined.
*/
public char readChar(String name) {
return readChar(indexOf(name));
}
/**
* Read the '<code>byte</code>' value at index '<code>index</code>'.
*
* @param index
* the field index.
* @throws IndexOutOfBoundsException
* if the index is out of bounds.
*/
public byte readByte(int index) {
return Byte.parseByte(readAndTrim(index));
}
/**
* Read the '<code>byte</code>' value from column with given '<code>name</code>'.
*
* @param name
* the field name.
*/
public byte readByte(String name) {
return readByte(indexOf(name));
}
/**
* Read the '<code>short</code>' value at index '<code>index</code>'.
*
* @param index
* the field index.
* @throws IndexOutOfBoundsException
* if the index is out of bounds.
*/
public short readShort(int index) {
return Short.parseShort(readAndTrim(index));
}
/**
* Read the '<code>short</code>' value from column with given '<code>name</code>'.
*
* @param name
* the field name.
* @throws IllegalArgumentException
* if a column with given name is not defined.
*/
public short readShort(String name) {
return readShort(indexOf(name));
}
/**
* Read the '<code>int</code>' value at index '<code>index</code>'.
*
* @param index
* the field index.
* @throws IndexOutOfBoundsException
* if the index is out of bounds.
*/
public int readInt(int index) {
return Integer.parseInt(readAndTrim(index));
}
/**
* Read the '<code>int</code>' value from column with given '<code>name</code>'.
*
* @param name
* the field name.
* @throws IllegalArgumentException
* if a column with given name is not defined.
*/
public int readInt(String name) {
return readInt(indexOf(name));
}
/**
* Read the '<code>int</code>' value at index '<code>index</code>',
* using the supplied <code>defaultValue</code> if the field value is
* blank.
*
* @param index
* the field index..
* @throws IndexOutOfBoundsException
* if the index is out of bounds.
*/
public int readInt(int index, int defaultValue) {
String value = readAndTrim(index);
return StringUtils.hasLength(value) ? Integer.parseInt(value)
: defaultValue;
}
/**
* Read the '<code>int</code>' value from column with given '<code>name</code>',
* using the supplied <code>defaultValue</code> if the field value is
* blank.
*
* @param name
* the field name.
* @throws IllegalArgumentException
* if a column with given name is not defined.
*/
public int readInt(String name, int defaultValue) {
return readInt(indexOf(name), defaultValue);
}
/**
* Read the '<code>long</code>' value at index '<code>index</code>'.
*
* @param index
* the field index.
* @throws IndexOutOfBoundsException
* if the index is out of bounds.
*/
public long readLong(int index) {
return Long.parseLong(readAndTrim(index));
}
/**
* Read the '<code>long</code>' value from column with given '<code>name</code>'.
*
* @param name
* the field name.
* @throws IllegalArgumentException
* if a column with given name is not defined.
*/
public long readLong(String name) {
return readLong(indexOf(name));
}
/**
* Read the '<code>long</code>' value at index '<code>index</code>',
* using the supplied <code>defaultValue</code> if the field value is
* blank.
*
* @param index
* the field index..
* @throws IndexOutOfBoundsException
* if the index is out of bounds.
*/
public long readLong(int index, long defaultValue) {
String value = readAndTrim(index);
return StringUtils.hasLength(value) ? Long.parseLong(value)
: defaultValue;
}
/**
* Read the '<code>long</code>' value from column with given '<code>name</code>',
* using the supplied <code>defaultValue</code> if the field value is
* blank.
*
* @param name
* the field name.
* @throws IllegalArgumentException
* if a column with given name is not defined.
*/
public long readLong(String name, long defaultValue) {
return readLong(indexOf(name), defaultValue);
}
/**
* Read the '<code>float</code>' value at index '<code>index</code>'.
*
* @param index
* the field index.
* @throws IndexOutOfBoundsException
* if the index is out of bounds.
*/
public float readFloat(int index) {
return Float.parseFloat(readAndTrim(index));
}
/**
* Read the '<code>float</code>' value from column with given '<code>name</code>.
*
* @param name
* the field name.
* @throws IllegalArgumentException
* if a column with given name is not defined.
*/
public float readFloat(String name) {
return readFloat(indexOf(name));
}
/**
* Read the '<code>double</code>' value at index '<code>index</code>'.
*
* @param index
* the field index.
* @throws IndexOutOfBoundsException
* if the index is out of bounds.
*/
public double readDouble(int index) {
return Double.parseDouble(readAndTrim(index));
}
/**
* Read the '<code>double</code>' value from column with given '<code>name</code>.
*
* @param name
* the field name.
* @throws IllegalArgumentException
* if a column with given name is not defined.
*/
public double readDouble(String name) {
return readDouble(indexOf(name));
}
/**
* Read the {@link java.math.BigDecimal} value at index '<code>index</code>'.
*
* @param index
* the field index.
* @throws IndexOutOfBoundsException
* if the index is out of bounds.
*/
public BigDecimal readBigDecimal(int index) {
return readBigDecimal(index, null);
}
/**
* Read the {@link java.math.BigDecimal} value from column with given '<code>name</code>.
*
* @param name
* the field name.
* @throws IllegalArgumentException
* if a column with given name is not defined.
*/
public BigDecimal readBigDecimal(String name) {
return readBigDecimal(name, null);
}
/**
* Read the {@link BigDecimal} value at index '<code>index</code>',
* returning the supplied <code>defaultValue</code> if the trimmed string
* value at index '<code>index</code>' is blank.
*
* @param index
* the field index.
* @throws IndexOutOfBoundsException
* if the index is out of bounds.
*/
public BigDecimal readBigDecimal(int index, BigDecimal defaultValue) {
String candidate = readAndTrim(index);
try {
return (StringUtils.hasText(candidate)) ? new BigDecimal(candidate)
: defaultValue;
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Unparseable number: "
+ candidate);
}
}
/**
* Read the {@link BigDecimal} value from column with given '<code>name</code>,
* returning the supplied <code>defaultValue</code> if the trimmed string
* value at index '<code>index</code>' is blank.
*
* @param name
* the field name.
* @throws IllegalArgumentException
* if a column with given name is not defined.
*/
public BigDecimal readBigDecimal(String name, BigDecimal defaultValue) {
try {
return readBigDecimal(indexOf(name), defaultValue);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(e.getMessage() + ", name: ["
+ name + "]");
}
}
/**
* Read the <code>java.util.Date</code> value in default format at
* designated column <code>index</code>.
*
* @param index
* the field index.
* @param pattern
* the pattern describing the date and time format
* @throws IndexOutOfBoundsException
* if the index is out of bounds.
* @see #DEFAULT_DATE_PATTERN
*/
public Date readDate(int index) {
return readDate(index, DEFAULT_DATE_PATTERN);
}
/**
* Read the <code>java.sql.Date</code> value in given format from column
* with given <code>name</code>.
*
* @param name
* the field name.
* @param pattern
* the pattern describing the date and time format
* @throws IllegalArgumentException
* if a column with given name is not defined.
* @see #DEFAULT_DATE_PATTERN
*/
public Date readDate(String name) {
return readDate(name, DEFAULT_DATE_PATTERN);
}
/**
* Read the <code>java.util.Date</code> value in default format at
* designated column <code>index</code>.
*
* @param index
* the field index.
* @param pattern
* the pattern describing the date and time format
* @throws IndexOutOfBoundsException
* if the index is out of bounds.
* @throws IllegalArgumentException
* if the date cannot be parsed.
*
*/
public Date readDate(int index, String pattern) {
SimpleDateFormat sdf = new SimpleDateFormat(pattern);
Date date;
String value = readAndTrim(index);
try {
date = sdf.parse(value);
} catch (ParseException e) {
throw new IllegalArgumentException(e.getMessage() + ", pattern: ["
+ pattern + "]");
}
return date;
}
/**
* Read the <code>java.sql.Date</code> value in given format from column
* with given <code>name</code>.
*
* @param name
* the field name.
* @param pattern
* the pattern describing the date and time format
* @throws IllegalArgumentException
* if a column with given name is not defined or if the
* specified field cannot be parsed
*
*/
public Date readDate(String name, String pattern) {
try {
return readDate(indexOf(name), pattern);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(e.getMessage() + ", name: ["
+ name + "]");
}
}
/**
* Return the number of fields in this '<code>FieldSet</code>'.
*/
public int getFieldCount() {
return tokens.length;
}
/**
* Read and trim the {@link String} value at '<code>index</code>'.
*
* @throws NullPointerException
* if the field value is <code>null</code>.
*/
private String readAndTrim(int index) {
String value = tokens[index];
if (value != null) {
return value.trim();
} else {
return null;
}
}
/**
* Read and trim the {@link String} value from column with given '<code>name</code>.
*
* @throws IllegalArgumentException
* if a column with given name is not defined.
*/
private int indexOf(String name) {
if (names == null) {
throw new IllegalArgumentException(
"Cannot access columns by name without meta data");
}
int index = names.indexOf(name);
if (index >= 0) {
return index;
}
throw new IllegalArgumentException("Cannot access column [" + name
+ "] from " + names);
}
public String toString() {
if (names != null) {
return getProperties().toString();
}
return tokens == null ? "" : Arrays.asList(tokens).toString();
}
/**
* @see java.lang.Object#equals(java.lang.Object)
*/
public boolean equals(Object object) {
if (object instanceof FieldSet) {
FieldSet fs = (FieldSet) object;
if (this.tokens == null) {
return fs.tokens == null;
} else {
return Arrays.equals(this.tokens, fs.tokens);
}
}
return false;
}
public int hashCode() {
// this algorithm was taken from java 1.5 jdk Arrays.hashCode(Object[])
if (tokens == null) {
return 0;
}
int result = 1;
for (int i = 0; i < tokens.length; i++) {
result = 31 * result
+ (tokens[i] == null ? 0 : tokens[i].hashCode());
}
return result;
}
/**
* Construct name-value pairs from the field names and string values. Null
* values are omitted.
*
* @return some properties representing the field set.
*
* @throws IllegalStateException
* if the field name meta data is not available.
*/
public Properties getProperties() {
if (names == null) {
throw new IllegalStateException(
"Cannot create properties without meta data");
}
Properties props = new Properties();
for (int i = 0; i < tokens.length; i++) {
String value = readAndTrim(i);
if (value != null) {
props.setProperty((String) names.get(i), value);
}
}
return props;
}
}

View File

@@ -0,0 +1,43 @@
/*
* 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.springframework.batch.io.file;
/**
* Interface that is used to map data obtained from a file into an object.
*
* @author tomas.slanina
* @author Dave Syer
*
*/
public interface FieldSetMapper {
/**
* Marker for the beginning of a multi-object record.
*/
static Object BEGIN_RECORD = new Object();
/**
* Marker for the end of a multi-object record.
*/
static Object END_RECORD = new Object();
/**
* Method used to map data obtained from a file into an object.
*/
public Object mapLine(FieldSet fs);
}

View File

@@ -0,0 +1,7 @@
<html>
<body>
<p>
Infrastructure implementations of io file concerns.
</p>
</body>
</html>

View File

@@ -0,0 +1,186 @@
/*
* 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.springframework.batch.io.file.support;
import java.util.HashSet;
import java.util.Properties;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.batch.io.Skippable;
import org.springframework.batch.repeat.synch.BatchTransactionSynchronizationManager;
import org.springframework.batch.restart.GenericRestartData;
import org.springframework.batch.restart.RestartData;
import org.springframework.batch.restart.Restartable;
import org.springframework.batch.statistics.StatisticsProvider;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationAdapter;
/**
* <p>
* This class is a {@link FieldSetInputSource} that supports restart,
* skipping invalid lines and storing statistics.
* </p>
*
* @author Waseem Malik
* @author Tomas Slanina
* @author Robert Kasanicky
*/
public class DefaultFlatFileInputSource extends SimpleFlatFileInputSource implements Skippable, Restartable,
StatisticsProvider {
private static Log log = LogFactory.getLog(DefaultFlatFileInputSource.class);
public static final String READ_STATISTICS_NAME = "lines.read.count";
public static final String SKIPPED_STATISTICS_NAME = "skipped.lines.count";
private Set skippedLines = new HashSet();
private TransactionSynchronization transactionSynchronization = new ResourceLineReaderTransactionSynchronization();
private Properties statistics = new Properties();
/**
* Initialize the input source.
*/
public void open() {
registerSynchronization();
super.open();
}
// Registers transaction synchronization.
private void registerSynchronization() {
BatchTransactionSynchronizationManager.registerSynchronization(transactionSynchronization);
}
/**
* This method initialises the Input Source for Restart. It opens the input
* file and position the buffer reader according to information provided by
* the restart data
*
* @param restartData restartData information
*/
public void restoreFrom(RestartData data) {
if (data==null ||
data.getProperties() == null ||
data.getProperties().getProperty(READ_STATISTICS_NAME) == null ||
getReader()==null) {
// do nothing
return;
}
log.debug("Initializing for restart. Restart data is: " + data);
int lineCount = Integer.parseInt(data.getProperties().getProperty(READ_STATISTICS_NAME));
ResourceLineReader reader = getReader();
Object record = "";
while (reader.getCurrentLineCount() < lineCount && record != null) {
record = reader.read();
}
}
/**
* This method returns the restart Data for the input Source. It returns the
* current Line Count which can be used to re initialise the batch job in
* case of restart.
*/
public RestartData getRestartData() {
return new GenericRestartData(getStatistics());
}
/**
* @return statistics for input template
* @see org.springframework.batch.statistics.StatisticsProvider#getStatistics()
*/
public Properties getStatistics() {
ResourceLineReader is = getReader();
statistics.setProperty(READ_STATISTICS_NAME, String.valueOf(is.getCurrentLineCount()));
return statistics;
}
/**
* This method marks the start of a transaction. It marks the InputBuffer
* Reader, so that in case of rollback it can position the file to start of
* the transaction.
*/
private void transactionStarted() {
getReader().mark();
}
/**
* Commit the transaction. At each commit point we clear the lines which we
* had skipped during the commit interval.
*/
private void transactionComitted() {
transactionStarted();
}
/**
* Rollback the transaction.
*/
private void transactionRolledback() {
getReader().reset();
}
/**
* Skip the current line which is being processed.
*/
public void skip() {
Integer count = new Integer(getReader().getCurrentLineCount());
// we are not really thread safe so we don't need to synchronize
skippedLines.add(count);
log.debug("Skipping line in template=[" + this + "], line=" + count);
}
protected String readLine() {
String line = super.readLine();
while (line != null && skippedLines.contains(new Integer(getReader().getCurrentLineCount()))) {
line = super.readLine();
}
return line;
}
// added package visibility method so that tests can invoke transaction
// events
TransactionSynchronization getTransactionSynchronization() {
return this.transactionSynchronization;
}
/**
* Encapsulates transaction events.
*/
private class ResourceLineReaderTransactionSynchronization extends TransactionSynchronizationAdapter {
/**
* TransactionSynchronization method indicating that a transaction has
* completed.
*
* @param status indicates whether it was a rollback or commit
*/
public void afterCompletion(int status) {
if (status == TransactionSynchronization.STATUS_COMMITTED) {
transactionComitted();
}
else if (status == TransactionSynchronization.STATUS_ROLLED_BACK) {
transactionRolledback();
}
}
}
}

View File

@@ -0,0 +1,547 @@
/*
* 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.springframework.batch.io.file.support;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.charset.UnsupportedCharsetException;
import java.util.Collection;
import java.util.Iterator;
import java.util.Properties;
import org.springframework.batch.io.ItemWriter;
import org.springframework.batch.io.exception.BatchCriticalException;
import org.springframework.batch.io.exception.BatchEnvironmentException;
import org.springframework.batch.io.file.support.transform.Converter;
import org.springframework.batch.io.support.AbstractTransactionalIoSource;
import org.springframework.batch.item.ResourceLifecycle;
import org.springframework.batch.restart.GenericRestartData;
import org.springframework.batch.restart.RestartData;
import org.springframework.batch.restart.Restartable;
import org.springframework.batch.statistics.StatisticsProvider;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.io.Resource;
import org.springframework.dao.DataAccessResourceFailureException;
import org.springframework.util.Assert;
/**
* This class is an output target that writes data to a file or stream. The
* writer also provides restart, statistics and transaction features by
* implementing corresponding interfaces where possible (with a file). The
* location of the file is defined by a {@link Resource} and must represent a
* writable file.<br/>
*
* Uses buffered writer to improve performance.<br/>
*
* Use {@link #write(String)} method to output a line to an item writer.
*
* @author Waseem Malik
* @author Tomas Slanina
* @author Robert Kasanicky
* @author Dave Syer
*/
public class FlatFileItemWriter extends AbstractTransactionalIoSource implements
ItemWriter, ResourceLifecycle, Restartable, StatisticsProvider, InitializingBean,
DisposableBean {
/**
* @author dsyer
*
*/
public static class BooleanHolder {
public boolean value;
}
private static final String LINE_SEPARATOR = System.getProperty("line.separator");
public static final String WRITTEN_STATISTICS_NAME = "Written";
public static final String RESTART_COUNT_STATISTICS_NAME = "Restart count";
public static final String RESTART_DATA_NAME = "flatfileoutputtemplate.currentLine";
private Resource resource;
private Properties statistics = new Properties();
private RestartData restartData = new GenericRestartData(new Properties());
private OutputState state = new OutputState();
private Converter converter = new Converter() {
public Object convert(Object input) {
return "" + input;
}
};
public void afterPropertiesSet() throws Exception {
Assert.notNull(resource);
File file = resource.getFile();
Assert.state(!file.exists() || file.canWrite(), "Resource is not writable: [" + resource + "]");
}
/**
* Public setter for the converter. If not-null this will be used to convert
* the input data before it is output.
*
* @param converter the converter to set
*/
public void setConverter(Converter converter) {
this.converter = converter;
}
/**
* Setter for resource. Represents a file that can be written.
*
* @param resource
*/
public void setResource(Resource resource) {
this.resource = resource;
}
/**
* Commit the transaction.
*/
protected void transactionCommitted() {
getOutputState().mark();
}
/**
* Rollback the transaction.
*/
protected void transactionRolledBack() {
getOutputState().checkFileSize();
resetPositionForRestart();
}
// This method removes any information in the file before this reset point.
private void resetPositionForRestart() {
getOutputState().truncate();
}
/**
* Writes out a string followed by a "new line", where the format of the new
* line separator is determined by the underlying operating system. If the
* input is not a String and a converter is available the converter will be
* applied and then this method recursively called with the result. If the
* input is an array or collection each value will be written to a separate
* line (recursively calling this method for each value). If no converter is
* supplied the input object's toString method will be used.<br/>
*
* @param data Object (a String or Object that can be converted) to be
* written to output stream
*/
public void write(Object data) {
convertAndWrite(data, new BooleanHolder());
}
/**
* Convert the date to a format that can be output and then write it out.
* @param data
* @param converted
*/
private void convertAndWrite(Object data, BooleanHolder converted) {
if (data instanceof Collection) {
converted.value = false;
for (Iterator iterator = ((Collection) data).iterator(); iterator.hasNext();) {
Object value = (Object) iterator.next();
// (recursive)
write(value);
}
return;
}
if (data.getClass().isArray()) {
converted.value = false;
Object[] array = (Object[]) data;
for (int i = 0; i < array.length; i++) {
Object value = array[i];
// (recursive)
write(value);
}
return;
}
if (data instanceof String) {
// This is where the output stream is actually written to
getOutputState().write(data + LINE_SEPARATOR);
}
else if (!converted.value) {
// (recursive)
converted.value = true;
convertAndWrite(converter.convert(data), converted);
return;
}
else {
// Should not happen...
throw new IllegalStateException(
"Infinite loop detected - converter did not convert to String or collection/array of objects convertible to String.");
}
}
/**
* @see ResourceLifecycle#close()
*/
public void close() {
getOutputState().close();
}
/**
* Calls close to ensure that bean factories can close and always release
* resources.
*
* @see org.springframework.beans.factory.DisposableBean#destroy()
*/
public void destroy() throws Exception {
close();
}
/**
* Sets encoding for output template.
*/
public void setEncoding(String newEncoding) {
getOutputState().setEncoding(newEncoding);
}
/**
* Sets buffer size for output template
*/
public void setBufferSize(int newSize) {
getOutputState().setBufferSize(newSize);
}
/**
* @param shouldDeleteIfExists the shouldDeleteIfExists to set
*/
public void setShouldDeleteIfExists(boolean shouldDeleteIfExists) {
getOutputState().setShouldDeleteIfExists(shouldDeleteIfExists);
}
/**
* Initialize the Output Template.
* @see ResourceLifecycle#open()
*/
public void open() {
super.registerSynchronization();
}
/**
* @see StatisticsProvider
*/
public Properties getStatistics() {
final OutputState os = getOutputState();
statistics.setProperty(WRITTEN_STATISTICS_NAME, String.valueOf(os.linesWritten));
statistics.setProperty(RESTART_COUNT_STATISTICS_NAME, String.valueOf(os.restartCount));
return statistics;
}
/**
* @see Restartable#getRestartData()
*/
public RestartData getRestartData() {
final OutputState os = getOutputState();
restartData.getProperties().setProperty(RESTART_DATA_NAME, String.valueOf(os.position()));
return restartData;
}
/**
* @see Restartable#restoreFrom(RestartData)
*/
public void restoreFrom(RestartData data) {
if (data == null)
return;
getOutputState().restoreFrom(data.getProperties());
}
// Returns object representing state.
private OutputState getOutputState() {
return (OutputState) state;
}
/**
* Encapsulates the runtime state of the writer. All state changing
* operations on the writer go through this class.
*/
private class OutputState {
// default encoding for writing to output files - set to UTF-8.
private static final String DEFAULT_CHARSET = "UTF-8";
private static final int DEFAULT_BUFFER_SIZE = 2048;
// The bufferedWriter over the file channel that is actually written
BufferedWriter outputBufferedWriter;
FileChannel fileChannel;
// this represents the charset encoding (if any is needed) for the
// output file
String encoding = DEFAULT_CHARSET;
// Optional write buffer size
int bufferSize = DEFAULT_BUFFER_SIZE;
boolean restarted = false;
boolean initialized = false;
long lastMarkedByteOffsetPosition = 0;
long linesWritten = 0;
long restartCount = 0;
boolean shouldDeleteIfExists = true;
/**
* Return the byte offset position of the cursor in the output file as a
* long integer.
*/
public long position() {
long pos = 0;
if (fileChannel == null) {
return 0;
}
try {
outputBufferedWriter.flush();
pos = fileChannel.position();
}
catch (IOException e) {
throw new BatchCriticalException("An Error occured while trying to get filechannel position", e);
}
return pos;
}
/**
* @param properties
*/
public void restoreFrom(Properties properties) {
lastMarkedByteOffsetPosition = Long.parseLong(properties.getProperty(RESTART_DATA_NAME));
restarted = true;
}
/**
* @param shouldDeleteIfExists2
*/
public void setShouldDeleteIfExists(boolean shouldDeleteIfExists) {
this.shouldDeleteIfExists = shouldDeleteIfExists;
}
/**
* @param newSize
*/
public void setBufferSize(int newSize) {
bufferSize = newSize;
}
/**
* @param newEncoding
*/
public void setEncoding(String newEncoding) {
encoding = newEncoding;
}
/**
* Close the open resource and reset counters.
*/
public void close() {
initialized = false;
restarted = false;
try {
if (outputBufferedWriter == null) {
return;
}
outputBufferedWriter.close();
fileChannel.close();
}
catch (IOException ioe) {
throw new BatchEnvironmentException("Unable to close the the ItemWriter", ioe);
}
}
/**
* @param data
* @param offset
* @param length
*/
public void write(String line) {
if (!initialized) {
initializeBufferedWriter();
}
try {
outputBufferedWriter.write(line);
outputBufferedWriter.flush();
linesWritten++;
}
catch (IOException e) {
throw new BatchCriticalException("An Error occured while trying to write to FlatFileItemWriter", e);
}
}
/**
* Truncate the output at the last known good point.
*/
public void truncate() {
try {
fileChannel.truncate(lastMarkedByteOffsetPosition);
fileChannel.position(lastMarkedByteOffsetPosition);
}
catch (Exception e) {
throw new BatchCriticalException("An Error occured while reseting position in a file for restart", e);
}
}
/**
* Mark the current position.
*/
public void mark() {
lastMarkedByteOffsetPosition = this.position();
}
/**
* Creates the buffered writer for the output file channel based on
* configuration information.
*/
private void initializeBufferedWriter() {
File file;
try {
file = resource.getFile();
// If the output source was restarted, keep existing file.
// If the output source was not restarted, check following:
// - if the file should be deleted, delete it if it was exiting
// and create blank file,
// - if the file should not be deleted, if it already exists,
// throw an exception,
// - if the file was not existing, create new.
if (!restarted) {
if (file.exists()) {
if (shouldDeleteIfExists) {
file.delete();
}
else {
throw new BatchEnvironmentException("Resource already exists: " + resource);
}
}
String parent = file.getParent();
if (parent!=null) {
new File(parent).mkdirs();
}
file.createNewFile();
}
}
catch (IOException ioe) {
throw new DataAccessResourceFailureException("Unable to write to file resource: [" + resource + "]",
ioe);
}
try {
fileChannel = (new FileOutputStream(file.getAbsolutePath(), true)).getChannel();
}
catch (FileNotFoundException fnfe) {
throw new BatchEnvironmentException("Bad filename property parameter " + file, fnfe);
}
outputBufferedWriter = getBufferedWriter(fileChannel, encoding, bufferSize);
// in case of restarting reset position to last commited point
if (restarted) {
this.resetPosition();
}
initialized = true;
linesWritten = 0;
}
/**
* Returns the buffered writer opened to the beginning of the file
* specified by the absolute path name contained in absoluteFileName.
*/
private BufferedWriter getBufferedWriter(FileChannel fileChannel, String encoding, int bufferSize) {
try {
BufferedWriter outputBufferedWriter = null;
// If a buffer was requested, allocate.
if (bufferSize > 0) {
outputBufferedWriter = new BufferedWriter(Channels.newWriter(fileChannel, encoding), bufferSize);
}
else {
outputBufferedWriter = new BufferedWriter(Channels.newWriter(fileChannel, encoding));
}
return outputBufferedWriter;
}
catch (UnsupportedCharsetException ucse) {
throw new BatchEnvironmentException("Bad encoding configuration for output file " + fileChannel, ucse);
}
}
/**
* Resets the file writer's current position to the point stored in the
* last marked byte offset position variable. It first checks to make
* sure the current size of the file is not less than the byte position
* to be moved to (if it is, throws an environment exception), then it
* truncates the file to that reset position, and set the cursor to
* start writing at that point.
*/
private void resetPosition() {
checkFileSize();
resetPositionForRestart();
}
/**
* Checks (on setState) to make sure that the current output file's size
* is not smaller than the last saved commit point. If it is, then the
* file has been damaged in some way and whole task must be started over
* again from the beginning.
*/
public void checkFileSize() {
long size = -1;
try {
outputBufferedWriter.flush();
size = fileChannel.size();
}
catch (Exception e) {
throw new BatchCriticalException("An Error occured while checking file size", e);
}
if (size < lastMarkedByteOffsetPosition) {
throw new BatchCriticalException("Current file size is smaller than size at last commit");
}
}
}
}

View File

@@ -0,0 +1,359 @@
/*
* 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.springframework.batch.io.file.support;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import org.springframework.batch.io.InputSource;
import org.springframework.batch.io.exception.BatchEnvironmentException;
import org.springframework.batch.io.file.support.separator.DefaultRecordSeparatorPolicy;
import org.springframework.batch.io.file.support.separator.RecordSeparatorPolicy;
import org.springframework.batch.item.ResourceLifecycle;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.core.io.Resource;
import org.springframework.util.Assert;
/**
* An input source that reads lines one by one from a resource. <br/>
*
* A line can consist of multiple lines in the input resource, according to the
* {@link RecordSeparatorPolicy} in force. By default a line is either
* terminated by a newline (as per {@link BufferedReader#readLine()}), or can
* be continued onto the next line if a field surrounded by quotes (\") contains
* a newline.<br/>
*
* Comment lines can be indicated using a line prefix (or collection of
* prefixes) and they will be ignored. The default is "#", so lines starting
* with a pound sign will be ignored.<br/>
*
* All the public methods that interact with the underlying resource (open,
* close, read etc.) are synchronized on this.<br/>
*
* Package private because this is not intended to be a public API - used
* internally by the flat file input sources. That makes abuses of the fact that
* it is stateful easier to control.<br/>
*
* @author Dave Syer
* @author Rob Harrop
*/
class ResourceLineReader implements ResourceLifecycle, InputSource,
DisposableBean {
private static final Collection DEFAULT_COMMENTS = Collections
.singleton("#");
private static final String DEFAULT_ENCODING = "ISO-8859-1";
private static final int READ_AHEAD_LIMIT = 100000;
private final Resource resource;
private final String encoding;
private Collection comments = DEFAULT_COMMENTS;
// Encapsulates the state of the input source.
private State state = null;
private RecordSeparatorPolicy recordSeparatorPolicy = new DefaultRecordSeparatorPolicy();
public ResourceLineReader(Resource resource) throws IOException {
this(resource, DEFAULT_ENCODING);
}
public ResourceLineReader(Resource resource, String encoding) {
Assert.notNull(resource, "'resource' cannot be null.");
Assert.notNull(encoding, "'encoding' cannot be null.");
this.resource = resource;
this.encoding = encoding;
}
/**
* Setter for the {@link RecordSeparatorPolicy}. Default value is a
* {@link DefaultRecordSeparatorPolicy}. Ideally should not be changed once
* a reader is in use, but it would not be fatal if it was.
*
* @param recordSeparatorPolicy
* the new {@link RecordSeparatorPolicy}
*/
public void setRecordSeparatorPolicy(
RecordSeparatorPolicy recordSeparatorPolicy) {
/*
* The rest of the code accesses the policy in synchronized blocks,
* copying the reference before using it. So in principle it can be
* changed in flight - the results might not be what the user expected!
*/
this.recordSeparatorPolicy = recordSeparatorPolicy;
}
/**
* Setter for comment prefixes. Can be used to ignore header lines as well
* by using e.g. the first couple of column names as a prefix.
*
* @param comments
* an array of comment line prefixes.
*/
public void setComments(String[] comments) {
this.comments = new HashSet(Arrays.asList(comments));
}
/**
* Read the next line from the input resource, ignoring comments, and
* according to the {@link RecordSeparatorPolicy}.
*
* @return a String.
*
* @throws BatchEnvironmentException
* if there is an IOException while accessing the input
* resource.
*
* @see org.springframework.batch.item.provider.support.InputSource#read()
*/
public synchronized Object read() {
// Make a copy of the recordSeparatorPolicy reference, in case it is
// changed during a read operation (unlikely, but you never know)...
RecordSeparatorPolicy recordSeparatorPolicy = this.recordSeparatorPolicy;
String line = readLine();
String record = line;
if (line != null) {
StringBuffer buffer = new StringBuffer(record);
while (line != null && !recordSeparatorPolicy.isEndOfRecord(record)) {
buffer.append(recordSeparatorPolicy
.preProcess(line = readLine()));
record = buffer.toString();
}
}
return recordSeparatorPolicy.postProcess(record);
}
/**
* @return
* @throws IOException
*/
private String readLine() {
return getState().readLine();
}
/**
* @return
*/
private State getState() {
if (state == null) {
open();
}
return state;
}
/**
* A no-op because the oobject is initialized with all it needs to open in
* the constructor.
*
* @see org.springframework.batch.item.ResourceLifecycle#open()
*/
public synchronized void open() {
state = new State();
state.open();
}
/**
* Close the reader associated with this input source.
*
* @see org.springframework.batch.io.InputSource#close()
* @throws BatchEnvironmentException
* if there is an {@link IOException} during the close
* operation.
*/
public synchronized void close() {
if (state == null) {
return;
}
try {
state.close();
} finally {
state = null;
}
}
/**
* Calls close to ensure that bean factory releases all resources.
*
* @see org.springframework.beans.factory.DisposableBean#destroy()
*/
public void destroy() throws Exception {
close();
}
/**
* Getter for current line count (not the current number of lines returned).
*
* @return the current line count.
*/
public int getCurrentLineCount() {
return getState().getCurrentLineCount();
}
/**
* Mark the state for return later with reset. Uses the read-ahead limit
* from the underlying {@link BufferedReader}, which means that there is a
* limit to how much data can be recovered if the mark needs to be reset.
*
* @see #reset()
*
* @throws BatchEnvironmentException
* if the mark could not be set.
*/
public synchronized void mark() {
getState().mark();
}
/**
* Reset the reader to the last mark.
*
* @see #mark()
*
* @throws BatchEnvironmentException
* if the reset is unsuccessful, e.g. if the read-ahead limit
* was breached.
*/
public synchronized void reset() {
getState().reset();
}
private boolean isComment(String line) {
for (Iterator iter = comments.iterator(); iter.hasNext();) {
String prefix = (String) iter.next();
if (line.startsWith(prefix)) {
return true;
}
}
return false;
}
private class State {
private BufferedReader reader;
private int currentLineCount = 0;
private int markedLineCount = -1;
public String readLine() {
if (reader == null) {
open();
}
String line = null;
try {
line = this.reader.readLine();
if (line == null) {
return null;
}
currentLineCount++;
while (isComment(line)) {
line = reader.readLine();
if (line == null) {
return null;
}
currentLineCount++;
}
} catch (IOException e) {
throw new BatchEnvironmentException(
"Unable to read from resource '" + resource
+ "' at line " + currentLineCount, e);
}
return line;
}
/**
*
*/
public void open() {
try {
reader = new BufferedReader(new InputStreamReader(resource
.getInputStream(), encoding));
mark();
} catch (IOException e) {
throw new BatchEnvironmentException("Could not open resource",
e);
}
}
/**
* Close the reader and reset the counters.
*/
public void close() {
if (reader == null) {
return;
}
try {
reader.close();
} catch (IOException e) {
throw new BatchEnvironmentException("Could not close reader", e);
} finally {
currentLineCount = 0;
markedLineCount = -1;
}
}
/**
* @return the current line count
*/
public int getCurrentLineCount() {
return currentLineCount;
}
/**
* Mark the underlying reader and set the line counters.
*/
public void mark() {
try {
reader.mark(READ_AHEAD_LIMIT);
markedLineCount = currentLineCount;
} catch (IOException e) {
throw new BatchEnvironmentException("Could not mark reader", e);
}
}
/**
* Reset the reader and line counters to the last marked position if
* possible.
*/
public void reset() {
if (markedLineCount < 0) {
return;
}
try {
this.reader.reset();
currentLineCount = markedLineCount;
} catch (IOException e) {
throw new BatchEnvironmentException("Could not reset reader", e);
}
}
}
}

View File

@@ -0,0 +1,265 @@
/*
* 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.springframework.batch.io.file.support;
import java.io.IOException;
import org.springframework.batch.io.InputSource;
import org.springframework.batch.io.exception.FlatFileParsingException;
import org.springframework.batch.io.file.FieldSet;
import org.springframework.batch.io.file.FieldSetMapper;
import org.springframework.batch.io.file.support.separator.RecordSeparatorPolicy;
import org.springframework.batch.io.file.support.transform.AbstractLineTokenizer;
import org.springframework.batch.io.file.support.transform.DelimitedLineTokenizer;
import org.springframework.batch.io.file.support.transform.LineTokenizer;
import org.springframework.batch.item.ResourceLifecycle;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.io.Resource;
import org.springframework.util.Assert;
/**
* This class represents a basic input source, that reads data from the file and
* returns it as structured tuples in the form of{@link FieldSet} instances.
* The location of the file is defined by the resource property. To separate the
* structure of the file, {@link LineTokenizer} is used to parse data obtained
* from the file. <br/>
*
* A {@link SimpleFlatFileInputSource} is not thread safe because it maintains
* state in the form of a {@link ResourceLineReader}. Be careful to configure a
* {@link SimpleFlatFileInputSource} using an appropriate factory or scope so
* that it is not shared between threads.<br/>
*
* @see FieldSetInputSource
*
* @author Dave Syer
*/
public class SimpleFlatFileInputSource implements InputSource,
InitializingBean, DisposableBean {
// default encoding for input files - set to ISO-8859-1
public static final String DEFAULT_CHARSET = "ISO-8859-1";
private Resource resource;
/**
* Encapsulates the state of the input source. If it is null then we are
* uninitialized.
*/
private ResourceLineReader reader;
private RecordSeparatorPolicy recordSeparatorPolicy;
private String[] comments;
private LineTokenizer tokenizer = new DelimitedLineTokenizer();
private FieldSetMapper fieldSetMapper;
private String encoding = DEFAULT_CHARSET;
private boolean firstLineIsHeader = false;
private int linesToSkip = 0;
/**
* Setter for resource property. The location of an input stream that can be
* read.
*
* @param resource
* @throws IOException
*/
public void setResource(Resource resource) throws IOException {
this.resource = resource;
}
/**
* Public setter for the recordSeparatorPolicy. Used to determine where the
* line endings are and do things like continue over a line ending if inside
* a quoted string.
*
* @param recordSeparatorPolicy
* the recordSeparatorPolicy to set
*/
public void setRecordSeparatorPolicy(
RecordSeparatorPolicy recordSeparatorPolicy) {
this.recordSeparatorPolicy = recordSeparatorPolicy;
}
/**
* Setter for comment prefixes. Can be used to ignore header lines as well
* by using e.g. the first couple of column names as a prefix.
*
* @param comments
* an array of comment line prefixes.
*/
public void setComments(String[] comments) {
this.comments = new String[comments.length];
System.arraycopy(comments, 0, this.comments, 0, comments.length);
}
public void afterPropertiesSet() throws Exception {
Assert.notNull(resource);
Assert.state(resource.exists(), "Resource must exist: [" + resource
+ "]");
Assert.notNull(fieldSetMapper, "FieldSetMapper must not be null.");
}
/**
* Initialize the reader if necessary.
*/
public void open() {
if (reader == null) {
reader = new ResourceLineReader(resource, encoding);
if (recordSeparatorPolicy != null) {
reader.setRecordSeparatorPolicy(recordSeparatorPolicy);
}
if (comments != null) {
reader.setComments(comments);
}
reader.open();
}
for (int i = 0; i < linesToSkip; i++) {
readLine();
}
if (firstLineIsHeader) {
// skip the header
String firstLine = readLine();
// set names in tokenizer if they haven't been set already
if (tokenizer instanceof AbstractLineTokenizer
&& !((AbstractLineTokenizer) tokenizer).hasNames()) {
String[] names = tokenizer.tokenize(firstLine).getValues();
((AbstractLineTokenizer) tokenizer).setNames(names);
}
}
}
/**
* Close and null out the reader.
*
* @see ResourceLifecycle
*/
public void close() {
try {
if (reader != null) {
reader.close();
}
} finally {
reader = null;
}
}
/**
* Calls close to ensure that bean factories can close and always release
* resources.
*
* @see org.springframework.beans.factory.DisposableBean#destroy()
*/
public void destroy() throws Exception {
close();
}
// Reads first valid line.
protected String readLine() {
return (String) getReader().read();
}
/**
* A wrapper for {@link #readFieldSet()} to make this into a real
* {@link InputSource}.
*
* @see org.springframework.batch.io.InputSource#read()
*/
public Object read() {
String line = readLine();
if (line != null) {
try {
FieldSet tokenizedLine = tokenizer.tokenize(line);
return fieldSetMapper.mapLine(tokenizedLine);
} catch (RuntimeException ex) {
// add current line count to message and re-throw
throw new FlatFileParsingException("Parsing error", ex, line,
getReader().getCurrentLineCount());
}
}
return null;
}
/**
* Setter for the encoding for this input source. Default value is
* {@value #DEFAULT_CHARSET}.
*
* @param encoding
* a properties object which possibly contains the encoding for
* this input file;
*/
public void setEncoding(String encoding) {
this.encoding = encoding;
}
/**
* Sets descriptor for this input template.
*/
public void setTokenizer(LineTokenizer lineTokenizer) {
this.tokenizer = lineTokenizer;
}
/**
* Set the FieldSetMapper to be used for each line.
*
* @param fieldSetMapper
*/
public void setFieldSetMapper(FieldSetMapper fieldSetMapper) {
this.fieldSetMapper = fieldSetMapper;
}
// Returns object representing state of the input template.
protected ResourceLineReader getReader() {
if (reader == null) {
open();
// reader is now not null, or else an exception is thrown
}
return reader;
}
/**
* Indicates whether first line is a header. If the tokenizer is an
* {@link AbstractLineTokenizer} and the column names haven't been set
* already then the header will be used to setup column names. Default is
* <code>false</code>.
*/
public void setFirstLineIsHeader(boolean firstLineIsHeader) {
this.firstLineIsHeader = firstLineIsHeader;
}
/**
* Public setter for the number of lines to skip at the start of a file. Can
* be used if the file contains a header without useful (column name)
* information, and without a comment delimiter at the beginning of the
* lines.
*
* @param linesToSkip
* the number of lines to skip
*/
public void setLinesToSkip(int linesToSkip) {
this.linesToSkip = linesToSkip;
}
}

View File

@@ -0,0 +1,302 @@
package org.springframework.batch.io.file.support;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import javax.xml.namespace.QName;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.StartElement;
import org.springframework.batch.io.InputSource;
import org.springframework.batch.io.Skippable;
import org.springframework.batch.io.file.support.stax.DefaultFragmentEventReader;
import org.springframework.batch.io.file.support.stax.DefaultTransactionalEventReader;
import org.springframework.batch.io.file.support.stax.FragmentDeserializer;
import org.springframework.batch.io.file.support.stax.FragmentEventReader;
import org.springframework.batch.io.file.support.stax.TransactionalEventReader;
import org.springframework.batch.item.ResourceLifecycle;
import org.springframework.batch.repeat.synch.BatchTransactionSynchronizationManager;
import org.springframework.batch.restart.GenericRestartData;
import org.springframework.batch.restart.RestartData;
import org.springframework.batch.restart.Restartable;
import org.springframework.batch.statistics.StatisticsProvider;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.io.Resource;
import org.springframework.dao.DataAccessResourceFailureException;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationAdapter;
import org.springframework.util.Assert;
/**
* Input source for reading XML input based on StAX.
*
* It extracts fragments from the input XML document which correspond to records
* for processing. The fragments are wrapped with StartDocument and EndDocument
* events so that the fragments can be further processed like standalone XML
* documents.
*
* @author Robert Kasanicky
*/
public class StaxEventReaderInputSource implements InputSource, ResourceLifecycle, Skippable, Restartable, StatisticsProvider, InitializingBean, DisposableBean {
public static final String READ_COUNT_STATISTICS_NAME = "StaxEventReaderInputSource.readCount";
private static final String RESTART_DATA_NAME = "StaxEventReaderInputSource.recordcount";
private FragmentEventReader fragmentReader;
private TransactionalEventReader txReader;
private FragmentDeserializer fragmentDeserializer;
private Resource resource;
private InputStream inputStream;
private String fragmentRootElementName;
private boolean initialized = false;
private TransactionSynchronization synchronization = new StaxEventReaderInputSourceTransactionSychronization();
private long lastCommitPointRecordCount = 0;
private long currentRecordCount = 0;
private List skipRecords = new ArrayList();
/**
* Read in the next root element from the file, and return it.
*
* @return the next available record, if none exist, return null
* @see org.springframework.batch.io.InputSource#read()
*/
public Object read() {
if (!initialized) {
open();
}
Object item = null;
do {
currentRecordCount++;
if (moveCursorToNextFragment(fragmentReader)) {
fragmentReader.markStartFragment();
item = fragmentDeserializer.deserializeFragment(fragmentReader);
fragmentReader.markFragmentProcessed();
}
} while (skipRecords.contains(new Long(currentRecordCount)));
if (item == null) {
currentRecordCount--;
}
return item;
}
public void close() {
initialized = false;
try {
fragmentReader.close();
inputStream.close();
}
catch (XMLStreamException e) {
throw new DataAccessResourceFailureException("Error while closing event reader", e);
}
catch (IOException e) {
throw new DataAccessResourceFailureException("Error while closing input stream", e);
}
finally {
fragmentReader = null;
inputStream = null;
}
}
public void open() {
registerSynchronization();
try {
inputStream = resource.getInputStream();
txReader = new DefaultTransactionalEventReader(XMLInputFactory
.newInstance().createXMLEventReader(inputStream));
fragmentReader = new DefaultFragmentEventReader(txReader);
}
catch (XMLStreamException xse) {
throw new DataAccessResourceFailureException("Unable to create XML reader", xse);
}
catch (IOException ioe) {
throw new DataAccessResourceFailureException("Unable to get input stream", ioe);
}
initialized = true;
}
public void setResource(Resource resource) {
this.resource = resource;
}
/**
* @param fragmentDeserializer maps xml fragments corresponding to records to
* objects
*/
public void setFragmentDeserializer(FragmentDeserializer fragmentDeserializer) {
this.fragmentDeserializer = fragmentDeserializer;
}
/**
* @param fragmentRootElementName name of the root element of the fragment
* TODO String can be ambiguous due to namespaces, use QName?
*/
public void setFragmentRootElementName(String fragmentRootElementName) {
this.fragmentRootElementName = fragmentRootElementName;
}
/**
* Mark the last read record as 'skipped', so that I will not be returned
* from read() in the case of a rollback.
*
* @see Skippable#skip()
*/
public void skip() {
skipRecords.add(new Long(currentRecordCount));
}
/**
* @return Properties wrapper for the count of records read so far.
*/
public Properties getStatistics() {
Properties statistics = new Properties();
statistics.setProperty(READ_COUNT_STATISTICS_NAME, String.valueOf(currentRecordCount));
return statistics;
}
/**
* Ensure that all required dependencies for the InputSource to run are provided
* after all properties have been set.
*
* @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
* @throws IllegalArgumentException if the Resource, FragmentDeserializer or
* FragmentRootElementName is null, or if the root element is empty.
* @throws IllegalStateException if the Resource does not exist.
*/
public void afterPropertiesSet() throws Exception {
Assert.notNull(resource, "The Resource must not be null.");
Assert.state(resource.exists(), "Input resource does not exist: [" + resource + "]");
Assert.notNull(fragmentDeserializer, "The FragmentDeserializer must not be null.");
Assert.hasLength(fragmentRootElementName, "The FragmentRootElementName must not be null");
}
/**
* @return wrapped count of records read so far.
* @see Restartable#getRestartData()
*/
public RestartData getRestartData() {
Properties restartData = new Properties();
restartData.setProperty(RESTART_DATA_NAME, String.valueOf(currentRecordCount));
return new GenericRestartData(restartData);
}
/**
* Restores the input source for the given restart data by rereading and skipping
* the number of records stored in the RestartData.
*
* @param RestartData that holds the line count from the last commit.
* @throws IllegalStateException if the InputSource has already been initialized
* or if the number of records to read and skip exceeds the available records.
*/
public void restoreFrom(RestartData data) {
Assert.state(!initialized);
if (data == null || data.getProperties() == null ||
data.getProperties().getProperty(RESTART_DATA_NAME) == null) {
return;
}
open();
long restoredRecordCount = Long.parseLong(data.getProperties().getProperty(RESTART_DATA_NAME));
int REASONABLE_ADHOC_COMMIT_FREQUENCY = 100;
while (currentRecordCount <= restoredRecordCount) {
currentRecordCount++;
if (currentRecordCount % REASONABLE_ADHOC_COMMIT_FREQUENCY == 0) {
txReader.onCommit(); // reset the history buffer
}
Assert.state(fragmentReader.hasNext(), "restore point must be before end of input");
fragmentReader.next();
moveCursorToNextFragment(fragmentReader);
}
txReader.onCommit(); // reset the history buffer
}
/**
* Responsible for moving the cursor before the StartElement of the fragment root.
*
* This implementation simply looks for the next corresponding element, it does not care
* about element nesting. You will need to override this method to correctly handle
* composite fragments.
*
* @return <code>true</code> if next fragment was found, <code>false</code> otherwise.
*/
protected boolean moveCursorToNextFragment(XMLEventReader reader) {
try {
while (true) {
while (reader.peek() != null && !reader.peek().isStartElement()) {
reader.nextEvent();
}
if (reader.peek() == null) {
return false;
}
QName startElementName = ((StartElement) reader.peek()).getName();
if (startElementName.getLocalPart().equals(fragmentRootElementName)) {
return true;
} else {
reader.nextEvent();
}
}
}
catch (XMLStreamException e) {
throw new DataAccessResourceFailureException("Error while reading from event reader", e);
}
}
// package visibility method for simulating transaction events in tests
TransactionSynchronization getSynchronization() {
return synchronization;
}
/**
* Encapsulates transaction events for the StaxEventReaderInputSource.
*/
private class StaxEventReaderInputSourceTransactionSychronization extends TransactionSynchronizationAdapter {
/**
* @param status
* @see org.springframework.transaction.support.TransactionSynchronizationAdapter#afterCompletion(int)
*/
public void afterCompletion(int status) {
if (status == TransactionSynchronization.STATUS_COMMITTED) {
lastCommitPointRecordCount = currentRecordCount;
txReader.onCommit();
skipRecords = new ArrayList();
}
else if (status == TransactionSynchronization.STATUS_ROLLED_BACK) {
currentRecordCount = lastCommitPointRecordCount;
txReader.onRollback();
fragmentReader.reset();
}
}
}
public void destroy() throws Exception {
close();
}
private void registerSynchronization() {
BatchTransactionSynchronizationManager.registerSynchronization(synchronization);
}
}

View File

@@ -0,0 +1,509 @@
package org.springframework.batch.io.file.support;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
import javax.xml.stream.XMLEventFactory;
import javax.xml.stream.XMLEventWriter;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import org.springframework.batch.io.ItemWriter;
import org.springframework.batch.io.file.support.stax.NoStartEndDocumentStreamWriter;
import org.springframework.batch.io.file.support.stax.ObjectToXmlSerializer;
import org.springframework.batch.io.support.FileUtils;
import org.springframework.batch.item.ResourceLifecycle;
import org.springframework.batch.repeat.synch.BatchTransactionSynchronizationManager;
import org.springframework.batch.restart.GenericRestartData;
import org.springframework.batch.restart.RestartData;
import org.springframework.batch.restart.Restartable;
import org.springframework.batch.statistics.StatisticsProvider;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.io.Resource;
import org.springframework.dao.DataAccessResourceFailureException;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationAdapter;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
/**
* An implementation of {@link ItemWriter} which uses
* StAX and {@link ObjectToXmlSerializer} for serializing object to XML.
*
* This output source also provides restart, statistics and transaction
* features by implementing corresponding interfaces.
*
* @author Peter Zozom
*
*/
public class StaxEventWriterItemWriter implements ItemWriter, ResourceLifecycle, Restartable,
StatisticsProvider, InitializingBean, DisposableBean {
// 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 = "staxstreamoutputsource.position";
// read statistics property name
public static final String WRITE_STATISTICS_NAME = "staxstreamoutputsource.processedrecordcount";
// file system resource
private Resource resource;
// xml serializer
private ObjectToXmlSerializer serializer;
// 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;
// root element attributes
private Map rootElementAttributes = null;
// signalizes that output source has been initialized
private boolean initialized = false;
// signalizes that marshalling was restarted
private boolean restarted = false;
// 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;
// transaction synchronization object
private TransactionSynchronization synchronization = new StaxEventWriterItemWriterTransactionSychronization();
// byte offset in file channel at last commit point
private long lastCommitPointPosition = 0;
// processed record count at last commit point
private long lastCommitPointRecordCount = 0;
// current count of processed records
private long currentRecordCount = 0;
/**
* Set output file.
*
* @param resource the output file
*/
public void setResource(Resource resource) {
this.resource = resource;
}
/**
* Set Object to XML serializer.
*
* @param serializer the Object to XML serializer
*/
public void setSerializer(ObjectToXmlSerializer serializer) {
this.serializer = serializer;
}
/**
* Get used encoding.
*
* @return the encoding used
*/
public String getEncoding() {
return encoding;
}
/**
* Set encoding to be used for output file.
*
* @param encoding the encoding to be used
*/
public void setEncoding(String encoding) {
this.encoding = encoding;
}
/**
* Get XML version.
*
* @return the XML version used
*/
public String getVersion() {
return version;
}
/**
* Set XML version to be used for output XML.
*
* @param version the XML version to be used
*/
public void setVersion(String version) {
this.version = version;
}
/**
* Get the tag name of the root element.
*
* @return the root element tag name
*/
public String getRootTagName() {
return rootTagName;
}
/**
* Set the tag name of the root element. If not set, default name is used ("root").
*
* @param rootTagName the tag name to be used for the root element
*/
public void setRootTagName(String rootTagName) {
this.rootTagName = rootTagName;
}
/**
* Get attributes of the root element.
*
* @return attributes of the root element
*/
public Map getRootElementAttributes() {
return rootElementAttributes;
}
/**
* Set the root element attributes to be written.
*
* @param rootElementAttributes attributes of the root element
*/
public void setRootElementAttributes(Map rootElementAttributes) {
this.rootElementAttributes = rootElementAttributes;
}
/**
* Set "overwrite" flag for the output file. Flag is ignored when output file processing is restarted.
*
* @param shouldDeleteIfExists
*/
public void setOverwriteOutput(boolean overwriteOutput) {
this.overwriteOutput = overwriteOutput;
}
protected FileChannel getChannel() {
return channel;
}
/**
* @throws Exception
* @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
*/
public void afterPropertiesSet() throws Exception {
Assert.notNull(resource);
Assert.notNull(serializer);
}
/**
* @throws Exception
* @see org.springframework.beans.factory.DisposableBean#destroy()
*/
public void destroy() throws Exception {
close();
}
/**
* Register the input source for transaction synchronization.
*/
private void registerSynchronization() {
BatchTransactionSynchronizationManager.registerSynchronization(synchronization);
}
/**
* Open the output source
*
* @see org.springframework.batch.item.ResourceLifecycle#open()
*/
public void open() {
open(0);
}
/*
* Helper method for opening output source at given file position
*/
private void open(long position) {
registerSynchronization();
File file;
FileOutputStream os = null;
try {
file = resource.getFile();
FileUtils.setUpOutputFile(file, restarted, overwriteOutput);
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();
try {
delegateEventWriter = outputFactory.createXMLEventWriter(os, encoding);
eventWriter = new NoStartEndDocumentStreamWriter(delegateEventWriter);
serializer.setEventWriter(eventWriter);
if (!restarted) {
startDocument(delegateEventWriter);
}
} catch (XMLStreamException xse) {
throw new DataAccessResourceFailureException(
"Unable to write to file resource: [" + resource + "]", xse);
}
initialized = true;
}
/**
* 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
*/
protected void startDocument(XMLEventWriter writer) throws XMLStreamException {
XMLEventFactory factory = XMLEventFactory.newInstance();
//write start document
writer.add(factory.createStartDocument(getEncoding(), getVersion()));
//write root tag
writer.add(factory.createStartElement("", "", getRootTagName()));
//write root tag attributes
if (!CollectionUtils.isEmpty(getRootElementAttributes())) {
for (Iterator i = getRootElementAttributes().entrySet().iterator(); i.hasNext();) {
Map.Entry entry = (Map.Entry)i.next();
writer.add(factory.createAttribute((String)entry.getKey(), (String)entry.getValue()));
}
}
}
/**
* Finishes the XML document. It closes any start tag and writes
* corresponding end tags.
*
* @param writer
* XML event writer
* @throws XMLStreamException
*/
protected void endDocument(XMLEventWriter writer)
throws XMLStreamException {
//writer.writeEndDocument(); <- this doesn't work after restart
//we need to write end tag of the root element manually
writer.flush();
ByteBuffer bbuf = ByteBuffer.wrap(("</" + getRootTagName() + ">").getBytes());
try {
getChannel().write(bbuf);
} catch (IOException ioe) {
throw new DataAccessResourceFailureException(
"Unable to close file resource: [" + resource + "]", ioe);
}
}
/**
* Close the output source.
*
* @see org.springframework.batch.item.ResourceLifecycle#close()
*/
public void close() {
initialized = false;
try {
endDocument(delegateEventWriter);
eventWriter.close();
channel.close();
} catch (XMLStreamException xse) {
throw new DataAccessResourceFailureException(
"Unable to close file resource: [" + resource + "]", xse);
}
catch (IOException ioe) {
throw new DataAccessResourceFailureException(
"Unable to close file resource: [" + resource + "]", ioe);
}
}
/**
* Write the value object to XML stream.
*
* @param output the value object
* @see org.springframework.batch.io.ItemWriter#write(java.lang.Object)
*/
public void write(Object output) {
if (!initialized) {
open();
}
currentRecordCount++;
serializer.serializeObject(output);
}
/**
* Get the restart data.
* @return the restart data
* @see org.springframework.batch.restart.Restartable#getRestartData()
*/
public RestartData getRestartData() {
Properties properties = new Properties();
properties.setProperty(RESTART_DATA_NAME, String.valueOf(getPosition()));
return new GenericRestartData(properties);
}
/**
* Restore processing from provided restart data.
* @param data the restart data
* @see org.springframework.batch.restart.Restartable#restoreFrom(org.springframework.batch.restart.RestartData)
*/
public void restoreFrom(RestartData data) {
long startAtPosition = 0;
//if restart data is provided, restart from provided offset
//otherwise start from beginning
if (data != null && data.getProperties() != null
&& data.getProperties().getProperty(RESTART_DATA_NAME) != null) {
startAtPosition = Long.parseLong(data.getProperties().getProperty(
RESTART_DATA_NAME));
restarted = true;
}
if (!initialized) {
open(startAtPosition);
}
}
/**
* Get actual statistics for output source.
* @return
* @see org.springframework.batch.statistics.StatisticsProvider#getStatistics()
*/
public Properties getStatistics() {
Properties p = new Properties();
p.setProperty(WRITE_STATISTICS_NAME, String.valueOf(currentRecordCount));
return p;
}
/*
* 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();
} 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(long newPosition) {
try {
Assert.state(channel.size() >= lastCommitPointPosition,
"Current file size is smaller than size at last commit");
channel.truncate(newPosition);
channel.position(newPosition);
} catch (IOException e) {
throw new DataAccessResourceFailureException(
"Unable to write to file resource: [" + resource + "]", e);
}
}
/**
* Encapsulates transaction events for the StaxEventWriterOutputSource.
*/
private class StaxEventWriterItemWriterTransactionSychronization extends
TransactionSynchronizationAdapter {
public void afterCompletion(int status) {
if (status == TransactionSynchronization.STATUS_COMMITTED) {
transactionComitted();
} else if (status == TransactionSynchronization.STATUS_ROLLED_BACK) {
transactionRolledback();
}
}
private void transactionComitted() {
lastCommitPointPosition = getPosition();
lastCommitPointRecordCount = currentRecordCount;
}
private void transactionRolledback() {
currentRecordCount = lastCommitPointRecordCount;
//close output
close();
//and reopen it - we do this because we need to reopen stream
//reader at specified position - calling setPosition() is not enough!
restarted = true;
open(lastCommitPointPosition);
}
}
TransactionSynchronization getSynchronization() {
return synchronization;
}
}

View File

@@ -0,0 +1,273 @@
/*
* 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.springframework.batch.io.file.support.mapping;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import org.springframework.batch.io.file.FieldSet;
import org.springframework.batch.io.file.FieldSetMapper;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.beans.NotWritablePropertyException;
import org.springframework.beans.PropertyAccessor;
import org.springframework.beans.PropertyAccessorUtils;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
/**
* {@link FieldSetMapper} implementation based on bean property paths. The
* {@link FieldSet} to be mapped should have field name meta data corresponding
* to bean property paths in a prototype instance of the desired type. The
* prototype instance is initialized either by referring to to object by bean
* name in the enclosing BeanFactory, or by providing a class to instantiate
* reflectively.<br/>
*
* Nested property paths, including indexed properties in maps and collections,
* can be referenced by the {@link FieldSet} names. They will be converted to
* nested bean properties inside the prototype. The {@link FieldSet} and the
* prototype are thus tightly coupled by the fields that are available and those
* that can be initialized. If some of the nested properties are optional (e.g.
* collection members) they need to be removed by a post processor.<br/>
*
* Property name matching is "fuzzy" in the sense that it tolerates close
* matches, as long as the match is unique. For instance:
*
* <ul>
* <li>Quantity = quantity (field names can be capitalised)</li>
* <li>ISIN = isin (acronyms can be lower case bean property names, as per Java
* Beans recommendations)</li>
* <li>DuckPate = duckPate (capitalisation including camel casing)</li>
* <li>ITEM_ID = itemId (capitalisation and replacing word boundary with
* underscore)</li>
* <li>ORDER.CUSTOMER_ID = order.customerId (nested paths are recursively
* checked)</li>
* </ul>
*
* The algorithm used to match a property name is to start with an exact match
* and then search successively through more distant matches until precisely one
* match is found. If more than one match is found there will be an error.
*
* @author Dave Syer
*
*/
public class BeanWrapperFieldSetMapper implements FieldSetMapper,
BeanFactoryAware, InitializingBean {
private String name;
private Class type;
private BeanFactory beanFactory;
private static Map propertiesMatched = new HashMap();
private static int distanceLimit = 5;
/*
* (non-Javadoc)
*
* @see org.springframework.beans.factory.BeanFactoryAware#setBeanFactory(org.springframework.beans.factory.BeanFactory)
*/
public void setBeanFactory(BeanFactory beanFactory) {
this.beanFactory = beanFactory;
}
/**
* The bean name (id) for an object that can be populated from the field set
* that will be passed into {@link #mapLine(FieldSet)}. Typically a
* prototype scoped bean so that a new instance is returned for each field
* set mapped.
*
* Either this property or the type property must be specified, but not
* both.
*
* @param name
* the name of a prototype bean in the enclosing BeanFactory
*/
public void setPrototypeBeanName(String name) {
this.name = name;
}
/**
* Public setter for the type of bean to create instead of using a prototype
* bean. An object of this type will be created from its default constructor
* for every call to {@link #mapLine(FieldSet)}.<br/>
*
* Either this property or the prototype bean name must be specified, but
* not both.
*
* @param type
* the type to set
*/
public void setTargetType(Class type) {
this.type = type;
}
/**
* Check that precisely one of type or prototype bean name is specified.
*
* @throws IllegalStateException
* if neither is set or both properties are set.
*
* @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
*/
public void afterPropertiesSet() throws Exception {
Assert.state(name != null || type != null,
"Either name or type must be provided.");
Assert.state(name == null || type == null,
"Both name and type cannot be specified together.");
}
/**
* Map the {@link FieldSet} to an object retrieved from the enclosing Spring
* context.
*
* @throws NotWritablePropertyException
* if the {@link FieldSet} contains a field that cannot be
* mapped to a bean property.
*
* @see org.springframework.batch.io.file.FieldSetMapper#mapLine(org.springframework.batch.io.file.FieldSet)
*/
public Object mapLine(FieldSet fs) {
Object copy = getBean();
BeanWrapper wrapper = new BeanWrapperImpl(copy);
wrapper.setPropertyValues(getBeanProperties(copy, fs.getProperties()));
return copy;
}
private Object getBean() {
if (name != null) {
return beanFactory.getBean(name);
}
try {
return type.newInstance();
} catch (InstantiationException e) {
ReflectionUtils.handleReflectionException(e);
} catch (IllegalAccessException e) {
ReflectionUtils.handleReflectionException(e);
}
throw new IllegalStateException(
"Internal error: could not create bean instance for mapping."); // should
// not
// happen
}
/**
* @param bean
* @param properties
* @return
*/
private Properties getBeanProperties(Object bean, Properties properties) {
Class cls = bean.getClass();
// Map from field names to property names
Map matches = (Map) propertiesMatched.get(cls);
if (matches == null) {
matches = new HashMap();
propertiesMatched.put(cls, matches);
}
Set keys = new HashSet(properties.keySet());
for (Iterator iter = keys.iterator(); iter.hasNext();) {
String key = (String) iter.next();
if (matches.containsKey(key)) {
switchPropertyNames(properties, key, (String) matches.get(key));
continue;
}
String name = findPropertyName(bean, key);
if (name != null) {
matches.put(key, name);
switchPropertyNames(properties, key, name);
}
}
return properties;
}
private String findPropertyName(Object bean, String key) {
Class cls = bean.getClass();
int index = PropertyAccessorUtils
.getFirstNestedPropertySeparatorIndex(key);
String prefix;
String suffix;
// If the property name is nested recurse down through the properties
// looking for a match.
if (index > 0) {
prefix = key.substring(0, index);
suffix = key.substring(index + 1, key.length());
String nestedName = findPropertyName(bean, prefix);
if (nestedName == null) {
return null;
}
Object nestedValue = new BeanWrapperImpl(bean)
.getPropertyValue(nestedName);
return nestedName + "." + findPropertyName(nestedValue, suffix);
}
String name = null;
int distance = 0;
index = key.indexOf(PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR);
if (index > 0) {
prefix = key.substring(0, index);
suffix = key.substring(index);
} else {
prefix = key;
suffix = "";
}
while (name == null && distance <= distanceLimit) {
String[] candidates = PropertyMatches.forProperty(prefix, cls,
distance).getPossibleMatches();
// If we find precisely one match, then use that one...
if (candidates.length == 1) {
String candidate = candidates[0];
if (candidate.equals(prefix)) { // if it's the same don't
// replace it...
name = key;
} else {
name = candidate + suffix;
}
}
distance++;
}
return name;
}
private void switchPropertyNames(Properties properties, String oldName,
String newName) {
String value = properties.getProperty(oldName);
properties.remove(oldName);
properties.setProperty(newName, value);
}
}

View File

@@ -0,0 +1,191 @@
/*
* 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.springframework.batch.io.file.support.mapping;
import java.beans.PropertyDescriptor;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.springframework.beans.BeanUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* Helper class for calculating bean property matches, according to.
* Used by BeanWrapperImpl to suggest alternatives for an invalid property name.<br/>
*
* Copied and slightly modified from Spring core,
*
* @author Alef Arendsen
* @author Arjen Poutsma
* @author Juergen Hoeller
* @author Dave Syer
*
* @since 1.0
* @see #forProperty(String, Class)
*/
final class PropertyMatches {
//---------------------------------------------------------------------
// Static section
//---------------------------------------------------------------------
/** Default maximum property distance: 2 */
public static final int DEFAULT_MAX_DISTANCE = 2;
/**
* Create PropertyMatches for the given bean property.
* @param propertyName the name of the property to find possible matches for
* @param beanClass the bean class to search for matches
*/
public static PropertyMatches forProperty(String propertyName, Class beanClass) {
return forProperty(propertyName, beanClass, DEFAULT_MAX_DISTANCE);
}
/**
* Create PropertyMatches for the given bean property.
* @param propertyName the name of the property to find possible matches for
* @param beanClass the bean class to search for matches
* @param maxDistance the maximum property distance allowed for matches
*/
public static PropertyMatches forProperty(String propertyName, Class beanClass, int maxDistance) {
return new PropertyMatches(propertyName, beanClass, maxDistance);
}
//---------------------------------------------------------------------
// Instance section
//---------------------------------------------------------------------
private final String propertyName;
private String[] possibleMatches;
/**
* Create a new PropertyMatches instance for the given property.
*/
private PropertyMatches(String propertyName, Class beanClass, int maxDistance) {
this.propertyName = propertyName;
this.possibleMatches = calculateMatches(BeanUtils.getPropertyDescriptors(beanClass), maxDistance);
}
/**
* Return the calculated possible matches.
*/
public String[] getPossibleMatches() {
return possibleMatches;
}
/**
* Build an error message for the given invalid property name,
* indicating the possible property matches.
*/
public String buildErrorMessage() {
StringBuffer buf = new StringBuffer();
buf.append("Bean property '");
buf.append(this.propertyName);
buf.append("' is not writable or has an invalid setter method. ");
if (ObjectUtils.isEmpty(this.possibleMatches)) {
buf.append("Does the parameter type of the setter match the return type of the getter?");
}
else {
buf.append("Did you mean ");
for (int i = 0; i < this.possibleMatches.length; i++) {
buf.append('\'');
buf.append(this.possibleMatches[i]);
if (i < this.possibleMatches.length - 2) {
buf.append("', ");
}
else if (i == this.possibleMatches.length - 2){
buf.append("', or ");
}
}
buf.append("'?");
}
return buf.toString();
}
/**
* Generate possible property alternatives for the given property and
* class. Internally uses the <code>getStringDistance</code> method, which
* in turn uses the Levenshtein algorithm to determine the distance between
* two Strings.
* @param propertyDescriptors the JavaBeans property descriptors to search
* @param maxDistance the maximum distance to accept
*/
private String[] calculateMatches(PropertyDescriptor[] propertyDescriptors, int maxDistance) {
List candidates = new ArrayList();
for (int i = 0; i < propertyDescriptors.length; i++) {
if (propertyDescriptors[i].getWriteMethod() != null) {
String possibleAlternative = propertyDescriptors[i].getName();
if (calculateStringDistance(this.propertyName, possibleAlternative) <= maxDistance) {
candidates.add(possibleAlternative);
}
}
}
Collections.sort(candidates);
return StringUtils.toStringArray(candidates);
}
/**
* Calculate the distance between the given two Strings
* according to the Levenshtein algorithm.
* @param s1 the first String
* @param s2 the second String
* @return the distance value
*/
private int calculateStringDistance(String s1, String s2) {
if (s1.length() == 0) {
return s2.length();
}
if (s2.length() == 0) {
return s1.length();
}
int d[][] = new int[s1.length() + 1][s2.length() + 1];
for (int i = 0; i <= s1.length(); i++) {
d[i][0] = i;
}
for (int j = 0; j <= s2.length(); j++) {
d[0][j] = j;
}
for (int i = 1; i <= s1.length(); i++) {
char s_i = s1.charAt(i - 1);
for (int j = 1; j <= s2.length(); j++) {
int cost;
char t_j = s2.charAt(j - 1);
if (Character.toLowerCase(s_i) == Character.toLowerCase(t_j)) {
cost = 0;
} else {
cost = 1;
}
d[i][j] = Math.min(Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1),
d[i - 1][j - 1] + cost);
}
}
return d[s1.length()][s2.length()];
}
}

View File

@@ -0,0 +1,7 @@
<html>
<body>
<p>
Infrastructure implementations of io file support mapping concerns.
</p>
</body>
</html>

View File

@@ -0,0 +1,43 @@
package org.springframework.batch.io.file.support.oxm;
import java.io.IOException;
import javax.xml.stream.XMLEventWriter;
import javax.xml.transform.Result;
import org.springframework.batch.io.file.support.stax.ObjectToXmlSerializer;
import org.springframework.dao.DataAccessResourceFailureException;
import org.springframework.oxm.Marshaller;
import org.springframework.xml.transform.StaxResult;
/**
* Object to xml serializer that wraps a Spring-OXM
* Marshaller object.
*
* @author Lucas Ward
*
*/
public class MarshallingObjectToXmlSerializer implements ObjectToXmlSerializer{
private Marshaller marshaller;
private Result result;
public MarshallingObjectToXmlSerializer(Marshaller marshaller){
this.marshaller = marshaller;
}
public void setEventWriter(XMLEventWriter writer) {
result = new StaxResult(writer);
}
public void serializeObject(Object output) {
try {
marshaller.marshal(output, result);
} catch (IOException xse) {
throw new DataAccessResourceFailureException(
"Unable to write to file resource: [" + result.getSystemId() + "]", xse);
}
}
}

View File

@@ -0,0 +1,38 @@
package org.springframework.batch.io.file.support.oxm;
import java.io.IOException;
import javax.xml.stream.XMLEventReader;
import org.springframework.batch.io.file.support.stax.FragmentDeserializer;
import org.springframework.dao.DataAccessResourceFailureException;
import org.springframework.oxm.Unmarshaller;
import org.springframework.util.Assert;
import org.springframework.xml.transform.StaxSource;
/**
* Delegates deserializing to Spring-WS {@link Unmarshaller}.
*
* @author Robert Kasanicky
* @author Lucas Ward
*/
public class UnmarshallingFragmentDeserializer implements FragmentDeserializer {
private Unmarshaller unmarshaller;
public UnmarshallingFragmentDeserializer(Unmarshaller unmarshaller){
Assert.notNull(unmarshaller);
this.unmarshaller = unmarshaller;
}
public Object deserializeFragment(XMLEventReader eventReader) {
Object item = null;
try {
item = unmarshaller.unmarshal(new StaxSource(eventReader));
}
catch (IOException e) {
throw new DataAccessResourceFailureException("IO error during unmarshalling", e);
}
return item;
}
}

View File

@@ -0,0 +1,7 @@
<html>
<body>
<p>
Infrastructure implementations of io file support concerns.
</p>
</body>
</html>

View File

@@ -0,0 +1,135 @@
/*
* 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.springframework.batch.io.file.support.separator;
import org.springframework.util.StringUtils;
/**
* A {@link RecordSeparatorPolicy} that treats all lines as record endings, as
* long as they do not have unterminated quotes, and do not end in a
* continuation marker.
*
* @author Dave Syer
*
*/
public class DefaultRecordSeparatorPolicy extends SimpleRecordSeparatorPolicy {
private static final String QUOTE = "\"";
private static final String CONTINUATION = "\\";
private String quoteCharacter = QUOTE;
private String continuation = CONTINUATION;
/**
* Default constructor.
*/
public DefaultRecordSeparatorPolicy() {
this(QUOTE, CONTINUATION);
}
/**
* Convenient constructor with quote character as parameter.
*/
public DefaultRecordSeparatorPolicy(String quoteCharacter) {
this(quoteCharacter, CONTINUATION);
}
/**
* Convenient constructor with quote character and continuation marker as
* parameters.
*/
public DefaultRecordSeparatorPolicy(String quoteCharacter, String continuation) {
super();
this.continuation = continuation;
this.quoteCharacter = quoteCharacter;
}
/**
* Public setter for the quoteCharacter. Defaults to double quote mark.
*
* @param quoteCharacter the quoteCharacter to set
*/
public void setQuoteCharacter(String quoteCharacter) {
this.quoteCharacter = quoteCharacter;
}
/**
* Public setter for the continuation. Defaults to back slash.
*
* @param continuation the continuation to set
*/
public void setContinuation(String continuation) {
this.continuation = continuation;
}
/**
* Return true if the line does not have unterminated quotes (delimited by
* "), and does not end with a continuation marker ('\'). The test for the
* continuation marker ignores whitespace at the end of the line.
*
* @see org.springframework.batch.io.file.support.separator.RecordSeparatorPolicy#isEndOfRecord(java.lang.String)
*/
public boolean isEndOfRecord(String line) {
return !isQuoteUnterminated(line) && !isContinued(line);
}
/**
* If we are in an unterminated quote, add a line separator. Otherwise
* remove the continuation marker (plus whitespace at the end) if it is
* there.
*
* @see org.springframework.batch.io.file.support.separator.SimpleRecordSeparatorPolicy#preProcess(java.lang.String)
*/
public String preProcess(String line) {
if (isQuoteUnterminated(line)) {
return line + "\n";
}
if (isContinued(line)) {
return line.substring(0, line.lastIndexOf(continuation));
}
return line;
}
/**
* Determine if the current line (or buffered concatenation of lines)
* contains an unterminated quote, indicating that the record is continuing
* onto the next line.
*
* @param result
* @return
*/
private boolean isQuoteUnterminated(String line) {
return StringUtils.countOccurrencesOf(line, quoteCharacter) % 2 != 0;
}
/**
* Determine if the current line (or buffered concatenation of lines)
* contains an unterminated quote, indicating that the record is continuing
* onto the next line.
*
* @param result
* @return
*/
private boolean isContinued(String line) {
if (line == null) {
return false;
}
return line.trim().endsWith(continuation);
}
}

View File

@@ -0,0 +1,59 @@
/*
* 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.springframework.batch.io.file.support.separator;
import java.io.BufferedReader;
/**
* Policy for text file-based input sources to determine the end of a record,
* e.g. a record might be a single line, or it might be multiple lines
* terminated by a semicolon.
*
* @author Dave Syer
*
*/
public interface RecordSeparatorPolicy {
/**
* Signal the end of a record based on the content of a line, being the
* latest line read from an input source. The input is what you would expect
* from {@link BufferedReader#readLine()} - i.e. no line separator character
* at the end. But it might have line separators embedded in it.
*
* @param line a String without a newline character at the end.
* @return true if this line is the end of a record.
*/
boolean isEndOfRecord(String line);
/**
* Give the policy a chance to postprocess a record, e.g. remove a suffix.
*
* @param record the complete record.
* @return a modified version of the record if desired.
*/
String postProcess(String record);
/**
* Preprocess a line before it is appended to a record. Can be used to
* remove a prefix or line-continuation marker.
*
* @param line the current line.
* @return the line as it should be appended to a record.
*/
String preProcess(String line);
}

View File

@@ -0,0 +1,54 @@
/*
* 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.springframework.batch.io.file.support.separator;
/**
* Simplest possible {@link RecordSeparatorPolicy} - treats all lines as record
* endings.
*
* @author Dave Syer
*
*/
public class SimpleRecordSeparatorPolicy implements RecordSeparatorPolicy {
/**
* Always returns true.
*
* @see org.springframework.batch.io.file.support.separator.RecordSeparatorPolicy#isEndOfRecord(java.lang.String)
*/
public boolean isEndOfRecord(String line) {
return true;
}
/**
* Pass the record through. Do nothing.
* @see org.springframework.batch.io.file.support.separator.RecordSeparatorPolicy#postProcess(java.lang.String)
*/
public String postProcess(String record) {
return record;
}
/**
* Pass the line through. Do nothing.
* @see org.springframework.batch.io.file.support.separator.RecordSeparatorPolicy#preProcess(java.lang.String)
*/
public String preProcess(String line) {
return line;
}
}

View File

@@ -0,0 +1,84 @@
/*
* 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.springframework.batch.io.file.support.separator;
/**
* A {@link RecordSeparatorPolicy} that looks for an exact match for a String at
* the end of a line (e.g. a semicolon).
*
* @author Dave Syer
*
*/
public class SuffixRecordSeparatorPolicy extends DefaultRecordSeparatorPolicy {
/**
* Default value for record terminator suffix.
*/
public static final String DEFAULT_SUFFIX = ";";
private String suffix = DEFAULT_SUFFIX;
private boolean ignoreWhitespace = true;
/**
* Lines ending in this terminator String signal the end of a record.
*
* @param suffix
*/
public void setSuffix(String suffix) {
this.suffix = suffix;
}
/**
* Flag to indicate that the decision to terminate a record should ignore
* whitespace at the end of the line.
*
* @param ignoreWhitespace
*/
public void setIgnoreWhitespace(boolean ignoreWhitespace) {
this.ignoreWhitespace = ignoreWhitespace;
}
/**
* Return true if the line ends with the specified substring. By default
* whitespace is trimmed before the comparison. Also returns true if the
* line is null, but not if it is empty.
*
* @see org.springframework.batch.io.file.support.separator.RecordSeparatorPolicy#isEndOfRecord(java.lang.String)
*/
public boolean isEndOfRecord(String line) {
if (line == null) {
return true;
}
String trimmed = ignoreWhitespace ? line.trim() : line;
return trimmed.endsWith(suffix);
}
/**
* Remove the suffix from the end of the record.
*
* @see org.springframework.batch.io.file.support.separator.SimpleRecordSeparatorPolicy#postProcess(java.lang.String)
*/
public String postProcess(String record) {
if (record==null) {
return null;
}
return record.substring(0, record.lastIndexOf(suffix));
}
}

View File

@@ -0,0 +1,7 @@
<html>
<body>
<p>
Infrastructure implementations of io file support separator concerns.
</p>
</body>
</html>

View File

@@ -0,0 +1,58 @@
package org.springframework.batch.io.file.support.stax;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.XMLEvent;
/**
* Delegates all functionality to the wrapped reader allowing
* subclasses to override only the methods they want to change.
*
* @author Robert Kasanicky
*/
abstract class AbstractEventReaderWrapper implements XMLEventReader {
protected XMLEventReader wrappedEventReader;
public AbstractEventReaderWrapper(XMLEventReader wrappedEventReader) {
this.wrappedEventReader = wrappedEventReader;
}
public void close() throws XMLStreamException {
wrappedEventReader.close();
}
public String getElementText() throws XMLStreamException {
return wrappedEventReader.getElementText();
}
public Object getProperty(String name) throws IllegalArgumentException {
return wrappedEventReader.getProperty(name);
}
public boolean hasNext() {
return wrappedEventReader.hasNext();
}
public XMLEvent nextEvent() throws XMLStreamException {
return wrappedEventReader.nextEvent();
}
public XMLEvent nextTag() throws XMLStreamException {
return wrappedEventReader.nextTag();
}
public XMLEvent peek() throws XMLStreamException {
return wrappedEventReader.peek();
}
public Object next() {
return wrappedEventReader.next();
}
public void remove() {
wrappedEventReader.remove();
}
}

View File

@@ -0,0 +1,58 @@
package org.springframework.batch.io.file.support.stax;
import javax.xml.namespace.NamespaceContext;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLEventWriter;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.XMLEvent;
/**
* Delegates all functionality to the wrapped writer allowing
* subclasses to override only the methods they want to change.
*
* @author Robert Kasanicky
*/
abstract class AbstractEventWriterWrapper implements XMLEventWriter {
protected XMLEventWriter wrappedEventWriter;
public AbstractEventWriterWrapper(XMLEventWriter wrappedEventWriter) {
this.wrappedEventWriter = wrappedEventWriter;
}
public void add(XMLEvent event) throws XMLStreamException {
wrappedEventWriter.add(event);
}
public void add(XMLEventReader reader) throws XMLStreamException {
wrappedEventWriter.add(reader);
}
public void close() throws XMLStreamException {
wrappedEventWriter.close();
}
public void flush() throws XMLStreamException {
wrappedEventWriter.flush();
}
public NamespaceContext getNamespaceContext() {
return wrappedEventWriter.getNamespaceContext();
}
public String getPrefix(String uri) throws XMLStreamException {
return wrappedEventWriter.getPrefix(uri);
}
public void setDefaultNamespace(String uri) throws XMLStreamException {
wrappedEventWriter.setDefaultNamespace(uri);
}
public void setNamespaceContext(NamespaceContext context) throws XMLStreamException {
wrappedEventWriter.setNamespaceContext(context);
}
public void setPrefix(String prefix, String uri) throws XMLStreamException {
wrappedEventWriter.setPrefix(prefix, uri);
}
}

View File

@@ -0,0 +1,180 @@
package org.springframework.batch.io.file.support.stax;
import java.util.NoSuchElementException;
import javax.xml.namespace.QName;
import javax.xml.stream.XMLEventFactory;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.EndDocument;
import javax.xml.stream.events.EndElement;
import javax.xml.stream.events.StartDocument;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.XMLEvent;
import org.springframework.dao.DataAccessResourceFailureException;
/**
* Default implementation of {@link FragmentEventReader}
*
* @author Robert Kasanicky
*/
public class DefaultFragmentEventReader extends AbstractEventReaderWrapper implements FragmentEventReader {
// true when the next event is the StartElement of next fragment
private boolean startFragmentFollows = false;
// true when the next event is the EndElement of current fragment
private boolean endFragmentFollows = false;
// true while cursor is inside fragment
private boolean insideFragment = false;
// true when reader should behave like the cursor was at the end of document
private boolean fakeDocumentEnd = false;
private StartDocument startDocumentEvent = null;
private EndDocument endDocumentEvent = null;
// fragment root name is remembered so that the matching closing element can
// be identified
private QName fragmentRootName = null;
// counts the occurrences of current fragmentRootName (increased for
// StartElement, decreased for EndElement)
private int matchCounter = 0;
/**
* Caches the StartDocument event for later use.
* @param wrappedEventReader the original wrapped event reader
*/
public DefaultFragmentEventReader(XMLEventReader wrappedEventReader) {
super(wrappedEventReader);
try {
startDocumentEvent = (StartDocument) wrappedEventReader.peek();
}
catch (XMLStreamException e) {
throw new DataAccessResourceFailureException("Error reading start document from event reader", e);
}
endDocumentEvent = XMLEventFactory.newInstance().createEndDocument();
}
public void markStartFragment() {
startFragmentFollows = true;
fragmentRootName = null;
}
public boolean hasNext() {
try {
if (peek() != null) {
return true;
}
}
catch (XMLStreamException e) {
throw new DataAccessResourceFailureException("Error reading XML stream", e);
}
return false;
}
public Object next() {
try {
return nextEvent();
}
catch (XMLStreamException e) {
throw new DataAccessResourceFailureException("Error reading XML stream", e);
}
}
public XMLEvent nextEvent() throws XMLStreamException {
if (fakeDocumentEnd) {
throw new NoSuchElementException();
}
XMLEvent event = wrappedEventReader.peek();
XMLEvent proxyEvent = alterEvent(event, false);
checkFragmentEnd(proxyEvent);
if (event == proxyEvent) {
wrappedEventReader.nextEvent();
}
return proxyEvent;
}
/**
* Sets the endFragmentFollows flag to true if next event is the last event
* of the fragment.
* @param event peek() from wrapped event reader
*/
private void checkFragmentEnd(XMLEvent event) {
if (event.isStartElement() && ((StartElement) event).getName().equals(fragmentRootName)) {
matchCounter++;
}
else if (event.isEndElement() && ((EndElement) event).getName().equals(fragmentRootName)) {
matchCounter--;
if (matchCounter == 0) {
endFragmentFollows = true;
}
}
}
/**
* @param event peek() from wrapped event reader
* @param peek if true do not change the internal state
* @return StartDocument event if peek() points to beginning of fragment
* EndDocument event if cursor is right behind the end of fragment original
* event otherwise
*/
private XMLEvent alterEvent(XMLEvent event, boolean peek) {
if (startFragmentFollows) {
fragmentRootName = ((StartElement) event).getName();
if (!peek) {
startFragmentFollows = false;
insideFragment = true;
}
return startDocumentEvent;
}
else if (endFragmentFollows) {
if (!peek) {
endFragmentFollows = false;
insideFragment = false;
fakeDocumentEnd = true;
}
return endDocumentEvent;
}
return event;
}
public XMLEvent peek() throws XMLStreamException {
if (fakeDocumentEnd) {
return null;
}
return alterEvent(wrappedEventReader.peek(), true);
}
/**
* Finishes reading the fragment in case the fragment was processed without
* being read until the end.
*/
public void markFragmentProcessed() {
if (insideFragment|| startFragmentFollows) {
try {
while (!(nextEvent() instanceof EndDocument)) {
// just read all events until EndDocument
}
}
catch (XMLStreamException e) {
throw new DataAccessResourceFailureException("Error reading XML stream", e);
}
}
fakeDocumentEnd = false;
}
public void reset() {
insideFragment = false;
startFragmentFollows = false;
endFragmentFollows = false;
fakeDocumentEnd = false;
}
}

View File

@@ -0,0 +1,96 @@
package org.springframework.batch.io.file.support.stax;
import java.util.NoSuchElementException;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.XMLEvent;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;
/**
* Class used to wrap XMLEventReader. Events from wrapped reader are stored in
* {@link EventSequence} to support transactions.
*
* @author Tomas Slanina
* @author Robert Kasanicky
*/
public class DefaultTransactionalEventReader extends AbstractEventReaderWrapper implements TransactionalEventReader, InitializingBean {
private EventSequence recorder = new EventSequence();
/**
* Creates instance of this class and wraps XMLEventReader.
*
* @param parent event reader to be wrapped.
*/
public DefaultTransactionalEventReader(XMLEventReader wrappedReader) {
super(wrappedReader);
}
public void afterPropertiesSet() throws Exception {
Assert.notNull(wrappedEventReader);
}
/**
* Callback on transaction rollback.
*/
public void onRollback() {
recorder.reset();
}
/**
* Callback on transacion commit.
*
*/
public void onCommit() {
recorder.clear();
}
/**
* Check if there are more events. Returns true if there are more events and
* false otherwise.
*
* @return true if the event reader has more events, false otherwise
*/
public boolean hasNext() {
return recorder.hasNext() || wrappedEventReader.hasNext();
}
/**
* Get the next XMLEvent
*
* @see XMLEvent
* @throws XMLStreamException if there is an error with the underlying XML.
* @throws NoSuchElementException iteration has no more elements.
*/
public XMLEvent nextEvent() throws XMLStreamException {
if (!recorder.hasNext()) {
recorder.addEvent(wrappedEventReader.nextEvent());
}
return recorder.nextEvent();
}
/**
* Check the next XMLEvent without reading it from the stream. Returns null
* if the stream is at EOF or has no more XMLEvents. A call to peek() will
* be equal to the next return of next().
*
* @see XMLEvent
* @throws XMLStreamException
*/
public XMLEvent peek() throws XMLStreamException {
return (recorder.hasNext()) ? recorder.peek() : wrappedEventReader.peek();
}
/**
* In this implementation throws UnsupportedOperationException.
*/
public void remove() {
throw new UnsupportedOperationException();
}
}

View File

@@ -0,0 +1,92 @@
package org.springframework.batch.io.file.support.stax;
import java.util.ArrayList;
import java.util.List;
import javax.xml.stream.events.XMLEvent;
/**
* Holds a list of XML events, typically corresponding to a single record.
*
* @author tomas.slanina
*/
class EventSequence {
private static final int BEFORE_BEGINNING = -1;
private List events;
private int currentIndex;
/**
* Creates instance of this class.
*
*/
public EventSequence() {
init();
}
/**
* Adds event to the list of stored events.
*
* @param event
*/
public void addEvent(XMLEvent event) {
events.add(event);
}
/**
* Gets next XMLEvent from cache and moves cursor to next event.
* If cache contains no more events, null is returned.
*
* @return
*/
public XMLEvent nextEvent() {
return (hasNext()) ? (XMLEvent)events.get(++currentIndex) :null;
}
/**
* Gets next XMLEvent from cache but cursor remains on the same position.
* If cache contains no more events, null is returned.
*
* @return
*/
public XMLEvent peek() {
return (hasNext()) ? (XMLEvent)events.get(currentIndex+1) :null;
}
/**
* Removes events from the internal cache.
*
*/
public void clear() {
init();
}
/**
* Resets cursor to the cache start.
*
*/
public void reset() {
currentIndex = BEFORE_BEGINNING;
}
/**
* Check if there are more events. Returns true if there are more events and
* false otherwise.
*
* @return true if the event reader has more events, false otherwise
*/
public boolean hasNext() {
return currentIndex + 1 < events.size();
}
private void init() {
events = (events != null) ? new ArrayList(events.size())
: new ArrayList(1000);
reset();
}
}

View File

@@ -0,0 +1,14 @@
package org.springframework.batch.io.file.support.stax;
import javax.xml.stream.XMLEventReader;
/**
* Deserializes XML fragment to domain object.
* XML fragment is a standalone XML document corresponding to a single record.
*
* @author Robert Kasanicky
*/
public interface FragmentDeserializer {
Object deserializeFragment(XMLEventReader eventReader);
}

View File

@@ -0,0 +1,33 @@
package org.springframework.batch.io.file.support.stax;
import javax.xml.stream.XMLEventReader;
/**
* Interface for event readers which support treating XML fragments as standalone XML documents
* by wrapping the fragments with StartDocument and EndDocument events.
*
* @author Robert Kasanicky
*/
public interface FragmentEventReader extends XMLEventReader {
/**
* Tells the event reader its cursor position is exactly before the fragment.
*/
void markStartFragment();
/**
* Tells the event reader the current fragment has been processed.
* If the cursor is still inside the fragment it should be moved
* after the end of the fragment.
*/
void markFragmentProcessed();
/**
* Reset the state of the fragment reader - make it forget
* it assumptions about current position of cursor
* (e.g. in case of rollback of the wrapped reader).
*/
void reset();
}

View File

@@ -0,0 +1,25 @@
package org.springframework.batch.io.file.support.stax;
import javax.xml.stream.XMLEventWriter;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.XMLEvent;
/**
* Delegating XMLEventWriter, which ignores start and end document events,
* but passes through everything else.
*
* @author peter.zozom
* @author Robert Kasanicky
*/
public class NoStartEndDocumentStreamWriter extends AbstractEventWriterWrapper {
public NoStartEndDocumentStreamWriter(XMLEventWriter wrappedEventWriter) {
super(wrappedEventWriter);
}
public void add(XMLEvent event) throws XMLStreamException {
if ((!event.isStartDocument()) && (!event.isEndDocument())) {
wrappedEventWriter.add(event);
}
}
}

View File

@@ -0,0 +1,28 @@
package org.springframework.batch.io.file.support.stax;
import javax.xml.stream.XMLEventWriter;
/**
* Interface wrapping the serialization of an object
* to xml. Primarily useful for abstracting how an object
* is serialized to an XMLEvent from a specific marshaller.
*
* @author Lucas Ward
*
*/
public interface ObjectToXmlSerializer {
/**
* Set event writer objects should be serialized to.
*
* @param writer
*/
void setEventWriter(XMLEventWriter writer);
/**
* Serialize an Object.
*
* @param output
*/
void serializeObject(Object output);
}

View File

@@ -0,0 +1,22 @@
package org.springframework.batch.io.file.support.stax;
import javax.xml.stream.XMLEventReader;
/**
* XMLEventReader with transactional capabilities (ability to rollback to last commit point).
*
* @author Robert Kasanicky
*/
public interface TransactionalEventReader extends XMLEventReader{
/**
* Callback on transaction rollback.
*/
public void onRollback();
/**
* Callback on transaction commit.
*/
public void onCommit();
}

View File

@@ -0,0 +1,83 @@
/*
* 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.springframework.batch.io.file.support.transform;
import java.util.ArrayList;
import java.util.List;
import org.springframework.batch.io.file.FieldSet;
/**
* @author Dave Syer
* @author Robert Kasanicky
*
*/
public abstract class AbstractLineTokenizer implements LineTokenizer {
protected String[] names = new String[0];
/**
* Setter for column names. Optional, but if set, then all lines must have
* as many or fewer tokens.
*
* @param names
*/
public void setNames(String[] names) {
this.names = names;
}
/**
* @return <code>true</code> if column names have been specified
* @see #setNames(String[])
*/
public boolean hasNames() {
if (names != null && names.length > 0) {
return true;
}
return false;
}
/**
* Yields the tokens resulting from the splitting of the supplied
* <code>line</code>.
*
* @param line the line to be tokenised (can be <code>null</code>)
*
* @return the resulting tokens
*/
public FieldSet tokenize(String line) {
if (line == null || line.length()==0) {
return new FieldSet(new String[0]);
}
List tokens = new ArrayList(doTokenize(line));
for (int i=tokens.size(); i<names.length; i++) {
tokens.add(null);
}
String[] values = (String[]) tokens.toArray(new String[tokens.size()]);
if (names.length==0) {
return new FieldSet(values);
}
return new FieldSet(values, names);
}
protected abstract List doTokenize(String line);
}

View File

@@ -0,0 +1,35 @@
/*
* 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.springframework.batch.io.file.support.transform;
/**
* Generic converter interface for transforming an object into another form for
* output or after input.
*
* @author Dave Syer
*
*/
public interface Converter {
/**
* Central method of converter interface.
*
* @param input the input value
* @return an Object, usually of a different type
*/
Object convert(Object input);
}

View File

@@ -0,0 +1,57 @@
/*
* 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.springframework.batch.io.file.support.transform;
/**
* Class used to create string representing object. Values are separated by
* defined delimiter.
*
* @author tomas.slanina
*
*/
public class DelimitedLineAggregator implements LineAggregator {
private String delimiter = ",";
/**
* Method used to create string representing object.
*
* @param args arrays of strings representing data to be stored
* @param lineDescriptor for this implementation this parameter is not
* used
*/
public String aggregate(String[] args) {
StringBuffer buffer = new StringBuffer();
for (int i = 0; i < args.length; i++) {
buffer.append(args[i]);
if (i != (args.length - 1)) {
buffer.append(delimiter);
}
}
return buffer.toString();
}
/**
* Sets the character to be used as a delimiter.
*/
public void setDelimiter(String delimiter) {
this.delimiter = delimiter;
}
}

View File

@@ -0,0 +1,183 @@
/*
* 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.springframework.batch.io.file.support.transform;
import java.util.ArrayList;
import java.util.List;
import org.springframework.batch.io.exception.BatchConfigurationException;
import org.springframework.util.StringUtils;
/**
*
* @author Rob Harrop
* @author Dave Syer
*
*/
public class DelimitedLineTokenizer extends AbstractLineTokenizer {
/**
* Convenient constant for the common case of a tab delimiter.
*/
public static final char DELIMITER_TAB = '\t';
/**
* Convenient constant for the common case of a comma delimiter.
*/
public static final char DELIMITER_COMMA = ',';
/**
* Convenient constant for the common case of a " character used to escape
* delimiters or line endings.
*/
public static final char DEFAULT_QUOTE_CHARACTER = '"';
// the delimiter character used when reading input.
private char delimiter;
private char quoteCharacter = DEFAULT_QUOTE_CHARACTER;
/**
* Create a new instance of the {@link DelimitedLineTokenizer} class for the
* common case where the delimiter is a {@link #DELIMITER_COMMA comma}.
*
* @see #DelimitedLineTokenizer(char)
* @see #DELIMITER_COMMA
*/
public DelimitedLineTokenizer() {
this(DELIMITER_COMMA);
}
/**
* Create a new instance of the {@link DelimitedLineTokenizer} class.
*
* @param delimiter the desired delimiter
*/
public DelimitedLineTokenizer(char delimiter) {
if (delimiter == DEFAULT_QUOTE_CHARACTER) {
throw new BatchConfigurationException("'" + DEFAULT_QUOTE_CHARACTER
+ "' is not allowed as delimiter for tokenizers.");
}
this.delimiter = delimiter;
}
/**
* Setter for the delimiter character.
* @param delimiter
*/
public void setDelimiter(char delimiter) {
this.delimiter = delimiter;
}
/**
* Public setter for the quoteCharacter. The quote character can be used to
* extend a field across line endings or to enclose a String which contains
* the delimiter. Inside a quoted token the quote character can be used to
* escape itself, thus "a""b""c" is tokenized to a"b"c.
*
* @param quoteCharacter the quoteCharacter to set
*
* @see #DEFAULT_QUOTE_CHARACTER
*/
public void setQuoteCharacter(char quoteCharacter) {
this.quoteCharacter = quoteCharacter;
}
/**
* Yields the tokens resulting from the splitting of the supplied
* <code>line</code>.
*
* @param line the line to be tokenized
*
* @return the resulting tokens
*/
protected List doTokenize(String line) {
List tokens = new ArrayList();
//line is never null in current implementation
//line is checked in parent: AbstractLineTokenizer.tokenize()
char[] chars = line.toCharArray();
boolean inQuoted = false;
char lastChar = 0;
int lastCut = 0;
int length = chars.length;
for (int i = 0; i < length; i++) {
char currentChar = chars[i];
boolean isEnd = (i == (length - 1));
if ((isDelimiterCharacter(currentChar) && !inQuoted) || isEnd) {
int endPosition = (isEnd ? (length - lastCut) : (i - lastCut));
if (isEnd && isDelimiterCharacter(currentChar)) {
endPosition--;
}
String value = null;
if (isQuoteCharacter(lastChar) || isQuoteCharacter(currentChar)) {
value = new String(chars, lastCut + 1, endPosition - 2);
value = StringUtils.replace(value, "" + quoteCharacter + quoteCharacter, "" + quoteCharacter);
}
else {
value = new String(chars, lastCut, endPosition);
}
tokens.add(value);
if (isEnd && (isDelimiterCharacter(currentChar))) {
tokens.add("");
}
lastCut = i + 1;
}
else if (isQuoteCharacter(currentChar)) {
inQuoted = !inQuoted;
}
lastChar = currentChar;
}
return tokens;
}
/**
* Is the supplied character the delimiter character?
*
* @param c the character to be checked
* @return <code>true</code> if the supplied character is the delimiter
* character
* @see DelimitedLineTokenizer#DelimitedLineTokenizer(char)
*/
private boolean isDelimiterCharacter(char c) {
return c == this.delimiter;
}
/**
* Is the supplied character a quote character?
*
* @param c the character to be checked
* @return <code>true</code> if the supplied character is an quote
* character
* @see #setQuoteCharacter(char)
*/
protected boolean isQuoteCharacter(char c) {
return c == quoteCharacter;
}
}

View File

@@ -0,0 +1,177 @@
/*
* 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.springframework.batch.io.file.support.transform;
import java.util.Arrays;
import org.springframework.util.Assert;
/**
* LineAggregator implementation which produces line by aggregating provided
* strings into columns with fixed length. Columns are specified by array of
* ranges ({@link #setColumns(Range[])}.</br>
*
* @author tomas.slanina
* @author peter.zozom
* @author Dave Syer
*/
public class FixedLengthLineAggregator implements LineAggregator {
private static final int ALIGN_CENTER = 1;
private static final int ALIGN_RIGHT = 2;
private static final int ALIGN_LEFT = 3;
private Range[] ranges;
private int lastColumn;
private int align = ALIGN_LEFT;
private char padding = ' ';
/**
* Set column ranges. Used in conjunction with the
* {@link RangeArrayPropertyEditor} this property can be set in the form of
* a String describing the range boundaries, e.g. "1,4,7" or "1-3,4-6,7" or
* "1-2,4-5,7-10".
*
* @param columns
* array of Range objects which specify column start and end
* position
*/
public void setColumns(Range[] columns) {
Assert.notNull(columns);
lastColumn = findLastColumn(columns);
this.ranges = columns;
}
/**
* Aggregate provided strings into single line using specified column
* ranges.
*
* @param args
* arrays of strings representing data to be aggregated
* @return aggregated strings
*/
public String aggregate(String[] args) {
Assert.notNull(args);
Assert.notNull(ranges);
Assert.isTrue(args.length <= ranges.length,
"Number of arguments must match number of fields in a record");
// calculate line length
int lineLength = ranges[lastColumn].hasMaxValue() ? ranges[lastColumn]
.getMax() : ranges[lastColumn].getMin()
+ args[lastColumn].length() - 1;
// create stringBuffer with length of line filled with padding
// characters
char[] emptyLine = new char[lineLength];
Arrays.fill(emptyLine, padding);
StringBuffer stringBuffer = new StringBuffer(lineLength);
stringBuffer.append(emptyLine);
// aggregate all strings
for (int i = 0; i < args.length; i++) {
// offset where text will be inserted
int start = ranges[i].getMin() - 1;
// calculate column length
int columnLength;
if ((i == lastColumn) && (!ranges[lastColumn].hasMaxValue())) {
columnLength = args[lastColumn].length();
} else {
columnLength = ranges[i].getMax() - ranges[i].getMin() + 1;
}
String textToInsert = (args[i] == null) ? "" : args[i];
Assert
.isTrue(columnLength >= textToInsert.length(),
"Supplied text: " + textToInsert
+ " is longer than defined length: "
+ columnLength);
switch (align) {
case ALIGN_RIGHT:
start += (columnLength - textToInsert.length());
break;
case ALIGN_CENTER:
start += ((columnLength - textToInsert.length()) / 2);
break;
case ALIGN_LEFT:
// nothing to do
break;
}
stringBuffer.replace(start, start + textToInsert.length(),
textToInsert);
}
return stringBuffer.toString();
}
/**
* Recognized alignments are <code>CENTER, RIGHT, LEFT</code>. An
* IllegalArgumentException is thrown in case the argument does not match
* any of the recognized values.
*
* @param alignment
* the alignment to be used
*/
public void setAlignment(String alignment) {
if ("CENTER".equalsIgnoreCase(alignment)) {
this.align = ALIGN_CENTER;
} else if ("RIGHT".equalsIgnoreCase(alignment)) {
this.align = ALIGN_RIGHT;
} else if ("LEFT".equalsIgnoreCase(alignment)) {
this.align = ALIGN_LEFT;
} else {
throw new IllegalArgumentException(
"Only 'CENTER', 'RIGHT' or 'LEFT' are allowed alignment values");
}
}
/**
* Setter for padding (default is space).
*
* @param padding
* the padding character
*/
public void setPadding(char padding) {
this.padding = padding;
}
/*
* Find last column. Columns are not sorted. Returns index of last column
* (column with highest offset).
*/
private int findLastColumn(Range[] columns) {
int lastOffset = 1;
int lastIndex = 0;
for (int i = 0; i < columns.length; i++) {
if (columns[i].getMin() > lastOffset) {
lastOffset = columns[i].getMin();
lastIndex = i;
}
}
return lastIndex;
}
}

View File

@@ -0,0 +1,80 @@
/*
* 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.springframework.batch.io.file.support.transform;
import java.util.ArrayList;
import java.util.List;
/**
* Tokenizer used to process data obtained from files with fixed-length format.
* Columns are specified by array of Range objects ({@link #setColumns(Range[])}).
*
* @author tomas.slanina
* @author peter.zozom
* @author Dave Syer
*/
public class FixedLengthTokenizer extends AbstractLineTokenizer {
private Range[] ranges;
/**
* Set the column ranges. Used in conjunction with the
* {@link RangeArrayPropertyEditor} this property can be set in the form of
* a String describing the range boundaries, e.g. "1,4,7" or "1-3,4-6,7" or
* "1-2,4-5,7-10".
*
* @param ranges the column ranges expected in the input
*/
public void setColumns(Range[] ranges) {
this.ranges = ranges;
}
/**
* Yields the tokens resulting from the splitting of the supplied
* <code>line</code>.
*
* @param line
* the line to be tokenised (can be <code>null</code>)
*
* @return the resulting tokens (empty if the line is null)
*/
protected List doTokenize(String line) {
List tokens = new ArrayList(ranges.length);
int lineLength;
String token;
lineLength = line.length();
for (int i = 0; i < ranges.length; i++) {
int startPos = ranges[i].getMin() - 1;
int endPos = ranges[i].getMax();
if (lineLength >= endPos) {
token = line.substring(startPos, endPos);
} else if (lineLength >= startPos) {
token = line.substring(startPos);
} else {
token = "";
}
tokens.add(token);
}
return tokens;
}
}

View File

@@ -0,0 +1,34 @@
/*
* 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.springframework.batch.io.file.support.transform;
/**
* Interface used to create string used to create string representing object.
* @author tomas.slanina
*
*/
public interface LineAggregator {
/**
* Method used to create a string to be stored from the array of values.
*
* @param args values to be stored
* @param lineDescriptor structure of final string
* @return
*/
public String aggregate(String[] args);
}

View File

@@ -0,0 +1,38 @@
/*
* 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.springframework.batch.io.file.support.transform;
import org.springframework.batch.io.file.FieldSet;
/**
* Interface that is used by framework to split string obtained typically from a
* file into tokens.
*
* @author tomas.slanina
*
*/
public interface LineTokenizer {
/**
* Yields the tokens resulting from the splitting of the supplied
* <code>line</code>.
*
* @param line the line to be tokenized (can be <code>null</code>)
*
* @return the resulting tokens
*/
FieldSet tokenize(String line);
}

View File

@@ -0,0 +1,67 @@
/*
* 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.springframework.batch.io.file.support.transform;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.batch.io.file.FieldSet;
public class PrefixMatchingCompositeLineTokenizer implements LineTokenizer {
private Map tokenizers = new HashMap();
public void setTokenizers(Map tokenizers) {
this.tokenizers = new LinkedHashMap(tokenizers);
}
public FieldSet tokenize(String line) {
if (line==null) {
return new FieldSet(new String[0]);
}
LineTokenizer tokenizer = null;
LineTokenizer defaultTokenizer = null;
for (Iterator iter = tokenizers.keySet().iterator(); iter.hasNext();) {
String key = (String) iter.next();
if ("".equals(key)) {
defaultTokenizer = (LineTokenizer) tokenizers.get(key);
// don't break here or the tokenizer may not be found
continue;
}
if (line.startsWith(key)) {
tokenizer = (LineTokenizer) tokenizers.get(key);
break;
}
}
if (tokenizer==null) {
tokenizer = defaultTokenizer;
}
if (tokenizer==null) {
throw new IllegalStateException("Could not match record to tokenizer for line=["+line+"]");
}
return tokenizer.tokenize(line);
}
}

View File

@@ -0,0 +1,62 @@
package org.springframework.batch.io.file.support.transform;
import org.springframework.util.Assert;
/**
* A class to represent ranges. A Range can have minimum/maximum values from
* interval &lt;1,Integer.MAX_VALUE-1&gt; A Range can be unbounded at maximum
* side. This can be specified by passing {@link Range#UPPER_BORDER_NOT_DEFINED}} as max
* value or using constructor {@link #Range(int)}.
*
* @author peter.zozom
*/
public class Range {
private final static int UPPER_BORDER_NOT_DEFINED = Integer.MAX_VALUE;
private int min;
private int max;
public Range(int min) {
checkMinMaxValues(min, UPPER_BORDER_NOT_DEFINED);
this.min = min;
this.max = UPPER_BORDER_NOT_DEFINED;
}
public Range(int min, int max) {
checkMinMaxValues(min, max);
this.min = min;
this.max = max;
}
public int getMax() {
return max;
}
public int getMin() {
return min;
}
public void setMax(int max) {
checkMinMaxValues(this.min, max);
this.max = max;
}
public void setMin(int min) {
checkMinMaxValues(min, this.max);
this.min = min;
}
public boolean hasMaxValue() {
return max != UPPER_BORDER_NOT_DEFINED;
}
public String toString() {
return hasMaxValue() ? min + "-" + max : String.valueOf(min);
}
private void checkMinMaxValues(int min, int max) {
Assert.isTrue(min>0, "Min value must be higher than zero");
Assert.isTrue(min<=max, "Min value should be lower or equal to max value");
}
}

View File

@@ -0,0 +1,131 @@
package org.springframework.batch.io.file.support.transform;
import java.beans.PropertyEditorSupport;
import java.util.Arrays;
import java.util.Comparator;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Property editor implementation which parses string and creates array of
* ranges. Ranges can be provided in any order. </br> Input string should be
* provided in following format: 'range1, range2, range3,...' where range is
* specified as:
* <ul>
* <li>'X-Y', where X is minimum value and Y is maximum value (condition X<=Y
* is verified)</li>
* <li>or 'Z', where Z is minimum and maximum is calculated as (minimum of
* adjacent range - 1). Maximum of the last range is never calculated. Range
* stays unbound at maximum side if maximum value is not provided.</li>
* </ul>
* Minimum and maximum values can be from interval &lt;1, Integer.MAX_VALUE-1&gt;
* <p>
* Examples:</br>
* '1, 15, 25, 38, 55-60' is equal to '1-14, 15-24, 25-37, 38-54, 55-60' </br>
* '36, 14, 1-10, 15, 49-57' is equal to '36-48, 14-14, 1-10, 15-35, 49-57'
* <p>
* Property editor also allows to validate whether ranges are disjoint. Validation
* can be turned on/off by using {@link #forceDisjointRanges}. By default
* validation is turned off.
*
* @author peter.zozom
*/
public class RangeArrayPropertyEditor extends PropertyEditorSupport {
private boolean forceDisjointRanges = false;
/**
* Set force disjoint ranges. If set to TRUE, ranges are validated to be disjoint.
* For example: defining ranges '1-10, 5-15' will cause IllegalArgumentException in
* case of forceDisjointRanges=TRUE.
* @param forceDisjointRanges
*/
public void setForceDisjointRanges(boolean forceDisjointRanges) {
this.forceDisjointRanges = forceDisjointRanges;
}
public void setAsText(String text) throws IllegalArgumentException {
//split text into ranges
String[] strRanges = text.split(",");
Range[] ranges = new Range[strRanges.length];
//parse ranges and create array of Range objects
for (int i = 0; i < strRanges.length; i++) {
String[] range = strRanges[i].split("-");
int min;
int max;
if ((range.length == 1) && (StringUtils.hasText(range[0]))) {
min = Integer.parseInt(range[0].trim());
// correct max value will be assigned later
ranges[i] = new Range(min);
} else if ((range.length == 2) && (StringUtils.hasText(range[0]))
&& (StringUtils.hasText(range[1]))) {
min = Integer.parseInt(range[0].trim());
max = Integer.parseInt(range[1].trim());
ranges[i] = new Range(min,max);
} else {
throw new IllegalArgumentException("Range[" + i + "]: range (" + strRanges[i] + ") is invalid");
}
}
setMaxValues(ranges);
setValue(ranges);
}
public String getAsText() {
Range[] ranges = (Range[])getValue();
StringBuffer sb = new StringBuffer();
for (int i = 0; i < ranges.length; i++) {
if(i>0) {
sb.append(", ");
}
sb.append(ranges[i]);
}
return sb.toString();
}
private void setMaxValues(Range[] ranges) {
//clone array, original array should stay same
Range[] c = (Range[])ranges.clone();
//sort array of Ranges
Arrays.sort(c, new Comparator() {
public int compare(Object o1, Object o2) {
Range c1 = (Range)o1;
Range c2 = (Range)o2;
return c1.getMin()-c2.getMin();
}
}
);
//set max values for all unbound ranges (except last range)
for (int i = 0; i < c.length - 1; i++) {
if (!c[i].hasMaxValue()) {
//set max value to (min value - 1) of the next range
c[i].setMax(c[i+1].getMin() - 1);
}
}
if (forceDisjointRanges) {
verifyRanges(c);
}
}
private void verifyRanges(Range[] ranges) {
//verify that ranges are disjoint
for(int i = 1; i < ranges.length;i++) {
Assert.isTrue(ranges[i-1].getMax() < ranges[i].getMin(),
"Ranges must be disjoint. Range[" + (i-1) + "]: (" + ranges[i-1] +
") Range[" + i +"]: (" + ranges[i] + ")");
}
}
}

View File

@@ -0,0 +1,7 @@
<html>
<body>
<p>
Infrastructure implementations of io file support transform concerns.
</p>
</body>
</html>

View File

@@ -0,0 +1,7 @@
<html>
<body>
<p>
Infrastructure implementations of io concerns.
</p>
</body>
</html>

View File

@@ -0,0 +1,99 @@
/*
* 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.springframework.batch.io.support;
import org.springframework.batch.io.InputSource;
import org.springframework.batch.io.ItemWriter;
import org.springframework.batch.repeat.synch.BatchTransactionSynchronizationManager;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationAdapter;
import org.springframework.transaction.support.TransactionSynchronizationManager;
/**
* <p>
* Abstract class that abstracts away transaction handling from input and
* output. Any {@link InputSource} or {@link ItemWriter} that wants to be
* notified of transaction events to maintain the contract that all calls to
* read or write can extend this base class to ensure that correct ordering is
* maintained regardless of rollbacks.
* </p>
*
* <p>
* This class is primarily useful because it allows its subclasses to implement
* a single method to be notified of a commit or rollback, rather than having an
* inner class that implements {@link TransactionSyncrhonization} and likely
* calls another method with similar semantics as commit and rollback.
* </p>
*
* <p>
* It should be noted that this implementation will only register for
* synchronization if a call to registerSynchronization() has been made. This is
* less than ideal, however, it is the best solution until {@link StepScope} is
* modified to handle registering synchronizations in a scoped manner.
* Otherwise, registering at instantiation or initialization (such as via the
* Spring {@link InitializingBean} interface) would cause commits to be called
* on input sources for all steps, rather than the currently running step.
* </p>
*
* @author Lucas Ward
* @since 1.0
* @see TransactionSynchronization
* @see TransactionSynchronizationManager
*/
public abstract class AbstractTransactionalIoSource {
private final TransactionSynchronization synchronization = new AbstractTransactionalIoSourceTransactionSynchronization();
/**
* Register for Synchronization. This method is left protected because
* clients of this class should not be registering for synchronization, but
* rather only subclasses, at the appropriate time, i.e. when they are not
* initialized.
*/
protected void registerSynchronization() {
BatchTransactionSynchronizationManager
.registerSynchronization(synchronization);
}
/*
* Called when a transaction has been committed.
*
* @see TransactionSynchronization#afterCompletion
*/
protected abstract void transactionCommitted();
/*
* Called when a transaction has been rolled back.
*
* @see TransactionSynchronization#afterCompletion
*/
protected abstract void transactionRolledBack();
/**
* Encapsulates transaction events handling.
*/
private class AbstractTransactionalIoSourceTransactionSynchronization extends
TransactionSynchronizationAdapter {
public void afterCompletion(int status) {
if (status == TransactionSynchronization.STATUS_ROLLED_BACK) {
transactionRolledBack();
} else if (status == TransactionSynchronization.STATUS_COMMITTED) {
transactionCommitted();
}
}
}
}

View File

@@ -0,0 +1,66 @@
package org.springframework.batch.io.support;
import java.io.File;
import java.io.IOException;
import org.springframework.dao.DataAccessResourceFailureException;
import org.springframework.util.Assert;
/**
* Utility methods for files used in batch processing.
*
* @author Peter Zozom
*/
public class FileUtils {
// forbids instantiation
private FileUtils() {}
/**
* Set up output file for batch processing. This method implements common logic for
* handling output files when starting or restarting job/step.
* When starting output file processing, method creates/overwrites new file.
* When restarting output file processing, method checks whether file is writable.
*
* @param file file to be set up
* @param restarted TRUE signalizes that we are restarting output file processing
* @param overwriteOutputFile If set to TRUE, output file will be overwritten
* (this flag is ignored when processing is restart)
*
* @throws IllegalArgumentException when file is NULL
* @throws IllegalStateException when staring output file processing, file exists and
* flag "shouldDeleteExisting" is set to FALSE
* @throws DataAccessResourceFailureException when unable to create file or file is not writable
*/
public static void setUpOutputFile(File file, boolean restarted,
boolean overwriteOutputFile) {
Assert.notNull(file);
try {
if (!restarted) {
if (file.exists()) {
if(!overwriteOutputFile){
throw new DataAccessResourceFailureException("File already exists: ["
+ file.getAbsolutePath() + "]");
}
file.delete();
}
if (file.getParent() != null ) {
new File(file.getParent()).mkdirs();
}
file.createNewFile();
}
} catch (IOException ioe) {
throw new DataAccessResourceFailureException(
"Unable to create file: [" + file.getAbsolutePath() + "]",
ioe);
}
if (!file.canWrite()) {
throw new DataAccessResourceFailureException(
"File is not writable: [" + file.getAbsolutePath() + "]");
}
}
}

View File

@@ -0,0 +1,289 @@
/*
* 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.springframework.batch.io.support;
import java.util.HashSet;
import java.util.Set;
import org.hibernate.SessionFactory;
import org.springframework.batch.io.ItemWriter;
import org.springframework.batch.repeat.ExitStatus;
import org.springframework.batch.repeat.RepeatContext;
import org.springframework.batch.repeat.RepeatInterceptor;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.AttributeAccessor;
import org.springframework.orm.hibernate3.HibernateOperations;
import org.springframework.orm.hibernate3.HibernateTemplate;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.util.Assert;
/**
* {@link ItemWriter} that is aware of the Hibernate session and can take some
* responsibilities to do with chunk boundaries away from a less smart
* {@link ItemWriter} (the delegate). A delegate is required, and will be used
* to do the actual writing of the item.<br/>
*
* This class implements {@link RepeatInterceptor} and it will only work if
* properly registered. If the delegate is also a {@link RepeatInterceptor} then
* it does not need to be separately registered as we make the callbacks here in
* the right places.
*
* @author Dave Syer
*
*/
public class HibernateAwareItemWriter implements ItemWriter, RepeatInterceptor,
InitializingBean {
/**
* Key for items processed in the current transaction {@link RepeatContext}.
*/
private static final String ITEMS_PROCESSED = HibernateAwareItemWriter.class
.getName()
+ ".ITEMS_PROCESSED";
/**
* Key for {@link RepeatContext} in transaction resource context.
*/
private static final String WRITER_REPEAT_CONTEXT = HibernateAwareItemWriter.class
.getName()
+ ".WRITER_REPEAT_CONTEXT";
private Set failed = new HashSet();
private ItemWriter delegate;
private HibernateOperations hibernateTemplate;
/**
* Public setter for the {@link ItemWriter} property.
*
* @param delegate
* the delegate to set
*/
public void setDelegate(ItemWriter delegate) {
this.delegate = delegate;
}
/**
* Public setter for the {@link HibernateOperations} property.
*
* @param hibernateTemplate
* the hibernateTemplate to set
*/
public void setHibernateTemplate(HibernateOperations hibernateTemplate) {
this.hibernateTemplate = hibernateTemplate;
}
/**
* Set the Hibernate SessionFactory to be used internally. Will
* automatically create a HibernateTemplate for the given SessionFactory.
*
* @see #setHibernateTemplate
*/
public final void setSessionFactory(SessionFactory sessionFactory) {
this.hibernateTemplate = new HibernateTemplate(sessionFactory);
;
}
/**
* Check mandatory properties - there must be a delegate.
*
* @see org.springframework.dao.support.DaoSupport#initDao()
*/
public void afterPropertiesSet() throws Exception {
Assert
.notNull(delegate,
"HibernateAwareItemWriter requires an ItemWriter as a delegate.");
Assert.notNull(hibernateTemplate,
"HibernateAwareItemWriter requires a HibernateOperations");
}
/**
* Use the delegate to actually do the writing, but flush aggressively if
* the item was previously part of a failed chunk.
*
* @see org.springframework.batch.io.OutputSource#write(java.lang.Object)
*/
public void write(Object output) {
getProcessed().add(output);
delegate.write(output);
flushIfNecessary(output);
}
/**
* Does nothing unless the delegate is also a {@link RepeatInterceptor} in
* which case pass on the call to him.
*
* @see org.springframework.batch.repeat.RepeatInterceptor#before(org.springframework.batch.repeat.RepeatContext)
*/
public void before(RepeatContext context) {
if (delegate instanceof RepeatInterceptor) {
RepeatInterceptor interceptor = (RepeatInterceptor) delegate;
interceptor.before(context);
}
}
/**
* Does nothing unless the delegate is also a {@link RepeatInterceptor} in
* which case pass on the call to him.
*
* @see org.springframework.batch.repeat.RepeatInterceptor#after(org.springframework.batch.repeat.RepeatContext,
* org.springframework.batch.repeat.ExitStatus)
*/
public void after(RepeatContext context, ExitStatus result) {
if (delegate instanceof RepeatInterceptor) {
RepeatInterceptor interceptor = (RepeatInterceptor) delegate;
interceptor.after(context, result);
}
}
/**
* Flush the Hibernate session so that any batch exceptions are within the
* RepeatContext. If the delegate is also a {@link RepeatInterceptor} then
* it will be given the call before flushing.
*
*
* @see org.springframework.batch.repeat.RepeatInterceptor#close(org.springframework.batch.repeat.RepeatContext)
*/
public void close(RepeatContext context) {
try {
if (delegate instanceof RepeatInterceptor) {
RepeatInterceptor interceptor = (RepeatInterceptor) delegate;
interceptor.close(context);
}
flush();
} catch (RuntimeException e) {
synchronized (failed) {
failed.addAll(getProcessed());
}
// onError will not be called after close() by the framework so we
// have to do it here.
onError(context, e);
throw e;
}
unsetContext();
}
/**
* Wrapper for Hibernate flush.
*/
private void flush() {
hibernateTemplate.flush();
// This should happen when the transaction commits anyway, but to be
// sure...
hibernateTemplate.clear();
}
/**
* Does nothing unless the delegate is also a {@link RepeatInterceptor} in
* which case pass on the call to him.
*
* @see org.springframework.batch.repeat.RepeatInterceptor#onError(org.springframework.batch.repeat.RepeatContext,
* java.lang.Throwable)
*/
public void onError(RepeatContext context, Throwable e) {
if (delegate instanceof RepeatInterceptor) {
RepeatInterceptor interceptor = (RepeatInterceptor) delegate;
interceptor.onError(context, e);
}
}
/**
* Sets up the context as a transaction resource so that we can store state
* and refer back to it in the {@link #write(Object)} method. If the
* delegate is also a {@link RepeatInterceptor} then it will be given the
* call afterwards.
*
* @see org.springframework.batch.repeat.RepeatInterceptor#open(org.springframework.batch.repeat.RepeatContext)
*/
public void open(RepeatContext context) {
this.setContext(context);
getProcessed().clear();
if (delegate instanceof RepeatInterceptor) {
RepeatInterceptor interceptor = (RepeatInterceptor) delegate;
interceptor.open(context);
}
}
/**
* Accessor for the list of processed items in this transaction.
*
* @return the processed
*/
private Set getProcessed() {
Assert.state(TransactionSynchronizationManager
.hasResource(WRITER_REPEAT_CONTEXT),
"RepeatContext not bound to transaction.");
Set processed = (Set) ((AttributeAccessor) TransactionSynchronizationManager
.getResource(WRITER_REPEAT_CONTEXT))
.getAttribute(ITEMS_PROCESSED);
return processed;
}
/**
* Set up the {@link RepeatContext} as a transaction resource.
*
* @param context
* the context to set
*/
private void setContext(RepeatContext context) {
if (TransactionSynchronizationManager
.hasResource(WRITER_REPEAT_CONTEXT)) {
return;
}
TransactionSynchronizationManager.bindResource(WRITER_REPEAT_CONTEXT,
context);
context.setAttribute(ITEMS_PROCESSED, new HashSet());
}
/**
* Remove the transaction resource associated with this context.
*/
private void unsetContext() {
if (!TransactionSynchronizationManager
.hasResource(WRITER_REPEAT_CONTEXT)) {
return;
}
TransactionSynchronizationManager.unbindResource(WRITER_REPEAT_CONTEXT);
}
/**
* Accessor for the context property.
*
* @param output
*
* @return the context
*/
private void flushIfNecessary(Object output) {
RepeatContext context = (RepeatContext) TransactionSynchronizationManager
.getResource(WRITER_REPEAT_CONTEXT);
boolean flush;
synchronized (failed) {
flush = failed.contains(output);
}
if (flush) {
// Force early completion to commit aggressively if we encounter a
// failed item (from a failed chunk but we don't know which one was
// the problem).
context.setCompleteOnly();
// Flush now, so that if there is a failure this record can be
// skipped.
flush();
}
}
}

View File

@@ -0,0 +1,39 @@
/*
* 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.springframework.batch.item;
/**
* Mixin interface for {@link ItemProvider} implementations if they can
* distinguish a new item from one that has been processed before and failed,
* e.g. by examining a message flag.
*
* @author Dave Syer
*
*/
public interface FailedItemIdentifier {
/**
* Inspect the item and determine if it has previously failed processing.
* The safest choice when the answer is indeterminate is 'true'.
*
* @param item the current item.
* @return true if the item has been seen before and is known to have failed
* processing.
*/
boolean hasFailed(Object item);
}

View File

@@ -0,0 +1,36 @@
/*
* 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.springframework.batch.item;
/**
* @author Dave Syer
*
*/
public interface ItemProcessor {
/**
* Process the supplied data element. Will be called multiple times during a
* larger batch operation. Will not be called with null data in normal
* operation.
*
* @throws Exception if there are errors. If the processor is used inside a
* retry or a batch the framework will catch the exception and convert or
* rethrow it as appropriate.
*/
void process(Object data) throws Exception;
}

View File

@@ -0,0 +1,71 @@
/*
* 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.springframework.batch.item;
/**
* Strategy interface for providing the data for a given batch stage execution.
* <br/>
*
* Implementations are expected to be stateful and will be called multiple times
* for each batch, with each call to {@link #next} returning a different value
* and finally returning <code>null</code> when all input data is exhausted.<br/>
*
* Implementations need to be thread safe and clients of a {@link ItemProvider}
* need to be aware that this is the case. Clients can code to this interface
* without worrying about thread safety by using the AbstractItemProvider base
* class.<br/>
*
* A richer interface (e.g. with a look ahead or peek) is not feasible because
* we need to support transactions in an asynchronous batch.
*
* @author Rob Harrop
* @author Dave Syer
*/
public interface ItemProvider {
/**
* Reads a piece of input data and advance to the next one. Implementations
* <strong>must</strong> return <code>null</code> at the end of the input
* data set. In a transactional setting, caller might get the same item
* twice from successive calls (or otherwise), if the first call was in a
* transaction that rolled back.
*
* @throws Exception if an underlying resource is unavailable.
*/
Object next() throws Exception;
/**
* Recover gracefully from an error. Clients can call this if processing of
* the item throws an unexpected exception. Caller can use the return value
* to decide whether to try more corrective action or perhaps throw an
* exception.
*
* @param data the item that failed.
* @param cause the cause of the failure that led to this recovery.
* @return true if recovery was successful.
*/
boolean recover(Object data, Throwable cause);
/**
* Get a unique identifier for the item that can be used to cache it between
* calls if necessary, and then identify it later.
*
* @param item the current item.
* @return a unique identifier.
*/
Object getKey(Object item);
}

View File

@@ -0,0 +1,37 @@
/*
* 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.springframework.batch.item;
/**
* Common interface for classes that require initialisation before they can be
* used and need to free resources after they are no longer used.
*/
public interface ResourceLifecycle {
/**
* This method should be invoked by clients at the start of processing to
* allow initialisation of resources.
*
*/
public void open();
/**
* This method should be invoked by clients after the completion of each
* step and the implementing class should close all managed resources.
*
*/
public void close();
}

View File

@@ -0,0 +1,36 @@
/*
* 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.springframework.batch.item.exception;
/**
* Used to signal an unexpected end of an input or message stream. This is an
* abnormal condition, not just the end of the data - e.g. if a resource becomes
* unavailable, or a stream becomes unreadable.
*
* @author Dave Syer
*/
public class UnexpectedInputException extends RuntimeException {
/**
* Generated serial UID.
*/
private static final long serialVersionUID = -8325588758094208905L;
public UnexpectedInputException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,7 @@
<html>
<body>
<p>
Infrastructure implementations of item exception concerns.
</p>
</body>
</html>

View File

@@ -0,0 +1,7 @@
<html>
<body>
<p>
Infrastructure implementations of item concerns.
</p>
</body>
</html>

View File

@@ -0,0 +1,143 @@
package org.springframework.batch.item.processor;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import org.springframework.batch.item.ItemProcessor;
import org.springframework.batch.restart.GenericRestartData;
import org.springframework.batch.restart.RestartData;
import org.springframework.batch.restart.Restartable;
import org.springframework.batch.statistics.StatisticsProvider;
/**
* Runs a collection of ItemProcessors in fixed-order sequence.
*
* @author Robert Kasanicky
*/
public class CompositeItemProcessor implements ItemProcessor, Restartable, StatisticsProvider {
private static final String SEPARATOR = "#";
private List itemProcessors;
/**
* Calls injected ItemProcessors in order.
*/
public void process(Object data) throws Exception {
for (Iterator iterator = itemProcessors.listIterator(); iterator.hasNext();) {
((ItemProcessor) iterator.next()).process(data);
}
}
/**
* Compound restart data of all injected (Restartable) ItemProcessors, property keys are
* prefixed with list index of the ItemProcessor.
*/
public RestartData getRestartData() {
Properties props = createCompoundProperties(new PropertiesExtractor() {
public Properties extractProperties(Object o) {
if (o instanceof Restartable) {
return ((Restartable)o).getRestartData().getProperties();
} else {
return null;
}
}
});
return new GenericRestartData(props);
}
/**
* @param data contains values of restart data, property keys are expected to be prefixed with
* list index of the ItemProcessor.
*/
public void restoreFrom(RestartData data) {
if (data == null || data.getProperties() == null) {
// do nothing
return;
}
List restartDataList = parseProperties(data.getProperties());
// iterators would make the loop below less readable
for (int i=0; i < itemProcessors.size(); i++) {
if (itemProcessors.get(i) instanceof Restartable) {
((Restartable) itemProcessors.get(i)).restoreFrom((RestartData) restartDataList.get(i));
}
}
}
/**
* @return Properties containing statistics of all injected ItemProcessors,
* property keys are prefixed with the list index of the ItemProcessor.
*/
public Properties getStatistics() {
return createCompoundProperties(new PropertiesExtractor() {
public Properties extractProperties(Object o) {
if (o instanceof StatisticsProvider){
return ((StatisticsProvider) o).getStatistics();
} else {
return null;
}
}
});
}
public void setItemProcessors(List itemProcessors) {
this.itemProcessors = itemProcessors;
}
/**
* Parses compound properties into a list of RestartData.
*/
private List parseProperties(Properties props) {
List restartDataList = new ArrayList(itemProcessors.size());
for (int i = 0; i<itemProcessors.size(); i++) {
restartDataList.add(new GenericRestartData(new Properties()));
}
for (Iterator iterator = props.entrySet().iterator(); iterator.hasNext();) {
Map.Entry entry = (Map.Entry) iterator.next();
String key = (String) entry.getKey();
String value = (String) entry.getValue();
int separatorIndex = key.indexOf(SEPARATOR);
int i = Integer.valueOf(key.substring(0, separatorIndex)).intValue();
((RestartData)restartDataList.get(i)).getProperties().setProperty(
key.substring(separatorIndex + 1), value);
}
return restartDataList;
}
/**
* @param extractor used to extract Properties from ItemProviders
* @return compound Properties containing all the Properties from injected ItemProcessors
* with property keys prefixed by list index.
*/
private Properties createCompoundProperties(PropertiesExtractor extractor) {
Properties stats = new Properties();
int index = 0;
for (Iterator iterator = itemProcessors.listIterator(); iterator.hasNext();) {
ItemProcessor processor = (ItemProcessor) iterator.next();
Properties processorStats = extractor.extractProperties(processor);
if (processorStats != null) {
for (Iterator iterator2 = processorStats.entrySet().iterator(); iterator2.hasNext();) {
Map.Entry entry = (Map.Entry) iterator2.next();
stats.setProperty("" + index + SEPARATOR + entry.getKey(), (String) entry.getValue());
}
}
index++;
}
return stats;
}
/**
* Extracts information from given object in the form of {@link Properties}. If the information
* is not available (e.g. unexpected object class) return null.
*/
private interface PropertiesExtractor {
Properties extractProperties(Object o);
}
}

View File

@@ -0,0 +1,42 @@
package org.springframework.batch.item.processor;
import java.util.Iterator;
import java.util.List;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;
/**
* Composite {@link ItemTransformer} that passes the item through a sequence
* of injected <code>ItemTransformer</code>s (return value of previous transformation
* is the entry value of the next).
*
* @author Robert Kasanicky
*/
public class CompositeItemTransformer implements ItemTransformer, InitializingBean {
private List itemTransformers;
public Object transform(Object item) throws Exception {
Object result = item;
for (Iterator iterator = itemTransformers.listIterator(); iterator.hasNext();) {
result = ((ItemTransformer)iterator.next()).transform(result);
}
return result;
}
public void afterPropertiesSet() throws Exception {
Assert.notEmpty(itemTransformers);
for (Iterator iterator = itemTransformers.iterator(); iterator.hasNext();) {
Assert.isInstanceOf(ItemTransformer.class, iterator.next());
}
}
/**
* @param itemTransformers will be chained to produce a composite transformation.
*/
public void setItemTransformers(List itemTransformers) {
this.itemTransformers = itemTransformers;
}
}

View File

@@ -0,0 +1,38 @@
/*
* 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.springframework.batch.item.processor;
import org.springframework.batch.item.ItemProcessor;
import org.springframework.batch.support.AbstractDelegator;
/**
* Delegates item processing to a custom method -
* passes the item as an argument for the delegate method.
*
* @see PropertyExtractingDelegatingItemProcessor
*
* @author Robert Kasanicky
*/
public class DelegatingItemProcessor extends AbstractDelegator implements ItemProcessor {
public void process(Object item) throws Exception {
invokeDelegateMethodWithArgument(item);
}
}

View File

@@ -0,0 +1,11 @@
package org.springframework.batch.item.processor;
/**
* Interface for item transformations during processing phase.
*
* @author Robert Kasanicky
*/
public interface ItemTransformer {
Object transform(Object item) throws Exception;
}

View File

@@ -0,0 +1,104 @@
package org.springframework.batch.item.processor;
import java.util.Properties;
import org.springframework.batch.io.ItemWriter;
import org.springframework.batch.io.Skippable;
import org.springframework.batch.item.ItemProcessor;
import org.springframework.batch.restart.GenericRestartData;
import org.springframework.batch.restart.RestartData;
import org.springframework.batch.restart.Restartable;
import org.springframework.batch.statistics.StatisticsProvider;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;
/**
* Simple wrapper around {@link ItemWriter} providing {@link Restartable} and
* {@link StatisticsProvider} where the {@link ItemWriter} does.
*
* @author Dave Syer
* @author Robert Kasanicky
*/
public class ItemWriterItemProcessor implements ItemProcessor, Restartable, Skippable,
StatisticsProvider, InitializingBean {
private ItemWriter writer;
/**
* Calls {@link #doProcess(Object)} and then writes the result to the {@link ItemWriter}.
*
* @see org.springframework.batch.item.ItemProcessor#process(java.lang.Object)
*/
final public void process(Object item) throws Exception {
Object result = doProcess(item);
writer.write(result);
}
/**
* By default returns the argument. This method is an extension point
* meant to be overridden by subclasses that implement processing logic.
*/
protected Object doProcess(Object item) throws Exception {
return item;
}
/**
* Setter for {@link ItemWriter}.
*/
public void setItemWriter(ItemWriter writer) {
this.writer = writer;
}
/**
* @see Restartable#getRestartData()
*/
public RestartData getRestartData() {
Assert.state(writer != null, "Source must not be null.");
if (writer instanceof Restartable) {
return ((Restartable) writer).getRestartData();
}
else{
return new GenericRestartData(new Properties());
}
}
/**
* @see Restartable#restoreFrom(RestartData)
*/
public void restoreFrom(RestartData data) {
Assert.state(writer != null, "Source must not be null.");
if (writer instanceof Restartable) {
((Restartable) writer).restoreFrom(data);
}
}
/**
* @return delegates to the parent template of it is a
* {@link StatisticsProvider}, otherwise returns an empty
* {@link Properties} instance.
* @see StatisticsProvider#getStatistics()
*/
public Properties getStatistics() {
if (!(writer instanceof StatisticsProvider)) {
return new Properties();
}
return ((StatisticsProvider) writer).getStatistics();
}
public void skip() {
if (writer instanceof Skippable) {
((Skippable)writer).skip();
}
}
public void afterPropertiesSet() throws Exception {
Assert.notNull(writer);
}
}

View File

@@ -0,0 +1,68 @@
/*
* 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.springframework.batch.item.processor;
import org.springframework.batch.item.ItemProcessor;
import org.springframework.batch.support.AbstractDelegator;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.util.Assert;
/**
* Delegates processing to a custom method - extracts property values
* from item object and uses them as arguments for the delegate method.
*
* @see DelegatingItemProcessor
*
* @author Robert Kasanicky
*/
public class PropertyExtractingDelegatingItemProcessor extends AbstractDelegator implements ItemProcessor {
private String[] fieldsUsedAsTargetMethodArguments;
/**
* Extracts values from item's fields named in fieldsUsedAsTargetMethodArguments
* and passes them as arguments to the delegate method.
*/
public void process(Object item) throws Exception {
// helper for extracting property values from a bean
BeanWrapper beanWrapper = new BeanWrapperImpl();
beanWrapper.setWrappedInstance(item);
Object[] methodArguments = new Object[fieldsUsedAsTargetMethodArguments.length];
for (int i = 0; i < fieldsUsedAsTargetMethodArguments.length; i++) {
methodArguments[i] = beanWrapper.getPropertyValue(fieldsUsedAsTargetMethodArguments[i]);
}
invokeDelegateMethodWithArguments(methodArguments);
}
public void afterPropertiesSet() throws Exception {
super.afterPropertiesSet();
Assert.notEmpty(fieldsUsedAsTargetMethodArguments);
}
/**
* @param fieldsUsedAsTargetMethodArguments the values of the these item's fields
* will be used as arguments for the delegate method. Nested property values are
* supported, e.g. <code>address.city</code>
*/
public void setFieldsUsedAsTargetMethodArguments(String[] fieldsUsedAsMethodArguments) {
this.fieldsUsedAsTargetMethodArguments = fieldsUsedAsMethodArguments;
}
}

View File

@@ -0,0 +1,35 @@
package org.springframework.batch.item.processor;
import org.springframework.batch.io.ItemWriter;
import org.springframework.util.Assert;
/**
* Transforms the item using injected {@link ItemTransformer}
* before it is written to output by {@link ItemWriter}.
*
* @author Robert Kasanicky
*/
public class TransformerWriterItemProcessor extends ItemWriterItemProcessor {
private ItemTransformer itemTransformer;
/**
* Transform the item using the {@link #itemTransformer}.
*/
protected Object doProcess(Object item) throws Exception {
return itemTransformer.transform(item);
}
/**
* @param itemTransformer will transform the item before
* it is passed to {@link ItemWriter}.
*/
public void setItemTransformer(ItemTransformer itemTransformer) {
this.itemTransformer = itemTransformer;
}
public void afterPropertiesSet() throws Exception {
super.afterPropertiesSet();
Assert.notNull(itemTransformer);
}
}

View File

@@ -0,0 +1,7 @@
<html>
<body>
<p>
Specific implementations of item processing concerns.
</p>
</body>
</html>

View File

@@ -0,0 +1,47 @@
/*
* 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.springframework.batch.item.provider;
import org.springframework.batch.item.ItemProvider;
public abstract class AbstractItemProvider implements ItemProvider {
/**
* Do nothing. Subclassses should override to implement recovery behaviour.
*
* @see org.springframework.batch.item.ItemProvider#recover(java.lang.Object,
* Throwable)
*
* @return false if nothing can be done (the default), or true if the item
* can now safely be ignored or committed.
*/
public boolean recover(Object item, Throwable cause) {
return false;
}
/**
* Simply returns the item itself. Will be adequate for many purposes, but
* not (for example) if the item is a message - in which case the identifier
* should be used.
*
* @see org.springframework.batch.item.ItemProvider#getKey(java.lang.Object)
*/
public Object getKey(Object item) {
return item;
}
}

View File

@@ -0,0 +1,115 @@
/*
* 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.springframework.batch.item.provider;
import java.util.ArrayList;
import java.util.Collection;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.batch.io.InputSource;
import org.springframework.batch.io.file.FieldSetMapper;
import org.springframework.batch.item.ItemProvider;
/**
* An {@link ItemProvider} that delivers a list as its item, storing up objects
* from the injected {@link InputSource} until they are ready to be packed out
* as a collection. The {@link InputSource} should mark the beginning and end of
* records with the constant values in {@link FieldSetMapper} ({@link FieldSetMapper#BEGIN_RECORD}
* and {@link FieldSetMapper#END_RECORD}).<br/>
*
* This class is thread safe (it can be used concurrently by multiple threads)
* as long as the {@link InputSource} is also thread safe.
*
* @author Dave Syer
*
*/
public class AggregateItemProvider extends AbstractItemProvider {
private static final Log log = LogFactory
.getLog(AggregateItemProvider.class);
private InputSource inputSource;
/**
* Get the next list of records.
*
* @see org.springframework.batch.item.ItemProvider#next()
*/
public Object next() {
ResultHolder holder = new ResultHolder();
while (process(inputSource.read(), holder)) {
continue;
}
if (!holder.exhausted) {
return holder.records;
} else {
return null;
}
}
private boolean process(Object value, ResultHolder holder) {
// finish processing if we hit the end of file
if (value == null) {
log.debug("Exhausted InputSource");
holder.exhausted = true;
return false;
}
// start a new collection
if (value == FieldSetMapper.BEGIN_RECORD) {
log.debug("Start of new record detected");
return true;
}
// mark we are finished with current collection
if (value == FieldSetMapper.END_RECORD) {
log.debug("End of record detected");
return false;
}
// add a simple record to the current collection
log.debug("Mapping: " + value);
holder.records.add(value);
return true;
}
/**
* Injection setter for {@link InputSource}.
*
* @param inputSource
* an {@link InputSource}.
*/
public void setInputSource(InputSource inputSource) {
this.inputSource = inputSource;
}
/**
* Private class for temporary state management while item is being
* collected.
*
* @author Dave Syer
*
*/
private static class ResultHolder {
Collection records = new ArrayList();
boolean exhausted = false;
}
}

View File

@@ -0,0 +1,47 @@
/*
* 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.springframework.batch.item.provider;
import org.springframework.batch.item.ItemProvider;
import org.springframework.batch.support.AbstractDelegator;
/**
* Invokes a custom method which provides an item.
*
* @author Robert Kasanicky
*/
public class DelegatingItemProvider extends AbstractDelegator implements ItemProvider {
/**
* @return return value of the target method.
*/
public Object next() throws Exception {
return invokeDelegateMethod();
}
//harmless implementation of method required by ItemProvider interface
public Object getKey(Object item) {
return item;
}
//harmless implementation of method required by ItemProvider interface
public boolean recover(Object data, Throwable cause) {
return false;
}
}

View File

@@ -0,0 +1,105 @@
/*
* 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.springframework.batch.item.provider;
import java.util.Properties;
import org.springframework.batch.io.InputSource;
import org.springframework.batch.io.Skippable;
import org.springframework.batch.restart.RestartData;
import org.springframework.batch.restart.Restartable;
import org.springframework.batch.statistics.StatisticsProvider;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;
/**
* Simple wrapper around {@link InputSource}. The input source is expected to
* take care of open and close operations. If necessary it should be registered
* as a step scoped bean to ensure that the lifecycle methods are called.
*
* @author Dave Syer
*/
public class InputSourceItemProvider extends AbstractItemProvider implements Restartable, StatisticsProvider, Skippable, InitializingBean{
private InputSource inputSource;
public void afterPropertiesSet() throws Exception {
Assert.notNull(inputSource, "InputSource must not be null.");
}
/**
* Get the next object from the input source.
* @see org.springframework.batch.item.ItemProvider#next()
*/
public Object next() {
return inputSource.read();
}
/**
* @see Restartable#getRestartData()
* @throws IllegalStateException if the parent template is not itself
* {@link Restartable}.
*/
public RestartData getRestartData() {
if (!(inputSource instanceof Restartable)) {
throw new IllegalStateException("Input Template is not Restartable");
}
return ((Restartable) inputSource).getRestartData();
}
/**
* @see Restartable#restoreFrom(RestartData)
* @throws IllegalStateException if the parent template is not itself
* {@link Restartable}.
*/
public void restoreFrom(RestartData data) {
if (!(inputSource instanceof Restartable)) {
throw new IllegalStateException("Input Template is not Restartable");
}
((Restartable) inputSource).restoreFrom(data);
}
/**
* @return delegates to the parent template of it is a
* {@link StatisticsProvider}, otherwise returns an empty
* {@link Properties} instance.
* @see StatisticsProvider#getStatistics()
*/
public Properties getStatistics() {
if (!(inputSource instanceof StatisticsProvider)) {
return new Properties();
}
return ((StatisticsProvider) inputSource).getStatistics();
}
/**
* Setter for input source.
* @param source
*/
public void setInputSource(InputSource source) {
this.inputSource = source;
}
public InputSource getInputSource() {
return inputSource;
}
public void skip() {
if (inputSource instanceof Skippable) {
((Skippable)inputSource).skip();
}
}
}

View File

@@ -0,0 +1,175 @@
/*
* 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.springframework.batch.item.provider;
import javax.jms.Destination;
import javax.jms.JMSException;
import javax.jms.Message;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.batch.item.FailedItemIdentifier;
import org.springframework.batch.item.ItemProvider;
import org.springframework.batch.item.exception.UnexpectedInputException;
import org.springframework.jms.JmsException;
import org.springframework.jms.core.JmsOperations;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.util.Assert;
/**
* An {@link ItemProvider} for JMS using a {@link JmsTemplate}. The template
* should have a default destination, which will be used to provide items in
* {@link #next()}. If a recovery step is needed, set the error destination and
* the item will be sent there if processing fails in an external retry.
*
* @author Dave Syer
*
*/
public class JmsItemProvider extends AbstractItemProvider implements FailedItemIdentifier {
protected Log logger = LogFactory.getLog(getClass());
private JmsOperations jmsTemplate;
private Class itemType;
private String errorDestinationName;
private Destination errorDestination;
/**
* Set the error destination. Should not be the same as the default
* destination of the jms template.
* @param errorDestination a JMS Destination
*/
public void setErrorDestination(Destination errorDestination) {
this.errorDestination = errorDestination;
}
/**
* Set the error destination by name. Will be resolved by the destination
* resolver in the jms template.
*
* @param errorDestinationName the name of a JMS Destination
*/
public void setErrorDestinationName(String errorDestinationName) {
this.errorDestinationName = errorDestinationName;
}
/**
* Setter for jms template.
*
* @param jmsTemplate a {@link JmsOperations} instance
*/
public void setJmsTemplate(JmsOperations jmsTemplate) {
this.jmsTemplate = jmsTemplate;
}
/**
* Set the expected type of incoming message payloads. Set this to
* {@link Message} to receive the raw underlying message.
*
* @param itemType the java class of the items to be delivered.
*
* @throws IllegalStateException if the message payload is of the wrong
* type.
*/
public void setItemType(Class itemType) {
this.itemType = itemType;
}
public Object next() {
if (itemType != null && itemType.isAssignableFrom(Message.class)) {
return jmsTemplate.receive();
}
Object result = jmsTemplate.receiveAndConvert();
if (itemType != null && result != null) {
Assert.state(itemType.isAssignableFrom(result.getClass()),
"Received message payload of wrong type: expected [" + itemType + "]");
}
return result;
}
/**
* Send the message back to the proovider using the specified error
* destination property of this provider.
*
* @see org.springframework.batch.item.provider.AbstractItemProvider#recover(java.lang.Object,
* Throwable)
*/
public boolean recover(Object item, Throwable cause) {
try {
if (errorDestination != null) {
jmsTemplate.convertAndSend(errorDestination, item);
}
else if (errorDestinationName != null) {
jmsTemplate.convertAndSend(errorDestinationName, item);
}
else {
// do nothing - it doesn't make sense to send the message back
// to
// the destination it came from
return false;
}
return true;
}
catch (JmsException e) {
logger.error("Could not recover because of JmsException.", e);
return false;
}
}
/**
* If the message is a {@link Message} then returns the JMS message ID.
* Otherwise just delegate to parent class.
*
* @see org.springframework.batch.item.provider.AbstractItemProvider#getKey(java.lang.Object)
*
* @throws UnexpectedInputException if the JMS id cannot be determined from
* a JMS Message
*/
public Object getKey(Object item) {
if (itemType != null && itemType.isAssignableFrom(Message.class)) {
try {
return ((Message) item).getJMSMessageID();
}
catch (JMSException e) {
throw new UnexpectedInputException("Could not extract message ID", e);
}
}
return super.getKey(item);
}
/**
* If the item is a message, check the JMS redelivered flag, otherwise
* return true to be on the safe side.
*
* @see org.springframework.batch.item.FailedItemIdentifier#hasFailed(java.lang.Object)
*/
public boolean hasFailed(Object item) {
if (itemType != null && itemType.isAssignableFrom(Message.class)) {
try {
return ((Message) item).getJMSRedelivered();
}
catch (JMSException e) {
throw new UnexpectedInputException("Could not extract message ID", e);
}
}
return true;
}
}

View File

@@ -0,0 +1,53 @@
/*
* 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.springframework.batch.item.provider;
import java.util.ArrayList;
import java.util.List;
import org.springframework.aop.support.AopUtils;
import org.springframework.batch.item.ItemProvider;
/**
* An {@link ItemProvider} that pulls data from a list. Useful for testing.
*
* @author Dave Syer
*
*/
public class ListItemProvider extends AbstractItemProvider {
private List list;
public ListItemProvider(List list) {
// If it is a proxy we assume it knows how to deal with its own state.
// (It's probably transaction aware.)
if (AopUtils.isAopProxy(list)) {
this.list = list;
}
else {
this.list = new ArrayList(list);
}
}
public Object next() {
if (!list.isEmpty()) {
return list.remove(0);
}
return null;
}
}

View File

@@ -0,0 +1,59 @@
/*
* 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.springframework.batch.item.provider;
import org.springframework.batch.item.validator.Validator;
import org.springframework.util.Assert;
/**
* Simple extension of InputsourceItemProvider that provides for
* validation before returning input.
*
* @author Lucas Ward
*
*/
public class ValidatingItemProvider extends InputSourceItemProvider {
private Validator validator;
/* (non-Javadoc)
* @see org.springframework.batch.item.provider.InputSourceItemProvider#afterPropertiesSet()
*/
public void afterPropertiesSet() throws Exception {
super.afterPropertiesSet();
Assert.notNull(validator, "Validator must not be null.");
}
/* (non-Javadoc)
* @see org.springframework.batch.item.provider.InputSourceItemProvider#next()
*/
public Object next() {
Object input = super.next();
if(input != null){
validator.validate(input);
}
return input;
}
/**
* Set the validator used to validate each item.
*
* @param validator
*/
public void setValidator(Validator validator) {
this.validator = validator;
}
}

View File

@@ -0,0 +1,7 @@
<html>
<body>
<p>
Infrastructure implementations of item provider concerns.
</p>
</body>
</html>

View File

@@ -0,0 +1,78 @@
/*
* 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.springframework.batch.item.validator;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.batch.io.exception.ValidationException;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.FieldError;
/**
* Adapter for the spring validator interface.
*
* @see Validator
*/
public class SpringValidator implements Validator {
private static final Log log = LogFactory.getLog(SpringValidator.class);
private org.springframework.validation.Validator validator;
/**
* @see Validator#validate(Object)
*/
public void validate(Object value) throws ValidationException {
if (validator == null) {
throw new ValidationException("Validator not specified.");
}
BeanPropertyBindingResult errors = new BeanPropertyBindingResult(value, "object");
if (validator.supports(value.getClass())) {
validator.validate(value, errors);
}
else {
throw new ValidationException(value.getClass() + " is not supported by validator.");
}
if (errors.hasErrors()) {
log.debug(errors);
throw new ValidationException("SpringValidator >> validation failed on: " + getInvalidColumnNames(errors));
}
}
public void setValidator(org.springframework.validation.Validator validator) {
this.validator = validator;
}
private String getInvalidColumnNames(BeanPropertyBindingResult errors) {
StringBuffer stringBuffer = new StringBuffer();
List list = errors.getFieldErrors();
for (int i = 0; i < list.size(); i++) {
if (i > 0) {
stringBuffer.append(", ");
}
stringBuffer.append(((FieldError) list.get(i)).getField());
}
return stringBuffer.toString();
}
}

Some files were not shown because too many files have changed in this diff Show More