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:
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<html>
|
||||
<body>
|
||||
<p>
|
||||
Infrastructure implementations of common concerns.
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<html>
|
||||
<body>
|
||||
<p>
|
||||
Infrastructure implementations of io exception concerns.
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<html>
|
||||
<body>
|
||||
<p>
|
||||
Infrastructure implementations of io file concerns.
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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()];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<html>
|
||||
<body>
|
||||
<p>
|
||||
Infrastructure implementations of io file support mapping concerns.
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<html>
|
||||
<body>
|
||||
<p>
|
||||
Infrastructure implementations of io file support concerns.
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<html>
|
||||
<body>
|
||||
<p>
|
||||
Infrastructure implementations of io file support separator concerns.
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 <1,Integer.MAX_VALUE-1> 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");
|
||||
}
|
||||
}
|
||||
@@ -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 <1, Integer.MAX_VALUE-1>
|
||||
* <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] + ")");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<html>
|
||||
<body>
|
||||
<p>
|
||||
Infrastructure implementations of io file support transform concerns.
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,7 @@
|
||||
<html>
|
||||
<body>
|
||||
<p>
|
||||
Infrastructure implementations of io concerns.
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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() + "]");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<html>
|
||||
<body>
|
||||
<p>
|
||||
Infrastructure implementations of item exception concerns.
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,7 @@
|
||||
<html>
|
||||
<body>
|
||||
<p>
|
||||
Infrastructure implementations of item concerns.
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<html>
|
||||
<body>
|
||||
<p>
|
||||
Specific implementations of item processing concerns.
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<html>
|
||||
<body>
|
||||
<p>
|
||||
Infrastructure implementations of item provider concerns.
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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
Reference in New Issue
Block a user