diff --git a/changelog.txt b/changelog.txt new file mode 100644 index 000000000..023c8f00c --- /dev/null +++ b/changelog.txt @@ -0,0 +1 @@ +Do not edit this file: use src/site/apt/changelog.apt instead. diff --git a/core/.classpath b/core/.classpath new file mode 100644 index 000000000..0536f6463 --- /dev/null +++ b/core/.classpath @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/core/.project b/core/.project new file mode 100644 index 000000000..9b1422bb6 --- /dev/null +++ b/core/.project @@ -0,0 +1,32 @@ + + + batch-core + Simple container application for batch processing, using the + Spring Batch Framework to express a domain of Jobs, Steps, + Chunks, etc. + + batch-infrastructure + + + + org.eclipse.jdt.core.javabuilder + + + + + org.springframework.ide.eclipse.core.springbuilder + + + + + org.maven.ide.eclipse.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.maven.ide.eclipse.maven2Nature + org.springframework.ide.eclipse.core.springnature + + diff --git a/core/.springBeans b/core/.springBeans new file mode 100644 index 000000000..0fd874a82 --- /dev/null +++ b/core/.springBeans @@ -0,0 +1,31 @@ + + + + xml + + + + + + + true + false + + + + + + true + false + + + + + + true + false + + + + + diff --git a/core/changelog.txt b/core/changelog.txt new file mode 100644 index 000000000..023c8f00c --- /dev/null +++ b/core/changelog.txt @@ -0,0 +1 @@ +Do not edit this file: use src/site/apt/changelog.apt instead. diff --git a/core/pom.xml b/core/pom.xml new file mode 100644 index 000000000..14f1fdd6a --- /dev/null +++ b/core/pom.xml @@ -0,0 +1,76 @@ + + + 4.0.0 + spring-batch-core + jar + Core + + + + + + + org.springframework.batch + spring-batch + 1.0-m2-SNAPSHOT + .. + + + + + org.springframework.batch + spring-batch-infrastructure + ${project.version} + + + + backport-util-concurrent + backport-util-concurrent + 3.0 + test + + + junit + junit + 3.8.1 + test + + + easymock + easymock + 1.1 + test + + + + + + + org.apache.maven.plugins + maven-clover-plugin + + ${basedir}/src/test/resources/clover.license + + + + pre-site + + instrument + + + + + + + + + + + org.apache.maven.plugins + maven-clover-plugin + + + + + diff --git a/core/src/main/java/org/springframework/batch/core/configuration/DuplicateJobConfigurationException.java b/core/src/main/java/org/springframework/batch/core/configuration/DuplicateJobConfigurationException.java new file mode 100644 index 000000000..f763afa2c --- /dev/null +++ b/core/src/main/java/org/springframework/batch/core/configuration/DuplicateJobConfigurationException.java @@ -0,0 +1,42 @@ +/* + * 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.core.configuration; + +/** + * Checked exception that indicates a name clash when registering + * {@link JobConfiguration} instances. + * + * @author Dave Syer + * + */ +public class DuplicateJobConfigurationException extends JobConfigurationException { + + /** + * Create an exception with the given message. + */ + public DuplicateJobConfigurationException(String msg) { + super(msg); + } + + /** + * @param msg The message to send to caller + * @param e the cause of the exception + */ + public DuplicateJobConfigurationException(String msg, Throwable e) { + super(msg, e); + } + +} diff --git a/core/src/main/java/org/springframework/batch/core/configuration/JobConfiguration.java b/core/src/main/java/org/springframework/batch/core/configuration/JobConfiguration.java new file mode 100644 index 000000000..d5a82f1b1 --- /dev/null +++ b/core/src/main/java/org/springframework/batch/core/configuration/JobConfiguration.java @@ -0,0 +1,102 @@ +/* + * 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.core.configuration; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.beans.factory.BeanNameAware; + +/** + * Batch domain object representing a job configuration. JobConfiguration is an + * explicit abstraction representing the configuration of a job specified by a + * developer. It should be noted that restart policy is applied to the job as a + * whole and not to a step. + * + * @author Lucas Ward + * @author Dave Syer + */ +public class JobConfiguration implements BeanNameAware { + + private List stepConfigurations = new ArrayList(); + + private String name; + + private boolean restartable = false; + + private int startLimit = Integer.MAX_VALUE; + + /** + * Default constructor. + */ + public JobConfiguration() { + super(); + } + + /** + * Convenience constructor to immediately add name (which is mandatory but + * not final). + * @param name + */ + public JobConfiguration(String name) { + super(); + this.name = name; + } + + public void setBeanName(String name) { + if (this.name == null) { + this.name = name; + } + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getStepConfigurations() { + return stepConfigurations; + } + + public void setSteps(List stepConfigurations) { + this.stepConfigurations.clear(); + this.stepConfigurations.addAll(stepConfigurations); + } + + public void addStep(StepConfiguration stepConfiguration) { + this.stepConfigurations.add(stepConfiguration); + } + + public int getStartLimit() { + return startLimit; + } + + public void setStartLimit(int startLimit) { + this.startLimit = startLimit; + } + + public void setRestartable(boolean restartable) { + this.restartable = restartable; + } + + public boolean isRestartable() { + return restartable; + } +} diff --git a/core/src/main/java/org/springframework/batch/core/configuration/JobConfigurationException.java b/core/src/main/java/org/springframework/batch/core/configuration/JobConfigurationException.java new file mode 100644 index 000000000..b0413aed9 --- /dev/null +++ b/core/src/main/java/org/springframework/batch/core/configuration/JobConfigurationException.java @@ -0,0 +1,42 @@ +/* + * 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.core.configuration; + +/** + * Base class for checked exceptions related to {@link JobConfiguration} + * creation, registration or use. + * + * @author Dave Syer + * + */ +public class JobConfigurationException extends Exception { + + /** + * Create an exception with the given message. + */ + public JobConfigurationException(String msg) { + super(msg); + } + + /** + * @param msg The message to send to caller + * @param e the cause of the exception + */ + public JobConfigurationException(String msg, Throwable e) { + super(msg, e); + } + +} diff --git a/core/src/main/java/org/springframework/batch/core/configuration/JobConfigurationLocator.java b/core/src/main/java/org/springframework/batch/core/configuration/JobConfigurationLocator.java new file mode 100644 index 000000000..8ee6bb0b1 --- /dev/null +++ b/core/src/main/java/org/springframework/batch/core/configuration/JobConfigurationLocator.java @@ -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.core.configuration; + +/** + * A runtime service locator interface for retrieving job configurations by + * name. + * + * @author Dave Syer + * + */ +public interface JobConfigurationLocator { + + /** + * Locates a {@link JobConfiguration} at runtime. + * + * @param name the name of the {@link JobConfiguration} which should be + * unique + * @return a {@link JobConfiguration} identified by the given name + * + * @throws NoSuchJobConfigurationException if the required configuratio can + * not be found. + */ + JobConfiguration getJobConfiguration(String name) throws NoSuchJobConfigurationException; +} diff --git a/core/src/main/java/org/springframework/batch/core/configuration/JobConfigurationRegistry.java b/core/src/main/java/org/springframework/batch/core/configuration/JobConfigurationRegistry.java new file mode 100644 index 000000000..67e009e11 --- /dev/null +++ b/core/src/main/java/org/springframework/batch/core/configuration/JobConfigurationRegistry.java @@ -0,0 +1,44 @@ +/* + * 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.core.configuration; + +/** + * A runtime service registry interface for registering job configurations by + * name. + * + * @author Dave Syer + * + */ +public interface JobConfigurationRegistry extends JobConfigurationLocator { + + /** + * Registers a {@link JobConfiguration} at runtime. + * + * @param jobConfiguration the {@link JobConfiguration} to be registered + * + * @throws DuplicateJobConfigurationException if a configuration with the + * same name has already been registered. + */ + void register(JobConfiguration jobConfiguration) throws DuplicateJobConfigurationException; + + /** + * Unregisters a previously registered {@link JobConfiguration}. If it was + * not previously registered there is no error. + * + * @param jobConfiguration the {@link JobConfiguration} to unregister. + */ + void unregister(JobConfiguration jobConfiguration); +} diff --git a/core/src/main/java/org/springframework/batch/core/configuration/ListableJobConfigurationRegistry.java b/core/src/main/java/org/springframework/batch/core/configuration/ListableJobConfigurationRegistry.java new file mode 100644 index 000000000..73ec2df56 --- /dev/null +++ b/core/src/main/java/org/springframework/batch/core/configuration/ListableJobConfigurationRegistry.java @@ -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.core.configuration; + +import java.util.Collection; + +/** + * A listable extension of {@link JobConfigurationRegistry}. + * + * @author Dave Syer + * + */ +public interface ListableJobConfigurationRegistry extends JobConfigurationRegistry { + + /** + * Provides the currently registered configurations. The return value is + * unmodifiable and disconnected from the underlying registry storage. + * + * @return a collection of {@link JobConfiguration} instances. Empty if none + * are registered. + */ + Collection getJobConfigurations(); +} diff --git a/core/src/main/java/org/springframework/batch/core/configuration/NoSuchJobConfigurationException.java b/core/src/main/java/org/springframework/batch/core/configuration/NoSuchJobConfigurationException.java new file mode 100644 index 000000000..a58401d23 --- /dev/null +++ b/core/src/main/java/org/springframework/batch/core/configuration/NoSuchJobConfigurationException.java @@ -0,0 +1,42 @@ +/* + * 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.core.configuration; + + +/** + * Checked exception to indicate that a required {@link JobConfiguration} is not + * available. + * + * @author Dave Syer + * + */ +public class NoSuchJobConfigurationException extends JobConfigurationException { + + /** + * Create an exception with the given message. + */ + public NoSuchJobConfigurationException(String msg) { + super(msg); + } + + /** + * @param msg The message to send to caller + * @param e the cause of the exception + */ + public NoSuchJobConfigurationException(String msg, Throwable e) { + super(msg, e); + } +} diff --git a/core/src/main/java/org/springframework/batch/core/configuration/StepConfiguration.java b/core/src/main/java/org/springframework/batch/core/configuration/StepConfiguration.java new file mode 100644 index 000000000..d00233daa --- /dev/null +++ b/core/src/main/java/org/springframework/batch/core/configuration/StepConfiguration.java @@ -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.core.configuration; + +import org.springframework.batch.core.tasklet.Tasklet; + +/** + * Batch domain interface representing the configuration of a step. As with the + * (@link JobConfiguration), step configuration is meant to explicitly represent + * a the configuration of a step by a developer. This allows for the separation + * of what a developer configures from the myriad of concerns required for + * executing a job. + * + * @author Dave Syer + * + */ +public interface StepConfiguration { + + /** + * @return the name of this step configuration. + */ + String getName(); + + /** + * @return the {@link Tasklet} instance to execute for each item processed. + */ + Tasklet getTasklet(); + + /** + * @return true if a job that is already marked as complete can be started + * again. + */ + boolean isAllowStartIfComplete(); + + /** + * @return the number of times a job can be started with the same + * identifier. + */ + int getStartLimit(); + +} \ No newline at end of file diff --git a/core/src/main/java/org/springframework/batch/core/configuration/StepConfigurationSupport.java b/core/src/main/java/org/springframework/batch/core/configuration/StepConfigurationSupport.java new file mode 100644 index 000000000..e0a6d237c --- /dev/null +++ b/core/src/main/java/org/springframework/batch/core/configuration/StepConfigurationSupport.java @@ -0,0 +1,117 @@ +/* + * 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.core.configuration; + +import org.springframework.batch.core.tasklet.Tasklet; + +/** + * Basic no-op support implementation for use as base class for + * {@link StepConfiguration}. + * + * @author Dave Syer + * + */ +public class StepConfigurationSupport implements StepConfiguration { + + private String name; + private int startLimit = Integer.MAX_VALUE; + private Tasklet tasklet; + private boolean allowStartIfComplete; + + /** + * Default constructor for {@link StepConfigurationSupport}. + */ + public StepConfigurationSupport() { + super(); + } + + /** + * @param string + */ + public StepConfigurationSupport(String string) { + super(); + this.name = string; + } + + /* + * (non-Javadoc) + * @see org.springframework.batch.core.configuration.StepConfiguration#getName() + */ + public String getName() { + return this.name; + } + + /** + * Public setter for the name. + * + * @param name the name to set + */ + public void setName(String name) { + this.name = name; + } + + /* + * (non-Javadoc) + * @see org.springframework.batch.core.configuration.StepConfiguration#getStartLimit() + */ + public int getStartLimit() { + return this.startLimit; + } + + /** + * Public setter for the startLimit. + * + * @param startLimit the startLimit to set + */ + public void setStartLimit(int startLimit) { + this.startLimit = startLimit; + } + + /* + * (non-Javadoc) + * @see org.springframework.batch.core.configuration.StepConfiguration#getTasklet() + */ + public Tasklet getTasklet() { + return this.tasklet; + } + + /** + * Public setter for the tasklet. + * + * @param tasklet the tasklet to set + */ + public void setTasklet(Tasklet tasklet) { + this.tasklet = tasklet; + } + + /* + * (non-Javadoc) + * @see org.springframework.batch.core.configuration.StepConfiguration#shouldAllowStartIfComplete() + */ + public boolean isAllowStartIfComplete() { + return this.allowStartIfComplete; + } + + /** + * Public setter for the shouldAllowStartIfComplete. + * + * @param allowStartIfComplete the shouldAllowStartIfComplete to set + */ + public void setAllowStartIfComplete(boolean allowStartIfComplete) { + this.allowStartIfComplete = allowStartIfComplete; + } + +} diff --git a/core/src/main/java/org/springframework/batch/core/configuration/package.html b/core/src/main/java/org/springframework/batch/core/configuration/package.html new file mode 100644 index 000000000..9a914b226 --- /dev/null +++ b/core/src/main/java/org/springframework/batch/core/configuration/package.html @@ -0,0 +1,7 @@ + + +

+Interfaces and generic implementations of configuration concerns. +

+ + diff --git a/core/src/main/java/org/springframework/batch/core/domain/BatchStatus.java b/core/src/main/java/org/springframework/batch/core/domain/BatchStatus.java new file mode 100644 index 000000000..75b2c8fd4 --- /dev/null +++ b/core/src/main/java/org/springframework/batch/core/domain/BatchStatus.java @@ -0,0 +1,62 @@ +/* + * 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.core.domain; + +/** + * Typesafe enumeration representating the status of an artifact within + * the batch container. See Effective Java Programming by Joshua Bloch + * for more details on the pattern used. + * + * @author Lucas Ward + * + */ + +public class BatchStatus { + + private final String name; + + private BatchStatus(String name) { + this.name = name; + } + + public String toString(){ + return name; + } + + public static final BatchStatus COMPLETED = new BatchStatus("COMPLETED"); + + public static final BatchStatus STARTED = new BatchStatus("STARTED"); + + public static final BatchStatus STARTING = new BatchStatus("STARTING"); + + public static final BatchStatus FAILED = new BatchStatus("FAILED"); + + public static final BatchStatus STOPPED = new BatchStatus("STOPPED"); + + private static final BatchStatus[] VALUES = {STARTING, STARTED, COMPLETED, FAILED, STOPPED}; + + public static BatchStatus getStatus(String statusAsString){ + + for(int i = 0; i < VALUES.length; i++){ + if(VALUES[i].toString().equals(statusAsString)){ + return (BatchStatus)VALUES[i]; + } + } + + return null; + } +} diff --git a/core/src/main/java/org/springframework/batch/core/domain/Entity.java b/core/src/main/java/org/springframework/batch/core/domain/Entity.java new file mode 100644 index 000000000..d6c539912 --- /dev/null +++ b/core/src/main/java/org/springframework/batch/core/domain/Entity.java @@ -0,0 +1,106 @@ +/* + * 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.core.domain; + +import java.io.Serializable; + +import org.springframework.util.ClassUtils; + +/** + * Batch Domain Entity class. Any class that should be uniquely identifiable + * from another should subclass from Entity. More information on this pattern + * and the difference between Entities and Value Objects can be found in Domain + * Driven Design by Eric Evans. + * + * @author Lucas Ward + * @author Dave Syer + * + */ +public class Entity implements Serializable { + + private Long id; + + private Integer version; + + public Entity() { + super(); + } + + public Entity(Long id) { + super(); + this.id = id; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + /** + * @return the version + */ + public Integer getVersion() { + return version; + } + + // @Override + public String toString() { + return ClassUtils.getShortName(getClass()) + ": id=" + getId(); + } + + /** + * Attempt to establish identity based on id if both exist. If either id + * does not exist use Object.equals(). + * + * @see java.lang.Object#equals(java.lang.Object) + */ + public boolean equals(Object other) { + if (other == null) { + return false; + } + if (!(other instanceof Entity)) { + return false; + } + Entity step = (Entity) other; + if (id == null || step.getId() == null) { + return step == this; + } + return id.equals(step.getId()); + } + + /** + * Use ID if it exists to establish hash code, otherwise fall back to + * Object.hashCode(). Based on the same information as equals, so if that + * changes, this will. N.B. this follows the contract of Object.hashCode(), + * but will cause problems for anyone adding an unsaved {@link Entity} to a + * Set because Set.contains() will almost certainly return false for the + * {@link Entity} after it is saved. Spring Batch does not store any of its + * entities in Sets as a matter of course, so internally this is consistent. + * Clients should not be exposed to unsaved entities. + * + * @see java.lang.Object#hashCode() + */ + public int hashCode() { + if (id == null) { + return super.hashCode(); + } + return 39 + 87 * id.hashCode(); + } +} diff --git a/core/src/main/java/org/springframework/batch/core/domain/JobExecution.java b/core/src/main/java/org/springframework/batch/core/domain/JobExecution.java new file mode 100644 index 000000000..4b6b96d2d --- /dev/null +++ b/core/src/main/java/org/springframework/batch/core/domain/JobExecution.java @@ -0,0 +1,98 @@ +/* + * 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.core.domain; + +import java.sql.Timestamp; + +/** + * Batch domain object representing the execution of a job. + * + * @author Lucas Ward + * + */ +public class JobExecution extends Entity { + +// TODO declare transient or make serializable + private BatchStatus status = BatchStatus.STARTING; + + private Timestamp startTime = new Timestamp(System.currentTimeMillis()); + + private Timestamp endTime = null; + + private Long jobId; + + private int exitCode; + + // Package private constructor for Hibernate + JobExecution() {} + + /** + * Because a JobExecution isn't valid unless the jobId is set, this + * constructor is the only valid one. + * + * @param jobId + */ + public JobExecution(Long jobId) { + this.jobId = jobId; + } + + public Timestamp getEndTime() { + return endTime; + } + + public void setEndTime(Timestamp endTime) { + this.endTime = endTime; + } + + public Timestamp getStartTime() { + return startTime; + } + + public void setStartTime(Timestamp startTime) { + this.startTime = startTime; + } + + public BatchStatus getStatus() { + return status; + } + + public void setStatus(BatchStatus status) { + this.status = status; + } + + public Long getJobId() { + return jobId; + } + + public void setJobId(Long jobId) { + this.jobId = jobId; + } + + /** + * @param exitCode + */ + public void setExitCode(int exitCode) { + this.exitCode = exitCode; + } + + /** + * @return the exitCode + */ + public int getExitCode() { + return exitCode; + } +} diff --git a/core/src/main/java/org/springframework/batch/core/domain/JobInstance.java b/core/src/main/java/org/springframework/batch/core/domain/JobInstance.java new file mode 100644 index 000000000..1db6d1680 --- /dev/null +++ b/core/src/main/java/org/springframework/batch/core/domain/JobInstance.java @@ -0,0 +1,106 @@ +/* + * 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.core.domain; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.batch.core.runtime.JobIdentifier; + +/** + * Batch domain object representing a job instance. A job instance is defined as + * a logical container for steps with unique identification of the unit as a + * whole. A job can be executed many times with the same instance, usually if it + * fails and is restarted, or if it is launched on an ad-hoc basis "on demand". + * + * @author Lucas Ward + * @author Dave Syer + */ +public class JobInstance extends Entity { + + private List steps = new ArrayList(); + + private JobIdentifier identifier; + + // TODO declare transient or make the class serializable + private BatchStatus status; + + private int jobExecutionCount; + + public JobInstance() { + this(null); + } + + public JobInstance(Long id) { + super(); + setId(id); + } + + public BatchStatus getStatus() { + return status; + } + + public void setStatus(BatchStatus status) { + this.status = status; + } + + public List getSteps() { + return steps; + } + + public void setSteps(List steps) { + this.steps = steps; + } + + public void addStep(StepInstance step) { + this.steps.add(step); + } + + public int getJobExecutionCount() { + return jobExecutionCount; + } + + public void setJobExecutionCount(int jobExecutionCount) { + this.jobExecutionCount = jobExecutionCount; + } + + /** + * Public accessor for the identifier property. + * + * @return the identifier + */ + public JobIdentifier getIdentifier() { + return identifier; + } + + /** + * Public setter for the identifier. + * + * @param identifier the identifier to set + */ + public void setIdentifier(JobIdentifier identifier) { + this.identifier = identifier; + } + + /** + * @return the identifier name if there is one + */ + public String getName() { + return identifier==null ? null : identifier.getName(); + } + +} diff --git a/core/src/main/java/org/springframework/batch/core/domain/StepExecution.java b/core/src/main/java/org/springframework/batch/core/domain/StepExecution.java new file mode 100644 index 000000000..c5d45288d --- /dev/null +++ b/core/src/main/java/org/springframework/batch/core/domain/StepExecution.java @@ -0,0 +1,190 @@ +/* + * 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.core.domain; + +import java.sql.Timestamp; +import java.util.Properties; + +/** + * Batch domain object representation the execution of a step. Unlike + * JobExecution, there are four additional properties: luwCount, commitCount, + * rollbackCount and statistics. These values represent how many times a step + * has iterated through logical units of work, how many times it has been + * committed, and any other statistics the developer wishes to store, + * respectively. + * + * @author Lucas Ward + * + */ +public class StepExecution extends Entity { + + // TODO declare transient or make serializable + private BatchStatus status = BatchStatus.STARTING; + + private int taskCount = 0; + + private int commitCount = 0; + + private int rollbackCount = 0; + + private Timestamp startTime = new Timestamp(System.currentTimeMillis()); + + private Timestamp endTime = null; + + private Properties statistics = new Properties(); + + private Long stepId; + + private Long jobExecutionId; + + private int exitCode; + + /** + * Package private constructor for Hibernate + */ + StepExecution() { + super(); + } + + public StepExecution(Long stepId, Long jobExecutionId) { + this(); + this.stepId = stepId; + this.jobExecutionId = jobExecutionId; + } + + public void incrementCommitCount() { + commitCount++; + } + + public void incrementTaskCount() { + taskCount++; + } + + public void incrementRollbackCount() { + rollbackCount++; + } + + public Properties getStatistics() { + return statistics; + } + + public void setStatistics(Properties statistics) { + this.statistics = statistics; + } + + public Integer getCommitCount() { + return new Integer(commitCount); + } + + public void setCommitCount(int commitCount) { + this.commitCount = commitCount; + } + + public Timestamp getEndTime() { + return endTime; + } + + public void setEndTime(Timestamp endTime) { + this.endTime = endTime; + } + + public Integer getTaskCount() { + return new Integer(taskCount); + } + + public void setTaskCount(int taskCount) { + this.taskCount = taskCount; + } + + public void setRollbackCount(int rollbackCount) { + this.rollbackCount = rollbackCount; + } + + public Integer getRollbackCount() { + return new Integer(rollbackCount); + } + + public Timestamp getStartTime() { + return startTime; + } + + public void setStartTime(Timestamp startTime) { + this.startTime = startTime; + } + + public BatchStatus getStatus() { + return status; + } + + public void setStatus(BatchStatus status) { + this.status = status; + } + + public Long getStepId() { + return stepId; + } + + /** + * Accessor for the job execution id. + * @return the jobExecutionId + */ + public Long getJobExecutionId() { + return jobExecutionId; + } + + /* (non-Javadoc) + * @see org.springframework.batch.container.common.domain.Entity#equals(java.lang.Object) + */ + public boolean equals(Object obj) { + if (stepId==null && jobExecutionId==null || !(obj instanceof StepExecution) || getId()!=null) { + return super.equals(obj); + } + StepExecution other = (StepExecution) obj; + if (stepId==null) { + return jobExecutionId.equals(other.getJobExecutionId()); + } + return stepId.equals(other.getStepId()) && (jobExecutionId==null || jobExecutionId.equals(other.getJobExecutionId())); + } + + /* (non-Javadoc) + * @see org.springframework.batch.container.common.domain.Entity#hashCode() + */ + public int hashCode() { + return super.hashCode() + 31*(stepId!=null ? stepId.hashCode() : 0) + 91*(jobExecutionId!=null ? jobExecutionId.hashCode() : 0); + } + + public String toString() { + return super.toString() + ", taskCount=" + taskCount + ", commitCount=" + commitCount + ", rollbackCount=" + + rollbackCount; + } + + + /** + * @param exitCode + */ + public void setExitCode(int exitCode) { + this.exitCode = exitCode; + } + + /** + * @return the exitCode + */ + public int getExitCode() { + return exitCode; + } + +} diff --git a/core/src/main/java/org/springframework/batch/core/domain/StepInstance.java b/core/src/main/java/org/springframework/batch/core/domain/StepInstance.java new file mode 100644 index 000000000..1a7cec95e --- /dev/null +++ b/core/src/main/java/org/springframework/batch/core/domain/StepInstance.java @@ -0,0 +1,127 @@ +/* + * 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.core.domain; + +import org.springframework.batch.restart.GenericRestartData; +import org.springframework.batch.restart.RestartData; + +/** + *

+ * Batch domain entity representing a step which is sequentially executed by a + * job. Logically, steps are identified as a function of a job plus each step's + * name. For example, job 'TestJob' which has 2 steps: "TestStep1" and + * "TestStep2". The first step can be thought of as identified by + * "TestJob.TestStep1". In relational terms this may be represented by a foreign + * key on the Job's ID. Therefore, Each step instance is uniquely identified by + * it's ID, which is obtained from a JobRepository. Two steps with the same name + * and same job can be considered the same step. + *

+ * + *

+ * Because each step represents a runnable batch artifact with it's own + * lifecycle, each step contains status and an execution count. Status + * represents the status of each step's last execution (such as started, + * completed, failed, etc) and execution count is the count of executions for + * this individual step. It should be noted that a restartable job will create a + * new step instance (the same logical step, with a different ID) for every run. + *

+ * + * @author Lucas Ward + * @author Dave Syer + * + */ +public class StepInstance extends Entity { + + private JobInstance job; + + // TODO declare transient or make serializable + private BatchStatus status; + + private RestartData restartData = new GenericRestartData(null); + + private int stepExecutionCount = 0; + + private StepExecution stepExecution; + + private String name; + + public StepInstance() { + this(null); + } + + public StepInstance(Long stepId) { + setId(stepId); + } + + public int getStepExecutionCount() { + return stepExecutionCount; + } + + public void setStepExecutionCount(int stepExecutionCount) { + this.stepExecutionCount = stepExecutionCount; + } + + public RestartData getRestartData() { + return restartData; + } + + public void setRestartData(RestartData restartData) { + this.restartData = restartData; + } + + public BatchStatus getStatus() { + return status; + } + + public void setStatus(BatchStatus status) { + this.status = status; + } + + public void setJob(JobInstance job) { + this.job = job; + } + + public JobInstance getJob() { + return job; + } + + public StepExecution getStepExecution() { + return stepExecution; + } + + public void setStepExecution(StepExecution stepInstance) { + this.stepExecution = stepInstance; + } + + public void setName(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public Long getJobId() { + return job==null ? null : job.getId(); + } + + // @Override + public String toString() { + return super.toString() + ", name=" + name + ", status=" + getStatus() + " in " + job; + } + +} diff --git a/core/src/main/java/org/springframework/batch/core/domain/package.html b/core/src/main/java/org/springframework/batch/core/domain/package.html new file mode 100644 index 000000000..5b7a23f51 --- /dev/null +++ b/core/src/main/java/org/springframework/batch/core/domain/package.html @@ -0,0 +1,7 @@ + + +

+Interfaces and generic implementations of domain concerns. +

+ + diff --git a/core/src/main/java/org/springframework/batch/core/executor/JobExecutor.java b/core/src/main/java/org/springframework/batch/core/executor/JobExecutor.java new file mode 100644 index 000000000..fb183fea8 --- /dev/null +++ b/core/src/main/java/org/springframework/batch/core/executor/JobExecutor.java @@ -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.core.executor; + +import org.springframework.batch.core.configuration.JobConfiguration; +import org.springframework.batch.core.runtime.JobExecutionContext; +import org.springframework.batch.io.exception.BatchCriticalException; + +/** + * Interface for running a job from its configuration. + * + * @author Lucas Ward + * @author Dave Syer + * @see JobConfiguration + * @see JobExecutionContext + */ +public interface JobExecutor { + + public void run(JobConfiguration configuration, JobExecutionContext jobExecutionContext) throws BatchCriticalException; + +} diff --git a/core/src/main/java/org/springframework/batch/core/executor/StepExecutor.java b/core/src/main/java/org/springframework/batch/core/executor/StepExecutor.java new file mode 100644 index 000000000..c7f6d913f --- /dev/null +++ b/core/src/main/java/org/springframework/batch/core/executor/StepExecutor.java @@ -0,0 +1,55 @@ +/* + * 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.core.executor; + +import org.springframework.batch.core.configuration.StepConfiguration; +import org.springframework.batch.core.runtime.StepExecutionContext; +import org.springframework.batch.io.exception.BatchCriticalException; +import org.springframework.batch.repeat.ExitStatus; + +/** + * Interface for processing a step. Implementations are free to process the step + * and return when finished, or to schedule the step for processing + * concurrently, or in the future. The status of the execution should be + * trackable with the step execution context ({@see Step#getContext()}). The + * configuration should be treated as immutable.
+ * + * Because step execution paramaters and policies can vary from step to step, a + * {@link StepExecutor} should be created by the caller using a + * {@link StepExecutorFactory}. + * + * @author Lucas Ward + * @author Dave Syer + * + */ +public interface StepExecutor { + + /** + * Process the step according to the given configuration. + * + * @param configuration the configuration to use when running the step. + * Contains a recipe for the business logic of an individual processing + * operation. Also used to determine policies for commit intervals and + * exception handling, for instance. + * @param stepExecutionContext an entity representing the step to be executed + * @throws StepInterruptedException if the step is interrupted externally + * @throws BatchCriticalException if there is a problem that needs to be + * signalled to the caller + */ + ExitStatus process(StepConfiguration configuration, StepExecutionContext stepExecutionContext) throws StepInterruptedException, BatchCriticalException; + +} diff --git a/core/src/main/java/org/springframework/batch/core/executor/StepExecutorFactory.java b/core/src/main/java/org/springframework/batch/core/executor/StepExecutorFactory.java new file mode 100644 index 000000000..ff9365191 --- /dev/null +++ b/core/src/main/java/org/springframework/batch/core/executor/StepExecutorFactory.java @@ -0,0 +1,44 @@ +/* + * 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.core.executor; + +import org.springframework.batch.core.configuration.StepConfiguration; + +/** + * Factory interface for creating or locating {@link StepExecutor} instances. + * Because step execution parameters and policies can vary from step to step, a + * {@link StepExecutor} should be created by the caller using a + * {@link StepExecutorFactory}. The factory is responsible for ensuring that + * the returned instance is appropriate for the configuration supplied. If the + * {@link StepExecutor} instance is stateful (which is normal) the factory + * should return a different instance for each call. + * + * @author Dave Syer + * + */ +public interface StepExecutorFactory { + + /** + * Use the configuration given to create or locate a suitable + * {@link StepExecutor}. + * + * @param configuration a {@link StepConfiguration} instance. + * @return a {@link StepExecutor} that can be used to execute a step with + * the given configuration + */ + StepExecutor getExecutor(StepConfiguration configuration); + +} diff --git a/core/src/main/java/org/springframework/batch/core/executor/StepInterruptedException.java b/core/src/main/java/org/springframework/batch/core/executor/StepInterruptedException.java new file mode 100644 index 000000000..c83622bc4 --- /dev/null +++ b/core/src/main/java/org/springframework/batch/core/executor/StepInterruptedException.java @@ -0,0 +1,30 @@ +/* + * 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.core.executor; + +/** + * Exception to indicate the the lifecycle has been interrupted. + * + * @author Lucas Ward + * + */ +public class StepInterruptedException extends Exception { + + public StepInterruptedException(String msg){ + super(msg); + } +} diff --git a/core/src/main/java/org/springframework/batch/core/executor/package.html b/core/src/main/java/org/springframework/batch/core/executor/package.html new file mode 100644 index 000000000..d3a965f37 --- /dev/null +++ b/core/src/main/java/org/springframework/batch/core/executor/package.html @@ -0,0 +1,7 @@ + + +

+Interfaces and generic implementations of executor concerns. +

+ + diff --git a/core/src/main/java/org/springframework/batch/core/package.html b/core/src/main/java/org/springframework/batch/core/package.html new file mode 100644 index 000000000..51b136d97 --- /dev/null +++ b/core/src/main/java/org/springframework/batch/core/package.html @@ -0,0 +1,11 @@ + + +

+Core domain context for Spring Batch covering jobs, steps, +configuration and execution abstractions. Most classes here are +interfaces with implementations saved for specific applications. This +is the public API of Spring Batch. There is a reference +implementation of the core interfaces in the execution module. +

+ + diff --git a/core/src/main/java/org/springframework/batch/core/repository/BatchRestartException.java b/core/src/main/java/org/springframework/batch/core/repository/BatchRestartException.java new file mode 100644 index 000000000..09e787b3d --- /dev/null +++ b/core/src/main/java/org/springframework/batch/core/repository/BatchRestartException.java @@ -0,0 +1,41 @@ +/* + * 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.core.repository; + +import org.springframework.batch.io.exception.BatchCriticalException; + +/** + * @author Dave Syer + * + */ +public class BatchRestartException extends BatchCriticalException { + + /** + * @param string the message + */ + public BatchRestartException(String string) { + super(string); + } + + /** + * @param msg the cause + * @param t the message + */ + public BatchRestartException(String msg, Throwable t) { + super(msg, t); + } + +} diff --git a/core/src/main/java/org/springframework/batch/core/repository/JobRepository.java b/core/src/main/java/org/springframework/batch/core/repository/JobRepository.java new file mode 100644 index 000000000..d32b536dd --- /dev/null +++ b/core/src/main/java/org/springframework/batch/core/repository/JobRepository.java @@ -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.core.repository; + +import org.springframework.batch.core.configuration.JobConfiguration; +import org.springframework.batch.core.domain.JobExecution; +import org.springframework.batch.core.domain.JobInstance; +import org.springframework.batch.core.domain.StepExecution; +import org.springframework.batch.core.domain.StepInstance; +import org.springframework.batch.core.runtime.JobIdentifier; + +/** + *

+ * Repository for storing batch jobs and steps. Before using any methods, a Job + * must first be obtained using the findOrCreateJob method. Once a Job and it's + * related steps are obtained, they can be updated. It should be noted that any + * reconstituted steps are expected to contain restart data if the + * RestartPolicy associated with the step returns true, and RestartData exists. + *

+ * + * Once a Job/Steps has been created, Job and Step executions can be created and + * associated with a job, by setting the JobId and StepId respectively. Once + * these Id's are set, an execution can be persisted. If the object is in a + * transient state (i.e. it has no id of it's own) then an ID will be created + * for that specific execution, and then stored ('saved'). (NOTE: The + * relationship between a Job/Step and Job/StepExecutions is 1:N) If an ID does + * exist, then the execution will be stored ('updated'). + * + * + * @author Lucas Ward + * + */ +public interface JobRepository { + + /** + * Find or create a job for a given Job identifier or configuration. If the + * job that is uniquely identified by JobIdentifier already exists, it's + * persisted values (including ID) will be returned in a new Job object. If + * no previous run is found, a new job will be created and returned. + * @param jobConfiguration - describes the configuration for jobs and steps + * @param runtimeInformation TODO + * + * @return a valid job + * + * + * @throws NoSuchBatchDomainObjectException if more than one job is found for + * the given configuration. + */ + public JobInstance findOrCreateJob(JobConfiguration jobConfiguration, JobIdentifier jobIdentifier); + + /** + * Update a Job. + * + * Preconditions: Job must contain a valid ID. This can be ensured by first + * obtaining a job from findOrCreateJob. + * + * @param job + * @see JobInstance + */ + public void update(JobInstance job); + + /** + * Save or Update a JobExecution. If no ID is found a new instance will be + * created. (saved). If an ID does exist it will be updated. It is not + * advisable that an ID be assigned to a JobExecution before calling this + * method. Instead, it should be left blank, to be assigned by a + * JobRepository. + * + * Preconditions: JobExecution must contain a valid JobId. + * + * @param jobInstance + */ + public void saveOrUpdate(JobExecution jobExecution); + + /** + * Update a step. + * + * Preconditions: Step must contain a valid ID. This can be ensured by first + * obtaining a Job from findOrCreateJob, and accessing it's step list. + * + * @param step + * @see StepInstance + */ + public void update(StepInstance step); + + /** + * Save or Update a StepExecution. If no ID is found a new instance will be + * created. (saved). If an ID does exist it will be updated. It is not + * advisable that an ID be assigned to a JobExecution before calling this + * method. Instead, it should be left blank, to be assigned by a + * JobRepository. + * + * Preconditions: StepExecution must have a valid StepId. + * + * @param jobInstance + */ + public void saveOrUpdate(StepExecution stepExecution); + +} diff --git a/core/src/main/java/org/springframework/batch/core/repository/NoSuchBatchDomainObjectException.java b/core/src/main/java/org/springframework/batch/core/repository/NoSuchBatchDomainObjectException.java new file mode 100644 index 000000000..12b65c91c --- /dev/null +++ b/core/src/main/java/org/springframework/batch/core/repository/NoSuchBatchDomainObjectException.java @@ -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.core.repository; + +/** + * This exception identifies that a batch domain object is invalid, which + * is generally caused by an invalid ID. (An ID which doesn't exist in the database). + * + * @author Lucas Ward + * @author Dave Syer + * + */ +public class NoSuchBatchDomainObjectException extends RuntimeException { + + private static final long serialVersionUID = 4399621765157283111L; + + public NoSuchBatchDomainObjectException(String message){ + super(message); + } +} diff --git a/core/src/main/java/org/springframework/batch/core/repository/package.html b/core/src/main/java/org/springframework/batch/core/repository/package.html new file mode 100644 index 000000000..88ab2c6da --- /dev/null +++ b/core/src/main/java/org/springframework/batch/core/repository/package.html @@ -0,0 +1,7 @@ + + +

+Interfaces and generic implementations of repository concerns. +

+ + diff --git a/core/src/main/java/org/springframework/batch/core/runtime/JobExecutionContext.java b/core/src/main/java/org/springframework/batch/core/runtime/JobExecutionContext.java new file mode 100644 index 000000000..370ad4972 --- /dev/null +++ b/core/src/main/java/org/springframework/batch/core/runtime/JobExecutionContext.java @@ -0,0 +1,198 @@ +/* + * 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.core.runtime; + +import java.sql.Timestamp; +import java.util.Collection; +import java.util.HashSet; + +import org.springframework.batch.core.domain.JobExecution; +import org.springframework.batch.core.domain.JobInstance; +import org.springframework.batch.core.domain.StepExecution; +import org.springframework.batch.repeat.RepeatContext; + +/** + * Context for an executing job. Maintains invariants and provides communication + * channel for all components requiring information about the job and its steps. + * + * @author Dave Syer + * + */ +public class JobExecutionContext { + + private JobIdentifier jobIdentifier; + + private final JobInstance job; + + private final JobExecution jobExecution; + + private Collection stepExecutions = new HashSet(); + + private Collection stepContexts = new HashSet(); + + private Collection chunkContexts = new HashSet(); + + /** + * Constructor with all the mandatory properties. + * + * @param jobIdentifier + */ + public JobExecutionContext(JobIdentifier jobIdentifier, JobInstance job) { + super(); + this.jobIdentifier = jobIdentifier; + this.job = job; + this.jobExecution = new JobExecution(job.getId()); + this.jobExecution.setStartTime(new Timestamp(System.currentTimeMillis())); + } + + /** + * Accessor for the potentially multiple chunk contexts that are in + * progress. In a single-threaded, sequential execution there would normally + * be only one current chunk, but in more complicated scenarios there might + * be multiple active contexts. + * @return all the chunk contexts that have been registered and not + * unregistered. A collection opf {@link RepeatContext} objects. + */ + public Collection getChunkContexts() { + synchronized (chunkContexts) { + return new HashSet(chunkContexts); + } + } + + /** + * Accessor for the runtime information of this execution. + * @return the {@link JobRuntimeInformation} that was used to start this job + * execution. + */ + public JobIdentifier getJobIdentifier() { + return jobIdentifier; + } + + /** + * Accessor for the potentially multiple step contexts that are in progress. + * In a single-threaded, sequential execution there would normally be only + * one current step, but in more complicated scenarios there might be + * multiple active contexts. + * @return all the step contexts that have been registered and not + * unregistered. A collection of {@link RepeatContext} objects. + */ + public Collection getStepContexts() { + synchronized (stepContexts) { + return new HashSet(stepContexts); + } + } + + /** + * Called at the start of a step, before any business logic is processed. + * @param context the current step context. + */ + public void registerStepContext(RepeatContext stepContext) { + synchronized (stepContexts) { + this.stepContexts.add(stepContext); + } + } + + /** + * Called at the end of a step, after all business logic is processed, or in + * the case of a failure. + * @param context the current step context. + */ + public void unregisterStepContext(RepeatContext stepContext) { + synchronized (stepContexts) { + this.stepContexts.remove(stepContext); + } + } + + /** + * Called at the start of a chunk, before any business logic is processed. + * @param context the current chunk context. + */ + public void registerChunkContext(RepeatContext chunkContext) { + synchronized (chunkContexts) { + this.chunkContexts.add(chunkContext); + } + } + + /** + * Called at the end of a chunk, after all business logic is processed, or + * in the case of a failure. + * @param context the current chunk context. + */ + public void unregisterChunkContext(RepeatContext chunkContext) { + synchronized (chunkContexts) { + this.chunkContexts.remove(chunkContext); + } + } + + /** + * @return the Job that is executing. + */ + public JobInstance getJob() { + return job; + } + + /** + * @return the current job execution. + */ + public JobExecution getJobExecution() { + return jobExecution; + } + + /** + * Accessor for the step executions. + * @return the step executions that were registered + */ + public Collection getStepExecutions() { + return stepExecutions; + } + + /** + * Register a step execution with the current job execution. + * @param stepExecution + */ + public void registerStepExecution(StepExecution stepExecution) { + this.stepExecutions.add(stepExecution); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + public boolean equals(Object obj) { + if (!(obj instanceof JobExecutionContext)) { + return super.equals(obj); + } + JobExecutionContext other = (JobExecutionContext) obj; + return job.equals(other.getJob()) && jobExecution.equals(other.getJobExecution()); + } + + /* (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + public int hashCode() { + return 23*job.hashCode() + 61*jobExecution.hashCode(); + } + + + /* + * (non-Javadoc) + * @see java.lang.Object#toString() + */ + public String toString() { + return "identifier=" + jobIdentifier + "; steps=" + stepContexts + "; chunks=" + chunkContexts; + } + +} diff --git a/core/src/main/java/org/springframework/batch/core/runtime/JobExecutionContextFactory.java b/core/src/main/java/org/springframework/batch/core/runtime/JobExecutionContextFactory.java new file mode 100644 index 000000000..ccc49bc58 --- /dev/null +++ b/core/src/main/java/org/springframework/batch/core/runtime/JobExecutionContextFactory.java @@ -0,0 +1,27 @@ +/* + * 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.core.runtime; + + +/** + * @author Dave Syer + * + */ +public interface JobExecutionContextFactory { + + JobExecutionContext create(JobIdentifier jobIdentifier); + +} diff --git a/core/src/main/java/org/springframework/batch/core/runtime/JobExecutionRegistry.java b/core/src/main/java/org/springframework/batch/core/runtime/JobExecutionRegistry.java new file mode 100644 index 000000000..38e8229dd --- /dev/null +++ b/core/src/main/java/org/springframework/batch/core/runtime/JobExecutionRegistry.java @@ -0,0 +1,94 @@ +/* + * 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.core.runtime; + +import java.util.Collection; + +import org.springframework.batch.core.domain.JobExecution; +import org.springframework.batch.core.domain.JobInstance; + +/** + * Registry for currently active job executions, which can be used for + * monitoring and management purposes. Not to be confused with a persistent + * repository of jobs. + * + * @author Dave Syer + * + */ +public interface JobExecutionRegistry { + + /** + * Register a job instance and obtain the runtime context of the + * execution. + * + * @param runtimeInformation the {@link JobRuntimeInformation} that can be + * used to identify this execution in subsequent calls to the registry. Must + * not be null. + * @param job + * @param the {@link JobInstance} instance to register. + * + * @throws NullPointerException if the first parameter is null. + */ + JobExecutionContext register(JobIdentifier jobIdentifier, JobInstance job); + + /** + * Check if a given {@link JobExecution}, or one with the same id property, + * is already registered. + * + * @param runtimeInformation the {@link JobIdentifier} to check. + * @return true if it has been registered. + */ + boolean isRegistered(JobIdentifier jobIdentifier); + + /** + * Unregister a particular {@link JobExecution}, or one with the same id + * property. + * + * @param execution the {@link JobIdentifier} to unregister. + */ + void unregister(JobIdentifier jobIdentifier); + + /** + * Find all the currently registered {@link JobExecutionContext} objects. + * + * @return all the currently registered contexts. + */ + Collection findAll(); + + /** + * Return a collection of {@link JobExecutionContext} objects representing + * the currently executing jobs with {@link JobRuntimeInformation} having + * the given name. + * + * @param name the name of the {@link JobRuntimeInformation} as a to key the + * search. The name can be null, in which case the key is null, i.e. + * {@link JobRuntimeInformation} instances with null name will match. + * @return a {@link Collection} of {@link JobExecutionContext}. + */ + Collection findByName(String name); + + /** + * Return a {@link JobExecutionContext} representing the currently executing + * jobs with the given {@link JobRuntimeInformation}. + * + * @param runtimeInformation the {@link JobIdentifier} to use as a + * search key. + * @return the {@link JobExecutionContext} that was registered under the + * given key, if there is one, null otherwise. + */ + JobExecutionContext get(JobIdentifier jobIdentifier); + +} diff --git a/core/src/main/java/org/springframework/batch/core/runtime/JobIdentifier.java b/core/src/main/java/org/springframework/batch/core/runtime/JobIdentifier.java new file mode 100644 index 000000000..6f572118b --- /dev/null +++ b/core/src/main/java/org/springframework/batch/core/runtime/JobIdentifier.java @@ -0,0 +1,24 @@ +/* + * 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.core.runtime; + + +public interface JobIdentifier { + + public String getName(); + +} diff --git a/core/src/main/java/org/springframework/batch/core/runtime/JobIdentifierFactory.java b/core/src/main/java/org/springframework/batch/core/runtime/JobIdentifierFactory.java new file mode 100644 index 000000000..d9ddadbfd --- /dev/null +++ b/core/src/main/java/org/springframework/batch/core/runtime/JobIdentifierFactory.java @@ -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.core.runtime; + +/** + * A job configuration can be executed with many possible runtime parameters, + * which identify the instance of the job. This factory allows job identifiers + * to be created with different properties according to the + * {@link JobIdentifier} strategy required. For example some projects or jobs + * need a schedule date as part of the {@link JobIdentifier} and some do not + * (e.g. for an ad-hoc execution a simple label might be enough). + * + * + * @author Dave Syer + * + */ +public interface JobIdentifierFactory { + + /** + * Get a new {@link JobIdentifier} instance. + * @param name the name of the job configuration. + * @return a {@link JobIdentifier} with the same name. + */ + public JobIdentifier getJobIdentifier(String name); +} diff --git a/core/src/main/java/org/springframework/batch/core/runtime/SimpleJobIdentifier.java b/core/src/main/java/org/springframework/batch/core/runtime/SimpleJobIdentifier.java new file mode 100644 index 000000000..01d06f832 --- /dev/null +++ b/core/src/main/java/org/springframework/batch/core/runtime/SimpleJobIdentifier.java @@ -0,0 +1,65 @@ +/* + * 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.core.runtime; + + +/** + * @author Dave Syer + * + */ +public class SimpleJobIdentifier implements JobIdentifier { + + private String name; + + + /** + * Default constructor. + */ + public SimpleJobIdentifier() { + super(); + } + + /** + * Convenience constructor with name. + * @param name + */ + public SimpleJobIdentifier(String name) { + super(); + this.name = name; + } + + /* (non-Javadoc) + * @see org.springframework.batch.core.runtime.JobIdentifier#getName() + */ + public String getName() { + return this.name; + } + + /** + * Public setter for the name. + * + * @param name the name to set + */ + public void setName(String name) { + this.name = name; + } + + public String toString() { + + return "name=" + name; + } + +} diff --git a/core/src/main/java/org/springframework/batch/core/runtime/StepExecutionContext.java b/core/src/main/java/org/springframework/batch/core/runtime/StepExecutionContext.java new file mode 100644 index 000000000..dfd57764a --- /dev/null +++ b/core/src/main/java/org/springframework/batch/core/runtime/StepExecutionContext.java @@ -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.core.runtime; + +import org.springframework.batch.core.domain.StepExecution; +import org.springframework.batch.core.domain.StepInstance; +import org.springframework.util.Assert; + +/** + * Context for an executing step within a job. Maintains invariants and provides + * communication channel for all components requiring information about the + * step. + * + * @author Dave Syer + * + */ +public class StepExecutionContext { + + private JobExecutionContext jobExecutionContext; + + private final StepInstance step; + + private final StepExecution stepExecution; + + /** + * Constructor with all the mandatory properties. + * + * @param jobExecutionContext + */ + public StepExecutionContext(JobExecutionContext jobExecutionContext, StepInstance step) { + super(); + Assert.notNull(jobExecutionContext); + Assert.notNull(jobExecutionContext.getJobExecution(), "The JobExecutionContext must have a JobExecution"); + Assert.notNull(step); + this.jobExecutionContext = jobExecutionContext; + this.step = step; + this.stepExecution = new StepExecution(step.getId(), jobExecutionContext.getJobExecution().getId()); + } + + /** + * Accessor for the step governing this execution. + * @return the step + */ + public StepInstance getStep() { + return step; + } + + /** + * Accessor for the execution context information of the enclosing job. + * @return the {@link jobExecutionContext} that was used to start this step + * execution. + */ + public JobExecutionContext getJobExecutionContext() { + return jobExecutionContext; + } + + /** + * Retrieve the current step execution or create a new one if there is none. + * @return the current step execution. + */ + public StepExecution getStepExecution() { + return stepExecution; + } + + /* + * (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + public boolean equals(Object obj) { + if (!(obj instanceof StepExecutionContext)) { + return super.equals(obj); + } + StepExecutionContext other = (StepExecutionContext) obj; + return step.equals(other.getStep()) && stepExecution.equals(other.getStepExecution()); + } + + /* (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + public int hashCode() { + return 23*step.hashCode() + 61*stepExecution.hashCode(); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#toString() + */ + public String toString() { + return "step=" + step + "; stepExecution=" + stepExecution; + } + +} diff --git a/core/src/main/java/org/springframework/batch/core/runtime/package.html b/core/src/main/java/org/springframework/batch/core/runtime/package.html new file mode 100644 index 000000000..5128279b8 --- /dev/null +++ b/core/src/main/java/org/springframework/batch/core/runtime/package.html @@ -0,0 +1,7 @@ + + +

+Interfaces and generic implementations of runtime concerns. +

+ + diff --git a/core/src/main/java/org/springframework/batch/core/tasklet/Recoverable.java b/core/src/main/java/org/springframework/batch/core/tasklet/Recoverable.java new file mode 100644 index 000000000..27b0baa1c --- /dev/null +++ b/core/src/main/java/org/springframework/batch/core/tasklet/Recoverable.java @@ -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.core.tasklet; + +/** + * Marker interface for {@link Tasklet} implementations that are able totake a + * recovery action in the case that an exception is thrown inside + * {@link Tasklet#execute()}. Containers must ensure that the recover method is + * called in a different transactional context than the failed execution, e.g. + * by creating a new transaction with propagation REQUIRES_NEW. + * + * @author Dave Syer + * + */ +public interface Recoverable { + + /** + * Take some action to recover the current batch operation. E.g. send a + * message to an error queue, or append a bad record to a special file. + * + * @param cause the exception that caused the recovery step to be called. + */ + void recover(Throwable cause); + +} diff --git a/core/src/main/java/org/springframework/batch/core/tasklet/Tasklet.java b/core/src/main/java/org/springframework/batch/core/tasklet/Tasklet.java new file mode 100644 index 000000000..0499ac254 --- /dev/null +++ b/core/src/main/java/org/springframework/batch/core/tasklet/Tasklet.java @@ -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.core.tasklet; + +import org.springframework.batch.core.configuration.StepConfiguration; + +/** + * The primary interface describing the touch-point between the batch developer + * and a spring-batch execution. The execute method will be called to indicate + * to the developer that it is time to execute business logic. The value + * returned from this method will indicate whether or not processing should + * continue. It is important to note that in the vast majority of cases this + * class should not be directly implemented by batch developers for processing. + * Most batch processing is significantly more complex than simple execute and + * should logically be broken into a minimum of two processes (read and write). + * However, many architecture teams may find creating their own implementations + * of this interface useful for differentiating different batch job types, or + * for creating more flexibility within their batch jobs. + * + * @see StepConfiguration + * @author Lucas Ward + * @author Dave Syer + * + */ +public interface Tasklet { + + /** + * Primary batch processing driver. All processing of batch business data + * should be handled within this method. Any processing which intends to + * control the flow of the batch lifecycle by throwing exceptions (such as + * BatchCriticalExeception) should throw them within this method. Doing so + * outside of this method will prevent the architecture from gracefully + * shutting down and providing such features as transaction rollback. + * + * @return boolean indicating whether the processing should continue (i.e. + * false when data are exhausted). + */ + public boolean execute() throws Exception; + +} diff --git a/core/src/main/java/org/springframework/batch/core/tasklet/package.html b/core/src/main/java/org/springframework/batch/core/tasklet/package.html new file mode 100644 index 000000000..5bf66aab7 --- /dev/null +++ b/core/src/main/java/org/springframework/batch/core/tasklet/package.html @@ -0,0 +1,7 @@ + + +

+Interfaces and generic implementations of tasklet concerns. +

+ + diff --git a/core/src/main/java/overview.html b/core/src/main/java/overview.html new file mode 100644 index 000000000..5310fa75f --- /dev/null +++ b/core/src/main/java/overview.html @@ -0,0 +1,8 @@ + + +

+The Core domain concepts expressed as interfaces and generic +implementations. +

+ + diff --git a/core/src/site/apt/changelog.apt b/core/src/site/apt/changelog.apt new file mode 100644 index 000000000..18947ba75 --- /dev/null +++ b/core/src/site/apt/changelog.apt @@ -0,0 +1,8 @@ +Changelog: Spring Batch Core + +* 1.0-M2 + +** 2007/07/12 + + * No-one uses this file: we should just switch to auto-generated changelogs? + diff --git a/core/src/site/apt/index.apt b/core/src/site/apt/index.apt new file mode 100644 index 000000000..a820196ce --- /dev/null +++ b/core/src/site/apt/index.apt @@ -0,0 +1,60 @@ + ------ + Simple Batch Execution Core + ------ + Dave Syer + ------ + August 2007 + +Overview of the Spring Batch Core Domain + + The Spring Batch Core Domain consists of a public API for launching, + monitoring and managing batch jobs. + +[images/core-domain-overview.png] The Spring Batch Core Domain with +dependencies to infrastructure indicated schematically. + + The figure above shows the central parts of the core domain and its + main touch points with the batch application develepor + (<<<*Configuration>>>). To launch a job there is a + <<>> interface and a facade for it that can be used to + simplify the launching for dumb clients like JMX or a command line. + + A <<>> is composed of a list of + <<>>s, each of which is executed in turn by the + <<>>, delegating to a <<>>. The + <<>> is a central strategy in the Spring Batch Core. + Implementations of <<>> are responsible for sharing + the work specified by the <<>> out, but in ways + that the configuration doesn't need to be aware of. For instance, + the same <<>> might be used in a simple + in-process sequential executor, or in a multi-threaded + implementation, or one that delegates to remote calls to a + distributed system. + +[images/core-domain-extended.png] The Spring Batch Core Domain +extended to include the datababase entities and identifier strategy. + + A <<>> can be re-used to create multiple job + instances and this is reflected in the figure above showing an + extended picture of the core domain. When a <<>> + is launched the <<>> first checks to see if a job with + the same <<>> was already executed. We expect one of + the following outcomes, depending on the <<>> + implementation and <<>>: + + * If the job was not previously launched then it can be created + and executed. A new <<>> is created and stored in a + repository (usually a database). A new <<>> is also + created to track the progress of this particular execution. + + * If the job was previously launched the <<>> + has a flag indicating whether or not to continue and launch a new + execution (was this expected?). The decision is parameterised to + depend on whether or not the job failed last time it was executed. + + If the there was a previous failure - maybe the operator has fixed + some bad input and wants to run it again - then we might want to + restart the previous job. Or it might be an ad-hoc request that + doesn't need to be distinguished from previous runs. In either + case a new <<>> is created and stored to monitor + this execution of the <<>>. diff --git a/core/src/site/resources/images/core-domain-extended.png b/core/src/site/resources/images/core-domain-extended.png new file mode 100644 index 000000000..a8307fea8 Binary files /dev/null and b/core/src/site/resources/images/core-domain-extended.png differ diff --git a/core/src/site/resources/images/core-domain-overview.png b/core/src/site/resources/images/core-domain-overview.png new file mode 100644 index 000000000..d65938f50 Binary files /dev/null and b/core/src/site/resources/images/core-domain-overview.png differ diff --git a/core/src/site/site.xml b/core/src/site/site.xml new file mode 100644 index 000000000..772a274cf --- /dev/null +++ b/core/src/site/site.xml @@ -0,0 +1,31 @@ + + + + Spring Batch: ${project.name} + + + images/shim.gif + + + + + + org.springframework.maven.skins + maven-spring-skin + 1.0.3 + + + + + + + + + + + + + + + + diff --git a/core/src/test/java/org/springframework/batch/core/AbstractExceptionTests.java b/core/src/test/java/org/springframework/batch/core/AbstractExceptionTests.java new file mode 100644 index 000000000..3fa611c9b --- /dev/null +++ b/core/src/test/java/org/springframework/batch/core/AbstractExceptionTests.java @@ -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.core; + +import junit.framework.TestCase; + +public abstract class AbstractExceptionTests extends TestCase { + + public void testExceptionString() throws Exception { + Exception exception = getException("foo"); + assertEquals("foo", exception.getMessage()); + } + + public void testExceptionStringThrowable() throws Exception { + Exception exception = getException("foo", new IllegalStateException()); + assertEquals("foo", exception.getMessage().substring(0, 3)); + } + + public abstract Exception getException(String msg) throws Exception; + + public abstract Exception getException(String msg, Throwable t) throws Exception; + +} diff --git a/core/src/test/java/org/springframework/batch/core/configuration/DuplicateJobConfigurationExceptionTests.java b/core/src/test/java/org/springframework/batch/core/configuration/DuplicateJobConfigurationExceptionTests.java new file mode 100644 index 000000000..d3dfeef11 --- /dev/null +++ b/core/src/test/java/org/springframework/batch/core/configuration/DuplicateJobConfigurationExceptionTests.java @@ -0,0 +1,44 @@ +/* + * 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.core.configuration; + +import org.springframework.batch.core.AbstractExceptionTests; +import org.springframework.batch.core.configuration.DuplicateJobConfigurationException; + +/** + * @author Dave Syer + * + */ +public class DuplicateJobConfigurationExceptionTests extends AbstractExceptionTests { + + /* + * (non-Javadoc) + * @see org.springframework.batch.io.exception.AbstractExceptionTests#getException(java.lang.String) + */ + public Exception getException(String msg) throws Exception { + return new DuplicateJobConfigurationException(msg); + } + + /* + * (non-Javadoc) + * @see org.springframework.batch.io.exception.AbstractExceptionTests#getException(java.lang.String, + * java.lang.Throwable) + */ + public Exception getException(String msg, Throwable t) throws Exception { + return new DuplicateJobConfigurationException(msg, t); + } + +} diff --git a/core/src/test/java/org/springframework/batch/core/configuration/JobConfigurationExceptionTests.java b/core/src/test/java/org/springframework/batch/core/configuration/JobConfigurationExceptionTests.java new file mode 100644 index 000000000..0d238482b --- /dev/null +++ b/core/src/test/java/org/springframework/batch/core/configuration/JobConfigurationExceptionTests.java @@ -0,0 +1,44 @@ +/* + * 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.core.configuration; + +import org.springframework.batch.core.AbstractExceptionTests; +import org.springframework.batch.core.configuration.JobConfigurationException; + +/** + * @author Dave Syer + * + */ +public class JobConfigurationExceptionTests extends AbstractExceptionTests { + + /* + * (non-Javadoc) + * @see org.springframework.batch.io.exception.AbstractExceptionTests#getException(java.lang.String) + */ + public Exception getException(String msg) throws Exception { + return new JobConfigurationException(msg); + } + + /* + * (non-Javadoc) + * @see org.springframework.batch.io.exception.AbstractExceptionTests#getException(java.lang.String, + * java.lang.Throwable) + */ + public Exception getException(String msg, Throwable t) throws Exception { + return new JobConfigurationException(msg, t); + } + +} diff --git a/core/src/test/java/org/springframework/batch/core/configuration/JobConfigurationTests.java b/core/src/test/java/org/springframework/batch/core/configuration/JobConfigurationTests.java new file mode 100644 index 000000000..4001e5f9f --- /dev/null +++ b/core/src/test/java/org/springframework/batch/core/configuration/JobConfigurationTests.java @@ -0,0 +1,106 @@ +/* + * 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.core.configuration; + +import java.util.Collections; + +import junit.framework.TestCase; + +/** + * @author Dave Syer + * + */ +public class JobConfigurationTests extends TestCase { + + JobConfiguration configuration = new JobConfiguration("job"); + + /** + * Test method for + * {@link org.springframework.batch.core.configuration.JobConfiguration#JobConfiguration()}. + */ + public void testJobConfiguration() { + configuration = new JobConfiguration(); + assertNull(configuration.getName()); + } + + /** + * Test method for + * {@link org.springframework.batch.core.configuration.JobConfiguration#setBeanName(java.lang.String)}. + */ + public void testSetBeanName() { + configuration.setBeanName("foo"); + assertEquals("job", configuration.getName()); + } + + /** + * Test method for + * {@link org.springframework.batch.core.configuration.JobConfiguration#setBeanName(java.lang.String)}. + */ + public void testSetBeanNameWithNullName() { + configuration.setName(null); + assertEquals(null, configuration.getName()); + configuration.setBeanName("foo"); + assertEquals("foo", configuration.getName()); + } + + /** + * Test method for + * {@link org.springframework.batch.core.configuration.JobConfiguration#setName(java.lang.String)}. + */ + public void testSetName() { + configuration.setName("foo"); + assertEquals("foo", configuration.getName()); + } + + /** + * Test method for + * {@link org.springframework.batch.core.configuration.JobConfiguration#setSteps(java.util.List)}. + */ + public void testSetSteps() { + configuration.setSteps(Collections.singletonList(new StepConfigurationSupport("step"))); + assertEquals(1, configuration.getStepConfigurations().size()); + } + + /** + * Test method for + * {@link org.springframework.batch.core.configuration.JobConfiguration#addStep(org.springframework.batch.core.configuration.StepConfiguration)}. + */ + public void testAddStep() { + configuration.addStep(new StepConfigurationSupport("step")); + assertEquals(1, configuration.getStepConfigurations().size()); + } + + /** + * Test method for + * {@link org.springframework.batch.core.configuration.JobConfiguration#setStartLimit(int)}. + */ + public void testSetStartLimit() { + assertEquals(Integer.MAX_VALUE, configuration.getStartLimit()); + configuration.setStartLimit(10); + assertEquals(10, configuration.getStartLimit()); + } + + /** + * Test method for + * {@link org.springframework.batch.core.configuration.JobConfiguration#setRestartable(boolean)}. + */ + public void testSetRestartable() { + assertFalse(configuration.isRestartable()); + configuration.setRestartable(true); + assertTrue(configuration.isRestartable()); + } + +} diff --git a/core/src/test/java/org/springframework/batch/core/configuration/NoSuchJobConfigurationExceptionTests.java b/core/src/test/java/org/springframework/batch/core/configuration/NoSuchJobConfigurationExceptionTests.java new file mode 100644 index 000000000..81a8948de --- /dev/null +++ b/core/src/test/java/org/springframework/batch/core/configuration/NoSuchJobConfigurationExceptionTests.java @@ -0,0 +1,44 @@ +/* + * 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.core.configuration; + +import org.springframework.batch.core.AbstractExceptionTests; +import org.springframework.batch.core.configuration.NoSuchJobConfigurationException; + +/** + * @author Dave Syer + * + */ +public class NoSuchJobConfigurationExceptionTests extends AbstractExceptionTests { + + /* + * (non-Javadoc) + * @see org.springframework.batch.io.exception.AbstractExceptionTests#getException(java.lang.String) + */ + public Exception getException(String msg) throws Exception { + return new NoSuchJobConfigurationException(msg); + } + + /* + * (non-Javadoc) + * @see org.springframework.batch.io.exception.AbstractExceptionTests#getException(java.lang.String, + * java.lang.Throwable) + */ + public Exception getException(String msg, Throwable t) throws Exception { + return new NoSuchJobConfigurationException(msg, t); + } + +} diff --git a/core/src/test/java/org/springframework/batch/core/configuration/StepConfigurationSupportTests.java b/core/src/test/java/org/springframework/batch/core/configuration/StepConfigurationSupportTests.java new file mode 100644 index 000000000..83ec82437 --- /dev/null +++ b/core/src/test/java/org/springframework/batch/core/configuration/StepConfigurationSupportTests.java @@ -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.core.configuration; + +import junit.framework.TestCase; + +import org.springframework.batch.core.tasklet.Tasklet; + +/** + * @author Dave Syer + * + */ +public class StepConfigurationSupportTests extends TestCase { + + private StepConfigurationSupport configuration = new StepConfigurationSupport("step"); + + /** + * Test method for {@link org.springframework.batch.core.configuration.StepConfigurationSupport#StepConfigurationSupport()}. + */ + public void testStepConfigurationSupport() { + configuration = new StepConfigurationSupport(); + assertNull(configuration.getName()); + } + + /** + * Test method for {@link org.springframework.batch.core.configuration.StepConfigurationSupport#getName()}. + */ + public void testGetName() { + configuration.setName("foo"); + assertEquals("foo", configuration.getName()); + } + + /** + * Test method for {@link org.springframework.batch.core.configuration.StepConfigurationSupport#getStartLimit()}. + */ + public void testGetStartLimit() { + assertEquals(Integer.MAX_VALUE, configuration.getStartLimit()); + configuration.setStartLimit(10); + assertEquals(10, configuration.getStartLimit()); + } + + /** + * Test method for {@link org.springframework.batch.core.configuration.StepConfigurationSupport#getTasklet()}. + */ + public void testGetTasklet() { + assertEquals(null, configuration.getTasklet()); + Tasklet tasklet = new Tasklet() { + public boolean execute() throws Exception { + return false; + } + }; + configuration.setTasklet(tasklet); + assertEquals(tasklet, configuration.getTasklet()); + } + + /** + * Test method for {@link org.springframework.batch.core.configuration.StepConfigurationSupport#isAllowStartIfComplete()}. + */ + public void testShouldAllowStartIfComplete() { + assertEquals(false, configuration.isAllowStartIfComplete()); + configuration.setAllowStartIfComplete(true); + assertEquals(true, configuration.isAllowStartIfComplete()); + } + +} diff --git a/core/src/test/java/org/springframework/batch/core/domain/BatchStatusTests.java b/core/src/test/java/org/springframework/batch/core/domain/BatchStatusTests.java new file mode 100644 index 000000000..8cb29f4cb --- /dev/null +++ b/core/src/test/java/org/springframework/batch/core/domain/BatchStatusTests.java @@ -0,0 +1,46 @@ +/* + * 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.core.domain; + +import junit.framework.TestCase; + +/** + * @author Dave Syer + * + */ +public class BatchStatusTests extends TestCase { + + /** + * Test method for {@link org.springframework.batch.core.domain.BatchStatus#toString()}. + */ + public void testToString() { + assertEquals("FAILED", BatchStatus.FAILED.toString()); + } + + /** + * Test method for {@link org.springframework.batch.core.domain.BatchStatus#getStatus(java.lang.String)}. + */ + public void testGetStatus() { + assertEquals(BatchStatus.FAILED, BatchStatus.getStatus(BatchStatus.FAILED.toString())); + } + + /** + * Test method for {@link org.springframework.batch.core.domain.BatchStatus#getStatus(java.lang.String)}. + */ + public void testGetStatusWrongCode() { + assertEquals(null, BatchStatus.getStatus("foo")); + } +} diff --git a/core/src/test/java/org/springframework/batch/core/domain/EntityTests.java b/core/src/test/java/org/springframework/batch/core/domain/EntityTests.java new file mode 100644 index 000000000..9ecc84e2f --- /dev/null +++ b/core/src/test/java/org/springframework/batch/core/domain/EntityTests.java @@ -0,0 +1,88 @@ +/* + * 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.core.domain; + +import junit.framework.TestCase; + +/** + * @author Dave Syer + * + */ +public class EntityTests extends TestCase { + + Entity entity = new Entity(new Long(11)); + + /** + * Test method for {@link org.springframework.batch.core.domain.Entity#hashCode()}. + */ + public void testHashCode() { + assertEquals(entity.hashCode(), new Entity(entity.getId()).hashCode()); + } + + /** + * Test method for {@link org.springframework.batch.core.domain.Entity#hashCode()}. + */ + public void testHashCodeNullId() { + int withoutNull = entity.hashCode(); + entity.setId(null); + int withNull = entity.hashCode(); + assertTrue(withoutNull!=withNull); + } + + /** + * Test method for {@link org.springframework.batch.core.domain.Entity#getVersion()}. + */ + public void testGetVersion() { + assertEquals(null, entity.getVersion()); + } + + /** + * @throws Exception + */ + public void testToString() throws Exception { + Entity job = new Entity(); + assertTrue(job.toString().indexOf("id=null") >= 0); + } + + /** + * Test method for {@link org.springframework.batch.core.domain.Entity#equals(java.lang.Object)}. + */ + public void testEqualsEntity() { + assertEquals(entity, new Entity(entity.getId())); + } + + /** + * Test method for {@link org.springframework.batch.core.domain.Entity#equals(java.lang.Object)}. + */ + public void testEqualsEntityWrongId() { + assertFalse(entity.equals(new Entity())); + } + + /** + * Test method for {@link org.springframework.batch.core.domain.Entity#equals(java.lang.Object)}. + */ + public void testEqualsObject() { + assertFalse(entity.equals(new Object())); + } + + /** + * Test method for {@link org.springframework.batch.core.domain.Entity#equals(java.lang.Object)}. + */ + public void testEqualsNull() { + assertFalse(entity.equals(null)); + } + +} diff --git a/core/src/test/java/org/springframework/batch/core/domain/JobExecutionTests.java b/core/src/test/java/org/springframework/batch/core/domain/JobExecutionTests.java new file mode 100644 index 000000000..8d42d6cfc --- /dev/null +++ b/core/src/test/java/org/springframework/batch/core/domain/JobExecutionTests.java @@ -0,0 +1,82 @@ +/* + * 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.core.domain; + +import java.sql.Timestamp; + +import junit.framework.TestCase; + +/** + * @author Dave Syer + * + */ +public class JobExecutionTests extends TestCase { + + private JobExecution execution = new JobExecution(new Long(11)); + + /** + * Test method for {@link org.springframework.batch.core.domain.JobExecution#JobExecution()}. + */ + public void testJobExecution() { + assertNull(new JobExecution().getId()); + } + + /** + * Test method for {@link org.springframework.batch.core.domain.JobExecution#getEndTime()}. + */ + public void testGetEndTime() { + assertNull(execution.getEndTime()); + execution.setEndTime(new Timestamp(100L)); + assertEquals(100L, execution.getEndTime().getTime()); + } + + /** + * Test method for {@link org.springframework.batch.core.domain.JobExecution#getStartTime()}. + */ + public void testGetStartTime() { + assertNotNull(execution.getStartTime()); + execution.setStartTime(new Timestamp(0L)); + assertEquals(0L, execution.getStartTime().getTime()); + } + + /** + * Test method for {@link org.springframework.batch.core.domain.JobExecution#getStatus()}. + */ + public void testGetStatus() { + assertEquals(BatchStatus.STARTING, execution.getStatus()); + execution.setStatus(BatchStatus.COMPLETED); + assertEquals(BatchStatus.COMPLETED, execution.getStatus()); + } + + /** + * Test method for {@link org.springframework.batch.core.domain.JobExecution#getJobId()}. + */ + public void testGetJobId() { + assertEquals(11, execution.getJobId().longValue()); + execution.setJobId(new Long(23)); + assertEquals(23, execution.getJobId().longValue()); + } + + /** + * Test method for {@link org.springframework.batch.core.domain.JobExecution#getExitCode()}. + */ + public void testGetExitCode() { + assertEquals(0, execution.getExitCode()); + execution.setExitCode(23); + assertEquals(23, execution.getExitCode()); + } + +} diff --git a/core/src/test/java/org/springframework/batch/core/domain/JobInstanceTests.java b/core/src/test/java/org/springframework/batch/core/domain/JobInstanceTests.java new file mode 100644 index 000000000..fc4c68070 --- /dev/null +++ b/core/src/test/java/org/springframework/batch/core/domain/JobInstanceTests.java @@ -0,0 +1,100 @@ +/* + * 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.core.domain; + +import java.util.Collections; + +import org.springframework.batch.core.runtime.JobIdentifier; + +import junit.framework.TestCase; + +/** + * @author dsyer + * + */ +public class JobInstanceTests extends TestCase { + + private JobInstance instance = new JobInstance(new Long(11)); + + /** + * Test method for {@link org.springframework.batch.core.domain.JobInstance#JobInstance()}. + */ + public void testJobInstance() { + assertNull(new JobInstance().getId()); + } + + /** + * Test method for {@link org.springframework.batch.core.domain.JobInstance#getStatus()}. + */ + public void testGetStatus() { + assertNull(instance.getStatus()); + instance.setStatus(BatchStatus.COMPLETED); + assertNotNull(instance.getStatus()); + } + + /** + * Test method for {@link org.springframework.batch.core.domain.JobInstance#getSteps()}. + */ + public void testGetSteps() { + assertEquals(0, instance.getSteps().size()); + instance.setSteps(Collections.singletonList(new StepInstance())); + assertEquals(1, instance.getSteps().size()); + } + + /** + * Test method for {@link org.springframework.batch.core.domain.JobInstance#addStep(org.springframework.batch.core.domain.StepInstance)}. + */ + public void testAddStep() { + instance.addStep(new StepInstance()); + assertEquals(1, instance.getSteps().size()); + } + + /** + * Test method for {@link org.springframework.batch.core.domain.JobInstance#getJobExecutionCount()}. + */ + public void testGetJobExecutionCount() { + assertEquals(0, instance.getJobExecutionCount()); + instance.setJobExecutionCount(22); + assertEquals(22, instance.getJobExecutionCount()); + } + + /** + * Test method for {@link org.springframework.batch.core.domain.JobInstance#getIdentifier()}. + */ + public void testGetIdentifier() { + assertEquals(null, instance.getIdentifier()); + instance.setIdentifier(new JobIdentifier() { + public String getName() { + return "foo"; + } + }); + assertEquals("foo", instance.getIdentifier().getName()); + } + + /** + * Test method for {@link org.springframework.batch.core.domain.JobInstance#getIdentifier()}. + */ + public void testGetName() { + assertEquals(null, instance.getName()); + instance.setIdentifier(new JobIdentifier() { + public String getName() { + return "foo"; + } + }); + assertEquals("foo", instance.getName()); + } + +} diff --git a/core/src/test/java/org/springframework/batch/core/domain/StepExecutionTests.java b/core/src/test/java/org/springframework/batch/core/domain/StepExecutionTests.java new file mode 100644 index 000000000..b9e8a5d9e --- /dev/null +++ b/core/src/test/java/org/springframework/batch/core/domain/StepExecutionTests.java @@ -0,0 +1,190 @@ +/* + * 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.core.domain; + +import java.sql.Timestamp; +import java.util.Properties; + +import junit.framework.TestCase; + +/** + * @author Dave Syer + * + */ +public class StepExecutionTests extends TestCase { + + private StepExecution execution = new StepExecution(new Long(11), new Long(23)); + + /** + * Test method for {@link org.springframework.batch.core.domain.JobExecution#JobExecution()}. + */ + public void testStepExecution() { + assertNull(new StepExecution().getId()); + } + + /** + * Test method for {@link org.springframework.batch.core.domain.JobExecution#getEndTime()}. + */ + public void testGetEndTime() { + assertNull(execution.getEndTime()); + execution.setEndTime(new Timestamp(0L)); + assertEquals(0L, execution.getEndTime().getTime()); + } + + /** + * Test method for {@link org.springframework.batch.core.domain.JobExecution#getStartTime()}. + */ + public void testGetStartTime() { + assertNotNull(execution.getStartTime()); + execution.setStartTime(new Timestamp(10L)); + assertEquals(10L, execution.getStartTime().getTime()); + } + + /** + * Test method for {@link org.springframework.batch.core.domain.JobExecution#getStatus()}. + */ + public void testGetStatus() { + assertEquals(BatchStatus.STARTING, execution.getStatus()); + execution.setStatus(BatchStatus.COMPLETED); + assertEquals(BatchStatus.COMPLETED, execution.getStatus()); + } + + /** + * Test method for {@link org.springframework.batch.core.domain.JobExecution#getJobId()}. + */ + public void testGetJobId() { + assertEquals(23, execution.getJobExecutionId().longValue()); + } + + /** + * Test method for {@link org.springframework.batch.core.domain.JobExecution#getExitCode()}. + */ + public void testGetExitCode() { + assertEquals(0, execution.getExitCode()); + execution.setExitCode(23); + assertEquals(23, execution.getExitCode()); + } + + /** + * Test method for {@link org.springframework.batch.core.domain.StepExecution#incrementCommitCount()}. + */ + public void testIncrementCommitCount() { + int before = execution.getCommitCount().intValue(); + execution.incrementCommitCount(); + int after = execution.getCommitCount().intValue(); + assertEquals(before+1, after); + } + + /** + * Test method for {@link org.springframework.batch.core.domain.StepExecution#incrementTaskCount()}. + */ + public void testIncrementLuwCount() { + int before = execution.getTaskCount().intValue(); + execution.incrementTaskCount(); + int after = execution.getTaskCount().intValue(); + assertEquals(before+1, after); + } + + /** + * Test method for {@link org.springframework.batch.core.domain.StepExecution#incrementRollbackCount()}. + */ + public void testIncrementRollbackCount() { + int before = execution.getRollbackCount().intValue(); + execution.incrementRollbackCount(); + int after = execution.getRollbackCount().intValue(); + assertEquals(before+1, after); + } + + /** + * Test method for {@link org.springframework.batch.core.domain.StepExecution#getCommitCount()}. + */ + public void testGetCommitCount() { + execution.setCommitCount(123); + assertEquals(123, execution.getCommitCount().intValue()); + } + + /** + * Test method for {@link org.springframework.batch.core.domain.StepExecution#getTaskCount()}. + */ + public void testGetTaskCount() { + execution.setTaskCount(123); + assertEquals(123, execution.getTaskCount().intValue()); + } + + /** + * Test method for {@link org.springframework.batch.core.domain.StepExecution#getRollbackCount()}. + */ + public void testGetRollbackCount() { + execution.setRollbackCount(123); + assertEquals(123, execution.getRollbackCount().intValue()); + } + + /** + * Test method for {@link org.springframework.batch.core.domain.StepExecution#getStepId()}. + */ + public void testGetStepId() { + assertEquals(11, execution.getStepId().longValue()); + } + + public void testToString() throws Exception { + assertTrue("Should contain task count: "+execution.toString(), execution.toString().indexOf("task")>=0); + assertTrue("Should contain commit count: "+execution.toString(), execution.toString().indexOf("commit")>=0); + assertTrue("Should contain rollback count: "+execution.toString(), execution.toString().indexOf("rollback")>=0); + } + + public void testStatistics() throws Exception { + assertNotNull(execution.getStatistics()); + execution.setStatistics(new Properties() {{ + setProperty("foo", "bar"); + }}); + assertEquals("bar", execution.getStatistics().getProperty("foo")); + } + + public void testEqualsWithSameIdentifier() throws Exception { + StepExecution step1 = new StepExecution(new Long(100), new Long(11)); + StepExecution step2 = new StepExecution(new Long(100), new Long(11)); + assertEquals(step1, step2); + } + + public void testEqualsWithNull() throws Exception { + StepExecution step = new StepExecution(new Long(100), new Long(11)); + assertFalse(step.equals(null)); + } + + public void testEqualsWithNullIdentifiers() throws Exception { + StepExecution step = new StepExecution(new Long(100), new Long(11)); + assertFalse(step.equals(new StepExecution())); + } + + public void testEqualsWithNullJob() throws Exception { + StepExecution step = new StepExecution(null, new Long(11)); + assertFalse(step.equals(new StepExecution())); + } + + public void testEqualsWithNullStep() throws Exception { + StepExecution step = new StepExecution(new Long(11), null); + assertFalse(step.equals(new StepExecution())); + } + + public void testHashCode() throws Exception { + assertTrue("Hash code same as parent", new Entity(execution.getId()).hashCode()!=execution.hashCode()); + } + + public void testHashCodeWithNullIds() throws Exception { + assertTrue("Hash code not same as parent", new Entity(execution.getId()).hashCode()!=new StepExecution().hashCode()); + } +} + diff --git a/core/src/test/java/org/springframework/batch/core/domain/StepInstanceTests.java b/core/src/test/java/org/springframework/batch/core/domain/StepInstanceTests.java new file mode 100644 index 000000000..d4366ae56 --- /dev/null +++ b/core/src/test/java/org/springframework/batch/core/domain/StepInstanceTests.java @@ -0,0 +1,125 @@ +/* + * 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.core.domain; + +import java.util.Properties; + +import org.springframework.batch.restart.GenericRestartData; + +import junit.framework.TestCase; + +/** + * @author Dave Syer + * + */ +public class StepInstanceTests extends TestCase { + + StepInstance instance = new StepInstance(new Long(13)); + + /** + * Test method for {@link org.springframework.batch.core.domain.StepInstance#StepInstance()}. + */ + public void testStepInstance() { + assertNull(new StepInstance().getId()); + } + + /** + * Test method for {@link org.springframework.batch.core.domain.StepInstance#getStepExecutionCount()}. + */ + public void testGetStepExecutionCount() { + assertEquals(0, instance.getStepExecutionCount()); + instance.setStepExecutionCount(23); + assertEquals(23, instance.getStepExecutionCount()); + } + + /** + * Test method for {@link org.springframework.batch.core.domain.StepInstance#getRestartData()}. + */ + public void testGetRestartData() { + assertNotNull(instance.getRestartData()); + assertEquals(null, instance.getRestartData().getProperties()); + instance.setRestartData(new GenericRestartData(new Properties() {{ + setProperty("foo", "bar"); + }})); + assertEquals("bar", instance.getRestartData().getProperties().getProperty("foo")); + } + + /** + * Test method for {@link org.springframework.batch.core.domain.StepInstance#getStatus()}. + */ + public void testGetStatus() { + assertEquals(null, instance.getStatus()); + instance.setStatus(BatchStatus.COMPLETED); + assertEquals(BatchStatus.COMPLETED, instance.getStatus()); + } + + /** + * Test method for {@link org.springframework.batch.core.domain.StepInstance#getJob()}. + */ + public void testGetJob() { + assertEquals(null, instance.getJob()); + JobInstance job = new JobInstance(); + instance.setJob(job); + assertEquals(job, instance.getJob()); + } + + /** + * Test method for {@link org.springframework.batch.core.domain.StepInstance#getStepExecution()}. + */ + public void testGetStepExecution() { + assertEquals(null, instance.getStepExecution()); + StepExecution execution = new StepExecution(instance.getId(), new Long(111)); + instance.setStepExecution(execution); + assertNotNull(execution.getJobExecutionId()); + assertEquals(execution.getJobExecutionId(), instance.getStepExecution().getJobExecutionId()); + } + + /** + * Test method for {@link org.springframework.batch.core.domain.StepInstance#getName()}. + */ + public void testGetName() { + assertEquals(null, instance.getName()); + instance.setName("foo"); + assertEquals("foo", instance.getName()); + } + + /** + * Test method for {@link org.springframework.batch.core.domain.StepInstance#getJobId()}. + */ + public void testGetJobId() { + assertEquals(null, instance.getJobId()); + instance.setJob(new JobInstance(new Long(23))); + assertEquals(23, instance.getJobId().longValue()); + } + + public void testEqualsWithSameIdentifier() throws Exception { + JobInstance job = new JobInstance(new Long(100)); + StepInstance step1 = new StepInstance(new Long(0)); + StepInstance step2 = new StepInstance(new Long(0)); + step1.setJob(job); + step2.setJob(job); + String stepName = "foo"; + step1.setName(stepName); + step2.setName(stepName); + assertEquals(step1, step2); + } + + public void testToString() throws Exception { + assertTrue("Should contain name", instance.toString().indexOf("name=")>=0); + assertTrue("Should contain status", instance.toString().indexOf("status=")>=0); + } + +} diff --git a/core/src/test/java/org/springframework/batch/core/executor/StepInterruptedExceptionTests.java b/core/src/test/java/org/springframework/batch/core/executor/StepInterruptedExceptionTests.java new file mode 100644 index 000000000..b3492e200 --- /dev/null +++ b/core/src/test/java/org/springframework/batch/core/executor/StepInterruptedExceptionTests.java @@ -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.core.executor; + +import org.springframework.batch.core.AbstractExceptionTests; + +/** + * @author Dave Syer + * + */ +public class StepInterruptedExceptionTests extends AbstractExceptionTests { + + /* (non-Javadoc) + * @see org.springframework.batch.io.exception.AbstractExceptionTests#getException(java.lang.String) + */ + public Exception getException(String msg) throws Exception { + return new StepInterruptedException(msg); + } + + /* (non-Javadoc) + * @see org.springframework.batch.io.exception.AbstractExceptionTests#getException(java.lang.String, java.lang.Throwable) + */ + public Exception getException(String msg, Throwable t) throws Exception { + return new RuntimeException(msg, t); + } + +} diff --git a/core/src/test/java/org/springframework/batch/core/repository/BatchRestartExceptionTests.java b/core/src/test/java/org/springframework/batch/core/repository/BatchRestartExceptionTests.java new file mode 100644 index 000000000..bf309e29e --- /dev/null +++ b/core/src/test/java/org/springframework/batch/core/repository/BatchRestartExceptionTests.java @@ -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.core.repository; + +import org.springframework.batch.core.AbstractExceptionTests; + +/** + * @author Dave Syer + * + */ +public class BatchRestartExceptionTests extends AbstractExceptionTests { + + /* + * (non-Javadoc) + * @see org.springframework.batch.io.exception.AbstractExceptionTests#getException(java.lang.String) + */ + public Exception getException(String msg) throws Exception { + return new BatchRestartException(msg); + } + + /* + * (non-Javadoc) + * @see org.springframework.batch.io.exception.AbstractExceptionTests#getException(java.lang.String, + * java.lang.Throwable) + */ + public Exception getException(String msg, Throwable t) throws Exception { + return new BatchRestartException(msg, t); + } + +} diff --git a/core/src/test/java/org/springframework/batch/core/repository/NoSuchBatchDomainObjectExceptionTests.java b/core/src/test/java/org/springframework/batch/core/repository/NoSuchBatchDomainObjectExceptionTests.java new file mode 100644 index 000000000..2056dd818 --- /dev/null +++ b/core/src/test/java/org/springframework/batch/core/repository/NoSuchBatchDomainObjectExceptionTests.java @@ -0,0 +1,30 @@ +/* + * 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.core.repository; + +import junit.framework.TestCase; + +/** + * @author Dave Syer + * + */ +public class NoSuchBatchDomainObjectExceptionTests extends TestCase { + + public void testCreateException() throws Exception { + NoSuchBatchDomainObjectException e = new NoSuchBatchDomainObjectException("Foo"); + assertEquals("Foo", e.getMessage()); + } +} diff --git a/core/src/test/java/org/springframework/batch/core/runtime/JobExecutionContextTests.java b/core/src/test/java/org/springframework/batch/core/runtime/JobExecutionContextTests.java new file mode 100644 index 000000000..abade62f6 --- /dev/null +++ b/core/src/test/java/org/springframework/batch/core/runtime/JobExecutionContextTests.java @@ -0,0 +1,118 @@ +/* + * 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.core.runtime; + +import junit.framework.TestCase; + +import org.springframework.batch.core.domain.JobInstance; +import org.springframework.batch.core.domain.StepExecution; +import org.springframework.batch.repeat.context.RepeatContextSupport; + +/** + * @author Dave Syer + * + */ +public class JobExecutionContextTests extends TestCase { + + private JobExecutionContext context = createContext("foo", 11); + + public void testContextContainsInfo() throws Exception { + assertEquals("foo", context.getJobIdentifier().getName()); + } + + public void testNullContexts() throws Exception { + assertEquals(0, context.getStepContexts().size()); + assertEquals(0, context.getChunkContexts().size()); + } + + public void testStepContext() throws Exception { + context.registerStepContext(new RepeatContextSupport(null)); + assertEquals(1, context.getStepContexts().size()); + } + + public void testAddAndRemoveStepContext() throws Exception { + context.registerStepContext(new RepeatContextSupport(null)); + assertEquals(1, context.getStepContexts().size()); + context.unregisterStepContext(new RepeatContextSupport(null)); + assertEquals(0, context.getStepContexts().size()); + } + + public void testAddAndRemoveStepExecution() throws Exception { + assertEquals(0, context.getStepExecutions().size()); + context.registerStepExecution(new StepExecution(new Long(11), new Long(12))); + assertEquals(1, context.getStepExecutions().size()); + } + + public void testAddAndRemoveChunkContext() throws Exception { + context.registerChunkContext(new RepeatContextSupport(null)); + assertEquals(1, context.getChunkContexts().size()); + context.unregisterChunkContext(new RepeatContextSupport(null)); + assertEquals(0, context.getChunkContexts().size()); + } + + public void testRemoveChunkContext() throws Exception { + context.unregisterChunkContext(new RepeatContextSupport(null)); + assertEquals(0, context.getChunkContexts().size()); + } + + /** + * Test method for + * {@link org.springframework.batch.core.runtime.StepExecutionContext#equals(java.lang.Object)}. + */ + public void testEqualsObject() { + assertFalse(context.equals(new Object())); + } + + /** + * Test method for + * {@link org.springframework.batch.core.runtime.StepExecutionContext#equals(java.lang.Object)}. + */ + public void testEqualsNull() { + assertFalse(context.equals(null)); + } + + /** + * Test method for + * {@link org.springframework.batch.core.runtime.StepExecutionContext#equals(java.lang.Object)}. + */ + public void testEqualsContext() { + JobExecutionContext other = createContext("foo", 11); + assertFalse("Expect unequal before save", context.equals(other)); + } + + /** + * Test method for + * {@link org.springframework.batch.core.runtime.StepExecutionContext#toString()}. + */ + public void testToString() { + assertTrue("Identifier not contained in toString: " + context.toString(), context.toString().indexOf("identifier=") >= 0); + } + + /** + * Test method for + * {@link org.springframework.batch.core.runtime.StepExecutionContext#hashCode()}. + */ + public void testHashCode() { + assertNotNull(context.getJob().getId()); + assertNull(context.getJobExecution().getId()); + assertTrue("Expecting unequal hash codes before save", context.hashCode() != createContext("foo", 11) + .hashCode()); + } + + private JobExecutionContext createContext(String name, int jobId) { + return new JobExecutionContext(new SimpleJobIdentifier(name), new JobInstance(new Long(jobId))); + } +} diff --git a/core/src/test/java/org/springframework/batch/core/runtime/SimpleJobIdentifierTests.java b/core/src/test/java/org/springframework/batch/core/runtime/SimpleJobIdentifierTests.java new file mode 100644 index 000000000..1d540fa9c --- /dev/null +++ b/core/src/test/java/org/springframework/batch/core/runtime/SimpleJobIdentifierTests.java @@ -0,0 +1,55 @@ +/* + * 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.core.runtime; + +import junit.framework.TestCase; + +/** + * @author Dave Syer + * + */ +public class SimpleJobIdentifierTests extends TestCase { + + private SimpleJobIdentifier identifier = new SimpleJobIdentifier("foo"); + + /** + * Test method for + * {@link org.springframework.batch.core.runtime.SimpleJobIdentifier#SimpleJobIdentifier()}. + */ + public void testSimpleJobIdentifier() { + assertNull(new SimpleJobIdentifier().getName()); + } + + /** + * Test method for + * {@link org.springframework.batch.core.runtime.SimpleJobIdentifier#getName()}. + */ + public void testGetName() { + assertEquals("foo", identifier.getName()); + identifier.setName("bar"); + assertEquals("bar", identifier.getName()); + } + + /** + * Test method for + * {@link org.springframework.batch.core.runtime.SimpleJobIdentifier#toString()}. + */ + public void testToString() { + assertTrue("SimpleJobIdentifier toString should contain name: " + identifier.toString(), identifier.toString() + .indexOf("name=") >= 0); + } + +} diff --git a/core/src/test/java/org/springframework/batch/core/runtime/StepExecutionContextTests.java b/core/src/test/java/org/springframework/batch/core/runtime/StepExecutionContextTests.java new file mode 100644 index 000000000..86d9c96fe --- /dev/null +++ b/core/src/test/java/org/springframework/batch/core/runtime/StepExecutionContextTests.java @@ -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.core.runtime; + +import junit.framework.TestCase; + +import org.springframework.batch.core.domain.JobInstance; +import org.springframework.batch.core.domain.StepInstance; + +/** + * @author Dave Syer + * + */ +public class StepExecutionContextTests extends TestCase { + + private StepExecutionContext context = createContext("foo", 11, 12); + + /** + * Test method for + * {@link org.springframework.batch.core.runtime.StepExecutionContext#hashCode()}. + */ + public void testHashCode() { + assertNotNull(context.getStep().getId()); + assertNull(context.getStepExecution().getId()); + assertTrue("Expecting unequal hash codes before save", context.hashCode() != createContext("foo", 11, 12) + .hashCode()); + } + + /** + * Test method for + * {@link org.springframework.batch.core.runtime.StepExecutionContext#getStep()}. + */ + public void testGetStep() { + assertNotNull(context.getStep()); + assertEquals(12, context.getStep().getId().longValue()); + } + + /** + * Test method for + * {@link org.springframework.batch.core.runtime.StepExecutionContext#getJobExecutionContext()}. + */ + public void testGetJobExecutionContext() { + assertNotNull(context.getJobExecutionContext()); + assertEquals(11, context.getJobExecutionContext().getJob().getId().longValue()); + } + + /** + * Test method for + * {@link org.springframework.batch.core.runtime.StepExecutionContext#getStepExecution()}. + */ + public void testGetStepExecution() { + assertNotNull(context.getStepExecution()); + assertEquals(null, context.getStepExecution().getId()); + } + + /** + * Test method for + * {@link org.springframework.batch.core.runtime.StepExecutionContext#equals(java.lang.Object)}. + */ + public void testEqualsObject() { + assertFalse(context.equals(new Object())); + } + + /** + * Test method for + * {@link org.springframework.batch.core.runtime.StepExecutionContext#equals(java.lang.Object)}. + */ + public void testEqualsNull() { + assertFalse(context.equals(null)); + } + + /** + * Test method for + * {@link org.springframework.batch.core.runtime.StepExecutionContext#equals(java.lang.Object)}. + */ + public void testEqualsContext() { + StepExecutionContext other = createContext("foo", 11, 12); + assertTrue(context.equals(other)); + } + + /** + * Test method for + * {@link org.springframework.batch.core.runtime.StepExecutionContext#toString()}. + */ + public void testToString() { + assertTrue("Step not contained in toString: " + context.toString(), context.toString().indexOf("step=") >= 0); + } + + /** + * @param name + * @param jobId + * @param stepId + * @return + */ + private StepExecutionContext createContext(String name, int jobId, int stepId) { + JobInstance job = new JobInstance(new Long(jobId)); + return new StepExecutionContext(new JobExecutionContext(new SimpleJobIdentifier(name), job), new StepInstance( + new Long(stepId))); + } +} diff --git a/core/src/test/resources/clover.license b/core/src/test/resources/clover.license new file mode 100644 index 000000000..36f3a294e --- /dev/null +++ b/core/src/test/resources/clover.license @@ -0,0 +1,172 @@ +Product: Clover +License: Open Source License, 0.x, 1.x +Issued: Thu Mar 15 2007 12:55:00 CDT +Expiry: Never +Maintenance Expiry: Never +Key: d24b469cbe33bb71017d39a9d +Name: Andy Colyer +Org: Spring Portfolio +Certificate: AAACUG+Ow8B7/zEbxOMqqKwwrdpP+a1COmJGHco7sCNLjHkHnajPF+dQW +Ct12PMy0uml0s9xuus5wKngJ9OFk5/FZgYzdyIG5/rxEgRevOoLO7uYipoJrkt4TPBwIm4 +hxEw+b9xUNP0x1tTqSsgUP6fqSYilMajaHYGuRD9iV3LeP7hwWulpXY3hz3W5WjsKYp3Nf +fPyts/AffWHANGj5DHV+4yGm2IGIzIgHOGx9hISC7boFknmwM/GQ78RO1yzNnkSJ9dHPz2 +VdGTrKob36k3OVy7vwwCPpSm+01KDpkY4ZQN4ynqPIFzvJ07F1IBUvU49CGzSvX3v6qmOp +mT11CTGtP49xPafrKNjDDV8PxCsoesEBRaY4FJzquzWz0j6CkIQqidzCj3WDCtog3ct+za +SuuZ51n027sVFhbM69dZZzv8bYCgSHdQ3sG1a9DxM7+6JRfRcIBFgt/V78vK41MF4p9Mi1 +qmEPMLizpu7eBo1GDoQ8Lb3EhIWrfxDb4Db3NFc9hYpCKoreFlEw1A+eJlrLeomy43pVtk +SNPTDDoahNrXLeIu7SiRoHiemMrUjWvYtT9jwnVOsIjELa8n8cfW7gMzJdenCcNWl/T3Cr +8rSu3pVfz07AvX6+wQZWqzvGGnlwpnFXu1YJROxITYNINVNKXCAby33Mdbm51DPA3rEyk+ +dpS31tU2XrR4iY1Zypja1M0voOkzL74pf9ExgUGeJqyvi5LWTn3b4kGGT/bkwhbbDn6sAA +zlxKRvxsbYOBUzk3UZ448Heg2HwCjwarCh/C0QIcX8vWnUjqssdvxT7Jlr1rZwqK1LKqbH +l7YvP9Ee7SoHfoHrW770yK23u2IdDK44Sf6G3NBE0Muq7W1bcZwrZ1/ZRk8vE2kt0F0fXI +wz7Thjs5lXvcZDJO4nEtXpmSdCaDjUXBpjvsZE2ZjPa2Q1tv3KFhHWqdfNRant7FyeWYg= += +License Agreement: CLOVER VERSION 1 (ONE) SOFTWARE LICENSE AGREEMENT + +1. Licenses and Software + +Cenqua Pty Ltd, an Australian Proprietary Limited Company ("CENQUA") +hereby grants to the purchaser (the "LICENSEE") a limited, revocable, +worldwide, non-exclusive, nontransferable, non-sublicensable license +to use the Clover version 1 (one) software (the "Software"), +including any minor upgrades thereof during the Term (hereinafter +defined) up to, but not including the next major version of the +Software. The licensee shall not, or knowingly allow others to, +reverse engineer, decompile, disassemble, modify, adapt, create +derivative works from or otherwise attempt to derive source code from +the Software provided. And, in accordance with the terms and +conditions of this Software License Agreement (the "Agreement"), the +Software shall be used solely by the licensed users in accordance +with the following edition specific conditions: + +a) Server Edition + +A Server Edition license entitles the Licensee to execute one +instance of Clover Server Edition on one (1) machine for the purposes +of instrumententing source code and generating reports. There are no +limitations on the use of the instrumented source code or generated +reports produced by Server Edition. + +b) Workstation Edition + +A Workstation Edition license entitles the licensee to use Clover +Workstation Edition on one (1) machine by one (1) individual end +user. Workstation Edition does not permit the generation of reports +for distribution. + +c) Team Edition + +A Team Edition license entitles the licensee to use Clover Team +edition on any number of machines solely by the licensed number of +users. Reports generated by Clover Team Edition are strictly for use +only by the licensed number of individual end users. + +2. License Fee + +In exchange for the License(s), the Licensee shall pay to CENQUA a +one-time, up front, non-refundable license fee. At the sole +discretion of CENQUA this fee will be waived for non-commercial +projects. Notwithstanding the Licensee's payment of the License Fee, +CENQUA reserves the right to terminate the License if CENQUA +discovers that the Licensee and/or the Licensee's use of the Software +is in breach of this Agreement. + +3. Proprietary Rights + +CENQUA will retain all right, title and interest in and to the +Software, all copies thereof, and CENQUA website(s), software, and +other intellectual property, including, but not limited to, ownership +of all copyrights, look and feel, trademark rights, design rights, +trade secret rights and any and all other intellectual property and +other proprietary rights therein. The Licensee will not directly or +indirectly obtain or attempt to obtain at any time, any right, title +or interest by registration or otherwise in or to the trademarks, + +service marks, copyrights, trade names, symbols, logos or +designations or other intellectual property rights owned or used by +CENQUA. All technical manuals or other information provided by CENQUA +to the Licensee shall be the sole property of CENQUA. + +4. Term and Termination + +Subject to the other provisions hereof, this Agreement shall commence +upon the Licensee's opting into this Agreement and continue until the +Licensee discontinues use of the Software or the Agreement terminates +automatically upon the Licensee's breach of any term or condition of +this Agreement (the "Term"). Upon any such termination, the Licensee +will delete the Software immediately. + +5. Copying & Transfer + +The Licensee may copy the Software for back-up purposes only. The + +Licensee may not assign or otherwise transfer the Software to any +third party. + +6. Specific Disclaimer of Warranty and Limitation of Liability + +THE SOFTWARE IS PROVIDED WITHOUT WARRANTY OF ANY KIND. CENQUA +DISCLAIMS ALL WARRANTIES, EXPRESSED OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE. CENQUA WILL NOT BE LIABLE FOR ANY DAMAGES +ASSOCIATED WITH THE SOFTWARE, INCLUDING, WITHOUT LIMITATION, +ORDINARY, INCIDENTAL, INDIRECT, OR CONSEQUENTIAL DAMAGES OF ANY KIND, +INCLUDING BUT NOT LIMITED TO DAMAGES RELATING TO LOST DATA OR LOST +PROFITS, EVEN IF CENQUA HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + +7. Warranties and Representations + +Licensee Indemnification. CENQUA agrees to indemnify, defend and hold +the Licensee harmless from and against any and all liabilities, +damages, losses, claims, costs, and expenses (including reasonable +legal fees) arising out of or resulting from the Software or the use +thereof infringing upon, misappropriating or violating any patents, +copyrights, trademarks, or trade secret rights or other proprietary +rights of persons, firms or entities who are not parties to this +Agreement. + +CENQUA Indemnification. The Licensee warrants and represents that the +Licensee's actions with regard to the Software will be in compliance +with all applicable laws; and the Licensee agrees to indemnify, +defend, and hold CENQUA harmless from and against any and all +liabilities, damages, losses, claims, costs, and expenses (including +reasonable legal fees) arising out of or resulting from the +Licensee's failure to observe the use restrictions set forth herein. + +8. Publicity + +The Licensee grants permission for CENQUA to use Licensee's name +solely in customer lists. CENQUA shall not, without prior consent in +writing, use the Licensee's name, or that of its affiliates, in any +form with the specific exception of customer lists. CENQUA agrees to +remove Licensee's name from any and all materials within 7 days if +notified by the Licensee in writing. + +9. Governing Law + +This Agreement shall be governed by the laws of New South Wales, +Australia. + +10.Independent Contractors + +The parties are independent contractors with respect to each other, +and nothing in this Agreement shall be construed as creating an +employer-employee relationship, a partnership, agency relationship or +a joint venture between the parties. + +11. Assignment + +This Agreement is not assignable or transferable by the Licensee. +CENQUA in its sole discretion may transfer a license to a third party +at the written request of the Licensee. + +12. Entire Agreement + +This Agreement constitutes the entire agreement between the parties +concerning the Licensee's use of the Software. This Agreement +supersedes any prior verbal understanding between the parties and any +Licensee purchase order or other ordering document, regardless of +whether such document is received by CENQUA before or after execution +of this Agreement. This Agreement may be amended only in writing by +CENQUA. diff --git a/core/src/test/resources/log4j.properties b/core/src/test/resources/log4j.properties new file mode 100644 index 000000000..6d5422d74 --- /dev/null +++ b/core/src/test/resources/log4j.properties @@ -0,0 +1,13 @@ +log4j.rootCategory=INFO, stdout + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - <%m>%n + +log4j.category.org.apache.activemq=ERROR +log4j.category.org.springframework.batch=DEBUG +log4j.category.org.springframework.transaction=INFO + +log4j.category.org.hibernate.SQL=DEBUG +# for debugging datasource initialization +# log4j.category.test.jdbc=DEBUG diff --git a/dictionary.txt b/dictionary.txt new file mode 100644 index 000000000..6cbf677cc --- /dev/null +++ b/dictionary.txt @@ -0,0 +1,49 @@ +pluggable +resynchronize +lifecycle +username +javadoc +refactoring +rollbacks +synchronization +programmatically +apache +rollback +callback +callbacks +throwable +interceptors +serializable +accessor +synchronized +registry +synchronizes +autostart +listable +rethrows +interceptor +transactional +continuable +proxy +dave +syer +versioned +unversionable +stateful +locatable +synchronizations +restartable +restartability +strategise +tokenized +initialize +uninitialized +aggregator +aggregators +lucas +throwables +hibernate +incrementer +hoc +unbind +tasklet diff --git a/docs/.project b/docs/.project new file mode 100644 index 000000000..101216565 --- /dev/null +++ b/docs/.project @@ -0,0 +1,23 @@ + + + batch-docs + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.maven.ide.eclipse.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.maven.ide.eclipse.maven2Nature + + diff --git a/docs/.settings/org.eclipse.mylyn.tasks.ui.prefs b/docs/.settings/org.eclipse.mylyn.tasks.ui.prefs new file mode 100644 index 000000000..7110cefba --- /dev/null +++ b/docs/.settings/org.eclipse.mylyn.tasks.ui.prefs @@ -0,0 +1,4 @@ +#Sun Jul 08 09:33:19 BST 2007 +eclipse.preferences.version=1 +project.repository.kind=jira +project.repository.url=http\://opensource.atlassian.com/projects/spring diff --git a/docs/pom.xml b/docs/pom.xml new file mode 100644 index 000000000..63d988dce --- /dev/null +++ b/docs/pom.xml @@ -0,0 +1,127 @@ + + + 4.0.0 + spring-batch-docs + Documentation + pom + Spring Batch Documentation - reference guide and user manuals. + + + org.springframework.batch + spring-batch + 1.0-m2-SNAPSHOT + .. + + + + + agilejava + http://agilejava.com/maven + + + + + + + com.agilejava.docbkx + docbkx-maven-plugin + + + single-page + + generate-html + generate-pdf + + + false + ${basedir}/src/docbkx/resources/xsl/html.xsl + ${basedir}/src/docbkx/resources/xsl/fopdf.xsl + + + + + + + + + + + + + + + + + + + + + + + + + + + pre-site + + + multi-page + + generate-html + + + true + ${basedir}/src/docbkx/resources/xsl/html_chunk.xsl + + + + + + + + + + + + + + + + + + + + + + + + pre-site + + + + + org.docbook + docbook-xml + 4.4 + runtime + + + + index.xml + css/html.css + true + ${basedir}/src/site/docbook/reference + + + version + ${version} + + + + + + + + \ No newline at end of file diff --git a/docs/src/site/apt/index.apt b/docs/src/site/apt/index.apt new file mode 100644 index 000000000..1f7eba6ff --- /dev/null +++ b/docs/src/site/apt/index.apt @@ -0,0 +1,15 @@ + ----- + Spring Batch Docsumentation + ----- + Dave Syer + ----- + Aug 2007 + ----- + +Spring Batch Documentation + + If youare seeing this page you probably built Spring Batch from the + SVN soure code. The real documentation is still work in progress + and will be released here shortly. Meanwhile you can get the latest + and greatest at the Spring Batch home page + http://static.springframework.org/spring-batch/docs. diff --git a/docs/src/site/site.xml b/docs/src/site/site.xml new file mode 100644 index 000000000..9d69cae51 --- /dev/null +++ b/docs/src/site/site.xml @@ -0,0 +1,28 @@ + + + + Spring Batch: ${project.name} + + + images/shim.gif + + + + + + org.springframework.maven.skins + maven-spring-skin + 1.0.3 + + + + + + + + + + + ${reports} + + diff --git a/execution/.classpath b/execution/.classpath new file mode 100644 index 000000000..06072ef5f --- /dev/null +++ b/execution/.classpath @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/execution/.project b/execution/.project new file mode 100644 index 000000000..904b540ed --- /dev/null +++ b/execution/.project @@ -0,0 +1,29 @@ + + + batch-execution + Execution tools and implementations of Spring Batch Core interfaces + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.maven.ide.eclipse.maven2Builder + + + + + org.springframework.ide.eclipse.core.springbuilder + + + + + + org.springframework.ide.eclipse.core.springnature + org.eclipse.jdt.core.javanature + org.maven.ide.eclipse.maven2Nature + + diff --git a/execution/.settings/org.eclipse.jdt.core.prefs b/execution/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 000000000..b0e76afe9 --- /dev/null +++ b/execution/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,5 @@ +#Fri Aug 03 14:01:44 BST 2007 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.4 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.source=1.4 +org.eclipse.jdt.core.compiler.compliance=1.4 diff --git a/execution/.springBeans b/execution/.springBeans new file mode 100644 index 000000000..339b541b3 --- /dev/null +++ b/execution/.springBeans @@ -0,0 +1,44 @@ + + + + xml + + + src/test/resources/simple-container-definition.xml + src/test/resources/job-configuration.xml + src/test/resources/org/springframework/batch/execution/repository/dao/data-source-context.xml + src/test/resources/org/springframework/batch/execution/repository/dao/hibernate-context.xml + src/test/resources/org/springframework/batch/execution/repository/dao/hibernate-dao-test.xml + src/test/resources/org/springframework/batch/execution/repository/dao/sql-dao-test.xml + + + + + true + false + + src/test/resources/job-configuration.xml + src/test/resources/simple-container-definition.xml + + + + + true + false + + src/test/resources/org/springframework/batch/execution/repository/dao/data-source-context.xml + src/test/resources/org/springframework/batch/execution/repository/dao/sql-dao-test.xml + + + + + true + false + + src/test/resources/org/springframework/batch/execution/repository/dao/data-source-context.xml + src/test/resources/org/springframework/batch/execution/repository/dao/hibernate-context.xml + src/test/resources/org/springframework/batch/execution/repository/dao/hibernate-dao-test.xml + + + + diff --git a/execution/pom.xml b/execution/pom.xml new file mode 100644 index 000000000..1097a45ab --- /dev/null +++ b/execution/pom.xml @@ -0,0 +1,231 @@ + + + 4.0.0 + spring-batch-execution + jar + Execution + + + + + + + org.springframework.batch + spring-batch + 1.0-m2-SNAPSHOT + .. + + + + + org.springframework.batch + spring-batch-core + ${project.version} + + + hsqldb + hsqldb + 1.8.0.7 + test + + + commons-io + commons-io + 1.2 + test + + + easymock + easymock + 1.1 + test + + + junit + junit + 3.8.1 + test + + + + org.apache.geronimo.specs + geronimo-jta_1.1_spec + 1.1 + provided + + + + cglib + cglib-nodep + 2.1_3 + true + + + org.hibernate + hibernate + 3.2.3.ga + true + + + commons-logging + commons-logging + + + javax.transaction + jta + + + cglib + cglib + + + net.sf.ehcache + ehcache + + + asm + asm-attrs + + + asm + asm + + + + + + + + + org.apache.maven.plugins + maven-antrun-plugin + + + generate-sql + generate-sources + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + run + + + + + + org.apache.maven.plugins + maven-clover-plugin + + ${basedir}/src/test/resources/clover.license + + + + pre-site + + instrument + + + + + + + + + + + org.apache.maven.plugins + maven-clover-plugin + + + + + diff --git a/execution/src/main/java/org/springframework/batch/execution/JobExecutorFacade.java b/execution/src/main/java/org/springframework/batch/execution/JobExecutorFacade.java new file mode 100644 index 000000000..0da6c32e2 --- /dev/null +++ b/execution/src/main/java/org/springframework/batch/execution/JobExecutorFacade.java @@ -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.execution; + +import org.springframework.batch.core.configuration.NoSuchJobConfigurationException; +import org.springframework.batch.core.runtime.JobIdentifier; + +/** + * Interface which defines a facade for running jobs. The interface is + * intentionally minimal, and depends only on simple java types, so that the + * facade can be used to launch a job from basic environments like a command + * line or a JMX console. TODO: remove dependency on + * {@link JobIdentifier}? + * + * @author Lucas Ward + * @author Dave Syer + */ +public interface JobExecutorFacade { + + /** + * Start a job execution identifiable by the {@link JobIdentifier}. + * Implementations normally require a job configuration to be locatable + * corresponding to the {@link JobIdentifier}, preferably matching + * them at least by name. + * @param runtimeInformation + * + * @throws NoSuchJobConfigurationException + */ + void start(JobIdentifier runtimeInformation) throws NoSuchJobConfigurationException; + + /** + * Stop the job execution that was started with this runtime information. + * @param runtimeInformation the {@link JobIdentifier}. + * @throws NoSuchJobExecutionException if a job with this runtime + * information is not running + */ + void stop(JobIdentifier runtimeInformation) throws NoSuchJobExecutionException; + + /** + * Simple check for whether or not there are jobs in progress. Can be used + * by clients to wait for all jobs to finish. Finer grained monitoring and + * reporting can be implemented using the persistent execution details + * (normally in a database), provided they are maintained by the + * implementation. + * + * @return true if any jobs are active. + */ + boolean isRunning(); + +} diff --git a/execution/src/main/java/org/springframework/batch/execution/NoSuchJobExecutionException.java b/execution/src/main/java/org/springframework/batch/execution/NoSuchJobExecutionException.java new file mode 100644 index 000000000..7abcfe544 --- /dev/null +++ b/execution/src/main/java/org/springframework/batch/execution/NoSuchJobExecutionException.java @@ -0,0 +1,32 @@ +/* + * 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.execution; + +/** + * @author Dave Syer + * + */ +public class NoSuchJobExecutionException extends Exception { + + /** + * @param message + */ + public NoSuchJobExecutionException(String message) { + super(message); + } + + +} diff --git a/execution/src/main/java/org/springframework/batch/execution/bootstrap/AbstractJobLauncher.java b/execution/src/main/java/org/springframework/batch/execution/bootstrap/AbstractJobLauncher.java new file mode 100644 index 000000000..9b0918e4b --- /dev/null +++ b/execution/src/main/java/org/springframework/batch/execution/bootstrap/AbstractJobLauncher.java @@ -0,0 +1,294 @@ +/* + * 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.execution.bootstrap; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.batch.core.configuration.JobConfiguration; +import org.springframework.batch.core.configuration.NoSuchJobConfigurationException; +import org.springframework.batch.core.runtime.JobIdentifier; +import org.springframework.batch.core.runtime.JobIdentifierFactory; +import org.springframework.batch.execution.JobExecutorFacade; +import org.springframework.batch.execution.NoSuchJobExecutionException; +import org.springframework.batch.execution.runtime.ScheduledJobIdentifierFactory; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.util.Assert; + +/** + * Base class for {@link JobLauncher} implementations making no + * choices about concurrent processing of jobs. + * + * @see JobLauncher + * @author Lucas Ward + */ +public abstract class AbstractJobLauncher implements JobLauncher, + InitializingBean, ApplicationListener { + + private static final Log logger = LogFactory.getLog(AbstractJobLauncher.class); + + protected JobExecutorFacade batchContainer; + + private String jobConfigurationName; + + private final Object monitor = new Object(); + + // Do not autostart by default - allow user to set job configuration + // later and then manually start: + private volatile boolean autoStart = false; + + private JobIdentifierFactory jobRuntimeInformationFactory = new ScheduledJobIdentifierFactory(); + + // A private registry for keeping track of running jobs. + private volatile Map registry = new HashMap(); + + /** + * Setter for {@link JobIdentifier}. + * + * @param jobRuntimeInformationFactory the jobRuntimeInformationFactory to + * set + */ + public void setJobRuntimeInformationFactory(JobIdentifierFactory jobRuntimeInformationFactory) { + this.jobRuntimeInformationFactory = jobRuntimeInformationFactory; + } + + /** + * Setter for the {@link JobConfiguration} that this launcher will run. + * + * @param jobConfiguration the jobConfiguration to set + */ + public void setJobConfigurationName(String jobConfiguration) { + this.jobConfigurationName = jobConfiguration; + } + + /** + * Setter for autostart flag. If this is true then the container will be + * started when the Spring context is refreshed. Defaults to false. + * + * @param autoStart + */ + public void setAutoStart(boolean autoStart) { + this.autoStart = autoStart; + } + + /** + * Setter for {@link JobExecutorFacade}. Mandatory property. + * + * @param batchContainer + */ + public void setBatchContainer(JobExecutorFacade batchContainer) { + this.batchContainer = batchContainer; + } + + /** + * Check that mandatory properties are set. + * + * @see #setBatchContainer(JobExecutorFacade) + * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() + */ + public void afterPropertiesSet() throws Exception { + Assert.notNull(batchContainer); + } + + /** + * If autostart flag is on, initialise on context start-up. + * + * @see org.springframework.context.ApplicationListener#onApplicationEvent(org.springframework.context.ApplicationEvent) + */ + public void onApplicationEvent(ApplicationEvent event) { + if ((event instanceof ContextRefreshedEvent) && this.autoStart && !isRunning()) { + start(); + } + } + + /** + * Extension point for subclasses. Implementations might choose to start the + * job in a new thread or in the current thread.
+ * @param runtimeInformation the {@link JobIdentifier} to start the + * launcher with. + * @throws NoSuchJobConfigurationException + */ + protected abstract void doStart(JobIdentifier jobIdentifier) throws NoSuchJobConfigurationException; + + /** + * Start the provided container. The current thread will first be saved. + * This may seem odd at first, however, this simple bootstrap requires that + * only one thread can kick off a container, and that the first thread that + * calls start is the 'processing thread'. If the container has already been + * started, no exception will be thrown. + * @throws NoSuchJobConfigurationException if the container cannot locate a job configuration + * @throws IllegalStateException if JobConfiguration is null. + * @see Lifecycle#start(). + */ + public void start(JobIdentifier jobIdentifier) throws NoSuchJobConfigurationException { + + synchronized (monitor) { + if (isRunning(jobIdentifier)) { + return; + } + } + + register(jobIdentifier); + doStart(jobIdentifier); + + /* + * Subclasses have to take care of unregistering the runtimeInformation - + * if we do it here and doStart() is implemented to return immediately + * without waiting for the job to finish, then we will have a job + * running that is not in the registry. + */ + } + + /** + * Start a job execution with the given name. If a job is already running + * has no effect. + * + * @param name the name to assign to the job + * @throws NoSuchJobConfigurationException + */ + public void start(String name) throws NoSuchJobConfigurationException { + JobIdentifier runtimeInformation = jobRuntimeInformationFactory.getJobIdentifier(name); + this.start(runtimeInformation); + } + + /** + * Start a job execution with default name and other runtime information + * provided by the factory. If a job is already running has no effect. The + * default name is taken from the enclosed {@link JobConfiguration}. + * @throws NoSuchJobConfigurationException if the job configuration cannot be located + * + * @see #setJobRuntimeInformationFactory(JobIdentifierFactory) + * @see org.springframework.context.Lifecycle#start() + */ + public void start() { + if (jobConfigurationName==null) { + return; + } + try { + this.start(jobConfigurationName); + } + catch (NoSuchJobConfigurationException e) { + logger.error("Could not start", e); + } + } + + /** + * Extension point for subclasses to stop a specific job. + * @throws NoSuchJobExecutionException + * + * @see org.springframework.batch.container.bootstrap.BatchContainerLauncher#stop(JobRuntimeInformation)) + */ + protected abstract void doStop(JobIdentifier runtimeInformation) throws NoSuchJobExecutionException; + + /** + * Stop all jobs if any are running. If not, no action will be taken. + * Delegates to the {@link #doStop()} method. + * @throws NoSuchJobExecutionException + * @see org.springframework.context.Lifecycle#stop() + * @see org.springframework.batch.execution.bootstrap.JobLauncher#stop() + */ + final public void stop() { + for (Iterator iter = new HashSet(registry.keySet()).iterator(); iter.hasNext();) { + JobIdentifier context = (JobIdentifier) iter.next(); + try { + stop(context); + } + catch (NoSuchJobExecutionException e) { + logger.error(e); + } + } + } + + /** + * Stop a job with this {@link JobIdentifier}. Delegates to the + * {@link #doStop(JobIdentifier)} method. + * @throws NoSuchJobExecutionException + * + * @see org.springframework.batch.execution.bootstrap.JobLauncher#stop(org.springframework.batch.core.runtime.JobIdentifier) + * @see BatchContainer#stop(JobRuntimeInformation)) + */ + final public void stop(JobIdentifier runtimeInformation) throws NoSuchJobExecutionException { + synchronized (monitor) { + doStop(runtimeInformation); + } + } + + /** + * Stop all jobs with {@link JobIdentifier} having this name. + * Delegates to the {@link #stop(JobIdentifier)}. + * @throws NoSuchJobExecutionException + * + * @see org.springframework.batch.execution.bootstrap.JobLauncher#stop(java.lang.String) + */ + final public void stop(String name) throws NoSuchJobExecutionException { + this.stop(jobRuntimeInformationFactory.getJobIdentifier(name)); + } + + /* + * (non-Javadoc) + * @see org.springframework.batch.container.bootstrap.BatchContainerLauncher#isRunning() + */ + public boolean isRunning() { + Collection jobs = new HashSet(registry.keySet()); + for (Iterator iter = jobs.iterator(); iter.hasNext();) { + JobIdentifier context = (JobIdentifier) iter.next(); + if (!isRunning(context)) { + return false; + } + } + return !jobs.isEmpty(); + } + + protected boolean isRunning(JobIdentifier runtimeInformation) { + synchronized (registry) { + return registry.get(runtimeInformation) != null; + } + } + + /** + * Convenient synchronized accessor for the registry. Can be used by + * subclasses if necessary (but it isn't likely). + * @param runtimeInformation + */ + protected void register(JobIdentifier runtimeInformation) { + synchronized (registry) { + registry.put(runtimeInformation, runtimeInformation); + } + } + + /** + * Convenient synchronized accessor for the registry. Must be used by + * subclasses to release the {@link JobIdentifier} when a job is + * finished (or stopped). + * + * @param runtimeInformation + */ + protected void unregister(JobIdentifier runtimeInformation) { + synchronized (registry) { + registry.remove(runtimeInformation); + } + } + +} diff --git a/execution/src/main/java/org/springframework/batch/execution/bootstrap/BatchCommandLineLauncher.java b/execution/src/main/java/org/springframework/batch/execution/bootstrap/BatchCommandLineLauncher.java new file mode 100644 index 000000000..fed173b97 --- /dev/null +++ b/execution/src/main/java/org/springframework/batch/execution/bootstrap/BatchCommandLineLauncher.java @@ -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.execution.bootstrap; + +import org.springframework.batch.core.configuration.NoSuchJobConfigurationException; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.access.ContextSingletonBeanFactoryLocator; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +/** + * @author Dave Syer + * @since 2.1 + */ +public class BatchCommandLineLauncher { + + /** + * The key for the parent context. + */ + public static final String PARENT_KEY = "simple-container"; + + private ConfigurableApplicationContext parent; + + private JobLauncher launcher; + + /** + * Default constructor for the launcher. Sets up the parent context to use + * for all job executions using a context key {@link #PARENT_KEY}. + */ + public BatchCommandLineLauncher() { + parent = (ConfigurableApplicationContext) ContextSingletonBeanFactoryLocator.getInstance().useBeanFactory( + PARENT_KEY).getFactory(); + } + + /** + * Injection setter for the {@link JobLauncher}. + * + * @param launcher the launcher to set + */ + public void setLauncher(JobLauncher launcher) { + this.launcher = launcher; + } + + /** + * @param path the path to a Spring context configuration for this job + * @param jobName the name of the job execution to use + * @throws NoSuchJobConfigurationException + */ + private void start(String path, String jobName) throws NoSuchJobConfigurationException { + if (!path.endsWith(".xml")) { + path = path + ".xml"; + } + ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(new String[] { path }, parent); + context.getAutowireCapableBeanFactory().autowireBeanProperties(this, + AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, true); + try { + if (!launcher.isRunning()) { + if (jobName == null) { + launcher.start(); + } + else { + launcher.start(jobName); + } + } + } + finally { + try { + context.stop(); + } + finally { + context.close(); + } + } + } + + /** + * Launch a batch job using a {@link BatchCommandLineLauncher}. Creates a + * new Spring context for the job execution, and uses a common parent for + * all such contexts. + * + * @param args 0 - path to resource to load job configuration context + * (default "job-configuration.xml"); 1 - runtime name for job execution + * (default "job-execution-id"). + * @throws NoSuchJobConfigurationException + */ + public static void main(String[] args) throws NoSuchJobConfigurationException { + String path = "job-configuration.xml"; + String name = null; + if (args.length > 0) { + path = args[0]; + } + if (args.length > 1) { + name = args[1]; + } + BatchCommandLineLauncher command = new BatchCommandLineLauncher(); + command.start(path, name); + } + +} diff --git a/execution/src/main/java/org/springframework/batch/execution/bootstrap/BatchExecutionRequestEvent.java b/execution/src/main/java/org/springframework/batch/execution/bootstrap/BatchExecutionRequestEvent.java new file mode 100644 index 000000000..3e547cf20 --- /dev/null +++ b/execution/src/main/java/org/springframework/batch/execution/bootstrap/BatchExecutionRequestEvent.java @@ -0,0 +1,46 @@ +/* + * 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.execution.bootstrap; + +import org.springframework.batch.execution.JobExecutorFacade; +import org.springframework.batch.repeat.interceptor.RepeatOperationsApplicationEvent; +import org.springframework.context.ApplicationEvent; + +/** + * {@link ApplicationEvent} that encodes a request from the execution layer to a + * running {@link JobExecutorFacade}. + * + * @author Dave Syer + * + */ +public class BatchExecutionRequestEvent extends ApplicationEvent { + + /** + * Constructor for {@link BatchExecutionRequestEvent}. The source is the + * execution layer service implementation that is sending the signal.
+ * + * TODO: the source sould be Serializable so really it should be just a + * message about the request? + * + * Currently encodes a request to publish back a + * {@link RepeatOperationsApplicationEvent}. Could be extended in the + * future to narrow the request to ask for specific information to be + * published back. + */ + public BatchExecutionRequestEvent(Object source) { + super(source); + } +} diff --git a/execution/src/main/java/org/springframework/batch/execution/bootstrap/JobLauncher.java b/execution/src/main/java/org/springframework/batch/execution/bootstrap/JobLauncher.java new file mode 100644 index 000000000..639227754 --- /dev/null +++ b/execution/src/main/java/org/springframework/batch/execution/bootstrap/JobLauncher.java @@ -0,0 +1,91 @@ +/* + * 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.execution.bootstrap; + +import org.springframework.batch.core.configuration.NoSuchJobConfigurationException; +import org.springframework.batch.core.runtime.JobIdentifier; +import org.springframework.batch.execution.JobExecutorFacade; +import org.springframework.batch.execution.NoSuchJobExecutionException; +import org.springframework.context.Lifecycle; + +/** + * Simple interface for controlling a {@link JobExecutorFacade} for a single job + * configuration, and also possibly ad-hoc executions, based on different + * runtime information. Implementations should concentrate on launching and + * controlling a single job, as configured in a {@link JobExecutorFacade} instance. + * + * @author Dave Syer + * @since 2.1 + */ +public interface JobLauncher extends Lifecycle { + + /** + * Return whether or not a job execution is currently running. + */ + boolean isRunning(); + + /** + * Start a job execution with the given runtime information. + * @throws NoSuchJobConfigurationException + */ + void start(JobIdentifier runtimeInformation) throws NoSuchJobConfigurationException; + + /** + * Start a job execution with the given name and other runtime information + * generated on the fly. + * + * @param name the name to assign to the job + * @throws NoSuchJobConfigurationException + */ + void start(String name) throws NoSuchJobConfigurationException; + + /** + * Start a job execution with default name and other runtime information + * generated on the fly.
+ * + * Because {@link Lifecycle#start()} does not throw checked exceptions this + * also does not, so an error message and stack trace will be logged if the + * required job(s) cannot be started. + * + * @see org.springframework.context.Lifecycle#start() + */ + public void start(); + + /** + * Stop the job execution that was started with this runtime information. + * @param runtimeInformation the {@link JobIdentifier}. + * @throws NoSuchJobExecutionException + */ + void stop(JobIdentifier runtimeInformation) throws NoSuchJobExecutionException; + + /** + * Stop all currently executing jobs matching the given name. All jobs + * started with {@link JobIdentifier} having this name will be + * stopped. + * @throws NoSuchJobExecutionException + */ + void stop(String name) throws NoSuchJobExecutionException; + + /** + * Stop the current job executions if there are any. If not, no action will + * be taken. + * @throws NoSuchJobExecutionException + * + * @see org.springframework.context.Lifecycle#stop() + */ + public void stop(); +} diff --git a/execution/src/main/java/org/springframework/batch/execution/bootstrap/SimpleJobLauncher.java b/execution/src/main/java/org/springframework/batch/execution/bootstrap/SimpleJobLauncher.java new file mode 100644 index 000000000..3943967d1 --- /dev/null +++ b/execution/src/main/java/org/springframework/batch/execution/bootstrap/SimpleJobLauncher.java @@ -0,0 +1,110 @@ +/* + * 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.execution.bootstrap; + +import org.springframework.batch.core.configuration.NoSuchJobConfigurationException; +import org.springframework.batch.core.runtime.JobIdentifier; +import org.springframework.batch.execution.JobExecutorFacade; +import org.springframework.context.Lifecycle; + +/** + * Simple bootstrapping mechanism for running a single job execution in a + * {@link JobExecutorFacade}. + * + *

+ * This simple implementation does not run the job asynchronously, so the start + * method will not return before the job ends. However, the job execution to be + * interrupted via the stop method in another thread. + *

+ * + * @see Lifecycle + * @author Lucas Ward + * @author Dave Syer + * @since 2.1 + */ +public class SimpleJobLauncher extends AbstractJobLauncher { + + private volatile Thread processingThread; + + private volatile boolean running = false; + + /** + * Return whether or not the container is currently running. This is done by + * checking the thread to see if it is still alive. + */ + public boolean isRunning() { + return running && processingThread != null && processingThread.isAlive(); + } + + /** + * Start the provided container. The current thread will first be saved. + * This may seem odd at first, however, this simple bootstrap requires that + * only one thread can kick off a container, and that the first thread that + * calls start is the 'processing thread'. If the container has already been + * started, no exception will be thrown. + * @throws NoSuchJobConfigurationException + * @see Lifecycle#start(). + * + * @throws IllegalStateException if JobConfiguration is null. + */ + protected void doStart(JobIdentifier jobIdentifier) throws NoSuchJobConfigurationException { + + /* + * There is no reason to kick off a new thread, since only one thread + * should be processing at once. However, a handle to the thread should + * be maintained to allow for interrupt + */ + processingThread = Thread.currentThread(); + // TODO: push this out to a method call in parent inside synchronized + // block? + running = true; + try { + batchContainer.start(jobIdentifier); + } + finally { + running = false; + unregister(jobIdentifier); + } + + } + + /** + * Stop the job if it is running by interrupting its thread. If no job is + * running, no action will be taken. + * + * (non-Javadoc) + * @see org.springframework.context.Lifecycle#stop() + */ + protected void doStop() { + + if (isRunning()) { + processingThread.interrupt(); + running = false; + } + } + + /** + * Delegates to {@link #doStop()}. Since there is only one job running in + * this launcher this is OK. + * + * (non-Javadoc) + * @see org.springframework.context.Lifecycle#stop() + */ + protected void doStop(JobIdentifier runtimeInformation) { + doStop(); + } +} diff --git a/execution/src/main/java/org/springframework/batch/execution/bootstrap/TaskExecutorJobLauncher.java b/execution/src/main/java/org/springframework/batch/execution/bootstrap/TaskExecutorJobLauncher.java new file mode 100644 index 000000000..f8d59fd05 --- /dev/null +++ b/execution/src/main/java/org/springframework/batch/execution/bootstrap/TaskExecutorJobLauncher.java @@ -0,0 +1,184 @@ +/* + * 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.execution.bootstrap; + +import java.util.Properties; + +import javax.management.Notification; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.batch.core.configuration.NoSuchJobConfigurationException; +import org.springframework.batch.core.runtime.JobExecutionContext; +import org.springframework.batch.core.runtime.JobIdentifier; +import org.springframework.batch.execution.JobExecutorFacade; +import org.springframework.batch.execution.NoSuchJobExecutionException; +import org.springframework.batch.repeat.interceptor.RepeatOperationsApplicationEvent; +import org.springframework.batch.statistics.StatisticsProvider; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; +import org.springframework.context.ApplicationListener; +import org.springframework.core.task.SyncTaskExecutor; +import org.springframework.core.task.TaskExecutor; +import org.springframework.jmx.export.notification.NotificationPublisher; +import org.springframework.jmx.export.notification.NotificationPublisherAware; +import org.springframework.util.Assert; + +/** + * Bootstrapping mechanism for running job executions concurrently with a + * {@link JobExecutorFacade}. + * + *

+ * This implementation can run jobs asynchronously. Jobs are stopped by calling + * the container stop methods, which is a graceful shutdown. + *

+ * + * @see JobExecutorFacade + * @author Dave Syer + * @since 2.1 + */ +public class TaskExecutorJobLauncher extends AbstractJobLauncher implements ApplicationListener, + NotificationPublisherAware, ApplicationEventPublisherAware { + + private static final Log logger = LogFactory.getLog(TaskExecutorJobLauncher.class); + + private TaskExecutor taskExecutor = new SyncTaskExecutor(); + + private NotificationPublisher notificationPublisher; + + private int notificationCount = 0; + + private ApplicationEventPublisher applicationEventPublisher; + + /* + * (non-Javadoc) + * @see org.springframework.context.ApplicationEventPublisherAware#setApplicationEventPublisher(org.springframework.context.ApplicationEventPublisher) + */ + public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { + this.applicationEventPublisher = applicationEventPublisher; + } + + /** + * Setter for the {@link TaskExecutor}. Defaults to a + * {@link SyncTaskExecutor}. + * + * @param taskExecutor the taskExecutor to set + */ + public void setTaskExecutor(TaskExecutor taskExecutor) { + this.taskExecutor = taskExecutor; + } + + /* + * (non-Javadoc) + * @see org.springframework.jmx.export.notification.NotificationPublisherAware#setNotificationPublisher(org.springframework.jmx.export.notification.NotificationPublisher) + */ + public void setNotificationPublisher(NotificationPublisher notificationPublisher) { + this.notificationPublisher = notificationPublisher; + } + + /** + * Start the provided container using the task executor provided. + * + * @throws IllegalStateException if JobConfiguration is null. + */ + protected void doStart(final JobIdentifier runtimeInformation) { + + Assert.state(taskExecutor != null, "TaskExecutor must be provided"); + + taskExecutor.execute(new Runnable() { + public void run() { + try { + batchContainer.start(runtimeInformation); + } + catch (NoSuchJobConfigurationException e) { + applicationEventPublisher.publishEvent(new RepeatOperationsApplicationEvent(runtimeInformation, + "No such job", RepeatOperationsApplicationEvent.ERROR)); + logger.error("JobConfiguration could not be located inside Runnable for runtime information: [" + + runtimeInformation + "]", e); + } + finally { + unregister(runtimeInformation); + } + } + }); + } + + /** + * Delegates to the underlying {@link JobExecutorFacade}. Does not wait for + * the jobs to stop (probably therefore returns immediately). + * @throws NoSuchJobExecutionException + * + * @see org.springframework.context.Lifecycle#stop() + */ + protected void doStop(JobIdentifier runtimeInformation) throws NoSuchJobExecutionException { + batchContainer.stop(runtimeInformation); + // TODO: wait for the jobs to stop? + } + + /** + * If the event is a {@link RepeatOperationsApplicationEvent} for open and + * close we log the event at INFO level and send a JMX notification if we + * are also an MBean. + * + * @see org.springframework.batch.execution.bootstrap.AbstractJobLauncher#onApplicationEvent(org.springframework.context.ApplicationEvent) + */ + public void onApplicationEvent(ApplicationEvent applicationEvent) { + super.onApplicationEvent(applicationEvent); + if (applicationEvent instanceof RepeatOperationsApplicationEvent) { + RepeatOperationsApplicationEvent event = (RepeatOperationsApplicationEvent) applicationEvent; + int type = event.getType(); + if (type == RepeatOperationsApplicationEvent.OPEN || type == RepeatOperationsApplicationEvent.CLOSE + || type == RepeatOperationsApplicationEvent.ERROR) { + String message = event.getMessage() + "; source=" + event.getSource(); + logger.info(message); + publish(message); + } + return; + } + } + + /** + * Accessor for the job executions passed back in response to a call to + * {@link #requestContextNotification()}. Because the request is + * potentially fulfilled asynchronously, and only on demand, the data might + * be out of date by the time this method is called, so it should be used + * for information purposes only. + * + * @return Properties representing the last {@link JobExecutionContext} + * objects passed up from the underlying execution. If there are no jobs + * running it will be empty. + */ + public Properties getStatistics() { + if (batchContainer instanceof StatisticsProvider) { + return ((StatisticsProvider) batchContainer).getStatistics(); + } else { + return new Properties(); + } + } + + /** + * @param event + */ + private void publish(String message) { + if (notificationPublisher != null) { + notificationPublisher.sendNotification(new Notification("RepeatOperationsApplicationEvent", this, + notificationCount++, message)); + } + } + +} diff --git a/execution/src/main/java/org/springframework/batch/execution/bootstrap/package.html b/execution/src/main/java/org/springframework/batch/execution/bootstrap/package.html new file mode 100644 index 000000000..bf6cb3777 --- /dev/null +++ b/execution/src/main/java/org/springframework/batch/execution/bootstrap/package.html @@ -0,0 +1,7 @@ + + +

+Specific implementations of bootstrap concerns. +

+ + diff --git a/execution/src/main/java/org/springframework/batch/execution/configuration/JobConfigurationRegistryBeanPostProcessor.java b/execution/src/main/java/org/springframework/batch/execution/configuration/JobConfigurationRegistryBeanPostProcessor.java new file mode 100644 index 000000000..63fbae4fd --- /dev/null +++ b/execution/src/main/java/org/springframework/batch/execution/configuration/JobConfigurationRegistryBeanPostProcessor.java @@ -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.execution.configuration; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; + +import org.springframework.batch.core.configuration.DuplicateJobConfigurationException; +import org.springframework.batch.core.configuration.JobConfiguration; +import org.springframework.batch.core.configuration.JobConfigurationLocator; +import org.springframework.batch.core.configuration.JobConfigurationRegistry; +import org.springframework.beans.BeansException; +import org.springframework.beans.FatalBeanException; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.util.Assert; + +/** + * A {@link BeanPostProcessor} that registers {@link JobConfiguration} beans + * with a {@link JobConfigurationRegistry}. Include a bean of this type along + * with your job configuration, and use the same + * {@link JobConfigurationRegistry} as a {@link JobConfigurationLocator} when + * you need to locate a {@link JobConfigurationLocator} to launch. + * + * @author Dave Syer + * + */ +public class JobConfigurationRegistryBeanPostProcessor implements BeanPostProcessor, InitializingBean, DisposableBean { + + // It doesn't make sense for this to have a default value... + private JobConfigurationRegistry jobConfigurationRegistry = null; + + private Collection jobConfigurations = new HashSet(); + + /** + * Injection setter for {@link JobConfigurationRegistry}. + * + * @param jobConfigurationRegistry the jobConfigurationRegistry to set + */ + public void setJobConfigurationRegistry(JobConfigurationRegistry jobConfigurationRegistry) { + this.jobConfigurationRegistry = jobConfigurationRegistry; + } + + /** + * Make sure the registry is set before use. + * + * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() + */ + public void afterPropertiesSet() throws Exception { + Assert.notNull(jobConfigurationRegistry, "JobConfigurationRegistry must not be null"); + } + + /** + * De-register all the {@link JobConfiguration} instances that were + * regsistered by this post processor. + * @see org.springframework.beans.factory.DisposableBean#destroy() + */ + public void destroy() throws Exception { + for (Iterator iter = jobConfigurations.iterator(); iter.hasNext();) { + JobConfiguration jobConfiguration = (JobConfiguration) iter.next(); + jobConfigurationRegistry.unregister(jobConfiguration); + } + jobConfigurations.clear(); + } + + /** + * If the bean is an instance of {@link JobConfiguration} then register it. + * @throws FatalBeanException if there is a + * {@link DuplicateJobConfigurationException}. + * + * @see org.springframework.beans.factory.config.BeanPostProcessor#postProcessAfterInitialization(java.lang.Object, + * java.lang.String) + */ + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof JobConfiguration) { + JobConfiguration jobConfiguration = (JobConfiguration) bean; + try { + jobConfigurationRegistry.register(jobConfiguration); + jobConfigurations.add(jobConfiguration); + } + catch (DuplicateJobConfigurationException e) { + throw new FatalBeanException("Cannot register job configuration", e); + } + } + return bean; + } + + /** + * Do nothing. + * + * @see org.springframework.beans.factory.config.BeanPostProcessor#postProcessBeforeInitialization(java.lang.Object, + * java.lang.String) + */ + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + return bean; + } + +} diff --git a/execution/src/main/java/org/springframework/batch/execution/configuration/MapJobConfigurationRegistry.java b/execution/src/main/java/org/springframework/batch/execution/configuration/MapJobConfigurationRegistry.java new file mode 100644 index 000000000..858894905 --- /dev/null +++ b/execution/src/main/java/org/springframework/batch/execution/configuration/MapJobConfigurationRegistry.java @@ -0,0 +1,97 @@ +/* + * 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.execution.configuration; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; + +import org.springframework.batch.core.configuration.DuplicateJobConfigurationException; +import org.springframework.batch.core.configuration.JobConfiguration; +import org.springframework.batch.core.configuration.JobConfigurationRegistry; +import org.springframework.batch.core.configuration.ListableJobConfigurationRegistry; +import org.springframework.batch.core.configuration.NoSuchJobConfigurationException; +import org.springframework.util.Assert; + +/** + * Simple map-based implementation of {@link JobConfigurationRegistry}. Access + * to the map is synchronized, guarded by an internal lock. + * + * @author Dave Syer + * + */ +public class MapJobConfigurationRegistry implements ListableJobConfigurationRegistry { + + private Map map = new HashMap(); + + /* + * (non-Javadoc) + * @see org.springframework.batch.container.common.configuration.JobConfigurationRegistry#registerJobConfiguration(org.springframework.batch.container.common.configuration.JobConfiguration) + */ + public void register(JobConfiguration jobConfiguration) throws DuplicateJobConfigurationException { + Assert.notNull(jobConfiguration); + String name = jobConfiguration.getName(); + Assert.notNull(name, "Job configuration must have a name."); + synchronized (map) { + if (map.containsKey(name) && jobConfiguration.equals(map.get(name))) { + throw new DuplicateJobConfigurationException("A job configuration with this name [" + name + + "] was already registered"); + } + // allow replacing job configuration with new instance + map.put(name, jobConfiguration); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.batch.container.common.configuration.JobConfigurationRegistry#unregister(org.springframework.batch.container.common.configuration.JobConfiguration) + */ + public void unregister(JobConfiguration jobConfiguration) { + String name = jobConfiguration.getName(); + Assert.notNull(name, "Job configuration must have a name."); + synchronized (map) { + map.remove(name); + } + + } + + /* + * (non-Javadoc) + * @see org.springframework.batch.container.common.configuration.JobConfigurationLocator#getJobConfiguration(java.lang.String) + */ + public JobConfiguration getJobConfiguration(String name) throws NoSuchJobConfigurationException { + synchronized (map) { + if (!map.containsKey(name)) { + throw new NoSuchJobConfigurationException("No job configuration with the name [" + name + + "] was registered"); + } + return (JobConfiguration) map.get(name); + } + } + + + /* (non-Javadoc) + * @see org.springframework.batch.container.common.configuration.ListableJobConfigurationRegistry#getJobConfigurations() + */ + public Collection getJobConfigurations() { + synchronized (map) { + return Collections.unmodifiableCollection(new HashSet(map.keySet())); + } + } + +} diff --git a/execution/src/main/java/org/springframework/batch/execution/configuration/package.html b/execution/src/main/java/org/springframework/batch/execution/configuration/package.html new file mode 100644 index 000000000..f0c19561d --- /dev/null +++ b/execution/src/main/java/org/springframework/batch/execution/configuration/package.html @@ -0,0 +1,7 @@ + + +

+Specific implementations of configuration concerns. +

+ + diff --git a/execution/src/main/java/org/springframework/batch/execution/facade/BatchResourceFactoryBean.java b/execution/src/main/java/org/springframework/batch/execution/facade/BatchResourceFactoryBean.java new file mode 100644 index 000000000..8625c270a --- /dev/null +++ b/execution/src/main/java/org/springframework/batch/execution/facade/BatchResourceFactoryBean.java @@ -0,0 +1,176 @@ +/* + * 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.execution.facade; + +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.config.AbstractFactoryBean; +import org.springframework.context.ResourceLoaderAware; +import org.springframework.core.io.FileSystemResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * *******This class is currently undergoing heavy refactoring***************** + * + * Strategy for locating different resources on the file system. For each unique + * step, the same file handle will be returned. A unique step is defined as + * having the same job name, job run, schedule date, stream name, and step name. + * An external file mover (such as an EAI solution) should rename and move any + * input files to conform to the patter defined by the file pattern.
+ * + * If no pattern is passed in, then following default is used: + * + *
+ * %BATCH_ROOT%/job_data/%JOB_NAME%/%SCHEDULE_DATE%-%STREAM_NAME%-%STEP_NAME%.txt
+ * 
+ * + * The %% variables are replaced with the corresponding bean property at run + * time, when the factory method is executed. + * + * @author Tomas Slanina + * @author Lucas Ward + * @author Dave Syer + * + * @see FactoryBean + */ +public class BatchResourceFactoryBean extends AbstractFactoryBean implements ResourceLoaderAware { + + private static final String BATCH_ROOT_PATTERN = "%BATCH_ROOT%"; + + private static final String JOB_NAME_PATTERN = "%JOB_NAME%"; + + private static final String JOB_RUN_PATTERN = "%JOB_RUN%"; + + private static final String STEP_NAME_PATTERN = "%STEP_NAME%"; + + private static final String STREAM_PATTERN = "%STREAM_NAME%"; + + private static final String SCHEDULE_DATE_PATTERN = "%SCHEDULE_DATE%"; + + private static final String DEFAULT_PATTERN = "%BATCH_ROOT%/job_data/%JOB_NAME%/" + + "%SCHEDULE_DATE%-%STREAM_NAME%-%STEP_NAME%.txt"; + + private String filePattern = DEFAULT_PATTERN; + + private String jobName = ""; + + private String jobStream = ""; + + private int jobRun = 0; + + private String scheduleDate = ""; + + private String rootDirectory = ""; + + private String stepName = ""; + + private ResourceLoader resourceLoader; + + /* + * (non-Javadoc) + * @see org.springframework.context.ResourceLoaderAware#setResourceLoader(org.springframework.core.io.ResourceLoader) + */ + public void setResourceLoader(ResourceLoader resourceLoader) { + this.resourceLoader = resourceLoader; + } + + /** + * Returns the Resource representing the file defined by the file pattern. + * + * @see FactoryBean#getObject() + * @return a resource representing the file on the file system. + */ + protected Object createInstance() { + + if (resourceLoader == null) { + resourceLoader = new FileSystemResourceLoader(); + } + + return resourceLoader.getResource(createFileName()); + } + + public Class getObjectType() { + return Resource.class; + } + + /** + * helper method for createFileName() + */ + private String replacePattern(String string, String pattern, String replacement) { + + // check to ensure pattern exists in string. + if (string.indexOf(pattern) != -1) { + return StringUtils.replace(string, pattern, replacement); + } + + return string; + } + + /** + * Creates a filename given a pattern and step context information. + * + * Deliberate package access, so that the method can be accessed by unit + * tests + */ + private String createFileName() { + Assert.notNull(filePattern, "filename pattern is null"); + + String fileName = filePattern; + + // TODO consider refactoring to void replacePattern() method and + // collecting variable fileName + fileName = replacePattern(fileName, BATCH_ROOT_PATTERN, rootDirectory); + fileName = replacePattern(fileName, JOB_NAME_PATTERN, jobName); + fileName = replacePattern(fileName, STEP_NAME_PATTERN, stepName); + fileName = replacePattern(fileName, STREAM_PATTERN, jobStream); + fileName = replacePattern(fileName, JOB_RUN_PATTERN, String.valueOf(jobRun)); + fileName = replacePattern(fileName, SCHEDULE_DATE_PATTERN, scheduleDate); + + return fileName; + } + + public void setFilePattern(String filePattern) { + this.filePattern = filePattern; + } + + public void setRootDirectory(String rootDirectory) { + this.rootDirectory = rootDirectory; + } + + public void setStepName(String stepName) { + this.stepName = stepName; + } + + public void setJobName(String jobName) { + this.jobName = jobName; + } + + public void setJobRun(int jobRun) { + this.jobRun = jobRun; + } + + public void setJobStream(String jobStream) { + this.jobStream = jobStream; + } + + public void setScheduleDate(String scheduleDate) { + this.scheduleDate = scheduleDate; + } + +} diff --git a/execution/src/main/java/org/springframework/batch/execution/facade/SimpleJobExecutorFacade.java b/execution/src/main/java/org/springframework/batch/execution/facade/SimpleJobExecutorFacade.java new file mode 100644 index 000000000..70fd06c47 --- /dev/null +++ b/execution/src/main/java/org/springframework/batch/execution/facade/SimpleJobExecutorFacade.java @@ -0,0 +1,215 @@ +/* + * 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.execution.facade; + +import java.util.Iterator; +import java.util.Properties; + +import org.springframework.batch.core.configuration.JobConfiguration; +import org.springframework.batch.core.configuration.JobConfigurationLocator; +import org.springframework.batch.core.configuration.NoSuchJobConfigurationException; +import org.springframework.batch.core.domain.JobInstance; +import org.springframework.batch.core.executor.JobExecutor; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.runtime.JobExecutionContext; +import org.springframework.batch.core.runtime.JobExecutionRegistry; +import org.springframework.batch.core.runtime.JobIdentifier; +import org.springframework.batch.execution.JobExecutorFacade; +import org.springframework.batch.execution.NoSuchJobExecutionException; +import org.springframework.batch.execution.job.DefaultJobExecutor; +import org.springframework.batch.repeat.RepeatContext; +import org.springframework.batch.statistics.StatisticsProvider; +import org.springframework.util.Assert; + +/** + *

+ * Simple implementation of (@link {@link JobExecutorFacade}). + * + *

+ * A {@link JobIdentifier} will be used to uniquely identify the job by the + * repository. Once the job is obtained, the {@link JobExecutor} will be used to + * run the job. + *

+ * + * @author Lucas Ward + * @author Dave Syer + * + */ +public class SimpleJobExecutorFacade implements JobExecutorFacade, StatisticsProvider { + + private JobExecutor jobExecutor; + + private JobRepository jobRepository; + + private JobExecutionRegistry jobExecutionRegistry = new VolatileJobExecutionRegistry(); + + // there is no sensible default for this + private JobConfigurationLocator jobConfigurationLocator; + + private int running = 0; + + private Object mutex = new Object(); + + /** + * Public accessor for the running property. + * + * @return the running + */ + public boolean isRunning() { + synchronized (mutex) { + return running > 0; + } + } + + public SimpleJobExecutorFacade() { + jobExecutor = new DefaultJobExecutor(); + } + + /** + * Setter for the job execution registry. The default should be adequate so + * this setter method is mainly used for testing. + * @param jobExecutionRegistry the jobExecutionRegistry to set + */ + public void setJobExecutionRegistry(JobExecutionRegistry jobExecutionRegistry) { + this.jobExecutionRegistry = jobExecutionRegistry; + } + + /** + * Setter for injection of {@link JobConfigurationLocator}. + * + * @param jobConfigurationLocator the jobConfigurationLocator to set + */ + public void setJobConfigurationLocator(JobConfigurationLocator jobConfigurationLocator) { + this.jobConfigurationLocator = jobConfigurationLocator; + } + + /** + * Locates a {@link JobConfiguration} by using the name of the provided + * {@link JobIdentifier} and the {@link JobConfigurationLocator}. + * + * @see org.springframework.batch.execution.JobExecutorFacade#start(org.springframework.batch.execution.common.domain.JobConfiguration, + * org.springframework.batch.core.runtime.JobIdentifier) + * + * @throws IllegalArgumentException if the runtime information is null or + * its name is null + * @throws IllegalStateException if the {@link JobConfigurationLocator} does + * not contain a {@link JobConfiguration} with the name provided. + * @throws IllegalStateException if the {@link JobExecutor} is null + * @throws IllegalStateException if the {@link JobConfigurationLocator} is + * null + * + */ + public void start(JobIdentifier jobRuntimeInformation) throws NoSuchJobConfigurationException { + + Assert.notNull(jobRuntimeInformation, "JobRuntimeInformation must not be null."); + Assert.notNull(jobRuntimeInformation.getName(), "JobRuntimeInformation name must not be null."); + + Assert.state(!jobExecutionRegistry.isRegistered(jobRuntimeInformation), + "A job with this JobRuntimeInformation is already executing in this container"); + + Assert.state(jobExecutor != null, "JobExecutor must be provided."); + Assert.state(jobConfigurationLocator != null, "JobConfigurationLocator must be provided."); + + JobConfiguration jobConfiguration = jobConfigurationLocator + .getJobConfiguration(jobRuntimeInformation.getName()); + + final JobInstance job = jobRepository.findOrCreateJob(jobConfiguration, jobRuntimeInformation); + JobExecutionContext jobExecutionContext = jobExecutionRegistry.register(jobRuntimeInformation, job); + try { + synchronized (mutex) { + running++; + } + jobExecutor.run(jobConfiguration, jobExecutionContext); + } + finally { + synchronized (mutex) { + // assume execution is synchronous so when we get to here we are + // not running any more + running--; + } + jobExecutionRegistry.unregister(jobRuntimeInformation); + } + + } + + /* + * (non-Javadoc) + * @see org.springframework.batch.container.BatchContainer#stop(org.springframework.batch.container.common.runtime.JobRuntimeInformation) + */ + public void stop(JobIdentifier runtimeInformation) throws NoSuchJobExecutionException { + JobExecutionContext jobExecutionContext = (JobExecutionContext) jobExecutionRegistry.get(runtimeInformation); + if (jobExecutionContext == null) { + throw new NoSuchJobExecutionException("No such Job is executing: [" + runtimeInformation + "]"); + } + for (Iterator iter = jobExecutionContext.getStepContexts().iterator(); iter.hasNext();) { + RepeatContext context = (RepeatContext) iter.next(); + context.setTerminateOnly(); + } + ; + for (Iterator iter = jobExecutionContext.getChunkContexts().iterator(); iter.hasNext();) { + RepeatContext context = (RepeatContext) iter.next(); + context.setTerminateOnly(); + } + } + + /** + * Setter for {@link JobExecutor}. + * + * @param jobExecutor + */ + public void setJobExecutor(JobExecutor jobExecutor) { + this.jobExecutor = jobExecutor; + } + + /** + * Setter for {@link JobRepository}. + * + * @param jobRepository + */ + public void setJobRepository(JobRepository jobRepository) { + this.jobRepository = jobRepository; + } + + /** + * @return a read-only view of the state of the running jobs. + */ + public Properties getStatistics() { + int i = 0; + Properties props = new Properties(); + for (Iterator iter = jobExecutionRegistry.findAll().iterator(); iter.hasNext();) { + JobExecutionContext element = (JobExecutionContext) iter.next(); + i++; + String runtime = "job" + i; + props.setProperty(runtime, "" + element.getJobIdentifier()); + int j = 0; + for (Iterator iterator = element.getStepContexts().iterator(); iterator.hasNext();) { + RepeatContext context = (RepeatContext) iterator.next(); + j++; + props.setProperty(runtime + ".step" + j, "" + context); + + } + j = 0; + for (Iterator iterator = element.getChunkContexts().iterator(); iterator.hasNext();) { + RepeatContext context = (RepeatContext) iterator.next(); + j++; + props.setProperty(runtime + ".chunk" + j, "" + context); + + } + } + return props; + } +} diff --git a/execution/src/main/java/org/springframework/batch/execution/facade/VolatileJobExecutionRegistry.java b/execution/src/main/java/org/springframework/batch/execution/facade/VolatileJobExecutionRegistry.java new file mode 100644 index 000000000..1b43c6404 --- /dev/null +++ b/execution/src/main/java/org/springframework/batch/execution/facade/VolatileJobExecutionRegistry.java @@ -0,0 +1,126 @@ +/* + * 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.execution.facade; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +import org.springframework.batch.core.domain.JobInstance; +import org.springframework.batch.core.runtime.JobExecutionContext; +import org.springframework.batch.core.runtime.JobExecutionRegistry; +import org.springframework.batch.core.runtime.JobIdentifier; + +/** + * Simple in-memory implementation of {@link JobExecutionRegistry}. + * Synchronizes all access to the underlying storage. Good for most purposes. + * + * @author Dave Syer + * + */ +public class VolatileJobExecutionRegistry implements JobExecutionRegistry { + + private Map contexts = new HashMap(); + + /* + * (non-Javadoc) + * @see org.springframework.batch.container.common.executor.JobExecutionRegistry#findByName(java.lang.String) + */ + public Collection findByName(String name) { + Set values = new HashSet(); + HashMap contexts; + synchronized (this.contexts) { + contexts = new HashMap(this.contexts); + } + for (Iterator iter = contexts.entrySet().iterator(); iter.hasNext();) { + Map.Entry entry = (Map.Entry) iter.next(); + String runtimeName = ((JobIdentifier) entry.getKey()).getName(); + if ((name == null && runtimeName == null) || name.equals(runtimeName)) { + values.add(entry.getValue()); + } + } + return values; + } + + /* + * (non-Javadoc) + * @see org.springframework.batch.container.common.executor.JobExecutionRegistry#findAll() + */ + public Collection findAll() { + + synchronized (this.contexts) { + return new HashSet(contexts.values()); + } + + } + + /* + * (non-Javadoc) + * @see org.springframework.batch.container.common.executor.JobExecutionRegistry#findByRuntimeInformation(org.springframework.batch.container.common.runtime.JobRuntimeInformation) + */ + public JobExecutionContext get(JobIdentifier runtimeInformation) { + + synchronized (this.contexts) { + return (JobExecutionContext) contexts.get(runtimeInformation); + } + + } + + /* + * (non-Javadoc) + * @see org.springframework.batch.container.common.executor.JobExecutionRegistry#isRegistered(org.springframework.batch.container.common.runtime.JobRuntimeInformation) + */ + public boolean isRegistered(JobIdentifier runtimeInformation) { + + synchronized (this.contexts) { + return contexts.containsKey(runtimeInformation); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.batch.container.common.executor.JobExecutionRegistry#register(org.springframework.batch.container.common.runtime.JobRuntimeInformation, + * org.springframework.batch.container.common.domain.JobExecution) + */ + public JobExecutionContext register(JobIdentifier jobIdentifier, JobInstance job) { + if (isRegistered(jobIdentifier)) { + return get(jobIdentifier); + } + JobExecutionContext context = new JobExecutionContext(jobIdentifier, job); + + synchronized (this.contexts) { + contexts.put(jobIdentifier, context); + } + + return context; + } + + /* + * (non-Javadoc) + * @see org.springframework.batch.container.common.executor.JobExecutionRegistry#unregister(org.springframework.batch.container.common.runtime.JobRuntimeInformation) + */ + public void unregister(JobIdentifier runtimeInformation) { + + synchronized (this.contexts) { + contexts.remove(runtimeInformation); + } + + } + +} diff --git a/execution/src/main/java/org/springframework/batch/execution/facade/package.html b/execution/src/main/java/org/springframework/batch/execution/facade/package.html new file mode 100644 index 000000000..1bdb94690 --- /dev/null +++ b/execution/src/main/java/org/springframework/batch/execution/facade/package.html @@ -0,0 +1,7 @@ + + +

+Specific implementations of facade concerns. +

+ + diff --git a/execution/src/main/java/org/springframework/batch/execution/job/DefaultJobExecutor.java b/execution/src/main/java/org/springframework/batch/execution/job/DefaultJobExecutor.java new file mode 100644 index 000000000..7b6e9f009 --- /dev/null +++ b/execution/src/main/java/org/springframework/batch/execution/job/DefaultJobExecutor.java @@ -0,0 +1,152 @@ +/* + * 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.execution.job; + +import java.sql.Timestamp; +import java.util.Iterator; +import java.util.List; + +import org.springframework.batch.core.configuration.JobConfiguration; +import org.springframework.batch.core.configuration.StepConfiguration; +import org.springframework.batch.core.domain.BatchStatus; +import org.springframework.batch.core.domain.JobExecution; +import org.springframework.batch.core.domain.JobInstance; +import org.springframework.batch.core.domain.StepInstance; +import org.springframework.batch.core.executor.JobExecutor; +import org.springframework.batch.core.executor.StepExecutor; +import org.springframework.batch.core.executor.StepExecutorFactory; +import org.springframework.batch.core.executor.StepInterruptedException; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.runtime.JobExecutionContext; +import org.springframework.batch.core.runtime.StepExecutionContext; +import org.springframework.batch.execution.step.DefaultStepExecutorFactory; +import org.springframework.batch.io.exception.BatchCriticalException; +import org.springframework.batch.repeat.ExitStatus; +import org.springframework.batch.repeat.RepeatContext; + +/** + * Default implementation of (@JobLifecycle) interface. Sequentially executes a + * job by iterating it's life of steps. Interruption of a job run is pluggable + * by passing in various interruption policies. + * + * @author Lucas Ward + */ +public class DefaultJobExecutor implements JobExecutor { + + private JobRepository jobRepository; + + private StepExecutorFactory stepExecutorResolver = new DefaultStepExecutorFactory(); + + public void run(JobConfiguration configuration, JobExecutionContext jobExecutionContext) + throws BatchCriticalException { + + JobInstance job = jobExecutionContext.getJob(); + JobExecution jobExecution = jobExecutionContext.getJobExecution(); + updateStatus(jobExecutionContext, BatchStatus.STARTING); + + List steps = job.getSteps(); + + ExitStatus status = ExitStatus.FAILED; + + try { + for (Iterator i = steps.iterator(), j = configuration.getStepConfigurations().iterator(); i.hasNext() + && j.hasNext();) { + StepInstance step = (StepInstance) i.next(); + StepConfiguration stepConfiguration = (StepConfiguration) j.next(); + if (shouldStart(step, stepConfiguration)) { + updateStatus(jobExecutionContext, BatchStatus.STARTED); + StepExecutor stepExecutor = stepExecutorResolver.getExecutor(stepConfiguration); + StepExecutionContext stepExecutionContext = new StepExecutionContext(jobExecutionContext, step); + status = stepExecutor.process(stepConfiguration, stepExecutionContext); + } + } + + updateStatus(jobExecutionContext, BatchStatus.COMPLETED); + } + catch (StepInterruptedException e) { + updateStatus(jobExecutionContext, BatchStatus.STOPPED); + rethrow(e); + } + catch (Throwable t) { + updateStatus(jobExecutionContext, BatchStatus.FAILED); + rethrow(t); + } + finally { + jobExecution.setEndTime(new Timestamp(System.currentTimeMillis())); + jobExecution.setExitCode(status.getExitCode()); + jobRepository.saveOrUpdate(jobExecution); + } + } + + private void updateStatus(JobExecutionContext jobExecutionContext, BatchStatus status) { + JobInstance job = jobExecutionContext.getJob(); + JobExecution jobExecution = jobExecutionContext.getJobExecution(); + jobExecution.setStatus(status); + job.setStatus(status); + jobRepository.update(job); + jobRepository.saveOrUpdate(jobExecution); + for (Iterator iter = jobExecutionContext.getStepContexts().iterator(); iter.hasNext();) { + RepeatContext context = (RepeatContext) iter.next(); + context.setAttribute("JOB_STATUS", status); + } + } + + /* + * Given a step and configuration, return true if the step should start, + * false if it should not, and throw an exception if the job should finish. + */ + private boolean shouldStart(StepInstance step, StepConfiguration stepConfiguration) { + + if (step.getStatus() == BatchStatus.COMPLETED && stepConfiguration.isAllowStartIfComplete() == false) { + // step is complete, false should be returned, indicated that the + // step should + // not be started + return false; + } + + if (step.getStepExecutionCount() < stepConfiguration.getStartLimit()) { + // step start count is less than start max, return true + return true; + } + else { + // start max has been exceeded, throw an exception. + throw new BatchCriticalException("Maximum start limit exceeded for step: " + step.getName() + "StartMax: " + + stepConfiguration.getStartLimit()); + } + } + + /** + * @param t + */ + private static void rethrow(Throwable t) throws RuntimeException { + if (t instanceof RuntimeException) { + throw (RuntimeException) t; + } + else { + throw new BatchCriticalException(t); + } + } + + public void setJobRepository(JobRepository jobRepository) { + this.jobRepository = jobRepository; + } + + public void setStepExecutorResolver(StepExecutorFactory stepExecutorResolver) { + this.stepExecutorResolver = stepExecutorResolver; + } + +} diff --git a/execution/src/main/java/org/springframework/batch/execution/job/package.html b/execution/src/main/java/org/springframework/batch/execution/job/package.html new file mode 100644 index 000000000..3e790dbd7 --- /dev/null +++ b/execution/src/main/java/org/springframework/batch/execution/job/package.html @@ -0,0 +1,7 @@ + + +

+Specific implementations of job concerns. +

+ + diff --git a/execution/src/main/java/org/springframework/batch/execution/package.html b/execution/src/main/java/org/springframework/batch/execution/package.html new file mode 100644 index 000000000..2141a8422 --- /dev/null +++ b/execution/src/main/java/org/springframework/batch/execution/package.html @@ -0,0 +1,7 @@ + + +

+Reference implementation of the Spring Batch Core. +

+ + diff --git a/execution/src/main/java/org/springframework/batch/execution/repository/SimpleJobRepository.java b/execution/src/main/java/org/springframework/batch/execution/repository/SimpleJobRepository.java new file mode 100644 index 000000000..6444b5c0c --- /dev/null +++ b/execution/src/main/java/org/springframework/batch/execution/repository/SimpleJobRepository.java @@ -0,0 +1,253 @@ +/* + * 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.execution.repository; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.springframework.batch.core.configuration.JobConfiguration; +import org.springframework.batch.core.configuration.StepConfiguration; +import org.springframework.batch.core.domain.JobExecution; +import org.springframework.batch.core.domain.JobInstance; +import org.springframework.batch.core.domain.StepExecution; +import org.springframework.batch.core.domain.StepInstance; +import org.springframework.batch.core.repository.BatchRestartException; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.repository.NoSuchBatchDomainObjectException; +import org.springframework.batch.core.runtime.JobIdentifier; +import org.springframework.batch.execution.repository.dao.JobDao; +import org.springframework.batch.execution.repository.dao.StepDao; +import org.springframework.util.Assert; + +/** + * + *

+ * Simple Job Repository that stores Jobs, JobExecutions, Steps, and + * StepExecutions using the provided JobDao and StepDao. + *

+ * + * @author Lucas Ward + * @author Dave Syer + * @see JobRepository + * @see StepDao + * @see JobDao + * + */ +public class SimpleJobRepository implements JobRepository { + + private JobDao jobDao; + + private StepDao stepDao; + + public SimpleJobRepository(JobDao jobDao, StepDao stepDao) { + super(); + this.jobDao = jobDao; + this.stepDao = stepDao; + } + + /** + *

+ * Find or Create a Job(@link Job) based on the passed in RuntimeInformation + * and Configuration. JobRuntimeInformation contains the following fields + * which logically identify a job: JobName, JobStream, JobRun, and Schedule + * Date. However, unique identification of a job can only come from the + * database, and therefore must come from JobDao by either creating a new + * job or finding an existing one, which will ensure that the id field of + * the job is populated with the correct value. + *

+ * + *

+ * There are two ways in which the method determines if a job should be + * created or an existing one should be returned. The first is + * restartability. The Job's restartPolicy will be checked first. If it is + * not restartable, a new job will be created, regardless of whether or not + * one exists. If it is restartable, the JobDao will be checked to determine + * if the job already exists, if it does, it's steps will be populated + * (there must be at least 1) and it will be returned. If no job is found, a + * new one will be created based on the configuration. + *

+ * + * @see JobRepository#findOrCreateJob(JobConfiguration, + * JobIdentifier) + */ + public JobInstance findOrCreateJob(JobConfiguration jobConfiguration, JobIdentifier runtimeInformation) { + + List jobs; + + // Check if a job is restartable, if not, create and return a new job + if (jobConfiguration.isRestartable() == false) { + return createJob(jobConfiguration, runtimeInformation); + } + else { + // find all jobs matching the runtime information. + jobs = jobDao.findJobs(runtimeInformation); + } + + if (jobs.size() == 1) { + // One job was found + JobInstance job = (JobInstance) jobs.get(0); + job.setSteps(findSteps(jobConfiguration.getStepConfigurations(), job)); + job.setJobExecutionCount(jobDao.getJobExecutionCount(job.getId())); + if (job.getJobExecutionCount() > jobConfiguration.getStartLimit()) { + throw new BatchRestartException("Restart Max exceeded for Job: " + job.toString()); + } + return job; + } + else if (jobs.size() == 0) { + // no job found, create one + return createJob(jobConfiguration, runtimeInformation); + } + else { + // More than one job found, throw exception + throw new NoSuchBatchDomainObjectException("Error obtaining" + "previous job run: " + + jobConfiguration.toString()); + } + } + + /** + * Save or Update a JobExecution. A JobExecution is considered one + * 'execution' of a particular job. Therefore, it must have it's jobId field + * set before it is passed into this method. It also has it's own unique + * identifer, because it must be updatable separately. If an id isn't found, + * a new JobExecution is created, if one is found, the current row is + * updated. + * + * @param JobExecution to be stored. + * @throws IllegalArgumentException if jobExecution is null. + */ + public void saveOrUpdate(JobExecution jobExecution) { + + Assert.notNull(jobExecution, "JobExecution cannot be null."); + Assert.notNull(jobExecution.getJobId(), "JobExecution must have a Job ID set."); + + if (jobExecution.getId() == null) { + // existing instance + jobDao.save(jobExecution); + } + else { + // new execution + jobDao.update(jobExecution); + } + } + + /** + * Update an existing job. A job must have been obtained from the + * findOrCreateJob method, otherwise it is likely that the id is incorrect + * or non-existant. + * + * @param job to be updated. + * @throws IllegalArgumentException if Job or it's Id is null. + */ + public void update(JobInstance job) { + + Assert.notNull(job, "Job cannot be null."); + Assert.notNull(job.getId(), "Job cannot be updated if it's ID is null. It must be obtained" + + "from SimpleJobRepository.findOrCreateJob to be considered valid."); + + jobDao.update(job); + } + + /** + * Save or Update the given StepExecution. If it's id is null, it will be + * saved and an id will be set, otherwise it will be updated. It should be + * noted that assigning an ID randomly will likely cause an exception + * depending on the StepDao implementation. + * + * @param StepExecution to be saved. + * @throws IllegalArgumentException if stepExecution is null. + */ + public void saveOrUpdate(StepExecution stepExecution) { + + Assert.notNull(stepExecution, "StepExecution cannot be null."); + Assert.notNull(stepExecution.getStepId(), "StepExecution's Step Id cannot be null."); + + if (stepExecution.getId() == null) { + // new execution, obtain id and insert + stepDao.save(stepExecution); + } + else { + // existing execution, update + stepDao.update(stepExecution); + } + } + + /** + * Update the given step. + * + * @param StepInstance to be updated. + * @throws IllegalArgumentException if step or it's id is null. + */ + public void update(StepInstance step) { + + Assert.notNull(step, "Step cannot be null."); + Assert.notNull(step.getId(), "Step cannot be updated if it's ID is null. It must be obtained" + + "from SimpleJobRepository.findOrCreateJob to be considered valid."); + + stepDao.update(step); + + } + + /* + * Convenience method for creating a new job. A new job is created by + * calling {@link JobDao#createJob(JobRuntimeInformation)} and then it's + * list of StepConfigurations is passed to the createSteps method. + */ + private JobInstance createJob(JobConfiguration jobConfiguration, JobIdentifier runtimeInformation) { + + JobInstance job = jobDao.createJob(runtimeInformation); + job.setSteps(createSteps(job, jobConfiguration.getStepConfigurations())); + return job; + } + + /* + * Create steps based on the given Job and list of StepConfigurations. + */ + private List createSteps(JobInstance job, List stepConfigurations) { + + List steps = new ArrayList(); + Iterator i = stepConfigurations.iterator(); + while (i.hasNext()) { + StepConfiguration stepConfiguration = (StepConfiguration) i.next(); + StepInstance step = stepDao.createStep(job, stepConfiguration.getName()); + steps.add(step); + } + + return steps; + } + + /* + * Find Steps for the given list of StepConfiguration's with a given JobId + */ + protected List findSteps(List stepConfigurations, JobInstance job) { + List steps = new ArrayList(); + Iterator i = stepConfigurations.iterator(); + while (i.hasNext()) { + + StepConfiguration stepConfiguration = (StepConfiguration) i.next(); + StepInstance step = stepDao.findStep(job, stepConfiguration.getName()); + if (step != null) { + + step.setStepExecutionCount(stepDao.getStepExecutionCount(step.getId())); + + steps.add(step); + } + } + return steps; + } + +} diff --git a/execution/src/main/java/org/springframework/batch/execution/repository/dao/BatchStatusUserType.java b/execution/src/main/java/org/springframework/batch/execution/repository/dao/BatchStatusUserType.java new file mode 100644 index 000000000..de5d602e3 --- /dev/null +++ b/execution/src/main/java/org/springframework/batch/execution/repository/dao/BatchStatusUserType.java @@ -0,0 +1,65 @@ +/* + * 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.execution.repository.dao; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.springframework.batch.core.domain.BatchStatus; +import org.springframework.jdbc.support.lob.LobCreator; +import org.springframework.jdbc.support.lob.LobHandler; +import org.springframework.orm.hibernate3.support.ClobStringType; + +/** + * User type object to help Hibernate to persist {@link BatchStatus} objects + * (just plonking it a Clob). + * + * @author tomas.slanina + * + */ +public class BatchStatusUserType extends ClobStringType { + + /** + * Get a {@link BatchStatus} from a Clob. + * + * @return a {@link BatchStatus} object whose string representation is the + * same as the database value. + * + * @see org.springframework.orm.hibernate3.support.ClobStringType#nullSafeGetInternal(java.sql.ResultSet, + * java.lang.String[], java.lang.Object, + * org.springframework.jdbc.support.lob.LobHandler) + */ + protected Object nullSafeGetInternal(ResultSet rs, String[] names, Object owner, LobHandler lobHandler) + throws SQLException { + String status = (String) super.nullSafeGetInternal(rs, names, owner, lobHandler); + return BatchStatus.getStatus(status); + } + + /** + * Convert an object to a string and then pop it in a Clob. + * + * @see org.springframework.orm.hibernate3.support.ClobStringType#nullSafeSetInternal(java.sql.PreparedStatement, + * int, java.lang.Object, org.springframework.jdbc.support.lob.LobCreator) + */ + protected void nullSafeSetInternal(PreparedStatement ps, int index, Object value, LobCreator lobCreator) + throws SQLException { + String status = (value == null) ? "" : value.toString(); + super.nullSafeSetInternal(ps, index, status, lobCreator); + } + +} diff --git a/execution/src/main/java/org/springframework/batch/execution/repository/dao/HibernateJobDao.java b/execution/src/main/java/org/springframework/batch/execution/repository/dao/HibernateJobDao.java new file mode 100644 index 000000000..f77c12577 --- /dev/null +++ b/execution/src/main/java/org/springframework/batch/execution/repository/dao/HibernateJobDao.java @@ -0,0 +1,196 @@ +/* + * 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.execution.repository.dao; + +import java.util.List; + +import org.hibernate.Criteria; +import org.hibernate.Session; +import org.hibernate.criterion.Expression; +import org.springframework.batch.core.domain.JobExecution; +import org.springframework.batch.core.domain.JobInstance; +import org.springframework.batch.core.repository.NoSuchBatchDomainObjectException; +import org.springframework.batch.core.runtime.JobIdentifier; +import org.springframework.batch.execution.runtime.ScheduledJobIdentifier; +import org.springframework.orm.hibernate3.HibernateCallback; +import org.springframework.orm.hibernate3.support.HibernateDaoSupport; +import org.springframework.util.Assert; + +/** + * Implementation of {@link JobDao} functionality based on the Hibernate ORM + * framework. Its advantage is the independence of implementation on the + * underlying database. + * + * @author tomas.slanina + * @author Dave Syer + */ + +public class HibernateJobDao extends HibernateDaoSupport implements JobDao { + + /** + * @see JobDao#createJob(JobIdentifier) + * + * In this Hibernate implementation a job is stored into the database. Id is + * obtained from Hibernate. + */ + public JobInstance createJob(JobIdentifier jobIdentifier) { + + ScheduledJobIdentifier jobRuntimeInformation = (ScheduledJobIdentifier) jobIdentifier; + + validateJobIdentifier(jobRuntimeInformation); + + JobInstance job = new JobInstance(); + job.setIdentifier(jobIdentifier); + + Long jobId = (Long) getHibernateTemplate().save(job); + + job.setId(jobId); + + return job; + } + + /** + * @see JobDao#findJobs(JobIdentifier) + * + * Hibernate is asked to get all jobs that matches criteria. Afterwards, + * result is mapped into domain objects. + */ + public List findJobs(JobIdentifier jobIdentifier) { + + final ScheduledJobIdentifier jobRuntimeInformation = (ScheduledJobIdentifier) jobIdentifier; + + validateJobIdentifier(jobRuntimeInformation); + + List list = this.getHibernateTemplate().executeFind(new HibernateCallback() { + public Object doInHibernate(Session session) { + Criteria criteria = session.createCriteria(JobInstance.class); + criteria.add(Expression.eq("identifier", jobRuntimeInformation)); + return criteria.list(); + } + }); + + return list; + } + + /** + * @see JobDao#getJobExecutionCount(Long) + */ + public int getJobExecutionCount(final Long jobId) { + + Assert.notNull(jobId, "JobId cannot be null"); + + Long result = (Long) this.getHibernateTemplate().execute(new HibernateCallback() { + public Object doInHibernate(Session session) { + return session.createQuery("select count(id) from JobExecution where jobId = :jobId").setLong("jobId", + jobId.longValue()).uniqueResult(); + } + }); + + return (result == null) ? 0 : result.intValue(); + } + + /** + * @see JobDao#save(JobExecution) + * + * Hibernate implementation persists JobExecution instance. Id is obtained + * from Hibernate. + */ + public void save(JobExecution jobExecution) { + + validateJobExecution(jobExecution); + + Long id = (Long) getHibernateTemplate().save(jobExecution); + jobExecution.setId(id); + } + + /** + * @see JobDao#update(JobInstance) + */ + public void update(JobInstance job) { + + Assert.notNull(job, "Job Cannot be Null"); + Assert.notNull(job.getStatus(), "Job Status cannot be Null"); + Assert.notNull(job.getId(), "Job ID cannot be null"); + + getHibernateTemplate().update(job); + } + + /** + * @see JobDao#update(JobExecution) + */ + public void update(final JobExecution jobExecution) { + + validateJobExecution(jobExecution); + + if (jobExecution.getId() == null) { + throw new IllegalArgumentException("JobExecution ID cannot be null. JobExecution must be saved " + + "before it can be updated."); + } + + if (getHibernateTemplate().get(JobExecution.class, jobExecution.getId()) == null) { + throw new NoSuchBatchDomainObjectException("Invalid JobExecution, ID " + jobExecution.getId() + + " not found."); + } + + getHibernateTemplate().update(jobExecution); + } + + public List findJobExecutions(JobInstance job) { + + Assert.notNull(job, "Job cannot be null."); + Assert.notNull(job.getId(), "Job ID cannot be null."); + + final Long jobId = job.getId(); + + List list = this.getHibernateTemplate().executeFind(new HibernateCallback() { + public Object doInHibernate(Session session) { + Criteria criteria = session.createCriteria(JobExecution.class); + criteria.add(Expression.eq("jobId", jobId)); + return criteria.list(); + } + }); + + return list; + } + + /* + * Validate JobExecution. At a minimum, JobId, StartTime, EndTime, and + * Status cannot be null. + * + * @param jobExecution @throws IllegalArgumentException + */ + private void validateJobExecution(JobExecution jobExecution) { + + Assert.notNull(jobExecution); + Assert.notNull(jobExecution.getJobId(), "JobExecution Job-Id cannot be null."); + Assert.notNull(jobExecution.getStartTime(), "JobExecution start time cannot be null."); + Assert.notNull(jobExecution.getStatus(), "JobExecution status cannot be null."); + } + + /* + * Validate JobRuntimeInformation. Due to differing requirements, it is + * acceptable for any field to be blank, however null fields may cause odd + * and vague exception reports from the database driver. + */ + private void validateJobIdentifier(ScheduledJobIdentifier jobRuntimeInformation) { + + Assert.notNull(jobRuntimeInformation, "JobRuntimeInformation cannot be null."); + Assert.notNull(jobRuntimeInformation.getName(), "JobRuntimeInformation name cannot be null."); + Assert.notNull(jobRuntimeInformation.getJobStream(), "JobRuntimeInformation JobStream cannot be null."); + Assert.notNull(jobRuntimeInformation.getScheduleDate(), "JobRuntimeInformation ScheduleDate cannot be null."); + } +} diff --git a/execution/src/main/java/org/springframework/batch/execution/repository/dao/HibernateStepDao.java b/execution/src/main/java/org/springframework/batch/execution/repository/dao/HibernateStepDao.java new file mode 100644 index 000000000..5d1a720ee --- /dev/null +++ b/execution/src/main/java/org/springframework/batch/execution/repository/dao/HibernateStepDao.java @@ -0,0 +1,188 @@ +/* + * 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.execution.repository.dao; + +import java.util.List; + +import org.hibernate.Criteria; +import org.hibernate.Session; +import org.hibernate.criterion.Expression; +import org.springframework.batch.core.domain.JobInstance; +import org.springframework.batch.core.domain.StepExecution; +import org.springframework.batch.core.domain.StepInstance; +import org.springframework.orm.hibernate3.HibernateCallback; +import org.springframework.orm.hibernate3.support.HibernateDaoSupport; +import org.springframework.util.Assert; + +/** + * It represents an implementation of {@link StepDao} functionality based + * on the Hibernate ORM framework. Its advantage is the independency of implementation + * on the underlying database. + * + * @author tomas.slanina + */ +public class HibernateStepDao extends HibernateDaoSupport implements StepDao { + + /* (non-Javadoc) + * @see org.springframework.batch.container.repository.dao.StepDao#createStep(String, java.lang.Long) + */ + public StepInstance createStep(JobInstance job, String stepName) { + + Assert.notNull(job, "Job cannot be null."); + Assert.notNull(stepName, "StepName cannot be null."); + + StepInstance step = new StepInstance(); + step.setName(stepName); + step.setJob(job); + + Long stepId = (Long)getHibernateTemplate().save(step); + + step.setId(stepId); + + return step; + + } + + /** + * @see StepDao#findStep(Long, String) + */ + public StepInstance findStep(final JobInstance job, final String stepName) { + + Assert.notNull(job, "Job cannot be null."); + Assert.notNull(job.getId(), "Job ID cannot be null"); + Assert.notNull(stepName, "StepName cannot be null"); + + return (StepInstance) this.getHibernateTemplate().execute(new HibernateCallback() { + public Object doInHibernate(Session session) { + Criteria criteria = session.createCriteria(StepInstance.class); + criteria.add(Expression.eq("name", stepName)); + criteria.add(Expression.eq("job.id", job.getId())); + return criteria.uniqueResult(); + } + }); + + } + + /** + * @see StepDao#findSteps(Long) + * + * Hibernate is asked to get all jobs that matches criteria. Afterwards, result is mapped into domain objects. + * It should be noted that restart data must be requested separately. + * + */ + public List findSteps(final Long jobId) { + + Assert.notNull(jobId, "JobId cannot be null."); + + List list = this.getHibernateTemplate().executeFind(new HibernateCallback() { + public Object doInHibernate(Session session) { + Criteria criteria = session.createCriteria(StepInstance.class); + criteria.add(Expression.eq("job.id", jobId)); + return criteria.list(); + } + }); + + return list; + } + + /** + * @see StepDao#getStepExecutionCount(Long) + */ + public int getStepExecutionCount(final Long stepId) { + Long result = (Long) this.getHibernateTemplate().execute(new HibernateCallback() { + public Object doInHibernate(Session session) { + return session.createQuery("select count(id) from StepExecution s where s.stepId = :stepId") + .setLong("stepId", stepId.longValue()) + .uniqueResult(); + } + }); + + return (result==null) ? 0 :result.intValue(); + } + + /** + * @see StepDao#save(StepExecution) + * + * Hibernate implementation persists StepExecution instance. Id is obtained from Hibernate. + */ + public void save(StepExecution stepExecution) { + + validateStepExecution(stepExecution); + + Long id = (Long)getHibernateTemplate().save(stepExecution); + stepExecution.setId(id); + } + + /** + * @see StepDao#update(StepInstance) + */ + public void update(StepInstance step) { + + Assert.notNull(step, "Step cannot be null."); + Assert.notNull(step.getStatus(), "Step status cannot be null."); + Assert.notNull(step.getId(), "Step Id cannot be null."); + + getHibernateTemplate().update(step); + } + + /** + * @see StepDao#update(StepExecution) + */ + public void update(StepExecution stepExecution) { + + validateStepExecution(stepExecution); + Assert.notNull(stepExecution.getId(), "StepExecution Id cannot be null. StepExecution must saved" + + " before it can be updated."); + + getHibernateTemplate().update(stepExecution); + } + + public List findStepExecutions(StepInstance step) { + + Assert.notNull(step, "Step cannot be null."); + Assert.notNull(step.getId(), "Step id cannot be null."); + + final Long stepId = step.getId(); + + List results = this.getHibernateTemplate().executeFind(new HibernateCallback() { + public Object doInHibernate(Session session) { + Criteria criteria = session.createCriteria(StepExecution.class); + criteria.add(Expression.eq("stepId", stepId)); + return criteria.list(); + } + }); + + return results; + + } + + /* + * Validate StepExecution. At a minimum, JobId, StartTime, EndTime, and Status cannot be + * null. EndTime can be null for an unfinished job. + * + * @param jobExecution + * @throws IllegalArgumentException + */ + private void validateStepExecution(StepExecution stepExecution){ + + Assert.notNull(stepExecution); + Assert.notNull(stepExecution.getStepId(), "StepExecution Step-Id cannot be null."); + Assert.notNull(stepExecution.getStartTime(), "StepExecution start time cannot be null."); + Assert.notNull(stepExecution.getStatus(), "StepExecution status cannot be null."); + } + +} diff --git a/execution/src/main/java/org/springframework/batch/execution/repository/dao/JobDao.java b/execution/src/main/java/org/springframework/batch/execution/repository/dao/JobDao.java new file mode 100644 index 000000000..c08cd4d9b --- /dev/null +++ b/execution/src/main/java/org/springframework/batch/execution/repository/dao/JobDao.java @@ -0,0 +1,96 @@ +/* + * 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.execution.repository.dao; + +import java.util.List; + +import org.springframework.batch.core.domain.JobExecution; +import org.springframework.batch.core.domain.JobInstance; +import org.springframework.batch.core.runtime.JobIdentifier; + +/** + * Data Access Object for jobs. + * + * @author Lucas Ward + * + */ +public interface JobDao { + + /** + * Create a job using the provided JobIdentifier as the natural key. + * + * PostConditions: A valid job will be returned which contains an unique Id. + * + * @param jobIdentifier + * @return Job + */ + public JobInstance createJob(JobIdentifier jobIdentifier); + + /** + * Find all jobs that match the given JobIdentifier. If no jobs matching the + * Identifier are found, then a list of size 0 will be returned. + * + * @param jobIdentifier + * @return List of jobs matching JobIdentifier + */ + public List findJobs(JobIdentifier jobIdentifier); + + /** + * Update an existing Job. + * + * Preconditions: Job must have an ID. + * + * @param job + */ + public void update(JobInstance job); + + /** + * Save a new JobExecution. + * + * Preconditions: JobExecution must have a JobId. + * + * @param jobExecution + */ + public void save(JobExecution jobExecution); + + /** + * Update and existing JobExecution. + * + * Preconditions: JobExecution must have an Id (which can be obtained by the + * save method) and a JobId. + * + * @param jobExecution + */ + public void update(JobExecution jobExecution); + + /** + * Return the number of JobExecutions with the given Job Id + * + * Preconditions: Job must have an id. + * + * @param job + */ + public int getJobExecutionCount(Long jobId); + + /** + * Return list of JobExecutions for given job. + * + * @param job + * @return list of jobExecutions. + */ + public List findJobExecutions(JobInstance job); +} diff --git a/execution/src/main/java/org/springframework/batch/execution/repository/dao/MapJobDao.java b/execution/src/main/java/org/springframework/batch/execution/repository/dao/MapJobDao.java new file mode 100644 index 000000000..ffec3a2ac --- /dev/null +++ b/execution/src/main/java/org/springframework/batch/execution/repository/dao/MapJobDao.java @@ -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.execution.repository.dao; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.batch.core.domain.JobExecution; +import org.springframework.batch.core.domain.JobInstance; +import org.springframework.batch.core.runtime.JobIdentifier; +import org.springframework.batch.support.transaction.TransactionAwareProxyFactory; + +public class MapJobDao implements JobDao { + + private static Map jobsById; + private static Map executionsById; + + private long currentId = 0; + + static { + jobsById = TransactionAwareProxyFactory.createTransactionalMap(); + executionsById = TransactionAwareProxyFactory.createTransactionalMap(); + } + + public static void clear() { + jobsById.clear(); + executionsById.clear(); + } + + public JobInstance createJob(JobIdentifier jobIdentifier) { + JobInstance job = new JobInstance(new Long(currentId++)); + job.setIdentifier(jobIdentifier); + + jobsById.put(job.getId(), job); + return job; + } + + public List findJobs(JobIdentifier jobRuntimeInformation) { + List list = new ArrayList(); + for (Iterator iter = jobsById.values().iterator(); iter.hasNext();) { + JobInstance job = (JobInstance) iter.next(); + if (job.getName().equals(jobRuntimeInformation.getName())) { + list.add(job); + } + } + return list; + } + + public int getJobExecutionCount(Long jobId) { + Set executions = (Set) executionsById.get(jobId); + if (executions==null) return 0; + return executions.size(); } + + public void save(JobExecution jobExecution) { + Set executions = (Set) executionsById.get(jobExecution.getJobId()); + if (executions==null) { + executions = TransactionAwareProxyFactory.createTransactionalSet(); + executionsById.put(jobExecution.getJobId(), executions); + } + executions.add(jobExecution); + jobExecution.setId(new Long(currentId++)); + } + + public List findJobExecutions(JobInstance job) { + Set executions = (Set) executionsById.get(job.getId()); + if( executions == null ){ + return new ArrayList(); + } + else{ + return new ArrayList(executions); + } + } + + public void update(JobInstance job) { + // no-op + } + + public void update(JobExecution jobExecution) { + // no-op + } + +} diff --git a/execution/src/main/java/org/springframework/batch/execution/repository/dao/MapStepDao.java b/execution/src/main/java/org/springframework/batch/execution/repository/dao/MapStepDao.java new file mode 100644 index 000000000..78c13f8ef --- /dev/null +++ b/execution/src/main/java/org/springframework/batch/execution/repository/dao/MapStepDao.java @@ -0,0 +1,129 @@ +/* + * 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.execution.repository.dao; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.batch.core.domain.JobInstance; +import org.springframework.batch.core.domain.StepExecution; +import org.springframework.batch.core.domain.StepInstance; +import org.springframework.batch.restart.RestartData; +import org.springframework.batch.support.transaction.TransactionAwareProxyFactory; + +public class MapStepDao implements StepDao { + + private static Map stepsByJobId; + private static Map executionsById; + private static Map restartsById; + private static long currentId = 0; + + static { + stepsByJobId = TransactionAwareProxyFactory.createTransactionalMap(); + executionsById = TransactionAwareProxyFactory.createTransactionalMap(); + restartsById = TransactionAwareProxyFactory.createTransactionalMap(); + } + + public static void clear() { + stepsByJobId.clear(); + executionsById.clear(); + restartsById.clear(); + } + + public StepInstance createStep(JobInstance job, String stepName) { + StepInstance step = new StepInstance(new Long(currentId++)); + step.setName(stepName); + step.setJob(job); + Set steps = (Set) stepsByJobId.get(job.getId()); + if (steps==null) { + steps = TransactionAwareProxyFactory.createTransactionalSet(); + stepsByJobId.put(job.getId(), steps); + } + steps.add(step); + //System.err.println(steps); + return step; + } + + public StepInstance findStep(JobInstance job, String stepName) { + for (Iterator iter = stepsByJobId.values().iterator(); iter.hasNext();) { + Set steps = (Set) iter.next(); + for (Iterator iterator = steps.iterator(); iterator.hasNext();) { + StepInstance step = (StepInstance) iterator.next(); + if (step.getName().equals(stepName)) { + return step; + } + } + } + return null; + } + + public List findSteps(Long jobId) { + Set steps = (Set) stepsByJobId.get(jobId); + if (steps==null) { + return new ArrayList(); + } + return new ArrayList(steps); + } + + public RestartData getRestartData(Long stepId) { + return (RestartData) restartsById.get(stepId); + } + + public int getStepExecutionCount(Long jobId) { + Set executions = (Set) executionsById.get(jobId); + if (executions==null) return 0; + return executions.size(); } + + public void save(StepExecution stepExecution) { + Set executions = (Set) executionsById.get(stepExecution.getStepId()); + if (executions==null) { + executions = TransactionAwareProxyFactory.createTransactionalSet(); + executionsById.put(stepExecution.getStepId(), executions); + } + stepExecution.setId(new Long(currentId++)); + executions.add(stepExecution); + } + + public void saveRestartData(Long stepId, RestartData restartData) { + restartsById.put(stepId, restartData); + } + + public List findStepExecutions(StepInstance step) { + Set executions = (Set) executionsById.get(step.getId()); + + if(executions == null){ + //no step executions, return empty array list. + return new ArrayList(); + } + else{ + return new ArrayList(executions); + } + } + + public void update(StepInstance step) { + // no-op + } + + public void update(StepExecution stepExecution) { + // no-op + } + +} + diff --git a/execution/src/main/java/org/springframework/batch/execution/repository/dao/PropertiesUserType.java b/execution/src/main/java/org/springframework/batch/execution/repository/dao/PropertiesUserType.java new file mode 100644 index 000000000..13c56c267 --- /dev/null +++ b/execution/src/main/java/org/springframework/batch/execution/repository/dao/PropertiesUserType.java @@ -0,0 +1,65 @@ +/* + * 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.execution.repository.dao; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Properties; + +import org.springframework.batch.support.PropertiesConverter; +import org.springframework.jdbc.support.lob.LobCreator; +import org.springframework.jdbc.support.lob.LobHandler; +import org.springframework.orm.hibernate3.support.ClobStringType; + +/** + * User type object to help Hibernate to persist Poperties objects + * (just plonking it a Clob). + * + * @author Dave Syer + * + */ +public class PropertiesUserType extends ClobStringType { + + /** + * Get a {@link Properties} from a Clob. + * + * @return a {@link Properties} object whose string representation is the + * same as the database value. + * + * @see org.springframework.orm.hibernate3.support.ClobStringType#nullSafeGetInternal(java.sql.ResultSet, + * java.lang.String[], java.lang.Object, + * org.springframework.jdbc.support.lob.LobHandler) + */ + protected Object nullSafeGetInternal(ResultSet rs, String[] names, Object owner, LobHandler lobHandler) + throws SQLException { + final String value = (String) super.nullSafeGetInternal(rs, names, owner, lobHandler); + return PropertiesConverter.stringToProperties(value); + } + + /** + * Convert a {@link Properties} object to a string and then pop it in a Clob. + * + * @see org.springframework.orm.hibernate3.support.ClobStringType#nullSafeSetInternal(java.sql.PreparedStatement, int, java.lang.Object, org.springframework.jdbc.support.lob.LobCreator) + */ + protected void nullSafeSetInternal(PreparedStatement ps, int index, Object value, LobCreator lobCreator) + throws SQLException { + String string = PropertiesConverter.propertiesToString((Properties)value); + super.nullSafeSetInternal(ps, index, string, lobCreator); + } + +} diff --git a/execution/src/main/java/org/springframework/batch/execution/repository/dao/RestartDataUserType.java b/execution/src/main/java/org/springframework/batch/execution/repository/dao/RestartDataUserType.java new file mode 100644 index 000000000..b1f2c3f8f --- /dev/null +++ b/execution/src/main/java/org/springframework/batch/execution/repository/dao/RestartDataUserType.java @@ -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.execution.repository.dao; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Properties; + +import org.springframework.batch.restart.GenericRestartData; +import org.springframework.batch.restart.RestartData; +import org.springframework.batch.support.PropertiesConverter; +import org.springframework.jdbc.support.lob.LobCreator; +import org.springframework.jdbc.support.lob.LobHandler; +import org.springframework.orm.hibernate3.support.ClobStringType; + +/** + * User type object to help Hibernate persist (@link RestartData) objects by setting + * a string in a clob. + * + * @author Lucas Ward + * + */ +public class RestartDataUserType extends ClobStringType { + + /** + * Get a {@link Properties} from a Clob. + * + * @return a {@link GenericRestartData} object whose internal properties string representation is the + * same as the database value. + * + * @see org.springframework.orm.hibernate3.support.ClobStringType#nullSafeGetInternal(java.sql.ResultSet, + * java.lang.String[], java.lang.Object, + * org.springframework.jdbc.support.lob.LobHandler) + */ + protected Object nullSafeGetInternal(ResultSet rs, String[] names, Object owner, LobHandler lobHandler) + throws SQLException { + final String value = (String) super.nullSafeGetInternal(rs, names, owner, lobHandler); + return new GenericRestartData(PropertiesConverter.stringToProperties(value)); + } + + /** + * Convert a {@link RestartData} object to a string and then pop it in a Clob. + * + * @see org.springframework.orm.hibernate3.support.ClobStringType#nullSafeSetInternal(java.sql.PreparedStatement, int, java.lang.Object, org.springframework.jdbc.support.lob.LobCreator) + */ + protected void nullSafeSetInternal(PreparedStatement ps, int index, Object value, LobCreator lobCreator) + throws SQLException { + final RestartData restartData = (RestartData)value; + String string = (restartData == null) ? "" + :PropertiesConverter.propertiesToString(restartData.getProperties()); + super.nullSafeSetInternal(ps, index, string, lobCreator); + } +} diff --git a/execution/src/main/java/org/springframework/batch/execution/repository/dao/SqlJobDao.java b/execution/src/main/java/org/springframework/batch/execution/repository/dao/SqlJobDao.java new file mode 100644 index 000000000..f52854066 --- /dev/null +++ b/execution/src/main/java/org/springframework/batch/execution/repository/dao/SqlJobDao.java @@ -0,0 +1,290 @@ +/* + * 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.execution.repository.dao; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; + +import org.springframework.batch.core.domain.BatchStatus; +import org.springframework.batch.core.domain.JobExecution; +import org.springframework.batch.core.domain.JobInstance; +import org.springframework.batch.core.repository.NoSuchBatchDomainObjectException; +import org.springframework.batch.core.runtime.JobIdentifier; +import org.springframework.batch.execution.runtime.ScheduledJobIdentifier; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.support.incrementer.DataFieldMaxValueIncrementer; +import org.springframework.util.Assert; + +/** + * SQL implementation of {@link JobDao}. Uses sequences (via Spring's + * @link DataFieldMaxValueIncrementer abstraction) to create all primary keys + * before inserting a new row. Objects are checked to ensure all mandatory + * fields to be stored are not null. If any are found to be null, an + * IllegalArgumentException will be thrown. This could be left to JdbcTemplate, + * however, the exception will be fairly vague, and fails to highlight which + * field caused the exception. + * + * @author Lucas Ward + * @author Dave Syer + */ +public class SqlJobDao implements JobDao, InitializingBean { + + // Job SQL statements + private static final String CREATE_JOB = "INSERT into BATCH_JOB(ID, JOB_NAME, JOB_STREAM, SCHEDULE_DATE, JOB_RUN)" + + " values (?, ?, ?, ?, ?)"; + + private static final String FIND_JOBS = "SELECT ID, STATUS from BATCH_JOB where JOB_NAME = ? and " + + "JOB_STREAM = ? and SCHEDULE_DATE = ? and JOB_RUN = ?"; + + private static final String UPDATE_JOB = "UPDATE BATCH_JOB set STATUS = ? where ID = ?"; + + private static final String GET_JOB_EXECUTION_COUNT = "SELECT count(ID) from BATCH_JOB_EXECUTION " + + "where JOB_ID = ?"; + + // Job Execution SqlStatements + private static final String UPDATE_JOB_EXECUTION = "UPDATE BATCH_JOB_EXECUTION set START_TIME = ?, END_TIME = ?, " + + " STATUS = ? where ID = ?"; + + private static final String SAVE_JOB_EXECUTION = "INSERT into BATCH_JOB_EXECUTION(ID, JOB_ID, START_TIME, END_TIME, STATUS)" + + " values (?, ?, ?, ?, ?)"; + + private static final String CHECK_JOB_EXECUTION_EXISTS = "SELECT COUNT(*) FROM BATCH_JOB_EXECUTION WHERE ID=?"; + + private static final String FIND_JOB_EXECUTIONS = "SELECT ID, START_TIME, END_TIME, STATUS from BATCH_JOB_EXECUTION" + + " where JOB_ID = ?"; + + private JdbcTemplate jdbcTemplate; + + private DataFieldMaxValueIncrementer jobIncrementer; + + private DataFieldMaxValueIncrementer jobExecutionIncrementer; + + /** + * In this sql implementation a job id is obtained by asking the + * jobIncrementer (which is likely a sequence) for the nextLong, and then + * passing the Id and identifier values (job name, stream, run, schedule + * date) into an INSERT statement. + * + * @see JobDao#createJob(JobIdentifier) + * @throws IllegalArgumentException if any JobRuntimeInformation fields are + * null. + */ + public JobInstance createJob(JobIdentifier jobIdentifier) { + + ScheduledJobIdentifier jobRuntimeInformation = (ScheduledJobIdentifier) jobIdentifier; + validateJobRuntimeInformation(jobRuntimeInformation); + + Long jobId = new Long(jobIncrementer.nextLongValue()); + Object[] parameters = new Object[] { jobId, jobRuntimeInformation.getName(), + jobRuntimeInformation.getJobStream(), jobRuntimeInformation.getScheduleDate(), + new Long(jobRuntimeInformation.getJobRun()) }; + jdbcTemplate.update(CREATE_JOB, parameters); + + JobInstance job = new JobInstance(jobId); + return job; + } + + /** + * The BATCH_JOB table is queried for any jobs that match + * the given identifier, adding them to a list via the RowMapper callback. + * + * @see JobDao#findJobs(JobIdentifier) + * @throws IllegalArgumentException if any JobRuntimeInformation fields are + * null. + */ + public List findJobs(JobIdentifier jobIdentifier) { + + ScheduledJobIdentifier defaultJobId = (ScheduledJobIdentifier) jobIdentifier; + validateJobRuntimeInformation(defaultJobId); + + Object[] parameters = new Object[] { defaultJobId.getName(), defaultJobId.getJobStream(), + defaultJobId.getScheduleDate(), new Integer(defaultJobId.getJobRun()) }; + + RowMapper rowMapper = new RowMapper() { + public Object mapRow(ResultSet rs, int rowNum) throws SQLException { + + JobInstance job = new JobInstance(new Long(rs.getLong(1))); + job.setStatus(BatchStatus.getStatus(rs.getString(2))); + + return job; + } + }; + + return jdbcTemplate.query(FIND_JOBS, parameters, rowMapper); + } + + /** + * @see JobDao#update(JobInstance) + * @throws IllegalArgumentException if Job, Job.status, or job.id is null + */ + public void update(JobInstance job) { + + Assert.notNull(job, "Job Cannot be Null"); + Assert.notNull(job.getStatus(), "Job Status cannot be Null"); + Assert.notNull(job.getId(), "Job ID cannot be null"); + + Object[] parameters = new Object[] { job.getStatus().toString(), job.getId() }; + jdbcTemplate.update(UPDATE_JOB, parameters); + } + + /** + * + * SQL implementation using Sequences via the Spring incrementer + * abstraction. Once a new id has been obtained, the JobExecution is saved + * via a SQL INSERT statement. + * + * @see JobDao#save(JobExecution) + * @throws IllegalArgumentException if jobExecution is null, as well as any + * of it's fields to be persisted. + */ + public void save(JobExecution jobExecution) { + + validateJobExecution(jobExecution); + + jobExecution.setId(new Long(jobExecutionIncrementer.nextLongValue())); + Object[] parameters = new Object[] { jobExecution.getId(), jobExecution.getJobId(), + jobExecution.getStartTime(), jobExecution.getEndTime(), jobExecution.getStatus().toString() }; + jdbcTemplate.update(SAVE_JOB_EXECUTION, parameters); + } + + /** + * Update given JobExecution using a SQL UPDATE statement. The JobExecution + * is first checked to ensure all fields are not null, and that it has an + * ID. The database is then queried to ensure that the ID exists, which + * ensures that it is valid. + * + * @see JobDao#update(JobExecution) + */ + public void update(JobExecution jobExecution) { + + validateJobExecution(jobExecution); + + Object[] parameters = new Object[] { jobExecution.getStartTime(), jobExecution.getEndTime(), + jobExecution.getStatus().toString(), jobExecution.getId() }; + + if (jobExecution.getId() == null) { + throw new IllegalArgumentException("JobExecution ID cannot be null. JobExecution must be saved " + + "before it can be updated."); + } + + // Check if given JobExecution's Id already exists, if none is found it + // is invalid and + // an exception should be thrown. + if (jdbcTemplate.queryForInt(CHECK_JOB_EXECUTION_EXISTS, new Object[] { jobExecution.getId() }) != 1) { + throw new NoSuchBatchDomainObjectException("Invalid JobExecution, ID " + jobExecution.getId() + + " not found."); + } + + jdbcTemplate.update(UPDATE_JOB_EXECUTION, parameters); + } + + /** + * @see JobDao#getJobExecutionCount(JobInstance) + * @throws IllegalArgumentException if jobId is null. + */ + public int getJobExecutionCount(Long jobId) { + + Assert.notNull(jobId, "JobId cannot be null"); + + Object[] parameters = new Object[] { jobId }; + + return jdbcTemplate.queryForInt(GET_JOB_EXECUTION_COUNT, parameters); + } + + public List findJobExecutions(JobInstance job) { + + Assert.notNull(job, "Job cannot be null."); + Assert.notNull(job.getId(), "Job Id cannot be null."); + + final Long jobId = job.getId(); + + RowMapper rowMapper = new RowMapper() { + + public Object mapRow(ResultSet rs, int rowNum) throws SQLException { + + JobExecution jobExecution = new JobExecution(jobId); + jobExecution.setId(new Long(rs.getLong(1))); + jobExecution.setStartTime(rs.getTimestamp(2)); + jobExecution.setEndTime(rs.getTimestamp(3)); + jobExecution.setStatus(BatchStatus.getStatus(rs.getString(4))); + + return jobExecution; + } + + }; + + return jdbcTemplate.query(FIND_JOB_EXECUTIONS, new Object[] { jobId }, rowMapper); + } + + public void setJdbcTemplate(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + public void setJobIncrementer(DataFieldMaxValueIncrementer jobIncrementer) { + this.jobIncrementer = jobIncrementer; + } + + public void setJobExecutionIncrementer(DataFieldMaxValueIncrementer jobExecutionIncrementer) { + this.jobExecutionIncrementer = jobExecutionIncrementer; + } + + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() + * + * Ensure jdbcTemplate and incrementers have been provided. + */ + public void afterPropertiesSet() throws Exception { + + Assert.notNull(jdbcTemplate, "JdbcTemplate cannot be null"); + Assert.notNull(jobIncrementer, "JobIncrementor cannot be null"); + Assert.notNull(jobExecutionIncrementer, "JobExecutionIncrementer cannot be null"); + } + + /* + * Validate JobExecution. At a minimum, JobId, StartTime, EndTime, and + * Status cannot be null. + * + * @param jobExecution @throws IllegalArgumentException + */ + private void validateJobExecution(JobExecution jobExecution) { + + Assert.notNull(jobExecution); + Assert.notNull(jobExecution.getJobId(), "JobExecution Job-Id cannot be null."); + Assert.notNull(jobExecution.getStartTime(), "JobExecution start time cannot be null."); + Assert.notNull(jobExecution.getStatus(), "JobExecution status cannot be null."); + } + + /* + * Validate JobRuntimeInformation. Due to differing requirements, it is + * acceptable for any field to be blank, however null fields may cause odd + * and vague exception reports from the database driver. + * + * TODO: remove dependency on ScheduledJobIdentifier + */ + private void validateJobRuntimeInformation(ScheduledJobIdentifier jobRuntimeInformation) { + + Assert.notNull(jobRuntimeInformation, "JobRuntimeInformation cannot be null."); + Assert.notNull(jobRuntimeInformation.getName(), "JobRuntimeInformation name cannot be null."); + Assert.notNull(jobRuntimeInformation.getJobStream(), "JobRuntimeInformation JobStream cannot be null."); + Assert.notNull(jobRuntimeInformation.getScheduleDate(), "JobRuntimeInformation ScheduleDate cannot be null."); + } + +} diff --git a/execution/src/main/java/org/springframework/batch/execution/repository/dao/SqlStepDao.java b/execution/src/main/java/org/springframework/batch/execution/repository/dao/SqlStepDao.java new file mode 100644 index 000000000..876283a45 --- /dev/null +++ b/execution/src/main/java/org/springframework/batch/execution/repository/dao/SqlStepDao.java @@ -0,0 +1,336 @@ +/* + * 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.execution.repository.dao; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; +import java.util.Properties; + +import org.springframework.batch.core.domain.BatchStatus; +import org.springframework.batch.core.domain.JobInstance; +import org.springframework.batch.core.domain.StepExecution; +import org.springframework.batch.core.domain.StepInstance; +import org.springframework.batch.core.repository.NoSuchBatchDomainObjectException; +import org.springframework.batch.restart.GenericRestartData; +import org.springframework.batch.restart.RestartData; +import org.springframework.batch.support.PropertiesConverter; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.support.incrementer.DataFieldMaxValueIncrementer; +import org.springframework.util.Assert; + +/** + * Sql implementation of StepDao. Uses Sequences (via Spring's + * @link DataFieldMaxValueIncrementer abstraction) to create all Step and + * StepExecution primary keys before inserting a new row. All objects are + * checked to ensure all fields to be stored are not null. If any are found to + * be null, an IllegalArgumentException will be thrown. This could be left to + * JdbcTemplate, however, the exception will be fairly vague, and fails to + * highlight which field caused the exception. + * + * TODO: JavaDoc should be geared more towards usability, the comments + * above are useful information, and should be there, but needs usability + * stuff. Depends on the step dao java docs as well. + * + * @author Lucas Ward + * @see StepDao + */ +public class SqlStepDao implements StepDao, InitializingBean { + + // Step SQL statements + private static final String FIND_STEPS = "SELECT ID, STEP_NAME, STATUS, RESTART_DATA from BATCH_STEP where JOB_ID = ?"; + + private static final String FIND_STEP = "SELECT ID, STATUS, RESTART_DATA from BATCH_STEP where JOB_ID = ? " + + "and STEP_NAME = ?"; + + private static final String CREATE_STEP = "INSERT into BATCH_STEP(ID, JOB_ID, STEP_NAME) values (?, ?, ?)"; + + private static final String UPDATE_STEP = "UPDATE BATCH_STEP set STATUS = ?, RESTART_DATA = ? where ID = ?"; + + // StepExecution statements + private static final String SAVE_STEP_EXECUTION = "INSERT into BATCH_STEP_EXECUTION(ID, VERSION, STEP_ID, JOB_EXECUTION_ID, START_TIME, " + + "END_TIME, STATUS, COMMIT_COUNT, TASK_COUNT, TASK_STATISTICS, EXIT_CODE) values(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + + private static final String UPDATE_STEP_EXECUTION = "UPDATE BATCH_STEP_EXECUTION set START_TIME = ?, END_TIME = ?, " + + "STATUS = ?, COMMIT_COUNT = ?, TASK_COUNT = ?, TASK_STATISTICS = ?, EXIT_CODE = ? where ID = ?"; + + private static final String GET_STEP_EXECUTION_COUNT = "SELECT count(ID) from BATCH_STEP_EXECUTION where " + + "STEP_ID = ?"; + + private static final String FIND_STEP_EXECUTIONS = "SELECT ID, JOB_EXECUTION_ID, START_TIME, END_TIME, STATUS, COMMIT_COUNT," + + " TASK_COUNT, TASK_STATISTICS, EXIT_CODE from BATCH_STEP_EXECUTION where STEP_ID = ?"; + + private JdbcTemplate jdbcTemplate; + + private DataFieldMaxValueIncrementer stepIncrementer; + + private DataFieldMaxValueIncrementer stepExecutionIncrementer; + + /** + * Find one step for given job and stepName. A RowMapper is used to map each + * row returned to a step object. If none are found, the list will be empty + * and null will be returned. If one step is found, it will be returned. If + * anymore than one step is found, an exception is thrown. + * + * @see StepDao#findStep(Long, String) + * @throws IllegalArgumentException if job, stepName, or job.id is null. + * @throws NoSuchBatchDomainObjectException if more than one step is found. + */ + public StepInstance findStep(JobInstance job, String stepName) { + + Assert.notNull(job, "Job cannot be null."); + Assert.notNull(job.getId(), "Job ID cannot be null"); + Assert.notNull(stepName, "StepName cannot be null"); + + Object[] parameters = new Object[] { job.getId(), stepName }; + + RowMapper rowMapper = new RowMapper() { + + public Object mapRow(ResultSet rs, int rowNum) throws SQLException { + + StepInstance step = new StepInstance(new Long(rs.getLong(1))); + step.setStatus(BatchStatus.getStatus(rs.getString(2))); + step.setRestartData( + new GenericRestartData(PropertiesConverter.stringToProperties(rs.getString(3)))); + return step; + } + + }; + + List steps = jdbcTemplate.query(FIND_STEP, parameters, rowMapper); + + if (steps.size() == 0) { + // No step found + return null; + } + else if (steps.size() == 1) { + StepInstance step = (StepInstance) steps.get(0); + step.setName(stepName); + return step; + } + else { + // This error will likely never be thrown, because there should + // never be two steps with the same name and Job_ID due to database + // constraints. + throw new NoSuchBatchDomainObjectException("Step Invalid, multiple steps found for StepName:" + stepName + + " and JobId:" + job.getId()); + } + + } + + /** + * @see StepDao#findSteps(Long) + * + * Sql implementation which uses a RowMapper to populate a list of all rows + * in the BATCH_STEP table with the same JOB_ID. + * + * @throws IllegalArgumentException if jobId is null. + */ + public List findSteps(Long jobId) { + + Assert.notNull(jobId, "JobId cannot be null."); + + Object[] parameters = new Object[] { jobId }; + + RowMapper rowMapper = new RowMapper() { + + public Object mapRow(ResultSet rs, int rowNum) throws SQLException { + + StepInstance step = new StepInstance(new Long(rs.getLong(1))); + step.setName(rs.getString(2)); + String status = rs.getString(3); + step.setStatus(BatchStatus.getStatus(status)); + step.setRestartData( + new GenericRestartData(PropertiesConverter.stringToProperties(rs.getString(3)))); + return step; + } + }; + + return jdbcTemplate.query(FIND_STEPS, parameters, rowMapper); + } + + /** + * Create a step with the given job's id, and the provided step name. A + * unique id is created for the step using an incrementer. (@link + * DataFieldMaxValueIncrementer) + * + * @see StepDao#createStep(JobInstance, String) + * @throws IllegalArgumentException if job or stepName is null. + */ + public StepInstance createStep(JobInstance job, String stepName) { + + Assert.notNull(job, "Job cannot be null."); + Assert.notNull(stepName, "StepName cannot be null."); + + Long stepId = new Long(stepIncrementer.nextLongValue()); + Object[] parameters = new Object[] { stepId, job.getId(), stepName }; + jdbcTemplate.update(CREATE_STEP, parameters); + + StepInstance step = new StepInstance(stepId); + step.setJob(job); + step.setName(stepName); + return step; + } + + /** + * @see StepDao#update(StepInstance) + * @throws IllegalArgumentException if step, or it's status and id is null. + */ + public void update(final StepInstance step) { + + Assert.notNull(step, "Step cannot be null."); + Assert.notNull(step.getStatus(), "Step status cannot be null."); + Assert.notNull(step.getId(), "Step Id cannot be null."); + + Properties restartProps = null; + RestartData restartData = step.getRestartData(); + if (restartData != null) { + restartProps = restartData.getProperties(); + } + + Object[] parameters = new Object[]{ step.getStatus().toString(), + PropertiesConverter.propertiesToString(restartProps), + step.getId() + }; + + jdbcTemplate.update(UPDATE_STEP, parameters); + } + + /** + * Save a StepExecution. A unique id will be generated by the + * stepExecutionIncrementor, and then set in the StepExecution. All values + * will then be stored via an INSERT statement. + * + * @see StepDao#save(StepExecution) + */ + public void save(StepExecution stepExecution) { + + validateStepExecution(stepExecution); + + stepExecution.setId(new Long(stepExecutionIncrementer.nextLongValue())); + Object[] parameters = new Object[] { stepExecution.getId(), new Long(0), stepExecution.getStepId(), stepExecution.getJobExecutionId(), + stepExecution.getStartTime(), stepExecution.getEndTime(), stepExecution.getStatus().toString(), + stepExecution.getCommitCount(), stepExecution.getTaskCount(), + PropertiesConverter.propertiesToString(stepExecution.getStatistics()), new Integer(stepExecution.getExitCode()) }; + jdbcTemplate.update(SAVE_STEP_EXECUTION, parameters); + + } + + /** + * @see StepDao#update(StepExecution) + */ + public void update(StepExecution stepExecution) { + + validateStepExecution(stepExecution); + Assert.notNull(stepExecution.getId(), "StepExecution Id cannot be null. StepExecution must saved" + + " before it can be updated."); + + // TODO: Not sure if this is a good idea on step execution considering + // it is saved at every commit + // point. + // if (jdbcTemplate.queryForInt(CHECK_STEP_EXECUTION_EXISTS, new + // Object[] { stepExecution.getId() }) != 1) { + // return; // throw exception? + // } + + Object[] parameters = new Object[] { stepExecution.getStartTime(), stepExecution.getEndTime(), + stepExecution.getStatus().toString(), stepExecution.getCommitCount(), + stepExecution.getTaskCount(), PropertiesConverter.propertiesToString(stepExecution.getStatistics()), + new Integer(stepExecution.getExitCode()), + stepExecution.getId() }; + jdbcTemplate.update(UPDATE_STEP_EXECUTION, parameters); + + } + + public int getStepExecutionCount(Long stepId) { + + Object[] parameters = new Object[] { stepId }; + + return jdbcTemplate.queryForInt(GET_STEP_EXECUTION_COUNT, parameters); + } + + /** + * Get StepExecution for the given step. Due to the nature of statistics, + * they will not be returned with reconstituted object. + * + * @see StepDao#getStepExecution(Long) + * @throws IllegalArgumentException if id is null. + * @throws NoSuchBatchDomainObjectException if more than one step execution is + * returned. + */ + public List findStepExecutions(StepInstance step) { + + Assert.notNull(step, "Step cannot be null."); + Assert.notNull(step.getId(), "Step id cannot be null."); + + final Long stepId = step.getId(); + + RowMapper rowMapper = new RowMapper() { + public Object mapRow(ResultSet rs, int rowNum) throws SQLException { + + StepExecution stepExecution = new StepExecution(stepId, new Long(rs.getLong(2))); + stepExecution.setId(new Long(rs.getLong(1))); + stepExecution.setStartTime(rs.getTimestamp(3)); + stepExecution.setEndTime(rs.getTimestamp(4)); + stepExecution.setStatus(BatchStatus.getStatus(rs.getString(5))); + stepExecution.setCommitCount(rs.getInt(6)); + stepExecution.setTaskCount(rs.getInt(7)); + stepExecution.setStatistics(PropertiesConverter.stringToProperties(rs.getString(8))); + stepExecution.setExitCode(rs.getInt(9)); + return stepExecution; + } + }; + + return jdbcTemplate.query(FIND_STEP_EXECUTIONS, new Object[] { stepId }, rowMapper); + + } + + public void setJdbcTemplate(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + public void setStepIncrementer(DataFieldMaxValueIncrementer stepIncrementer) { + this.stepIncrementer = stepIncrementer; + } + + public void setStepExecutionIncrementer(DataFieldMaxValueIncrementer stepExecutionIncrementer) { + this.stepExecutionIncrementer = stepExecutionIncrementer; + } + + public void afterPropertiesSet() throws Exception { + Assert.notNull(jdbcTemplate, "JdbcTemplate cannot be null."); + Assert.notNull(stepIncrementer, "StepIncrementer cannot be null."); + Assert.notNull(stepExecutionIncrementer, "StepExecutionIncrementer canot be null."); + } + + /* + * Validate StepExecution. At a minimum, JobId, StartTime, and + * Status cannot be null. EndTime can be null for an unfinished job. + * + * @param jobExecution @throws IllegalArgumentException + */ + private void validateStepExecution(StepExecution stepExecution) { + + Assert.notNull(stepExecution); + Assert.notNull(stepExecution.getStepId(), "StepExecution Step-Id cannot be null."); + Assert.notNull(stepExecution.getStartTime(), "StepExecution start time cannot be null."); + Assert.notNull(stepExecution.getStatus(), "StepExecution status cannot be null."); + } + +} diff --git a/execution/src/main/java/org/springframework/batch/execution/repository/dao/StepDao.java b/execution/src/main/java/org/springframework/batch/execution/repository/dao/StepDao.java new file mode 100644 index 000000000..18f858083 --- /dev/null +++ b/execution/src/main/java/org/springframework/batch/execution/repository/dao/StepDao.java @@ -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.execution.repository.dao; + +import java.util.List; + +import org.springframework.batch.core.domain.JobInstance; +import org.springframework.batch.core.domain.StepExecution; +import org.springframework.batch.core.domain.StepInstance; + +/** + * Data access object for steps. + * + * TODO: Add java doc. + * + * @author Lucas Ward + * + */ +public interface StepDao { + + /** + * Find a step with the given JobId and Step Name. Return null if none + * are found. + * + * @param jobId + * @param stepName + * @return Step + */ + public StepInstance findStep(JobInstance job, String stepName); + + /** + * Find all steps with the given Job ID. + * + * @param jobId + * @return + */ + public List findSteps(Long jobId); + + /** + * Create a step for the given Step Name and Job Id. + * @param job + * @param stepName + * + * @return + */ + public StepInstance createStep(JobInstance job, String stepName); + + /** + * Update an existing Step. + * + * Preconditions: Step must have an ID. + * + * @param job + */ + public void update(StepInstance step); + + /** + * Save the given StepExecution. + * + * Preconditions: Id must be null. Postconditions: Id will be set to a + * Unique Long. + * + * @param stepExecution + */ + public void save(StepExecution stepExecution); + + /** + * Update the given StepExecution + * + * Preconditions: Id must not be null. + * + * @param stepExecution + */ + public void update(StepExecution stepExecution); + + /** + * Return the count of StepExecutions with the given StepId. + * + * @param stepId + * @return + */ + public int getStepExecutionCount(Long stepId); + + /** + * Return all StepExecutions for the given step. + * + * @param id + * @return list of stepExecutions + */ + public List findStepExecutions(StepInstance step); +} diff --git a/execution/src/main/java/org/springframework/batch/execution/repository/dao/package.html b/execution/src/main/java/org/springframework/batch/execution/repository/dao/package.html new file mode 100644 index 000000000..480b83c4f --- /dev/null +++ b/execution/src/main/java/org/springframework/batch/execution/repository/dao/package.html @@ -0,0 +1,7 @@ + + +

+Specific implementations of dao concerns. +

+ + diff --git a/execution/src/main/java/org/springframework/batch/execution/repository/package.html b/execution/src/main/java/org/springframework/batch/execution/repository/package.html new file mode 100644 index 000000000..d8f255db5 --- /dev/null +++ b/execution/src/main/java/org/springframework/batch/execution/repository/package.html @@ -0,0 +1,7 @@ + + +

+Specific implementations of repository concerns. +

+ + diff --git a/execution/src/main/java/org/springframework/batch/execution/runtime/ScheduledJobIdentifier.java b/execution/src/main/java/org/springframework/batch/execution/runtime/ScheduledJobIdentifier.java new file mode 100644 index 000000000..49df08bf0 --- /dev/null +++ b/execution/src/main/java/org/springframework/batch/execution/runtime/ScheduledJobIdentifier.java @@ -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.execution.runtime; + +import java.util.Date; + +import org.apache.commons.lang.builder.EqualsBuilder; +import org.apache.commons.lang.builder.HashCodeBuilder; +import org.springframework.batch.core.runtime.JobIdentifier; +import org.springframework.batch.core.runtime.SimpleJobIdentifier; + +public class ScheduledJobIdentifier extends SimpleJobIdentifier implements JobIdentifier { + + private Date scheduleDate = new Date(0); + + private int jobRun = 0; + + private String jobStream = ""; + + ScheduledJobIdentifier() {} + + public ScheduledJobIdentifier(String name) { + super(name); + } + + public int getJobRun() { + return jobRun; + } + + public void setJobRun(int jobRun) { + this.jobRun = jobRun; + } + + public String getJobStream() { + return jobStream; + } + + public void setJobStream(String jobStream) { + this.jobStream = jobStream; + } + + public Date getScheduleDate() { + return scheduleDate; + } + + public void setScheduleDate(Date scheduleDate) { + this.scheduleDate = scheduleDate; + } + + public String toString() { + + return super.toString() + ",stream=" + jobStream + ",run=" + jobRun + ",scheduleDate=" + + scheduleDate; + } + + /** + * Returns true if the provided JobIdentifier equals this JobIdentifier. Two + * Identifiers are considered to be equal if they have the same name, + * stream, run, and schedule date. + */ + public boolean equals(Object other) { + return EqualsBuilder.reflectionEquals(this, other); + } + + public int hashCode() { + return HashCodeBuilder.reflectionHashCode(this); + } + +} diff --git a/execution/src/main/java/org/springframework/batch/execution/runtime/ScheduledJobIdentifierFactory.java b/execution/src/main/java/org/springframework/batch/execution/runtime/ScheduledJobIdentifierFactory.java new file mode 100644 index 000000000..5b0d8a3f1 --- /dev/null +++ b/execution/src/main/java/org/springframework/batch/execution/runtime/ScheduledJobIdentifierFactory.java @@ -0,0 +1,60 @@ +/* + * 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.execution.runtime; + +import java.util.Date; + +import org.springframework.batch.core.runtime.JobIdentifier; +import org.springframework.batch.core.runtime.JobIdentifierFactory; + +/** + * {@link JobIdentifierFactory} for creating {@link ScheduledJobIdentifier} + * instances. + * + * @author Dave Syer + * + */ +public class ScheduledJobIdentifierFactory implements JobIdentifierFactory { + + private String jobStream = "stream"; + + private int jobRun = 0; + + private Date scheduleDate = new Date(0L); + + public JobIdentifier getJobIdentifier(String name) { + + ScheduledJobIdentifier runtimeInformation = new ScheduledJobIdentifier(name); + runtimeInformation.setJobStream(jobStream); + runtimeInformation.setJobRun(jobRun); + runtimeInformation.setScheduleDate(scheduleDate); + return runtimeInformation; + } + + public void setJobRun(int jobRun) { + this.jobRun = jobRun; + } + + public void setJobStream(String jobStream) { + this.jobStream = jobStream; + } + + public void setScheduleDate(Date scheduleDate) { + this.scheduleDate = scheduleDate; + } + +} diff --git a/execution/src/main/java/org/springframework/batch/execution/runtime/package.html b/execution/src/main/java/org/springframework/batch/execution/runtime/package.html new file mode 100644 index 000000000..67504b2c5 --- /dev/null +++ b/execution/src/main/java/org/springframework/batch/execution/runtime/package.html @@ -0,0 +1,7 @@ + + +

+Specific implementations of runtime concerns. +

+ + diff --git a/execution/src/main/java/org/springframework/batch/execution/scope/SimpleStepContext.java b/execution/src/main/java/org/springframework/batch/execution/scope/SimpleStepContext.java new file mode 100644 index 000000000..14ff81220 --- /dev/null +++ b/execution/src/main/java/org/springframework/batch/execution/scope/SimpleStepContext.java @@ -0,0 +1,139 @@ +/* + * 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.execution.scope; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.batch.core.runtime.JobIdentifier; +import org.springframework.batch.repeat.context.SynchronizedAttributeAccessor; + +/** + * Simple implementation of {@link StepContext}. + * + * @author Dave Syer + * + */ +public class SimpleStepContext extends SynchronizedAttributeAccessor implements StepContext { + + private Map callbacks = new HashMap(); + private StepContext parent; + private JobIdentifier jobIdentifier; + + /** + * Default constructor. + */ + public SimpleStepContext() { + this(null); + } + + /** + * @param object + */ + public SimpleStepContext(StepContext parent) { + super(); + this.parent = parent; + } + + /* (non-Javadoc) + * @see org.springframework.batch.execution.scope.StepContext#getParent() + */ + public StepContext getParent() { + return parent; + } + + /* + * (non-Javadoc) + * @see org.springframework.batch.repeat.RepeatContext#registerDestructionCallback(java.lang.String, + * java.lang.Runnable) + */ + /* (non-Javadoc) + * @see org.springframework.batch.execution.scope.StepContext#registerDestructionCallback(java.lang.String, java.lang.Runnable) + */ + public void registerDestructionCallback(String name, Runnable callback) { + synchronized (callbacks) { + Set set = (Set) callbacks.get(name); + if (set == null) { + set = new HashSet(); + callbacks.put(name, set); + } + set.add(callback); + } + } + + /* + * Package access because only needed internally. + */ + void close() { + + List errors = new ArrayList(); + + Set copy; + + synchronized (callbacks) { + copy = new HashSet(callbacks.entrySet()); + } + + for (Iterator iter = copy.iterator(); iter.hasNext();) { + Map.Entry entry = (Map.Entry) iter.next(); + String name = (String) entry.getKey(); + Set set = (Set) entry.getValue(); + for (Iterator iterator = set.iterator(); iterator.hasNext();) { + Runnable callback = (Runnable) iterator.next(); + if (hasAttribute(name) && callback != null) { + /* + * The documentation of the interface says that these + * callbacks must not throw exceptions, but we don't trust + * them necessarily... + */ + try { + callback.run(); + } + catch (RuntimeException t) { + errors.add(t); + } + } + } + } + + if (errors.isEmpty()) { + return; + } + + throw (RuntimeException) errors.get(0); + } + + + /** + * @param jobIdentifier + */ + public void setJobIdentifier(JobIdentifier jobIdentifier) { + this.jobIdentifier = jobIdentifier; + } + + /* (non-Javadoc) + * @see org.springframework.batch.execution.scope.StepContext#getJobIdentifier() + */ + public JobIdentifier getJobIdentifier() { + return jobIdentifier; + } + +} diff --git a/execution/src/main/java/org/springframework/batch/execution/scope/StepContext.java b/execution/src/main/java/org/springframework/batch/execution/scope/StepContext.java new file mode 100644 index 000000000..c448a71a0 --- /dev/null +++ b/execution/src/main/java/org/springframework/batch/execution/scope/StepContext.java @@ -0,0 +1,48 @@ +/* + * 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.execution.scope; + +import org.springframework.batch.core.runtime.JobIdentifier; +import org.springframework.core.AttributeAccessor; + +/** + * Interface for step-scoped context object and step-scoped services. + * + * @author Dave Syer + * + */ +public interface StepContext extends AttributeAccessor { + + /** + * Accessor for the {@link JobIdentifier} associated with the currently + * executing step. + * + * @return the {@link JobIdentifier} associated with the current step + */ + JobIdentifier getJobIdentifier(); + + /** + * Accessor for the parent context. + * + * @return the parent of this context (or null if there isn't one) + */ + StepContext getParent(); + + /** + * Register a destruction callback for the end of life of the scope. + */ + void registerDestructionCallback(String name, Runnable callback); +} \ No newline at end of file diff --git a/execution/src/main/java/org/springframework/batch/execution/scope/StepContextAware.java b/execution/src/main/java/org/springframework/batch/execution/scope/StepContextAware.java new file mode 100644 index 000000000..98c273e18 --- /dev/null +++ b/execution/src/main/java/org/springframework/batch/execution/scope/StepContextAware.java @@ -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.execution.scope; + +import org.springframework.batch.repeat.RepeatContext; +import org.springframework.core.AttributeAccessor; + +/** + * Marker interface for beans to be injected with a {@link RepeatContext}. + * Useful for business logic implementations that want to store some state in + * the context, to communicate between iterations, or with an enclosing + * interceptor. + * + * @author Dave Syer + * + */ +public interface StepContextAware { + + /** + * Callback for injection of {@link RepeatContext}. + * + * @param context the current context supplied by framework. + */ + void setStepScopeContext(AttributeAccessor context); +} diff --git a/execution/src/main/java/org/springframework/batch/execution/scope/StepScope.java b/execution/src/main/java/org/springframework/batch/execution/scope/StepScope.java new file mode 100644 index 000000000..27af7e8b0 --- /dev/null +++ b/execution/src/main/java/org/springframework/batch/execution/scope/StepScope.java @@ -0,0 +1,138 @@ +/* + * 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.execution.scope; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.beans.factory.config.Scope; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; + +/** + * Scope for step context. Objects in this scope with <aop:scoped-proxy/> + * use the Spring container as an object factory, so there is only one instance + * of such a bean per executing step. + * + * @author Dave Syer + * + */ +public class StepScope implements Scope, BeanFactoryAware, BeanPostProcessor { + + /** + * Context key for clients to use for conversation identifier. + */ + public static final String ID_KEY = "JOB_IDENTIFIER"; + + /** + * Injection callback for BeanFactory. Ensures that the bean factory + * contains a BeanPostProcessor of this type (so if this bean is an inner + * bean it will still be applied as a post processor). + * + * @see org.springframework.beans.factory.BeanFactoryAware#setBeanFactory(org.springframework.beans.factory.BeanFactory) + */ + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + if (beanFactory instanceof DefaultListableBeanFactory) { + DefaultListableBeanFactory listable = (DefaultListableBeanFactory) beanFactory; + if (listable.getBeanNamesForType(getClass()).length == 0) { + listable.addBeanPostProcessor(this); + } + } + } + + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.config.Scope#get(java.lang.String, + * org.springframework.beans.factory.ObjectFactory) + */ + public Object get(String name, ObjectFactory objectFactory) { + SimpleStepContext context = getContext(); + Object scopedObject = context.getAttribute(name); + if (scopedObject == null) { + scopedObject = objectFactory.getObject(); + context.setAttribute(name, scopedObject); + } + return scopedObject; + } + + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.config.Scope#getConversationId() + */ + public String getConversationId() { + SimpleStepContext context = getContext(); + Object id = context.getAttribute(ID_KEY); + return "" + id; + } + + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.config.Scope#registerDestructionCallback(java.lang.String, + * java.lang.Runnable) + */ + public void registerDestructionCallback(String name, Runnable callback) { + StepContext context = getContext(); + context.registerDestructionCallback(name, callback); + } + + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.config.Scope#remove(java.lang.String) + */ + public Object remove(String name) { + SimpleStepContext context = getContext(); + return context.removeAttribute(name); + } + + /** + * Get an attribute accessor in the form of a {@link SimpleStepContext} that + * can be used to store scoped bean instances. + * + * @return the current step context which we can use as a scope storage + * medium + */ + private SimpleStepContext getContext() { + SimpleStepContext context = StepSynchronizationManager.getContext(); + if (context == null) { + throw new IllegalStateException("No context holder available for step scope"); + } + return context; + } + + /** + * No-op. + * @see org.springframework.beans.factory.config.BeanPostProcessor#postProcessAfterInitialization(java.lang.Object, + * java.lang.String) + */ + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + return bean; + } + + /** + * Check for {@link StepContextAware} and set context. + * @see org.springframework.beans.factory.config.BeanPostProcessor#postProcessBeforeInitialization(java.lang.Object, + * java.lang.String) + */ + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof StepContextAware) { + SimpleStepContext context = getContext(); + ((StepContextAware) bean).setStepScopeContext(context); + } + return bean; + } + +} diff --git a/execution/src/main/java/org/springframework/batch/execution/scope/StepSynchronizationManager.java b/execution/src/main/java/org/springframework/batch/execution/scope/StepSynchronizationManager.java new file mode 100644 index 000000000..a46d59ab9 --- /dev/null +++ b/execution/src/main/java/org/springframework/batch/execution/scope/StepSynchronizationManager.java @@ -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.execution.scope; + +/** + * @author Dave Syer + * + */ +public class StepSynchronizationManager { + + private static final ThreadLocal contextHolder = new ThreadLocal(); + + /** + * Getter for the current context.. + * + * @return the current {@link SimpleStepContext} or null if there is none (if + * we are not in a step). + */ + public static SimpleStepContext getContext() { + return (SimpleStepContext) contextHolder.get(); + } + + /** + * Method for registering a context - should only be used by + * {@link StepExecutor} implementations to ensure that {@link #getContext()} + * always returns the correct value. + * + * @return a new context at the start of a batch. + */ + public static SimpleStepContext open() { + StepContext oldSession = getContext(); + SimpleStepContext context = new SimpleStepContext(oldSession); + StepSynchronizationManager.contextHolder.set(context); + return context; + } + + /** + * Method for de-registering the current context - should only be used by + * {@link StepExecutor} implementations to ensure that {@link #getContext()} + * always returns the correct value. + * + * @return the old value if there was one. + */ + public static StepContext close() { + SimpleStepContext oldSession = getContext(); + if (oldSession==null) { + return null; + } + oldSession.close(); + StepContext context = oldSession.getParent(); + StepSynchronizationManager.contextHolder.set(context); + return context; + } + + /** + * Used internally by {@link StepExecutor} implementations to clear the + * current context at the end of a batch. + * + * @return the old value if there was one. + */ + public static StepContext clear() { + StepContext context = getContext(); + StepSynchronizationManager.contextHolder.set(null); + return context; + } + +} diff --git a/execution/src/main/java/org/springframework/batch/execution/scope/package.html b/execution/src/main/java/org/springframework/batch/execution/scope/package.html new file mode 100644 index 000000000..d9e268e6d --- /dev/null +++ b/execution/src/main/java/org/springframework/batch/execution/scope/package.html @@ -0,0 +1,7 @@ + + +

+Specific implementations of scope concerns. +

+ + diff --git a/execution/src/main/java/org/springframework/batch/execution/step/DefaultStepExecutorFactory.java b/execution/src/main/java/org/springframework/batch/execution/step/DefaultStepExecutorFactory.java new file mode 100644 index 000000000..a08b0820f --- /dev/null +++ b/execution/src/main/java/org/springframework/batch/execution/step/DefaultStepExecutorFactory.java @@ -0,0 +1,144 @@ +/* + * 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.execution.step; + +import org.springframework.batch.core.configuration.StepConfiguration; +import org.springframework.batch.core.executor.StepExecutor; +import org.springframework.batch.core.executor.StepExecutorFactory; +import org.springframework.batch.execution.step.simple.SimpleStepConfiguration; +import org.springframework.batch.execution.step.simple.SimpleStepExecutor; +import org.springframework.batch.repeat.RepeatOperations; +import org.springframework.batch.repeat.policy.SimpleCompletionPolicy; +import org.springframework.batch.repeat.support.RepeatTemplate; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.util.Assert; + +/** + * A {@link StepExecutorFactory} that uses a prototype bean in the application + * context to satisfy the factory contract. If the prototype bean and + * {@link StepConfiguration} are of known (simple) type, they can be combined to + * add the commit interval information from the configuration. + * + * @author Dave Syer + * + */ +public class DefaultStepExecutorFactory implements StepExecutorFactory, BeanFactoryAware, InitializingBean { + + private String stepExecutorName = null; + + private BeanFactory beanFactory; + + /** + * Setter for injected {@link BeanFactory}. + * @see org.springframework.beans.factory.BeanFactoryAware#setBeanFactory(org.springframework.beans.factory.BeanFactory) + */ + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + this.beanFactory = beanFactory; + } + + /** + * Assert that if the step executor name is provided, then it is valid and + * is of prototype scope. + * + * @see InitializingBean#afterPropertiesSet() + */ + public void afterPropertiesSet() throws Exception { + // Make an assertion that the bean exists and is of the correct type + Assert.notNull(beanFactory.getBean(stepExecutorName, StepExecutor.class), + "Step executor name must correspond to a StepExecutor instance."); + Assert.state(beanFactory.isPrototype(stepExecutorName), + "StepExecutor must be a prototype. Change the scope of the bean named '" + stepExecutorName + + "' to prototype."); + } + + /** + * Locate a {@link StepExecutor} for this configuration, allowing different + * strategies for configuring the inner loop (chunk operations). Try the + * following in this order, until one succeeds. In each case first obtain + * the {@link StepExecutor} referred to by the {@link #stepExecutorName}, + * then: + *
    + * + *
  • If the {@link StepExecutor} refers to a {@link SimpleStepExecutor}, + * and {@link StepConfiguration} is an instance of + * {@link RepeatOperationsHolder}, then the {@link RepeatOperations} for + * the chunk will be pulled from there directly. This gives maximum + * flexibility for clients to control the properties of the iteration. For + * simple use cases where clients only need to control a few aspects of the + * execution, like the commit interval, this is not necessary.
  • + * + *
  • If the {@link StepExecutor} is a {@link SimpleStepExecutor} and the + * configuration is a {@link SimpleStepConfiguration} then this + * implementation modifies the state of the {@link StepExecutor} to set the + * completion policy of the chunk operations. In this case the chunk + * operations cannot be set by the client of this factory.
  • + * + *
  • Use the {@link StepExecutor} directly.
  • + * + *
+ *
+ * + * @throws IllegalStateException if no {@link StepExecutor} can be located. + * + * @see StepExecutorFactory#getExecutor(StepConfiguration) + */ + public StepExecutor getExecutor(StepConfiguration configuration) { + + StepExecutor executor = getStepExecutor(); + + if (executor instanceof SimpleStepExecutor) { + RepeatTemplate template = new RepeatTemplate(); + RepeatOperations repeatOperations = template; + if (configuration instanceof RepeatOperationsHolder) { + repeatOperations = ((RepeatOperationsHolder) configuration).getChunkOperations(); + Assert.state(repeatOperations != null, + "Chunk operations obtained from step configuration must be non-null."); + } + else if (configuration instanceof SimpleStepConfiguration) { + template.setCompletionPolicy(new SimpleCompletionPolicy(((SimpleStepConfiguration) configuration) + .getCommitInterval())); + } + ((SimpleStepExecutor) executor).setChunkOperations(repeatOperations); + } + + return executor; + + } + + /** + * Setter for the bean name of the {@link StepExecutor} to use. The + * corresponding bean must be prototype scoped, so that its properties can + * be overridden per execution by the {@link StepConfiguration}. + * + * @param stepExecutor the stepExecutor to set + */ + public void setStepExecutorName(String stepExecutorName) { + this.stepExecutorName = stepExecutorName; + } + + /** + * Internal convenience method to get a step executor instance. + * + * @return the step executor instance to use. + */ + private StepExecutor getStepExecutor() { + return (StepExecutor) beanFactory.getBean(stepExecutorName, StepExecutor.class); + } + +} diff --git a/execution/src/main/java/org/springframework/batch/execution/step/RepeatOperationsHolder.java b/execution/src/main/java/org/springframework/batch/execution/step/RepeatOperationsHolder.java new file mode 100644 index 000000000..8f0d5610a --- /dev/null +++ b/execution/src/main/java/org/springframework/batch/execution/step/RepeatOperationsHolder.java @@ -0,0 +1,42 @@ +/* + * 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.execution.step; + +import org.springframework.batch.core.configuration.StepConfiguration; +import org.springframework.batch.core.executor.StepExecutor; +import org.springframework.batch.repeat.RepeatOperations; + +/** + * Marker interface for indicating that a {@link RepeatOperations} instance is + * available for the inner loop (chunk operations) in a {@link StepExecutor}. + * The inner loop is normally going to be in-process and thread-bound so it + * makes sense for {@link StepConfiguration} implementations to be able to + * override the strategies that control that loop. + * + * @author Dave Syer + * + */ +public interface RepeatOperationsHolder { + + /** + * Principal method in the {@link RepeatOperationsHolder} interface. + * + * @return a {@link RepeatOperations} which can be used to iterate over an + * inner loop (chunk). + */ + RepeatOperations getChunkOperations(); + +} diff --git a/execution/src/main/java/org/springframework/batch/execution/step/package.html b/execution/src/main/java/org/springframework/batch/execution/step/package.html new file mode 100644 index 000000000..25753d39d --- /dev/null +++ b/execution/src/main/java/org/springframework/batch/execution/step/package.html @@ -0,0 +1,7 @@ + + +

+Specific implementations of step concerns. +

+ + diff --git a/execution/src/main/java/org/springframework/batch/execution/step/simple/AbstractStepConfiguration.java b/execution/src/main/java/org/springframework/batch/execution/step/simple/AbstractStepConfiguration.java new file mode 100644 index 000000000..e2a97e640 --- /dev/null +++ b/execution/src/main/java/org/springframework/batch/execution/step/simple/AbstractStepConfiguration.java @@ -0,0 +1,91 @@ +/* + * 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.execution.step.simple; + +import org.springframework.batch.core.configuration.StepConfiguration; +import org.springframework.batch.core.configuration.StepConfigurationSupport; +import org.springframework.batch.repeat.exception.handler.ExceptionHandler; +import org.springframework.beans.factory.BeanNameAware; + +/** + * A {@link StepConfiguration} implementation that provides common behaviour to + * subclasses. Implements {@link BeanNameAware} so that if no name is provided + * explicitly it will be inferred from the bean definition in Spring + * configuration. + * + * @author Dave Syer + * + */ +public class AbstractStepConfiguration extends StepConfigurationSupport implements BeanNameAware { + + private int skipLimit = 0; + + private boolean saveRestartData = false; + + private ExceptionHandler exceptionHandler; + + /** + * Default constructor. + */ + public AbstractStepConfiguration() { + super(); + } + + /** + * Convenent constructor for setting only the name property. + * @param name + */ + public AbstractStepConfiguration(String name) { + super(name); + } + + /** + * Set the name property if it has not already been set explicitly (and is + * therefore not null). + * + * @see org.springframework.beans.factory.BeanNameAware#setBeanName(java.lang.String) + */ + public void setBeanName(String name) { + if (getName() == null) { + setName(name); + } + } + + public ExceptionHandler getExceptionHandler() { + return exceptionHandler; + } + + public void setExceptionHandler(ExceptionHandler exceptionHandler) { + this.exceptionHandler = exceptionHandler; + } + + public void setSkipLimit(int skipLimit) { + this.skipLimit = skipLimit; + } + + public int getSkipLimit() { + return skipLimit; + } + + public void setSaveRestartData(boolean saveRestartData) { + this.saveRestartData = saveRestartData; + } + + public boolean isSaveRestartData() { + return saveRestartData; + } + +} \ No newline at end of file diff --git a/execution/src/main/java/org/springframework/batch/execution/step/simple/ChunkOperationsStepConfiguration.java b/execution/src/main/java/org/springframework/batch/execution/step/simple/ChunkOperationsStepConfiguration.java new file mode 100644 index 000000000..9ff54d3e0 --- /dev/null +++ b/execution/src/main/java/org/springframework/batch/execution/step/simple/ChunkOperationsStepConfiguration.java @@ -0,0 +1,69 @@ +/* + * 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.execution.step.simple; + +import org.springframework.batch.core.configuration.StepConfiguration; +import org.springframework.batch.core.tasklet.Tasklet; +import org.springframework.batch.execution.step.RepeatOperationsHolder; +import org.springframework.batch.repeat.RepeatOperations; + +/** + * {@link StepConfiguration} implementation that allows full configuration of + * the {@link RepeatOperations} that will be used in the chunk (inner loop). + * + * @author Lucas Ward + * @author Dave Syer + * + */ +public class ChunkOperationsStepConfiguration extends AbstractStepConfiguration implements RepeatOperationsHolder { + + // default StepExecutor is null + private RepeatOperations chunkOperations; + + public ChunkOperationsStepConfiguration() { + super(); + } + + public ChunkOperationsStepConfiguration(RepeatOperations repeatOperations) { + this(); + this.chunkOperations = repeatOperations; + } + + public ChunkOperationsStepConfiguration(Tasklet module) { + this(); + setTasklet(module); + } + + /** + * Public accessor for the chunkOperations property. + * + * @return the executor + */ + public RepeatOperations getChunkOperations() { + return chunkOperations; + } + + /** + * Public setter for the chunkOperations. + * + * @param chunkOperations the repeatOperations to set + */ + public void setChunkOperations(RepeatOperations chunkOperations) { + this.chunkOperations = chunkOperations; + } + +} diff --git a/execution/src/main/java/org/springframework/batch/execution/step/simple/DefaultStepExecutor.java b/execution/src/main/java/org/springframework/batch/execution/step/simple/DefaultStepExecutor.java new file mode 100644 index 000000000..171810ed2 --- /dev/null +++ b/execution/src/main/java/org/springframework/batch/execution/step/simple/DefaultStepExecutor.java @@ -0,0 +1,86 @@ +/* + * 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.execution.step.simple; + +import org.springframework.batch.core.domain.StepInstance; +import org.springframework.batch.core.tasklet.Tasklet; +import org.springframework.batch.core.tasklet.Recoverable; +import org.springframework.batch.io.Skippable; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.DefaultTransactionDefinition; +import org.springframework.transaction.support.TransactionCallback; +import org.springframework.transaction.support.TransactionTemplate; + +/** + * Adds some recovery behaviour to {@link SimpleStepExecutor}. + * + * @author Dave Syer + * + */ +public class DefaultStepExecutor extends SimpleStepExecutor { + + /** + * Extends {@link SimpleStepExecutor#doTaskletProcessing(Tasklet, StepInstance)} to + * add some basic recovery behaviour. If the {@link Tasklet} implements + * {@link Recoverable} and {@link Skippable} then the recovery + * and skip methods are called. The recovery is done in a new transaction, + * started with propagation + * {@link TransactionDefinition#PROPAGATION_REQUIRES_NEW} so that the + * inevitable rollback on the main processing loop does not cause the + * recovery to roll back as well. + * + * @throws Exception whenever {@link SimpleStepExecutor} would, but takes + * the recovery path first. + * + * @see org.springframework.batch.execution.step.simple.SimpleStepExecutor#doTaskletProcessing(org.springframework.batch.core.tasklet.Tasklet, + * org.springframework.batch.core.domain.StepInstance) + */ + protected boolean doTaskletProcessing(Tasklet module, final StepInstance step) throws Exception { + + boolean result = true; + + try { + + result = super.doTaskletProcessing(module, step); + + } + catch (final Exception e) { + + if (module instanceof Recoverable && module instanceof Skippable) { + final Recoverable recoverable = (Recoverable) module; + new TransactionTemplate(transactionManager, new DefaultTransactionDefinition( + TransactionDefinition.PROPAGATION_REQUIRES_NEW)).execute(new TransactionCallback() { + public Object doInTransaction(TransactionStatus status) { + recoverable.recover(e); + return null; + } + }); + } + if (module instanceof Skippable) { + ((Skippable) module).skip(); + } + + // Rethrow so that outer transaction is rolled back properly + throw e; + + } + + return result; + } + +} diff --git a/execution/src/main/java/org/springframework/batch/execution/step/simple/SimpleStepConfiguration.java b/execution/src/main/java/org/springframework/batch/execution/step/simple/SimpleStepConfiguration.java new file mode 100644 index 000000000..b6bdd667c --- /dev/null +++ b/execution/src/main/java/org/springframework/batch/execution/step/simple/SimpleStepConfiguration.java @@ -0,0 +1,56 @@ +/* + * 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.execution.step.simple; + +import org.springframework.batch.core.configuration.StepConfiguration; +import org.springframework.batch.core.tasklet.Tasklet; + +/** + * Simple {@link StepConfiguration} good enough for most purposes and easy to + * configure simple properties, principally the commit interval. + * + * @author Lucas Ward + * @author Dave Syer + * + */ +public class SimpleStepConfiguration extends AbstractStepConfiguration { + + // default commit interval is one + private int commitInterval = 1; + + public SimpleStepConfiguration() { + super(); + } + + public SimpleStepConfiguration(String name) { + super(name); + } + + public SimpleStepConfiguration(Tasklet module) { + this(); + setTasklet(module); + } + + public void setCommitInterval(int commitInterval) { + this.commitInterval = commitInterval; + } + + public int getCommitInterval() { + return commitInterval; + } + +} diff --git a/execution/src/main/java/org/springframework/batch/execution/step/simple/SimpleStepExecutor.java b/execution/src/main/java/org/springframework/batch/execution/step/simple/SimpleStepExecutor.java new file mode 100644 index 000000000..930b5c6c9 --- /dev/null +++ b/execution/src/main/java/org/springframework/batch/execution/step/simple/SimpleStepExecutor.java @@ -0,0 +1,378 @@ +/* + * 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.execution.step.simple; + +import java.sql.Timestamp; +import java.util.Iterator; +import java.util.Properties; + +import org.springframework.batch.core.configuration.StepConfiguration; +import org.springframework.batch.core.domain.BatchStatus; +import org.springframework.batch.core.domain.StepExecution; +import org.springframework.batch.core.domain.StepInstance; +import org.springframework.batch.core.executor.StepExecutor; +import org.springframework.batch.core.executor.StepInterruptedException; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.runtime.JobExecutionContext; +import org.springframework.batch.core.runtime.StepExecutionContext; +import org.springframework.batch.core.tasklet.Tasklet; +import org.springframework.batch.execution.scope.StepScope; +import org.springframework.batch.execution.scope.SimpleStepContext; +import org.springframework.batch.execution.scope.StepSynchronizationManager; +import org.springframework.batch.io.exception.BatchCriticalException; +import org.springframework.batch.repeat.ExitStatus; +import org.springframework.batch.repeat.RepeatCallback; +import org.springframework.batch.repeat.RepeatContext; +import org.springframework.batch.repeat.RepeatOperations; +import org.springframework.batch.repeat.support.RepeatTemplate; +import org.springframework.batch.repeat.synch.BatchTransactionSynchronizationManager; +import org.springframework.batch.restart.RestartData; +import org.springframework.batch.restart.Restartable; +import org.springframework.batch.statistics.StatisticsProvider; +import org.springframework.batch.support.transaction.ResourcelessTransactionManager; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.TransactionCallback; +import org.springframework.transaction.support.TransactionTemplate; +import org.springframework.util.Assert; + +/** + * Simple implementation of {@link StepExecutor} executing the step as a set of + * chunks, each chunk surrounded by a transaction. The structure is therefore + * that of two nested loops, with transaction boundary around the whole inner + * loop. The outer loop is controlled by the step operations ({@link #setStepOperations(RepeatOperations)}), + * and the inner loop by the chunk operations ({@link #setChunkOperations(RepeatOperations)}). + * The inner loop should always be executed in a single thread, so the chunk + * operations should not do any concurrent execution. N.B. usually that means + * that the chunk operations should be a {@link RepeatTemplate} (which is the + * default).
+ * + * Clients can use interceptors in the step operations to intercept or listen to + * the iteration on a step-wide basis, for instance to get a callback when the + * step is complete. Those that want callbacks at the level of an individual + * tasks, can specify interceptors for the chunk operations. + * + * @author Dave Syer + * @author Lucas Ward + * + */ +public class SimpleStepExecutor implements StepExecutor { + + /** + * Key placed in step scope context to identify the + * {@link StepExecutionContext}. + */ + public static final String STEP_KEY = "STEP"; + + /** + * Context attribute key for step execution. Used by monitoring and managing + * clients to inspect current step execution. + */ + private static final String STEP_EXECUTION_KEY = "STEP_EXECUTION"; + + /** + * Attribute key for statistics instance in step context. + */ + public static final String STATISTICS_KEY = "STATISTICS"; + + private RepeatOperations chunkOperations = new RepeatTemplate(); + + private RepeatOperations stepOperations = new RepeatTemplate(); + + private JobRepository jobRepository; + + // default to checking current thread for interruption. + private StepInterruptionPolicy interruptionPolicy = new ThreadStepInterruptionPolicy(); + + // Not for production use... + protected PlatformTransactionManager transactionManager = new ResourcelessTransactionManager(); + + public void setTransactionManager(PlatformTransactionManager transactionManager) { + this.transactionManager = transactionManager; + } + + /** + * Injected strategy for storage and retrieval of persistent step + * information. Mandatory property. + * @param jobRepository + */ + public void setRepository(JobRepository jobRepository) { + this.jobRepository = jobRepository; + } + + /** + * The {@link RepeatOperations} to use for the outer loop of the batch + * processing. Should be set up by the caller through a factory. Defaults to + * a plain {@link RepeatTemplate}. + * @param stepOperations a {@link RepeatOperations} instance. + */ + public void setStepOperations(RepeatOperations stepOperations) { + this.stepOperations = stepOperations; + } + + /** + * The {@link RepeatOperations} to use for the inner loop of the batch + * processing. Should be set up by the caller through a factory. Defaults to + * a plain {@link RepeatTemplate}. + * @param chunkOperations a {@link RepeatOperations} instance. + */ + public void setChunkOperations(RepeatOperations chunkOperations) { + this.chunkOperations = chunkOperations; + } + + /** + * Process the step and update its context so that progress can be monitored + * by the caller. The step is broken down into chunks, each one executing in + * a transaction. The step and its execution and execution context are all + * given an up to date {@link BatchStatus}, and the {@link JobRepository} + * is used to store the result. Various reporting information are also added + * to the current context (the {@link RepeatContext} governing the step + * execution, which would normally be available to the caller somehow + * through the step's {@link JobExecutionContext}. + * @throws StepInterruptedException if the step or a chunk is interrupted + * @throws RuntimeException if there is an exception during a chunk + * execution + * @see StepExecutor#process(StepConfiguration, StepExecutionContext) + */ + public ExitStatus process(final StepConfiguration configuration, final StepExecutionContext stepExecutionContext) + throws BatchCriticalException, StepInterruptedException { + + final StepInstance step = stepExecutionContext.getStep(); + Assert.notNull(step); + + final StepExecution stepExecution = stepExecutionContext.getStepExecution(); + final Tasklet module = configuration.getTasklet(); + step.setStepExecution(stepExecution); + + ExitStatus status = ExitStatus.FAILED; + + final SimpleStepContext stepScopeContext = StepSynchronizationManager.open(); + + try { + stepExecution.setStartTime(new Timestamp(System.currentTimeMillis())); + updateStatus(stepExecutionContext, BatchStatus.STARTED); + + final boolean shouldPersistRestartData = ((AbstractStepConfiguration) configuration).isSaveRestartData(); + + if (shouldPersistRestartData) { + restoreFromRestartData(module, step.getRestartData()); + } + + status = stepOperations.iterate(new RepeatCallback() { + + public ExitStatus doInIteration(final RepeatContext context) throws Exception { + + stepExecutionContext.getJobExecutionContext().registerStepContext(context); + context.registerDestructionCallback("STEP_EXECUTION_CONTEXT_CALLBACK", new Runnable() { + public void run() { + stepExecutionContext.getJobExecutionContext().unregisterStepContext(context); + } + }); + stepScopeContext.setJobIdentifier(stepExecutionContext.getJobExecutionContext().getJobIdentifier()); + context.setAttribute(StepScope.ID_KEY, stepExecutionContext.getJobExecutionContext() + .getJobIdentifier()); + // Mark the context as a step context as a hint to scope + // implementations. + context.setAttribute(STEP_KEY, stepExecutionContext); + // Add the step execution as an attribute so monitoring + // clients can see it. + context.setAttribute(STEP_EXECUTION_KEY, stepExecution); + // Before starting a new transaction, check for + // interruption. + interruptionPolicy.checkInterrupted(context); + + ExitStatus result = (ExitStatus) new TransactionTemplate(transactionManager) + .execute(new TransactionCallback() { + public Object doInTransaction(TransactionStatus status) { + // New transaction obtained, resynchronize + // TransactionSyncrhonization objects + BatchTransactionSynchronizationManager.resynchronize(); + ExitStatus result; + + try { + result = processChunk(configuration, stepExecutionContext); + } + catch (Throwable t) { + /* + * any exception thrown within the + * transaction template will + * automatically cause the transaction + * to rollback + */ + stepExecution.incrementRollbackCount(); + if (t instanceof RuntimeException) { + throw (RuntimeException) t; + } + else { + throw new RuntimeException(t); + } + } + + if (shouldPersistRestartData) { + step.setRestartData(getRestartData(module)); + jobRepository.update(step); + } + Properties statistics = getStatistics(module); + stepExecution.setStatistics(statistics); + context.setAttribute(STATISTICS_KEY, statistics); + stepExecution.incrementCommitCount(); + jobRepository.saveOrUpdate(stepExecution); + return result; + } + }); + + // Check for interruption after transaction as well, so that + // the interrupted exception is correctly propagated up to + // caller + interruptionPolicy.checkInterrupted(context); + + return result; + + } + + }); + + stepExecution.setExitCode(status.getExitCode()); + updateStatus(stepExecutionContext, BatchStatus.COMPLETED); + return status; + } + catch (RuntimeException e) { + if (e.getCause() instanceof StepInterruptedException) { + updateStatus(stepExecutionContext, BatchStatus.STOPPED); + throw (StepInterruptedException) e.getCause(); + } + else { + updateStatus(stepExecutionContext, BatchStatus.FAILED); + throw e; + } + } + finally { + stepExecution.setEndTime(new Timestamp(System.currentTimeMillis())); + try { + jobRepository.saveOrUpdate(stepExecution); + } + finally { + // clear any registered synchronizations + try { + StepSynchronizationManager.close(); + } + finally { + BatchTransactionSynchronizationManager.clearSynchronizations(); + } + } + } + + } + + /** + * Convenience method to update the status in all relevant places. + * @param step the current step + * @param stepExecution the current stepExecution + * @param status the status to set + */ + private void updateStatus(StepExecutionContext stepExecutionContext, BatchStatus status) { + StepInstance step = stepExecutionContext.getStep(); + StepExecution stepExecution = stepExecutionContext.getStepExecution(); + stepExecution.setStatus(status); + step.setStatus(status); + jobRepository.update(step); + jobRepository.saveOrUpdate(stepExecution); + for (Iterator iter = stepExecutionContext.getJobExecutionContext().getStepContexts().iterator(); iter.hasNext();) { + RepeatContext context = (RepeatContext) iter.next(); + context.setAttribute("JOB_STATUS", status); + } + } + + /** + * Execute a bunch of identical business logic operations all within a + * transaction. The transaction is programmatically started and stopped + * outside this method, so subclasses that override do not need to create a + * transaction. + * + * @param configuration the current step configuration + * @param stepExecutionContext the current step, containing the + * {@link Tasklet} with the business logic. + * @return true if there is more data to process. + */ + protected final ExitStatus processChunk(final StepConfiguration configuration, + final StepExecutionContext stepExecutionContext) { + return chunkOperations.iterate(new RepeatCallback() { + public ExitStatus doInIteration(final RepeatContext context) throws Exception { + stepExecutionContext.getJobExecutionContext().registerChunkContext(context); + context.registerDestructionCallback("CHUNK_EXECUTION_CONTEXT_CALLBACK", new Runnable() { + public void run() { + stepExecutionContext.getJobExecutionContext().unregisterStepContext(context); + } + }); + // check for interruption before each item as well + interruptionPolicy.checkInterrupted(context); + boolean result = doTaskletProcessing(configuration.getTasklet(), stepExecutionContext.getStep()); + stepExecutionContext.getStepExecution().incrementTaskCount(); + // check for interruption after each item as well + interruptionPolicy.checkInterrupted(context); + return new ExitStatus(result); + } + }); + } + + /** + * Execute the business logic, delegating to the given {@link Tasklet}. + * Subclasses could extend the behaviour as long as they always return the + * value of this method call in their superclass. + * @param tasklet the unit of business logic to execute + * @param step the current step + * @return boolean if there is more processing to do + * @throws Exception if there is an error + */ + protected boolean doTaskletProcessing(Tasklet tasklet, StepInstance step) throws Exception { + return tasklet.execute(); + } + + private RestartData getRestartData(Tasklet module) { + if (module instanceof Restartable) { + return ((Restartable) module).getRestartData(); + } + else { + return null; + } + } + + private void restoreFromRestartData(Tasklet tasklet, RestartData restartData) { + if (tasklet instanceof Restartable && restartData != null) { + ((Restartable) tasklet).restoreFrom(restartData); + } + } + + private Properties getStatistics(Tasklet tasklet) { + if (tasklet instanceof StatisticsProvider) { + return ((StatisticsProvider) tasklet).getStatistics(); + } + else { + return null; + } + } + + /** + * Setter for the {@link StepInterruptionPolicy}. The policy is used to + * check whether an external request has been made to interrupt the job + * execution. + * @param interruptionPolicy a {@link StepInterruptionPolicy} + */ + public void setInterruptionPolicy(StepInterruptionPolicy interruptionPolicy) { + this.interruptionPolicy = interruptionPolicy; + } +} diff --git a/execution/src/main/java/org/springframework/batch/execution/step/simple/StepInterruptionPolicy.java b/execution/src/main/java/org/springframework/batch/execution/step/simple/StepInterruptionPolicy.java new file mode 100644 index 000000000..8ed02dfd9 --- /dev/null +++ b/execution/src/main/java/org/springframework/batch/execution/step/simple/StepInterruptionPolicy.java @@ -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.execution.step.simple; + +import org.springframework.batch.core.executor.StepExecutor; +import org.springframework.batch.core.executor.StepInterruptedException; +import org.springframework.batch.repeat.RepeatContext; + +/** + * Strategy interface for an interruption policy. This policy allows + * {@link StepExecutor} implementations to check if a job has been interrupted. + * + * @author Lucas Ward + * + */ +public interface StepInterruptionPolicy { + + /** + * Has the job been interrupted? If so then throw a + * {@link StepInterruptedException}. + * @param context the current context of the running step. + * + * @throws StepInterruptedException when the job has been interrupted. + */ + void checkInterrupted(RepeatContext context) throws StepInterruptedException; +} diff --git a/execution/src/main/java/org/springframework/batch/execution/step/simple/ThreadStepInterruptionPolicy.java b/execution/src/main/java/org/springframework/batch/execution/step/simple/ThreadStepInterruptionPolicy.java new file mode 100644 index 000000000..bdbd24636 --- /dev/null +++ b/execution/src/main/java/org/springframework/batch/execution/step/simple/ThreadStepInterruptionPolicy.java @@ -0,0 +1,52 @@ +/* + * 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.execution.step.simple; + +import org.springframework.batch.core.executor.StepInterruptedException; +import org.springframework.batch.repeat.RepeatContext; + +/** + * Policy that checks the current thread to see if it has been interrupted. + * + * @author Lucas Ward + * @author Dave Syer + * + */ +public class ThreadStepInterruptionPolicy implements StepInterruptionPolicy { + + /** + * Returns if the current job lifecycle has been interrupted by checking if + * the current thread is interrupted. + */ + public void checkInterrupted(RepeatContext context) throws StepInterruptedException { + + if (isInterrupted(context)) { + throw new StepInterruptedException("Job interrupted status detected."); + } + + } + + /** + * TODO: add more interruption policies: they should all check context.isTerminateOnly() + * @param context the current context + * @return true if the job has been interrupted + */ + private boolean isInterrupted(RepeatContext context) { + return Thread.currentThread().isInterrupted() || context.isTerminateOnly(); + } + +} diff --git a/execution/src/main/java/org/springframework/batch/execution/step/simple/package.html b/execution/src/main/java/org/springframework/batch/execution/step/simple/package.html new file mode 100644 index 000000000..654cd2178 --- /dev/null +++ b/execution/src/main/java/org/springframework/batch/execution/step/simple/package.html @@ -0,0 +1,7 @@ + + +

+Specific implementations of simple concerns. +

+ + diff --git a/execution/src/main/java/org/springframework/batch/execution/tasklet/ItemProviderProcessTasklet.java b/execution/src/main/java/org/springframework/batch/execution/tasklet/ItemProviderProcessTasklet.java new file mode 100644 index 000000000..2339eaed8 --- /dev/null +++ b/execution/src/main/java/org/springframework/batch/execution/tasklet/ItemProviderProcessTasklet.java @@ -0,0 +1,282 @@ +/* + * 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.execution.tasklet; + +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import org.springframework.batch.core.tasklet.Recoverable; +import org.springframework.batch.core.tasklet.Tasklet; +import org.springframework.batch.io.Skippable; +import org.springframework.batch.item.ItemProcessor; +import org.springframework.batch.item.ItemProvider; +import org.springframework.batch.repeat.RepeatContext; +import org.springframework.batch.repeat.synch.RepeatSynchronizationManager; +import org.springframework.batch.retry.RetryOperations; +import org.springframework.batch.retry.RetryPolicy; +import org.springframework.batch.retry.callback.ItemProviderRetryCallback; +import org.springframework.batch.retry.policy.ItemProviderRetryPolicy; +import org.springframework.batch.retry.support.RetryTemplate; +import org.springframework.batch.statistics.StatisticsProvider; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.util.Assert; + +/** + * A concrete implementation of the {@link Tasklet} interface that provides + * functionality for 'split processing'. This type of processing is + * characterised by separating the reading and processing of batch data into two + * separate classes: ItemProvider and DataProcessor. The ItemProvider class + * provides a solid means for re-usability and enforces good architecture + * practices. Because an object *must* be returned by the {@link ItemProvider} + * to continue processing, (returning null indicates processing should end) a + * developer is forced to read in all relevant data, place it into a domain + * object, and return that object. The {@link ItemProcessor} will then use this + * object for calculations and output.
+ * + * If a {@link RetryPolicy} is provided it will be used to construct a stateful + * retry around the {@link ItemProcessor}, delegating recover and identity + * concerns to the {@link ItemProvider}. In this case clients of this class do + * not need to take any additional action at runtime to take advantage of the + * retry and recovery, provided the {@link #execute()} method is called again + * with the {@link ItemProvider} in the same state (normally this would be the + * case because a transaction would have rolled back and the item would be + * represented).
+ * + * If neither a {@link RetryPolicy} nor a {@link RetryOperations} is provided + * then the {@link Recoverable} interface can be used to attempt to recover + * immediately (with no retry) from a processing error. Clients of this class + * must call {@link Recoverable#recover(Throwable)} directly, which is simply + * delegated to {@link ItemProvider#recover(Object, Throwable)}. + * + * @see ItemProvider + * @see ItemProcessor + * @see RetryPolicy + * @see Recoverable + * + * @author Lucas Ward + * @author Dave Syer + * @author Robert Kasanicky + * + */ +public class ItemProviderProcessTasklet implements Tasklet, Recoverable, Skippable, StatisticsProvider, + InitializingBean { + + /** + * Prefix added to statistics keys from processor if needed to avoid + * ambiguity between provider and processor. + */ + public static final String PROCESSOR_STATISTICS_PREFIX = "processor."; + + /** + * Prefix added to statistics keys from provider if needed to avoid + * ambiguity between provider and processor. + */ + public static final String PROVIDER_STATISTICS_PREFIX = "provider."; + + /** + * Attribute key in the surrounding {@link RepeatContext} for the current + * item being processed. Needed to provide recoverable behaviour if + * {@link RetryOperations} are not provided. + */ + private static final String ITEM_KEY = ItemProviderProcessTasklet.class + ".ITEM"; + + private RetryPolicy retryPolicy = null; + + private RetryOperations retryOperations = null; + + protected ItemProvider itemProvider; + + protected ItemProcessor itemProcessor; + + /** + * Check mandatory properties (provider and processor), and ensure that only + * one (or neither) of {@link RetryPolicy} or {@link RetryOperations} is + * provided. + * + * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() + */ + public void afterPropertiesSet() throws Exception { + Assert.notNull(itemProvider, "ItemProvider must be provided"); + Assert.notNull(itemProcessor, "ItemProcessor must be provided"); + Assert.state(!(retryPolicy != null && retryOperations != null), + "Either RetryOperations or RetryPolicy can be provided, but not both."); + if (retryPolicy != null) { + RetryTemplate template = new RetryTemplate(); + template.setRetryPolicy(new ItemProviderRetryPolicy(retryPolicy)); + retryOperations = template; + } + } + + /** + * Read from the {@link ItemProvider} and process (if not null) with the + * {@link ItemProcessor}. The call to {@link ItemProcessor} is wrapped in a + * retry, if either a {@link RetryPolicy} or a {@link RetryOperations} is + * provided. + * + * @see org.springframework.batch.core.tasklet.Tasklet#execute() + */ + public boolean execute() throws Exception { + if (retryOperations != null) { + return retryOperations.execute(new ItemProviderRetryCallback(itemProvider, itemProcessor)) != null; + } + else { + Object data = itemProvider.next(); + if (data == null) { + return false; + } + RepeatContext context = RepeatSynchronizationManager.getContext(); + Assert.state(context != null, + "No context available: you probably need to use this class inside a batch operation."); + context.setAttribute(ITEM_KEY, data); + itemProcessor.process(data); + // No exception so clear context (we can't recover directly because + // the current transaction is going to roll back) + context.removeAttribute(ITEM_KEY); + return true; + } + } + + /** + * Call out to the provider for recovery step. + * + * @see org.springframework.batch.core.tasklet.Recoverable#recover(java.lang.Throwable) + */ + public void recover(Throwable cause) { + RepeatContext context = RepeatSynchronizationManager.getContext(); + Assert.state(context != null, + "No context available: you probably need to use this class inside a batch operation."); + + try { + Object data = context.getAttribute(ITEM_KEY); + itemProvider.recover(data, cause); + } + finally { + context.removeAttribute(ITEM_KEY); + } + } + + /** + * @param itemProvider + */ + public void setItemProvider(ItemProvider itemProvider) { + this.itemProvider = itemProvider; + } + + /** + * @param moduleProcessor + */ + public void setItemProcessor(ItemProcessor moduleProcessor) { + this.itemProcessor = moduleProcessor; + } + + /** + * If the provider and / or processor are {@link Skippable} then delegate to + * them in that order. + * + * @see org.springframework.batch.io.Skippable#skip() + */ + public void skip() { + if (this.itemProvider instanceof Skippable) { + ((Skippable) this.itemProvider).skip(); + } + if (this.itemProcessor instanceof Skippable) { + ((Skippable) this.itemProcessor).skip(); + } + } + + /** + * If the provider and / or processor are {@link StatisticsProvider} then + * delegate to them in that order. If they both implement + * {@link StatisticsProvider} then the property keys are prepended with + * special prefixes to avoid potential ambiguity. The prefixes are only + * prepended in the case of a duplicate key shared between provider and + * processor. + * + * @see org.springframework.batch.io.Skippable#skip() + */ + public Properties getStatistics() { + Properties stats = new Properties(); + if (this.itemProvider instanceof StatisticsProvider) { + stats = ((StatisticsProvider) this.itemProvider).getStatistics(); + } + if (this.itemProcessor instanceof StatisticsProvider) { + Properties props = ((StatisticsProvider) this.itemProcessor).getStatistics(); + if (!stats.isEmpty()) { + stats = prependKeys(stats, props, PROVIDER_STATISTICS_PREFIX, PROCESSOR_STATISTICS_PREFIX); + } else { + stats.putAll(props); + } + } + return stats; + } + + /** + * @param props1 + * @param string + * @return + */ + private Properties prependKeys(Properties props1, Properties props2, String prefix1, String prefix2) { + Properties result = new Properties(); + Set duplicates = new HashSet(); + for (Iterator iterator = props1.entrySet().iterator(); iterator.hasNext();) { + Map.Entry entry = (Map.Entry) iterator.next(); + String key = (String) entry.getKey(); + String value = (String) entry.getValue(); + if (props2.containsKey(key)) { + duplicates.add(key); + continue; + } + result.setProperty(key, value); + } + for (Iterator iterator = props2.entrySet().iterator(); iterator.hasNext();) { + Map.Entry entry = (Map.Entry) iterator.next(); + String key = (String) entry.getKey(); + String value = (String) entry.getValue(); + if (duplicates.contains(key)) { + continue; + } + result.setProperty(key, value); + } + for (Iterator iterator = duplicates.iterator(); iterator.hasNext();) { + String key = (String) iterator.next(); + result.setProperty(prefix1+key, props1.getProperty(key)); + result.setProperty(prefix2+key, props2.getProperty(key)); + } + return result; + } + + /** + * Public setter for the retryPolicy. + * + * @param retyPolicy the retryPolicy to set + */ + public void setRetryPolicy(RetryPolicy retryPolicy) { + this.retryPolicy = retryPolicy; + } + + /** + * Public setter for the retryOperations. + * + * @param retryOperations the retryOperations to set + */ + public void setRetryOperations(RetryOperations retryOperations) { + this.retryOperations = retryOperations; + } +} diff --git a/execution/src/main/java/org/springframework/batch/execution/tasklet/ReadProcessTasklet.java b/execution/src/main/java/org/springframework/batch/execution/tasklet/ReadProcessTasklet.java new file mode 100644 index 000000000..eb64bc20f --- /dev/null +++ b/execution/src/main/java/org/springframework/batch/execution/tasklet/ReadProcessTasklet.java @@ -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.execution.tasklet; + +import org.springframework.batch.core.tasklet.Tasklet; + +/** + * Provides the basic batch module for reading and processing data. + * Implementations of this class will be handling both the input and output of + * data within one class. Developers should ensure that all reading is done + * before returning from the read() method. This is to ensure all data has been + * read first, before beginning to process. It is possibly detrimental to + * performance if processing begins when records still need to be read, because + * any writing of output will put the transaction in a volatile state, since + * errors with any additional input will need to cause a rollback, rather than + * simply skipping that record. + * + * @author Lucas Ward + * + */ +public abstract class ReadProcessTasklet implements Tasklet { + + /** + * Required for implementation of the {@link Tasklet} interface. The boolean returned + * from the abstract read method will be returned to the {@link Tasklet}, to indicate + * whether or not processing should continue. + */ + public final boolean execute() throws Exception { + if (!read()) { + return false; + } + process(); + return true; + } + + /** + * Abstract read method to be implemented by batch developers. All data + * should be read from within this method and a boolean indicated whether or + * not processing should continue should be returned. + * + * @return boolean indicating whether or not processing should continue. + */ + public abstract boolean read() throws Exception; + + /** + * Abstract process method to be implemented by batch developers. All + * processing and writing out of data should be done within this method. + */ + public abstract void process() throws Exception; +} diff --git a/execution/src/main/java/org/springframework/batch/execution/tasklet/RestartableItemProviderTasklet.java b/execution/src/main/java/org/springframework/batch/execution/tasklet/RestartableItemProviderTasklet.java new file mode 100644 index 000000000..a02330ffc --- /dev/null +++ b/execution/src/main/java/org/springframework/batch/execution/tasklet/RestartableItemProviderTasklet.java @@ -0,0 +1,120 @@ +/* + * 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.execution.tasklet; + +import java.util.Properties; + +import org.springframework.batch.item.ItemProcessor; +import org.springframework.batch.item.ItemProvider; +import org.springframework.batch.restart.GenericRestartData; +import org.springframework.batch.restart.RestartData; +import org.springframework.batch.restart.Restartable; +import org.springframework.batch.support.PropertiesConverter; + +/** + * An extension of {@link ItemProviderProcessTasklet} that delegates calls to + * {@link Restartable} to the provider and processor. + * + * @see ItemProvider + * @see ItemProcessor + * @see Restartable + * + * @author Lucas Ward + * @author Dave Syer + * + */ +public class RestartableItemProviderTasklet extends ItemProviderProcessTasklet implements Restartable { + + /** + * @see Restartable#getRestartData() + */ + public RestartData getRestartData() { + + RestartData itemProviderRestartData = null; + RestartData itemProcessorRestartData = null; + + if (itemProvider instanceof Restartable) { + itemProviderRestartData = ((Restartable) itemProvider).getRestartData(); + } + + if (itemProcessor instanceof Restartable) { + itemProcessorRestartData = ((Restartable) itemProcessor).getRestartData(); + } + + RestartableItemProviderTaskletRestartData restartData = new RestartableItemProviderTaskletRestartData(itemProviderRestartData, itemProcessorRestartData); + + return restartData; + } + + /** + * @see Restartable#restoreFrom(RestartData) + */ + public void restoreFrom(RestartData data) { + if (data == null || data.getProperties() == null) + return; + + RestartableItemProviderTaskletRestartData moduleRestartData; + + if (data instanceof RestartableItemProviderTaskletRestartData) { + moduleRestartData = (RestartableItemProviderTaskletRestartData) data; + } + else { + moduleRestartData = new RestartableItemProviderTaskletRestartData(data.getProperties()); + } + + if (itemProvider instanceof Restartable) { + ((Restartable) itemProvider).restoreFrom(moduleRestartData.providerData); + } + if (itemProcessor instanceof Restartable) { + ((Restartable) itemProcessor).restoreFrom(moduleRestartData.processorData); + } + } + + private class RestartableItemProviderTaskletRestartData implements RestartData { + + private static final String PROVIDER_KEY = "DATA_PROVIDER"; + + private static final String PROCESSOR_KEY = "DATA_PROCESSOR"; + + RestartData providerData; + + RestartData processorData; + + public RestartableItemProviderTaskletRestartData(RestartData providerData, RestartData processorData) { + this.providerData = providerData; + this.processorData = processorData; + } + + public RestartableItemProviderTaskletRestartData(Properties data) { + providerData = new GenericRestartData(PropertiesConverter + .stringToProperties(data.getProperty(PROVIDER_KEY))); + processorData = new GenericRestartData(PropertiesConverter.stringToProperties(data + .getProperty(PROCESSOR_KEY))); + } + + public Properties getProperties() { + Properties props = new Properties(); + if (providerData != null) { + props.setProperty(PROVIDER_KEY, PropertiesConverter.propertiesToString(providerData.getProperties())); + } + if (processorData != null) { + props.setProperty(PROCESSOR_KEY, PropertiesConverter.propertiesToString(processorData.getProperties())); + } + return props; + } + } +} diff --git a/execution/src/main/java/org/springframework/batch/execution/tasklet/package.html b/execution/src/main/java/org/springframework/batch/execution/tasklet/package.html new file mode 100644 index 000000000..85c3fc49e --- /dev/null +++ b/execution/src/main/java/org/springframework/batch/execution/tasklet/package.html @@ -0,0 +1,7 @@ + + +

+Specific implementations of tasklet concerns. +

+ + diff --git a/execution/src/main/java/org/springframework/batch/execution/tasklet/support/CompositeItemProcessor.java b/execution/src/main/java/org/springframework/batch/execution/tasklet/support/CompositeItemProcessor.java new file mode 100644 index 000000000..7a8c70291 --- /dev/null +++ b/execution/src/main/java/org/springframework/batch/execution/tasklet/support/CompositeItemProcessor.java @@ -0,0 +1,143 @@ +package org.springframework.batch.execution.tasklet.support; +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 + +

+Specific implementations of support concerns. +

+ + diff --git a/execution/src/main/java/overview.html b/execution/src/main/java/overview.html new file mode 100644 index 000000000..5716a1861 --- /dev/null +++ b/execution/src/main/java/overview.html @@ -0,0 +1,9 @@ + + +

+Reference implementations of the Core API. Provides basic support for +runtime environments like JMX monitors and command-line launchers for +batch processes. +

+ + diff --git a/execution/src/main/resources/batch.template.properties b/execution/src/main/resources/batch.template.properties new file mode 100644 index 000000000..b24969b96 --- /dev/null +++ b/execution/src/main/resources/batch.template.properties @@ -0,0 +1,9 @@ +batch.jdbc.driver= +batch.jdbc.url= +batch.jdbc.user= +batch.jdbc.password= +batch.schema= +batch.jndi.name= +batch.naming.factory.initial= +batch.naming.provider.url= +batch.database.vendor= \ No newline at end of file diff --git a/execution/src/main/resources/org/springframework/batch/execution/repository/dao/JobExecution.hbm.xml b/execution/src/main/resources/org/springframework/batch/execution/repository/dao/JobExecution.hbm.xml new file mode 100644 index 000000000..823de3ce1 --- /dev/null +++ b/execution/src/main/resources/org/springframework/batch/execution/repository/dao/JobExecution.hbm.xml @@ -0,0 +1,22 @@ + + + %globals; +]> + + + + + &job-execution-generator; + + + + + + + + + + diff --git a/execution/src/main/resources/org/springframework/batch/execution/repository/dao/JobInstance.hbm.xml b/execution/src/main/resources/org/springframework/batch/execution/repository/dao/JobInstance.hbm.xml new file mode 100644 index 000000000..81c3bdd12 --- /dev/null +++ b/execution/src/main/resources/org/springframework/batch/execution/repository/dao/JobInstance.hbm.xml @@ -0,0 +1,25 @@ + + + %globals; +]> + + + + + &job-generator; + + + + + + + + + + + + \ No newline at end of file diff --git a/execution/src/main/resources/org/springframework/batch/execution/repository/dao/StepExecution.hbm.xml b/execution/src/main/resources/org/springframework/batch/execution/repository/dao/StepExecution.hbm.xml new file mode 100644 index 000000000..09caf4a9f --- /dev/null +++ b/execution/src/main/resources/org/springframework/batch/execution/repository/dao/StepExecution.hbm.xml @@ -0,0 +1,27 @@ + + + %globals; +]> + + + + + &step-execution-generator; + + + + + + + + + + + + + + + diff --git a/execution/src/main/resources/org/springframework/batch/execution/repository/dao/StepInstance.hbm.xml b/execution/src/main/resources/org/springframework/batch/execution/repository/dao/StepInstance.hbm.xml new file mode 100644 index 000000000..f5b3b0610 --- /dev/null +++ b/execution/src/main/resources/org/springframework/batch/execution/repository/dao/StepInstance.hbm.xml @@ -0,0 +1,23 @@ + + + %globals; +]> + + + + + &step-generator; + + + + + + + + diff --git a/execution/src/main/resources/org/springframework/batch/execution/repository/dao/globals.dtd b/execution/src/main/resources/org/springframework/batch/execution/repository/dao/globals.dtd new file mode 100644 index 000000000..de8bd0b2c --- /dev/null +++ b/execution/src/main/resources/org/springframework/batch/execution/repository/dao/globals.dtd @@ -0,0 +1,20 @@ + + + + +'> + + + + +]]> + +'> + '> + + + + +]]> diff --git a/execution/src/main/resources/schema-db2.sql b/execution/src/main/resources/schema-db2.sql new file mode 100644 index 000000000..2ac110a60 --- /dev/null +++ b/execution/src/main/resources/schema-db2.sql @@ -0,0 +1,56 @@ +-- Autogenerated: do not edit this file +DROP TABLE BATCH_STEP_EXECUTION ; +DROP TABLE BATCH_JOB_EXECUTION ; +DROP TABLE BATCH_STEP ; +DROP TABLE BATCH_JOB ; + +DROP SEQUENCE BATCH_STEP_EXECUTION_SEQ ; +DROP SEQUENCE BATCH_STEP_SEQ ; +DROP SEQUENCE BATCH_JOB_EXECUTION_SEQ ; +DROP SEQUENCE BATCH_JOB_SEQ ; + +-- Autogenerated: do not edit this file +CREATE TABLE BATCH_JOB ( + ID BIGINT PRIMARY KEY , + VERSION BIGINT, + JOB_NAME VARCHAR(100) NOT NULL , + JOB_STREAM VARCHAR(20) , + SCHEDULE_DATE DATE , + JOB_RUN CHAR(2), + STATUS VARCHAR(10) ); + +CREATE TABLE BATCH_JOB_EXECUTION ( + ID BIGINT PRIMARY KEY , + VERSION BIGINT, + JOB_ID BIGINT NOT NULL, + START_TIME TIMESTAMP NOT NULL , + END_TIME TIMESTAMP , + STATUS VARCHAR(10), + EXIT_CODE BIGINT); + +CREATE TABLE BATCH_STEP ( + ID BIGINT PRIMARY KEY , + VERSION BIGINT, + JOB_ID BIGINT NOT NULL, + STEP_NAME VARCHAR(100) NOT NULL, + STATUS VARCHAR(10), + RESTART_DATA VARCHAR(200)); + +CREATE TABLE BATCH_STEP_EXECUTION ( + ID BIGINT PRIMARY KEY , + VERSION BIGINT NOT NULL, + STEP_ID BIGINT NOT NULL, + JOB_EXECUTION_ID BIGINT NOT NULL, + START_TIME TIMESTAMP NOT NULL , + END_TIME TIMESTAMP , + STATUS VARCHAR(10), + COMMIT_COUNT BIGINT , + TASK_COUNT BIGINT , + TASK_STATISTICS VARCHAR(250), + EXIT_CODE BIGINT, + EXIT_MESSAGE VARCHAR(250)); + +CREATE SEQUENCE BATCH_STEP_EXECUTION_SEQ; +CREATE SEQUENCE BATCH_STEP_SEQ; +CREATE SEQUENCE BATCH_JOB_EXECUTION_SEQ; +CREATE SEQUENCE BATCH_JOB_SEQ; diff --git a/execution/src/main/resources/schema-derby.sql b/execution/src/main/resources/schema-derby.sql new file mode 100644 index 000000000..c27ebc8cc --- /dev/null +++ b/execution/src/main/resources/schema-derby.sql @@ -0,0 +1,56 @@ +-- Autogenerated: do not edit this file +DROP TABLE BATCH_STEP_EXECUTION ; +DROP TABLE BATCH_JOB_EXECUTION ; +DROP TABLE BATCH_STEP ; +DROP TABLE BATCH_JOB ; + +DROP SEQUENCE BATCH_STEP_EXECUTION_SEQ ; +DROP SEQUENCE BATCH_STEP_SEQ ; +DROP SEQUENCE BATCH_JOB_EXECUTION_SEQ ; +DROP SEQUENCE BATCH_JOB_SEQ ; + +-- Autogenerated: do not edit this file +CREATE TABLE BATCH_JOB ( + ID BIGINT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, + VERSION BIGINT, + JOB_NAME VARCHAR(100) NOT NULL , + JOB_STREAM VARCHAR(20) , + SCHEDULE_DATE DATE , + JOB_RUN CHAR(2), + STATUS VARCHAR(10) ); + +CREATE TABLE BATCH_JOB_EXECUTION ( + ID BIGINT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, + VERSION BIGINT, + JOB_ID BIGINT NOT NULL, + START_TIME TIMESTAMP NOT NULL , + END_TIME TIMESTAMP , + STATUS VARCHAR(10), + EXIT_CODE BIGINT); + +CREATE TABLE BATCH_STEP ( + ID BIGINT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, + VERSION BIGINT, + JOB_ID BIGINT NOT NULL, + STEP_NAME VARCHAR(100) NOT NULL, + STATUS VARCHAR(10), + RESTART_DATA VARCHAR(200)); + +CREATE TABLE BATCH_STEP_EXECUTION ( + ID BIGINT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, + VERSION BIGINT NOT NULL, + STEP_ID BIGINT NOT NULL, + JOB_EXECUTION_ID BIGINT NOT NULL, + START_TIME TIMESTAMP NOT NULL , + END_TIME TIMESTAMP , + STATUS VARCHAR(10), + COMMIT_COUNT BIGINT , + TASK_COUNT BIGINT , + TASK_STATISTICS VARCHAR(250), + EXIT_CODE BIGINT, + EXIT_MESSAGE VARCHAR(250)); + +CREATE SEQUENCE BATCH_STEP_EXECUTION_SEQ; +CREATE SEQUENCE BATCH_STEP_SEQ; +CREATE SEQUENCE BATCH_JOB_EXECUTION_SEQ; +CREATE SEQUENCE BATCH_JOB_SEQ; diff --git a/execution/src/main/resources/schema-hsqldb.sql b/execution/src/main/resources/schema-hsqldb.sql new file mode 100644 index 000000000..40dd02f1b --- /dev/null +++ b/execution/src/main/resources/schema-hsqldb.sql @@ -0,0 +1,64 @@ +-- Autogenerated: do not edit this file +DROP TABLE BATCH_STEP_EXECUTION IF EXISTS; +DROP TABLE BATCH_JOB_EXECUTION IF EXISTS; +DROP TABLE BATCH_STEP IF EXISTS; +DROP TABLE BATCH_JOB IF EXISTS; + +DROP TABLE BATCH_STEP_EXECUTION_SEQ IF EXISTS; +DROP TABLE BATCH_STEP_SEQ IF EXISTS; +DROP TABLE BATCH_JOB_EXECUTION_SEQ IF EXISTS; +DROP TABLE BATCH_JOB_SEQ IF EXISTS; + +-- Autogenerated: do not edit this file +CREATE TABLE BATCH_JOB ( + ID BIGINT IDENTITY PRIMARY KEY , + VERSION BIGINT, + JOB_NAME VARCHAR(100) NOT NULL , + JOB_STREAM VARCHAR(20) , + SCHEDULE_DATE DATE , + JOB_RUN CHAR(2), + STATUS VARCHAR(10) ); + +CREATE TABLE BATCH_JOB_EXECUTION ( + ID BIGINT IDENTITY PRIMARY KEY , + VERSION BIGINT, + JOB_ID BIGINT NOT NULL, + START_TIME TIMESTAMP NOT NULL , + END_TIME TIMESTAMP , + STATUS VARCHAR(10), + EXIT_CODE BIGINT); + +CREATE TABLE BATCH_STEP ( + ID BIGINT IDENTITY PRIMARY KEY , + VERSION BIGINT, + JOB_ID BIGINT NOT NULL, + STEP_NAME VARCHAR(100) NOT NULL, + STATUS VARCHAR(10), + RESTART_DATA VARCHAR(200)); + +CREATE TABLE BATCH_STEP_EXECUTION ( + ID BIGINT IDENTITY PRIMARY KEY , + VERSION BIGINT NOT NULL, + STEP_ID BIGINT NOT NULL, + JOB_EXECUTION_ID BIGINT NOT NULL, + START_TIME TIMESTAMP NOT NULL , + END_TIME TIMESTAMP , + STATUS VARCHAR(10), + COMMIT_COUNT BIGINT , + TASK_COUNT BIGINT , + TASK_STATISTICS VARCHAR(250), + EXIT_CODE BIGINT, + EXIT_MESSAGE VARCHAR(250)); + +CREATE TABLE BATCH_STEP_EXECUTION_SEQ ( + ID BIGINT IDENTITY +); +CREATE TABLE BATCH_STEP_SEQ ( + ID BIGINT IDENTITY +); +CREATE TABLE BATCH_JOB_EXECUTION_SEQ ( + ID BIGINT IDENTITY +); +CREATE TABLE BATCH_JOB_SEQ ( + ID BIGINT IDENTITY +); diff --git a/execution/src/main/resources/schema-oracle10g.sql b/execution/src/main/resources/schema-oracle10g.sql new file mode 100644 index 000000000..1f591e38f --- /dev/null +++ b/execution/src/main/resources/schema-oracle10g.sql @@ -0,0 +1,56 @@ +-- Autogenerated: do not edit this file +DROP TABLE BATCH_STEP_EXECUTION ; +DROP TABLE BATCH_JOB_EXECUTION ; +DROP TABLE BATCH_STEP ; +DROP TABLE BATCH_JOB ; + +DROP SEQUENCE BATCH_STEP_EXECUTION_SEQ ; +DROP SEQUENCE BATCH_STEP_SEQ ; +DROP SEQUENCE BATCH_JOB_EXECUTION_SEQ ; +DROP SEQUENCE BATCH_JOB_SEQ ; + +-- Autogenerated: do not edit this file +CREATE TABLE BATCH_JOB ( + ID INT PRIMARY KEY , + VERSION INT, + JOB_NAME VARCHAR(100) NOT NULL , + JOB_STREAM VARCHAR(20) , + SCHEDULE_DATE DATE , + JOB_RUN CHAR(2), + STATUS VARCHAR(10) ); + +CREATE TABLE BATCH_JOB_EXECUTION ( + ID INT PRIMARY KEY , + VERSION INT, + JOB_ID INT NOT NULL, + START_TIME TIMESTAMP NOT NULL , + END_TIME TIMESTAMP , + STATUS VARCHAR(10), + EXIT_CODE INT); + +CREATE TABLE BATCH_STEP ( + ID INT PRIMARY KEY , + VERSION INT, + JOB_ID INT NOT NULL, + STEP_NAME VARCHAR(100) NOT NULL, + STATUS VARCHAR(10), + RESTART_DATA VARCHAR(200)); + +CREATE TABLE BATCH_STEP_EXECUTION ( + ID INT PRIMARY KEY , + VERSION INT NOT NULL, + STEP_ID INT NOT NULL, + JOB_EXECUTION_ID INT NOT NULL, + START_TIME TIMESTAMP NOT NULL , + END_TIME TIMESTAMP , + STATUS VARCHAR(10), + COMMIT_COUNT INT , + TASK_COUNT INT , + TASK_STATISTICS VARCHAR(250), + EXIT_CODE INT, + EXIT_MESSAGE VARCHAR(250)); + +CREATE SEQUENCE BATCH_STEP_EXECUTION_SEQ; +CREATE SEQUENCE BATCH_STEP_SEQ; +CREATE SEQUENCE BATCH_JOB_EXECUTION_SEQ; +CREATE SEQUENCE BATCH_JOB_SEQ; diff --git a/execution/src/main/resources/schema-postgresql.sql b/execution/src/main/resources/schema-postgresql.sql new file mode 100644 index 000000000..2ac110a60 --- /dev/null +++ b/execution/src/main/resources/schema-postgresql.sql @@ -0,0 +1,56 @@ +-- Autogenerated: do not edit this file +DROP TABLE BATCH_STEP_EXECUTION ; +DROP TABLE BATCH_JOB_EXECUTION ; +DROP TABLE BATCH_STEP ; +DROP TABLE BATCH_JOB ; + +DROP SEQUENCE BATCH_STEP_EXECUTION_SEQ ; +DROP SEQUENCE BATCH_STEP_SEQ ; +DROP SEQUENCE BATCH_JOB_EXECUTION_SEQ ; +DROP SEQUENCE BATCH_JOB_SEQ ; + +-- Autogenerated: do not edit this file +CREATE TABLE BATCH_JOB ( + ID BIGINT PRIMARY KEY , + VERSION BIGINT, + JOB_NAME VARCHAR(100) NOT NULL , + JOB_STREAM VARCHAR(20) , + SCHEDULE_DATE DATE , + JOB_RUN CHAR(2), + STATUS VARCHAR(10) ); + +CREATE TABLE BATCH_JOB_EXECUTION ( + ID BIGINT PRIMARY KEY , + VERSION BIGINT, + JOB_ID BIGINT NOT NULL, + START_TIME TIMESTAMP NOT NULL , + END_TIME TIMESTAMP , + STATUS VARCHAR(10), + EXIT_CODE BIGINT); + +CREATE TABLE BATCH_STEP ( + ID BIGINT PRIMARY KEY , + VERSION BIGINT, + JOB_ID BIGINT NOT NULL, + STEP_NAME VARCHAR(100) NOT NULL, + STATUS VARCHAR(10), + RESTART_DATA VARCHAR(200)); + +CREATE TABLE BATCH_STEP_EXECUTION ( + ID BIGINT PRIMARY KEY , + VERSION BIGINT NOT NULL, + STEP_ID BIGINT NOT NULL, + JOB_EXECUTION_ID BIGINT NOT NULL, + START_TIME TIMESTAMP NOT NULL , + END_TIME TIMESTAMP , + STATUS VARCHAR(10), + COMMIT_COUNT BIGINT , + TASK_COUNT BIGINT , + TASK_STATISTICS VARCHAR(250), + EXIT_CODE BIGINT, + EXIT_MESSAGE VARCHAR(250)); + +CREATE SEQUENCE BATCH_STEP_EXECUTION_SEQ; +CREATE SEQUENCE BATCH_STEP_SEQ; +CREATE SEQUENCE BATCH_JOB_EXECUTION_SEQ; +CREATE SEQUENCE BATCH_JOB_SEQ; diff --git a/execution/src/main/sql/db2.properties b/execution/src/main/sql/db2.properties new file mode 100644 index 000000000..532bd942b --- /dev/null +++ b/execution/src/main/sql/db2.properties @@ -0,0 +1,6 @@ +platform=db2 +# SQL language oddities +BIGINT = BIGINT +IDENTITY = +# for generating drop statements... +SEQUENCE = SEQUENCE diff --git a/execution/src/main/sql/db2.vpp b/execution/src/main/sql/db2.vpp new file mode 100644 index 000000000..3713aa6f5 --- /dev/null +++ b/execution/src/main/sql/db2.vpp @@ -0,0 +1,2 @@ +#macro (sequence $name)CREATE SEQUENCE ${name}; +#end diff --git a/execution/src/main/sql/derby.properties b/execution/src/main/sql/derby.properties new file mode 100644 index 000000000..9f34bc337 --- /dev/null +++ b/execution/src/main/sql/derby.properties @@ -0,0 +1,7 @@ +platform=db2 +# SQL language oddities +BIGINT = BIGINT +IDENTITY = +GENERATED = GENERATED BY DEFAULT AS IDENTITY +# for generating drop statements... +SEQUENCE = SEQUENCE diff --git a/execution/src/main/sql/derby.vpp b/execution/src/main/sql/derby.vpp new file mode 100644 index 000000000..3713aa6f5 --- /dev/null +++ b/execution/src/main/sql/derby.vpp @@ -0,0 +1,2 @@ +#macro (sequence $name)CREATE SEQUENCE ${name}; +#end diff --git a/execution/src/main/sql/destroy.sql.vpp b/execution/src/main/sql/destroy.sql.vpp new file mode 100644 index 000000000..df3a8eb03 --- /dev/null +++ b/execution/src/main/sql/destroy.sql.vpp @@ -0,0 +1,10 @@ +-- Autogenerated: do not edit this file +DROP TABLE BATCH_STEP_EXECUTION $!{IFEXISTS}; +DROP TABLE BATCH_JOB_EXECUTION $!{IFEXISTS}; +DROP TABLE BATCH_STEP $!{IFEXISTS}; +DROP TABLE BATCH_JOB $!{IFEXISTS}; + +DROP ${SEQUENCE} BATCH_STEP_EXECUTION_SEQ $!{IFEXISTS}; +DROP ${SEQUENCE} BATCH_STEP_SEQ $!{IFEXISTS}; +DROP ${SEQUENCE} BATCH_JOB_EXECUTION_SEQ $!{IFEXISTS}; +DROP ${SEQUENCE} BATCH_JOB_SEQ $!{IFEXISTS}; \ No newline at end of file diff --git a/execution/src/main/sql/hsqldb.properties b/execution/src/main/sql/hsqldb.properties new file mode 100644 index 000000000..d72d28999 --- /dev/null +++ b/execution/src/main/sql/hsqldb.properties @@ -0,0 +1,7 @@ +platform=hsqldb +# SQL language oddities +BIGINT = BIGINT +IDENTITY = IDENTITY +IFEXISTS = IF EXISTS +# for generating drop statements... +SEQUENCE = TABLE diff --git a/execution/src/main/sql/hsqldb.vpp b/execution/src/main/sql/hsqldb.vpp new file mode 100644 index 000000000..6259ee124 --- /dev/null +++ b/execution/src/main/sql/hsqldb.vpp @@ -0,0 +1,4 @@ +#macro (sequence $name)CREATE TABLE ${name} ( + ID BIGINT IDENTITY +); +#end diff --git a/execution/src/main/sql/init.sql.vpp b/execution/src/main/sql/init.sql.vpp new file mode 100644 index 000000000..76157be5c --- /dev/null +++ b/execution/src/main/sql/init.sql.vpp @@ -0,0 +1,45 @@ +-- Autogenerated: do not edit this file +CREATE TABLE BATCH_JOB ( + ID ${BIGINT} $!{IDENTITY} PRIMARY KEY $!{GENERATED}, + VERSION ${BIGINT}, + JOB_NAME VARCHAR(100) NOT NULL , + JOB_STREAM VARCHAR(20) , + SCHEDULE_DATE DATE , + JOB_RUN CHAR(2), + STATUS VARCHAR(10) ); + +CREATE TABLE BATCH_JOB_EXECUTION ( + ID ${BIGINT} $!{IDENTITY} PRIMARY KEY $!{GENERATED}, + VERSION ${BIGINT}, + JOB_ID ${BIGINT} NOT NULL, + START_TIME TIMESTAMP NOT NULL , + END_TIME TIMESTAMP , + STATUS VARCHAR(10), + EXIT_CODE ${BIGINT}); + +CREATE TABLE BATCH_STEP ( + ID ${BIGINT} $!{IDENTITY} PRIMARY KEY $!{GENERATED}, + VERSION ${BIGINT}, + JOB_ID ${BIGINT} NOT NULL, + STEP_NAME VARCHAR(100) NOT NULL, + STATUS VARCHAR(10), + RESTART_DATA VARCHAR(200)); + +CREATE TABLE BATCH_STEP_EXECUTION ( + ID ${BIGINT} $!{IDENTITY} PRIMARY KEY $!{GENERATED}, + VERSION ${BIGINT} NOT NULL, + STEP_ID ${BIGINT} NOT NULL, + JOB_EXECUTION_ID ${BIGINT} NOT NULL, + START_TIME TIMESTAMP NOT NULL , + END_TIME TIMESTAMP , + STATUS VARCHAR(10), + COMMIT_COUNT ${BIGINT} , + TASK_COUNT ${BIGINT} , + TASK_STATISTICS VARCHAR(250), + EXIT_CODE ${BIGINT}, + EXIT_MESSAGE VARCHAR(250)); + +#sequence( "BATCH_STEP_EXECUTION_SEQ" ) +#sequence( "BATCH_STEP_SEQ" ) +#sequence( "BATCH_JOB_EXECUTION_SEQ" ) +#sequence( "BATCH_JOB_SEQ" ) diff --git a/execution/src/main/sql/oracle10g.properties b/execution/src/main/sql/oracle10g.properties new file mode 100644 index 000000000..3b048390e --- /dev/null +++ b/execution/src/main/sql/oracle10g.properties @@ -0,0 +1,7 @@ +platform=oracle10g +# SQL language oddities +BIGINT = INT +IDENTITY = +GENERATED = +# for generating drop statements... +SEQUENCE = SEQUENCE diff --git a/execution/src/main/sql/oracle10g.vpp b/execution/src/main/sql/oracle10g.vpp new file mode 100644 index 000000000..3713aa6f5 --- /dev/null +++ b/execution/src/main/sql/oracle10g.vpp @@ -0,0 +1,2 @@ +#macro (sequence $name)CREATE SEQUENCE ${name}; +#end diff --git a/execution/src/main/sql/postgresql.properties b/execution/src/main/sql/postgresql.properties new file mode 100644 index 000000000..c716d74b2 --- /dev/null +++ b/execution/src/main/sql/postgresql.properties @@ -0,0 +1,7 @@ +platform=postgresql +# SQL language oddities +BIGINT = BIGINT +IDENTITY = +GENERATED = +# for generating drop statements... +SEQUENCE = SEQUENCE diff --git a/execution/src/main/sql/postgresql.vpp b/execution/src/main/sql/postgresql.vpp new file mode 100644 index 000000000..3713aa6f5 --- /dev/null +++ b/execution/src/main/sql/postgresql.vpp @@ -0,0 +1,2 @@ +#macro (sequence $name)CREATE SEQUENCE ${name}; +#end diff --git a/execution/src/main/sql/schema.sql.vpp b/execution/src/main/sql/schema.sql.vpp new file mode 100644 index 000000000..6dbfe6d43 --- /dev/null +++ b/execution/src/main/sql/schema.sql.vpp @@ -0,0 +1,4 @@ +#parse("${includes}/destroy.sql.vpp") + + +#parse("${includes}/init.sql.vpp") diff --git a/execution/src/site/apt/changelog.apt b/execution/src/site/apt/changelog.apt new file mode 100644 index 000000000..17926156e --- /dev/null +++ b/execution/src/site/apt/changelog.apt @@ -0,0 +1,7 @@ +Changelog: Spring Batch Execution + +* 1.0-M2 + +** 2007/07/12 + + * No-one uses this file: we should just switch to auto-generated changelogs? diff --git a/execution/src/site/apt/executable.apt b/execution/src/site/apt/executable.apt new file mode 100644 index 000000000..ec70e9936 --- /dev/null +++ b/execution/src/site/apt/executable.apt @@ -0,0 +1,38 @@ + + Tried to create an executable jar with this: + ++--- + + + + org.apache.maven.plugins + maven-jar-plugin + + + + org.springframework.batch.container.bootstrap.BatchCommandLineLauncher + true + + + + + + ++--- + + But the resulting MANIFEST.MF is rubbish. Look at the classpath + (where did that come from)? + ++--- +Manifest-Version: 1.0 +Archiver-Version: Plexus Archiver +Created-By: Apache Maven +Built-By: dsyer +Build-Jdk: 1.5.0_09 +Main-Class: org.springframework.batch.container.bootstrap.BatchCommand + LineLauncher +Class-Path: spring-2.1-m2.jar commons-logging-1.1.jar log4j-1.2.12.jar + dom4j-1.6.1.jar commons-lang-2.1.jar spring-batch-infrastructure-1.0 + -m2-SNAPSHOT.jar antlr-2.7.6.jar commons-collections-2.1.1.jar hibern + ate-3.2.3.ga.jar spring-mock-2.1-m2.jar ehcache-1.2.3.jar ++--- diff --git a/execution/src/site/apt/glossary.apt b/execution/src/site/apt/glossary.apt new file mode 100644 index 000000000..1b2e1aac5 --- /dev/null +++ b/execution/src/site/apt/glossary.apt @@ -0,0 +1,46 @@ + ------ + Glossary + ------ + Wayne Lund + ------ + May 2007 + + + [[1]]<>: An accumulation of business transactions over time. + + [[2]]<>: Term used to designate batch as an application style in its own right similar to online, Web or SOA. It has standard elements of input, validation, transformation of information to business model, business processing and output. In addition, it requires monitoring at a macro level. + + [[3]]<>: The handling of a batch of many business transactions that have accumulated over a period of time (e.g. an hour, day, week, month, or year). It is the application of a process, or set of processes, to many data entities or objects in a repetitive and predictable fashion with either no manual element, or a separate manual element for error processing. + + [[4]]<>: The time frame within which a batch job must complete. This can be constrained by other systems coming online, other dependent jobs needing to execute or other factors specific to the batch environment. + + [[5]]<>: It is the main batch task or Unit of Work controller. It initializes the module, and controls the transaction environment based on commit interval setting, etc. + + [[6]]<>: The main application program created by application developer to process the business logic for each LUW. + + [[7]]<>: Job Types describe application of jobs for particular type of processing. Common areas are interface processing (typically flat files), forms processing (either for online pdf generation or print formats), report processing. s + + [[8]]<>: A driving query identifies the set of work for a job to do; the job then breaks that work into individual units of work. For instance, identify all financial transactions that have a status of "pending transmission" and send them to our partner system. The driving query returns a set of record IDs to process; each record ID then becomes a unit of work. A driving query may involve a join (if the criteria for selection falls across two or more tables) or it may work with a single table. + + [[9]]<>: A batch job iterates through a driving query (or another input source such as a file) to perform the set of work that the job must accomplish. Each iteration of work performed is a unit of work. + + [[10]]<>: A set of LUWs constitute a commit interval. + + [[11]]<>: Splitting a job into multiple threads where each thread is responsible for a subset of the overall data to be processed. The threads of execution may be within the same JVM or they may span JVMs in a clustered environment that supports workload balancing. + + [[12]]<>: A table that holds temporary data while it is being processed. + + [[13]]<>: - a job that can be executed again and will assume the same identity as when run initially. In othewords, it is has the same job instance id. + + [[14]]Rerunnable - a job that is restartable and manages it's own state in terms of previous run's record processing. Note>>: Rerunnable is tied to the driving query. If the query can be formed so that it will limit the processed rows when the job is restarted than re-runnable = true. Often times a condition is added to the where statement to limit the rows returned by the driving query with something like "and processedFlag != true". + +--------------------------------------------------------------------- +Note: If its false the architecture assumes responsibility for tracking which rows have been processed. There is a default strategy for tracking the last record processed by partition. Most batch jobs only have one partition. The option is only valid for a restartable job. The reason being is that we have to persist the restart data which is only available on a restartable job. + +In DSL it is the following: + StartOver ::= restartable = false. Restartable ::= true | false + If (Restartable) + re-runnable ::= true | false + +We don't persist restart information for a non-restartable job. As you can see, it doesn't make sense. Rerunnable has always confused the best of us. +---------------------------------------------------------------------------- diff --git a/execution/src/site/apt/index.apt b/execution/src/site/apt/index.apt new file mode 100644 index 000000000..baf3ac791 --- /dev/null +++ b/execution/src/site/apt/index.apt @@ -0,0 +1,61 @@ + ------ + Simple Batch Execution Container Overview + ------ + Scott Wintermute + ------ + May 2007 + +Overview of the Spring Batch Simple Batch Execution Container + + The diagram below provides an overview of the high level components, technical services, and basic operations required by a batch architecture. This architecture framework is a blueprint that has been proven through decades of implementations on the last several generations of platforms (COBOL/Mainframe, C++/Unix, and now Java/anywhere). The Simple Batch Execution Container provides a physical implementation of the layers, components and technical services commonly found in robust, maintainable systems used to address the creation of simple to complex batch applications, with the infrastructure and extensions to address very complex processing needs. The materials below will walk through the details of the diagram. + +[images/simple-batch-execution-container.jpg] Simple Batch Execution Container high level flow and interaction of the architecture. + + Tiers + The application style is organized into four logical tiers, which include Run, Job, Application, and Data tiers. The primary goal for organizing an application according to the tiers is to embed what is known as "separation of concerns" within the system. Effective separation of concerns results in reducing the impact of change to the system. + + * <> The Run Tier is concerned with the scheduling and launching of the application. A vendor product is typically used in this tier to allow time-based and interdependent scheduling of batch jobs as well as providing parallel processing capabilities. + + * <> The Job Tier is responsible for the overall execution of a batch job. It sequentially executes batch steps, ensuring that all steps are in the correct state and all appropriate policies are enforced. + + * <> The Application Tier contains components required to execute the program. It contains specific modules that address the required batch functionality and enforces policies around a module execution (e.g., commit intervals, capture of statistics, etc.) + + * <> The Data Tier provides the integration with the physical data sources that might include databases, files, or queues. <>: In some cases the Job tier can be completely missing and in other cases one Job Script can start several Batch Job instances. + +High Level Processing Flow + + The diagram above illustrates the flow and architecture components in a typical batch run execution. + + Standard interaction is described as follows: + + <<1.>> In the Run tier, a Scheduler starts a batch application by invoking a Job Script. The Scheduler identifies what batch process it wants to run by passing the name of the batch process and any required additional parameters to the Job Script. + + <<2.>> The Job Script initializes the program and executes any job specific scripts prior to calling the Batch Launcher. + + <<3.>> The Batch Launcher starts the Batch Execution Container based upon any environment settings established in the script. (NOTE: A Batch Execution Container is not a Java EE container) + + <<3.1>> The Batch Container starts and controls the batch execution. It initializes the Job execution environment with static configuration items such as database settings, logging levels and creates a Job based on the Job Configuration created by a Batch Developer. + + <<4>> Based on configuration provided by a Batch Developer, the Job sequentially executes steps after checking policies to ensure that each step should be started. The status of the job and step (start time, end time, status such as "started" or "completed") is stored at various points during the process. + + <<5.1>> In order to maintain data integrity, at the application tier, the Step acts as a controller to ensure that either an entire group of actions completes successfully or that none of the actions completes. This group of actions is referred to as a logical unit of work (LUW). The Step controls the overall execution of the Tasklet, ensuring that transaction are committed at the appropriate time, and restart and statistics information is stored appropriately. The first thing the Step is responsible for is the initialization of the data required to begin processing. The Step will interact with other architecture components, such as the Input Source, to setup the data required to be processed. + + <<5.1.1>> The Input Source provides services to access various data sources. It provides location transparency to the Batch Tasklet and hides the physical location details of the data. + + <<5.2>> Once the data is initialized by the Input Source, the Step will call into the Tasklet to begin processing. The Tasklet contains the business logic to define the LUW and the Step repeatedly calls the Tasklets LUW to finish the business function. The Step does this by first invoking the execute method on the Tasklet in order to acquire a single record/set of data for processing. + + <<5.2.1>> Before a record is returned to the Tasklet, it may be validated by any number of validation Frameworks that can be provided to an input source. A single record/set of data is gathered by interacting with the Input Source. + + <<5.3>> Once a record/set has been obtained, the step calls the module to begin processing. + + <<5.3.1>> The Tasklet executes its internal business logic by calling other Business Logic components as necessary. Based on the business service, it can requests or persists objects from the data access components. + + <<5.3.3>> Data Access components can be leveraged retrieve or persist domain objects. + + <<5.3.4>> Once the business logic has been executed, the resulting output record is written out by utilizing the Output Source interface. The Step will repeatedly call steps 4.2 \-> 4.4 for every record provided by the Input Source. + + <<5.4>> Once all of the records are processed, the Step calls the Tasklet to perform any clean up activities such as closing connections, exporting files, etc. + + <<5.4.1>> The Step is responsible for committing data associated with the remaining logical units of work as well as performing any finalization and administrative functions (e.g. closing database connections). + + Once the Step has completed finalization the control is passed back to the Job, where any necessary logging or clean up is executed for application termination and wrap-up -- provided there are no additional Steps to execute. \ No newline at end of file diff --git a/execution/src/site/apt/introduction.apt b/execution/src/site/apt/introduction.apt new file mode 100644 index 000000000..44b7a3764 --- /dev/null +++ b/execution/src/site/apt/introduction.apt @@ -0,0 +1,201 @@ + ------ + Batch Processing Strategy + ------ + Scott Wintermute + ------ + May 2007 + +Batch Processing Strategy + + To help design and implement batch systems, basic batch application building blocks and patterns should be provided to the designers and programmers in form of sample structure charts and code shells. When starting to design a batch job, the business logic should be decomposed into a series of steps which can be implemented using the following standard building blocks: + + * Conversion Applications: For each type of file supplied by or generated to an external system, a conversion application will need to be created to convert the transaction records supplied into a standard format required for processing. This type of batch application can partly or entirely consist of translation utility modules (see Basic Batch Services). + + * Validation Applications: Validation applications ensure that all input/output records are correct and consistent. Validation is typically based on file headers and trailers, checksums and validation algorithms as well as record level cross-checks. + + * Extract Applications: An application that reads a set of records from a database or input file, selects records based on predefined rules, and writes the records to an output file. + + * Extract/Update Applications: An application that reads records from a database or an input file, and makes changes to a database or an output file driven by the data found in each input record. + + * Processing and Updating Applications: An application that performs processing on input transactions from an extract or a validation application. The processing will usually involve reading a database to obtain data required for processing, potentially updating the database and creating records for output processing. + + * Output/Format Applications: Applications reading an input file, restructures data from this record according to a standard format, and produces an output file for printing or transmission to another program or system. + + <> + + Additionally a basic application shell should be provided for business logic that cannot be built using the previously mentioned building blocks. + + In addition to the main building blocks, each application may use one or more of standard utility steps, such as: + + * Sort - A Program that reads an input file and produces an output file where records have been re-sequenced according to a sort key field in the records. Sorts are usually performed by standard system utilities. + + * Split - A program that reads a single input file, and writes each record to one of several output files based on a field value. Splits can be tailored or performed by parameter-driven standard system utilities. + + * Merge - A program that reads records from multiple input files and produces one output file with combined data from the input files. Merges can be tailored or performed by parameter-driven standard system utilities. + + Batch applications can additionally be categorized by their input source: + + * Database-driven applications are driven by rows or values retrieved from the database. + + * File-driven applications are driven by records or values retrieved from a file + + The foundation of any batch system is the processing strategy. Factors affecting the selection of the strategy include estimated batch system volume, concurrency with on-line or with another batch systems, available batch windows etc. Also with more enterprises wanting to be up and running 24x7, leaving no obvious batch windows. + + Typical processing options for batch are: + + * Normal processing in a batch window during off-line + + * Concurrent batch / on-line processing + + * Parallel processing of many different batch runs or jobs at the same time + + * Streaming i.e. processing of many instances of the same job at the same time + + * A combination of these + + The order in the list above reflects the implementation complexity, processing in a batch window being the easiest and streaming the most complex to implement. + + Some or all of these options may be supported by a commercial scheduler. + + In the following section these processing options will be discussed in more detail. It is important to notice that the commit and locking strategy adopted by batch processes will be dependent on the type of processing performed and as a rule of thumb, the on-line locking should use the same principles. Therefore a batch architecture cannot be simply an afterthought when designing an overall architecture. + + The locking strategy can use only normal database locks, or an additional custom locking service can be implemented in the architecture. The locking service would track database locking (for example by storing the necessary information in a dedicated db-table) and give or deny permissions to the application programs requesting a db operation. Retry logic could also be implemented by this architecture to avoid aborting a batch job in case of a lock situation. + + <<1. Normal processing in a batch window>> + For simple batch processes running in a separate batch window, where the data being updated is not required by on-line users or other batch processes, concurrency is not an issue and a single commit can be done at the end of the batch run. + + In most cases a more robust approach is more appropriate. A thing to keep in mind is that batch systems have a tendency to grow as time goes by, both in terms of complexity and the data volumes they will handle. If no locking strategy is in place and the system still relies on a single commit point, modifying the batch programs can be painful. Therefore, even with the simplest batch systems, consider the need for commit logic depicted in the [Restart/Recovery section|Restart & Recovery] as well as the information concerning the more complex cases below. + + <<2. Concurrent batch / on-line processing>> + Batch applications processing data that can simultaneously be updated by on-line users, should not lock any data (either in the database or in files) which could be required by on-line users for more than a few seconds. Also updates should be committed to the database at the end of every few transaction. This minimizes the portion of data that is unavailable to other processes and the elapsed time the data is unavailable. + + Another option to minimize physical locking is to have a logical row-level locking implemented using either an Optimistic Locking Pattern or a Pessimistic Locking Pattern. + + * Optimistic locking assumes a low likelihood of record contention. It typically means inserting a timestamp column in each database table used concurrently by both batch and on-line processing. When an application fetches a row for processing, it also fetches the timestamp. As the application then tries to update the processed row, the update uses the original timestamp in the WHERE clause. If the timestamp matches, the data and the timestamp will be updated successfully. If the timestamp does not match, this indicates that another application has updated the same row between the fetch and the update attempt and therefore the update cannot be performed. + + * Pessimistic locking is any locking strategy that assumes there is a high likelihood of record contention and therefore either a physical or logical lock needs to be obtained at retrieval time. One type of pessimistic logical locking uses a dedicated lock-column in the database table. When an application retrieves the row for update, it sets a flag in the lock column. With the flag in place, other applications attempting to retrieve the same row will logically fail. When the application that set the flag updates the row, it also clears the flag, enabling the row to be retrieved by other applications. Please note, that the integrity of data must be maintained also between the initial fetch and the setting of the flag, for example by using db locks (e.g.,SELECT FOR UPDATE). Note also that this method suffers from the same downside as physical locking except that it is somewhat easier to manage building a time-out mechanism that will get the lock released if the user goes to lunch while the record is locked. + + These patterns are not necessarily suitable for batch processing, but they might be used for concurrent batch and on-line processing for example in cases where the database doesn't support row-level locking. As a general rule, optimistic locking is more suitable for on-line applications, while pessimistic locking is more suitable for batch applications. Whenever logical locking is used, the same scheme must be used for all applications accessing data entities protected by logical locks. + + Note that both of these solutions only address locking a single record. Often we may need to lock a logically related group of records. With physical locks, you have to manage these very carefully in order to avoid potential deadlocks. With logical locks, it is usually best to build a logical lock manager that understands the logical record groups you want to protect and can ensure that locks are coherent and non-deadlocking. This logical lock manager usually uses its own tables for lock management, contention reporting, time-out mechanism, etc. + + <<3. Parallel Processing>> + Parallel processing allows multiple batch runs / jobs to run in parallel to minimize the total elapsed batch processing time. This is not a problem as long as the jobs are not sharing the same files, db-tables or index spaces. If they do, this service should be implemented using partitioned data. Another option is to build an architecture module for maintaining interdependencies using a control table. A control table should contain a row for each shared resource and whether it is in use by an application or not. The batch architecture (Control Program Tasklet) or the application in a parallel job would then retrieve information from that table to determine if it can get access to the resource it needs or not. + + If the data access is not a problem, parallel processing can be implemented in a mainframe environment using parallel job classes, in order to ensure adequate CPU time for all the processes. In an environment other than the mainframe, a similar solution can be put in place with for example threads. The solution has to be robust enough to ensure time slices for all the running processes. + + Other key issues in parallel processing include load balancing and the availability of general system resources such as files, database buffer pools etc. Also note that the control table itself can easily become a critical resource. + + <<4. Partitioning>> + Using partitioning allows multiple versions of large batch applications to run in concurrent. The purpose of this is to reduce the elapsed time required to process long batch jobs. Processes which can be successfully partitioned are those where the input file can be split and/or the main database tables partitioned to allow the application to run against different sets of data. + + In addition, processes which are partitioned must be designed to only process their assigned data set. A partitioning architecture has to be closely tied to the database design and the database partitioning strategy. Please note, that the database partitioning doesn't necessarily mean physical partitioning of the database, although in most cases this is advisable. The following picture illustrates the partitioning approach:!app_style_batch_processing.png|align=center! + + The architecture should be flexible enough to allow dynamic configuration of the number of partitions. Both automatic and user controlled configuration should be considered. Automatic configuration may be based on parameters such as the input file size and/or the number of input records. + + <<4.1 Streaming Approaches>> + The following lists some of the possible streaming approaches. Selecting a streaming approach has to be done on a case-by-case basis. + + <1. Fixed and Even Break-Up of Record Set> + + This involves breaking the input record set into an even number of portions (e.g. 10, where each portion will have exactly 1/10th of the entire record set). Each portion is then processed by one instance of the batch/extract application. + + In order to use this approach, preprocessing will be required to split the recordset up. The result of this split will be a lower and upper bound placement number which can be used as input to the batch/extract application in order to restrict its processing to its portion alone. + + Preprocessing could be a large overhead as it has to calculate and determine the bounds of each portion of the record set. + + <2. Breakup by a Key Column> + + This involves breaking up the input record set by a key column such as a location code, and assigning data from each key to a batch instance. In order to achieve this, column values can either be + + <3. Assigned to a batch instance via a streaming table (see below for details).> + + <4. Assigned to a batch instance by a portion of the value (e.g. values 0000-0999, 1000 - 1999, etc.)> + + Under option 1, addition of new values will mean a manual reconfiguration of the batch/extract to ensure that the new value is added to a particular instance. + + Under option 2, this will ensure that all values are covered via an instance of the batch job. However, the number of values processed by one instance is dependent on the distribution of column values (i.e. there may be a large number of locations in the 0000-0999 range, and few in the 1000-1999 range). Under this option, the data range should be designed with streaming in mind. + + Under both options, the optimal even distribution of records to batch instances cannot be realized. There is no dynamic configuration of the number of batch instances used. + + <5. Breakup by Views> + + This approach is basically breakup by a key column, but on the database level. It involves breaking up the recordset into views. These views will be used by each instance of the batch application during its processing. The breakup will be done by grouping the data. + + With this option, each instance of a batch application will have to be configured to hit a particular view (instead of the master table). Also, with the addition of new data values, this new group of data will have to be included into a view. There is no dynamic configuration capability, as a change in the number of instances will result in a change to the views. + + <6. Addition of a Processing Indicator> + + This involves the addition of a new column to the input table, which acts as an indicator. As a preprocessing step, all indicators would be marked to non-processed. During the record fetch stage of the batch application, records are read on the condition that that record is marked non-processed, and once they are read (with lock), they are marked processing. When that record is completed, the indicator is updated to either complete or error. Many instances of a batch application can be started without an change, as the additional column ensures that a record is only processed once. + + With this option, I/O on the table increased dynamically. In the case of a updating batch application, this impact is reduced, as a write will have to occur anyway. + + <7. Extract Table to a Flat File> + + This involves the extraction of the table into a file. This file can then be split into multiple segments and used as input to the batch instances. + + With this option, the additional overhead of extracting the table into a file, and splitting it, may cancel out the effect of multi-streaming. Dynamic configuration can be achieved via changing the file splitting script. + + <8. Use of a Hashing Column> + + This scheme involves the addition of a hash column (key/index) to the database tables used to retrieve the driver record. This hash column will have an indicator to determine which instance of the batch application will process this particular row. For example, if there are three batch instances to be started, then an indicator of 'A' will mark that row for processing by instance 1, an indicator of 'B' will mark that row for processing by instance 2, etc. + + The procedure used to retrieve the records would then have an additional WHERE clause to select all rows marked by a particular indicator. The inserts in this table would involve the addition of the marker field, which would be defaulted to one of the instances (e.g. 'A'). + + A simple batch application would be used to update the indicators such as to redistribute the load between the different instances. When a sufficiently large number of new rows have been added, this batch can be run (anytime, except in the batch window) to redistribute the new rows to other instances. + + Additional instances of the batch application only require the running of the batch application as above to redistribute the indicators to cater for a new number of instances. + + + 4.2 Database and Application design Principles + + An architecture that supports multi-streamed applications which run against partitioned database tables using the key column approach, should include a central streaming repository for storing streaming parameters. This provides flexibility and ensures maintainability. The repository will generally consist of a single table known as the streaming table. + + Information stored in the streaming table will be static and in general should be maintained by the DBA. The table should consist of one row of information for each stream of a multi-streamed application. The table should have a similar layout to the following table: + + {center} + || Streaming Table || + | Program ID Code + Stream Number (Logical ID of the stream) + Low Value of the db key column for this stream + High Value of the db key column for this stream | + {center} + + On program start-up the program id and stream number should be passed to the application from the architecture (Control Processing Tasklet). These variables are used to read the streaming table, to determine what range of data the application is to process (if a key column approach is used). In addition the stream number must be used throughout the processing to: + + * Add to the output files/database updates in order for the merge process to work properly + + * Report normal processing to the batch log and any errors that occur during execution to the architecture error handler + + 4.3 Minimizing Deadlocks + When applications run in parallel or streamed, contention in database resources and deadlocks may occur. It is critical that the database design team eliminates potential contention situations as far as possible as part of the database design. + + Also ensure that the database index tables are designed with deadlock prevention and performance in mind. + + Deadlocks or hot spots often occur in administration or architecture tables such as log tables, control tables, lock tables etc.. The implications of these should be taken into account as well. A realistic stress test is crucial for identifying the possible bottlenecks in the architecture. + + To minimize the impact of conflicts on data, the architecture should provide services such as wait-and-retry intervals when attaching to a database or when encountering a deadlock. This means a built-in mechanism to react to certain database return codes and instead of issuing an immediate error handling, waiting a predetermined amount of time and retrying the database operation. + + 4.4 Parameter Passing and Validation + + The streaming architecture should be relatively transparent to application developers. The architecture should perform all tasks associated with running the application in a streamed mode i.e. + + * Retrieve streaming parameters before application start-up + + * Validate streaming parameters before application start-up + + * Pass parameters to application at start-up + + The validation should include checks to ensure that: + + * the application has sufficient streams to cover the whole data range + + * there are no gaps between streams + + If the database is partitioned, some additional validation may be necessary to ensure that a single stream does not span database partitions. + + Also the architecture should take into consideration the consolidation of streams. Key questions include: + + * Must all the streams be finished before going into the next job step? + + * What happens if one of the streams aborts? diff --git a/execution/src/site/apt/outline.apt b/execution/src/site/apt/outline.apt new file mode 100644 index 000000000..4a95c4a28 --- /dev/null +++ b/execution/src/site/apt/outline.apt @@ -0,0 +1,124 @@ + ------------------------------------------ + The Spring Batch - Reference Documentation + ---------------------------------------- + Wayne Lund, Waseem Malik, Lucas Ward, Scott Wintermute, + Kerry O'Brien, Tomi Vanek + ------------------------------------------- + May 2007 + +Preface + +*1. {{{introduction.html}Spring Container Batch Processing}} + + *1.1. Overview + + *1.2 Usage Scenarios + +*2. {{{overview.html}Architecture Overview}} + + **2.1. Introduction to Architecture Layers + + **2.2. Batch Applications + + **2.3 Container Application layer + + **2.4 Container Support Layer + + **2.5 The Container Core Layer + + **2.6 Using the Spring-batch infrastructure + + *2.6.1 Infrastructure Provided I/O Support + + *2.6.2 Infrastucture Provided Base Services + + *2.7. Batch Execution Container Configurations + + *2.7.1. Single VM Simple Batch Execution Container - One Job, One Step, One Partition + + *2.7.2. Single VM Multi-threaded Batch Execution Container Configuration - One Job, One Step, Multiple Partitions + + *2.7.3 Batch Execution Container Hosted in J2EE Container - managed environment + + +*3. Core Batch Services + + *3.1. Launching Batch Jobs + + *3.2. Mapping Batch Error Codes to Launch Client Error Codes + + *3.2.1. Returning error codes to Enterprise Schedulers with command line interfaces + + *3.2.2. Web Request - returning error codes to "On-Demand Batch Requests" + + *3.3. Job Services + + *3.3.1. Step Configuration + + *3.3.2. Job Status Service + + *3.3.3. Job Status + + *3.3.4. Job Statistics + + *3.4. Step Services + + *3.4.1. Step Execution + + *3.4.2. Restart Services + + *3.4.3. Skip Services + + *3.4.5. Step Statistics + + *3.5. Data Providers - Operations, Templates and Convenience Callbacks + + *3.5.1. Wrapping input sources + + *3.5.2. Validation of input + + *3.5.3. Delimited File Data Providers + + *3.5.4. Fixed Position File Data Providers + + *3.5.5. XML File Data Providers + + *3.5.6. SQL Input Source Data Provider + + *3.6. Transaction Management + + *3.6.1. Transaction Synchronization with non-transactional resources + + *3.7. Partitioning Batch Jobs + + *3.7.1. Partitioning Strategies + + *3.7.2. Partitioning Job Steps + + *3.7.3. Partition Status + + *3.7.4. Partition Statistics + + *3.7.5. Handling Exceptions within partitions + +*4. [Batch in a J2EE Container] + +*5. [Testing Batch Jobs] + + *5.1. Unit Testing & Mock Objects Provided by the Framework + + *5.2. Integration Testing + + *5.3. Performance Testing Batch Jobs + +*6. {{{samples.html}Practical Examples for Spring Batch}} + + *6.1. Sample Applications|Spring Reference-Application Job-Map + + *6.2. Running the Sample Batch Applications + +*7. [INCUB:Batch XML Schema] + +*8. {{{glossary.html}Glossary}} + + diff --git a/execution/src/site/apt/overview.apt b/execution/src/site/apt/overview.apt new file mode 100644 index 000000000..2edcc8b03 --- /dev/null +++ b/execution/src/site/apt/overview.apt @@ -0,0 +1,145 @@ + ------ + Architecture Overview + ------ + Wayne Lund + ------ + May 2007 + +2. Architecture Overview + + +*2.1 Introduction + + This chapter covers the overall spring batch architecture. The Spring Container Archtiecture is made up of five logical layers; 1) the Batch Application, 2) the Batch Application Layer, 3) the batch core layer, and 4) the batch infrastucture layer. + +*----------*----------------*------------+ +|Provided By | Layer | Description +*----------*----------------*------------* +| Application Developer | Batch Application | This is where the application writes their batch jobs and modules. | +*----------*----------------*------------* +| Spring Batch Execution Container | Container Application Layer | Allows for extending and overwriting of the batch support layer for custom requirements. Facilities implemented in this layer could migrate down to Batch Support Layer. This is also the layer to add the project specific jars required by job types (e.g. reporting jars like Crystal, Brio, etc, form generation jars like Central Pro or Adobe, etc). | +*----------*----------------*------------* +| Spring Batch Execution Container | Container Support Layer | Provides default implementations of batch core services including I/O, Restart, Partitioning, Statistics, and configurations | +*----------*----------------*------------* +| Spring Batch Execution Container | Container Core Layer | Enables configuration, Common Services & Interfaces, management | +*----------*----------------*------------* +| Spring Batch Infrastructure | Batch-Infrastructure | Provides IO support, Batch style transactions, advanced exception handling, batch-template, batch-retry | +*----------*----------------*------------* + + [Figure 2.0] - Batch Architecture Layers + + The batch architecture is modeled after a container architecture, meaning that there are managed resources essential to high performance batch architectures that are configured through a spring context. The following sections will provide a quick review of each layer and their role in the batch architecture. + +*2.2 Batch Applications + +*2.3 Container Application Layer + +*2.4 Container Support Layer + + The batch support layer provides default implementations for all interfaces, interceptors, advice and other core batch services. Figure 2.3.1 illustrates the following logical packages. !Batch Support.png! +Although physically they break out into many more than depicted, logically you can think of the groupings in the following manner: +* I/O Support packages +* Restart Support +* Lifecycle Support packages +* DAO support layer + + **2.4.1 I/O Support Packages + + The I/O related packages are currently the richest packages in the batch architecture. They are modeled after Spring Patterns of Operations and Templates. For example, you'll see FlatFileInputOperations accompanied with a FlatFileInputTemplate. The FlatFileInputTemplate is wired up with a File Descriptor, which contains a Record Descriptor along with various other properties. With the File and Record Descriptors the InputTemplate supports a callback method that allows for the mapping of a record into an object. This support applies to fixed length records, delimited records and XML records. To further simplify this a DefaultFlatFileDataProvider is supplied an input template, which contains the field and record descriptions, along with a line mapper that knows how to map the line to an object. The next() operation on a record simply needs to readAndMap(lineMapper) a record. This pattern is used over again for XML and SQL input for simple mapping of input records to objects. + + In addition to declarative descriptions of the records that can be re-used by multiple batch jobs, the I/O facilities also support configurable validation strategies. The two currently supported are Apache Commons Validator and Spring's VALang. + + **2.4.2 Restart Support + + The Restart Support provides implementations for a few common restart strategies that will be discussed further in the respective section. The following are provided out-of-the-box: + * IDList Restart Strategy - a strategy that supports a batch application where the application does not have a "process" flag and needs the batch architecture to track which records have been processed. This is not the ideal scenario. + * Last Processed Restart Strategy - when the record can be identified through a where and order by only the last record(s) processed needs to be saved for restart. + * No Restart Strategy - some batch jobs simply can't support restart. When they are re-run they are considered to be a new instance of a batch job. + * Sql Restart Strategy - \[need some additional javadoc for this strategy\]. + + **2.4.3 Lifecycle Support + +*2.5. The Container Core Layer + + The Batch Core interfaces and services are illiustrated in a simplied view of a package diagram. There are roughly seven logical packages: + * Core Spring Extensions + * Core Batch Advice + * Core Batch Configuration + * Core Batch Repository + * Core Batch Tasklet + \\ !Batch Core.png! + [Figure 2.5] Batch Core Layer + + In the actual physical packaging there are a few more packages but the above illistration serves as an overview of the logical services that the batch container provides. The following sections will provide an introduction into each set of core batch facilities. + + **2.5.1 Core Spring Extensions + + The Core Spring extensions provide the scaffolding for a batch container. This includes facilities for managing the batch architecture in terms of launching, suspending and stopping batch jobs. There is house keeping that goes on, especially in concurrent batch jobs, related to ensuring that batch jobs quiese properly. The lifecycle management provides services for the proper initialization and subsequent shutdown of batch resources and services. The batch architecture is flexible in terms of how batch jobs may be launched. For example, batch jobs can be started via JMX facilities, scripts from the command line that launch a Java VM. It can also support launching batch jobs through web services or http. There are no restrictions. Finally, there are standard batch error codes. These error codes can be exposed to external utilities, like Schedulers, to ensure that batch jobs expose the status of jobs to an operational environment. This is especially important in the batch context where the modus operandi is headless, meaning unattended operation. + + **2.5.2 Core Batch Advice + + Core Batch Advice is an inventory of the type of advice that batch architectures will inject during the runtime of a batch application. These are defined as a set of extensible interfaces, with a number of default implementations in the support layer that provide some of the most common types of advice. Partition Advice is helpful with large datasets that need to be "chunked" up and run concurrently for better through put. Resource Advice is helpful for registering interest in transactional information so that file locations can be kept in sync with information processed within a transaction. In addition, the resource is associated with the correct step context and its associated configuration properties. Skip advice is applied for records that the module is unable to process. Restart Advice is helpful for Restartable jobs where for advising the job on how to restart. There is considerable variability on how restart can occur. For example, a job may be marking records as "processed" and the restart advice will advise the process with query that restarts the job at the last successfully processed record. Finally, Statistics are vital in operational environments to report on records processed, records skipped and total number of records read. In addition, certain batch jobs lend themselves to custom reporting to expose additional business level information like the number of trades processed or cases opened, etc. + + **2.5.3 Core Batch Configuration + + Batch configuration is considerably different from online web applications or SOA based applications. The Core Batch Configuration provides a place for configuring runtime properties related to the batch application style. This includes the ability to add Commit Policy. In a batch style application it is often advantageous to keep the commit interval as high as possible when processing Logical Units of Work. Whereas in an online web application with declarative transaction the transaction scope would be at the entrance to a business service, a batch transaction scope may include many logical units of work before a transaction commit is executed. A Start Policy allows a configuration to tell the batch job whether it is Restartable, and if so, what type of restart to initiate. Some jobs are not restartable and care should be taken to ensure that information is not applied multiple times when the business rules do not allow for it. Exception policies deal with what to do when exceptions occur. This impacts logging policies and exception handling. The architecture defines a common set of exceptions that projects can apply handlers to like processing errors, validation errors, parsing errors, missing configuration parameters, etc. + + **2.5.4 Container Repository + + This is an internal package for storing the state of a batch job and any associated partition and step status. + + **2.5.5 Core Batch Tasklet + + The core batch module is where control is handed off to the application. There are a number of patterns that have been observed in processing batch data. Spring Core Batch Tasklet implements the most common patterns and provides and extension point for additional Tasklet processing implementations. The basic idea of module provides the facilities for reading and processing data. The simplest implementation of Tasklet, the ReadProcessTasklet, handles both the input and output of data within one class. An alternative implementation, the DataProviderProcessTasklet, provides functionality for 'split processing'. This type of processing is characterized by separating the reading and processing of batch data into two seperate classes: DataProvider and TaskletProcessor. The DataProvider class provides a solid means for reusablility and enforces good architecture practices. Because an object \*must\* be returned by the DataProider to continue processing, (Returning null indicates processing should end) a developer is forced to read in all relevant data, place it into domain or value objects, and return the object. The TaskletProcessor will then use this object within the business logic and final output. + +2.6 Container's Use of batch infrastructure + + **2.6.1 Infrastructure Provided I/O + + The I/O core interfaces and implementations provide facilities for simplifying the extraction of data from I/O sources like files and database tables. The key concepts are FieldDescriptors and FieldSets along with appropriate CallBack Handlers. These are modeled after common spring operations and templates like JdbcTemplate. Through the use of LineMappers a developer needs only to describe a record format and write the appropriate callback method that maps the parsed record into an object of their choice. These can either be true POJO objects or Value Objects (structures) that are subsequently available for the module to processs. The interface for Field Descriptors also allows for a level of validation through the use of Spring's VALang or Apache's Common Validator. + + **2.6.2 Core Batch Interceptors & Interceptor Services + + * Batch Operations & Batch Template + + Interceptors and the associated services are the key to how advise is applied in the batch architecture. The interceptors are Point Cuts in the batch lifecycle that allow the injection of advise. The shared lifecycle behavior abstracted through the BatchLifeCycleInterceptor defineds three methods; init, onError and finalize. All subclasses of LifeCycleInterceptor define default behavior for these three methods. The JobLifecycleInterceptor further exposes the methods beforeJob(), beforeStep(), afterJob(), and afterStep() allowing hooks into the lifecycle for specific advise. The Batch Architecture provides default implementations for all lifecycle point cuts, or interception points. The Tasklet Interceptor, in addition to the standard lifecycle methods, implements logic around beforeLuw(), afterLuw(), commitIntervalStarted() and commitIntervalCompleted(). Having well defined lifecycle interception points allows for the easy insertion of custom advice into the batch runtime environment. + +2.7 Batch Esecution Container Configurations + + In addition to core facilities for configuring or wiring together jobs and steps with required resources, policies, and interceptors, spring batch allows considerable flexibility in how scalability is achieved. More options for scalability will be available in the future. The important key for scalability in Java is the recognition that there is a limit to what one JVM may scale up to in terms of number of threads, managed resources, memory configuration, etc. The spring batch architecture allows for the configuration of simple batch jobs where one VM and one process is sufficient to do perform the work within a batch window all the way through many threads distributed within a cluster of JEE servers. The figure below illistrates the scalability spectrum. + + This is not to be understood as the only way to scale batch jobs as there are many factors. For example, other federated java architectures hold potential like Teracotta or Gigaspaces although there is no current implementation for these distributed models in the current batch architecture. + !scalability-model.png! + [Figure 2.3.1] - Scalability Model + +*2.7.1. Single VM Simple Batch Execution Container - One Job, One Step, One Partition + + The simplest configuration is one job with with step and hence, one implied partition. Implied means that there is nothing for the developer to consider because the default number of partitions is one. There is typically one input source and one output source in this simple configuration. See the Simple Tasklet Job for an example of what this configuration looks like. A simple configuration still typically configures a datasource context, the batch configuration for describing the Job, Step, along with the associated configured policies, field descriptors, and line mappers. !SimpleTradeConfiguration.jpg! + [Figure 2.3.1] Simple Container Configuration + + The details of this configuration will be covered thoroughly in subsequent sections of the document but for now it should be understood that Job, the Step, the input template, the file descriptor with its associated line mapper, and the output (e.g. the TradeWriter). + +*2.3.2. Single VM Multi-threaded Batch Execution Container Configuration - One Job, One Step, Multiple Partitions + + In a Single JVM using partitioning a multi-threaded execution is supported. \[This is still work in progress\] + +*2.3.3. Batch Execution Container Hosted in J2EE Container - managed environment + + The J2EE container model has fallen under fire over the past few years for many valid reasons. There are some things that the J2EE container do very well though that projects should consider when planning for scalability with batch architectures. Commercial and open source containers like WebSphere, BEA and JBOSS typically: + + * manage datasources effectively along with attendent services like prepared statement caching. + + * manage transactions effectively including many configurable properties for long lived transactions. + + * manage thread pools more effectively. + + * supply robust implementations of JTA, a requirement when batch jobs output to multiple XA resources like JMS and JDBC. + + * manage distribution effectively including domains, clusters and cells + + * provide robust JMX management for configuring, managing and administering distributed applications. + + * workload management facilities (clusters) provided by J2EE vendors + + Projects are encouraged to deploy batch applications with the simplest configuration possible, but when federated JVMs are a requirement to process volumes of data within a batch window, batch-in-container provides an effective way of distributing the processing. Spring Batch supports this through a simple change in configuration. \[Work in progress on the exact implementation - being released as part of M2\]. + diff --git a/execution/src/site/apt/samples.apt b/execution/src/site/apt/samples.apt new file mode 100644 index 000000000..e548a7766 --- /dev/null +++ b/execution/src/site/apt/samples.apt @@ -0,0 +1,266 @@ + ------ + Sample Container Applications + ------ + Wayne Lund + ------ + May 2007 + +Overview of Batch Reference Applications + + There is considerable variability in the types of input and output formats in batch jobs. There is also a number of options to consider in terms of how the types of strategies that will be used to handle skips, recovery, and statistics. However, when approaching a new batch job there are a few standard questions to answer to help determine how the job will be written and how to utilize the services offered by the spring batch framework. Consider the following: + + * How do I configure this batch job? In the reference applications the pattern is to follow the convention of Job.xml. Each section with identify the XML definition. + + * What is the input source? Each sample batch job will identify its input source. + + * What is my output source? Each sample batch job will identify its output source. + + * How are records read and validated from the input source? This refers to the input type and its format (e.g. flat file with fixed position, comma separated or XML, etc.) + + * What is the policy of the job if a input record fails the validation step? The most important aspect is whether the record can be skipped so that processing can be continued. + + * How will I process the data and write to the output source? How and what business logic is being applied to the processing of a record. + + * How do I recover from an exception while operating on the output source? There are numerous recovery strategies that can be applied to handling errors on transactional targets. The reference applications will provide a feeling for some of the choices. + + * Can I restart the job and if so which strategy will I use to restart the job? The reference applications will show some of the options available to jobs and what the decision criteria is for the respective choices. + + + [Samples] Reference Applications Table of Features + + +*--------------*-----------------*--------------------*-----------*------------------------*-----------------*------------------*---------------------*-----------*------*---------*-------------------* +| Job / Feature | delimited input | fixed-length input | xml input | db driving query input | db cursor input | delimited output | fixed-length output | db output | skip | restart | quartz scheduling | +*--------------*-----------------*--------------------*-----------*------------------------*-----------------*------------------*---------------------*-----------*------*---------*-------------------* +| simpleTaskletJob | | | | | | | | | | | | +*--------------*-----------------*--------------------*-----------*------------------------*-----------------*------------------*---------------------*-----------*------*---------*-------------------* +| fixedLengthImport | | | | | | | | | | | | +*--------------*-----------------*--------------------*-----------*------------------------*-----------------*------------------*---------------------*-----------*------*---------*-------------------* +| multi-line order | | | | | | | | | | | | +*--------------*-----------------*--------------------*-----------*------------------------*-----------------*------------------*---------------------*-----------*------*---------*-------------------* +| quartzBatch | | | | | | | | | | | | +*--------------*-----------------*--------------------*-----------*------------------------*-----------------*------------------*---------------------*-----------*------*---------*-------------------* +| simple skip sample | | | | | | | | | | | | +*--------------*-----------------*--------------------*-----------*------------------------*-----------------*------------------*---------------------*-----------*------*---------*-------------------* +| Skip And Restart Sample | | | | | | | | | | | | +*--------------*-----------------*--------------------*-----------*------------------------*-----------------*------------------*---------------------*-----------*------*---------*-------------------* +| SQL Cursor Trade Job | | | | | | | | | | | | +*--------------*-----------------*--------------------*-----------*------------------------*-----------------*------------------*---------------------*-----------*------*---------*-------------------* +| Trade Job | | | | | | | | | | | | +*--------------*-----------------*--------------------*-----------*------------------------*-----------------*------------------*---------------------*-----------*------*---------*-------------------* +| XML Job | | | | | | | | | | | | +*--------------*-----------------*--------------------*-----------*------------------------*-----------------*------------------*---------------------*-----------*------*---------*-------------------* + +{Simple Tasklet Job} + + The goal is to show the simplest use of the batch framework with a single job with a single step where the module processes one input source to one output source. + + <> This job is defined by simpleTaskletJob.xml file. Job itself is defined by element simpleTaskletJob. Each job consists of several steps, these steps are defined in steps property. In this example we have only one step. Each step defines module that is responsible for . In this case processing will be handled by SimpleTradeTasklet class. Each module must implement execute() method. All processing of business data should be handled by this method. In this example execute() method tries to read the data from defined input source using read() method and if the data exists, it is processed using process() method. If there is no data to read, method returns false to signal, that there is nothing for further processing. + + <> gets the data from the input template defined and maps it to an object using mapper defined in XML definition. This sample uses FlatFileInputTemplate class as input template. This template reads the whole line from the file and pass it to tokenizer which knows the structure of the line. Location of the file is defined by fileLocatorStrategy property, structurte of the line is defined by fixedFileDescriptor. Result of parsing the line is stored in FieldSet, which is used by mapper to create value object. In our example we use DefaultLineMapper which creates an instance of Trade class. + + <> is quite simple - just writes trade object using DbTradeWriter class. This class writes values obtained from an object to the database. + + <> This job has whole logic implemented in Tasklet. It is not using Data provider as well as Tasklet processor, which is typical way how to handle data. + + + <> simpleTaskletJob.xml + + \[Note: we need to document Spring IDE in setup and installation so we can use to describe the project. Also, if we could also publish we can provide links to the graphics from docs. This is a sample only\]. + + Visualization of the spring configuration through Spring-IDE exposes the structure of a job configuration. The following is the visualization of the Simple Tasklet Job configuration. See {{{http://springide.org/blog/}Spring IDE}}. + +[images/simple-module-job-configuration.jpg]Spring IDE Graph of Simple Tasklet Job Configuration. + + [Figure:]\ Simple Tasklet Job Configuration + + For simplicity we are only displaying the job configuration itself and leaving out the details of the supporting container configuration. The source view of the configuration is as follows: + + [] +-------------------------------------------------------------------------------------- + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +----------------------------------------------------------------------------------------------------------------------- + + You should take the time to make sure you understand the relationship of the xml configuration with the visualization as provided by Spring IDE. \[Note: this will be updated when we use the namespace handler\]. + + + <> file with fixed row structure + + In this example we are using a simple fixed length record structure that can be found in the project at /testBatchRoot/job_data/simpleTaskletJob/input/20070122.teststream.ImportTradeDataStep.txt. There's generally a considerable amount of thought that goes into architecting the folder structures for batch file management. See [provide a link to DefaultFileStrategy]. The only point to note here is the ImportTradeDataStep matches the name of the step in the configuration and the fixed length records look like: + + [] + +------------------------------------------------------------------------------------ + 20070122.teststream.ImportTradeDataStep.txt + + UK21341EAH4597898.34customer1 + UK21341EAH4611218.12customer2 + UK21341EAH4724512.78customer2 + UK21341EAH48108109.25customer3 + UK21341EAH49854123.39customer4 +------------------------------------------------------------------------------------ + + + Looking back to the configuration file you will see where this is documented in the propery of the DefaultRecordDescriptor. You can see the following: + +*--------------*-----------------* +|| FieldName | Length || +*--------------*-----------------* +| ISIN | 12 | +*--------------*-----------------* +| Quantity | 3 | +*--------------*-----------------* +| Price | 5 | +*--------------*-----------------* +| Customer | 9 | +*--------------*-----------------* + + <> database + + <> data provider is not used, all functionality is implemented directly in Tasklet. + + <> module processor is not used, all functionality is implemented directly in Tasklet. + +Fixed Length Import Job + + The goal is to demonstrate a typical scenarion of importing data from a fixed-length file to database + + <> This job shows a more typical scenario, when reading input data and processing the data is cleanly separated. The data provider is responsible for reading input and mapping each record to a domain object, which is then passed to the module processor. The module processor handles the processing of the domain objects, in this case it only writes them to database. + + <> fixedLengthImportJob.xml + + <> file with fixed row structure + + <> database + + <> DefaultFlatFileDataProvider which uses the injected FlatFileInputTemplate to read input and the DefaultLineMapper to map each line to an object according to the file descriptor. + + <> module processor does not do any special processing, it just writes the data to database using a DAO object (called OutputSource in this case, because it is specialized for writing to database, it has no methods for reading data). + +Multiline Order Job + + The goal is to demostrate how to handle a more complex file input format, where a record meant for processing inludes nested records and spans multiple lines + + <> multilineOrderJob.xml + + <> file with multiline records + + <> file with multiline records + + <> OrderDataProvider is an example of a non-default programmatic data provider. It reads input until it detects that the multiline record has finished and encapsulates the record in a single domain object. + + <> module processor passes the object to a an injected 'report service' which in this case writes the output to a file do demonstrate how to use the FlatFileOutputTemplate for writing multiline output according to a file descriptor. + +Quartz Batch + + The goal is to demonstrate how to schedule job execution using Quartz scheduler + + <> quartzBatch.xml + + <> First, declares launcher beans. Each launcher bean is able to launch a job using injected arguments. Second, triggers are declared saying when the launchers should be run. Last, there is the scheduler bean, where the triggers are registered. + +Simple Skip Sample + + +Skip And Restart Sample + + +SQL Cursor Trade Job + + +Trade Job + + The goal is to show a reasonably complex scenario, that would resemble the real-life usage of the framework. + + <> This job has 3 steps. First, data about trades is imported from a file to database. Second, the data about trades is read from the database and credit on customer accounts is decreased appropriately. Last, a report about customers is exported to a file. + + <> tradeJob.xml - the job definition, tradeJobIo.xml - input and output configuration, tradeJobAop.xml - optional AOP logging + + <> This job has 3 steps. First, data about trades is imported from a file to database. Second, the data about trades is read from the database and credit on customer accounts is decreased appropriately. Last, a report about customers is exported to a file. + + +XML Job \ No newline at end of file diff --git a/execution/src/site/resources/images/simple-batch-execution-container.jpg b/execution/src/site/resources/images/simple-batch-execution-container.jpg new file mode 100644 index 000000000..56c634253 Binary files /dev/null and b/execution/src/site/resources/images/simple-batch-execution-container.jpg differ diff --git a/execution/src/site/resources/images/simple-module-job-configuration.jpg b/execution/src/site/resources/images/simple-module-job-configuration.jpg new file mode 100644 index 000000000..6c223d8b9 Binary files /dev/null and b/execution/src/site/resources/images/simple-module-job-configuration.jpg differ diff --git a/execution/src/site/site.xml b/execution/src/site/site.xml new file mode 100644 index 000000000..f63e46050 --- /dev/null +++ b/execution/src/site/site.xml @@ -0,0 +1,31 @@ + + + + Spring Batch: ${project.name} + + + images/shim.gif + + + + + + org.springframework.maven.skins + maven-spring-skin + 1.0.3 + + + + + + + + + + + + + + + + diff --git a/execution/src/test/java/org/springframework/batch/execution/bootstrap/BatchCommandLineLauncherTests.java b/execution/src/test/java/org/springframework/batch/execution/bootstrap/BatchCommandLineLauncherTests.java new file mode 100644 index 000000000..2fd8aa202 --- /dev/null +++ b/execution/src/test/java/org/springframework/batch/execution/bootstrap/BatchCommandLineLauncherTests.java @@ -0,0 +1,75 @@ +/* + * 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.execution.bootstrap; + +import org.springframework.batch.core.configuration.NoSuchJobConfigurationException; +import org.springframework.batch.execution.bootstrap.BatchCommandLineLauncher; + +import junit.framework.TestCase; + +/** + * @author Dave Syer + * + */ +public class BatchCommandLineLauncherTests extends TestCase { + + BatchCommandLineLauncher commandLine = new BatchCommandLineLauncher(); + + int count = 0; + + /** + * Test method for {@link org.springframework.batch.execution.bootstrap.BatchCommandLineLauncher#main(java.lang.String[])}. + * @throws Exception + */ + public void testMainWithDefaultArguments() throws Exception { + BatchCommandLineLauncher.main(new String[0]); + // TODO: find a way to assert something. No error actually + // means the test was successful... + } + + /** + * Test method for {@link org.springframework.batch.execution.bootstrap.BatchCommandLineLauncher#main(java.lang.String[])}. + * @throws Exception + */ + public void testMainWithParentContext() throws Exception { + // Try an XML file name for the parent context with no suffix + BatchCommandLineLauncher.main(new String[]{"job-configuration"}); + } + + /** + * Test method for {@link org.springframework.batch.execution.bootstrap.BatchCommandLineLauncher#main(java.lang.String[])}. + * @throws Exception + */ + public void testMainWithParentContextAndValidJobId() throws Exception { + // Try a job id as the second argument + BatchCommandLineLauncher.main(new String[]{"job-configuration", "test-job"}); + } + + /** + * Test method for {@link org.springframework.batch.execution.bootstrap.BatchCommandLineLauncher#main(java.lang.String[])}. + * @throws Exception + */ + public void testMainWithParentContextAndInvalidJobId() throws Exception { + // Try a job id as the second argument test-job + try { + BatchCommandLineLauncher.main(new String[]{"job-configuration", "foo-bar-spam"}); + fail("Expected NoSuchJobConfigurationException"); + } + catch (NoSuchJobConfigurationException e) { + // expected + } + } +} diff --git a/execution/src/test/java/org/springframework/batch/execution/bootstrap/BatchExecutionRequestEventTests.java b/execution/src/test/java/org/springframework/batch/execution/bootstrap/BatchExecutionRequestEventTests.java new file mode 100644 index 000000000..33fac69da --- /dev/null +++ b/execution/src/test/java/org/springframework/batch/execution/bootstrap/BatchExecutionRequestEventTests.java @@ -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.execution.bootstrap; + +import junit.framework.TestCase; + +import org.springframework.batch.execution.bootstrap.BatchExecutionRequestEvent; +import org.springframework.context.ApplicationEvent; + +/** + * @author Dave Syer + * + */ +public class BatchExecutionRequestEventTests extends TestCase { + + /** + * Test method for {@link org.springframework.batch.execution.bootstrap.BatchExecutionRequestEvent#BatchContainerRequestEvent(java.lang.Object)}. + */ + public void testBatchContainerRequestEvent() { + ApplicationEvent event = new BatchExecutionRequestEvent(this); + assertEquals(this, event.getSource()); + } + +} diff --git a/execution/src/test/java/org/springframework/batch/execution/bootstrap/SimpleJobLauncherTests.java b/execution/src/test/java/org/springframework/batch/execution/bootstrap/SimpleJobLauncherTests.java new file mode 100644 index 000000000..4d9fcc372 --- /dev/null +++ b/execution/src/test/java/org/springframework/batch/execution/bootstrap/SimpleJobLauncherTests.java @@ -0,0 +1,195 @@ +/* + * 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.execution.bootstrap; + +import junit.framework.TestCase; + +import org.easymock.MockControl; +import org.springframework.batch.core.configuration.JobConfiguration; +import org.springframework.batch.core.runtime.JobIdentifier; +import org.springframework.batch.core.runtime.JobIdentifierFactory; +import org.springframework.batch.core.runtime.SimpleJobIdentifier; +import org.springframework.batch.execution.JobExecutorFacade; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.context.support.GenericApplicationContext; + +public class SimpleJobLauncherTests extends TestCase { + + public void testAutoStartContainer() throws Exception { + + MockControl containerControl = MockControl.createControl(JobExecutorFacade.class); + JobExecutorFacade mockContainer; + + AbstractJobLauncher bootstrap = new SimpleJobLauncher(); + final SimpleJobIdentifier runtimeInformation = new SimpleJobIdentifier("foo"); + bootstrap.setJobRuntimeInformationFactory(new JobIdentifierFactory() { + public JobIdentifier getJobIdentifier(String name) { + return runtimeInformation; + } + }); + mockContainer = (JobExecutorFacade) containerControl.getMock(); + bootstrap.setBatchContainer(mockContainer); + + JobConfiguration jobConfiguration = new JobConfiguration("foo"); + bootstrap.setJobConfigurationName(jobConfiguration.getName()); + + mockContainer.start(runtimeInformation); + containerControl.replay(); + + bootstrap.setAutoStart(true); + + bootstrap.onApplicationEvent(new ContextRefreshedEvent(new GenericApplicationContext())); + // It ran and then stopped... + assertFalse(bootstrap.isRunning()); + + containerControl.verify(); + } + + public void testApplicationEventNotContextRefresh() throws Exception { + + MockControl containerControl = MockControl.createControl(JobExecutorFacade.class); + JobExecutorFacade mockContainer; + + AbstractJobLauncher bootstrap = new SimpleJobLauncher(); + mockContainer = (JobExecutorFacade) containerControl.getMock(); + bootstrap.setBatchContainer(mockContainer); + + containerControl.replay(); + + bootstrap.setAutoStart(true); + + bootstrap.onApplicationEvent(new ApplicationEvent(new GenericApplicationContext()) { + }); + assertFalse(bootstrap.isRunning()); + + containerControl.verify(); + } + + public void testStartWithNoConfiguration() throws Exception { + final AbstractJobLauncher bootstrap = new SimpleJobLauncher(); + try { + bootstrap.afterPropertiesSet(); + fail("Expected IllegalArgumentException"); + } + catch (IllegalArgumentException e) { + // expected + assertTrue(e.getMessage().indexOf("required") >= 0); + } + } + + public void testInitializeWithNoConfiguration() throws Exception { + final AbstractJobLauncher bootstrap = new SimpleJobLauncher(); + try { + bootstrap.start(); + // should do nothing + } + catch (Exception e) { + fail("Unexpected IllegalStateException"); + } + } + + public void testStartTwiceNotFatal() throws Exception { + AbstractJobLauncher bootstrap = new SimpleJobLauncher(); + final SimpleJobIdentifier runtimeInformation = new SimpleJobIdentifier("foo"); + bootstrap.setJobRuntimeInformationFactory(new JobIdentifierFactory() { + public JobIdentifier getJobIdentifier(String name) { + return runtimeInformation; + } + }); + InterruptibleContainer container = new InterruptibleContainer(); + bootstrap.setBatchContainer(container); + bootstrap.setJobConfigurationName(new JobConfiguration("foo").getName()); + bootstrap.start(); + bootstrap.start(); + // Both jobs finished running because they were not launched in a new + // Thread + assertFalse(bootstrap.isRunning()); + } + + public void testInterruptContainer() throws Exception { + final AbstractJobLauncher bootstrap = new SimpleJobLauncher(); + final SimpleJobIdentifier runtimeInformation = new SimpleJobIdentifier("foo"); + bootstrap.setJobRuntimeInformationFactory(new JobIdentifierFactory() { + public JobIdentifier getJobIdentifier(String name) { + return runtimeInformation; + } + }); + + InterruptibleContainer container = new InterruptibleContainer(); + bootstrap.setBatchContainer(container); + bootstrap.setJobConfigurationName(new JobConfiguration("foo").getName()); + + Thread bootstrapThread = new Thread() { + public void run() { + bootstrap.start(); + } + }; + + bootstrapThread.start(); + + // give the thread a second to start up + Thread.sleep(100); + assertTrue(bootstrap.isRunning()); + bootstrap.stop(); + Thread.sleep(100); + assertFalse(bootstrap.isRunning()); + } + + public void testStopOnUnstartedContainer() { + + AbstractJobLauncher bootstrap = new SimpleJobLauncher(); + + assertFalse(bootstrap.isRunning()); + // no exception should be thrown if stop is called on unstarted + // container + // this is to fullfill the contract outlined in Lifecycle#stop(). + bootstrap.stop(); + } + + private class InterruptibleContainer implements JobExecutorFacade { + /* + * (non-Javadoc) + * @see org.springframework.batch.container.BatchContainer#start() + */ + public void start() { + try { + // 1 seconds should be long enough to allow the thread to be + // started and + // for interrupt to be called; + Thread.sleep(300); + } + catch (InterruptedException ex) { + // thread interrupted, allow to exit normally + } + } + + public void start(JobIdentifier runtimeInformation) { + start(); + } + + public void stop(JobIdentifier runtimeInformation) { + // not needed + } + + public boolean isRunning() { + // not needed + return false; + } + } + +} diff --git a/execution/src/test/java/org/springframework/batch/execution/bootstrap/TaskExecutorJobLauncherTests.java b/execution/src/test/java/org/springframework/batch/execution/bootstrap/TaskExecutorJobLauncherTests.java new file mode 100644 index 000000000..b773a2e43 --- /dev/null +++ b/execution/src/test/java/org/springframework/batch/execution/bootstrap/TaskExecutorJobLauncherTests.java @@ -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.execution.bootstrap; + +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +import javax.management.Notification; + +import junit.framework.TestCase; + +import org.easymock.MockControl; +import org.springframework.batch.core.configuration.JobConfiguration; +import org.springframework.batch.core.configuration.NoSuchJobConfigurationException; +import org.springframework.batch.core.runtime.JobIdentifier; +import org.springframework.batch.core.runtime.JobIdentifierFactory; +import org.springframework.batch.core.runtime.SimpleJobIdentifier; +import org.springframework.batch.execution.JobExecutorFacade; +import org.springframework.batch.repeat.interceptor.RepeatOperationsApplicationEvent; +import org.springframework.batch.statistics.StatisticsProvider; +import org.springframework.batch.support.PropertiesConverter; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.core.task.SimpleAsyncTaskExecutor; +import org.springframework.jmx.export.notification.NotificationPublisher; +import org.springframework.jmx.export.notification.UnableToSendNotificationException; + +public class TaskExecutorJobLauncherTests extends TestCase { + + private TaskExecutorJobLauncher bootstrap = new TaskExecutorJobLauncher(); + + protected void setUp() throws Exception { + super.setUp(); + final SimpleJobIdentifier runtimeInformation = new SimpleJobIdentifier("foo"); + bootstrap.setJobRuntimeInformationFactory(new JobIdentifierFactory() { + public JobIdentifier getJobIdentifier(String name) { + return runtimeInformation; + } + }); + } + + public void testStopContainer() throws Exception { + + // Important (otherwise start() does not return!) + bootstrap.setTaskExecutor(new SimpleAsyncTaskExecutor()); + + InterruptibleContainer container = new InterruptibleContainer(); + bootstrap.setBatchContainer(container); + bootstrap.setJobConfigurationName(new JobConfiguration("foo").getName()); + + bootstrap.start(); + // give the thread some time to start up: + Thread.sleep(100); + assertTrue(bootstrap.isRunning()); + bootstrap.stop(); + // ...and to shut down: + Thread.sleep(400); + assertFalse(bootstrap.isRunning()); + } + + public void testNormalApplicationEventNotRecognized() throws Exception { + bootstrap.onApplicationEvent(new ApplicationEvent("foo") {}); + // nothing happens + } + + public void testRepeatOperationsBeforeNotUsed() throws Exception { + final List list = new ArrayList(); + bootstrap.setNotificationPublisher(new NotificationPublisher() { + public void sendNotification(Notification notification) throws UnableToSendNotificationException { + list.add(notification); + } + }); + bootstrap.onApplicationEvent(new RepeatOperationsApplicationEvent(this, "foo", RepeatOperationsApplicationEvent.BEFORE) {}); + assertEquals(0, list.size()); + } + + public void testRepeatOperationsOpenUsed() throws Exception { + final List list = new ArrayList(); + bootstrap.setNotificationPublisher(new NotificationPublisher() { + public void sendNotification(Notification notification) throws UnableToSendNotificationException { + list.add(notification); + } + }); + bootstrap.onApplicationEvent(new RepeatOperationsApplicationEvent(this, "foo", RepeatOperationsApplicationEvent.OPEN)); + assertEquals(1, list.size()); + assertEquals("foo", ((Notification) list.get(0)).getMessage().substring(0, 3)); + } + + public void testStatisticsRetrieved() throws Exception { + MockControl control = MockControl.createControl(JobExecutorFacadeWithStatistics.class); + JobExecutorFacadeWithStatistics batchContainer = (JobExecutorFacadeWithStatistics) control.getMock(); + bootstrap.setBatchContainer(batchContainer); + + Properties properties = PropertiesConverter.stringToProperties("a=b"); + control.expectAndReturn(batchContainer.getStatistics(), properties); + + control.replay(); + assertEquals(properties, bootstrap.getStatistics()); + control.verify(); + } + + public void testStatisticsNotRetrieved() throws Exception { + MockControl control = MockControl.createControl(JobExecutorFacade.class); + JobExecutorFacade batchContainer = (JobExecutorFacade) control.getMock(); + bootstrap.setBatchContainer(batchContainer); + + Properties properties = new Properties(); + control.replay(); + assertEquals(properties, bootstrap.getStatistics()); + control.verify(); + } + + private class InterruptibleContainer implements JobExecutorFacade { + private volatile boolean running = true; + + public void start() { + while (running) { + try { + // 1 seconds should be long enough to allow the thread to be + // started and + // for interrupt to be called; + Thread.sleep(300); + } + catch (InterruptedException ex) { + // thread intterrupted, allow to exit normally + } + } + } + + public void start(JobIdentifier runtimeInformation) { + start(); + } + + public void stop(JobIdentifier runtimeInformation) { + running = false; + } + + public boolean isRunning() { + // not needed + return false; + } + } + + public void testPublishApplicationEvent() throws Exception { + final List list = new ArrayList(); + bootstrap.setApplicationEventPublisher(new ApplicationEventPublisher() { + public void publishEvent(ApplicationEvent event) { + list.add(event); + } + }); + + MockControl control = MockControl.createControl(JobExecutorFacade.class); + JobExecutorFacade batchContainer = (JobExecutorFacade) control.getMock(); + bootstrap.setBatchContainer(batchContainer); + SimpleJobIdentifier jobRuntimeInformation = new SimpleJobIdentifier("spam"); + batchContainer.start(jobRuntimeInformation); + control.setThrowable(new NoSuchJobConfigurationException("SPAM")); + + control.replay(); + bootstrap.start(jobRuntimeInformation); + assertEquals(1, list.size()); + control.verify(); + } + + private interface JobExecutorFacadeWithStatistics extends JobExecutorFacade, StatisticsProvider { + } + +} diff --git a/execution/src/test/java/org/springframework/batch/execution/configuration/JobConfigurationRegistryBeanPostProcessorTests.java b/execution/src/test/java/org/springframework/batch/execution/configuration/JobConfigurationRegistryBeanPostProcessorTests.java new file mode 100644 index 000000000..ad4ed6f50 --- /dev/null +++ b/execution/src/test/java/org/springframework/batch/execution/configuration/JobConfigurationRegistryBeanPostProcessorTests.java @@ -0,0 +1,94 @@ +/* + * 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.execution.configuration; + +import junit.framework.TestCase; + +import org.springframework.batch.core.configuration.DuplicateJobConfigurationException; +import org.springframework.batch.core.configuration.JobConfiguration; +import org.springframework.batch.core.configuration.NoSuchJobConfigurationException; +import org.springframework.batch.execution.configuration.JobConfigurationRegistryBeanPostProcessor; +import org.springframework.batch.execution.configuration.MapJobConfigurationRegistry; +import org.springframework.beans.FatalBeanException; + +/** + * @author Dave Syer + * + */ +public class JobConfigurationRegistryBeanPostProcessorTests extends TestCase { + + private JobConfigurationRegistryBeanPostProcessor processor = new JobConfigurationRegistryBeanPostProcessor(); + + public void testInitialization() throws Exception { + try { + processor.afterPropertiesSet(); + fail("Expected IllegalArgumentException"); + } + catch (IllegalArgumentException e) { + // expected + assertTrue(e.getMessage().indexOf("JobConfigurationRegistry") >= 0); + } + } + + public void testBeforeInitialization() throws Exception { + // should be a no-op + assertEquals("foo", processor.postProcessAfterInitialization("foo", "bar")); + } + + public void testAfterInitializationWithWrongType() throws Exception { + // should be a no-op + assertEquals("foo", processor.postProcessAfterInitialization("foo", "bar")); + } + + public void testAfterInitializationWithCorrectType() throws Exception { + MapJobConfigurationRegistry registry = new MapJobConfigurationRegistry(); + processor.setJobConfigurationRegistry(registry); + JobConfiguration configuration = new JobConfiguration(); + configuration.setName("foo"); + assertEquals(configuration, processor.postProcessAfterInitialization(configuration, "bar")); + assertEquals(configuration, registry.getJobConfiguration("foo")); + } + + public void testAfterInitializationWithDuplicate() throws Exception { + MapJobConfigurationRegistry registry = new MapJobConfigurationRegistry(); + processor.setJobConfigurationRegistry(registry); + JobConfiguration configuration = new JobConfiguration(); + configuration.setName("foo"); + processor.postProcessAfterInitialization(configuration, "bar"); + try { + processor.postProcessAfterInitialization(configuration, "spam"); + fail("Expected FatalBeanException"); + } catch (FatalBeanException e) { + // Expected + assertTrue(e.getCause() instanceof DuplicateJobConfigurationException); + } + } + + public void testUnregisterOnDestroy() throws Exception { + MapJobConfigurationRegistry registry = new MapJobConfigurationRegistry(); + processor.setJobConfigurationRegistry(registry); + JobConfiguration configuration = new JobConfiguration(); + configuration.setName("foo"); + assertEquals(configuration, processor.postProcessAfterInitialization(configuration, "bar")); + processor.destroy(); + try { + assertEquals(null, registry.getJobConfiguration("foo")); + fail("Expected NoSuchJobConfigurationException"); + } catch (NoSuchJobConfigurationException e) { + // expected + } + } +} diff --git a/execution/src/test/java/org/springframework/batch/execution/configuration/MapJobConfigurationRegistryTests.java b/execution/src/test/java/org/springframework/batch/execution/configuration/MapJobConfigurationRegistryTests.java new file mode 100644 index 000000000..46d89078a --- /dev/null +++ b/execution/src/test/java/org/springframework/batch/execution/configuration/MapJobConfigurationRegistryTests.java @@ -0,0 +1,90 @@ +/* + * 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.execution.configuration; + +import junit.framework.TestCase; + +import org.springframework.batch.core.configuration.DuplicateJobConfigurationException; +import org.springframework.batch.core.configuration.JobConfiguration; +import org.springframework.batch.core.configuration.NoSuchJobConfigurationException; +import org.springframework.batch.execution.configuration.MapJobConfigurationRegistry; + +/** + * @author Dave Syer + * + */ +public class MapJobConfigurationRegistryTests extends TestCase { + + private MapJobConfigurationRegistry registry = new MapJobConfigurationRegistry(); + + /** + * Test method for {@link org.springframework.batch.execution.configuration.MapJobConfigurationRegistry#unregister(org.springframework.batch.core.configuration.JobConfiguration)}. + * @throws Exception + */ + public void testUnregister() throws Exception { + registry.register(new JobConfiguration("foo")); + assertNotNull(registry.getJobConfiguration("foo")); + registry.unregister(new JobConfiguration("foo")); + try { + assertNull(registry.getJobConfiguration("foo")); + fail("Expected NoSuchJobConfigurationException"); + } + catch (NoSuchJobConfigurationException e) { + // expected + assertTrue(e.getMessage().indexOf("foo")>=0); + } + } + + /** + * Test method for {@link org.springframework.batch.execution.configuration.MapJobConfigurationRegistry#getJobConfiguration(java.lang.String)}. + */ + public void testReplaceDuplicateConfiguration() throws Exception { + registry.register(new JobConfiguration("foo")); + try { + registry.register(new JobConfiguration("foo")); + } catch (DuplicateJobConfigurationException e) { + fail("Unexpected DuplicateJobConfigurationException"); + // expected + assertTrue(e.getMessage().indexOf("foo")>=0); + } + } + + /** + * Test method for {@link org.springframework.batch.execution.configuration.MapJobConfigurationRegistry#getJobConfiguration(java.lang.String)}. + */ + public void testRealDuplicateConfiguration() throws Exception { + JobConfiguration jobConfiguration = new JobConfiguration("foo"); + registry.register(jobConfiguration); + try { + registry.register(jobConfiguration); + fail("Unexpected DuplicateJobConfigurationException"); + } catch (DuplicateJobConfigurationException e) { + // expected + assertTrue(e.getMessage().indexOf("foo")>=0); + } + } + + /** + * Test method for {@link org.springframework.batch.execution.configuration.MapJobConfigurationRegistry#getJobConfigurations()}. + * @throws Exception + */ + public void testGetJobConfigurations() throws Exception { + registry.register(new JobConfiguration("foo")); + registry.register(new JobConfiguration("bar")); + assertEquals(2, registry.getJobConfigurations().size()); + } + +} diff --git a/execution/src/test/java/org/springframework/batch/execution/facade/BatchResourceFactoryBeanTests.java b/execution/src/test/java/org/springframework/batch/execution/facade/BatchResourceFactoryBeanTests.java new file mode 100644 index 000000000..68e9cc4a8 --- /dev/null +++ b/execution/src/test/java/org/springframework/batch/execution/facade/BatchResourceFactoryBeanTests.java @@ -0,0 +1,125 @@ +/* + * 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.execution.facade; + +import java.util.Calendar; + +import junit.framework.TestCase; + +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; + +/** + * Unit tests for {@link BatchResourceFactoryBean} + * + * @author robert.kasanicky + * @author Lucas Ward + * @author Dave Syer + */ +public class BatchResourceFactoryBeanTests extends TestCase { + + /** + * object under test + */ + private BatchResourceFactoryBean resourceFactory = new BatchResourceFactoryBean(); + + private String PATTERN_STRING = "%BATCH_ROOT%\\%JOB_NAME%-%SCHEDULE_DATE%-%JOB_RUN%-%STREAM_NAME%"; + + private String EXPECTED_ABSOLUTE_PATH = "C:\\temp\\testJob-20070730-0-testStream"; + + private String NULL_JOB_NAME_PATH = "C:\\temp\\%JOB_NAME%-20070730-0-testStream"; + + /** + * mock step context + */ + + protected void setUp() throws Exception { + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.YEAR, 2007); + calendar.set(Calendar.MONTH, Calendar.JULY); + calendar.set(Calendar.DAY_OF_MONTH, 30); + + // define mock behaviour + resourceFactory.setScheduleDate("20070730"); + resourceFactory.setRootDirectory("C:\\temp"); + resourceFactory.setJobName("testJob"); + resourceFactory.setJobStream("testStream"); + resourceFactory.setJobRun(0); + resourceFactory.setStepName("testStep"); + resourceFactory.setFilePattern(PATTERN_STRING); + + resourceFactory.afterPropertiesSet(); + } + + /** + * regular use with valid context and pattern provided + */ + public void testCreateFileName() throws Exception { + + Resource resource = (Resource) resourceFactory.getObject(); + + String returnedPath = resource.getFile().getAbsolutePath(); + + assertEquals(returnedPath, EXPECTED_ABSOLUTE_PATH); + } + + /** + * Set the job name to null and attempt to get the resource, %JOB_NAME% + * should not be replaced. + */ + public void testNullJobName() throws Exception { + + resourceFactory.setJobName(null); + // set singleton to false so a new instance is returned. + resourceFactory.setSingleton(false); + + Resource resource = (Resource) resourceFactory.getObject(); + + assertEquals(NULL_JOB_NAME_PATH, resource.getFile().getAbsolutePath()); + } + + public void testObjectType() throws Exception { + assertEquals(Resource.class, resourceFactory.getObjectType()); + } + + public void testNullFilePattern() throws Exception { + resourceFactory = new BatchResourceFactoryBean(); + resourceFactory.setSingleton(false); + resourceFactory.setFilePattern(null); + try { + resourceFactory.getObject(); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + // expected + } + } + + public void testResoureLoaderAware() throws Exception { + resourceFactory = new BatchResourceFactoryBean(); + resourceFactory.setSingleton(false); + resourceFactory.setResourceLoader(new DefaultResourceLoader() { + public Resource getResource(String location) { + return new ByteArrayResource("foo".getBytes()); + } + }); + Resource resource = (Resource) resourceFactory.getObject(); + assertNotNull(resource); + assertTrue(resource.exists()); + } + +} diff --git a/execution/src/test/java/org/springframework/batch/execution/facade/EmptyItemProcessor.java b/execution/src/test/java/org/springframework/batch/execution/facade/EmptyItemProcessor.java new file mode 100644 index 000000000..b6d27ae1a --- /dev/null +++ b/execution/src/test/java/org/springframework/batch/execution/facade/EmptyItemProcessor.java @@ -0,0 +1,61 @@ +/* + * 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.execution.facade; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.batch.item.ItemProcessor; +import org.springframework.batch.support.transaction.TransactionAwareProxyFactory; +import org.springframework.beans.factory.InitializingBean; + +public class EmptyItemProcessor implements ItemProcessor, InitializingBean { + + private boolean failed = false; + + // point at which to fail... + private int failurePoint = Integer.MAX_VALUE; + + protected Log logger = LogFactory.getLog(EmptyItemProcessor.class); + + List list; + + public void afterPropertiesSet() throws Exception { + TransactionAwareProxyFactory factory = new TransactionAwareProxyFactory(new ArrayList()); + list = (List) factory.createInstance(); + } + + public void setFailurePoint(int failurePoint) { + this.failurePoint = failurePoint; + } + + public void process(Object data) { + if (!failed && list.size() == failurePoint) { + failed = true; + throw new RuntimeException("Failed processing: [" + data + "]"); + } + logger.info("Processing: [" + data + "]"); + list.add(data); + } + + public List getList() { + return list; + } + +} diff --git a/execution/src/test/java/org/springframework/batch/execution/facade/JobConfigurationTests.java b/execution/src/test/java/org/springframework/batch/execution/facade/JobConfigurationTests.java new file mode 100644 index 000000000..540308dd8 --- /dev/null +++ b/execution/src/test/java/org/springframework/batch/execution/facade/JobConfigurationTests.java @@ -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.execution.facade; + +import junit.framework.TestCase; + +import org.springframework.batch.core.configuration.JobConfiguration; +import org.springframework.context.support.StaticApplicationContext; + +public class JobConfigurationTests extends TestCase { + + public void testBeanName() throws Exception { + StaticApplicationContext context = new StaticApplicationContext(); + JobConfiguration configuration = new JobConfiguration(); + context.getAutowireCapableBeanFactory().initializeBean(configuration, "bean"); + assertNotNull(configuration.getName()); + configuration.setName("foo"); + context.getAutowireCapableBeanFactory().initializeBean(configuration, "bean"); + assertEquals("foo", configuration.getName()); + } +} diff --git a/execution/src/test/java/org/springframework/batch/execution/facade/SimpleJobExecutorFacaderTests.java b/execution/src/test/java/org/springframework/batch/execution/facade/SimpleJobExecutorFacaderTests.java new file mode 100644 index 000000000..3f07e3bff --- /dev/null +++ b/execution/src/test/java/org/springframework/batch/execution/facade/SimpleJobExecutorFacaderTests.java @@ -0,0 +1,222 @@ +/* + * 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.execution.facade; + +import java.util.Collections; +import java.util.Properties; + +import junit.framework.TestCase; + +import org.easymock.MockControl; +import org.springframework.batch.core.configuration.JobConfiguration; +import org.springframework.batch.core.configuration.JobConfigurationLocator; +import org.springframework.batch.core.configuration.NoSuchJobConfigurationException; +import org.springframework.batch.core.configuration.StepConfiguration; +import org.springframework.batch.core.domain.JobInstance; +import org.springframework.batch.core.executor.JobExecutor; +import org.springframework.batch.core.executor.StepExecutor; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.runtime.JobExecutionContext; +import org.springframework.batch.core.runtime.JobExecutionRegistry; +import org.springframework.batch.core.runtime.SimpleJobIdentifier; +import org.springframework.batch.core.runtime.StepExecutionContext; +import org.springframework.batch.execution.NoSuchJobExecutionException; +import org.springframework.batch.io.exception.BatchCriticalException; +import org.springframework.batch.repeat.ExitStatus; +import org.springframework.batch.repeat.context.RepeatContextSupport; + +/** + * SimpleBatchContainer unit tests. + * + * @author Lucas Ward + * @author Dave Syer + */ +public class SimpleJobExecutorFacaderTests extends TestCase { + + SimpleJobExecutorFacade simpleContainer = new SimpleJobExecutorFacade(); + + JobExecutor jobExecutor; + + MockControl jobLifecycleControl = MockControl.createControl(JobExecutor.class); + + JobRepository jobRepository; + + MockControl jobRepositoryControl = MockControl.createControl(JobRepository.class); + + JobConfiguration jobConfiguration = new JobConfiguration(); + + StepExecutor stepExecutor = new StepExecutor() { + public ExitStatus process(StepConfiguration configuration, StepExecutionContext stepExecutionContext) throws BatchCriticalException { + return ExitStatus.FINISHED; + } + }; + + protected void setUp() throws Exception { + + super.setUp(); + jobConfiguration.setName("TestJob"); + jobExecutor = (JobExecutor) jobLifecycleControl.getMock(); + simpleContainer.setJobExecutor(jobExecutor); + jobRepository = (JobRepository) jobRepositoryControl.getMock(); + simpleContainer.setJobRepository(jobRepository); + } + + public void testNormalStart() throws Exception { + + final SimpleJobIdentifier jobRuntimeInformation = new SimpleJobIdentifier("bar"); + jobRepository.findOrCreateJob(jobConfiguration, jobRuntimeInformation); + jobExecutor = new JobExecutor() { + public void run(JobConfiguration configuration, JobExecutionContext jobExecutionContext) throws BatchCriticalException { + jobExecutionContext.getJob().setIdentifier(jobRuntimeInformation); + } + }; + JobInstance job = new JobInstance(); + JobExecutionContext jobExecutionContext = new JobExecutionContext(jobRuntimeInformation, job); + jobRepositoryControl.setReturnValue(job); + jobExecutor.run(jobConfiguration, jobExecutionContext); + jobRepositoryControl.replay(); + simpleContainer.setJobConfigurationLocator(new JobConfigurationLocator() { + public JobConfiguration getJobConfiguration(String name) throws NoSuchJobConfigurationException { + return jobConfiguration; + } + }); + simpleContainer.start(jobRuntimeInformation); + assertEquals(job, jobExecutionContext.getJob()); + assertEquals("bar", job.getName()); + jobRepositoryControl.verify(); + } + + private volatile boolean running = false; + + public void testIsRunning() throws Exception { + simpleContainer.setJobExecutor(new JobExecutor() { + public void run(JobConfiguration configuration, JobExecutionContext jobExecutionContext) + throws BatchCriticalException { + while (running) { + try { + Thread.sleep(100L); + } + catch (InterruptedException e) { + throw new BatchCriticalException("Interrupted unexpectedly!"); + } + } + } + }); + simpleContainer.setJobConfigurationLocator(new JobConfigurationLocator() { + public JobConfiguration getJobConfiguration(String name) throws NoSuchJobConfigurationException { + return jobConfiguration; + } + }); + final SimpleJobIdentifier jobRuntimeInformation = new SimpleJobIdentifier("foo"); + jobRepository.findOrCreateJob(jobConfiguration, jobRuntimeInformation); + JobInstance job = new JobInstance(); + jobRepositoryControl.setReturnValue(job); + jobRepositoryControl.replay(); + + running = true; + new Thread(new Runnable() { + public void run() { + try { + simpleContainer.start(jobRuntimeInformation); + } + catch (NoSuchJobConfigurationException e) { + System.err.println("Shouldn't happen"); + } + } + }).start(); + // Give Thread time to start + Thread.sleep(100L); + assertTrue(simpleContainer.isRunning()); + running = false; + int count = 0; + while(simpleContainer.isRunning() && count ++<5) { + Thread.sleep(100L); + } + assertFalse(simpleContainer.isRunning()); + jobRepositoryControl.verify(); + } + + public void testInvalidState() throws Exception { + + simpleContainer.setJobExecutor(null); + + try { + simpleContainer.start(new SimpleJobIdentifier("TestJob")); + fail("Expected IllegalStateException"); + } + catch (IllegalStateException ex) { + // expected + } + } + + public void testStopWithNoJob() throws Exception { + MockControl control = MockControl.createControl(JobExecutionRegistry.class); + JobExecutionRegistry jobExecutionRegistry = (JobExecutionRegistry) control.getMock(); + simpleContainer.setJobExecutionRegistry(jobExecutionRegistry); + SimpleJobIdentifier runtimeInformation = new SimpleJobIdentifier("TestJob"); + control.expectAndReturn(jobExecutionRegistry.get(runtimeInformation), null); + control.replay(); + try { + simpleContainer.stop(runtimeInformation); + fail("Expected NoSuchJobExecutionException"); + } catch (NoSuchJobExecutionException e) { + // expected + assertTrue(e.getMessage().indexOf("TestJob")>=0); + } + control.verify(); + } + + public void testStop() throws Exception { + JobExecutionRegistry jobExecutionRegistry = new VolatileJobExecutionRegistry(); + simpleContainer.setJobExecutionRegistry(jobExecutionRegistry); + SimpleJobIdentifier runtimeInformation = new SimpleJobIdentifier("TestJob"); + JobExecutionContext context = jobExecutionRegistry.register(runtimeInformation, new JobInstance(new Long(0))); + + RepeatContextSupport stepContext = new RepeatContextSupport(null); + RepeatContextSupport chunkContext = new RepeatContextSupport(stepContext); + context.registerStepContext(stepContext); + context.registerChunkContext(chunkContext); + simpleContainer.stop(runtimeInformation); + + // It is only unregistered when the start method finishes, and it hasn't + // been called. + assertTrue(jobExecutionRegistry.isRegistered(runtimeInformation)); + + assertTrue(stepContext.isCompleteOnly()); + assertTrue(chunkContext.isCompleteOnly()); + } + + public void testStatisticsWithNoContext() throws Exception { + assertNotNull(simpleContainer.getStatistics()); + } + + public void testStatisticsWithContext() throws Exception { + MockControl control = MockControl.createControl(JobExecutionRegistry.class); + JobExecutionRegistry jobExecutionRegistry = (JobExecutionRegistry) control.getMock(); + simpleContainer.setJobExecutionRegistry(jobExecutionRegistry); + SimpleJobIdentifier runtimeInformation = new SimpleJobIdentifier("TestJob"); + JobExecutionContext jobExecutionContext = new JobExecutionContext(runtimeInformation, new JobInstance(new Long(0))); + jobExecutionContext.registerStepContext(new RepeatContextSupport(null)); + jobExecutionContext.registerChunkContext(new RepeatContextSupport(null)); + control.expectAndReturn(jobExecutionRegistry.findAll(), Collections.singleton(jobExecutionContext)); + control.replay(); + Properties statistics = simpleContainer.getStatistics(); + assertNotNull(statistics); + assertTrue(statistics.containsKey("job1.step1")); + control.verify(); + } +} diff --git a/execution/src/test/java/org/springframework/batch/execution/facade/SimpleJobTests.java b/execution/src/test/java/org/springframework/batch/execution/facade/SimpleJobTests.java new file mode 100644 index 000000000..1a84f4474 --- /dev/null +++ b/execution/src/test/java/org/springframework/batch/execution/facade/SimpleJobTests.java @@ -0,0 +1,208 @@ +/* + * 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.execution.facade; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import junit.framework.TestCase; + +import org.springframework.batch.core.configuration.JobConfiguration; +import org.springframework.batch.core.configuration.StepConfiguration; +import org.springframework.batch.core.domain.BatchStatus; +import org.springframework.batch.core.domain.JobInstance; +import org.springframework.batch.core.executor.StepExecutor; +import org.springframework.batch.core.executor.StepExecutorFactory; +import org.springframework.batch.core.runtime.JobExecutionContext; +import org.springframework.batch.core.runtime.JobIdentifier; +import org.springframework.batch.core.runtime.SimpleJobIdentifier; +import org.springframework.batch.core.tasklet.Tasklet; +import org.springframework.batch.execution.job.DefaultJobExecutor; +import org.springframework.batch.execution.repository.SimpleJobRepository; +import org.springframework.batch.execution.repository.dao.MapJobDao; +import org.springframework.batch.execution.repository.dao.MapStepDao; +import org.springframework.batch.execution.runtime.ScheduledJobIdentifierFactory; +import org.springframework.batch.execution.step.simple.DefaultStepExecutor; +import org.springframework.batch.execution.step.simple.SimpleStepConfiguration; +import org.springframework.batch.execution.tasklet.ItemProviderProcessTasklet; +import org.springframework.batch.item.ItemProcessor; +import org.springframework.batch.item.ItemProvider; +import org.springframework.batch.item.provider.ListItemProvider; +import org.springframework.batch.repeat.RepeatContext; +import org.springframework.batch.repeat.exception.handler.ExceptionHandler; +import org.springframework.batch.repeat.support.RepeatTemplate; +import org.springframework.batch.support.transaction.ResourcelessTransactionManager; +import org.springframework.batch.support.transaction.TransactionAwareProxyFactory; +import org.springframework.transaction.interceptor.TransactionProxyFactoryBean; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.util.StringUtils; + +public class SimpleJobTests extends TestCase { + + private List list = new ArrayList(); + + // private int count; + + private SimpleJobRepository repository = new SimpleJobRepository(new MapJobDao(), new MapStepDao()); + + private List processed = new ArrayList(); + + private ItemProcessor processor = new ItemProcessor() { + public void process(Object data) throws Exception { + processed.add((String) data); + } + }; + + private ItemProvider provider; + + private DefaultJobExecutor jobLifecycle = new DefaultJobExecutor();; + + private DefaultStepExecutor stepLifecycle = new DefaultStepExecutor(); + + protected void setUp() throws Exception { + super.setUp(); + jobLifecycle.setJobRepository(repository); + stepLifecycle.setRepository(repository); + jobLifecycle.setStepExecutorResolver(new StepExecutorFactory() { + public StepExecutor getExecutor(StepConfiguration configuration) { + return stepLifecycle; + } + }); + } + + private Tasklet getTasklet(String arg) { + return getTasklet(new String[] { arg }); + } + + private Tasklet getTasklet(String arg0, String arg1) { + return getTasklet(new String[] { arg0, arg1 }); + } + + private Tasklet getTasklet(String[] args) { + ItemProviderProcessTasklet module = new ItemProviderProcessTasklet(); + List items = TransactionAwareProxyFactory.createTransactionalList(); + items.addAll(Arrays.asList(args)); + provider = new ListItemProvider(items) { + public boolean recover(Object item, Throwable cause) { + list.add(item); + assertTrue(TransactionSynchronizationManager.isActualTransactionActive()); + return true; + } + }; + module.setItemProvider(provider); + module.setItemProcessor(processor); + return module; + } + + public void testSimpleJob() throws Exception { + + JobConfiguration jobConfiguration = new JobConfiguration(); + JobIdentifier runtimeInformation = new ScheduledJobIdentifierFactory() + .getJobIdentifier("real.job"); + + jobConfiguration.addStep(new SimpleStepConfiguration(getTasklet("foo", "bar"))); + jobConfiguration.addStep(new SimpleStepConfiguration(getTasklet("spam"))); + + JobInstance job = repository.findOrCreateJob(jobConfiguration, runtimeInformation); + + assertEquals(job.getName(), "real.job"); + + JobExecutionContext jobExecutionContext = new JobExecutionContext(runtimeInformation, job); + + jobLifecycle.run(jobConfiguration, jobExecutionContext); + assertEquals(BatchStatus.COMPLETED, job.getStatus()); + assertEquals(3, processed.size()); + assertTrue(processed.contains("foo")); + + } + + public void testSimpleJobWithRecovery() throws Exception { + + JobConfiguration jobConfiguration = new JobConfiguration(); + JobIdentifier runtimeInformation = new SimpleJobIdentifier("real.job"); + + RepeatTemplate chunkOperations = new RepeatTemplate(); + // Always handle the exception a check it is the right one... + chunkOperations.setExceptionHandler(new ExceptionHandler() { + public void handleExceptions(RepeatContext context, Collection throwables) { + assertEquals(1, throwables.size()); + assertEquals("Try again Dummy!", ((Throwable) throwables.iterator().next()).getMessage()); + } + }); + stepLifecycle.setChunkOperations(chunkOperations); + + TransactionProxyFactoryBean proxyFactoryBean = new TransactionProxyFactoryBean(); + proxyFactoryBean.setTransactionManager(new ResourcelessTransactionManager()); + proxyFactoryBean.setTarget(stepLifecycle); + proxyFactoryBean.setTransactionAttributes(StringUtils.splitArrayElementsIntoProperties( + new String[] { "processChunk=PROPAGATION_REQUIRED" }, "=")); + proxyFactoryBean.setExposeProxy(true); + proxyFactoryBean.afterPropertiesSet(); + + /* + * Each message fails once and the chunk (size=1) "rolls back"; then it + * is recovered ("skipped") on the second attempt (see retry policy + * definition above)... + */ + final Tasklet module = getTasklet(new String[] { "foo", "bar", "spam" }); + StepConfiguration step = new SimpleStepConfiguration(module); + ((ItemProviderProcessTasklet) module).setItemProcessor(new ItemProcessor() { + public void process(Object data) throws Exception { + throw new RuntimeException("Try again Dummy!"); + } + }); + jobConfiguration.addStep(step); + + JobInstance job = repository.findOrCreateJob(jobConfiguration, runtimeInformation); + JobExecutionContext jobExecutionContext = new JobExecutionContext(runtimeInformation, job); + jobLifecycle.run(jobConfiguration, jobExecutionContext); + + assertEquals(BatchStatus.COMPLETED, job.getStatus()); + assertEquals(0, processed.size()); + // provider should be exhausted + assertEquals(null, provider.next()); + assertEquals(3, list.size()); + } + + public void testExceptionTerminates() throws Exception { + + JobConfiguration jobConfiguration = new JobConfiguration(); + JobIdentifier runtimeInformation = new SimpleJobIdentifier("real.job"); + final Tasklet module = getTasklet(new String[] { "foo", "bar", "spam" }); + StepConfiguration step = new SimpleStepConfiguration(module); + ((ItemProviderProcessTasklet) module).setItemProcessor(new ItemProcessor() { + public void process(Object data) throws Exception { + throw new RuntimeException("Foo"); + } + }); + jobConfiguration.addStep(step); + + JobInstance job = repository.findOrCreateJob(jobConfiguration, runtimeInformation); + JobExecutionContext jobExecutionContext = new JobExecutionContext(runtimeInformation, job); + try { + jobLifecycle.run(jobConfiguration, jobExecutionContext); + fail("Expected RuntimeException"); + } + catch (RuntimeException e) { + assertEquals("Foo", e.getMessage()); + // expected + } + assertEquals(BatchStatus.FAILED, job.getStatus()); + } +} diff --git a/execution/src/test/java/org/springframework/batch/execution/facade/VolatileJobExecutionRegistryTests.java b/execution/src/test/java/org/springframework/batch/execution/facade/VolatileJobExecutionRegistryTests.java new file mode 100644 index 000000000..1ad6e58e9 --- /dev/null +++ b/execution/src/test/java/org/springframework/batch/execution/facade/VolatileJobExecutionRegistryTests.java @@ -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.execution.facade; + +import java.util.Collection; + +import junit.framework.TestCase; + +import org.springframework.batch.core.domain.JobInstance; +import org.springframework.batch.core.runtime.JobExecutionContext; +import org.springframework.batch.core.runtime.SimpleJobIdentifier; + +/** + * @author Dave Syer + * + */ +public class VolatileJobExecutionRegistryTests extends TestCase { + + private SimpleJobIdentifier runtimeInformation = new SimpleJobIdentifier("foo"); + private JobInstance job = new JobInstance(new Long(0)); + + private VolatileJobExecutionRegistry registry = new VolatileJobExecutionRegistry(); + + public void testAddAndRetrieveSingle() throws Exception { + JobExecutionContext context = registry.register(runtimeInformation, job); + assertEquals(context, registry.get(runtimeInformation)); + } + + public void testAddAndFindAll() throws Exception { + JobExecutionContext context = registry.register(runtimeInformation, job); + Collection list = registry.findAll(); + assertEquals(1, list.size()); + assertTrue(list.contains(context)); + } + + public void testAddAndFindAllMultiple() throws Exception { + JobExecutionContext context1 = registry.register(runtimeInformation, job); + JobExecutionContext context2 = registry.register(new SimpleJobIdentifier("spam"), job); + Collection list = registry.findAll(); + assertEquals(2, list.size()); + assertTrue(list.contains(context1)); + assertTrue(list.contains(context2)); + } + + public void testAddAndFindByName() throws Exception { + JobExecutionContext context = registry.register(runtimeInformation, job); + registry.register(new SimpleJobIdentifier("bar"), job); + Collection list = registry.findByName(runtimeInformation.getName()); + assertEquals(1, list.size()); + assertTrue(list.contains(context)); + } + + public void testAddAndUnregister() throws Exception { + registry.register(runtimeInformation, job); + assertTrue(registry.isRegistered(runtimeInformation)); + registry.unregister(runtimeInformation); + assertFalse(registry.isRegistered(runtimeInformation)); + } + + public void testAddAndIsRegistered() throws Exception { + assertFalse(registry.isRegistered(runtimeInformation)); + registry.register(runtimeInformation, job); + assertTrue(registry.isRegistered(runtimeInformation)); + } +} diff --git a/execution/src/test/java/org/springframework/batch/execution/job/DefaultJobExecutorTests.java b/execution/src/test/java/org/springframework/batch/execution/job/DefaultJobExecutorTests.java new file mode 100644 index 000000000..2df9f3d35 --- /dev/null +++ b/execution/src/test/java/org/springframework/batch/execution/job/DefaultJobExecutorTests.java @@ -0,0 +1,247 @@ +/* + * 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.execution.job; + +import java.util.ArrayList; +import java.util.List; + +import junit.framework.TestCase; + +import org.springframework.batch.core.configuration.JobConfiguration; +import org.springframework.batch.core.configuration.StepConfiguration; +import org.springframework.batch.core.domain.BatchStatus; +import org.springframework.batch.core.domain.JobExecution; +import org.springframework.batch.core.domain.JobInstance; +import org.springframework.batch.core.domain.StepInstance; +import org.springframework.batch.core.executor.StepExecutor; +import org.springframework.batch.core.executor.StepExecutorFactory; +import org.springframework.batch.core.executor.StepInterruptedException; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.runtime.JobExecutionContext; +import org.springframework.batch.core.runtime.SimpleJobIdentifier; +import org.springframework.batch.core.runtime.StepExecutionContext; +import org.springframework.batch.execution.repository.SimpleJobRepository; +import org.springframework.batch.execution.repository.dao.JobDao; +import org.springframework.batch.execution.repository.dao.MapJobDao; +import org.springframework.batch.execution.repository.dao.MapStepDao; +import org.springframework.batch.execution.repository.dao.StepDao; +import org.springframework.batch.execution.step.simple.AbstractStepConfiguration; +import org.springframework.batch.execution.step.simple.SimpleStepConfiguration; +import org.springframework.batch.io.exception.BatchCriticalException; +import org.springframework.batch.repeat.ExitStatus; + +/** + * Tests for DefaultJobLifecycle. MapJobDao and MapStepDao are used instead of a + * mock repository to test that status is being stored correctly. + * + * @author Lucas Ward + */ +public class DefaultJobExecutorTests extends TestCase { + + private JobRepository jobRepository; + + private JobDao jobDao; + + private StepDao stepDao; + + private List list = new ArrayList(); + + StepExecutor defaultStepLifecycle = new StepExecutor() { + public ExitStatus process(StepConfiguration configuration, StepExecutionContext stepExecutionContext) + throws StepInterruptedException, BatchCriticalException { + list.add("default"); + return ExitStatus.FINISHED; + } + }; + + StepExecutor configurationStepLifecycle = new StepExecutor() { + public ExitStatus process(StepConfiguration configuration, StepExecutionContext stepExecutionContext) + throws StepInterruptedException, BatchCriticalException { + list.add("special"); + return ExitStatus.FINISHED; + } + }; + + private JobInstance job; + + private JobExecutionContext jobExecutionContext; + + private StepInstance step1; + + private StepInstance step2; + + private StepExecutionContext stepExecutionContext1; + + private StepExecutionContext stepExecutionContext2; + + private AbstractStepConfiguration stepConfiguration1; + + private AbstractStepConfiguration stepConfiguration2; + + private JobConfiguration jobConfiguration; + + private SimpleJobIdentifier jobRuntimeInformation; + + private DefaultJobExecutor jobLifecycle; + + protected void setUp() throws Exception { + super.setUp(); + + MapJobDao.clear(); + MapStepDao.clear(); + jobDao = new MapJobDao(); + stepDao = new MapStepDao(); + jobRepository = new SimpleJobRepository(jobDao, stepDao); + jobLifecycle = new DefaultJobExecutor(); + jobLifecycle.setJobRepository(jobRepository); + + jobLifecycle.setStepExecutorResolver(new StepExecutorFactory() { + public StepExecutor getExecutor(StepConfiguration configuration) { + return defaultStepLifecycle; + } + }); + + stepConfiguration1 = new SimpleStepConfiguration(); + stepConfiguration1.setName("TestStep1"); + stepConfiguration2 = new SimpleStepConfiguration(); + stepConfiguration2.setName("TestStep2"); + List stepConfigurations = new ArrayList(); + stepConfigurations.add(stepConfiguration1); + stepConfigurations.add(stepConfiguration2); + jobConfiguration = new JobConfiguration(); + jobConfiguration.setSteps(stepConfigurations); + + jobRuntimeInformation = new SimpleJobIdentifier("TestJob"); + + job = jobRepository.findOrCreateJob(jobConfiguration, jobRuntimeInformation); + + jobExecutionContext = new JobExecutionContext(jobRuntimeInformation, job); + + List steps = job.getSteps(); + step1 = (StepInstance) steps.get(0); + step2 = (StepInstance) steps.get(1); + stepExecutionContext1 = new StepExecutionContext(jobExecutionContext, step1); + stepExecutionContext2 = new StepExecutionContext(jobExecutionContext, step2); + + } + + protected void tearDown() throws Exception { + super.tearDown(); + } + + public void testRunWithDefaultLifecycle() throws Exception { + + stepConfiguration1.setStartLimit(5); + stepConfiguration2.setStartLimit(5); + jobLifecycle.run(jobConfiguration, jobExecutionContext); + assertEquals(2, list.size()); + checkRepository(BatchStatus.COMPLETED); + } + + public void testExecutionContextIsSet() throws Exception { + + testRunWithDefaultLifecycle(); + assertEquals(job, jobExecutionContext.getJob()); + assertEquals(step1, stepExecutionContext1.getStep()); + assertEquals(step2, stepExecutionContext2.getStep()); + } + + public void testRunWithNonDefaultExecutor() throws Exception { + + jobLifecycle.setStepExecutorResolver(new StepExecutorFactory() { + public StepExecutor getExecutor(StepConfiguration configuration) { + return configuration == stepConfiguration2 ? defaultStepLifecycle : configurationStepLifecycle; + } + }); + stepConfiguration1.setStartLimit(5); + stepConfiguration2.setStartLimit(5); + + jobLifecycle.run(jobConfiguration, jobExecutionContext); + + assertEquals(2, list.size()); + assertEquals("special", list.get(0)); + assertEquals("default", list.get(1)); + checkRepository(BatchStatus.COMPLETED); + } + + public void testInterrupted() throws Exception { + stepConfiguration1.setStartLimit(5); + stepConfiguration2.setStartLimit(5); + final StepInterruptedException exception = new StepInterruptedException("Interrupt!"); + defaultStepLifecycle = new StepExecutor() { + public ExitStatus process(StepConfiguration configuration, StepExecutionContext stepExecutionContext) + throws StepInterruptedException, BatchCriticalException { + throw exception; + } + }; + try { + jobLifecycle.run(jobConfiguration, jobExecutionContext); + } + catch (BatchCriticalException e) { + assertEquals(exception, e.getCause()); + } + assertEquals(0, list.size()); + checkRepository(BatchStatus.STOPPED); + } + + public void testFailed() throws Exception { + stepConfiguration1.setStartLimit(5); + stepConfiguration2.setStartLimit(5); + final RuntimeException exception = new RuntimeException("Foo!"); + defaultStepLifecycle = new StepExecutor() { + public ExitStatus process(StepConfiguration configuration, StepExecutionContext stepExecutionContext) + throws StepInterruptedException, BatchCriticalException { + throw exception; + } + }; + try { + jobLifecycle.run(jobConfiguration, jobExecutionContext); + } + catch (RuntimeException e) { + assertEquals(exception, e); + } + assertEquals(0, list.size()); + checkRepository(BatchStatus.FAILED); + } + + public void testStepShouldNotStart() throws Exception { + // Start policy will return false, keeping the step from being started. + stepConfiguration1.setStartLimit(0); + + try{ + jobLifecycle.run(jobConfiguration, jobExecutionContext); + fail(); + } + catch( Exception ex ){ + //expected + } + } + + /* + * Check JobRepository to ensure status is being saved. + */ + private void checkRepository(BatchStatus status) { + assertEquals(job, jobDao.findJobs(jobRuntimeInformation).get(0)); + // because map dao stores in memory, it can be checked directly + assertEquals(status, job.getStatus()); + JobExecution jobExecution = (JobExecution) jobDao.findJobExecutions(job).get(0); + assertEquals(job.getId(), jobExecution.getJobId()); + assertEquals(status, jobExecution.getStatus()); + int exitCode = status==BatchStatus.STOPPED || status==BatchStatus.FAILED ? -1 : 0; + assertEquals(exitCode, jobExecution.getExitCode()); + } +} diff --git a/execution/src/test/java/org/springframework/batch/execution/repository/MockStepDao.java b/execution/src/test/java/org/springframework/batch/execution/repository/MockStepDao.java new file mode 100644 index 000000000..6976fa408 --- /dev/null +++ b/execution/src/test/java/org/springframework/batch/execution/repository/MockStepDao.java @@ -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.execution.repository; + +import java.util.List; + +import org.springframework.batch.core.domain.JobInstance; +import org.springframework.batch.core.domain.StepExecution; +import org.springframework.batch.core.domain.StepInstance; +import org.springframework.batch.execution.repository.dao.StepDao; +import org.springframework.batch.restart.RestartData; + +public class MockStepDao implements StepDao { + + private List newSteps; + + private int currentNewStep = 0; + + public StepInstance createStep(JobInstance job, String stepName) { + StepInstance newStep = (StepInstance) newSteps.get(currentNewStep); + currentNewStep++; + newStep.setName(stepName); + return newStep; + } + + public StepInstance findStep(JobInstance job, String stepName) { + StepInstance newStep = (StepInstance) newSteps.get(currentNewStep); + currentNewStep++; + newStep.setName(stepName); + return newStep; + } + + public List findSteps(Long jobId) { + return newSteps; + } + + public RestartData getRestartData(Long stepId) { + return null; + } + + public int getStepExecutionCount(Long stepId) { + return 1; + } + + public void save(StepExecution stepExecution) { + } + + public void saveRestartData(Long stepId, RestartData restartData) { + } + + public void update(StepInstance step) { + } + + public void update(StepExecution stepExecution) { + } + + public void setStepsToReturnOnCreate(List steps) { + this.newSteps = steps; + } + + public void resetCurrentNewStep() { + currentNewStep = 0; + } + + public List findStepExecutions(StepInstance step) { + + return null; + } + +} diff --git a/execution/src/test/java/org/springframework/batch/execution/repository/SimpleJobRepositoryTests.java b/execution/src/test/java/org/springframework/batch/execution/repository/SimpleJobRepositoryTests.java new file mode 100644 index 000000000..433026d36 --- /dev/null +++ b/execution/src/test/java/org/springframework/batch/execution/repository/SimpleJobRepositoryTests.java @@ -0,0 +1,316 @@ +/* + * 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.execution.repository; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import junit.framework.TestCase; + +import org.easymock.MockControl; +import org.springframework.batch.core.configuration.JobConfiguration; +import org.springframework.batch.core.configuration.StepConfiguration; +import org.springframework.batch.core.domain.JobExecution; +import org.springframework.batch.core.domain.JobInstance; +import org.springframework.batch.core.domain.StepExecution; +import org.springframework.batch.core.domain.StepInstance; +import org.springframework.batch.core.runtime.SimpleJobIdentifier; +import org.springframework.batch.core.tasklet.Tasklet; +import org.springframework.batch.execution.repository.dao.JobDao; +import org.springframework.batch.execution.repository.dao.StepDao; + +/* + * Test SimpleJobRepository. The majority of test cases are tested using EasyMock, + * however, there were some issues with using it for the stepDao when testing finding + * or creating steps, so an actual mock class had to be written. + * + * @author Lucas Ward + * + */ +public class SimpleJobRepositoryTests extends TestCase { + + SimpleJobRepository jobRepository; + + JobConfiguration jobConfiguration; + + SimpleJobIdentifier jobRuntimeInformation; + + StepConfiguration stepConfiguration1; + + StepConfiguration stepConfiguration2; + + MockControl jobDaoControl = MockControl.createControl(JobDao.class); + + MockControl stepDaoControl = MockControl.createControl(StepDao.class); + + JobDao jobDao; + + StepDao stepDao; + + MockStepDao mockStepDao = new MockStepDao(); + + JobInstance databaseJob; + + StepInstance databaseStep1; + + StepInstance databaseStep2; + + List steps; + + public void setUp() throws Exception { + + jobDao = (JobDao) jobDaoControl.getMock(); + stepDao = (StepDao) stepDaoControl.getMock(); + + jobRepository = new SimpleJobRepository(jobDao, stepDao); + + jobRuntimeInformation = new SimpleJobIdentifier("RepositoryTest"); + + jobConfiguration = new JobConfiguration(); + jobConfiguration.setName("RepositoryTest"); + jobConfiguration.setRestartable(true); + + stepConfiguration1 = new StubStepConfiguration("TestStep1"); + + stepConfiguration2 = new StubStepConfiguration("TestStep2"); + + List stepConfigurations = new ArrayList(); + stepConfigurations.add(stepConfiguration1); + stepConfigurations.add(stepConfiguration2); + + jobConfiguration.setSteps(stepConfigurations); + + databaseJob = new JobInstance(new Long(1)); + + databaseStep1 = new StepInstance(new Long(1)); + databaseStep2 = new StepInstance(new Long(2)); + + steps = new ArrayList(); + steps.add(databaseStep1); + steps.add(databaseStep2); + } + + public void testCreateRestartableJob(){ + + List jobs = new ArrayList(); + + jobDao.findJobs(jobRuntimeInformation); + jobDaoControl.setReturnValue(jobs); + jobDao.createJob(jobRuntimeInformation); + jobDaoControl.setReturnValue(databaseJob); + stepDao.createStep(databaseJob, "TestStep1"); + stepDaoControl.setReturnValue(databaseStep1); + stepDao.createStep(databaseJob, "TestStep2"); + stepDaoControl.setReturnValue(databaseStep2); + stepDaoControl.replay(); + jobDaoControl.replay(); + JobInstance job = jobRepository.findOrCreateJob(jobConfiguration, jobRuntimeInformation); + assertTrue(job.equals(databaseJob)); + List jobSteps = job.getSteps(); + Iterator it = jobSteps.iterator(); + StepInstance step = (StepInstance) it.next(); + assertTrue(step.equals(databaseStep1)); + step = (StepInstance) it.next(); + assertTrue(step.equals(databaseStep2)); + } + + public void testRestartedJob(){ + List jobs = new ArrayList(); + jobDao.findJobs(jobRuntimeInformation); + jobs.add(databaseJob); + jobDaoControl.setReturnValue(jobs); + stepDao.findStep(databaseJob, "TestStep1"); + stepDaoControl.setReturnValue(databaseStep1); + stepDao.getStepExecutionCount(databaseStep1.getId()); + stepDaoControl.setReturnValue(1); + stepDao.findStep(databaseJob, "TestStep2"); + stepDaoControl.setReturnValue(databaseStep2); + stepDao.getStepExecutionCount(databaseStep2.getId()); + stepDaoControl.setReturnValue(1); + stepDaoControl.replay(); + jobDao.getJobExecutionCount(databaseJob.getId()); + jobDaoControl.setReturnValue(1); + jobDaoControl.replay(); + JobInstance job = jobRepository.findOrCreateJob(jobConfiguration, jobRuntimeInformation); + assertTrue(job.equals(databaseJob)); + List jobSteps = job.getSteps(); + Iterator it = jobSteps.iterator(); + StepInstance step = (StepInstance) it.next(); + assertTrue(step.equals(databaseStep1)); + assertTrue(step.getStepExecutionCount() == 1); + step = (StepInstance) it.next(); + assertTrue(step.equals(databaseStep2)); + assertTrue(step.getStepExecutionCount() == 1); + } + + public void testCreateNonRestartableJob(){ + + List jobs = new ArrayList(); + + jobDao.findJobs(jobRuntimeInformation); + jobDaoControl.setReturnValue(jobs); + jobDao.createJob(jobRuntimeInformation); + jobDaoControl.setReturnValue(databaseJob); + stepDao.createStep(databaseJob, "TestStep1"); + stepDaoControl.setReturnValue(databaseStep1); + stepDao.createStep(databaseJob, "TestStep2"); + stepDaoControl.setReturnValue(databaseStep2); + stepDaoControl.replay(); + jobDaoControl.replay(); + JobInstance job = jobRepository.findOrCreateJob(jobConfiguration, jobRuntimeInformation); + assertTrue(job.equals(databaseJob)); + List jobSteps = job.getSteps(); + Iterator it = jobSteps.iterator(); + StepInstance step = (StepInstance) it.next(); + assertTrue(step.equals(databaseStep1)); + step = (StepInstance) it.next(); + assertTrue(step.equals(databaseStep2)); + } + + public void testUpdateJob() { + + // failure scenario - no ID + JobInstance updateJob = new JobInstance(null); + try { + jobRepository.update(updateJob); + fail(); + } + catch (Exception ex) { + // expected + } + + // successful update + updateJob = new JobInstance(new Long(0L)); + jobDao.update(updateJob); + jobDaoControl.replay(); + jobRepository.update(updateJob); + + } + + public void testSaveOrUpdateJobExecution() { + + // failure scenario - must have job ID + JobExecution jobExecution = new JobExecution(null); + try { + jobRepository.saveOrUpdate(jobExecution); + fail(); + } + catch (Exception ex) { + // expected + } + + // new execution - call save on job dao + jobExecution.setJobId(new Long(1)); + jobDao.save(jobExecution); + jobDaoControl.replay(); + jobRepository.saveOrUpdate(jobExecution); + jobDaoControl.reset(); + + // update existing execution + jobExecution.setId(new Long(5)); + jobDao.update(jobExecution); + jobDaoControl.replay(); + jobRepository.saveOrUpdate(jobExecution); + } + + public void testUpdateStep() { + + StepInstance step = new StepInstance(null); + + // failure scenario - id not set + try { + jobRepository.update(step); + fail(); + } + catch (Exception ex) { + // expected + } + + // successful update + step = new StepInstance(new Long(0L)); + stepDao.update(step); + stepDaoControl.replay(); + jobRepository.update(step); + } + + public void testUpdateStepExecution(){ + StepExecution stepExecution = new StepExecution(new Long(10), null); + stepExecution.setId(new Long(11)); + stepDao.update(stepExecution); + stepDaoControl.replay(); + jobRepository.saveOrUpdate(stepExecution); + stepDaoControl.verify(); + } + + public void testSaveStepExecution(){ + StepExecution stepExecution = new StepExecution(new Long(10), null); + //TODO: Not sure why, but calling save on the EasyMock stepDao causes a NullPointerException +// stepDao.save(stepExecution); +// stepDaoControl.replay(); + jobRepository.saveOrUpdate(stepExecution); +// stepDaoControl.verify(); + } + + public void testSaveOrUpdateStepExecutionException() { + + StepExecution stepExecution = new StepExecution(null, null); + + // failure scenario -- no step id set. + try { + jobRepository.saveOrUpdate(stepExecution); + fail(); + } + catch (Exception ex) { + // expected + } + } + + /** + * @author Dave Syer + * + */ + private class StubStepConfiguration implements StepConfiguration { + + private String name; + + /** + * @param name + */ + public StubStepConfiguration(String name) { + this.name = name; + } + + public Tasklet getTasklet() { + return null; + } + + public String getName() { + return name; + } + + public int getStartLimit() { + return 1; + } + + public boolean isAllowStartIfComplete() { + return true; + } + + } + +} diff --git a/execution/src/test/java/org/springframework/batch/execution/repository/dao/HibernateJobDaoTests.java b/execution/src/test/java/org/springframework/batch/execution/repository/dao/HibernateJobDaoTests.java new file mode 100644 index 000000000..a8e29ae63 --- /dev/null +++ b/execution/src/test/java/org/springframework/batch/execution/repository/dao/HibernateJobDaoTests.java @@ -0,0 +1,52 @@ +/* + * 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.execution.repository.dao; + +import java.sql.Timestamp; +import java.util.List; +import java.util.Map; + +import org.hibernate.SessionFactory; +import org.springframework.batch.core.domain.BatchStatus; +import org.springframework.util.ClassUtils; + +public class HibernateJobDaoTests extends SqlJobDaoTests { + + private SessionFactory sessionFactory; + + protected String[] getConfigLocations(){ + return new String[] { ClassUtils.addResourcePathToPackagePath(getClass(), "hibernate-dao-test.xml") }; + } + + public void setSessionFactory(SessionFactory sessionFactory){ + this.sessionFactory = sessionFactory; + } + + public void testUpdateJobExecution() { + + jobExecution.setStatus(BatchStatus.COMPLETED); + jobExecution.setEndTime(new Timestamp(System.currentTimeMillis())); + jobDao.update(jobExecution); + + sessionFactory.getCurrentSession().flush(); + + List executions = jdbcTemplate.queryForList("SELECT * FROM BATCH_JOB_EXECUTION where JOB_ID=?", new Object[] {job.getId()}); + assertEquals(1, executions.size()); + assertEquals(jobExecution.getEndTime(), ((Map)executions.get(0)).get("END_TIME")); + } + +} diff --git a/execution/src/test/java/org/springframework/batch/execution/repository/dao/HibernateStepDaoTests.java b/execution/src/test/java/org/springframework/batch/execution/repository/dao/HibernateStepDaoTests.java new file mode 100644 index 000000000..264c94723 --- /dev/null +++ b/execution/src/test/java/org/springframework/batch/execution/repository/dao/HibernateStepDaoTests.java @@ -0,0 +1,56 @@ +/* + * 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.execution.repository.dao; + +import java.util.Properties; + +import org.hibernate.SessionFactory; +import org.springframework.batch.core.domain.StepExecution; +import org.springframework.batch.core.domain.StepInstance; +import org.springframework.batch.support.PropertiesConverter; +import org.springframework.util.ClassUtils; + +public class HibernateStepDaoTests extends SqlStepDaoTests { + + private SessionFactory sessionFactory; + + public void setSessionFactory(SessionFactory sessionFactory) { + this.sessionFactory = sessionFactory; + } + + protected String[] getConfigLocations() { + return new String[] { ClassUtils.addResourcePathToPackagePath(getClass(), "hibernate-dao-test.xml") }; + } + + public void testSaveStatistics() throws Exception { + StepInstance step = stepDao.createStep(job, "foo"); + StepExecution stepExecution = new StepExecution(step.getId(), new Long(10)); + Properties statistics = new Properties(); + statistics.setProperty("x", "y"); + statistics.setProperty("a", "b"); + stepExecution.setStatistics(statistics); + stepDao.save(stepExecution); + sessionFactory.getCurrentSession().flush(); + String returnedStatistics = (String) jdbcTemplate.queryForObject( + "SELECT TASK_STATISTICS from BATCH_STEP_EXECUTION where ID=?", new Object[] { stepExecution.getId() }, + String.class); + + Properties fromDb = PropertiesConverter.stringToProperties(returnedStatistics); + //assertEquals("x=y, a=b", returnedStatistics); + assertEquals(fromDb, statistics); + } +} diff --git a/execution/src/test/java/org/springframework/batch/execution/repository/dao/MapJobDaoTests.java b/execution/src/test/java/org/springframework/batch/execution/repository/dao/MapJobDaoTests.java new file mode 100644 index 000000000..4b63d0056 --- /dev/null +++ b/execution/src/test/java/org/springframework/batch/execution/repository/dao/MapJobDaoTests.java @@ -0,0 +1,74 @@ +/* + * 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.execution.repository.dao; + +import java.util.List; + +import junit.framework.TestCase; + +import org.springframework.batch.core.domain.JobExecution; +import org.springframework.batch.core.domain.JobInstance; +import org.springframework.batch.core.runtime.SimpleJobIdentifier; + +public class MapJobDaoTests extends TestCase { + + MapJobDao dao = new MapJobDao(); + + protected void setUp() throws Exception { + MapJobDao.clear(); + } + + public void testCreateAndRetrieveSingle() throws Exception { + JobInstance job = dao.createJob(new SimpleJobIdentifier("foo")); + List result = dao.findJobs(new SimpleJobIdentifier("foo")); + assertTrue(result.contains(job)); + } + + public void testCreateAndRetrieveMultiple() throws Exception { + JobInstance job = dao.createJob(new SimpleJobIdentifier("foo")); + job = dao.createJob(new SimpleJobIdentifier("bar")); + List result = dao.findJobs(new SimpleJobIdentifier("bar")); + assertEquals(1, result.size()); + assertTrue(result.contains(job)); + } + + public void testNoExecutionsForNewJob() throws Exception { + JobInstance job = dao.createJob(new SimpleJobIdentifier("foo")); + assertEquals(0, dao.getJobExecutionCount(job.getId())); + } + + public void testSaveExecutionUpdatesId() throws Exception { + JobInstance job = dao.createJob(new SimpleJobIdentifier("foo")); + JobExecution execution = new JobExecution(job.getId()); + assertNull(execution.getId()); + dao.save(execution); + assertNotNull(execution.getId()); + } + public void testCorrectExecutionCountForExistingJob() throws Exception { + JobInstance job = dao.createJob(new SimpleJobIdentifier("foo")); + dao.save(new JobExecution(job.getId())); + assertEquals(1, dao.getJobExecutionCount(job.getId())); + } + + public void testMultipleExecutionsPerExisting() throws Exception { + JobInstance job = dao.createJob(new SimpleJobIdentifier("foo")); + dao.save(new JobExecution(job.getId())); + Thread.sleep(50L); // Hack, hack, hackety, hack - job executions are not unique if created too close together! + dao.save(new JobExecution(job.getId())); + assertEquals(2, dao.getJobExecutionCount(job.getId())); + } +} diff --git a/execution/src/test/java/org/springframework/batch/execution/repository/dao/MapStepDaoTests.java b/execution/src/test/java/org/springframework/batch/execution/repository/dao/MapStepDaoTests.java new file mode 100644 index 000000000..b597075db --- /dev/null +++ b/execution/src/test/java/org/springframework/batch/execution/repository/dao/MapStepDaoTests.java @@ -0,0 +1,122 @@ +/* + * 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.execution.repository.dao; + +import java.util.List; +import java.util.Properties; + +import junit.framework.TestCase; + +import org.springframework.batch.core.domain.BatchStatus; +import org.springframework.batch.core.domain.JobInstance; +import org.springframework.batch.core.domain.StepExecution; +import org.springframework.batch.core.domain.StepInstance; +import org.springframework.batch.execution.repository.dao.MapStepDao; +import org.springframework.batch.restart.GenericRestartData; +import org.springframework.batch.restart.RestartData; + +public class MapStepDaoTests extends TestCase { + + MapStepDao dao = new MapStepDao(); + private JobInstance job; + private StepInstance step; + + // Make sure we get a new job for each test... + static long jobId=100; + + protected void setUp() throws Exception { + MapStepDao.clear(); + job = new JobInstance(new Long(jobId++)); + step = dao.createStep(job, "foo"); + } + + public void testCreateUnequal() throws Exception { + StepInstance step2 = dao.createStep(job, "foo");; + assertFalse(step.equals(step2)); + assertFalse(step.hashCode()==step2.hashCode()); + } + + public void testCreateAndRetrieveSingle() throws Exception { + StepInstance result = dao.findStep(job, "foo"); + assertEquals(step, result); + } + + public void testCreateAndRetrieveSingleWhenMultipleStored() throws Exception { + dao.createStep(job, "bar");; + StepInstance result = dao.findStep(job, "foo"); + assertEquals(step, result); + } + + public void testCreateAndRetrieveSingleFromList() throws Exception { + List result = dao.findSteps(job.getId()); + assertTrue(result.contains(step)); + } + + public void testCreateAndRetrieveMultiple() throws Exception { + step = dao.createStep(job, "bar"); + List result = dao.findSteps(job.getId()); + assertEquals(2, result.size()); + assertTrue(result.contains(step)); + } + + public void testFindWithEmptyResults() throws Exception { + List result = dao.findSteps(new Long(22)); + assertEquals(0, result.size()); + } + + public void testFindSingleWithEmptyResults() throws Exception { + StepInstance result = dao.findStep(new JobInstance(new Long(22)), "bar"); + assertEquals(null, result); + } + + public void testNoExecutionsForNew() throws Exception { + assertEquals(0, dao.getStepExecutionCount(step.getId())); + } + + public void testSaveExecutionUpdatesId() throws Exception { + StepExecution execution = new StepExecution(step.getId(), null); + assertNull(execution.getId()); + dao.save(execution); + assertNotNull(execution.getId()); + } + + public void testCorrectExecutionCountForExisting() throws Exception { + dao.save(new StepExecution(step.getId(), null)); + assertEquals(1, dao.getStepExecutionCount(step.getId())); + } + + public void testOnlyOneExecutionPerStep() throws Exception { + dao.save(new StepExecution(step.getId(), null)); + dao.save(new StepExecution(step.getId(), null)); + assertEquals(2, dao.getStepExecutionCount(step.getId())); + } + + public void testSaveRestartData() throws Exception { + assertEquals(null, dao.getRestartData(step.getId())); + step.setStatus(BatchStatus.COMPLETED); + Properties data = new Properties(); + data.setProperty("restart.key1", "restartData"); + RestartData restartData = new GenericRestartData(data); + step.setRestartData(restartData); + dao.update(step); + StepInstance tempStep = dao.findStep(job, step.getName()); + assertEquals(tempStep, step); + assertEquals(tempStep.getRestartData().getProperties().toString(), + restartData.getProperties().toString()); + } + +} diff --git a/execution/src/test/java/org/springframework/batch/execution/repository/dao/SqlJobDaoTests.java b/execution/src/test/java/org/springframework/batch/execution/repository/dao/SqlJobDaoTests.java new file mode 100644 index 000000000..07d7e2daa --- /dev/null +++ b/execution/src/test/java/org/springframework/batch/execution/repository/dao/SqlJobDaoTests.java @@ -0,0 +1,221 @@ +/* + * 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.execution.repository.dao; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.text.SimpleDateFormat; +import java.util.List; + +import org.springframework.batch.core.domain.BatchStatus; +import org.springframework.batch.core.domain.JobExecution; +import org.springframework.batch.core.domain.JobInstance; +import org.springframework.batch.core.repository.NoSuchBatchDomainObjectException; +import org.springframework.batch.execution.runtime.ScheduledJobIdentifier; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.test.AbstractTransactionalDataSourceSpringContextTests; +import org.springframework.util.ClassUtils; + +public class SqlJobDaoTests extends AbstractTransactionalDataSourceSpringContextTests { + + private static final String GET_JOB_EXECUTION = "SELECT JOB_ID, START_TIME, END_TIME, STATUS from " + + "BATCH_JOB_EXECUTION where ID = ?"; + + protected JobDao jobDao; + + protected ScheduledJobIdentifier jobRuntimeInformation; + + protected JobInstance job; + + protected JobExecution jobExecution; + + protected Timestamp jobExecutionStartTime = new Timestamp(System.currentTimeMillis()); + + protected String[] getConfigLocations() { + return new String[] { ClassUtils.addResourcePathToPackagePath(getClass(), "sql-dao-test.xml") }; + } + + /* + * Because AbstractTransactionalSpringContextTests is used, this method will + * be called by Spring to set the JobRepository. + */ + public void setJobRepositoryDao(JobDao jobRepositoryDao) { + this.jobDao = jobRepositoryDao; + } + + protected void onSetUpInTransaction() throws Exception { + jobRuntimeInformation = new ScheduledJobIdentifier("Job1"); + jobRuntimeInformation.setName("Job1"); + jobRuntimeInformation.setJobStream("TestStream"); + jobRuntimeInformation.setJobRun(1); + jobRuntimeInformation.setScheduleDate(new SimpleDateFormat("yyyyMMdd").parse("20070505")); + + // Create job. + job = jobDao.createJob(jobRuntimeInformation); + + // Create an execution + jobExecutionStartTime = new Timestamp(System.currentTimeMillis()); + jobExecution = new JobExecution(job.getId()); + jobExecution.setStartTime(jobExecutionStartTime); + jobExecution.setStatus(BatchStatus.STARTED); + jobDao.save(jobExecution); + } + + public void testVersionIsNotNullForJob() throws Exception { + int version = jdbcTemplate.queryForInt("select version from BATCH_JOB where ID="+job.getId()); + assertEquals(0, version); + } + + public void testVersionIsNotNullForJobExecution() throws Exception { + int version = jdbcTemplate.queryForInt("select version from BATCH_JOB_EXECUTION where ID="+jobExecution.getId()); + assertEquals(0, version); + } + + public void testFindNonExistentJob(){ + // No job should be found since it hasn't been created. + List jobs = jobDao.findJobs(new ScheduledJobIdentifier("Job2")); + assertTrue(jobs.size() == 0); + } + + public void testFindJob(){ + + List jobs = jobDao.findJobs(jobRuntimeInformation); + assertTrue(jobs.size() == 1); + JobInstance tempJob = (JobInstance) jobs.get(0); + assertTrue(job.equals(tempJob)); + } + + public void testFindJobWithNullRuntime(){ + + ScheduledJobIdentifier runtimeInformation = null; + + try{ + jobDao.findJobs(runtimeInformation); + fail(); + }catch(IllegalArgumentException ex){ + //expected + } + } + + public void testUpdateJob(){ + // Update the returned job with a new status + job.setStatus(BatchStatus.COMPLETED); + jobDao.update(job); + + // The job just updated should be found, with the saved status. + List jobs = jobDao.findJobs(jobRuntimeInformation); + assertTrue(jobs.size() == 1); + JobInstance tempJob = (JobInstance) jobs.get(0); + assertTrue(job.equals(tempJob)); + assertEquals(tempJob.getStatus(), BatchStatus.COMPLETED); + } + + public void testUpdateJobWithNullId(){ + + JobInstance testJob = new JobInstance(null); + try{ + jobDao.update(testJob); + fail(); + }catch(IllegalArgumentException ex){ + //expected + } + } + + public void testUpdateNullJob(){ + + JobInstance testJob = null; + try{ + jobDao.update(testJob); + }catch(IllegalArgumentException ex){ + //expected + } + } + + public void testUpdateJobExecution() { + + jobExecution.setStatus(BatchStatus.COMPLETED); + jobExecution.setEndTime(new Timestamp(System.currentTimeMillis())); + jobDao.update(jobExecution); + + List executions = retrieveJobExecution(jobExecution.getId()); + assertEquals(executions.size(), 1); + assertEquals(jobExecution, ((JobExecution)executions.get(0))); + } + + public void testUpdateInvalidJobExecution(){ + + JobExecution execution = new JobExecution(job.getId()); + //id is invalid + execution.setId(new Long(29432)); + try{ + jobDao.update(execution); + fail(); + }catch(NoSuchBatchDomainObjectException ex){ + //expected + } + } + + public void testUpdateNullIdJobExection(){ + + JobExecution execution = new JobExecution(job.getId()); + try{ + jobDao.update(execution); + fail(); + }catch(IllegalArgumentException ex){ + //expected + } + } + + public void testIncrementExecutionCount(){ + + // 1 JobExection already added in setup + assertEquals(jobDao.getJobExecutionCount(job.getId()), 1); + + // Save new JobExecution for same job + JobExecution testJobExecution = new JobExecution(job.getId()); + jobDao.save(testJobExecution); + //JobExecutionCount should be incremented by 1 + assertEquals(jobDao.getJobExecutionCount(job.getId()), 2); + } + + public void testZeroExecutionCount(){ + + JobInstance testJob = jobDao.createJob(new ScheduledJobIdentifier("TestJob")); + //no jobExecutions saved for new job, count should be 0 + assertEquals(jobDao.getJobExecutionCount(testJob.getId()), 0); + } + + private List retrieveJobExecution(final Long id){ + + RowMapper rowMapper = new RowMapper(){ + public Object mapRow(ResultSet rs, int rowNum) throws SQLException { + + JobExecution execution = new JobExecution(new Long(rs.getLong(1))); + execution.setStartTime(rs.getTimestamp(2)); + execution.setEndTime(rs.getTimestamp(3)); + execution.setStatus(BatchStatus.getStatus(rs.getString(4))); + execution.setId(id); + + return execution; + } + }; + + return jdbcTemplate.query(GET_JOB_EXECUTION, new Object[]{id}, rowMapper); + } + +} diff --git a/execution/src/test/java/org/springframework/batch/execution/repository/dao/SqlStepDaoTests.java b/execution/src/test/java/org/springframework/batch/execution/repository/dao/SqlStepDaoTests.java new file mode 100644 index 000000000..ad67ee74e --- /dev/null +++ b/execution/src/test/java/org/springframework/batch/execution/repository/dao/SqlStepDaoTests.java @@ -0,0 +1,206 @@ +/* + * 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.execution.repository.dao; + +import java.sql.Timestamp; +import java.util.List; +import java.util.Properties; + +import org.springframework.batch.core.domain.BatchStatus; +import org.springframework.batch.core.domain.JobInstance; +import org.springframework.batch.core.domain.StepExecution; +import org.springframework.batch.core.domain.StepInstance; +import org.springframework.batch.core.runtime.JobIdentifier; +import org.springframework.batch.execution.runtime.ScheduledJobIdentifier; +import org.springframework.batch.restart.GenericRestartData; +import org.springframework.batch.restart.RestartData; +import org.springframework.test.AbstractTransactionalDataSourceSpringContextTests; +import org.springframework.util.ClassUtils; + +/** + * Test for StepDao. Because it is very reasonable to assume that there is a + * foreign key constraint on the JobId of a step, the JobDao is used to create + * jobs, to have an id for creating steps. + * + * @author Lucas Ward + */ +public class SqlStepDaoTests extends AbstractTransactionalDataSourceSpringContextTests { + + protected JobDao jobDao; + + protected StepDao stepDao; + + protected JobInstance job; + + protected StepInstance step1; + + protected StepInstance step2; + + protected StepExecution stepExecution; + + public void setJobDao(JobDao jobDao) { + this.jobDao = jobDao; + } + + public void setStepDao(StepDao stepDao) { + this.stepDao = stepDao; + } + + /* + * (non-Javadoc) + * @see org.springframework.test.AbstractSingleSpringContextTests#getConfigLocations() + */ + protected String[] getConfigLocations() { + return new String[] { ClassUtils.addResourcePathToPackagePath(getClass(), "sql-dao-test.xml") }; + } + + /* + * (non-Javadoc) + * @see org.springframework.test.AbstractTransactionalSpringContextTests#onSetUpInTransaction() + */ + protected void onSetUpInTransaction() throws Exception { + JobIdentifier jobIdentifier = new ScheduledJobIdentifier("TestJob"); + job = jobDao.createJob(jobIdentifier); + step1 = stepDao.createStep(job, "TestStep1"); + step2 = stepDao.createStep(job, "TestStep2"); + + stepExecution = new StepExecution(step1.getId(), new Long(23)); + stepExecution.setStatus(BatchStatus.STARTED); + stepExecution.setStartTime(new Timestamp(System.currentTimeMillis())); + stepDao.save(stepExecution); + } + + public void testVersionIsNotNullForStep() throws Exception { + int version = jdbcTemplate.queryForInt("select version from BATCH_STEP where ID="+step1.getId()); + assertEquals(0, version); + } + + public void testVersionIsNotNullForStepExecution() throws Exception { + int version = jdbcTemplate.queryForInt("select version from BATCH_STEP_EXECUTION where ID="+stepExecution.getId()); + assertEquals(0, version); + } + + public void testFindStepNull(){ + + StepInstance step = stepDao.findStep(job, "UnSavedStep"); + assertNull(step); + } + + public void testFindStep(){ + + StepInstance tempStep = stepDao.findStep(job, "TestStep1"); + assertEquals(tempStep, step1); + } + + public void testFindSteps(){ + + List steps = stepDao.findSteps(job.getId()); + assertEquals(steps.size(), 2); + assertTrue(steps.contains(step1)); + assertTrue(steps.contains(step2)); + } + + public void testFindStepsNotSaved(){ + + //no steps are saved for given id, empty list should be returned + List steps = stepDao.findSteps(new Long(38922)); + assertEquals(steps.size(), 0); + } + + public void testCreateStep(){ + + StepInstance step3 = stepDao.createStep(job, "TestStep3"); + StepInstance tempStep = stepDao.findStep(job, "TestStep3"); + assertEquals(step3, tempStep); + } + + public void testUpdateStepWithoutRestartData(){ + + step1.setStatus(BatchStatus.COMPLETED); + stepDao.update(step1); + StepInstance tempStep = stepDao.findStep(job, step1.getName()); + assertEquals(tempStep, step1); + } + + public void testUpdateStepWithRestartData(){ + + step1.setStatus(BatchStatus.COMPLETED); + Properties data = new Properties(); + data.setProperty("restart.key1", "restartData"); + RestartData restartData = new GenericRestartData(data); + step1.setRestartData(restartData); + stepDao.update(step1); + StepInstance tempStep = stepDao.findStep(job, step1.getName()); + assertEquals(tempStep, step1); + assertEquals(tempStep.getRestartData().getProperties().toString(), + restartData.getProperties().toString()); + } + + public void testSaveStepExecution(){ + + StepExecution execution = new StepExecution(step2.getId(), new Long(10)); + execution.setStatus(BatchStatus.STARTED); + execution.setStartTime(new Timestamp(System.currentTimeMillis())); + Properties statistics = new Properties(); + statistics.setProperty("statistic.key1", "0"); + statistics.setProperty("statistic.key2", "5"); + execution.setStatistics(statistics); + stepDao.save(execution); + List executions = stepDao.findStepExecutions(step2); + assertEquals(1, executions.size()); + StepExecution tempExecution = (StepExecution)executions.get(0); + assertEquals(execution, tempExecution); + assertEquals(execution.getStatistics(), tempExecution.getStatistics()); + } + + public void testUpdateStepExecution(){ + + stepExecution.setStatus(BatchStatus.COMPLETED); + stepExecution.setEndTime(new Timestamp(System.currentTimeMillis())); + stepExecution.setCommitCount(5); + stepExecution.setTaskCount(5); + stepExecution.setStatistics(new Properties()); + stepDao.update(stepExecution); + List executions = stepDao.findStepExecutions(step1); + assertEquals(1, executions.size()); + assertEquals(stepExecution, (StepExecution)executions.get(0)); + } + + public void testUpdateStepExecutionWithNullId(){ + StepExecution stepExecution = new StepExecution(null, null); + try{ + stepDao.update(stepExecution); + fail(); + }catch(IllegalArgumentException ex){ + //expected + } + } + + public void testGetStepExecutionCountForNoExecutions(){ + + int executionCount = stepDao.getStepExecutionCount(step2.getId()); + assertEquals(executionCount, 0); + } + + public void testIncrementStepExecutionCount(){ + + assertEquals(1, stepDao.getStepExecutionCount(step1.getId())); + StepExecution execution = new StepExecution(step1.getId(), new Long(9)); + stepDao.save(execution); + assertEquals(2, stepDao.getStepExecutionCount(step1.getId())); + } +} diff --git a/execution/src/test/java/org/springframework/batch/execution/runtime/ScheduledJobIdentifierTests.java b/execution/src/test/java/org/springframework/batch/execution/runtime/ScheduledJobIdentifierTests.java new file mode 100644 index 000000000..73e3b194f --- /dev/null +++ b/execution/src/test/java/org/springframework/batch/execution/runtime/ScheduledJobIdentifierTests.java @@ -0,0 +1,66 @@ +/* + * 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.execution.runtime; + +import java.sql.Date; + +import junit.framework.TestCase; + +/** + * @author Dave Syer + * + */ +public class ScheduledJobIdentifierTests extends TestCase { + + private ScheduledJobIdentifier instance = new ScheduledJobIdentifier(null); + + /** + * Test method for {@link org.springframework.batch.core.domain.JobInstance#getName()}. + */ + public void testGetName() { + assertEquals(null, instance.getName()); + instance.setName("foo"); + assertEquals("foo", instance.getName()); + } + + /** + * Test method for {@link org.springframework.batch.core.domain.JobInstance#getJobStream()}. + */ + public void testGetJobStream() { + assertEquals("", instance.getJobStream()); + instance.setJobStream("foo"); + assertEquals("foo", instance.getJobStream()); + } + + /** + * Test method for {@link org.springframework.batch.core.domain.JobInstance#getScheduleDate()}. + */ + public void testGetScheduleDate() { + assertNotNull(instance.getScheduleDate()); + instance.setScheduleDate(new Date(100L)); + assertEquals(100L, instance.getScheduleDate().getTime()); + } + + /** + * Test method for {@link org.springframework.batch.core.domain.JobInstance#getJobRun()}. + */ + public void testGetJobRun() { + assertEquals(0, instance.getJobRun()); + instance.setJobRun(1); + assertEquals(1, instance.getJobRun()); + } + +} diff --git a/execution/src/test/java/org/springframework/batch/execution/scope/StepContextAwareStepScopeTests.java b/execution/src/test/java/org/springframework/batch/execution/scope/StepContextAwareStepScopeTests.java new file mode 100644 index 000000000..a18e25f4f --- /dev/null +++ b/execution/src/test/java/org/springframework/batch/execution/scope/StepContextAwareStepScopeTests.java @@ -0,0 +1,89 @@ +/* + * 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.execution.scope; + +import java.util.ArrayList; +import java.util.List; + +import junit.framework.TestCase; + +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.core.AttributeAccessor; + +/** + * @author Dave Syer + * + */ +public class StepContextAwareStepScopeTests extends TestCase { + + private static List list = new ArrayList(); + + /* (non-Javadoc) + * @see junit.framework.TestCase#tearDown() + */ + protected void tearDown() throws Exception { + StepSynchronizationManager.clear(); + list.clear(); + } + + public void testScopedBean() throws Exception { + StepSynchronizationManager.open(); + ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("scope-tests.xml", getClass()); + TestBean bean = (TestBean) applicationContext.getBean("bean"); + assertNotNull(bean); + assertEquals("foo", bean.name); + } + + public void testScopedBeanWithDestroyCallback() throws Exception { + assertEquals(0, list.size()); + StepSynchronizationManager.open(); + ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("scope-tests.xml", getClass()); + TestBean bean = (TestBean) applicationContext.getBean("bean"); + assertNotNull(bean); + StepSynchronizationManager.close(); + assertEquals(1, list.size()); + } + + public void testScopedBeanWithAware() throws Exception { + StepContext context = StepSynchronizationManager.open(); + ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("scope-tests.xml", getClass()); + TestBeanAware bean = (TestBeanAware) applicationContext.getBean("aware"); + assertNotNull(bean); + assertEquals("bar", bean.name); + assertEquals(context, bean.context); + } + + public static class TestBean { + String name; + TestBean child; + public void setName(String name) { + this.name = name; + } + public void setChild(TestBean child) { + this.child = child; + } + public void close() { + list.add("close"); + } + } + + public static class TestBeanAware extends TestBean implements StepContextAware { + AttributeAccessor context; + public void setStepScopeContext(AttributeAccessor context) { + this.context = context; + } + } +} diff --git a/execution/src/test/java/org/springframework/batch/execution/scope/StepScopeContextTests.java b/execution/src/test/java/org/springframework/batch/execution/scope/StepScopeContextTests.java new file mode 100644 index 000000000..97711cd81 --- /dev/null +++ b/execution/src/test/java/org/springframework/batch/execution/scope/StepScopeContextTests.java @@ -0,0 +1,120 @@ +/* + * 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.execution.scope; + +import java.util.ArrayList; +import java.util.List; + +import junit.framework.TestCase; + +import org.springframework.batch.core.runtime.SimpleJobIdentifier; + +/** + * @author Dave Syer + * + */ +public class StepScopeContextTests extends TestCase { + + private SimpleStepContext context = new SimpleStepContext(new SimpleStepContext()); + + /** + * Test method for {@link org.springframework.batch.execution.scope.SimpleStepContext#StepScopeContext()}. + */ + public void testStepScopeContext() { + assertNull(new SimpleStepContext().getParent()); + } + + /** + * Test method for {@link org.springframework.batch.execution.scope.SimpleStepContext#getParent()}. + */ + public void testGetParent() { + assertNotNull(context.getParent()); + } + + /** + * Test method for {@link org.springframework.batch.execution.scope.SimpleStepContext#getJobIdentifier()}. + */ + public void testGetJobIdentifier() { + assertNull(context.getJobIdentifier()); + context.setJobIdentifier(new SimpleJobIdentifier("bar")); + assertEquals("bar", context.getJobIdentifier().getName()); + } + + private List list = new ArrayList(); + + /** + * Test method for {@link org.springframework.batch.repeat.context.SimpleStepContext#registerDestructionCallback(java.lang.String, java.lang.Runnable)}. + */ + public void testDestructionCallbackSunnyDay() throws Exception { + SimpleStepContext context = new SimpleStepContext(null); + context.setAttribute("foo", "FOO"); + context.registerDestructionCallback("foo", new Runnable() { + public void run() { + list.add("bar"); + } + }); + context.close(); + assertEquals(1, list.size()); + assertEquals("bar", list.get(0)); + } + + /** + * Test method for {@link org.springframework.batch.repeat.context.SimpleStepContext#registerDestructionCallback(java.lang.String, java.lang.Runnable)}. + */ + public void testDestructionCallbackMissingAttribute() throws Exception { + SimpleStepContext context = new SimpleStepContext(null); + context.registerDestructionCallback("foo", new Runnable() { + public void run() { + list.add("bar"); + } + }); + context.close(); + assertEquals(0, list.size()); + } + + /** + * Test method for {@link org.springframework.batch.repeat.context.SimpleStepContext#registerDestructionCallback(java.lang.String, java.lang.Runnable)}. + */ + public void testDestructionCallbackWithException() throws Exception { + SimpleStepContext context = new SimpleStepContext(null); + context.setAttribute("foo", "FOO"); + context.setAttribute("bar", "BAR"); + context.registerDestructionCallback("bar", new Runnable() { + public void run() { + list.add("spam"); + throw new RuntimeException("fail!"); + } + }); + context.registerDestructionCallback("foo", new Runnable() { + public void run() { + list.add("bar"); + throw new RuntimeException("fail!"); + } + }); + try { + context.close(); + fail("Expected RuntimeException"); + } catch (RuntimeException e) { + // We don't care which one was thrown... + assertEquals("fail!", e.getMessage()); + } + // ...but we do care that both were executed: + assertEquals(2, list.size()); + assertTrue(list.contains("bar")); + assertTrue(list.contains("spam")); + } + +} diff --git a/execution/src/test/java/org/springframework/batch/execution/scope/StepScopeTests.java b/execution/src/test/java/org/springframework/batch/execution/scope/StepScopeTests.java new file mode 100644 index 000000000..405e032b2 --- /dev/null +++ b/execution/src/test/java/org/springframework/batch/execution/scope/StepScopeTests.java @@ -0,0 +1,192 @@ +/* + * 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.execution.scope; + +import java.util.ArrayList; +import java.util.List; + +import junit.framework.TestCase; + +import org.springframework.batch.execution.scope.StepScope; +import org.springframework.batch.execution.scope.SimpleStepContext; +import org.springframework.batch.execution.scope.StepSynchronizationManager; +import org.springframework.batch.repeat.synch.RepeatSynchronizationManager; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.ObjectFactory; + +/** + * @author Dave Syer + * + */ +public class StepScopeTests extends TestCase { + + private StepScope scope = new StepScope(); + + private SimpleStepContext context; + + /* + * (non-Javadoc) + * @see junit.framework.TestCase#setUp() + */ + protected void setUp() throws Exception { + super.setUp(); + context = StepSynchronizationManager.open(); + } + + /* (non-Javadoc) + * @see junit.framework.TestCase#tearDown() + */ + protected void tearDown() throws Exception { + RepeatSynchronizationManager.clear(); + super.tearDown(); + } + + public void testGetWithNoContext() throws Exception { + final String foo = "bar"; + StepSynchronizationManager.clear(); + try { + scope.get("foo", new ObjectFactory() { + public Object getObject() throws BeansException { + return foo; + } + }); + fail("Expected IllegalStateException"); + } + catch (IllegalStateException e) { + // expected + } + + } + + /** + * Test method for + * {@link org.springframework.batch.execution.scope.StepScope#get(java.lang.String, org.springframework.beans.factory.ObjectFactory)}. + */ + public void testGetWithNothingAlreadyThere() { + final String foo = "bar"; + Object value = scope.get("foo", new ObjectFactory() { + public Object getObject() throws BeansException { + return foo; + } + }); + assertEquals(foo, value); + assertTrue(context.hasAttribute("foo")); + } + + /** + * Test method for + * {@link org.springframework.batch.execution.scope.StepScope#get(java.lang.String, org.springframework.beans.factory.ObjectFactory)}. + */ + public void testGetWithSomethingAlreadyThere() { + context.setAttribute("foo", "bar"); + Object value = scope.get("foo", new ObjectFactory() { + public Object getObject() throws BeansException { + return null; + } + }); + assertEquals("bar", value); + assertTrue(context.hasAttribute("foo")); + } + + /** + * Test method for + * {@link org.springframework.batch.execution.scope.StepScope#get(java.lang.String, org.springframework.beans.factory.ObjectFactory)}. + */ + public void testGetWithSomethingAlreadyInParentContext() { + SimpleStepContext context = StepSynchronizationManager.open(); + context.setAttribute("foo", "bar"); + Object value = scope.get("foo", new ObjectFactory() { + public Object getObject() throws BeansException { + return null; + } + }); + assertEquals("bar", value); + assertTrue(context.hasAttribute("foo")); + } + + /** + * Test method for + * {@link org.springframework.batch.execution.scope.StepScope#getConversationId()}. + */ + public void testGetConversationId() { + String id = scope.getConversationId(); + assertNotNull(id); + } + + /** + * Test method for + * {@link org.springframework.batch.execution.scope.StepScope#getConversationId()}. + */ + public void testGetConversationIdFromAttribute() { + context.setAttribute(StepScope.ID_KEY, "foo"); + String id = scope.getConversationId(); + assertEquals("foo", id); + } + + /** + * Test method for + * {@link org.springframework.batch.execution.scope.StepScope#registerDestructionCallback(java.lang.String, java.lang.Runnable)}. + */ + public void testRegisterDestructionCallback() { + final List list = new ArrayList(); + context.setAttribute("foo", "bar"); + scope.registerDestructionCallback("foo", new Runnable() { + public void run() { + list.add("foo"); + } + }); + assertEquals(0, list.size()); + // When the context is closed, provided the attribute exists the + // callback is called... + context.close(); + assertEquals(1, list.size()); + } + + /** + * Test method for + * {@link org.springframework.batch.execution.scope.StepScope#registerDestructionCallback(java.lang.String, java.lang.Runnable)}. + */ + public void testRegisterAnotherDestructionCallback() { + final List list = new ArrayList(); + context.setAttribute("foo", "bar"); + scope.registerDestructionCallback("foo", new Runnable() { + public void run() { + list.add("foo"); + } + }); + scope.registerDestructionCallback("foo", new Runnable() { + public void run() { + list.add("bar"); + } + }); + assertEquals(0, list.size()); + // When the context is closed, provided the attribute exists the + // callback is called... + context.close(); + assertEquals(2, list.size()); + } + + /** + * Test method for + * {@link org.springframework.batch.execution.scope.StepScope#remove(java.lang.String)}. + */ + public void testRemove() { + context.setAttribute("foo", "bar"); + scope.remove("foo"); + assertFalse(context.hasAttribute("foo")); + } + +} diff --git a/execution/src/test/java/org/springframework/batch/execution/step/DefaultStepExecutorFactoryTests.java b/execution/src/test/java/org/springframework/batch/execution/step/DefaultStepExecutorFactoryTests.java new file mode 100644 index 000000000..01af94dcc --- /dev/null +++ b/execution/src/test/java/org/springframework/batch/execution/step/DefaultStepExecutorFactoryTests.java @@ -0,0 +1,136 @@ +/* + * 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.execution.step; + +import junit.framework.TestCase; + +import org.springframework.batch.core.configuration.StepConfiguration; +import org.springframework.batch.core.configuration.StepConfigurationSupport; +import org.springframework.batch.core.executor.StepExecutor; +import org.springframework.batch.core.runtime.StepExecutionContext; +import org.springframework.batch.execution.step.simple.SimpleStepConfiguration; +import org.springframework.batch.execution.step.simple.SimpleStepExecutor; +import org.springframework.batch.io.exception.BatchCriticalException; +import org.springframework.batch.repeat.ExitStatus; +import org.springframework.batch.repeat.RepeatOperations; +import org.springframework.batch.repeat.support.RepeatTemplate; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.support.StaticApplicationContext; + +/** + * @author Dave Syer + * + */ +public class DefaultStepExecutorFactoryTests extends TestCase { + + private DefaultStepExecutorFactory factory = new DefaultStepExecutorFactory(); + private StaticApplicationContext applicationContext = new StaticApplicationContext(); + + protected void setUp() throws Exception { + factory.setBeanFactory(applicationContext); + } + + public void testMissingStepExecutorName() throws Exception { + try { + factory.afterPropertiesSet(); + fail("Expected IllegalArgumentException"); + } catch(IllegalArgumentException e) { + // Missing name is illegal + } + } + + public void testMissingStepExecutor() throws Exception { + factory.setStepExecutorName("foo"); + try { + factory.afterPropertiesSet(); + fail("Expected NoSuchBeanDefinitionException"); + } catch(NoSuchBeanDefinitionException e) { + // expected + } + } + + public void testSingletonStepExecutor() throws Exception { + applicationContext.getDefaultListableBeanFactory().registerBeanDefinition("foo", new RootBeanDefinition(SimpleStepExecutor.class)); + factory.setStepExecutorName("foo"); + try { + factory.afterPropertiesSet(); + fail("Expected IllegalStateException"); + } catch(IllegalStateException e) { + // expected + } + } + + public void testSuccessfulStepExecutor() throws Exception { + SimpleStepExecutor executor = new SimpleStepExecutor(); + applicationContext.getBeanFactory().registerSingleton("foo", executor); + factory.setStepExecutorName("foo"); + assertEquals(executor, factory.getExecutor(new SimpleStepConfiguration())); + } + + public void testSuccessfulStepExecutorWithNonSimpleConfigugration() throws Exception { + SimpleStepExecutor executor = new SimpleStepExecutor(); + applicationContext.getBeanFactory().registerSingleton("foo", executor); + factory.setStepExecutorName("foo"); + assertEquals(executor, factory.getExecutor(new StepConfigurationSupport())); + } + + public void testSuccessfulStepExecutorWithSimpleConfigurationAndNotSimpleExecutor() throws Exception { + StepExecutor executor = new StepExecutor() { + public ExitStatus process(StepConfiguration configuration, StepExecutionContext stepExecutionContext) throws BatchCriticalException { + return ExitStatus.FINISHED; + } + }; + applicationContext.getBeanFactory().registerSingleton("foo", executor); + factory.setStepExecutorName("foo"); + assertEquals(executor, factory.getExecutor(new SimpleStepConfiguration())); + } + + public void testSuccessfulStepExecutorHolderStrategy() throws Exception { + SimpleStepExecutor executor = new SimpleStepExecutor(); + applicationContext.getBeanFactory().registerSingleton("foo", executor); + factory.setStepExecutorName("foo"); + RepeatTemplate repeatTemplate = new RepeatTemplate(); + assertEquals(executor, factory.getExecutor(new SimpleHolderStepConfiguration(repeatTemplate))); + } + + public void testUnsuccessfulStepExecutorHolderStrategy() throws Exception { + SimpleStepExecutor executor = new SimpleStepExecutor(); + applicationContext.getBeanFactory().registerSingleton("foo", executor); + factory.setStepExecutorName("foo"); + try { + factory.getExecutor(new SimpleHolderStepConfiguration(null)); + fail("Expected IllegalStateException"); + } catch (IllegalStateException e) { + // expected + } + } + + /** + * @author Dave Syer + * + */ + public class SimpleHolderStepConfiguration extends SimpleStepConfiguration implements RepeatOperationsHolder { + private RepeatOperations executor; + public SimpleHolderStepConfiguration(RepeatOperations executor) { + this.executor = executor; + } + public RepeatOperations getChunkOperations() { + return executor; + } + } + +} diff --git a/execution/src/test/java/org/springframework/batch/execution/step/simple/ChunkOperationsStepConfigurationTests.java b/execution/src/test/java/org/springframework/batch/execution/step/simple/ChunkOperationsStepConfigurationTests.java new file mode 100644 index 000000000..3e44d388e --- /dev/null +++ b/execution/src/test/java/org/springframework/batch/execution/step/simple/ChunkOperationsStepConfigurationTests.java @@ -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.execution.step.simple; + +import junit.framework.TestCase; + +import org.springframework.batch.core.tasklet.Tasklet; +import org.springframework.batch.repeat.support.RepeatTemplate; + +/** + * @author Dave Syer + * + */ +public class ChunkOperationsStepConfigurationTests extends TestCase { + + ChunkOperationsStepConfiguration configuration = new ChunkOperationsStepConfiguration(); + + /** + * Test method for {@link org.springframework.batch.execution.step.simple.ChunkOperationsStepConfiguration#StepExecutorStepConfiguration(org.springframework.batch.core.executor.StepExecutor)}. + */ + public void testStepExecutorStepConfigurationRepeatOperations() { + RepeatTemplate executor = new RepeatTemplate(); + configuration = new ChunkOperationsStepConfiguration(executor); + assertEquals(executor, configuration.getChunkOperations()); + } + + /** + * Test method for {@link org.springframework.batch.execution.step.simple.ChunkOperationsStepConfiguration#StepExecutorStepConfiguration(org.springframework.batch.core.tasklet.Tasklet)}. + */ + public void testStepExecutorStepConfigurationTasklet() { + Tasklet tasklet = new Tasklet() { + public boolean execute() throws Exception { + return false; + } + }; + configuration = new ChunkOperationsStepConfiguration(tasklet); + assertEquals(tasklet, configuration.getTasklet()); + } + + /** + * Test method for {@link org.springframework.batch.execution.step.simple.ChunkOperationsStepConfiguration#getChunkOperations()}. + */ + public void testGetExecutor() { + assertNull(configuration.getChunkOperations()); + RepeatTemplate executor = new RepeatTemplate(); + configuration.setChunkOperations(executor); + assertEquals(executor, configuration.getChunkOperations()); + + } + +} diff --git a/execution/src/test/java/org/springframework/batch/execution/step/simple/DefaultStepExecutorTests.java b/execution/src/test/java/org/springframework/batch/execution/step/simple/DefaultStepExecutorTests.java new file mode 100644 index 000000000..5208ef06f --- /dev/null +++ b/execution/src/test/java/org/springframework/batch/execution/step/simple/DefaultStepExecutorTests.java @@ -0,0 +1,196 @@ +/* + * 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.execution.step.simple; + +import java.util.ArrayList; +import java.util.Arrays; + +import junit.framework.TestCase; + +import org.springframework.batch.core.domain.JobInstance; +import org.springframework.batch.core.domain.StepExecution; +import org.springframework.batch.core.domain.StepInstance; +import org.springframework.batch.core.runtime.JobExecutionContext; +import org.springframework.batch.core.runtime.SimpleJobIdentifier; +import org.springframework.batch.core.runtime.StepExecutionContext; +import org.springframework.batch.core.tasklet.Tasklet; +import org.springframework.batch.execution.repository.SimpleJobRepository; +import org.springframework.batch.execution.repository.dao.MapJobDao; +import org.springframework.batch.execution.repository.dao.MapStepDao; +import org.springframework.batch.execution.scope.StepSynchronizationManager; +import org.springframework.batch.execution.tasklet.ItemProviderProcessTasklet; +import org.springframework.batch.item.ItemProcessor; +import org.springframework.batch.item.ItemProvider; +import org.springframework.batch.item.provider.ListItemProvider; +import org.springframework.batch.repeat.policy.SimpleCompletionPolicy; +import org.springframework.batch.repeat.support.RepeatTemplate; + +public class DefaultStepExecutorTests extends TestCase { + + ArrayList processed = new ArrayList(); + + ItemProcessor processor = new ItemProcessor() { + public void process(Object data) throws Exception { + processed.add((String) data); + } + }; + + private DefaultStepExecutor stepExecutor; + + private AbstractStepConfiguration stepConfiguration; + + private ItemProvider getProvider(String[] args) { + return new ListItemProvider(Arrays.asList(args)); + } + + /** + * @param strings + * @return + */ + private Tasklet getTasklet(String[] strings) { + ItemProviderProcessTasklet module = new ItemProviderProcessTasklet(); + module.setItemProcessor(processor); + module.setItemProvider(getProvider(strings)); + return module; + } + + /* (non-Javadoc) + * @see junit.framework.TestCase#setUp() + */ + protected void setUp() throws Exception { + super.setUp(); + stepExecutor = new DefaultStepExecutor(); + stepExecutor.setRepository(new JobRepositorySupport()); + stepConfiguration = new SimpleStepConfiguration(); + stepConfiguration.setTasklet(getTasklet(new String[] {"foo", "bar", "spam"})); + // Only process one chunk: + RepeatTemplate template = new RepeatTemplate(); + template.setCompletionPolicy(new SimpleCompletionPolicy(1)); + stepExecutor.setStepOperations(template); + // Only process one item: + template = new RepeatTemplate(); + template.setCompletionPolicy(new SimpleCompletionPolicy(1)); + stepExecutor.setChunkOperations(template); + } + + public void testStepExecutor() throws Exception { + + StepInstance step = new StepInstance(new Long(9)); + JobExecutionContext jobExecutionContext = new JobExecutionContext(new SimpleJobIdentifier("FOO"), new JobInstance(new Long(3))); + StepExecutionContext stepExecutionContext = new StepExecutionContext(jobExecutionContext, step); + + stepExecutor.process(stepConfiguration, stepExecutionContext); + assertEquals(1, processed.size()); + } + + public void testChunkExecutor() throws Exception { + + RepeatTemplate template = new RepeatTemplate(); + + // Only process one item: + template.setCompletionPolicy(new SimpleCompletionPolicy(1)); + stepExecutor.setChunkOperations(template); + + StepInstance step = new StepInstance(new Long(1)); + step.setStepExecution(new StepExecution(new Long(1), new Long(2))); + JobExecutionContext jobExecutionContext = new JobExecutionContext(new SimpleJobIdentifier("FOO"), new JobInstance(new Long(1))); + + StepExecutionContext stepExecutionContext = new StepExecutionContext(jobExecutionContext, step); + stepExecutor.processChunk(stepConfiguration, stepExecutionContext); + assertEquals(1, processed.size()); + + } + + public void testStepContextInitialized() throws Exception { + + RepeatTemplate template = new RepeatTemplate(); + + // Only process one item: + template.setCompletionPolicy(new SimpleCompletionPolicy(1)); + stepExecutor.setChunkOperations(template); + + final StepInstance step = new StepInstance(new Long(1)); + step.setStepExecution(new StepExecution(new Long(1),new Long(1))); + final JobExecutionContext jobExecutionContext = new JobExecutionContext(new SimpleJobIdentifier("FOO"), new JobInstance(new Long(3))); + final StepExecutionContext stepExecutionContext = new StepExecutionContext(jobExecutionContext, step); + + stepConfiguration.setTasklet(new Tasklet() { + public boolean execute() throws Exception { + assertEquals(step, stepExecutionContext.getStep()); + assertEquals(1, jobExecutionContext.getChunkContexts().size()); + assertEquals(1, jobExecutionContext.getStepContexts().size()); + assertNotNull(StepSynchronizationManager.getContext().getJobIdentifier()); + processed.add("foo"); + return true; + } + }); + + stepExecutor.process(stepConfiguration, stepExecutionContext); + assertEquals(1, processed.size()); + + } + + public void testRepository() throws Exception { + + SimpleJobRepository repository = new SimpleJobRepository(new MapJobDao(), new MapStepDao()); + stepExecutor.setRepository(repository); + + StepInstance step = new StepInstance(new Long(1)); + JobExecutionContext jobExecutionContext = new JobExecutionContext(new SimpleJobIdentifier("FOO"), new JobInstance(new Long(3))); + StepExecutionContext stepExecutionContext = new StepExecutionContext(jobExecutionContext, step); + + JobInstance job = new JobInstance(new Long(1)); + job.setIdentifier(new SimpleJobIdentifier("foo_bar")); + + stepExecutor.process(stepConfiguration, stepExecutionContext); + assertEquals(1, processed.size()); + + // assertEquals(1, repository.findJobs(job.?).size()); + } + + public void testIncrementRollbackCount(){ + + Tasklet module = new Tasklet(){ + + public boolean execute() throws Exception { + int counter = 0; + counter++; + + if(counter == 1){ + throw new Exception(); + } + + return true; + } + + }; + + StepInstance step = new StepInstance(new Long(1)); + stepConfiguration.setTasklet(module); + JobExecutionContext jobExecutionContext = new JobExecutionContext(new SimpleJobIdentifier("FOO"), new JobInstance(new Long(3))); + StepExecutionContext stepExecutionContext = new StepExecutionContext(jobExecutionContext, step); + + try{ + stepExecutor.process(stepConfiguration, stepExecutionContext); + } + catch(Exception ex){ + assertEquals(step.getStepExecution().getRollbackCount(), new Integer(1)); + } + + } + +} diff --git a/execution/src/test/java/org/springframework/batch/execution/step/simple/JobRepositorySupport.java b/execution/src/test/java/org/springframework/batch/execution/step/simple/JobRepositorySupport.java new file mode 100644 index 000000000..a471fc140 --- /dev/null +++ b/execution/src/test/java/org/springframework/batch/execution/step/simple/JobRepositorySupport.java @@ -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.execution.step.simple; + +import org.springframework.batch.core.configuration.JobConfiguration; +import org.springframework.batch.core.domain.JobExecution; +import org.springframework.batch.core.domain.JobInstance; +import org.springframework.batch.core.domain.StepExecution; +import org.springframework.batch.core.domain.StepInstance; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.runtime.JobIdentifier; + +/** + * @author Dave Syer + * + */ +public class JobRepositorySupport implements JobRepository { + + /* (non-Javadoc) + * @see org.springframework.batch.container.common.repository.JobRepository#findOrCreateJob(org.springframework.batch.container.common.domain.JobConfiguration) + */ + public JobInstance findOrCreateJob(JobConfiguration jobConfiguration, JobIdentifier runtimeInformation) { + return null; + } + + /* (non-Javadoc) + * @see org.springframework.batch.container.common.repository.JobRepository#saveOrUpdate(org.springframework.batch.container.common.domain.JobExecution) + */ + public void saveOrUpdate(JobExecution jobExecution) { + } + + /* (non-Javadoc) + * @see org.springframework.batch.container.common.repository.JobRepository#saveOrUpdate(org.springframework.batch.container.common.domain.StepExecution) + */ + public void saveOrUpdate(StepExecution stepExecution) { + } + + /* (non-Javadoc) + * @see org.springframework.batch.container.common.repository.JobRepository#update(org.springframework.batch.container.common.domain.Job) + */ + public void update(JobInstance job) { + } + + /* (non-Javadoc) + * @see org.springframework.batch.container.common.repository.JobRepository#update(org.springframework.batch.container.common.domain.Step) + */ + public void update(StepInstance step) { + } + +} diff --git a/execution/src/test/java/org/springframework/batch/execution/step/simple/SimpleStepConfigurationTests.java b/execution/src/test/java/org/springframework/batch/execution/step/simple/SimpleStepConfigurationTests.java new file mode 100644 index 000000000..85384089b --- /dev/null +++ b/execution/src/test/java/org/springframework/batch/execution/step/simple/SimpleStepConfigurationTests.java @@ -0,0 +1,106 @@ +/* + * 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.execution.step.simple; + +import junit.framework.TestCase; + +import org.springframework.batch.core.tasklet.Tasklet; +import org.springframework.batch.repeat.exception.handler.DefaultExceptionHandler; + +/** + * @author Dave Syer + * + */ +public class SimpleStepConfigurationTests extends TestCase { + + SimpleStepConfiguration configuration = new SimpleStepConfiguration("foo"); + + /** + * Test method for {@link org.springframework.batch.execution.step.simple.SimpleStepConfiguration#SimpleStepConfiguration()}. + */ + public void testSimpleStepConfiguration() { + assertNotNull(configuration.getName()); + configuration = new SimpleStepConfiguration(); + assertNull(configuration.getName()); + } + + /** + * Test method for {@link org.springframework.batch.execution.step.simple.SimpleStepConfiguration#SimpleStepConfiguration(org.springframework.batch.core.tasklet.Tasklet)}. + */ + public void testSimpleStepConfigurationTasklet() { + Tasklet tasklet = new Tasklet() { + public boolean execute() throws Exception { + return false; + } + }; + configuration = new SimpleStepConfiguration(tasklet); + assertEquals(tasklet, configuration.getTasklet()); + } + + /** + * Test method for {@link org.springframework.batch.execution.step.simple.SimpleStepConfiguration#getCommitInterval()}. + */ + public void testGetCommitInterval() { + assertEquals(1, configuration.getCommitInterval()); + configuration.setCommitInterval(20); + assertEquals(20, configuration.getCommitInterval()); + } + + /** + * Test method for {@link org.springframework.batch.execution.step.simple.AbstractStepConfiguration#setBeanName(java.lang.String)}. + */ + public void testSetBeanName() { + configuration.setBeanName("bar"); + assertEquals("foo", configuration.getName()); + } + + /** + * Test method for {@link org.springframework.batch.execution.step.simple.AbstractStepConfiguration#setBeanName(java.lang.String)}. + */ + public void testSetBeanNameOverrideNull() { + configuration = new SimpleStepConfiguration(); + configuration.setBeanName("bar"); + assertEquals("bar", configuration.getName()); + } + + /** + * Test method for {@link org.springframework.batch.execution.step.simple.AbstractStepConfiguration#getExceptionHandler()}. + */ + public void testGetExceptionHandler() { + assertNull(configuration.getExceptionHandler()); + configuration.setExceptionHandler(new DefaultExceptionHandler()); + assertNotNull(configuration.getExceptionHandler()); + } + + /** + * Test method for {@link org.springframework.batch.execution.step.simple.AbstractStepConfiguration#getSkipLimit()}. + */ + public void testGetSkipLimit() { + assertEquals(0, configuration.getSkipLimit()); + configuration.setSkipLimit(20); + assertEquals(20, configuration.getSkipLimit()); + } + + /** + * Test method for {@link org.springframework.batch.execution.step.simple.AbstractStepConfiguration#isSaveRestartData()}. + */ + public void testIsSaveRestartData() { + assertEquals(false, configuration.isSaveRestartData()); + configuration.setSaveRestartData(true); + assertEquals(true, configuration.isSaveRestartData()); + } + +} diff --git a/execution/src/test/java/org/springframework/batch/execution/step/simple/StepExecutorInterruptionTests.java b/execution/src/test/java/org/springframework/batch/execution/step/simple/StepExecutorInterruptionTests.java new file mode 100644 index 000000000..235c1c425 --- /dev/null +++ b/execution/src/test/java/org/springframework/batch/execution/step/simple/StepExecutorInterruptionTests.java @@ -0,0 +1,124 @@ +/* + * 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.execution.step.simple; + +import java.util.List; + +import junit.framework.TestCase; + +import org.springframework.batch.core.configuration.JobConfiguration; +import org.springframework.batch.core.domain.BatchStatus; +import org.springframework.batch.core.domain.JobInstance; +import org.springframework.batch.core.domain.StepInstance; +import org.springframework.batch.core.executor.StepInterruptedException; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.runtime.JobExecutionContext; +import org.springframework.batch.core.runtime.JobIdentifier; +import org.springframework.batch.core.runtime.SimpleJobIdentifier; +import org.springframework.batch.core.runtime.StepExecutionContext; +import org.springframework.batch.core.tasklet.Tasklet; +import org.springframework.batch.execution.repository.SimpleJobRepository; +import org.springframework.batch.execution.repository.dao.JobDao; +import org.springframework.batch.execution.repository.dao.MapJobDao; +import org.springframework.batch.execution.repository.dao.MapStepDao; +import org.springframework.batch.execution.repository.dao.StepDao; +import org.springframework.batch.repeat.policy.SimpleCompletionPolicy; +import org.springframework.batch.repeat.support.RepeatTemplate; + +public class StepExecutorInterruptionTests extends TestCase { + + private JobRepository jobRepository; + + private JobDao jobDao = new MapJobDao(); + + private StepDao stepDao = new MapStepDao(); + + private JobInstance job; + + private AbstractStepConfiguration stepConfiguration; + + private SimpleStepExecutor executor; + + public void setUp() { + + jobRepository = new SimpleJobRepository(jobDao, stepDao); + + JobConfiguration jobConfiguration = new JobConfiguration(); + stepConfiguration = new SimpleStepConfiguration(); + jobConfiguration.addStep(stepConfiguration); + JobIdentifier runtimeInformation = new SimpleJobIdentifier("TestJob"); + jobConfiguration.setName("testJob"); + job = jobRepository.findOrCreateJob(jobConfiguration, runtimeInformation); + executor = new SimpleStepExecutor(); + } + + public void testInterruptChunk() throws Exception { + + executor.setRepository(jobRepository); + + List steps = job.getSteps(); + final StepInstance step = (StepInstance) steps.get(0); + JobExecutionContext jobExecutionContext = new JobExecutionContext(null, new JobInstance(new Long(0))); + final StepExecutionContext stepExecutionContext = new StepExecutionContext(jobExecutionContext, step); + stepConfiguration.setTasklet(new Tasklet() { + public boolean execute() throws Exception { + // do something non-trivial (and not Thread.sleep()) + double foo = 1; + for (int i = 2; i < 250; i++) { + foo = foo * i; + } + // always return true, so processing always continues + return foo != 1; + } + }); + + Thread processingThread = new Thread() { + public void run() { + try { + executor.process(stepConfiguration, stepExecutionContext); + } + catch (StepInterruptedException e) { + // do nothing... + } + } + }; + + processingThread.start(); + + Thread.sleep(500); + + processingThread.interrupt(); + + int count = 0; + while (processingThread.isAlive() && count < 15) { + Thread.sleep(20); + count++; + } + + assertFalse(processingThread.isAlive()); + assertEquals(BatchStatus.STOPPED, step.getStatus()); + } + + public void testInterruptStep() throws Exception { + RepeatTemplate template = new RepeatTemplate(); + // N.B, If we don't set the completion policy it might run forever + template.setCompletionPolicy(new SimpleCompletionPolicy(2)); + executor.setChunkOperations(template); + testInterruptChunk(); + } + +} diff --git a/execution/src/test/java/org/springframework/batch/execution/step/simple/ThreadStepInterruptionPolicyTests.java b/execution/src/test/java/org/springframework/batch/execution/step/simple/ThreadStepInterruptionPolicyTests.java new file mode 100644 index 000000000..22e7f0629 --- /dev/null +++ b/execution/src/test/java/org/springframework/batch/execution/step/simple/ThreadStepInterruptionPolicyTests.java @@ -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.execution.step.simple; + +import junit.framework.TestCase; + +import org.springframework.batch.core.executor.StepInterruptedException; +import org.springframework.batch.repeat.RepeatContext; +import org.springframework.batch.repeat.context.RepeatContextSupport; + +/** + * @author Dave Syer + * + */ +public class ThreadStepInterruptionPolicyTests extends TestCase { + + ThreadStepInterruptionPolicy policy = new ThreadStepInterruptionPolicy(); + private RepeatContext context = new RepeatContextSupport(null);; + + /** + * Test method for {@link org.springframework.batch.core.executor.interrupt.ThreadStepInterruptionPolicy#checkInterrupted(org.springframework.batch.repeat.RepeatContext)}. + * @throws Exception + */ + public void testCheckInterruptedNotComplete() throws Exception { + policy.checkInterrupted(context); + // no exception + } + + /** + * Test method for {@link org.springframework.batch.core.executor.interrupt.ThreadStepInterruptionPolicy#checkInterrupted(org.springframework.batch.repeat.RepeatContext)}. + * @throws Exception + */ + public void testCheckInterruptedComplete() throws Exception { + context.setTerminateOnly(); + try { + policy.checkInterrupted(context); + fail("Expected StepInterruptedException"); + } catch (StepInterruptedException e) { + // expected + assertTrue(e.getMessage().indexOf("interrupt")>=0); + } + } + +} diff --git a/execution/src/test/java/org/springframework/batch/execution/tasklet/ItemProviderProcessTaskletTests.java b/execution/src/test/java/org/springframework/batch/execution/tasklet/ItemProviderProcessTaskletTests.java new file mode 100644 index 000000000..2298e4874 --- /dev/null +++ b/execution/src/test/java/org/springframework/batch/execution/tasklet/ItemProviderProcessTaskletTests.java @@ -0,0 +1,342 @@ +/* + * 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.execution.tasklet; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Properties; + +import junit.framework.TestCase; + +import org.springframework.batch.execution.tasklet.ItemProviderProcessTasklet; +import org.springframework.batch.io.Skippable; +import org.springframework.batch.item.ItemProcessor; +import org.springframework.batch.item.ItemProvider; +import org.springframework.batch.item.provider.AbstractItemProvider; +import org.springframework.batch.repeat.context.RepeatContextSupport; +import org.springframework.batch.repeat.synch.RepeatSynchronizationManager; +import org.springframework.batch.retry.policy.NeverRetryPolicy; +import org.springframework.batch.retry.support.RetryTemplate; +import org.springframework.batch.statistics.StatisticsProvider; +import org.springframework.batch.support.PropertiesConverter; + +/** + * @author Dave Syer + * @author Peter Zozom + */ +public class ItemProviderProcessTaskletTests extends TestCase { + + private List list = new ArrayList(); + + private List items = new ArrayList(); + + private ItemProvider itemProvider = new AbstractItemProvider() { + int count = 0; + + public Object next() throws Exception { + if (count < items.size()) { + Object data = items.get(count++); + if (data instanceof Exception) { + throw (Exception) data; + } + return data; + } + return null; + } + }; + + private ItemProcessor itemProcessor = new ItemProcessor() { + public void process(Object data) throws Exception { + list.add(data); + } + }; + + private ItemProviderProcessTasklet module; + + public void setUp() { + + // create module + module = new ItemProviderProcessTasklet(); + + // set up module + module.setItemProvider(itemProvider); + module.setItemProcessor(itemProcessor); + + RepeatSynchronizationManager.register(new RepeatContextSupport(null)); + + } + + /* + * (non-Javadoc) + * @see junit.framework.TestCase#tearDown() + */ + protected void tearDown() throws Exception { + super.tearDown(); + RepeatSynchronizationManager.clear(); + } + + // tests also read and process + public void testExecute() throws Exception { + + // TEST1: data provider returns some object and data processor should + // process it + + // set up mock objects + items = Collections.singletonList("foo"); + + // call execute + assertTrue(module.execute()); + + // verify method calls + assertEquals(1, list.size()); + assertEquals("foo", list.get(0)); + } + + public void testExecuteWithNothingToRead() throws Exception { + + // TEST2: data provider returns null (nothing to read) + + // call read + assertFalse(module.execute()); + + } + + public void testExecuteWithExceptionOnRead() throws Exception { + + // TEST3: exception is thrown by data provider + + // set up mock objects + items = Collections.singletonList(new RuntimeException("foo")); + + // call read + try { + module.execute(); + // TODO: should we expect Batch exception? + fail("RuntimeException was expected"); + } + catch (RuntimeException bce) { + // expected + } + } + + public void testNotSkippable() throws Exception { + try { + module.skip(); + } catch (Exception e) { + // Unexpected + throw e; + } + } + + public void testSkippableProvider() throws Exception { + module.setItemProvider(new SkippableItemProvider()); + module.skip(); + assertEquals(1, list.size()); + } + + public void testSkippablProviderProcessor() throws Exception { + module.setItemProvider(new SkippableItemProvider()); + module.setItemProcessor(new SkippableItemProcessor()); + module.skip(); + assertEquals(2, list.size()); + } + + public void testStatisticsProvider() throws Exception { + module.setItemProvider(new SkippableItemProvider()); + Properties stats = module.getStatistics(); + assertEquals(1, stats.size()); + assertEquals("bar", stats.getProperty("foo")); + } + + public void testStatisticsProcessor() throws Exception { + module.setItemProcessor(new SkippableItemProcessor()); + Properties stats = module.getStatistics(); + assertEquals(1, stats.size()); + assertEquals("bar", stats.getProperty("foo")); + } + + public void testStatisticsProviderProcessor() throws Exception { + module.setItemProvider(new SkippableItemProvider()); + module.setItemProcessor(new SkippableItemProcessor()); + Properties stats = module.getStatistics(); + assertEquals(2, stats.size()); + assertEquals("bar", stats.getProperty("provider.foo")); + assertEquals("bar", stats.getProperty("processor.foo")); + } + + public void testStatisticsProviderProcessorMergeDuplicates() throws Exception { + module.setItemProvider(new SkippableItemProvider()); + module.setItemProcessor(new SkippableItemProcessor("foo=bar\nspam=bucket")); + Properties stats = module.getStatistics(); + assertEquals(3, stats.size()); + assertEquals("bar", stats.getProperty("provider.foo")); + assertEquals("bar", stats.getProperty("processor.foo")); + assertEquals("bucket", stats.getProperty("spam")); + } + + public void testRecoverable() throws Exception { + + // set up and call execute + items = Collections.singletonList("foo"); + + module.setItemProvider(new AbstractItemProvider() { + public boolean recover(Object item, Throwable cause) { + assertEquals("foo", cause.getMessage()); + list.add(item); + return true; + } + + public Object next() throws Exception { + return itemProvider.next(); + } + }); + module.setItemProcessor(new ItemProcessor() { + public void process(Object data) throws Exception { + throw new RuntimeException("FOO"); + } + }); + + try { + module.execute(); + fail("Expected RuntimeException"); + } + catch (RuntimeException e) { + assertEquals("FOO", e.getMessage()); + } + list.clear(); + + // After a processing exception client has to call recover directly + module.recover(new RuntimeException("foo")); + + // verify method calls + assertEquals(1, list.size()); + assertEquals("The item was not passed in to recover method", "foo", list.get(0)); + } + + public void testRetryPolicy() throws Exception { + module.setRetryPolicy(new NeverRetryPolicy()); + // set up mock objects + items = new ArrayList() { + { + add("foo"); + add("foo"); // in production use this would be the second + // attempt after rollback + } + }; + + module.setItemProvider(new AbstractItemProvider() { + public boolean recover(Object item, Throwable cause) { + assertEquals("FOO", cause.getMessage()); + list.add(item + "_recovered"); + return true; + } + + public Object next() throws Exception { + return itemProvider.next(); + } + }); + module.setItemProcessor(new ItemProcessor() { + public void process(Object data) throws Exception { + throw new RuntimeException("FOO"); + } + }); + + // finish initialisation + module.afterPropertiesSet(); + + try { + module.execute(); + fail("Expected RuntimeException"); + } + catch (RuntimeException e) { + assertEquals("FOO", e.getMessage()); + } + + // No exception thrown now because we are going to recover... + module.execute(); + + // No need for client has to call recover directly + + // verify method calls + assertEquals(1, list.size()); + assertEquals("The item was not passed in to recover method", "foo_recovered", list.get(0)); + } + + public void testInitialisationWithNullProvider() throws Exception { + module.setItemProvider(null); + try { + module.afterPropertiesSet(); + } + catch (IllegalArgumentException e) { + assertTrue(e.getMessage().toLowerCase().indexOf("provider") >= 0); + } + } + + public void testInitialisationWithNullProcessor() throws Exception { + module.setItemProcessor(null); + try { + module.afterPropertiesSet(); + } + catch (IllegalArgumentException e) { + assertTrue(e.getMessage().toLowerCase().indexOf("processor") >= 0); + } + } + + public void testInitialisationWithNotNullPolicyAndOperations() throws Exception { + module.setRetryPolicy(new NeverRetryPolicy()); + module.setRetryOperations(new RetryTemplate()); + try { + module.afterPropertiesSet(); + } + catch (IllegalStateException e) { + assertTrue(e.getMessage().toLowerCase().indexOf("not both") >= 0); + } + } + + private class SkippableItemProvider extends AbstractItemProvider implements Skippable, StatisticsProvider { + public Object next() throws Exception { + return itemProvider.next(); + } + public void skip() { + list.add("provider"); + } + public Properties getStatistics() { + return PropertiesConverter.stringToProperties("foo=bar"); + } + } + + private class SkippableItemProcessor implements ItemProcessor, Skippable, StatisticsProvider { + String props = "foo=bar"; + public SkippableItemProcessor() { + super(); + } + public SkippableItemProcessor(String props) { + this(); + this.props = props; + } + public void process(Object data) throws Exception { + // no-op + } + public void skip() { + list.add("processor"); + } + public Properties getStatistics() { + return PropertiesConverter.stringToProperties(props); + } + } +} diff --git a/execution/src/test/java/org/springframework/batch/execution/tasklet/RestartableItemProviderTaskletTests.java b/execution/src/test/java/org/springframework/batch/execution/tasklet/RestartableItemProviderTaskletTests.java new file mode 100644 index 000000000..56ee1d03c --- /dev/null +++ b/execution/src/test/java/org/springframework/batch/execution/tasklet/RestartableItemProviderTaskletTests.java @@ -0,0 +1,149 @@ +/* + * 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.execution.tasklet; + +import java.util.Properties; + +import junit.framework.TestCase; + +import org.springframework.batch.execution.tasklet.RestartableItemProviderTasklet; +import org.springframework.batch.item.ItemProcessor; +import org.springframework.batch.item.ItemProvider; +import org.springframework.batch.restart.GenericRestartData; +import org.springframework.batch.restart.RestartData; +import org.springframework.batch.restart.Restartable; +import org.springframework.batch.support.PropertiesConverter; + +/** + * @author Peter Zozom + */ +public class RestartableItemProviderTaskletTests extends TestCase { + + private static class MockProvider implements ItemProvider, Restartable { + + RestartData data = new RestartData() { + + public Properties getProperties() { + return PropertiesConverter.stringToProperties("a=b"); + } + + }; + + public Object next() { + return null; + } + + public RestartData getRestartData() { + return data; + } + + public void restoreFrom(RestartData data) { + // restart data should be same as returned by getRestartData + assertEquals(this.data.getProperties(), data.getProperties()); + } + + public Object getKey(Object item) { + return null; + } + + public boolean recover(Object data, Throwable cause) { + return false; + } + + } + + private static class MockProcessor implements ItemProcessor, Restartable { + + RestartData data = new RestartData() { + public Properties getProperties() { + return PropertiesConverter.stringToProperties("x=y"); + } + }; + + public void process(Object data) { + } + + public RestartData getRestartData() { + return data; + } + + public void restoreFrom(RestartData data) { + // restart data should be same as returned by getRestartData + assertEquals(this.data.getProperties(), data.getProperties()); + } + + } + + private ItemProvider itemProvider; + + private ItemProcessor itemProcessor; + + private RestartableItemProviderTasklet module; + + public void testRestart() { + + // create data provider and data processor + itemProvider = new MockProvider(); + itemProcessor = new MockProcessor(); + + // create and set up module + module = new RestartableItemProviderTasklet(); + module.setItemProvider(itemProvider); + module.setItemProcessor(itemProcessor); + + // get restart data + RestartData data = module.getRestartData(); + assertNotNull(data); + // restore from restart data (see asserts in mock classes) + module.restoreFrom(data); + } + + public void testRestartFromGenericData() { + + // create data provider and data processor + itemProvider = new MockProvider(); + itemProcessor = new MockProcessor(); + + // create and set up module + module = new RestartableItemProviderTasklet(); + module.setItemProvider(itemProvider); + module.setItemProcessor(itemProcessor); + + // get restart data + RestartData data = module.getRestartData(); + assertNotNull(data); + data = new GenericRestartData(data.getProperties()); + // restore from restart data (see asserts in mock classes) + module.restoreFrom(data); + } + + public void testRestartFromNotRestartable() { + + // create and set up module + module = new RestartableItemProviderTasklet(); + module.setItemProvider(null); + module.setItemProcessor(null); + + // get restart data + RestartData data = module.getRestartData(); + assertNotNull(data); + // restore from restart data (see asserts in mock classes) + module.restoreFrom(data); + System.err.println(data.getProperties()); + } + +} diff --git a/execution/src/test/java/org/springframework/batch/execution/tasklet/support/CompositeItemProcessorTests.java b/execution/src/test/java/org/springframework/batch/execution/tasklet/support/CompositeItemProcessorTests.java new file mode 100644 index 000000000..3dcf760c0 --- /dev/null +++ b/execution/src/test/java/org/springframework/batch/execution/tasklet/support/CompositeItemProcessorTests.java @@ -0,0 +1,148 @@ +package org.springframework.batch.execution.tasklet.support; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Properties; + +import junit.framework.TestCase; + +import org.easymock.MockControl; +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; + +/** + * Tests for {@link CompositeItemProcessor} + * + * @author Robert Kasanicky + */ +public class CompositeItemProcessorTests extends TestCase { + + // object under test + private CompositeItemProcessor itemProcessor = new CompositeItemProcessor(); + + /** + * Regular usage scenario. + * All injected processors should be called. + */ + public void testProcess() throws Exception { + + final int NUMBER_OF_PROCESSORS = 10; + Object data = new Object(); + + List controls = new ArrayList(NUMBER_OF_PROCESSORS); + List processors = new ArrayList(NUMBER_OF_PROCESSORS); + + for (int i = 0; i < NUMBER_OF_PROCESSORS; i++) { + MockControl control = MockControl.createStrictControl(ItemProcessor.class); + ItemProcessor processor = (ItemProcessor) control.getMock(); + + processor.process(data); + control.setVoidCallable(); + control.replay(); + + processors.add(processor); + controls.add(control); + } + + itemProcessor.setItemProcessors(processors); + itemProcessor.process(data); + + for (Iterator iterator = controls.iterator(); iterator.hasNext();) { + MockControl control = (MockControl) iterator.next(); + control.verify(); + } + } + + /** + * Statistics of injected ItemProcessors should be returned under keys prefixed with their list index. + */ + public void testStatistics() { + final ItemProcessor p1 = new ItemProcessorStub(); + final ItemProcessor p2 = new ItemProcessorStub(); + + List itemProcessors = new ArrayList(){{ + add(p1); + add(p2); + }}; + + itemProcessor.setItemProcessors(itemProcessors); + Properties stats = itemProcessor.getStatistics(); + assertEquals(String.valueOf(p1.hashCode()), stats.getProperty("0#" + ItemProcessorStub.STATS_KEY)); + assertEquals(String.valueOf(p2.hashCode()), stats.getProperty("1#" + ItemProcessorStub.STATS_KEY)); + } + + /** + * All Restartable processors should be restarted, not-Restartable processors should be ignored. + */ + public void testRestart() { + //this mock with undefined behavior makes sure not-Restartable processor is ignored + MockControl p1c = MockControl.createStrictControl(ItemProcessor.class); + final ItemProcessor p1 = (ItemProcessor) p1c.getMock(); + + final ItemProcessor p2 = new ItemProcessorStub(); + final ItemProcessor p3 = new ItemProcessorStub(); + List itemProcessors = new ArrayList(){{ + add(p1); + add(p2); + add(p3); + }}; + itemProcessor.setItemProcessors(itemProcessors); + + RestartData rd = itemProcessor.getRestartData(); + itemProcessor.restoreFrom(rd); + + for (Iterator iterator = itemProcessors.iterator(); iterator.hasNext();) { + ItemProcessor processor = (ItemProcessor) iterator.next(); + if (processor instanceof ItemProcessorStub) { + assertTrue("Injected processors are restarted", + ((ItemProcessorStub)processor).restarted); + } + } + + } + + /** + * Stub for testing restart. Checks the restart data received is the same that was returned by + * getRestartData() + */ + private static class ItemProcessorStub implements ItemProcessor, Restartable, StatisticsProvider { + + private static final String RESTART_KEY = "restartData"; + private static final String STATS_KEY = "stats"; + + private boolean restarted = false; + + private final int hashCode = this.hashCode(); + + + public RestartData getRestartData() { + Properties props = new Properties(){{ + setProperty(RESTART_KEY, String.valueOf(hashCode)); + }}; + return new GenericRestartData(props); + } + + public void restoreFrom(RestartData data) { + if (Integer.valueOf(data.getProperties().getProperty(RESTART_KEY)).intValue() != hashCode()) { + fail("received restart data is not the same which was saved"); + } + restarted = true; + } + + public void process(Object data) throws Exception { + // do nothing + } + + public Properties getStatistics() { + return new Properties() {{ + setProperty(STATS_KEY, String.valueOf(hashCode)); + }}; + } + + } + +} diff --git a/execution/src/test/java/org/springframework/batch/execution/tasklet/support/DefaultFlatFileItemProviderTests.java b/execution/src/test/java/org/springframework/batch/execution/tasklet/support/DefaultFlatFileItemProviderTests.java new file mode 100644 index 000000000..d3926db34 --- /dev/null +++ b/execution/src/test/java/org/springframework/batch/execution/tasklet/support/DefaultFlatFileItemProviderTests.java @@ -0,0 +1,189 @@ +/* + * 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.execution.tasklet.support; + +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +import junit.framework.TestCase; + +import org.springframework.batch.execution.tasklet.support.DefaultFlatFileItemProvider; +import org.springframework.batch.io.exception.ValidationException; +import org.springframework.batch.io.file.FieldSet; +import org.springframework.batch.io.file.FieldSetInputSource; +import org.springframework.batch.io.file.FieldSetMapper; +import org.springframework.batch.io.file.support.DefaultFlatFileInputSource; +import org.springframework.batch.item.validator.Validator; +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.batch.support.PropertiesConverter; +import org.springframework.core.io.ByteArrayResource; + +/** + * Unit tests for {@link DefaultFlatFileItemProvider} + * + * @author Robert Kasanicky + */ +public class DefaultFlatFileItemProviderTests extends TestCase { + + public static String FOO = "foo"; + // object under test + private DefaultFlatFileItemProvider itemProvider = new DefaultFlatFileItemProvider(); + + // Input source + private DefaultFlatFileInputSource source; + + //mock mapper + private FieldSetMapper mapper; + + private List list = new ArrayList(); + + // create mock objects and inject them into data provider + protected void setUp() throws Exception { + source = new DefaultFlatFileInputSource(); + source.setResource(new ByteArrayResource("a,b".getBytes())); + mapper = new FieldSetMapper() { + public Object mapLine(FieldSet fs) { + return FOO; + } + }; + itemProvider.setSource(source); + itemProvider.setMapper(mapper); + assertTrue(Restartable.class.isAssignableFrom(DefaultFlatFileInputSource.class)); + assertTrue(FieldSetInputSource.class.isAssignableFrom(DefaultFlatFileInputSource.class)); + assertTrue(StatisticsProvider.class.isAssignableFrom(DefaultFlatFileInputSource.class)); + } + + /** + * Uses input template to provide the domain object. + */ + public void testNext() { + Object result = itemProvider.next(); + assertSame("domain object is provided by the input template", FOO, result); + } + + /** + * Uses input template to provide the domain object. + */ + public void testNextWithValidator() { + itemProvider.setValidator(new Validator() { + public void validate(Object value) throws ValidationException { + list.add(value); + } + }); + itemProvider.next(); + assertSame("domain object is provided by the input template", FOO, list.get(0)); + } + + /** + * Uses input template to provide the domain object. + */ + public void testNextWithValidatorAndInvalidData() { + itemProvider.setValidator(new Validator() { + public void validate(Object value) throws ValidationException { + throw new ValidationException("Invalid input"); + } + }); + try { + itemProvider.next(); + fail("Expected ValidationException"); + } catch (ValidationException e) { + // expected + assertEquals("Invalid input", e.getMessage()); + } + } + + /** + * Gets statistics from the input template + */ + public void testGetStatistics() { + Properties statistics = ((StatisticsProvider) source).getStatistics(); + assertEquals(statistics, itemProvider.getStatistics()); + } + + /** + * Gets statistics from the input template + */ + public void testGetStatisticsWithoutStatisticsProvider() { + itemProvider.setSource(null); + Properties props = itemProvider.getStatistics(); + assertEquals(null, props.getProperty("a")); + } + + /** + * Gets restart data from the input template + */ + public void testGetRestartData() { + RestartData data = ((Restartable) source).getRestartData(); + assertEquals(data.getProperties(), itemProvider.getRestartData().getProperties()); + } + + /** + * Forwarded restart data to input template + */ + public void testRestoreFrom() { + + final List list = new ArrayList(); + + RestartData data = new RestartData() { + + public Properties getProperties() { + list.add(FOO); + return ((Restartable) source).getRestartData().getProperties(); + }}; + + itemProvider.restoreFrom(data); + + //assertEquals(1, list.size()); getProperties are called multiple times due to null checks + assertTrue(list.size() > 0); + } + + /** + * Forward restart data to input template + * @throws Exception + */ + public void testRestoreFromWithoutRestartable() throws Exception { + itemProvider.setSource(null); + try { + itemProvider.restoreFrom(new GenericRestartData(PropertiesConverter.stringToProperties("value=bar"))); + fail("Expected IllegalStateException"); + } + catch (IllegalStateException e) { + // expected + } + } + + /** + * Forward restart data to input template + * @throws Exception + */ + public void testGetRestartDataWithoutRestartable() throws Exception { + itemProvider.setSource(null); + try { + itemProvider.getRestartData(); + fail("Expected IllegalStateException"); + } + catch (IllegalStateException e) { + // expected + } + } + + +} diff --git a/execution/src/test/java/org/springframework/batch/execution/tasklet/support/InputSourceItemProviderTests.java b/execution/src/test/java/org/springframework/batch/execution/tasklet/support/InputSourceItemProviderTests.java new file mode 100644 index 000000000..ed3407e83 --- /dev/null +++ b/execution/src/test/java/org/springframework/batch/execution/tasklet/support/InputSourceItemProviderTests.java @@ -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.execution.tasklet.support; + +import java.util.Properties; + +import junit.framework.TestCase; + +import org.springframework.batch.execution.tasklet.support.InputSourceItemProvider; +import org.springframework.batch.io.InputSource; +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.batch.support.PropertiesConverter; + +/** + * Unit test for {@link InputSourceItemProvider} + * + * @author Robert Kasanicky + */ +public class InputSourceItemProviderTests extends TestCase { + + // object under test + private InputSourceItemProvider itemProvider = new InputSourceItemProvider(); + + private InputSource source; + + // create input template and inject it to data provider + protected void setUp() throws Exception { + source = new MockInputSource(this); + itemProvider.setInputSource(source); + } + + /** + * Uses input template to provide the domain object. + */ + public void testNext() { + Object result = itemProvider.next(); + assertSame("domain object is provided by the input template", this, result); + } + + /** + * Gets statistics from the input template + */ + public void testGetStatistics() { + Properties props = itemProvider.getStatistics(); + assertEquals("b", props.getProperty("a")); + } + + /** + * Gets restart data from the input template + */ + public void testGetRestartData() { + Properties props = itemProvider.getRestartData().getProperties(); + assertEquals("foo", props.getProperty("value")); + } + + /** + * Forwared restart data to input template + */ + public void testRestoreFrom() { + itemProvider.restoreFrom(new GenericRestartData(PropertiesConverter.stringToProperties("value=bar"))); + assertEquals("bar", itemProvider.next()); + } + + private class MockInputSource implements InputSource, StatisticsProvider, Restartable { + + private Object value; + + public Properties getStatistics() { + return PropertiesConverter.stringToProperties("a=b"); + } + + public RestartData getRestartData() { + return new GenericRestartData(PropertiesConverter.stringToProperties("value=foo")); + } + + public void restoreFrom(RestartData data) { + value = data.getProperties().getProperty("value"); + } + + public MockInputSource(Object value) { + this.value = value; + } + + public Object read() { + return value; + } + + public void close() { + } + + public void open() { + } + + } + +} diff --git a/execution/src/test/java/org/springframework/batch/execution/tasklet/support/OutputSourceItemProcessorTests.java b/execution/src/test/java/org/springframework/batch/execution/tasklet/support/OutputSourceItemProcessorTests.java new file mode 100644 index 000000000..42355deb5 --- /dev/null +++ b/execution/src/test/java/org/springframework/batch/execution/tasklet/support/OutputSourceItemProcessorTests.java @@ -0,0 +1,160 @@ +/* + * 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.execution.tasklet.support; + +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +import junit.framework.TestCase; + +import org.springframework.batch.execution.tasklet.support.OutputSourceItemProcessor; +import org.springframework.batch.io.OutputSource; +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.batch.support.PropertiesConverter; + +/** + * @author Dave Syer + * + */ +public class OutputSourceItemProcessorTests extends TestCase { + + private OutputSourceItemProcessor processor = new OutputSourceItemProcessor(); + + private OutputSource source; + + /* + * (non-Javadoc) + * @see junit.framework.TestCase#setUp() + */ + protected void setUp() throws Exception { + source = new MockOutputSource("test"); + processor.setOutputSource(source); + } + + public void testProcess() throws Exception { + processor.process("foo"); + assertEquals(1, list.size()); + assertEquals("test:foo", list.get(0)); + } + + /** + * Gets statistics from the input template + */ + public void testGetStatistics() { + Properties props = processor.getStatistics(); + assertEquals("b", props.getProperty("a")); + } + + /** + * Gets restart data from the input template + */ + public void testGetRestartData() { + Properties props = processor.getRestartData().getProperties(); + assertEquals("foo", props.getProperty("value")); + } + + /** + * Forward restart data to input template + * @throws Exception + */ + public void testRestoreFrom() throws Exception { + processor.restoreFrom(new GenericRestartData(PropertiesConverter.stringToProperties("value=bar"))); + processor.process("foo"); + assertEquals("bar:foo", list.get(0)); + } + + /** + * Forward restart data to input template + * @throws Exception + */ + public void testGetRestartDataWithoutRestartable() throws Exception { + processor.setOutputSource(null); + try { + processor.getRestartData(); + fail("Expected IllegalStateException"); + } + catch (IllegalStateException e) { + // expected + } + } + + /** + * Forward restart data to input template + * @throws Exception + */ + public void testRestoreFromWithoutRestartable() throws Exception { + processor.setOutputSource(null); + try { + processor.restoreFrom(new GenericRestartData(PropertiesConverter.stringToProperties("value=bar"))); + fail("Expected IllegalStateException"); + } + catch (IllegalStateException e) { + // expected + } + } + + /** + * Gets statistics from the input template + */ + public void testGetStatisticsWithoutStatisticsProvider() { + processor.setOutputSource(null); + Properties props = processor.getStatistics(); + assertEquals(null, props.getProperty("a")); + } + + private List list = new ArrayList(); + + /** + * @author Dave Syer + * + */ + public class MockOutputSource implements OutputSource, StatisticsProvider, Restartable { + + private String value; + + public MockOutputSource(String string) { + this.value = string; + } + + public void write(Object output) { + list.add(value+":"+output); + } + + public void close() { + } + + public void open() { + } + + public Properties getStatistics() { + return PropertiesConverter.stringToProperties("a=b"); + } + + public RestartData getRestartData() { + return new GenericRestartData(PropertiesConverter.stringToProperties("value=foo")); + } + + public void restoreFrom(RestartData data) { + value = data.getProperties().getProperty("value"); + } + + } + +} diff --git a/execution/src/test/java/test/jdbc/datasource/InitializingDataSourceFactoryBean.java b/execution/src/test/java/test/jdbc/datasource/InitializingDataSourceFactoryBean.java new file mode 100644 index 000000000..ff6b0bc9a --- /dev/null +++ b/execution/src/test/java/test/jdbc/datasource/InitializingDataSourceFactoryBean.java @@ -0,0 +1,156 @@ +/* + * 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 test.jdbc.datasource; + +import java.io.IOException; +import java.util.Iterator; +import java.util.List; + +import javax.sql.DataSource; + +import org.apache.commons.io.IOUtils; +import org.springframework.beans.factory.BeanInitializationException; +import org.springframework.beans.factory.config.AbstractFactoryBean; +import org.springframework.core.io.Resource; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.TransactionCallback; +import org.springframework.transaction.support.TransactionTemplate; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +public class InitializingDataSourceFactoryBean extends AbstractFactoryBean { + + private Resource[] initScripts; + + private Resource destroyScript; + + DataSource dataSource; + + private static boolean initialized = false; + + /** + * @throws Throwable + * @see java.lang.Object#finalize() + */ + protected void finalize() throws Throwable { + super.finalize(); + initialized = false; + logger.debug("finalize called"); + } + + protected void destroyInstance(Object instance) throws Exception { + try { + doExecuteScript(destroyScript); + } + catch (Exception e) { + if (logger.isDebugEnabled()) { + logger.warn("Could not execute destroy script [" + destroyScript + "]", e); + } + else { + logger.warn("Could not execute destroy script [" + destroyScript + "]"); + } + } + } + + public void afterPropertiesSet() throws Exception { + Assert.notNull(dataSource); + super.afterPropertiesSet(); + } + + protected Object createInstance() throws Exception { + Assert.notNull(dataSource); + if (!initialized) { + try { + doExecuteScript(destroyScript); + } + catch (Exception e) { + logger.debug("Could not execute destroy script [" + destroyScript + "]", e); + } + if (initScripts != null) { + for (int i = 0; i < initScripts.length; i++) { + Resource initScript = initScripts[i]; + doExecuteScript(initScript); + } + } + initialized = true; + } + return dataSource; + } + + private void doExecuteScript(final Resource scriptResource) { + if (scriptResource == null || !scriptResource.exists()) + return; + TransactionTemplate transactionTemplate = new TransactionTemplate(new DataSourceTransactionManager(dataSource)); + transactionTemplate.execute(new TransactionCallback() { + + public Object doInTransaction(TransactionStatus status) { + JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); + String[] scripts; + try { + scripts = StringUtils.delimitedListToStringArray(stripComments(IOUtils.readLines(scriptResource + .getInputStream())), ";"); + } + catch (IOException e) { + throw new BeanInitializationException("Cannot load script from [" + scriptResource + "]", e); + } + for (int i = 0; i < scripts.length; i++) { + String script = scripts[i].trim(); + if (StringUtils.hasText(script)) { + jdbcTemplate.execute(scripts[i]); + } + } + return null; + } + + }); + + } + + private String stripComments(List list) { + StringBuffer buffer = new StringBuffer(); + for (Iterator iter = list.iterator(); iter.hasNext();) { + String line = (String) iter.next(); + if (!line.startsWith("//") && !line.startsWith("--")) { + buffer.append(line + "\n"); + } + } + return buffer.toString(); + } + + public Class getObjectType() { + return DataSource.class; + } + + public void setInitScript(Resource initScript) { + this.initScripts = new Resource[] { initScript }; + } + + public void setInitScripts(Resource[] initScripts) { + this.initScripts = initScripts; + } + + public void setDestroyScript(Resource destroyScript) { + this.destroyScript = destroyScript; + } + + public void setDataSource(DataSource dataSource) { + this.dataSource = dataSource; + } + +} diff --git a/execution/src/test/resources/beanRefContext.xml b/execution/src/test/resources/beanRefContext.xml new file mode 100644 index 000000000..1e8b9b7a5 --- /dev/null +++ b/execution/src/test/resources/beanRefContext.xml @@ -0,0 +1,11 @@ + + + + + + simple-container-definition.xml + + + + \ No newline at end of file diff --git a/execution/src/test/resources/clover.license b/execution/src/test/resources/clover.license new file mode 100644 index 000000000..36f3a294e --- /dev/null +++ b/execution/src/test/resources/clover.license @@ -0,0 +1,172 @@ +Product: Clover +License: Open Source License, 0.x, 1.x +Issued: Thu Mar 15 2007 12:55:00 CDT +Expiry: Never +Maintenance Expiry: Never +Key: d24b469cbe33bb71017d39a9d +Name: Andy Colyer +Org: Spring Portfolio +Certificate: AAACUG+Ow8B7/zEbxOMqqKwwrdpP+a1COmJGHco7sCNLjHkHnajPF+dQW +Ct12PMy0uml0s9xuus5wKngJ9OFk5/FZgYzdyIG5/rxEgRevOoLO7uYipoJrkt4TPBwIm4 +hxEw+b9xUNP0x1tTqSsgUP6fqSYilMajaHYGuRD9iV3LeP7hwWulpXY3hz3W5WjsKYp3Nf +fPyts/AffWHANGj5DHV+4yGm2IGIzIgHOGx9hISC7boFknmwM/GQ78RO1yzNnkSJ9dHPz2 +VdGTrKob36k3OVy7vwwCPpSm+01KDpkY4ZQN4ynqPIFzvJ07F1IBUvU49CGzSvX3v6qmOp +mT11CTGtP49xPafrKNjDDV8PxCsoesEBRaY4FJzquzWz0j6CkIQqidzCj3WDCtog3ct+za +SuuZ51n027sVFhbM69dZZzv8bYCgSHdQ3sG1a9DxM7+6JRfRcIBFgt/V78vK41MF4p9Mi1 +qmEPMLizpu7eBo1GDoQ8Lb3EhIWrfxDb4Db3NFc9hYpCKoreFlEw1A+eJlrLeomy43pVtk +SNPTDDoahNrXLeIu7SiRoHiemMrUjWvYtT9jwnVOsIjELa8n8cfW7gMzJdenCcNWl/T3Cr +8rSu3pVfz07AvX6+wQZWqzvGGnlwpnFXu1YJROxITYNINVNKXCAby33Mdbm51DPA3rEyk+ +dpS31tU2XrR4iY1Zypja1M0voOkzL74pf9ExgUGeJqyvi5LWTn3b4kGGT/bkwhbbDn6sAA +zlxKRvxsbYOBUzk3UZ448Heg2HwCjwarCh/C0QIcX8vWnUjqssdvxT7Jlr1rZwqK1LKqbH +l7YvP9Ee7SoHfoHrW770yK23u2IdDK44Sf6G3NBE0Muq7W1bcZwrZ1/ZRk8vE2kt0F0fXI +wz7Thjs5lXvcZDJO4nEtXpmSdCaDjUXBpjvsZE2ZjPa2Q1tv3KFhHWqdfNRant7FyeWYg= += +License Agreement: CLOVER VERSION 1 (ONE) SOFTWARE LICENSE AGREEMENT + +1. Licenses and Software + +Cenqua Pty Ltd, an Australian Proprietary Limited Company ("CENQUA") +hereby grants to the purchaser (the "LICENSEE") a limited, revocable, +worldwide, non-exclusive, nontransferable, non-sublicensable license +to use the Clover version 1 (one) software (the "Software"), +including any minor upgrades thereof during the Term (hereinafter +defined) up to, but not including the next major version of the +Software. The licensee shall not, or knowingly allow others to, +reverse engineer, decompile, disassemble, modify, adapt, create +derivative works from or otherwise attempt to derive source code from +the Software provided. And, in accordance with the terms and +conditions of this Software License Agreement (the "Agreement"), the +Software shall be used solely by the licensed users in accordance +with the following edition specific conditions: + +a) Server Edition + +A Server Edition license entitles the Licensee to execute one +instance of Clover Server Edition on one (1) machine for the purposes +of instrumententing source code and generating reports. There are no +limitations on the use of the instrumented source code or generated +reports produced by Server Edition. + +b) Workstation Edition + +A Workstation Edition license entitles the licensee to use Clover +Workstation Edition on one (1) machine by one (1) individual end +user. Workstation Edition does not permit the generation of reports +for distribution. + +c) Team Edition + +A Team Edition license entitles the licensee to use Clover Team +edition on any number of machines solely by the licensed number of +users. Reports generated by Clover Team Edition are strictly for use +only by the licensed number of individual end users. + +2. License Fee + +In exchange for the License(s), the Licensee shall pay to CENQUA a +one-time, up front, non-refundable license fee. At the sole +discretion of CENQUA this fee will be waived for non-commercial +projects. Notwithstanding the Licensee's payment of the License Fee, +CENQUA reserves the right to terminate the License if CENQUA +discovers that the Licensee and/or the Licensee's use of the Software +is in breach of this Agreement. + +3. Proprietary Rights + +CENQUA will retain all right, title and interest in and to the +Software, all copies thereof, and CENQUA website(s), software, and +other intellectual property, including, but not limited to, ownership +of all copyrights, look and feel, trademark rights, design rights, +trade secret rights and any and all other intellectual property and +other proprietary rights therein. The Licensee will not directly or +indirectly obtain or attempt to obtain at any time, any right, title +or interest by registration or otherwise in or to the trademarks, + +service marks, copyrights, trade names, symbols, logos or +designations or other intellectual property rights owned or used by +CENQUA. All technical manuals or other information provided by CENQUA +to the Licensee shall be the sole property of CENQUA. + +4. Term and Termination + +Subject to the other provisions hereof, this Agreement shall commence +upon the Licensee's opting into this Agreement and continue until the +Licensee discontinues use of the Software or the Agreement terminates +automatically upon the Licensee's breach of any term or condition of +this Agreement (the "Term"). Upon any such termination, the Licensee +will delete the Software immediately. + +5. Copying & Transfer + +The Licensee may copy the Software for back-up purposes only. The + +Licensee may not assign or otherwise transfer the Software to any +third party. + +6. Specific Disclaimer of Warranty and Limitation of Liability + +THE SOFTWARE IS PROVIDED WITHOUT WARRANTY OF ANY KIND. CENQUA +DISCLAIMS ALL WARRANTIES, EXPRESSED OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE. CENQUA WILL NOT BE LIABLE FOR ANY DAMAGES +ASSOCIATED WITH THE SOFTWARE, INCLUDING, WITHOUT LIMITATION, +ORDINARY, INCIDENTAL, INDIRECT, OR CONSEQUENTIAL DAMAGES OF ANY KIND, +INCLUDING BUT NOT LIMITED TO DAMAGES RELATING TO LOST DATA OR LOST +PROFITS, EVEN IF CENQUA HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + +7. Warranties and Representations + +Licensee Indemnification. CENQUA agrees to indemnify, defend and hold +the Licensee harmless from and against any and all liabilities, +damages, losses, claims, costs, and expenses (including reasonable +legal fees) arising out of or resulting from the Software or the use +thereof infringing upon, misappropriating or violating any patents, +copyrights, trademarks, or trade secret rights or other proprietary +rights of persons, firms or entities who are not parties to this +Agreement. + +CENQUA Indemnification. The Licensee warrants and represents that the +Licensee's actions with regard to the Software will be in compliance +with all applicable laws; and the Licensee agrees to indemnify, +defend, and hold CENQUA harmless from and against any and all +liabilities, damages, losses, claims, costs, and expenses (including +reasonable legal fees) arising out of or resulting from the +Licensee's failure to observe the use restrictions set forth herein. + +8. Publicity + +The Licensee grants permission for CENQUA to use Licensee's name +solely in customer lists. CENQUA shall not, without prior consent in +writing, use the Licensee's name, or that of its affiliates, in any +form with the specific exception of customer lists. CENQUA agrees to +remove Licensee's name from any and all materials within 7 days if +notified by the Licensee in writing. + +9. Governing Law + +This Agreement shall be governed by the laws of New South Wales, +Australia. + +10.Independent Contractors + +The parties are independent contractors with respect to each other, +and nothing in this Agreement shall be construed as creating an +employer-employee relationship, a partnership, agency relationship or +a joint venture between the parties. + +11. Assignment + +This Agreement is not assignable or transferable by the Licensee. +CENQUA in its sole discretion may transfer a license to a third party +at the written request of the Licensee. + +12. Entire Agreement + +This Agreement constitutes the entire agreement between the parties +concerning the Licensee's use of the Software. This Agreement +supersedes any prior verbal understanding between the parties and any +Licensee purchase order or other ordering document, regardless of +whether such document is received by CENQUA before or after execution +of this Agreement. This Agreement may be amended only in writing by +CENQUA. diff --git a/execution/src/test/resources/job-configuration.xml b/execution/src/test/resources/job-configuration.xml new file mode 100644 index 000000000..836948bea --- /dev/null +++ b/execution/src/test/resources/job-configuration.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/execution/src/test/resources/log4j.properties b/execution/src/test/resources/log4j.properties new file mode 100644 index 000000000..6d5422d74 --- /dev/null +++ b/execution/src/test/resources/log4j.properties @@ -0,0 +1,13 @@ +log4j.rootCategory=INFO, stdout + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - <%m>%n + +log4j.category.org.apache.activemq=ERROR +log4j.category.org.springframework.batch=DEBUG +log4j.category.org.springframework.transaction=INFO + +log4j.category.org.hibernate.SQL=DEBUG +# for debugging datasource initialization +# log4j.category.test.jdbc=DEBUG diff --git a/execution/src/test/resources/org/springframework/batch/execution/repository/dao/data-source-context.xml b/execution/src/test/resources/org/springframework/batch/execution/repository/dao/data-source-context.xml new file mode 100644 index 000000000..bf7573ea1 --- /dev/null +++ b/execution/src/test/resources/org/springframework/batch/execution/repository/dao/data-source-context.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/execution/src/test/resources/org/springframework/batch/execution/repository/dao/destroy.sql b/execution/src/test/resources/org/springframework/batch/execution/repository/dao/destroy.sql new file mode 100644 index 000000000..377fdc36b --- /dev/null +++ b/execution/src/test/resources/org/springframework/batch/execution/repository/dao/destroy.sql @@ -0,0 +1,10 @@ +-- Autogenerated: do not edit this file +DROP TABLE BATCH_STEP_EXECUTION IF EXISTS; +DROP TABLE BATCH_JOB_EXECUTION IF EXISTS; +DROP TABLE BATCH_STEP IF EXISTS; +DROP TABLE BATCH_JOB IF EXISTS; + +DROP TABLE BATCH_STEP_EXECUTION_SEQ IF EXISTS; +DROP TABLE BATCH_STEP_SEQ IF EXISTS; +DROP TABLE BATCH_JOB_EXECUTION_SEQ IF EXISTS; +DROP TABLE BATCH_JOB_SEQ IF EXISTS; \ No newline at end of file diff --git a/execution/src/test/resources/org/springframework/batch/execution/repository/dao/hibernate-context.xml b/execution/src/test/resources/org/springframework/batch/execution/repository/dao/hibernate-context.xml new file mode 100644 index 000000000..cabd910c8 --- /dev/null +++ b/execution/src/test/resources/org/springframework/batch/execution/repository/dao/hibernate-context.xml @@ -0,0 +1,34 @@ + + + + + + + + + classpath:/org/springframework/batch/execution/repository/dao/JobInstance.hbm.xml + classpath:/org/springframework/batch/execution/repository/dao/JobExecution.hbm.xml + classpath:/org/springframework/batch/execution/repository/dao/StepInstance.hbm.xml + classpath:/org/springframework/batch/execution/repository/dao/StepExecution.hbm.xml + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/execution/src/test/resources/org/springframework/batch/execution/repository/dao/hibernate-dao-test.xml b/execution/src/test/resources/org/springframework/batch/execution/repository/dao/hibernate-dao-test.xml new file mode 100644 index 000000000..af9418331 --- /dev/null +++ b/execution/src/test/resources/org/springframework/batch/execution/repository/dao/hibernate-dao-test.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/execution/src/test/resources/org/springframework/batch/execution/repository/dao/init.sql b/execution/src/test/resources/org/springframework/batch/execution/repository/dao/init.sql new file mode 100644 index 000000000..08b590ede --- /dev/null +++ b/execution/src/test/resources/org/springframework/batch/execution/repository/dao/init.sql @@ -0,0 +1,53 @@ +-- Autogenerated: do not edit this file +CREATE TABLE BATCH_JOB ( + ID BIGINT IDENTITY PRIMARY KEY , + VERSION BIGINT, + JOB_NAME VARCHAR(100) NOT NULL , + JOB_STREAM VARCHAR(20) , + SCHEDULE_DATE DATE , + JOB_RUN CHAR(2), + STATUS VARCHAR(10) ); + +CREATE TABLE BATCH_JOB_EXECUTION ( + ID BIGINT IDENTITY PRIMARY KEY , + VERSION BIGINT, + JOB_ID BIGINT NOT NULL, + START_TIME TIMESTAMP NOT NULL , + END_TIME TIMESTAMP , + STATUS VARCHAR(10), + EXIT_CODE BIGINT); + +CREATE TABLE BATCH_STEP ( + ID BIGINT IDENTITY PRIMARY KEY , + VERSION BIGINT, + JOB_ID BIGINT NOT NULL, + STEP_NAME VARCHAR(100) NOT NULL, + STATUS VARCHAR(10), + RESTART_DATA VARCHAR(200)); + +CREATE TABLE BATCH_STEP_EXECUTION ( + ID BIGINT IDENTITY PRIMARY KEY , + VERSION BIGINT NOT NULL, + STEP_ID BIGINT NOT NULL, + JOB_EXECUTION_ID BIGINT NOT NULL, + START_TIME TIMESTAMP NOT NULL , + END_TIME TIMESTAMP , + STATUS VARCHAR(10), + COMMIT_COUNT BIGINT , + TASK_COUNT BIGINT , + TASK_STATISTICS VARCHAR(250), + EXIT_CODE BIGINT, + EXIT_MESSAGE VARCHAR(250)); + +CREATE TABLE BATCH_STEP_EXECUTION_SEQ ( + ID BIGINT IDENTITY +); +CREATE TABLE BATCH_STEP_SEQ ( + ID BIGINT IDENTITY +); +CREATE TABLE BATCH_JOB_EXECUTION_SEQ ( + ID BIGINT IDENTITY +); +CREATE TABLE BATCH_JOB_SEQ ( + ID BIGINT IDENTITY +); diff --git a/execution/src/test/resources/org/springframework/batch/execution/repository/dao/sql-dao-test.xml b/execution/src/test/resources/org/springframework/batch/execution/repository/dao/sql-dao-test.xml new file mode 100644 index 000000000..22a06ea94 --- /dev/null +++ b/execution/src/test/resources/org/springframework/batch/execution/repository/dao/sql-dao-test.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/execution/src/test/resources/org/springframework/batch/execution/scope/scope-tests.xml b/execution/src/test/resources/org/springframework/batch/execution/scope/scope-tests.xml new file mode 100644 index 000000000..276b26c11 --- /dev/null +++ b/execution/src/test/resources/org/springframework/batch/execution/scope/scope-tests.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/execution/src/test/resources/simple-container-definition.xml b/execution/src/test/resources/simple-container-definition.xml new file mode 100644 index 000000000..d21a05246 --- /dev/null +++ b/execution/src/test/resources/simple-container-definition.xml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/infrastructure/.classpath b/infrastructure/.classpath index 46d76d281..5bec2f24d 100644 --- a/infrastructure/.classpath +++ b/infrastructure/.classpath @@ -1,7 +1,10 @@ - + + + + - + diff --git a/infrastructure/.project b/infrastructure/.project index 877ebc1ae..160b623fe 100644 --- a/infrastructure/.project +++ b/infrastructure/.project @@ -16,8 +16,14 @@ + + org.springframework.ide.eclipse.core.springbuilder + + + + org.springframework.ide.eclipse.core.springnature org.eclipse.jdt.core.javanature org.maven.ide.eclipse.maven2Nature diff --git a/infrastructure/.springBeans b/infrastructure/.springBeans new file mode 100644 index 000000000..bc8aa601a --- /dev/null +++ b/infrastructure/.springBeans @@ -0,0 +1,12 @@ + + + + xml + + + src/test/resources/org/springframework/batch/io/file/support/mapping/bean-wrapper.xml + src/test/resources/org/springframework/batch/io/sql/data-source-context.xml + + + + diff --git a/infrastructure/changelog.txt b/infrastructure/changelog.txt new file mode 100644 index 000000000..023c8f00c --- /dev/null +++ b/infrastructure/changelog.txt @@ -0,0 +1 @@ +Do not edit this file: use src/site/apt/changelog.apt instead. diff --git a/infrastructure/pom.xml b/infrastructure/pom.xml index ad79128d7..bf57c4581 100644 --- a/infrastructure/pom.xml +++ b/infrastructure/pom.xml @@ -1,41 +1,22 @@ - - + + + 4.0.0 + spring-batch-infrastructure + jar + Infrastructure + + + + org.springframework.batch spring-batch - 1.0-SNAPSHOT + 1.0-m2-SNAPSHOT + .. - 4.0.0 - spring-batch-infrastructure - jar - Spring Batch Infrastructure - - The Spring Batch Infrastructure is a set of low-level components, interfaces and tools for batch processing - applications and optimizations. - - - - - org.apache.maven.plugins - maven-clover-plugin - - ${basedir}/src/test/resources/clover.license - - - - pre-site - - instrument - - - - - - + junit @@ -43,21 +24,29 @@ 3.8.1 test + + cglib + cglib-nodep + 2.1_3 + test + org.springframework spring - 2.0.3 + 2.1-m2 backport-util-concurrent backport-util-concurrent 3.0 + true - + org.apache.geronimo.specs geronimo-jms_1.1_spec 1.0 + provided mockobjects @@ -88,10 +77,27 @@ + + commons-lang + commons-lang + 2.1 + + + commons-io + commons-io + 1.2 + test + + + hsqldb + hsqldb + 1.8.0.7 + test + org.springframework spring-mock - 2.0.3 + 2.1-m2 org.springframework @@ -145,70 +151,52 @@ com.thoughtworks.xstream xstream 1.2.1 + true stax stax 1.2.0 + true - commons-validator - commons-validator - 1.1.4 - - - org.apache.derby - derby - 10.1.1.0 - test - - - commons-io - commons-io - 1.2 - test - - - com.experlog - xapool - 1.5.0 - - - commons-collections - commons-collections - 3.1 - - - commons-dbcp - commons-dbcp - 1.2.1 - - - geronimo-spec - geronimo-spec-j2ee - 1.4-rc4 - - - jotm - jotm - 2.0.10 - - - javax.transaction - jta - - - javax.resource - connector - - - - - org.springmodules - spring-modules-validation - 0.7 - + org.springframework.ws + spring-oxm + 1.0-rc2 + true + + + org.springframework + spring-beans + + + org.springframework + spring-core + + + + + + + + org.apache.maven.plugins + maven-clover-plugin + + ${basedir}/src/test/resources/clover.license + + + + pre-site + + instrument + + + + + + + @@ -217,4 +205,5 @@ + diff --git a/infrastructure/spring-batch-infrastructure.iml b/infrastructure/spring-batch-infrastructure.iml new file mode 100644 index 000000000..2e369ae5a --- /dev/null +++ b/infrastructure/spring-batch-infrastructure.iml @@ -0,0 +1,393 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/infrastructure/src/main/java/org/springframework/batch/common/BinaryExceptionClassifier.java b/infrastructure/src/main/java/org/springframework/batch/common/BinaryExceptionClassifier.java new file mode 100644 index 000000000..2e4f9abff --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/common/BinaryExceptionClassifier.java @@ -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); + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/common/ExceptionClassifier.java b/infrastructure/src/main/java/org/springframework/batch/common/ExceptionClassifier.java new file mode 100644 index 000000000..d26705e11 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/common/ExceptionClassifier.java @@ -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); + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/common/ExceptionClassifierSupport.java b/infrastructure/src/main/java/org/springframework/batch/common/ExceptionClassifierSupport.java new file mode 100644 index 000000000..123c24602 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/common/ExceptionClassifierSupport.java @@ -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); + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/common/SubclassExceptionClassifier.java b/infrastructure/src/main/java/org/springframework/batch/common/SubclassExceptionClassifier.java new file mode 100644 index 000000000..b68ca341a --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/common/SubclassExceptionClassifier.java @@ -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; + } + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/common/package.html b/infrastructure/src/main/java/org/springframework/batch/common/package.html new file mode 100644 index 000000000..789feb3d7 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/common/package.html @@ -0,0 +1,7 @@ + + +

+Infrastructure implementations of common concerns. +

+ + diff --git a/infrastructure/src/main/java/org/springframework/batch/io/InputSource.java b/infrastructure/src/main/java/org/springframework/batch/io/InputSource.java new file mode 100644 index 000000000..557a32675 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/InputSource.java @@ -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; + +import org.springframework.batch.item.ResourceLifecycle; + +/** + * 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 extends ResourceLifecycle { + + /** + * Read record from input stream and map it to an object. + * + * @return the value object + */ + public Object read(); +} diff --git a/infrastructure/src/main/java/org/springframework/batch/io/OutputSource.java b/infrastructure/src/main/java/org/springframework/batch/io/OutputSource.java new file mode 100644 index 000000000..bfcf8d8f9 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/OutputSource.java @@ -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; + +import org.springframework.batch.item.ResourceLifecycle; + +/** + * Basic interface for generic output operations. Class implementing this + * interface will be responsible for serializing objects to output source. + * 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 OutputSource extends ResourceLifecycle { + + /** + * Writes provided value object to an output stream or similar. + * + * @param output the value object + */ + public void write(Object output); +} diff --git a/infrastructure/src/main/java/org/springframework/batch/io/Skippable.java b/infrastructure/src/main/java/org/springframework/batch/io/Skippable.java new file mode 100644 index 000000000..fd8070fc6 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/Skippable.java @@ -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(); + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/io/exception/BatchConfigurationException.java b/infrastructure/src/main/java/org/springframework/batch/io/exception/BatchConfigurationException.java new file mode 100644 index 000000000..7b90f0a92 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/exception/BatchConfigurationException.java @@ -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); + } +} diff --git a/infrastructure/src/main/java/org/springframework/batch/io/exception/BatchCriticalException.java b/infrastructure/src/main/java/org/springframework/batch/io/exception/BatchCriticalException.java new file mode 100644 index 000000000..dfe78aa4f --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/exception/BatchCriticalException.java @@ -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(); + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/io/exception/BatchEnvironmentException.java b/infrastructure/src/main/java/org/springframework/batch/io/exception/BatchEnvironmentException.java new file mode 100644 index 000000000..4c58aa328 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/exception/BatchEnvironmentException.java @@ -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); + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/io/exception/TransactionInvalidException.java b/infrastructure/src/main/java/org/springframework/batch/io/exception/TransactionInvalidException.java new file mode 100644 index 000000000..7abf8af28 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/exception/TransactionInvalidException.java @@ -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); + } +} diff --git a/infrastructure/src/main/java/org/springframework/batch/io/exception/TransactionValidException.java b/infrastructure/src/main/java/org/springframework/batch/io/exception/TransactionValidException.java new file mode 100644 index 000000000..12df250c7 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/exception/TransactionValidException.java @@ -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); + } +} diff --git a/infrastructure/src/main/java/org/springframework/batch/io/exception/ValidationException.java b/infrastructure/src/main/java/org/springframework/batch/io/exception/ValidationException.java new file mode 100644 index 000000000..dbc252d8a --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/exception/ValidationException.java @@ -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); + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/io/exception/package.html b/infrastructure/src/main/java/org/springframework/batch/io/exception/package.html new file mode 100644 index 000000000..d4fdd596b --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/exception/package.html @@ -0,0 +1,7 @@ + + +

+Infrastructure implementations of io exception concerns. +

+ + diff --git a/infrastructure/src/main/java/org/springframework/batch/io/file/FieldSet.java b/infrastructure/src/main/java/org/springframework/batch/io/file/FieldSet.java new file mode 100644 index 000000000..2cc7f769f --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/file/FieldSet.java @@ -0,0 +1,549 @@ +/* + * 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.batch.io.exception.BatchConfigurationException; +import org.springframework.batch.io.exception.BatchCriticalException; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * @author Rob Harrop + */ +public final class FieldSet { + + private final static String DEFAULT_DATE_PATTERN = "yyyy-MM-dd"; + + /** + * The fields wrapped by this 'FieldSet' instance. + */ + private String[] tokens; + + private List names; + + public FieldSet(String[] tokens) { + this.tokens = tokens; + } + + public FieldSet(String[] tokens, String[] 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 = tokens; + this.names = Arrays.asList(names); + } + + /** + * Read the {@link String} value at index 'index'. + * + * @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 'name'. + * + * @param name the field name. + * @throws BatchConfigurationException if the recordDescriptor is not + * provided or column with given name is not defined. + */ + public String readString(String name) { + return readString(indexOf(name)); + } + + /** + * Read the 'boolean' value at index 'index'. + * + * @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 'boolean' value from column with given 'name'. + * + * @param name the field name. + * @throws BatchConfigurationException if the recordDescriptor is not + * provided or column with given name is not defined. + */ + public boolean readBoolean(String name) { + return readBoolean(indexOf(name)); + } + + /** + * Read the 'boolean' value at index 'index'. + * + * @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 trueValue is null. + */ + 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 'boolean' value from column with given 'name'. + * + * @param name the field name. + * @param trueValue the value that signifies {@link Boolean#TRUE true}; + * case-sensitive. + * @throws BatchConfigurationException if the recordDescriptor is not + * provided or column with given name is not defined, or if the supplied + * trueValue is null. + */ + public boolean readBoolean(String name, String trueValue) { + return readBoolean(indexOf(name), trueValue); + } + + /** + * Read the 'char' value at index 'index'. + * + * @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 'char' value from column with given 'name'. + * + * @param name the field name. + * @throws BatchConfigurationException if the recordDescriptor is not + * provided or column with given name is not defined. + */ + public char readChar(String name) { + return readChar(indexOf(name)); + } + + /** + * Read the 'byte' value at index 'index'. + * + * @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 'byte' value from column with given 'name'. + * + * @param name the field name. + * @throws BatchConfigurationException if the recordDescriptor is not + * provided or column with given name is not defined. + */ + public byte readByte(String name) { + return readByte(indexOf(name)); + } + + /** + * Read the 'short' value at index 'index'. + * + * @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 'short' value from column with given 'name'. + * + * @param name the field name. + * @throws BatchConfigurationException if the recordDescriptor is not + * provided or column with given name is not defined. + */ + public short readShort(String name) { + return readShort(indexOf(name)); + } + + /** + * Read the 'int' value at index 'index'. + * + * @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 'int' value from column with given 'name'. + * + * @param name the field name. + * @throws BatchConfigurationException if the recordDescriptor is not + * provided or column with given name is not defined. + */ + public int readInt(String name) { + return readInt(indexOf(name)); + } + + /** + * Read the 'int' value at index 'index', + * using the supplied defaultValue 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 'int' value from column with given 'name', + * using the supplied defaultValue if the field value is + * blank. + * + * @param name the field name. + * @throws BatchConfigurationException if the recordDescriptor is not + * provided or column with given name is not defined. + */ + public int readInt(String name, int defaultValue) { + return readInt(indexOf(name), defaultValue); + } + + /** + * Read the 'long' value at index 'index'. + * + * @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 'long' value from column with given 'name'. + * + * @param name the field name. + * @throws BatchConfigurationException if the recordDescriptor is not + * provided or column with given name is not defined. + */ + public long readLong(String name) { + return readLong(indexOf(name)); + } + + /** + * Read the 'long' value at index 'index', + * using the supplied defaultValue 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 'long' value from column with given 'name', + * using the supplied defaultValue if the field value is + * blank. + * + * @param name the field name. + * @throws BatchConfigurationException if the recordDescriptor is not + * provided or column with given name is not defined. + */ + public long readLong(String name, long defaultValue) { + return readLong(indexOf(name), defaultValue); + } + + /** + * Read the 'float' value at index 'index'. + * + * @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 'float' value from column with given 'name. + * + * @param name the field name. + * @throws BatchConfigurationException if the recordDescriptor is not + * provided or column with given name is not defined. + */ + public float readFloat(String name) { + return readFloat(indexOf(name)); + } + + /** + * Read the 'double' value at index 'index'. + * + * @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 'double' value from column with given 'name. + * + * @param name the field name. + * @throws BatchConfigurationException if the recordDescriptor is not + * provided or 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 'index'. + * + * @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 'name. + * + * @param name the field name. + * @throws BatchConfigurationException if the recordDescriptor is not + * provided or column with given name is not defined. + */ + public BigDecimal readBigDecimal(String name) { + return readBigDecimal(name, null); + } + + /** + * Read the {@link BigDecimal} value at index 'index', + * returning the supplied defaultValue if the trimmed string + * value at index 'index' 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 'name, + * returning the supplied defaultValue if the trimmed string + * value at index 'index' is blank. + * + * @param name the field name. + * @throws BatchConfigurationException if the recordDescriptor is not + * provided or 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 java.util.Date value in default format at + * designated column index. + * + * @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 BatchCriticalException if the specified field cannot be parsed + * @see #DEFAULT_DATE_PATTERN + */ + public Date readDate(int index) { + return readDate(index, DEFAULT_DATE_PATTERN); + } + + /** + * Read the java.sql.Date value in given format from column + * with given name. + * + * @param name the field name. + * @param pattern the pattern describing the date and time format + * @throws BatchCriticalException if the specified field cannot be parsed + * @see #DEFAULT_DATE_PATTERN + */ + public Date readDate(String name) { + return readDate(name, DEFAULT_DATE_PATTERN); + } + + /** + * Read the java.util.Date value in default format at + * designated column index. + * + * @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 BatchCriticalException if the specified field 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 java.sql.Date value in given format from column + * with given name. + * + * @param name the field name. + * @param pattern the pattern describing the date and time format + * @throws BatchCriticalException 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 'FieldSet'. + */ + public int getFieldCount() { + return tokens.length; + } + + /** + * Read and trim the {@link String} value at 'index'. + * + * @throws NullPointerException if the field value is null. + */ + private String readAndTrim(int index) { + String value = tokens[index]; + + if (value != null) { + return value.trim(); + } + else { + return value; + } + } + + /** + * Read and trim the {@link String} value from column with given 'name. + * + * @throws BatchConfigurationException if the recordDescriptor is not + * provided or 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(); + } + //TODO return "" instead of null? + return tokens==null ? 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() { + return (tokens == null) ? 0 : tokens.hashCode(); + } + + /** + * Construct name-value pairs from the field names and string values. + * + * @return some properties representing the fle 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++) { + props.setProperty((String) names.get(i), tokens[i]); + } + return props; + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/io/file/FieldSetInputSource.java b/infrastructure/src/main/java/org/springframework/batch/io/file/FieldSetInputSource.java new file mode 100644 index 000000000..fea876ef7 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/file/FieldSetInputSource.java @@ -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; + +import org.springframework.batch.io.InputSource; + +/** + * Common interface for reading input e.g. from a file or other stream-based + * resource. Providers are expected to use this interface to access an input + * source.
+ * + * If we had generics this would be a parameterised input source, but + * for type safety with the current constraints we are going to use this + * interface. + * + */ +public interface FieldSetInputSource extends InputSource { + + public FieldSet readFieldSet(); + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/io/file/FieldSetMapper.java b/infrastructure/src/main/java/org/springframework/batch/io/file/FieldSetMapper.java new file mode 100644 index 000000000..1d09805bf --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/file/FieldSetMapper.java @@ -0,0 +1,31 @@ +/* + * 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 + * + */ +public interface FieldSetMapper { + /** + * Method used to map data obtained from a file into an object. + */ + public Object mapLine(FieldSet fs); +} diff --git a/infrastructure/src/main/java/org/springframework/batch/io/file/package.html b/infrastructure/src/main/java/org/springframework/batch/io/file/package.html new file mode 100644 index 000000000..acc13f0c6 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/file/package.html @@ -0,0 +1,7 @@ + + +

+Infrastructure implementations of io file concerns. +

+ + diff --git a/infrastructure/src/main/java/org/springframework/batch/io/file/support/DefaultFlatFileInputSource.java b/infrastructure/src/main/java/org/springframework/batch/io/file/support/DefaultFlatFileInputSource.java new file mode 100644 index 000000000..2c4e3802a --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/file/support/DefaultFlatFileInputSource.java @@ -0,0 +1,188 @@ +/* + * 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.io.file.FieldSetInputSource; +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; + +/** + *

+ * This class is a {@link FieldSetInputSource} that supports restart, + * skipping invalid lines and storing statistics. + *

+ * + * @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) { + + //TODO this does not look very nice... + 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(); + } + } + } +} diff --git a/infrastructure/src/main/java/org/springframework/batch/io/file/support/FlatFileOutputSource.java b/infrastructure/src/main/java/org/springframework/batch/io/file/support/FlatFileOutputSource.java new file mode 100644 index 000000000..4fdebc378 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/file/support/FlatFileOutputSource.java @@ -0,0 +1,578 @@ +/* + * 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.OutputSource; +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.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; + +/** + * This class is an output target that writes data to a file or stream. The + * output source 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.
+ * + * Uses buffered writer to improve performance.
+ * + * Use {@link #write(String)} method to output a line to an output source. + * + * @author Waseem Malik + * @author Tomas Slanina + * @author Robert Kasanicky + * @author Dave Syer + */ +public class FlatFileOutputSource implements OutputSource, Restartable, StatisticsProvider, InitializingBean, + DisposableBean { + + /** + * @author dsyer + * + */ + public 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 TransactionSynchronization transactionSynchronization = new FlatFileOutputTemplateTransactionSynchronization(); + + 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. + */ + private void transactionComitted() { + getOutputState().mark(); + } + + /** + * Rollback the transaction. + */ + private 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.
+ * + * @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() { + 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()); + + } + + // Registers a new transaction synchronization for the current thread. + private void registerSynchronization() { + BatchTransactionSynchronizationManager.registerSynchronization(this.transactionSynchronization); + } + + // Returns object representing state. + private OutputState getOutputState() { + return (OutputState) state; + } + + // added package visibility method so that tests can invoke transaction + // events + TransactionSynchronization getTransactionSynchronization() { + return this.transactionSynchronization; + } + + /** + * Encapsulates the runtime state of the output source. All state changing + * operations on the output source 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 Output Source", 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 FileWriterOutputSource", 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); + } + } + 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"); + } + } + + } + + /** + * Encapsulates transaction events. + */ + private class FlatFileOutputTemplateTransactionSynchronization 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(); + } + } + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/io/file/support/ResourceLineReader.java b/infrastructure/src/main/java/org/springframework/batch/io/file/support/ResourceLineReader.java new file mode 100644 index 000000000..781f9bdfb --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/file/support/ResourceLineReader.java @@ -0,0 +1,340 @@ +/* + * 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.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.
+ * + * 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.
+ * + * 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.
+ * + * All the public methods that interact wit hthe underlying resource (open, + * close, read etc.) are synchronized on this.
+ * + * @author Dave Syer + * @author Rob Harrop + */ +public class ResourceLineReader implements 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(); + 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)); + } + 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); + } + + } + + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/io/file/support/SimpleFlatFileInputSource.java b/infrastructure/src/main/java/org/springframework/batch/io/file/support/SimpleFlatFileInputSource.java new file mode 100644 index 000000000..b2d209186 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/file/support/SimpleFlatFileInputSource.java @@ -0,0 +1,199 @@ +/* + * 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.ValidationException; +import org.springframework.batch.io.file.FieldSet; +import org.springframework.batch.io.file.FieldSetInputSource; +import org.springframework.batch.io.file.support.separator.RecordSeparatorPolicy; +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.
+ * + * 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 appropiate factory or scope so + * that it is not shared between threads.
+ * + * @see FieldSetInputSource + * + * @author Dave Syer + */ +public class SimpleFlatFileInputSource implements FieldSetInputSource, 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 LineTokenizer tokenizer = new DelimitedLineTokenizer(); + + private String encoding = DEFAULT_CHARSET; + + /** + * 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; + } + + public void afterPropertiesSet() throws Exception { + Assert.notNull(resource); + Assert.state(resource.exists(), "Resource must exist: [" + resource + "]"); + } + + /** + * Initialize the reader if necessary. + */ + public void open() { + if (reader == null) { + reader = new ResourceLineReader(resource, encoding); + if (recordSeparatorPolicy!=null) { + reader.setRecordSeparatorPolicy(recordSeparatorPolicy); + } + reader.open(); + } + } + + /** + * 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 final Object read() { + return readFieldSet(); + } + + /** + * Get the next {@link FieldSet} from the input. + * + * @see org.springframework.batch.io.file.FieldSetInputSource#readFieldSet() + */ + public FieldSet readFieldSet() { + String line = readLine(); + + if (line != null) { + try { + return this.tokenizer.tokenize(line); + } + catch (RuntimeException ve) { + // add current line count to message and re-throw + // TODO: wrap the exception more carefully to preserve type etc. + ValidationException newVe = new ValidationException("Validation error at line " + + getReader().getCurrentLineCount() + ": " + ve.getMessage()); + throw newVe; + } + } + 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; + } + + // 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; + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/io/file/support/mapping/BeanWrapperFieldSetMapper.java b/infrastructure/src/main/java/org/springframework/batch/io/file/support/mapping/BeanWrapperFieldSetMapper.java new file mode 100644 index 000000000..88cbaf658 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/file/support/mapping/BeanWrapperFieldSetMapper.java @@ -0,0 +1,216 @@ +/* + * 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; + +/** + * {@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 by referring to to object by bean name in + * the enclosing BeanFactory.
+ * + * 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.
+ * + * Property name matching is "fuzzy" in the sense that it tolerates close + * matches, as long as the match is unique. For instance: + * + *
    + *
  • Quantity = quantity (field names can be capitalised)
  • + *
  • ISIN = isin (acronyms can be lower case bean property names, as per Java + * Beans recommendations)
  • + *
  • DuckPate = duckPate (capitalisation including camel casing)
  • + *
  • ITEM_ID = itemId (capitalisation and replacing word boundary with + * underscore)
  • + *
  • ORDER.CUSTOMER_ID = order.customerId (nested paths are recursively + * checked)
  • + *
+ * + * 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 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. + * + * @param name + */ + public void setPrototypeBeanName(String name) { + this.name = name; + } + + public void afterPropertiesSet() throws Exception { + Assert.notNull(name); + } + + /** + * 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 = beanFactory.getBean(name); + BeanWrapper wrapper = new BeanWrapperImpl(copy); + wrapper.setPropertyValues(getBeanProperties(copy, fs.getProperties())); + return copy; + } + + /** + * @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); + } +} diff --git a/infrastructure/src/main/java/org/springframework/batch/io/file/support/mapping/PropertyMatches.java b/infrastructure/src/main/java/org/springframework/batch/io/file/support/mapping/PropertyMatches.java new file mode 100644 index 000000000..b54d4c0c3 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/file/support/mapping/PropertyMatches.java @@ -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.
+ * + * 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 getStringDistance 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()]; + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/io/file/support/mapping/package.html b/infrastructure/src/main/java/org/springframework/batch/io/file/support/mapping/package.html new file mode 100644 index 000000000..f6a1929c8 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/file/support/mapping/package.html @@ -0,0 +1,7 @@ + + +

+Infrastructure implementations of io file support mapping concerns. +

+ + diff --git a/infrastructure/src/main/java/org/springframework/batch/io/file/support/package.html b/infrastructure/src/main/java/org/springframework/batch/io/file/support/package.html new file mode 100644 index 000000000..cd9eb79a1 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/file/support/package.html @@ -0,0 +1,7 @@ + + +

+Infrastructure implementations of io file support concerns. +

+ + diff --git a/infrastructure/src/main/java/org/springframework/batch/io/file/support/separator/DefaultRecordSeparatorPolicy.java b/infrastructure/src/main/java/org/springframework/batch/io/file/support/separator/DefaultRecordSeparatorPolicy.java new file mode 100644 index 000000000..76b8f8f4f --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/file/support/separator/DefaultRecordSeparatorPolicy.java @@ -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); + } +} diff --git a/infrastructure/src/main/java/org/springframework/batch/io/file/support/separator/RecordSeparatorPolicy.java b/infrastructure/src/main/java/org/springframework/batch/io/file/support/separator/RecordSeparatorPolicy.java new file mode 100644 index 000000000..f4c9dce11 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/file/support/separator/RecordSeparatorPolicy.java @@ -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 spearator 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 tru 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 befor eit 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); + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/io/file/support/separator/SimpleRecordSeparatorPolicy.java b/infrastructure/src/main/java/org/springframework/batch/io/file/support/separator/SimpleRecordSeparatorPolicy.java new file mode 100644 index 000000000..7214fed80 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/file/support/separator/SimpleRecordSeparatorPolicy.java @@ -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; + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/io/file/support/separator/SuffixRecordSeparatorPolicy.java b/infrastructure/src/main/java/org/springframework/batch/io/file/support/separator/SuffixRecordSeparatorPolicy.java new file mode 100644 index 000000000..3925d5840 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/file/support/separator/SuffixRecordSeparatorPolicy.java @@ -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)); + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/io/file/support/separator/package.html b/infrastructure/src/main/java/org/springframework/batch/io/file/support/separator/package.html new file mode 100644 index 000000000..11f311caf --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/file/support/separator/package.html @@ -0,0 +1,7 @@ + + +

+Infrastructure implementations of io file support separator concerns. +

+ + diff --git a/infrastructure/src/main/java/org/springframework/batch/io/file/support/transform/AbstractLineTokenizer.java b/infrastructure/src/main/java/org/springframework/batch/io/file/support/transform/AbstractLineTokenizer.java new file mode 100644 index 000000000..636ef2977 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/file/support/transform/AbstractLineTokenizer.java @@ -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.ArrayList; +import java.util.List; + +import org.springframework.batch.io.file.FieldSet; + + +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; + } + + /** + * Yields the tokens resulting from the splitting of the supplied + * line. + * + * @param line the line to be tokenised (can be null) + * + * @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(); iline. + * + * @param line the line to be tokenised (can be null) + * + * @return the resulting tokens + */ + public List doTokenize(String line) { + + List tokens = new ArrayList(); + + char[] chars = line.toCharArray(); + boolean inQuoted = false; + char lastChar = 0; + int lastCut = 0; + int length = chars.length; + + // TODO if line was null there would be exception while getting chars + // value + if (line != null) { + + 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 true 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 true if the supplied character is an quote + * character + * @see #setQuoteCharacter(char) + */ + protected boolean isQuoteCharacter(char c) { + return c == quoteCharacter; + } +} diff --git a/infrastructure/src/main/java/org/springframework/batch/io/file/support/transform/FixedLengthLineAggregator.java b/infrastructure/src/main/java/org/springframework/batch/io/file/support/transform/FixedLengthLineAggregator.java new file mode 100644 index 000000000..a912d3e6a --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/file/support/transform/FixedLengthLineAggregator.java @@ -0,0 +1,141 @@ +/* + * 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.util.Assert; + +/** + * Class used to create string representing object. Each value has define length + * defined by record descriptor. + * + * @author tomas.slanina + * + */ +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 int[] lengths = new int[0]; + private int align = ALIGN_LEFT; + private String padding = " "; + + /** + * Setter for field lengths. + * + * @param lengths + */ + public void setLengths(int[] lengths) { + this.lengths = lengths; + } + + /** + * Method used to create string representing object. + * + * @param args arrays of strings representing data to be stored + * @param lineDescriptor defines the structure of the final string + */ + public String aggregate(String[] args) { + StringBuffer stringBuffer = new StringBuffer(); + + Assert.notNull(args); + Assert.isTrue(args.length<=lengths.length, + "Number of arguments must match number of fields in a record"); + + for (int i = 0; i < args.length; i++) { + stringBuffer.append(formatText(args[i], lengths[i])); + } + + return stringBuffer.toString(); + } + + private String formatText(String textToFormat, int length) { + String text; + + if (textToFormat == null) { + text = ""; + } + else { + text = textToFormat; + } + + int currentLength = text.length(); + + Assert.isTrue(currentLength <= length, "Supplied text: " + text + " is longer than defined length: " + length); + + if (currentLength == length) { + return text; + } + else { + StringBuffer stringBuffer = new StringBuffer(); + + switch (align) { + case ALIGN_RIGHT: + pad(stringBuffer, length - text.length()); + stringBuffer.append(text); + break; + case ALIGN_CENTER: + int toAdd = length - text.length(); + pad(stringBuffer, toAdd / 2); + stringBuffer.append(text); + pad(stringBuffer, toAdd - toAdd / 2); + break; + case ALIGN_LEFT: + stringBuffer.append(text); + pad(stringBuffer, length - text.length()); + break; + } + + return stringBuffer.toString(); + } + } + + private void pad(StringBuffer stringBuffer, int howMany) { + for (int i = 0; i < howMany; i++) { + stringBuffer.append(padding); + } + } + + /** + * Recognized alignments are CENTER, RIGHT, LEFT. + * LEFT is used as default in case the argument does not + * match any of the recognized values. + */ + public void setAlignment(String alignment) { + if ("CENTER".equalsIgnoreCase(alignment)) { + this.align = ALIGN_CENTER; + } + else if ("RIGHT".equalsIgnoreCase(alignment)) { + this.align = ALIGN_RIGHT; + } + else { + // LEFT is default alignment, therefore use it + // if no other alignment was defined. + this.align = ALIGN_LEFT; + } + } + + /** + * Setter for padding (default space). + * @param padding + */ + public void setPadding(String padding) { + this.padding = padding; + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/io/file/support/transform/FixedLengthTokenizer.java b/infrastructure/src/main/java/org/springframework/batch/io/file/support/transform/FixedLengthTokenizer.java new file mode 100644 index 000000000..319fd8eeb --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/file/support/transform/FixedLengthTokenizer.java @@ -0,0 +1,76 @@ +/* + * 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. + * + * @author tomas.slanina + */ +public class FixedLengthTokenizer extends AbstractLineTokenizer { + + private int[] lengths = new int[0]; + + /** + * Setter for field lengths. + * + * @param lengths + */ + public void setLengths(int[] lengths) { + this.lengths = lengths; + } + + /** + * Yields the tokens resulting from the splitting of the supplied + * line. + * + * @param line the line to be tokenised (can be null) + * + * @return the resulting tokens + */ + protected List doTokenize(String line) { + List tokens = new ArrayList(); + int lineLength; + int startPos = 0; + int endPos = 0; + String token; + + lineLength = (line == null) ? (-1) : line.length(); + + for (int i = 0; i < lengths.length; i++) { + endPos += lengths[i]; + + if (lineLength >= endPos) { + token = line.substring(startPos, endPos); + } + else if (lineLength >= startPos) { + token = line.substring(startPos); + } + else { + token = ""; + } + + tokens.add(token); + startPos = endPos; + } + + return tokens; + } +} diff --git a/infrastructure/src/main/java/org/springframework/batch/io/file/support/transform/LineAggregator.java b/infrastructure/src/main/java/org/springframework/batch/io/file/support/transform/LineAggregator.java new file mode 100644 index 000000000..6bc2bc68a --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/file/support/transform/LineAggregator.java @@ -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); +} diff --git a/infrastructure/src/main/java/org/springframework/batch/io/file/support/transform/LineTokenizer.java b/infrastructure/src/main/java/org/springframework/batch/io/file/support/transform/LineTokenizer.java new file mode 100644 index 000000000..b760eb7f7 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/file/support/transform/LineTokenizer.java @@ -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 + * line. + * + * @param line the line to be tokenised (can be null) + * + * @return the resulting tokens + */ + FieldSet tokenize(String line); +} diff --git a/infrastructure/src/main/java/org/springframework/batch/io/file/support/transform/PrefixMatchingCompositeLineTokenizer.java b/infrastructure/src/main/java/org/springframework/batch/io/file/support/transform/PrefixMatchingCompositeLineTokenizer.java new file mode 100644 index 000000000..6cd6f452a --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/file/support/transform/PrefixMatchingCompositeLineTokenizer.java @@ -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); + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/io/file/support/transform/package.html b/infrastructure/src/main/java/org/springframework/batch/io/file/support/transform/package.html new file mode 100644 index 000000000..79853a581 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/file/support/transform/package.html @@ -0,0 +1,7 @@ + + +

+Infrastructure implementations of io file support transform concerns. +

+ + diff --git a/infrastructure/src/main/java/org/springframework/batch/io/package.html b/infrastructure/src/main/java/org/springframework/batch/io/package.html new file mode 100644 index 000000000..83e05d744 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/package.html @@ -0,0 +1,7 @@ + + +

+Infrastructure implementations of io concerns. +

+ + diff --git a/infrastructure/src/main/java/org/springframework/batch/io/sql/SingleKeySqlDrivingQueryInputSource.java b/infrastructure/src/main/java/org/springframework/batch/io/sql/SingleKeySqlDrivingQueryInputSource.java new file mode 100644 index 000000000..9b9b1ac7e --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/sql/SingleKeySqlDrivingQueryInputSource.java @@ -0,0 +1,218 @@ +/* + * 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.sql; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Iterator; +import java.util.List; +import java.util.Properties; + +import javax.sql.DataSource; + +import org.springframework.batch.io.InputSource; +import org.springframework.batch.restart.GenericRestartData; +import org.springframework.batch.restart.RestartData; +import org.springframework.batch.restart.Restartable; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.util.Assert; + +/** + *

+ * DrivingQuery based input source for input data that can be uniquely + * identified by a single primary key. The current implementation is + * forward-only, and requires a transactional buffer to ensure rollbacks are + * handled correctly. + *

+ * + *

+ * Users of this input source must provide a 'Driving Query' that returns only + * one column. (If more than one is returned, the first column will be used) A + * 'Details Query' must then be provided that requires only one parameter. + * (question mark) Invalid queries will throw SqlExceptions from JdbcTemplate. + *

+ * + * + * @author Lucas Ward + * + */ +public class SingleKeySqlDrivingQueryInputSource implements InputSource, Restartable { + + private static final String RESTART_KEY = "SingleKeySqlDrivingQueryInputSource.lastProcessedKey"; + + private DataSource dataSource; + + private JdbcTemplate jdbcTemplate; + + private String drivingQuery; + + private String detailsQuery; + + private String restartQuery; + + private Object[] detailArgs = new Object[1]; + + private List keys; + + private Iterator keysIterator; + + private RowMapper mapper; + + /** + * Read one record by passing in the current key to the details query. + * + */ + public Object read() { + if (keys == null) { + retrieveKeys(); + } + + if (keysIterator.hasNext()) { + detailArgs[0] = keysIterator.next(); + + return jdbcTemplate.queryForObject(detailsQuery, detailArgs, mapper); + } + + return null; + } + + /* + * Retrieve the keys by calling the DrivingQuery. + */ + private void retrieveKeys() { + + jdbcTemplate = new JdbcTemplate(dataSource); + + keys = jdbcTemplate.query(drivingQuery, new SingleColumnRowMapper()); + + keysIterator = keys.iterator(); + } + + /* + * (non-Javadoc) + * @see org.springframework.batch.item.ResourceLifecycle#close() + */ + public void close() { + keys = null; + keysIterator = null; + } + + /* + * (non-Javadoc) + * @see org.springframework.batch.item.ResourceLifecycle#open() + */ + public void open() { + + } + + public void setDataSource(DataSource dataSource) { + this.dataSource = dataSource; + } + + /** + * Set the query to be used to obtain the list of keys at + * initialization. Each key returned will be fed into the + * details query. + * + * @param drivingQuery + */ + public void setDrivingQuery(String drivingQuery) { + this.drivingQuery = drivingQuery; + } + + /** + * Set the query to be used for each 'detail' record. Meaning, + * the query each key (row returned from the driving query) will + * be fed into in order to return a row to be mapped. + * + * @param detailsQuery + */ + public void setDetailsQuery(String detailsQuery) { + this.detailsQuery = detailsQuery; + } + + /** + * Set the query to be used in the case of a restart. The current + * key at the time restart data is requested will be fed into this + * query as a parameter upon restart, allowing for only the remaining + * keys to be returned. + * + * @param restartQuery + */ + public void setRestartQuery(String restartQuery) { + this.restartQuery = restartQuery; + } + + /** + * Set RowMapper to be used for each call to the provided details + * query. + * + * @param mapper + */ + public void setMapper(RowMapper mapper) { + this.mapper = mapper; + } + + // Required because JdbcTemplate.queryForList returns a list + // of maps based on metadata. + private class SingleColumnRowMapper implements RowMapper { + + public Object mapRow(ResultSet rs, int rowNum) throws SQLException { + return rs.getObject(1); + } + + } + + public RestartData getRestartData() { + Properties props = new Properties(); + props.setProperty(RESTART_KEY, detailArgs[0].toString()); + return new GenericRestartData(props); + } + + /** + * 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 same input source as the one + * being restored from otherwise it is invalid. + * + * @param RestartData obtained by calling getRestartData during a previous + * run. + * @throws IllegalStateException if input source has already been read from. + */ + public void restoreFrom(RestartData data) { + + Assert.notNull(data, "RestartData must not be null."); + + if (keys != null) { + throw new IllegalStateException("Cannot restore when already intialized. Call" + + " close() first before restore()"); + } + + Properties restartData = data.getProperties(); + String lastProcessedKey = restartData.getProperty(RESTART_KEY); + if (lastProcessedKey != null) { + jdbcTemplate = new JdbcTemplate(dataSource); + + keys = jdbcTemplate.query(restartQuery, new Object[] { lastProcessedKey }, new SingleColumnRowMapper()); + + keysIterator = keys.iterator(); + } + } +} diff --git a/infrastructure/src/main/java/org/springframework/batch/io/sql/SqlCursorInputSource.java b/infrastructure/src/main/java/org/springframework/batch/io/sql/SqlCursorInputSource.java new file mode 100644 index 000000000..63a48ce23 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/sql/SqlCursorInputSource.java @@ -0,0 +1,532 @@ +/* + * 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.sql; + +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.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.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.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationAdapter; +import org.springframework.util.Assert; + +/** + *

+ * 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. + *

+ * + *

+ * 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. + *

+ * + *

+ * 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. + *

+ * + *

+ * Statistics: There are two statistics returned by this input source: the + * current line being processed and the number of lines that have been skipped. + *

+ * + *

+ * 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. + *

+ * + *

+ * 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. + * + *

+ * 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. + *

+ * + * @author Lucas Ward + * @author Peter Zozom + */ +public class SqlCursorInputSource implements InputSource, DisposableBean, Restartable, StatisticsProvider, Skippable { + + private static Log log = LogFactory.getLog(SqlCursorInputSource.class); + + public static final int VALUE_NOT_SET = -1; + + private static final String CURRENT_PROCESSED_ROW = "sqlCursorInput.lastProcessedRowNum"; + + 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 final SqlInputTransactionSynchronization transactionSynchronization = new SqlInputTransactionSynchronization(); + + private RowMapper mapper; + + public SqlCursorInputSource(DataSource dataSource) { + + this(dataSource, null); + } + + public SqlCursorInputSource(DataSource dataSource, String sql) { + + Assert.notNull(dataSource, "DataSource must not be null."); + + this.dataSource = dataSource; + this.sql = sql; + } + + /** + * 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 (this.rs == null) { + this.executeQuery(); + } + + 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++; + } + } + verifyCursorPosition(currentProcessedRow); + + return mapper.mapRow(rs, currentProcessedRow); + } + } + 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. + */ + private void mark() { + lastCommittedRow = currentProcessedRow; + skippedRows.clear(); + } + + /** + * Set the ResultSet's current row to the last marked position. + * + * @throws DataAccessException + */ + private void reset() { + 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() { + 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", stmt.toString(), se); + } + + BatchTransactionSynchronizationManager.registerSynchronization(transactionSynchronization); + } + + /* + * 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.

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 + * null, 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 { + throw new SQLWarningException("Warning not ignored", warnings); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.batch.restart.Restartable#getRestartData() + */ + public RestartData getRestartData() { + return new GenericRestartData(getStatistics()); + } + + /* + * (non-Javadoc) + * @see org.springframework.batch.restart.Restartable#restoreFrom(org.springframework.batch.restart.RestartData) + */ + public void restoreFrom(RestartData data) { + if (data==null) return; + + if (rs == null) { + executeQuery(); + } + + try { + this.currentProcessedRow = Integer.parseInt(data.getProperties().getProperty(CURRENT_PROCESSED_ROW)); + rs.absolute(currentProcessedRow); + } + catch (SQLException se) { + throw getExceptionTranslator().translate("Attempted to move ResultSet to last committed row", sql, se); + } + } + + /* + * (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 + * ResultSet 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 + * ResultSet 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 + * Statement object to execute to the given number of + * seconds. If the limit is exceeded, an SQLException 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 String getSql() { + return sql; + } + + public void open() { + // TODO Auto-generated method stub + + } + + private class SqlInputTransactionSynchronization extends TransactionSynchronizationAdapter { + + /* + * @param status transaction status + * @see org.springframework.transaction.support.TransactionSynchronization#afterCompletion(int) + */ + public void afterCompletion(int status) { + + if (status == TransactionSynchronization.STATUS_ROLLED_BACK) { + reset(); + } + else if (status == TransactionSynchronization.STATUS_COMMITTED) { + mark(); + } + } + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/io/sql/package.html b/infrastructure/src/main/java/org/springframework/batch/io/sql/package.html new file mode 100644 index 000000000..9d6c8ff39 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/sql/package.html @@ -0,0 +1,7 @@ + + +

+Infrastructure implementations of io sql concerns. +

+ + diff --git a/infrastructure/src/main/java/org/springframework/batch/io/xml/ObjectInput.java b/infrastructure/src/main/java/org/springframework/batch/io/xml/ObjectInput.java new file mode 100644 index 000000000..b89c42446 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/xml/ObjectInput.java @@ -0,0 +1,61 @@ +/* + * 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.xml; + +import java.io.IOException; + +import org.springframework.batch.io.xml.xstream.XStreamFactory; + +/** + * The ObjectInput interface provides method for reading objects + * from input stream and also provides methods for manipulating input stream. + * @author peter.zozom + * @see XStreamFactory.ObjectInputWrapper + */ +public interface ObjectInput { + + /** + * Enables input stream postprocess after reading from the stream has been + * restarted. + * @param data + */ + public void afterRestart(Object data); + + /** + * Read and return an object. The class that implements this interface + * defines where the object is "read" from. + * + * @return the object read from the stream + * @exception java.lang.ClassNotFoundException If the class of a serialized + * object cannot be found. + * @exception IOException If any of the usual Input/Output related + * exceptions occur. + */ + public Object readObject() throws ClassNotFoundException, IOException; + + /** + * Closes the input stream. Must be called to release any resources + * associated with the stream. + */ + public void close(); + + /** + * Get actual position in the input stream. + * @return actual position in the input stream + */ + public long position(); +} diff --git a/infrastructure/src/main/java/org/springframework/batch/io/xml/ObjectInputFactory.java b/infrastructure/src/main/java/org/springframework/batch/io/xml/ObjectInputFactory.java new file mode 100644 index 000000000..2c1aceb66 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/xml/ObjectInputFactory.java @@ -0,0 +1,33 @@ +/* + * 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.xml; + +import org.springframework.core.io.Resource; + +/** + * ObjectInputFactory creates instance of ObjectInput. + * Implementation should be thread-safe. + * @author peter.zozom + */ +public interface ObjectInputFactory { + + /** + * Creates instance of {@link ObjectInput} + * @return ObjectInput + */ + public ObjectInput createObjectInput(Resource resource, String encoding); +} diff --git a/infrastructure/src/main/java/org/springframework/batch/io/xml/ObjectOutput.java b/infrastructure/src/main/java/org/springframework/batch/io/xml/ObjectOutput.java new file mode 100644 index 000000000..3f6ecd67b --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/xml/ObjectOutput.java @@ -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.xml; + +import java.io.IOException; + +import org.springframework.batch.io.xml.xstream.XStreamFactory; + +/** + * The ObjectOutput interface provides methods for writing + * objects to output stream and also provides methods for manipulating output + * stream (such as flush, position, truncate, etc.). + * @author peter.zozom + * @see XStreamFactory.ObjectOutputWrapper + */ +public interface ObjectOutput { + + /** + * Enables output stream postprocess after writing to the stream has been + * restarted. + * @param data + */ + public void afterRestart(Object data); + + /** + * Write an object to the stream. The class that implements this interface + * defines how the object is written. + * + * @param obj the object to be written + * @exception IOException Any of the usual Input/Output related exceptions. + */ + public void writeObject(Object obj) throws IOException; + + /** + * Closes the stream. This method must be called to release any resources + * associated with the stream. + */ + public void close(); + + /** + * Flushes the stream. This will write any buffered output bytes. + */ + public void flush(); + + /** + * Get actual position in the output stream. + * @return actual position in the output stream + */ + public long position(); + + /** + * Set a new position in the output stream. + * @param newPosition the new position, a non-negative integer counting the + * number of bytes from the beginning of the file + */ + public void position(long newPosition); + + /** + * Truncates the otput file to the given size. + * @param size the new size, a non-negative byte count + */ + public void truncate(long size); + + /** + * Returns the current size of the output file + * @return The current size of the output file, measured in bytes + */ + public long size(); + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/io/xml/ObjectOutputFactory.java b/infrastructure/src/main/java/org/springframework/batch/io/xml/ObjectOutputFactory.java new file mode 100644 index 000000000..a8624c9d7 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/xml/ObjectOutputFactory.java @@ -0,0 +1,33 @@ +/* + * 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.xml; + +import org.springframework.core.io.Resource; + +/** + * ObjectOutputFactory creates instance of ObjectOutput. + * Implementation should be thread-safe. + * @author peter.zozom + */ +public interface ObjectOutputFactory { + + /** + * Creates instance of {@link ObjectOutput} + * @return + */ + public ObjectOutput createObjectOutput(Resource resource, String encoding); +} diff --git a/infrastructure/src/main/java/org/springframework/batch/io/xml/UnmarshallerAdapter.java b/infrastructure/src/main/java/org/springframework/batch/io/xml/UnmarshallerAdapter.java new file mode 100644 index 000000000..6f5b9b46b --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/xml/UnmarshallerAdapter.java @@ -0,0 +1,198 @@ +package org.springframework.batch.io.xml; + +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.charset.Charset; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.transform.Source; +import javax.xml.transform.sax.SAXSource; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.oxm.Unmarshaller; +import org.springframework.oxm.XmlMappingException; +import org.springframework.xml.transform.StaxSource; + +import org.xml.sax.SAXException; +import org.xml.sax.XMLReader; +import org.xml.sax.helpers.XMLReaderFactory; + +/** + * Unmarshaller adapter allows to use {@link org.springframework.oxm.Unmarshaller} for + * iterative XML record processing. + * + * {@link org.springframework.oxm.Unmarshaller} always processes whole XML + * input at once, but we need to process only one record per one module + * iteration (or per one call of the read() method). + * + * Solution is to cut XML in smaller pieces (each piece contains single record) and call + * the unmarshaller to process only this single piece (record). + * + */ +class UnmarshallerAdapter { + + private static final Log log = LogFactory.getLog(UnmarshallerAdapter.class); + + private FileChannel fc; + + private String encoding; + + //original unmarshaller - processes only pieces of XML passed from wrapper + private Unmarshaller u; + + //regex matcher - used to cut XML piece holding one record + private final Matcher matcher; + + private SourceFactory factory; + + /* ***** Constructor ***** */ + + /** + * @param originalUnmarshaller unmarshaller to be wrapped + */ + public UnmarshallerAdapter(Unmarshaller originalUnmarshaller, String recordElementName, FileChannel fc, + String encoding, boolean useSaxParser) { + this.u = originalUnmarshaller; + this.fc = fc; + this.encoding = encoding; + this.matcher = getMatcher(recordElementName); + this.factory = getSourceFactory(useSaxParser); + } + + /** + * Unmarshal next record. + * @see org.springframework.oxm.Unmarshaller#unmarshal(javax.xml.transform.Source) + */ + public Object unmarshal() throws XmlMappingException, IOException { + + Object result = null; + + String record = readNextRecord(); + if (record != null) { + result = u.unmarshal(factory.getSource(record)); + } + return result; + } + + + /** + * Read next piece of XML. + * @return xml string holding one record + */ + private String readNextRecord() { + + String result = null; + + if (matcher.find()) { + result = matcher.group(); + } + if (result != null) { + try { + fc.position(matcher.end()); + } catch (IOException e) { + throw new IllegalStateException("Error while adjusting filechannel position"); + } + } + + return result; + } + + + /* ***** Private helper methods ***** */ + + /** + * Get regex matcher, which cuts XML to pieces + * @param elemName name of the element that represents one record + * @return the matcher + */ + private Matcher getMatcher(String elemName) { + + CharSequence cs = null; + + try { + // Create a read-only CharBuffer on the file + ByteBuffer bbuf = fc.map(FileChannel.MapMode.READ_ONLY, fc.position(), fc.size() - fc.position()); + cs = Charset.forName(encoding).newDecoder().decode(bbuf); + } + catch (IOException ioe) { + log.error(ioe); + throw new DataAccessResourceFailureException("Unable to get input stream", ioe); + } + + String prefix = (elemName.indexOf(':') < 0) ? "(?:[^:]*?:)?" : ""; + String re = "<" + prefix + elemName + "[^/>]*?/>|<" + prefix + elemName + "[\\s>][\\S\\s]*?"; + + return Pattern.compile(re, Pattern.MULTILINE).matcher(cs); + } + + /** + * Get SourceFactory which will create SAX or StAX source (based on useSaxParser flag) + * @param useSaxParser + */ + private SourceFactory getSourceFactory(boolean useSaxParser) { + + SourceFactory sf; + + if (useSaxParser) { + sf = new SourceFactory() { + public Source getSource(String xml) { + + Source source = null; + + Reader r = new StringReader(xml); + try { + XMLReader reader; + try { + reader = XMLReaderFactory.createXMLReader(); + } + catch (SAXException se) { + reader = XMLReaderFactory.createXMLReader("org.apache.crimson.parser.XMLReaderImpl"); + } + source = new SAXSource(reader, new org.xml.sax.InputSource(r)); + } + catch (SAXException se) { + log.error(se); + throw new DataAccessResourceFailureException("Unable to get XML reader", se); + } + + return source; + } + }; + } + else { + sf = new SourceFactory() { + public Source getSource(String xml) { + XMLInputFactory xsf = XMLInputFactory.newInstance(); + + Source source = null; + + try { + Reader r = new StringReader(xml); + source = new StaxSource(xsf.createXMLStreamReader(r)); + } + catch (XMLStreamException xse) { + log.error(xse); + throw new DataAccessResourceFailureException("Unable to get XML reader", xse); + } + return source; + } + }; + } + + return sf; + } + + private interface SourceFactory { + public Source getSource(String xml); + } +} diff --git a/infrastructure/src/main/java/org/springframework/batch/io/xml/XmlErrorHandler.java b/infrastructure/src/main/java/org/springframework/batch/io/xml/XmlErrorHandler.java new file mode 100644 index 000000000..109216c17 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/xml/XmlErrorHandler.java @@ -0,0 +1,79 @@ +/* + * 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.xml; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; +import org.xml.sax.helpers.DefaultHandler; + +/** + * SAX error handler implementation. This implementation only logs warnings and + * errors and re-throws exceptions. + * + * @author peter.zozom + */ +public class XmlErrorHandler extends DefaultHandler { + private static final Log log = LogFactory.getLog(XmlErrorHandler.class); + + /** + * @param e parsing exception + * @throws SAXException re-thrown exception + * @see org.xml.sax.helpers.DefaultHandler#warning(org.xml.sax.SAXParseException) + */ + public void warning(SAXParseException e) throws SAXException { + log.debug("Warning: \n" + printInfo(e)); + throw new SAXException(e); + } + + /** + * @param e parsing exception + * @throws SAXException re-thrown exception + * @see org.xml.sax.helpers.DefaultHandler#error(org.xml.sax.SAXParseException) + */ + public void error(SAXParseException e) throws SAXException { + log.debug("Warning: \n" + printInfo(e)); + throw new SAXException(e); + } + + /** + * @param e parsing exception + * @throws SAXException re-thrown exception + * @see org.xml.sax.helpers.DefaultHandler#fatalError(org.xml.sax.SAXParseException) + */ + public void fatalError(SAXParseException e) throws SAXException { + log.debug("Warning: \n" + printInfo(e)); + throw new SAXException(e); + } + + private String printInfo(SAXParseException e) { + StringBuffer sb = new StringBuffer(); + sb.append(" Public ID: \n"); + sb.append(e.getPublicId()); + sb.append(" System ID: \n"); + sb.append(e.getSystemId()); + sb.append(" Line number: \n"); + sb.append(e.getLineNumber()); + sb.append(" Column number: \n"); + sb.append(e.getColumnNumber()); + sb.append(" Message: \n"); + sb.append(e.getMessage()); + + return sb.toString(); + } +} diff --git a/infrastructure/src/main/java/org/springframework/batch/io/xml/XmlInputSource.java b/infrastructure/src/main/java/org/springframework/batch/io/xml/XmlInputSource.java new file mode 100644 index 000000000..3bf0184f4 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/xml/XmlInputSource.java @@ -0,0 +1,489 @@ +/* + * 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.xml; + +import java.io.EOFException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +import javax.xml.parsers.FactoryConfigurationError; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; + +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.exception.BatchCriticalException; +import org.springframework.batch.io.exception.BatchEnvironmentException; +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.transaction.support.TransactionSynchronization; +import org.springframework.util.Assert; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +/** + * XmlInputTemplate is implementation of {@link InputSource} which + * processes XML input independently of technologies used for parsing XML files + * and mapping XML to value objects. It has references only to interfaces, not + * concrete implementations. It uses {@link ObjectInputFactory} interface for + * getting {@link ObjectInput}, which is interface for retrieving value objects + * from the input stream. So it's easy to plug-in any technology for parsing XML + * and mapping it to value object by implementing these interfaces. See + * implementation of {@link ObjectInputFactory} interface, which uses StAX + * as XML parser and XStream as XML-to-ValueObjects mapper. + *

+ * Current implementation also allows validation of XML file against XSD schema. + * This validation is realized by using SAXParser. Validation of the whole XML + * file is performed in the {@link #open()} method, because SAXParser does not + * allow to process only part of the XML, then stop and continue later with + * processing. This type of processing can be realized by using any pull-parser + * (e.g. StAX), but StAX API does not provide methods for validation. So be + * careful when using this validation, because it means to parse XML twice: once + * to validate and once to read and map. Validation can be turned on/off by + * {@link #setValidating(boolean)} method. By default is validation turned off.
+ * This validation should be refactored in future. Validation should be provided + * either by {@link ObjectInput} or some new interface. + *

+ * This input template also provides restart, skip, statistics and transaction + * features by implementing corresponding interfaces. + * + * @author peter.zozom + * @see ObjectInput + * @see ObjectInputFactory + */ +public class XmlInputSource implements InputSource, Skippable, Restartable, + TransactionSynchronization, StatisticsProvider, InitializingBean, DisposableBean { + + private static final Log log = LogFactory.getLog(XmlInputSource.class); + + private static final String DEFAULT_ENCODING = "UTF-8"; + + /* + * Unique source name used to construct this xml reader input source - + * specified by the configuration file. + */ + private String sourceName = null; + + private Resource resource; + + private ObjectInputFactory inputFactory; + + private boolean validating = false; + + private String encoding = DEFAULT_ENCODING; + + private Properties statistics = new Properties(); + + public static final String READ_STATISTICS_NAME = "Read"; + + public static final String RESTART_DATA_NAME = "xmlinputtemplate.currentRecordCount"; + + private InputState state = new InputState(); + + // accessor method for the threadlocal state object + private InputState getState() { + return (InputState) state; + } + + public void afterPropertiesSet() throws Exception { + Assert.notNull(resource); + Assert.state(resource.exists(), "Input resource does not exist: ["+resource+"]"); + } + + /** + * Setter for input resource. + * + * @param resource + */ + public void setResource(Resource resource) { + this.resource = resource; + } + + /** + * Return current status of the validation flag + * + * @return + */ + public boolean isValidating() { + return validating; + } + + /** + * Turn validation on/off for the input source + * + * @param validating + */ + public void setValidating(boolean validating) { + this.validating = validating; + } + + /** + * Initialize the input source and validates input XML file if validation is + * turned on. This method should be called for each thread using same + * XmlInputTemplate instance. + * + * @see org.springframework.batch.item.ResourceLifecycle#open() + */ + public void open() { + InputState is = getState(); + if (!is.initialized) { + registerSynchronization(); + initializeObjectInput(); + } + } + + /** + * Registers object for transaction synchronization + */ + protected void registerSynchronization() { + BatchTransactionSynchronizationManager.registerSynchronization(this); + } + + /** + * Close the input source + * + * @see org.springframework.batch.item.ResourceLifecycle#close() + */ + public void close() { + InputState is = getState(); + is.objectInput.close(); + is.initialized = false; + } + + /** + * 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(); + } + + /* + * Initializes and obtains the ObjectInput from the ObjectInputFactory and + * validates input XML file if validation is turned on. + */ + private void initializeObjectInput() { + InputState is = getState(); + is.initialized = false; + + if (isValidating()) { + validateInputFile(resource); + } + + is.objectInput = inputFactory.createObjectInput(resource, getEncoding()); + + is.lastCommitPoint = is.objectInput.position(); + is.initialized = true; + } + + /* + * Validates input xml file against XSD schema. + * + * @param file File to be validated + */ + private void validateInputFile(Resource resource) { + try { + SAXParserFactory factory = getSaxFactory(); + + factory.setValidating(true); + factory.setNamespaceAware(true); + factory.setFeature("http://apache.org/xml/features/validation/schema", true); + + SAXParser parser = factory.newSAXParser(); + DefaultHandler handler = getDefaultHandler(); + parser.parse(resource.getInputStream(), handler, resource.getURL().toExternalForm()); + } + catch (ParserConfigurationException pce) { + log.error(pce); + throw new BatchEnvironmentException("Unable to configure parser.", pce); + } + catch (SAXException se) { + log.error(se); + throw new BatchEnvironmentException("Error during parsing the input file.", se); + } + catch (IOException ioe) { + log.error(ioe); + throw new BatchEnvironmentException("Error reading input file.", ioe); + } + } + + /* + * Get default handler for xml validation + * + * @return instance of DefaultHandler + */ + private DefaultHandler getDefaultHandler() { + return new XmlErrorHandler(); + } + + /* + * Creates instance of SAXParserFactory + * + * @return instance of SAXParserFactory @throws FactoryConfigurationError + */ + protected SAXParserFactory getSaxFactory() throws FactoryConfigurationError { + SAXParserFactory factory = SAXParserFactory.newInstance(); + return factory; + } + + /** + * Return the next record from the input file and map it to a value object. + * + * @return next record if found or null to signal the end of + * the input data. + * @see org.springframework.batch.io.InputSource + */ + public Object read() { + InputState is = getState(); + + if (!is.initialized) { + open(); + } + + Object result = null; + + do { + result = readNextRecord(); + is.currentRecordCount++; + } while (is.skipLines.contains(new Integer(is.currentRecordCount))); + + return result; + } + + /* + * Reads next record + * + * @return next record + */ + private Object readNextRecord() { + InputState is = getState(); + Object result; + + try { + result = is.objectInput.readObject(); + } + catch (EOFException eofe) { + log.debug("Parsing of XML finished"); + result = null; + } + catch (IOException ioe) { + log.error(ioe); + throw new BatchCriticalException("Unable to read from ObjectInputStream", ioe); + } + catch (ClassNotFoundException cnfe) { + log.error(cnfe); + throw new BatchEnvironmentException("Bad xml mapping", cnfe); + } + + return result; + } + + /** + * Postprocess after transaction commit/rollback. Called from transaction + * manager. + * + * @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(); + } + } + + /* + * Postprocess after transaction commit + */ + private void transactionComitted() { + InputState is = getState(); + is.skipLines = new ArrayList(); + is.lastCommitPoint = is.objectInput.position(); + } + + /* + * Postprocess after transaction rollback + */ + private void transactionRolledback() { + InputState is = getState(); + is.currentRecordCount = 0; + + // remember list of skipped lines and last commit point + List sl = is.skipLines; + long cp = is.lastCommitPoint; + // XMLStreamReader is forward only, so we need to start from beginnig + close(); + // this will also reset skipLines + open(); + // so after init get new InputState and set skipLines and + // lastCommitPoint + is = getState(); + is.skipLines = sl; + is.lastCommitPoint = cp; + + long currentLocation = is.objectInput.position(); + while (currentLocation < is.lastCommitPoint) { + readNextRecord(); + is.currentRecordCount++; + currentLocation = is.objectInput.position(); + } + } + + /** + * Returns name of the input source. + * + * @return input source name + */ + public String getName() { + return sourceName; + } + + /** + * Sets name of the input source. + * + */ + public void setName(String newName) { + this.sourceName = newName; + } + + /** + * This method returns the restart data for the input source. It returns the + * current record count which can be used to re-initialze the batch job in + * case of restart. + * + * @see org.springframework.batch.container.Restartable#getRestartData() + */ + public RestartData getRestartData() { + Properties restartData = new Properties(); + restartData.setProperty(RESTART_DATA_NAME, String.valueOf(getState().currentRecordCount)); + return new GenericRestartData(restartData); + } + + /** + * This method initializes the input source for restart. It opens the input + * file and position the xml reader according to information provided by the + * restart data. + * + * @param restartData restart data information + * @see org.springframework.batch.container.Restartable#initForRestart(java.lang.Object) + */ + public void restoreFrom(RestartData restartData) { + if (restartData == null || restartData.getProperties() == null || + restartData.getProperties().getProperty(RESTART_DATA_NAME) == null) { + return; + } + + InputState is = getState(); + int startAtRecord = Integer.parseInt(restartData.getProperties().getProperty(RESTART_DATA_NAME)); + + for (int i = 0; i < startAtRecord; i++) { + readNextRecord(); + } + + is.currentRecordCount = startAtRecord; + } + + /** + * Skip the current record. + * @see org.springframework.batch.container.advice.SkipAdvice#skip() + */ + public void skip() { + InputState is = getState(); + is.skipLines.add(new Integer(is.currentRecordCount)); + } + + /** + * Get encoding. + * @return the character encoding of the stream + */ + public String getEncoding() { + return encoding; + } + + /** + * Set encoding. + * @param encoding the character encoding of the stream + */ + public void setEncoding(String encoding) { + this.encoding = encoding; + } + + /** + * Get statistics for the processed input. + * @return actual statistics for the processed input + * @see org.springframework.batch.container.advice.StatisticsAdvice#getStatistics() + */ + public Properties getStatistics() { + + statistics.setProperty(READ_STATISTICS_NAME, String.valueOf(getState().currentRecordCount)); + return statistics; + } + + /** + * Set the ObjectInputFactory which is used for retrieving ObjectInput + * @param inputFactory the factory to use + */ + public void setInputFactory(ObjectInputFactory inputFactory) { + this.inputFactory = inputFactory; + } + + /* *** Intentionally unimplemented methods *** */ + + public void suspend() { + } + + public void resume() { + } + + public void beforeCommit(boolean arg0) { + } + + public void beforeCompletion() { + } + + public void afterCommit() { + } + + /** + * Value object holding state of the input source. + */ + private static class InputState { + boolean initialized = false; + + int currentRecordCount = 0; + + ObjectInput objectInput; + + List skipLines = new ArrayList(); + + long lastCommitPoint; + } +} diff --git a/infrastructure/src/main/java/org/springframework/batch/io/xml/XmlInputSource2.java b/infrastructure/src/main/java/org/springframework/batch/io/xml/XmlInputSource2.java new file mode 100644 index 000000000..9a9b867d0 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/xml/XmlInputSource2.java @@ -0,0 +1,397 @@ +package org.springframework.batch.io.xml; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.channels.FileChannel; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +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.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.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.oxm.Unmarshaller; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationAdapter; +import org.springframework.util.Assert; + +/** + *

XmlInputSource2 is {@link InputSource} implementation which processes XML input file + * and maps XML elements to objects. It uses {@link org.springframework.oxm.Unmarshaller} + * for parsing XML and for OXM mapping. This allows to plug-in various OXM frameworks. + * Spring-ws' OXM package provides implementations for Castor, JAXB, JiBX, + * XmlBeans and XStream.

+ * + *

{@link org.springframework.oxm.Unmarshaller} always processes whole XML + * input at once, but we need to process only one record per one module + * iteration. Therefore XmlInputSource2 uses {@link UnmarshallerAdapter} which cuts XML + * into smaller pieces (each piece contains a single record) and calls the unmarshaller + * to process only this single piece (record).

+ * + *

XmlInputSource2 configuration

+ *

Mandatory bean properties: + *

    + *
  • resource - resource pointing to XML input file. Currently, only + * {@link FileSystemResource} is supported.
  • + *
  • unmarshaller - any implementation of the {@link Unmarshaller}
  • + *
  • recordElementName - name of the XML element which represents single record
  • + *
+ *

+ *

Optional bean properties (if not set, default value is used): + *

    + *
  • encoding - input file encoding (default value is UTF-8)
  • + *
  • useSaxParser - TRUE forces unmarshaller to use SAX parser instead StAX parser. + * This can be used for performance optimization. Performance of SAX and StAX can vary + * depending on OXM framework used and XML record size. Default value is FALSE
  • + *
+ *

+ * + *

Limitations: + *

    + *
  • processing of nested records is not supported:
  • + *
    			<folder>
    + * 				<folder></folder> <- this is not supported
    + * 				<file></file>
    + * 			</folder> 
    + * 			<folder>
    + * 				<file></file>
    + * 			</folder>
    + *
  • References (idref) in XML are not supported
  • + *
  • Validation against XSD schema is not implemented
  • + *
+ *

+ * + * @see org.springframework.oxm.Unmarshaller + * @see org.springframework.batch.io.xml.UnmarshallerAdapter + * @author Peter Zozom + */ +public class XmlInputSource2 implements InputSource, Skippable, Restartable, StatisticsProvider, InitializingBean, + DisposableBean { + + //logger + private static final Log log = LogFactory.getLog(XmlInputSource2.class); + + //default encoding + private static final String DEFAULT_ENCODING = "UTF-8"; + + //restart data property name + private static final String RESTART_DATA_NAME = "staxinputsource.position"; + + //read statistics property name + public static final String READ_STATISTICS_NAME = "Read"; + + //file system resource + private Resource resource; + + //xml unmarshaller (Castor, JAXB, JiBX, XmlBeans or XStream) + private Unmarshaller unmarshaller; + + //unmarshaller adapter + private UnmarshallerAdapter unmarshallerAdapter; + + //file channel associated with Resource + private FileChannel fc; + + //encoding to be used while reading from the resource + private String encoding = DEFAULT_ENCODING; + + //name of the element which represents record + private String recordElementName; + + //force to use SAX parser. By default StAX parser is used. + private boolean useSaxParser = false; + + //signalizes that input source has been initialized + private boolean initialized = false; + + //transaction synchronization object + private TransactionSynchronization synchronization = new XmlInputSource2TransactionSychronization(); + + //current count of processed records + private long currentRecordCount = 0; + + //file channel position at last commit point + private long lastCommitPointPosition = 0; + + //count of processed records at last commit point + private long lastCommitPointRecordCount = 0; + + //list of skipped record numbers + private List skipRecords = new ArrayList(); + + //statistics + private Properties statistics = new Properties(); + + /** + * Set the encoding to be used while reading from the Resource + * @param encoding the encoding to be used + */ + public void setEncoding(String encoding) { + this.encoding = encoding; + } + + /** + * Set the resource to be read from. Currently is only {@link FileSystemResource} supported. + * @param resource the Resource to be read from + */ + public void setResource(Resource resource) { + this.resource = resource; + } + + /** + * Set the {@link Unmarshaller} implementation to be used for Object XML mapping (e.g. JAXB, JiBX, XmlBeans...). + * @see org.springframework.oxm.Unmarshaller + * @param unmarshaller the OXM unmarshaller + */ + public void setUnmarshaller(Unmarshaller unmarshaller) { + this.unmarshaller = unmarshaller; + } + + /** + * Set the name of the element which represents the record. + * @param elementName the element name + */ + public void setRecordElementName(String elementName) { + this.recordElementName = elementName; + } + + /** + * Force to use SAX parser instead of StAX parser. + * @param useSaxParser if true SAX parser will be used, else StAX parser will be used + */ + public void setUseSaxParser(boolean useSaxParser) { + this.useSaxParser = useSaxParser; + } + + /** + * Verifies bean configuration. Resource, Unmarshaller and record element name are mandatory. + * @throws Exception + * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() + */ + public void afterPropertiesSet() throws Exception { + Assert.notNull(resource); + Assert.state(resource.exists(), "Input resource does not exist: [" + resource + "]"); + Assert.notNull(unmarshaller); + Assert.hasLength(recordElementName); + } + + /** + * @throws Exception + * @see org.springframework.beans.factory.DisposableBean#destroy() + */ + public void destroy() throws Exception { + close(); + } + + /** + * Registers the input source for transaction synchronization. + */ + private void registerSynchronization() { + BatchTransactionSynchronizationManager.registerSynchronization(synchronization); + } + + /** + * Opens the input source. + * @see org.springframework.batch.item.ResourceLifecycle#open() + */ + public void open() { + + registerSynchronization(); + + try { + InputStream is = resource.getInputStream(); + + if (is instanceof FileInputStream) { + fc = ((FileInputStream) is).getChannel(); + } + else { + throw new IllegalArgumentException("Only file input stream is supported"); + } + + this.unmarshallerAdapter = new UnmarshallerAdapter(unmarshaller, recordElementName, fc, encoding, + useSaxParser); + + } + catch (IOException ioe) { + log.error(ioe); + throw new DataAccessResourceFailureException("Unable to get input stream", ioe); + } + + initialized = true; + } + + /** + * Closes the input source. + * @see org.springframework.batch.item.ResourceLifecycle#close() + */ + public void close() { + + initialized = false; + + try { + fc.close(); + } + catch (IOException ioe) { + log.error(ioe); + throw new DataAccessResourceFailureException("Unable to close input Source", ioe); + } + } + + /** + * Read object from the input. + * @return + * @see org.springframework.batch.io.InputSource#read() + */ + public Object read() { + if (!initialized) { + open(); + } + + Object o; + + do { + currentRecordCount++; + + try { + o = unmarshallerAdapter.unmarshal(); + } + catch (IOException ioe) { + log.error(ioe); + throw new DataAccessResourceFailureException("Unable to close XML Input Source", ioe); + } + } while (skipRecords.contains(new Long(currentRecordCount))); + + return o; + } + + /** + * Mark current record to be skipped. + * @see org.springframework.batch.io.Skippable#skip() + */ + public void skip() { + skipRecords.add(new Long(currentRecordCount)); + } + + /** + * @return + * @see org.springframework.batch.restart.Restartable#getRestartData() + */ + public RestartData getRestartData() { + Properties restartData = new Properties(); + + restartData.setProperty(RESTART_DATA_NAME, String.valueOf(getPosition())); + + return new GenericRestartData(restartData); + } + + /** + * Get current file position. + * @return file position + */ + private long getPosition() { + + long pos = 0; + + try { + pos = fc.position(); + } + catch (IOException ioe) { + throw new DataAccessResourceFailureException("Unable to get file position", ioe); + } + + return pos; + } + + /** + * @param data + * @see org.springframework.batch.restart.Restartable#restoreFrom(org.springframework.batch.restart.RestartData) + */ + public void restoreFrom(RestartData data) { + if (data == null || data.getProperties() == null || data.getProperties().getProperty(RESTART_DATA_NAME) == null) { + return; + } + + if (!initialized) { + open(); + } + + long startAtPosition = Long.parseLong(data.getProperties().getProperty(RESTART_DATA_NAME)); + setPosition(startAtPosition); + this.unmarshallerAdapter = new UnmarshallerAdapter(unmarshaller, recordElementName, fc, encoding, useSaxParser); + } + + /** + * @param startAtPosition + */ + private void setPosition(long position) { + + try { + fc.position(position); + } + catch (IOException ioe) { + throw new DataAccessResourceFailureException("Unable to set file position", ioe); + } + } + + /** + * @return + * @see org.springframework.batch.statistics.StatisticsProvider#getStatistics() + */ + public Properties getStatistics() { + statistics.setProperty(READ_STATISTICS_NAME, String.valueOf(currentRecordCount)); + return statistics; + } + + + + /** + * Encapsulates transaction events for the XmlInputSource2. + */ + private class XmlInputSource2TransactionSychronization extends TransactionSynchronizationAdapter { + + /** + * @param status + * @see org.springframework.transaction.support.TransactionSynchronizationAdapter#afterCompletion(int) + */ + 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; + skipRecords = new ArrayList(); + } + + private void transactionRolledback() { + currentRecordCount = lastCommitPointRecordCount; + setPosition(lastCommitPointPosition); + unmarshallerAdapter = new UnmarshallerAdapter(unmarshaller, recordElementName, fc, encoding, useSaxParser); + } + } + + + //package visibility method necessary to simulate transaction events in tests + TransactionSynchronization getSynchronization() { + return synchronization; + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/io/xml/XmlOutputSource.java b/infrastructure/src/main/java/org/springframework/batch/io/xml/XmlOutputSource.java new file mode 100644 index 000000000..f1ea6d8b7 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/xml/XmlOutputSource.java @@ -0,0 +1,408 @@ +/* + * 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.xml; + +import java.io.File; +import java.io.IOException; +import java.util.Properties; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.batch.io.OutputSource; +import org.springframework.batch.io.exception.BatchEnvironmentException; +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.core.io.Resource; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.util.Assert; + +/** + * XmlOutputTemplate is implementation of {@link OutputSource} which processes + * XML output independently of technologies used for serializing value objects + * to XML files. It has references only to interfaces, not concrete + * implementations. It uses {@link ObjectOutputFactory} interface for getting + * {@link ObjectOutput}, which is interface for writing value objects to the + * output stream. So it's easy to plug-in any technology for serializing value + * objects to XML files by implementing these interfaces. See implementations of + * {@link ObjectOutputFactory} interface. + *

+ * This output template also provides restart, statistics and transaction + * features by implementing corresponding 'advice' interfaces. + * + * @author peter.zozom + * @see ObjectOutput + * @see ObjectOutputFactory + */ +public class XmlOutputSource implements OutputSource, Restartable, StatisticsProvider, TransactionSynchronization, + DisposableBean { + private static final Log log = LogFactory.getLog(XmlOutputSource.class); + + private static final String DEFAULT_ENCODING = "UTF-8"; + + private ObjectOutputFactory outputFactory; + + private String encoding = DEFAULT_ENCODING; + + private Resource resource; + + private Properties statistics = new Properties(); + + /* + * Unique source name used to construct this xml reader input source - + * specified by the configuration file. + */ + private String sourceName = null; + + public static final String WRITTEN_STATISTICS_NAME = "Written"; + + public static final String RESTART_DATA_NAME = "xmloutputtemplate.currentRecordCount"; + + private OutputState state; + + // Accessor method for the state object + private OutputState getState() { + if (state == null) { + state = new OutputState(); + } + return state; + } + + public void afterPropertiesSet() throws Exception { + Assert.notNull(resource); + Assert.state(resource.getFile().canWrite(), "Resource is not writable"); + } + + /** + * Setter for resource. Represents a file that can be written. + * + * @param resource + */ + public void setResource(Resource resource) { + this.resource = resource; + } + + /** + * Initialize the output source. This method should be called for each + * thread using same XmlOutputTemplate instance. + * + * @see org.springframework.batch.item.ResourceLifecycle#open() + */ + public void open() { + + OutputState os = getState(); + if (!os.initialized) { + } + } + + /** + * Registers object for transaction synchronization + */ + protected void registerSynchronization() { + BatchTransactionSynchronizationManager.registerSynchronization(this); + } + + /** + * Just calls {@link #close()} so that bean factories will clean up + * resources correctly. + * + * @see org.springframework.beans.factory.DisposableBean#destroy() + */ + public void destroy() throws Exception { + close(); + } + + /** + * Close the output source + * + * @see org.springframework.batch.item.ResourceLifecycle#close() + */ + public void close() { + OutputState os = getState(); + try { + if (os != null && os.objectOutput != null) { + os.objectOutput.close(); + } + } + finally { + if (os != null) { + os.initialized = false; + os.restarted = false; + } + } + } + + /* + * Initializes and obtains a ObjectOutput from ObjectOutputFactory. + */ + private void initializeXmlWriter() { + OutputState os = getState(); + os.initialized = false; + 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 (!os.restarted) { + if (file.exists()) { + if (os.shouldDeleteIfExists) { + file.delete(); + } + else { + throw new BatchEnvironmentException("Resource already exists: " + resource); + } + } + file.createNewFile(); + } + + } + catch (IOException ioe) { + log.error(ioe); + throw new DataAccessResourceFailureException("Unable to write to file resource: [" + resource + "]", ioe); + } + + os.objectOutput = outputFactory.createObjectOutput(resource, encoding); + + if (os.restarted) { + os.objectOutput.afterRestart(new Long(os.lastMarkedByteOffsetPosition)); + checkFileSize(); + } + + os.initialized = true; + } + + /** + * Write the value object to output xml stream. + * @param output the value object + * @see org.springframework.batch.io.OutputSource#write(java.lang.Object) + */ + public void write(Object output) { + OutputState os = getState(); + if (!os.initialized) { + open(); + initializeXmlWriter(); + } + + try { + os.objectOutput.writeObject(output); + os.objectsWritten++; + } + catch (IOException ioe) { + log.error(ioe); + throw new DataAccessResourceFailureException("Unable to write to ObjectOutputStream", ioe); + } + } + + /** + * Return the name of the output source. + */ + public void setName(String newName) { + this.sourceName = newName; + } + + /** + * Set the name of the output source. + * + * @see org.springframework.batch.restart.Restartable#getName() + */ + public String getName() { + return sourceName; + } + + /** + * Postprocess after transaction commit/rollback. Called from transaction + * manager. + * + * @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(); + } + } + + /* + * Postprocess after transaction commit + */ + private void transactionComitted() { + OutputState os = getState(); + + os.objectOutput.flush(); + os.lastMarkedByteOffsetPosition = this.position(); + } + + /* + * Postprocess after transaction rollback + */ + private void transactionRolledback() { + checkFileSize(); + resetPositionForRestart(); + } + + /* + * This method removes any information in the file before this reset point. + * + * @param resetByteOffset a long integer representing the byte offset to + * trunction and reposition the file cursor to for restarting. + */ + private void resetPositionForRestart() { + OutputState os = getState(); + os.objectOutput.truncate(os.lastMarkedByteOffsetPosition); + os.objectOutput.position(os.lastMarkedByteOffsetPosition); + } + + /* + * 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. + */ + private void checkFileSize() { + OutputState os = getState(); + long size = -1; + + size = os.objectOutput.size(); + + Assert.state(size >= os.lastMarkedByteOffsetPosition, "Current file size is smaller than size at last commit"); + } + + /* + * Return the byte offset position of the cursor in the output file as a + * long integer. + * + * @return long integer representing the byte offset position of the cursor + * in the output file. + */ + private long position() { + OutputState os = getState(); + return os.objectOutput.position(); + } + + /** + * Get statistics for the processed output. + * @return actual statistics for the processed output + * @see org.springframework.batch.container.advice.StatisticsAdvice#getStatistics() + */ + public Properties getStatistics() { + + statistics.setProperty(WRITTEN_STATISTICS_NAME, new Long(getState().objectsWritten).toString()); + return statistics; + } + + /** + * Set encoding. + * @param encoding the character encoding of the stream + */ + public void setEncoding(String encoding) { + this.encoding = encoding; + } + + /** + * Set the ObjectOutputFactory which is used for retrieving ObjectOutput + * @param outputFactory the factory to use + */ + public void setOutputFactory(ObjectOutputFactory outputFactory) { + this.outputFactory = outputFactory; + } + + /* *** Intentionally unimplemented methods *** */ + + public void suspend() { + } + + public void resume() { + } + + public void beforeCommit(boolean arg0) { + } + + public void beforeCompletion() { + } + + public void afterCommit() { + } + + /* + * Value object holding state of the output source. + */ + private static class OutputState { + private boolean initialized = false; + + private ObjectOutput objectOutput; + + private long lastMarkedByteOffsetPosition = 0; + + private long objectsWritten = 0; + + private boolean shouldDeleteIfExists = true; + + private boolean restarted = false; + + } + + public void restoreFrom(RestartData data) { + if (data == null || data.getProperties() == null || + data.getProperties().getProperty(RESTART_DATA_NAME) == null) { + return; + } + + OutputState os = getState(); + os.lastMarkedByteOffsetPosition = Long.parseLong(data.getProperties().getProperty(RESTART_DATA_NAME)); + os.restarted = true; + initializeXmlWriter(); + } + + /** + * This method returns the restart data for the output source. It returns + * the current byte offset position of the cursor in the output file which + * can be used to re-initialze the batch job in case of restart. + * + * @see org.springframework.batch.container.Restartable#getRestartData() + */ + + public RestartData getRestartData() { + Properties restartData = new Properties(); + restartData.setProperty(RESTART_DATA_NAME, new Long(position()).toString()); + return new GenericRestartData(restartData); + } + + /** + * This method initializes the output source for restart. It opens the + * output file and position the xml writer according to information provided + * by the restart data. + * + * @param restartData restart data information + * @see org.springframework.batch.container.advice.RestartAdvice#initForRestart(java.lang.Object) + */ + public void initForRestart(Object restartData) { + + } +} diff --git a/infrastructure/src/main/java/org/springframework/batch/io/xml/package.html b/infrastructure/src/main/java/org/springframework/batch/io/xml/package.html new file mode 100644 index 000000000..8e827d2e0 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/xml/package.html @@ -0,0 +1,7 @@ + + +

+Infrastructure implementations of io xml concerns. +

+ + diff --git a/infrastructure/src/main/java/org/springframework/batch/io/xml/xstream/AttributeAlias.java b/infrastructure/src/main/java/org/springframework/batch/io/xml/xstream/AttributeAlias.java new file mode 100644 index 000000000..250bbc9b0 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/xml/xstream/AttributeAlias.java @@ -0,0 +1,58 @@ +/* + * 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.xml.xstream; + +/** + * Represents alias for an attribute. + * @author peter.zozom + */ +public class AttributeAlias { + + private String alias; + + private String attributeName; + + /** + * @return the alias + */ + public String getAlias() { + return alias; + } + + /** + * Set alias for attribute + * @param alias the alias itself + */ + public void setAlias(String alias) { + this.alias = alias; + } + + /** + * @return the attribute name + */ + public String getAttributeName() { + return attributeName; + } + + /** + * Set the attribute name. + * @param attributeName the name of the attribute + */ + public void setAttributeName(String attributeName) { + this.attributeName = attributeName; + } +} diff --git a/infrastructure/src/main/java/org/springframework/batch/io/xml/xstream/AttributeProperties.java b/infrastructure/src/main/java/org/springframework/batch/io/xml/xstream/AttributeProperties.java new file mode 100644 index 000000000..921a849c7 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/xml/xstream/AttributeProperties.java @@ -0,0 +1,58 @@ +/* + * 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.xml.xstream; + +/** + * Defines which XML attribute to use for a field or a specific type. + * @author peter.zozom + */ +public class AttributeProperties { + + private String type; + + private String fieldName; + + /** + * @return the field name which will be rendered as XML attribute + */ + public String getFieldName() { + return fieldName; + } + + /** + * Set the field to be rendered as XML attribute + * @param fieldName the name of the field + */ + public void setFieldName(String fieldName) { + this.fieldName = fieldName; + } + + /** + * @return the type + */ + public String getType() { + return type; + } + + /** + * Set type to be used for XML attribute. + * @param type the name of the type to be rendered as XML attribute + */ + public void setType(String type) { + this.type = type; + } +} diff --git a/infrastructure/src/main/java/org/springframework/batch/io/xml/xstream/ClassAlias.java b/infrastructure/src/main/java/org/springframework/batch/io/xml/xstream/ClassAlias.java new file mode 100644 index 000000000..254d4e207 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/xml/xstream/ClassAlias.java @@ -0,0 +1,76 @@ +/* + * 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.xml.xstream; + +/** + * Represents mapping of a class to a shorter name to be used in XML elements. + * @author peter.zozom + */ +public class ClassAlias { + + private String name; + + private String type; + + private String defaultImplementation; + + /** + * @return the default implementation of the type + */ + public String getDefaultImplementation() { + return defaultImplementation; + } + + /** + * Set default implementation of type to use. + * @param defaultImplementation Default implementation of type to use if no + * other specified. + */ + public void setDefaultImplementation(String defaultImplementation) { + this.defaultImplementation = defaultImplementation; + } + + /** + * @return short name for the type + */ + public String getName() { + return name; + } + + /** + * Set short name. + * @param name the name to set + */ + public void setName(String name) { + this.name = name; + } + + /** + * @return aliased type + */ + public String getType() { + return type; + } + + /** + * Set aliased type. + * @param type type to be aliased + */ + public void setType(String type) { + this.type = type; + } +} diff --git a/infrastructure/src/main/java/org/springframework/batch/io/xml/xstream/ConverterProperties.java b/infrastructure/src/main/java/org/springframework/batch/io/xml/xstream/ConverterProperties.java new file mode 100644 index 000000000..0b977011a --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/xml/xstream/ConverterProperties.java @@ -0,0 +1,61 @@ +/* + * 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.xml.xstream; + +import com.thoughtworks.xstream.XStream; + +/** + * Represents converter to be registered for parsing. Converter acts as a + * strategy for converting a particular type of class to XML and back again. + * + * @author peter.zozom + */ + +public class ConverterProperties { + private String className; + + private int priority = XStream.PRIORITY_NORMAL; + + /** + * @return converter class name + */ + protected String getClassName() { + return className; + } + + /** + * Set converter class name. + * @param className converter class name + */ + protected void setClassName(String className) { + this.className = className; + } + + /** + * @return converter priority + */ + protected int getPriority() { + return priority; + } + + /** + * @param priority converter priority + */ + protected void setPriority(int priority) { + this.priority = priority; + } +} diff --git a/infrastructure/src/main/java/org/springframework/batch/io/xml/xstream/DefaultImplementation.java b/infrastructure/src/main/java/org/springframework/batch/io/xml/xstream/DefaultImplementation.java new file mode 100644 index 000000000..c4e452118 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/xml/xstream/DefaultImplementation.java @@ -0,0 +1,62 @@ +/* + * 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.xml.xstream; + +/** + * Defines default implementation of a class which should associated with an + * object. Whenever XStream encounters an instance of this type, it will use the + * default implementation instead. For example, java.util.ArrayList is the + * default implementation of java.util.List. + * @author peter.zozom + */ +public class DefaultImplementation { + + private String defaultImpl; + + private String type; + + /** + * @return class name of the default implementation + */ + public String getDefaultImpl() { + return defaultImpl; + } + + /** + * Set the class name of the default implementation which should be + * associated with ofType. + * @param defaultImpl class name of the default implementation + */ + public void setDefaultImpl(String defaultImpl) { + this.defaultImpl = defaultImpl; + } + + /** + * @return type name associated with default implementation + */ + public String getType() { + return type; + } + + /** + * Set type name which should be associated with default implementation. + * @param ofType type name + */ + public void setType(String ofType) { + this.type = ofType; + } +} diff --git a/infrastructure/src/main/java/org/springframework/batch/io/xml/xstream/FieldAlias.java b/infrastructure/src/main/java/org/springframework/batch/io/xml/xstream/FieldAlias.java new file mode 100644 index 000000000..4b842dddb --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/xml/xstream/FieldAlias.java @@ -0,0 +1,74 @@ +/* + * 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.xml.xstream; + +/** + * Represents an alias for a field name. + * @author peter.zozom + */ +public class FieldAlias { + private String aliasName; + + private String type; + + private String fieldName; + + /** + * @return field alias + */ + public String getAliasName() { + return aliasName; + } + + /** + * Set field alias name. + * @param aliasName the alias itself + */ + public void setAliasName(String aliasName) { + this.aliasName = aliasName; + } + + /** + * @return field name to be aliased + */ + public String getFieldName() { + return fieldName; + } + + /** + * Set the name of the field to be aliased. + * @param fieldName the name of the field to be aliased + */ + public void setFieldName(String fieldName) { + this.fieldName = fieldName; + } + + /** + * @return the type that declares the field + */ + public String getType() { + return type; + } + + /** + * Set the type that declares the field. + * @param type the type that declares the field + */ + public void setType(String type) { + this.type = type; + } +} diff --git a/infrastructure/src/main/java/org/springframework/batch/io/xml/xstream/ImplicitCollection.java b/infrastructure/src/main/java/org/springframework/batch/io/xml/xstream/ImplicitCollection.java new file mode 100644 index 000000000..8a93d1aaa --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/xml/xstream/ImplicitCollection.java @@ -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.xml.xstream; + +/** + * Represents implicit collection definition. Implicit collection is used for: + *
    + *
  • any unmapped xml tag
  • + *
  • or all items of the given itemType
  • + *
  • or all items of the given element name defined by itemFieldName
  • + *
+ * + * @author peter.zozom + */ +public class ImplicitCollection { + private String ownerType; + + private String fieldName; + + private String itemFieldName; + + private String itemType; + + /** + * @return name of the field in the ownerType + */ + protected String getFieldName() { + return fieldName; + } + + /** + * Set name of the field in the owner class. This field must be an + * java.util.ArrayList. + * @param fieldName name of the field in the ownerType + */ + protected void setFieldName(String fieldName) { + this.fieldName = fieldName; + } + + /** + * @return element name of the implicit collection + */ + protected String getItemFieldName() { + return itemFieldName; + } + + /** + * Set element name of the implicit collection. + * @param itemFieldName element name of the implicit collection + */ + protected void setItemFieldName(String itemFieldName) { + this.itemFieldName = itemFieldName; + } + + /** + * @return type of the items to be part of this collection + */ + protected String getItemType() { + return itemType; + } + + /** + * Set yype of the items to be part of this collection (aliased with + * ItemFieldName, if provided). + * @param itemType type of the items to be part of this collection + */ + protected void setItemType(String itemType) { + this.itemType = itemType; + } + + /** + * @return class owning the implicit collection + */ + protected String getOwnerType() { + return ownerType; + } + + /** + * Set class that owns implicit collection. + * @param ownerType class owning the implicit collection + */ + protected void setOwnerType(String ownerType) { + this.ownerType = ownerType; + } +} diff --git a/infrastructure/src/main/java/org/springframework/batch/io/xml/xstream/Mapping.java b/infrastructure/src/main/java/org/springframework/batch/io/xml/xstream/Mapping.java new file mode 100644 index 000000000..7833ef4c5 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/xml/xstream/Mapping.java @@ -0,0 +1,98 @@ +/* + * 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.xml.xstream; + +/** + * Represents a mapping of qualified tag names to Java class names allowing + * class aliases and namespace aware mappings of qualified tag names to class + * names. + * @author peter.zozom + * @see javax.xml.namespace.QName + */ +public class Mapping { + + private String namespaceURI; + + private String localPart; + + private String prefix; + + private String className; + + /** + * @return class name + */ + public String getClassName() { + return className; + } + + /** + * Set Java type which will be mapped. + * @param className type name to be mapped + */ + public void setClassName(String className) { + this.className = className; + } + + /** + * @return local part of the qualified name + */ + public String getLocalPart() { + return localPart; + } + + /** + * Set local part of the qualified name. + * @param localPart local part of the qualified name + * @see javax.xml.namespace.QName + */ + public void setLocalPart(String localPart) { + this.localPart = localPart; + } + + /** + * @return namespace URI of the qualified name + */ + public String getNamespaceURI() { + return namespaceURI; + } + + /** + * Set namespace URI of the qualified name. + * @param namespaceURI namespace URI of the qualified name + * @see javax.xml.namespace.QName + */ + public void setNamespaceURI(String namespaceURI) { + this.namespaceURI = namespaceURI; + } + + /** + * @return prefix of the qualified name + */ + public String getPrefix() { + return prefix; + } + + /** + * Set prefix of the qualified name. + * @param prefix prefix of the qualified name + * @see javax.xml.namespace.QName + */ + public void setPrefix(String prefix) { + this.prefix = prefix; + } +} diff --git a/infrastructure/src/main/java/org/springframework/batch/io/xml/xstream/OmmitedField.java b/infrastructure/src/main/java/org/springframework/batch/io/xml/xstream/OmmitedField.java new file mode 100644 index 000000000..03ef27a49 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/xml/xstream/OmmitedField.java @@ -0,0 +1,60 @@ +/* + * 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.xml.xstream; + +/** + * Defines a field which shouldn't be serialized. To omit a field you must + * always provide the declaring type and not necessarily the type that is + * converted. + * @author peter.zozom + * + */ +public class OmmitedField { + private String type; + + private String fieldName; + + /** + * @return field which should be ommited + */ + protected String getFieldName() { + return fieldName; + } + + /** + * Set field which should be ommited. + * @param fieldName field which should be ommited + */ + protected void setFieldName(String fieldName) { + this.fieldName = fieldName; + } + + /** + * @return declaring type of the ommited field + */ + protected String getType() { + return type; + } + + /** + * Set declaring type of the ommited field. + * @param type declaring type of the ommited field + */ + protected void setType(String type) { + this.type = type; + } +} diff --git a/infrastructure/src/main/java/org/springframework/batch/io/xml/xstream/TypeAlias.java b/infrastructure/src/main/java/org/springframework/batch/io/xml/xstream/TypeAlias.java new file mode 100644 index 000000000..68a6f3d1b --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/xml/xstream/TypeAlias.java @@ -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.xml.xstream; + +/** + * Represents mapping a type to a shorter name to be used in XML elements. Any + * class that is assignable to this type will be aliased to the same name. + * @author peter.zozom + */ +public class TypeAlias { + + private String name; + + private String type; + + /** + * @return short name for the type + */ + public String getName() { + return name; + } + + /** + * Set short name. + * @param name the name to set + */ + public void setName(String name) { + this.name = name; + } + + /** + * @return aliased type + */ + public String getType() { + return type; + } + + /** + * Set aliased type. + * @param type type to be aliased + */ + public void setType(String type) { + this.type = type; + } +} diff --git a/infrastructure/src/main/java/org/springframework/batch/io/xml/xstream/XStreamConfiguration.java b/infrastructure/src/main/java/org/springframework/batch/io/xml/xstream/XStreamConfiguration.java new file mode 100644 index 000000000..98e51d53b --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/xml/xstream/XStreamConfiguration.java @@ -0,0 +1,301 @@ +/* + * 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.xml.xstream; + +import java.util.List; +import java.util.Map; + +import com.thoughtworks.xstream.XStream; + +/** + * Value object, which holds configuration for XStream. + * + * @author peter.zozom + * @author Dave Syer + * + * @see XStreamConfigurationFactoryBean + * @see Mapping + * @see ClassAlias + * @see TypeAlias + * @see FieldAlias + * @see AttributeAlias + * @see AttributeProperties + * @see ConverterProperties + * @see XStream#setMode(int) + * @see ImplicitCollection + * @see OmmitedField + * @see XStream#addImmutableType(Class) + * @see DefaultImplementation + */ +public class XStreamConfiguration { + + private List mappings = null; + + private String rootElementName = null; + + private Map rootElementAttributes; + + private List classAliases = null; + + private List typeAliases = null; + + private List fieldAliases = null; + + private List attributeAliases = null; + + private List attributes = null; + + private List converters = null; + + private int mode = XStream.XPATH_RELATIVE_REFERENCES; + + private List implicitCollections = null; + + private List ommitedFields = null; + + private List immutableTypes = null; + + private List defaultImplementations = null; + + /** + * @return list of the {@link DefaultImplementation} objects + */ + public List getDefaultImplementations() { + return defaultImplementations; + } + + /** + * Set list of default implementations. + * @param defaultImplementations list of the {@link DefaultImplementation} + * objects + * @see DefaultImplementation + */ + public void setDefaultImplementations(List defaultImplementations) { + this.defaultImplementations = defaultImplementations; + } + + /** + * @return list of the immutable type names + */ + public List getImmutableTypes() { + return immutableTypes; + } + + /** + * Set list of immutable types. + * @param immutableTypes list of the immutable type names + * @see XStream#addImmutableType(Class) + */ + public void setImmutableTypes(List immutableTypes) { + this.immutableTypes = immutableTypes; + } + + /** + * @return list of the {@link AttributeAlias} objects + */ + public List getAttributeAliases() { + return attributeAliases; + } + + /** + * Set list of attribute aliases. + * @param attributeAliases list of the {@link AttributeAlias} objects + * @see AttributeAlias + */ + public void setAttributeAliases(List attributeAliases) { + this.attributeAliases = attributeAliases; + } + + /** + * @return list of the {@link AttributeProperties} objects + */ + public List getAttributes() { + return attributes; + } + + /** + * Set list of attribute properties. + * @param attributes list of the {@link AttributeProperties} + * objects + * @see AttributeProperties + */ + public void setAttributes(List attributes) { + this.attributes = attributes; + } + + /** + * @return the classAliases + */ + public List getClassAliases() { + return classAliases; + } + + /** + * Set list of class aliases. + * @param classAliases the classAliases to set + * @see ClassAlias + */ + public void setClassAliases(List classAliases) { + this.classAliases = classAliases; + } + + /** + * @return list of the {@link ConverterProperties} objects + */ + public List getConverters() { + return converters; + } + + /** + * Set list of custom converters. + * @param converters list of the {@link ConverterProperties} objects + * @see ConverterProperties + */ + public void setConverters(List converters) { + this.converters = converters; + } + + /** + * @return the fieldAliases + */ + public List getFieldAliases() { + return fieldAliases; + } + + /** + * Set list of field aliases. + * @param fieldAliases the list of fieldAliases to set + * @see FieldAlias + */ + public void setFieldAliases(List fieldAliases) { + this.fieldAliases = fieldAliases; + } + + /** + * @return list of the {@link ImplicitCollections} objects + */ + public List getImplicitCollections() { + return implicitCollections; + } + + /** + * Set list of implicit collection definitions. + * @param implicitCollections list of the {@link ImplicitCollections} + * objects + * @see ImplicitCollection + */ + public void setImplicitCollections(List implicitCollections) { + this.implicitCollections = implicitCollections; + } + + /** + * @return list of the {@link Mapping} objects + */ + public List getMappings() { + return mappings; + } + + /** + * Set list of "qualified tag name - to - class name" mappigs. + * @param mappings list of the {@link Mapping} objects + * @see Mapping + */ + public void setMappings(List mappings) { + this.mappings = mappings; + } + + /** + * @return the actual mode + */ + public int getMode() { + return mode; + } + + /** + * Set mode for dealing with duplicate references. If not provided, default + * value is used ({@link XStream#XPATH_RELATIVE_REFERENCES}). + * @param mode the mode to set + * @see XStream#setMode(int) + */ + public void setMode(int mode) { + this.mode = mode; + } + + /** + * @return list of the {@link OmmitedFields} objects + */ + public List getOmmitedFields() { + return ommitedFields; + } + + /** + * Set list of ommited fields. + * @param ommitedFields list of the {@link OmmitedFields} objects + * @see OmmitedField + */ + public void setOmmitedFields(List ommitedFields) { + this.ommitedFields = ommitedFields; + } + + /** + * @return the root element attributes + */ + public Map getRootElementAttributes() { + return rootElementAttributes; + } + + /** + * Set attributes of root element. Each Map entry has key = "attribute name" + * and value = "attribute value". + * @param rootElementAttributes map of the root element attributes + */ + public void setRootElementAttributes(Map rootElementAttributes) { + this.rootElementAttributes = rootElementAttributes; + } + + /** + * @return the root element name + */ + public String getRootElementName() { + return rootElementName; + } + + /** + * Set name of the root element. Valid only for writing to XML. + * @param rootElementName the root element name + */ + public void setRootElementName(String rootElementName) { + this.rootElementName = rootElementName; + } + + /** + * @return the list of the {@link TypeAlias} objects + */ + public List getTypeAliases() { + return typeAliases; + } + + /** + * Set list of type aliases. + * @param typeAliases list of the {@link TypeAlias} objects + * @see TypeAlias + */ + public void setTypeAliases(List typeAliases) { + this.typeAliases = typeAliases; + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/io/xml/xstream/XStreamConfigurationFactoryBean.java b/infrastructure/src/main/java/org/springframework/batch/io/xml/xstream/XStreamConfigurationFactoryBean.java new file mode 100644 index 000000000..b3f17dd31 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/xml/xstream/XStreamConfigurationFactoryBean.java @@ -0,0 +1,146 @@ +/* + * 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.xml.xstream; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Map.Entry; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.batch.io.exception.BatchEnvironmentException; +import org.springframework.beans.factory.config.AbstractFactoryBean; +import org.springframework.core.io.Resource; + +import com.thoughtworks.xstream.XStream; + +/** + * Factory creates {@link XStreamConfiguration} object, which hold XStream's + * configuration settings. These settings are read from provided configuration + * XML file. + * + * @author peter.zozom + * @author Dave Syer + */ +public class XStreamConfigurationFactoryBean extends AbstractFactoryBean { + + private Log log = LogFactory.getLog(XStreamConfigurationFactoryBean.class); + + private Resource resource; + + /** + * Creates {@link XStreamConfiguration} object from XStream's config file. + * @return XStream's configuration settings. + */ + private XStreamConfiguration getXStreamConfiguration() { + + XStream stream = new XStream(); + setConfigAliases(stream); + + XStreamConfiguration xsc; + try { + InputStream is = resource.getInputStream(); + xsc = (XStreamConfiguration) stream.fromXML(is); + is.close(); + } + catch (IOException ioe) { + log.debug(ioe); + throw new BatchEnvironmentException("Could not read XStream mapping file.", ioe); + } + + return xsc; + } + + /* + * Set aliases necessary for parsing configuration file in xstream format. + */ + private void setConfigAliases(XStream stream) { + + stream.aliasField("root-element-name", XStreamConfiguration.class, "rootElementName"); + stream.aliasField("root-element-attributes", XStreamConfiguration.class, "rootElementAttributes"); + + stream.alias("class-alias", ClassAlias.class); + stream.aliasField("default-implementation", ClassAlias.class, "defaultImplementation"); + stream.aliasField("class-aliases", XStreamConfiguration.class, "classAliases"); + + stream.alias("type-alias", TypeAlias.class); + stream.aliasField("type-aliases", XStreamConfiguration.class, "typeAliases"); + + stream.alias("field-alias", FieldAlias.class); + stream.aliasField("alias-name", FieldAlias.class, "aliasName"); + stream.aliasField("field-name", FieldAlias.class, "fieldName"); + stream.aliasField("field-aliases", XStreamConfiguration.class, "fieldAliases"); + + stream.alias("attribute-alias", AttributeAlias.class); + stream.aliasField("attribute-name", AttributeAlias.class, "attributeName"); + stream.aliasField("attribute-aliases", XStreamConfiguration.class, "attributeAliases"); + + stream.alias("attribute-properties", AttributeProperties.class); + stream.aliasField("field-name", AttributeProperties.class, "fieldName"); + stream.aliasField("attributes", XStreamConfiguration.class, "attributes"); + + stream.alias("converter-properties", ConverterProperties.class); + stream.aliasField("class-name", ConverterProperties.class, "className"); + + stream.alias("implicit-collection", ImplicitCollection.class); + stream.aliasField("owner-type", ImplicitCollection.class, "ownerType"); + stream.aliasField("item-type", ImplicitCollection.class, "itemType"); + stream.aliasField("field-name", ImplicitCollection.class, "fieldName"); + stream.aliasField("item-field-name", ImplicitCollection.class, "itemFieldName"); + stream.aliasField("implicit-collections", XStreamConfiguration.class, "implicitCollections"); + + stream.alias("ommited-field", OmmitedField.class); + stream.aliasField("field-name", OmmitedField.class, "fieldName"); + stream.aliasField("ommited-fields", XStreamConfiguration.class, "ommitedFields"); + + stream.alias("default-implementation", DefaultImplementation.class); + stream.aliasField("default-impl", DefaultImplementation.class, "defaultImpl"); + stream.aliasField("default-implementations", XStreamConfiguration.class, "defaultImplementations"); + + stream.aliasField("immutable-types", XStreamConfiguration.class, "immutableTypes"); + + stream.alias("attribute", Entry.class); + stream.alias("mapping", Mapping.class); + stream.aliasField("class-name", Mapping.class, "className"); + stream.alias("configuration", XStreamConfiguration.class); + stream.alias("key", java.lang.String.class); + stream.alias("value", java.lang.String.class); + stream.alias("type", java.lang.String.class); + } + + /** + * Set the filename of the configuration XML file. + * @param configFile resource for reading configuration + */ + public void setConfigFile(Resource resource) { + this.resource = resource; + } + + /* (non-Javadoc) + * @see org.springframework.beans.factory.config.AbstractFactoryBean#createInstance() + */ + protected Object createInstance() throws Exception { + return getXStreamConfiguration(); + } + + /* (non-Javadoc) + * @see org.springframework.beans.factory.config.AbstractFactoryBean#getObjectType() + */ + public Class getObjectType() { + return XStreamConfiguration.class; + } +} diff --git a/infrastructure/src/main/java/org/springframework/batch/io/xml/xstream/XStreamFactory.java b/infrastructure/src/main/java/org/springframework/batch/io/xml/xstream/XStreamFactory.java new file mode 100644 index 000000000..d73a27813 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/xml/xstream/XStreamFactory.java @@ -0,0 +1,853 @@ +/* + * 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.xml.xstream; + +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.channels.FileChannel; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import javax.xml.namespace.QName; +import javax.xml.stream.Location; +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; +import javax.xml.stream.XMLStreamWriter; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.batch.io.exception.BatchEnvironmentException; +import org.springframework.batch.io.xml.ObjectInput; +import org.springframework.batch.io.xml.ObjectInputFactory; +import org.springframework.batch.io.xml.ObjectOutput; +import org.springframework.batch.io.xml.ObjectOutputFactory; +import org.springframework.core.io.Resource; +import org.springframework.dao.DataAccessResourceFailureException; + +import com.thoughtworks.xstream.XStream; +import com.thoughtworks.xstream.converters.Converter; +import com.thoughtworks.xstream.converters.SingleValueConverter; +import com.thoughtworks.xstream.io.xml.QNameMap; +import com.thoughtworks.xstream.io.xml.StaxReader; +import com.thoughtworks.xstream.io.xml.StaxWriter; + +/** + * XStreamFactory class implements both factory interfaces - + * {@link ObjectInputFactory} and {@link ObjectOutputFactory}. Factory methods + * {@link #createObjectInput(Resource, String)} and + * {@link #createObjectOutput(Resource, String)} return implementations of + * {@link ObjectInput} and {@link ObjectOutput} interfaces. These + * implementations (ObjectInputWrapper and ObjecOutputWrapper) are defined as + * factory's inner classes. They are wrapping StAX reader/writer for accessing + * xml streams, XStream mapper for mapping Xml-to-ValueObjects and + * {@link FileChannel} for file manipulation This factory implementation uses + * {@link XStreamConfiguration} as source for XStream configuration settings. + * + * @author peter.zozom + * @see ObjectInputFactory + * @see ObjectOutputFactory + * @see XStreamConfiguration + */ +public class XStreamFactory implements ObjectOutputFactory, ObjectInputFactory { + private static final Log log = LogFactory.getLog(XStreamFactory.class); + + private XStreamConfiguration config; + + /** + * Set the XStream's configuration. + * @param config value object holding XStream's configuration settings + */ + public void setConfig(XStreamConfiguration config) { + this.config = config; + } + + /* + * Set up XStream. Proctected visibility modifier is used to allow easier + * testing of set...() methods. + */ + protected void setUpXStream(XStream stream) { + setClassAliases(stream); + setTypeAliases(stream); + setFieldAliases(stream); + setAttributeAliases(stream); + setAttributes(stream); + registerConverters(stream); + setMode(stream); + addImplicitCollections(stream); + setOmittedFileds(stream); + addImmutableTypes(stream); + addDefaultImplementations(stream); + } + + /* + * Iterate over list of DefaultImplementation objects and add default + * implementations to the XStream. + */ + private void addDefaultImplementations(XStream stream) { + + // get list of DefaultImplementation objects + List defaultImplementations = config.getDefaultImplementations(); + // if not null iterate over list + if (defaultImplementations != null) { + for (Iterator i = defaultImplementations.iterator(); i.hasNext();) { + DefaultImplementation di = (DefaultImplementation) i.next(); + + // try to create Class object for default implementation class + // name + Class defaultImplementation; + try { + defaultImplementation = Class.forName(di.getDefaultImpl()); + } + catch (ClassNotFoundException cnfe) { + log.debug(cnfe); + throw new BatchEnvironmentException("Unable to find class: " + di.getDefaultImpl(), cnfe); + } + + // try to create Class object for ofType class name + Class ofType; + try { + ofType = Class.forName(di.getType()); + } + catch (ClassNotFoundException cnfe) { + log.debug(cnfe); + throw new BatchEnvironmentException("Unable to find class: " + di.getType(), cnfe); + } + + // add default implementation + stream.addDefaultImplementation(defaultImplementation, ofType); + } + } + } + + /* + * Iterate over list of immutable type names and pass them to the XStream. + */ + private void addImmutableTypes(XStream stream) { + // get list of names of immutable types + List immutableTypes = config.getImmutableTypes(); + // if not null iterate over list + if (immutableTypes != null) { + for (Iterator i = immutableTypes.iterator(); i.hasNext();) { + String it = (String) i.next(); + + // try to create Class object for immutableTypeName + Class immutableType; + try { + immutableType = Class.forName(it); + } + catch (ClassNotFoundException cnfe) { + log.debug(cnfe); + throw new BatchEnvironmentException("Unable to find class: " + it, cnfe); + } + + // add immutable type + stream.addImmutableType(immutableType); + } + } + + } + + /* + * Iterate over list of OmmitField objects and register ommited fields to + * the XStream. + */ + private void setOmittedFileds(XStream stream) { + // get list of OmmitedField objects + List ommitedFields = config.getOmmitedFields(); + // if not null iterate over list + if (ommitedFields != null) { + for (Iterator i = ommitedFields.iterator(); i.hasNext();) { + OmmitedField ommitedField = (OmmitedField) i.next(); + + // register field to be ommited + try { + stream.omitField(Class.forName(ommitedField.getType()), ommitedField.getFieldName()); + } + catch (ClassNotFoundException cnfe) { + log.debug(cnfe); + throw new BatchEnvironmentException("Unable to find class: " + ommitedField.getType(), cnfe); + } + } + } + } + + /* + * Iterate over list of ImplicitCollection objects and add implicit + * collections to the XStream. XStream has 3 methods for adding implicit + * collections. Decision which method to call is based on provided settings. + */ + private void addImplicitCollections(XStream stream) { + // get list of ImplicionCollection object + List implicitCollections = config.getImplicitCollections(); + // if not null iterate over list + if (implicitCollections != null) { + for (Iterator i = implicitCollections.iterator(); i.hasNext();) { + ImplicitCollection impCol = (ImplicitCollection) i.next(); + String typeName = impCol.getOwnerType(); + + // create Class object for typeName + try { + Class ownerType = Class.forName(typeName); + + // if itemType not provided, add implicit collection for any + // unmapped xml tag + if (impCol.getItemType() == null) { + stream.addImplicitCollection(ownerType, impCol.getFieldName()); + } + else { + typeName = impCol.getItemType(); + + // create Class object for itemType + Class itemType = Class.forName(typeName); + // if itemFieldName not provided, add implicit + // collection for all items of the given itemType + if (impCol.getItemFieldName() == null) { + stream.addImplicitCollection(ownerType, impCol.getFieldName(), itemType); + } + else { + // else add implicit collection for all items of the + // given element name defined by itemFieldName + stream.addImplicitCollection(ownerType, impCol.getFieldName(), impCol.getItemFieldName(), + itemType); + } + } + } + catch (ClassNotFoundException cnfe) { + log.debug(cnfe); + throw new BatchEnvironmentException("Unable to find class: " + typeName, cnfe); + } + } + } + } + + /* + * Set mode for dealing with duplicate references. + */ + private void setMode(XStream stream) { + stream.setMode(config.getMode()); + } + + /* + * Iterate over list of ConverterProperties objects and register converters + * to XStream. + */ + private void registerConverters(XStream stream) { + // get list of ConverterProperties + List converters = config.getConverters(); + // if not null iterate over list + if (converters != null) { + for (Iterator i = converters.iterator(); i.hasNext();) { + ConverterProperties cp = (ConverterProperties) i.next(); + + // create Class object for converter class name + try { + Class converter = Class.forName(cp.getClassName()); + + // if converter type is assignable to SingleValueConverter, + // register it as SingleValueConverter + if (SingleValueConverter.class.isAssignableFrom(converter)) { + stream.registerConverter((SingleValueConverter) converter.newInstance(), cp.getPriority()); + // if converter type is assignable to Converter, + // register it as Converter + } + else if (Converter.class.isAssignableFrom(converter)) { + stream.registerConverter((Converter) converter.newInstance(), cp.getPriority()); + } + else { + throw new BatchEnvironmentException("Unable to register converter for class: " + + cp.getClassName()); + } + } + catch (ClassNotFoundException cnfe) { + log.debug(cnfe); + throw new BatchEnvironmentException("Unable to find class: " + cp.getClassName(), cnfe); + } + catch (InstantiationException ie) { + log.debug(ie); + throw new BatchEnvironmentException("Unable to instantiate class: " + cp.getClassName(), + ie); + } + catch (IllegalAccessException iae) { + log.debug(iae); + throw new BatchEnvironmentException("Unable to instantiate class: " + cp.getClassName(), + iae); + } + } + } + } + + /* + * Iterate over list of AttributeProperties objects and map XML attributes + * to fields or types. + */ + private void setAttributes(XStream stream) { + // get list of AttributeProperties objects + List attributeProperties = config.getAttributes(); + // if not null iterate over list + if (attributeProperties != null) { + for (Iterator i = attributeProperties.iterator(); i.hasNext();) { + AttributeProperties ap = (AttributeProperties) i.next(); + String fieldName = ap.getFieldName(); + + // create Class object for type name + try { + Class type = Class.forName(ap.getType()); + + // if field name is provided, map attribute to the field + if (fieldName != null) { + stream.useAttributeFor(fieldName, type); + } + else { + // else map attribute to the type + stream.useAttributeFor(type); + } + + } + catch (ClassNotFoundException cnfe) { + log.debug(cnfe); + throw new BatchEnvironmentException("Unable to find class: " + ap.getType(), cnfe); + } + } + } + } + + /* + * Iterate over list of AttributeAlias objects and configure attribute + * aliases + */ + private void setAttributeAliases(XStream stream) { + // get list of AttributeAlias objects + List attributeAliases = config.getAttributeAliases(); + // if not null iterate over list + if (attributeAliases != null) { + for (Iterator i = attributeAliases.iterator(); i.hasNext();) { + AttributeAlias alias = (AttributeAlias) i.next(); + stream.aliasAttribute(alias.getAlias(), alias.getAttributeName()); + } + } + } + + /* + * Iterate over list of FieldAlias objects and configure field aliases + */ + private void setFieldAliases(XStream stream) { + // get list of FieldAlias objects + List fieldAliases = config.getFieldAliases(); + // if not null iterate over list + if (fieldAliases != null) { + for (Iterator i = fieldAliases.iterator(); i.hasNext();) { + FieldAlias alias = (FieldAlias) i.next(); + + try { + stream.aliasField(alias.getAliasName(), Class.forName(alias.getType()), alias.getFieldName()); + } + catch (ClassNotFoundException cnfe) { + log.debug(cnfe); + throw new BatchEnvironmentException("Unable to find class: " + alias.getType(), cnfe); + } + } + } + } + + /* + * Iterate over list of TypeAlias objects and configure type aliases + */ + private void setTypeAliases(XStream stream) { + // get list of TypeAlias objects + List typeAliases = config.getTypeAliases(); + // if not null iterate over list + if (typeAliases != null) { + for (Iterator i = typeAliases.iterator(); i.hasNext();) { + TypeAlias alias = (TypeAlias) i.next(); + + try { + stream.aliasType(alias.getName(), Class.forName(alias.getType())); + } + catch (ClassNotFoundException cnfe) { + log.debug(cnfe); + throw new BatchEnvironmentException("Unable to find class: " + alias.getType(), cnfe); + } + } + } + } + + /* + * Iterate over list of ClassAlias objects and configure class aliases + */ + private void setClassAliases(XStream stream) { + // get list of ClassAlias objects + List classAliases = config.getClassAliases(); + // if not null iterate over list + if (classAliases != null) { + for (Iterator i = classAliases.iterator(); i.hasNext();) { + ClassAlias alias = (ClassAlias) i.next(); + + Class type; + try { + type = Class.forName(alias.getType()); + } + catch (ClassNotFoundException cnfe) { + log.debug(cnfe); + throw new BatchEnvironmentException("Unable to find class: " + alias.getType(), cnfe); + } + + if (alias.getDefaultImplementation() != null) { + Class defaultImplementation; + try { + defaultImplementation = Class.forName(alias.getDefaultImplementation()); + } + catch (ClassNotFoundException cnfe) { + log.debug(cnfe); + throw new BatchEnvironmentException( + "Unable to find class: " + alias.getDefaultImplementation(), cnfe); + } + + stream.alias(alias.getName(), type, defaultImplementation); + } + else { + stream.alias(alias.getName(), type); + } + } + } + } + + /* + * Create QNameMap from list of Mapping objects. + */ + private QNameMap getMapping() { + + List mappings = config.getMappings(); + + QNameMap map = new QNameMap(); + + if (mappings != null) { + for (Iterator i = mappings.iterator(); i.hasNext();) { + Mapping mapping = (Mapping) i.next(); + QName qname = new QName(mapping.getNamespaceURI(), mapping.getLocalPart(), mapping.getPrefix()); + map.registerMapping(qname, mapping.getClassName()); + } + } + + return map; + } + + /** + * Creates instance of {@link ObjectInput} which is used by + * for deserializing object from XML file. + * @param resource the input XML file + * @param encoding the encoding to use + * @return ObjectInput which will read from the provided file + * @see org.springframework.batch.io.xml.ObjectInputFactory#createObjectInput(Resource, + * java.lang.String) + */ + public ObjectInput createObjectInput(Resource resource, String encoding) { + + ObjectInput wrapper; + + XStream stream = new XStream(); + setUpXStream(stream); + + try { + XMLInputFactory xmlif = XMLInputFactory.newInstance(); + XMLStreamReader xmlReader = xmlif.createXMLStreamReader(resource.getInputStream(), encoding); + + StaxReader reader = new StaxReader(getMapping(), xmlReader); + java.io.ObjectInput input = stream.createObjectInputStream(reader); + wrapper = new ObjectInputWrapper(xmlReader, input); + } + catch (XMLStreamException xse) { + log.error(xse); + throw new DataAccessResourceFailureException("Unable to get XML reader", xse); + } + catch (IOException ioe) { + log.error(ioe); + throw new DataAccessResourceFailureException("Unable to get ObjectInputStream", ioe); + } + + return wrapper; + } + + /** + * Creates instance of {@link ObjectOutput} which is used by + * for serializing object to XML file. + * @param resource the output XML file + * @param encoding the encoding to use + * @return ObjectOutput which will write to the provided file + * @see org.springframework.batch.io.xml.ObjectOutputFactory#createObjectOutput(Resource, + * java.lang.String) + */ + public ObjectOutput createObjectOutput(Resource resource, String encoding) { + + ObjectOutput wrapper; + FileChannel channel; + + XStream stream = new XStream(); + setUpXStream(stream); + + try { + XMLOutputFactory xmlof = XMLOutputFactory.newInstance(); + + FileOutputStream os; + + try { + os = new FileOutputStream(resource.getFile(), true); + channel = os.getChannel(); + } + catch (FileNotFoundException ioe) { + log.error(ioe); + throw new DataAccessResourceFailureException("Unable to write to file resource: [" + resource + "]", + ioe); + } + + XMLStreamWriter xmlWriter = xmlof.createXMLStreamWriter(os, encoding); + + StaxWriter writer = new StaxWriter(getMapping(), xmlWriter); + String rootElementName = config.getRootElementName(); + java.io.ObjectOutput output; + if (rootElementName != null) { + output = stream.createObjectOutputStream(writer, rootElementName); + } + else { + output = stream.createObjectOutputStream(writer); + } + + writeAttributes(xmlWriter, config.getRootElementAttributes()); + wrapper = new ObjectOutputWrapper(xmlWriter, channel, output); + + } + catch (XMLStreamException xse) { + log.error(xse); + throw new DataAccessResourceFailureException("Unable to get XML writer", xse); + } + catch (IOException ioe) { + log.error(ioe); + throw new DataAccessResourceFailureException("Unable to get ObjectOutputStream", ioe); + } + + return wrapper; + } + + /* + * Writes attributes to current xml element + * + * @param attributes map of attributes (key, value) @throws + * XMLStreamException + */ + private void writeAttributes(XMLStreamWriter xmlWriter, Map attributes) throws XMLStreamException { + if ((attributes != null) && !attributes.isEmpty()) { + + for (Iterator i = attributes.entrySet().iterator(); i.hasNext();) { + Map.Entry entry = (Map.Entry) i.next(); + xmlWriter.writeAttribute((String) entry.getKey(), (String) entry.getValue()); + } + } + } + + /** + * Implementation of {@link ObjectInput} which wraps + * {@link java.io.ObjectInput} and {@link XMLStreamReader} (which is StAX + * parser) objects. Each of these objects handles the same input file on + * different level and provides different set of methods: + *
    + *
  • java.io.ObjectInput object is used for reading mapped objects
  • + *
  • XMLStreamReader object is used for getting actual position (line + * number) within xml file
  • + *
+ */ + public static class ObjectInputWrapper implements ObjectInput { + + java.io.ObjectInput input; + + XMLStreamReader reader; + + /** + * Postprocessing after restart. Current implementation does nothing. + * @param data + * @see org.springframework.batch.io.xml.ObjectInput#afterRestart(java.lang.Object) + */ + public void afterRestart(Object data) { + } + + /** + * Constructor. + * + * @param reader the xml stream reader + * @param input the object input pointing to same file as reader + */ + public ObjectInputWrapper(XMLStreamReader reader, java.io.ObjectInput input) { + this.input = input; + this.reader = reader; + } + + /** + * Close the object input. It closes all wraped input streams + * @see org.springframework.batch.io.xml.ObjectInput#close() + */ + public void close() { + try { + input.close(); + reader.close(); + } + catch (XMLStreamException xse) { + log.error(xse); + throw new DataAccessResourceFailureException("Unable to close XML Input Source", xse); + } + catch (IOException ioe) { + log.error(ioe); + throw new DataAccessResourceFailureException("Unable to close ObjectInputStream", ioe); + } + } + + /** + * Return the current line number in the input stream. + * @return the current line number + * @see org.springframework.batch.io.xml.ObjectInput#position() + */ + public long position() { + Location location = reader.getLocation(); + return location.getLineNumber(); + } + + /** + * Read and return an object. + * @return the object read from the stream + * @throws ClassNotFoundException If the class of a serialized bject + * cannot be found. + * @throws IOException If any of the usual Input/Output related + * exceptions occur. + * @see org.springframework.batch.io.xml.ObjectInput#readObject() + */ + public Object readObject() throws ClassNotFoundException, IOException { + return input.readObject(); + } + + } + + /** + * Implementation of ObjectOutput which wraps java.io.ObjectOutput, + * XMLStreamWriter and FileChannel objects. Each of these objects handles + * the same output file on different level and provides different set of + * methods: + *
    + *
  • java.io.ObjectOutput object is used for writing java objects to xml + * output file
  • + *
  • FileChannel object is used for file manipulation (truncate, + * position, size)
  • + *
  • XMLStreamWriter object is used for writing XML elements directly to + * XML output stream
  • + *
+ */ + public static class ObjectOutputWrapper implements ObjectOutput { + + java.io.ObjectOutput output; + + XMLStreamWriter writer; + + FileChannel channel; + + /** + * Constructor. + * + * @param writer the xml stream writer + * @param channel the file channel pointing to same file as writer + * @param output the object output pointing to same file as writer + */ + public ObjectOutputWrapper(XMLStreamWriter writer, FileChannel channel, java.io.ObjectOutput output) { + this.writer = writer; + this.channel = channel; + this.output = output; + } + + /** + * Postprocessing after restart. It removes redundant xml header. + * @param data java.lang.Long restart file position + * @see org.springframework.batch.io.xml.ObjectOutput#afterRestart(java.lang.Object) + */ + public void afterRestart(Object data) { + + long offset = ((Long) data).longValue(); + + // When xmlWriter is initialized, it always writes xml header and + // opening tag of root element + // but this is unwanted, because currently we are restarting job. + // Header and opening + // tag of root element have been already written at the beginning of + // job processing. + // Current output file looks like this: + + // 1. + // 2. + // 3-n. .... .... + // n+1. + // n+2. + // n+3. + // n+4. + writer.writeComment(""); + // Now we flush output stream. Lines n+2,n+3,n+4 are now written + // to the file. + output.flush(); + } + catch (IOException ioe) { + log.error(ioe); + throw new DataAccessResourceFailureException("Unable to write to ObjectOutputStream", ioe); + } + catch (XMLStreamException xse) { + log.error(xse); + throw new DataAccessResourceFailureException("Unable to get XML writer", xse); + } + + // Finally we truncate file size to lastMarkedByteOffsetPosition. + // This will remove lines n+1 .. n+4. + truncate(offset); + position(offset); + } + + /** + * Close the object output, which means to close all wrapped output + * streams. + * @see org.springframework.batch.io.xml.ObjectOutput#close() + */ + public void close() { + try { + output.close(); + writer.close(); + channel.close(); + } + catch (XMLStreamException xse) { + log.error(xse); + throw new DataAccessResourceFailureException("Unable to close XML Output Source", xse); + } + catch (IOException ioe) { + log.error(ioe); + throw new DataAccessResourceFailureException("Unable to close ObjectOutputStream", ioe); + } + } + + /** + * Flush the object output. This will write any buffered output bytes. + * @see org.springframework.batch.io.xml.ObjectOutput#flush() + */ + public void flush() { + try { + output.flush(); + } + catch (IOException ioe) { + log.debug(ioe); + throw new DataAccessResourceFailureException("An error occured while writing to XmlOutputSource", ioe); + } + } + + /** + * Retrieve file position. + * @return File position, a non-negative integer counting the number of + * bytes from the beginning of the file to the current position + * @see org.springframework.batch.io.xml.ObjectOutput#position() + */ + public long position() { + long position = 0; + + // flush buffer before getting position + flush(); + + try { + position = channel.position(); + } + catch (IOException ioe) { + log.debug(ioe); + throw new DataAccessResourceFailureException("An error occured while writing to XmlOutputSource", ioe); + } + return position; + } + + /** + * Set the file position. + * @param newPosition The new position, a non-negative integer counting + * the number of bytes from the beginning of the file + * @see org.springframework.batch.io.xml.ObjectOutput#position(long) + */ + public void position(long newPosition) { + try { + channel.position(newPosition); + } + catch (IOException ioe) { + log.error(ioe); + throw new DataAccessResourceFailureException("An error occured while writing to XmlOutputSource", ioe); + } + } + + /** + * Returns the current size of the file. + * @return The current size of the file, measured in bytes + * @see org.springframework.batch.io.xml.ObjectOutput#size() + */ + public long size() { + long size; + + try { + size = channel.size(); + } + catch (IOException ioe) { + log.debug(ioe); + throw new DataAccessResourceFailureException("An error occured while writing to XmlOutputSource", ioe); + } + return size; + } + + /** + * Truncates the file to the given size. + * @param size The new size, a non-negative byte count + * @see org.springframework.batch.io.xml.ObjectOutput#truncate(long) + */ + public void truncate(long size) { + try { + channel.truncate(size); + } + catch (IOException ioe) { + log.error(ioe); + throw new DataAccessResourceFailureException("An error occured while writing to XmlOutputSource", ioe); + } + } + + /** + * Write object to the underlying stream. + * @param obj the object to write + * @throws IOException Any of the usual Input/Output related exceptions. + * @see org.springframework.batch.io.xml.ObjectOutput#writeObject(java.lang.Object) + */ + public void writeObject(Object obj) throws IOException { + output.writeObject(obj); + } + } +} diff --git a/infrastructure/src/main/java/org/springframework/batch/io/xml/xstream/XmlInputOutput.dnx b/infrastructure/src/main/java/org/springframework/batch/io/xml/xstream/XmlInputOutput.dnx new file mode 100644 index 000000000..eac076aed --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/xml/xstream/XmlInputOutput.dnx @@ -0,0 +1,569 @@ + + +?> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/infrastructure/src/main/java/org/springframework/batch/io/xml/xstream/package.html b/infrastructure/src/main/java/org/springframework/batch/io/xml/xstream/package.html new file mode 100644 index 000000000..99b97f10e --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/io/xml/xstream/package.html @@ -0,0 +1,7 @@ + + +

+Infrastructure implementations of io xml xstream concerns. +

+ + diff --git a/infrastructure/src/main/java/org/springframework/batch/item/FailedItemIdentifier.java b/infrastructure/src/main/java/org/springframework/batch/item/FailedItemIdentifier.java new file mode 100644 index 000000000..21e30ff3c --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/item/FailedItemIdentifier.java @@ -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); + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/item/ItemProcessor.java b/infrastructure/src/main/java/org/springframework/batch/item/ItemProcessor.java new file mode 100644 index 000000000..2f50da9d5 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/item/ItemProcessor.java @@ -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; + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/item/ItemProvider.java b/infrastructure/src/main/java/org/springframework/batch/item/ItemProvider.java new file mode 100644 index 000000000..a9403c569 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/item/ItemProvider.java @@ -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. + *
+ * + * 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 null when all input data is exhausted.
+ * + * 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.
+ * + * 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 + * must return null 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); +} diff --git a/infrastructure/src/main/java/org/springframework/batch/item/ResourceLifecycle.java b/infrastructure/src/main/java/org/springframework/batch/item/ResourceLifecycle.java new file mode 100644 index 000000000..ef79b044b --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/item/ResourceLifecycle.java @@ -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 initialization before they can be + * used and need to free resources after they are no longer used. + */ +public interface ResourceLifecycle { + /** + * This method will be invoked at the start of processing to allow + * initialization of resources. + * + */ + public void open(); + + /** + * This method will be called invoked after the completion of each step and + * the implementing class should close all managed resources. + * + */ + public void close(); +} diff --git a/infrastructure/src/main/java/org/springframework/batch/item/exception/UnexpectedInputException.java b/infrastructure/src/main/java/org/springframework/batch/item/exception/UnexpectedInputException.java new file mode 100644 index 000000000..3bdbcad9a --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/item/exception/UnexpectedInputException.java @@ -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); + } +} diff --git a/infrastructure/src/main/java/org/springframework/batch/item/exception/package.html b/infrastructure/src/main/java/org/springframework/batch/item/exception/package.html new file mode 100644 index 000000000..0fc74230d --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/item/exception/package.html @@ -0,0 +1,7 @@ + + +

+Infrastructure implementations of item exception concerns. +

+ + diff --git a/infrastructure/src/main/java/org/springframework/batch/item/package.html b/infrastructure/src/main/java/org/springframework/batch/item/package.html new file mode 100644 index 000000000..4d3f72cbf --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/item/package.html @@ -0,0 +1,7 @@ + + +

+Infrastructure implementations of item concerns. +

+ + diff --git a/infrastructure/src/main/java/org/springframework/batch/item/provider/AbstractFieldSetItemProvider.java b/infrastructure/src/main/java/org/springframework/batch/item/provider/AbstractFieldSetItemProvider.java new file mode 100644 index 000000000..2976fd313 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/item/provider/AbstractFieldSetItemProvider.java @@ -0,0 +1,74 @@ +/* + * 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.io.file.FieldSet; +import org.springframework.batch.io.file.FieldSetInputSource; +import org.springframework.batch.item.ItemProvider; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.util.Assert; + +/** + * {@link ItemProvider} based on {@link FieldSetInputSource}. Not restartable + * or transaction aware, but can be used by multiple concurrent threads, so + * useful as a base class or for testing. + * + * @author Rob Harrop + * @author Dave Syer + */ +public abstract class AbstractFieldSetItemProvider extends AbstractItemProvider implements InitializingBean { + + protected FieldSetInputSource source; + + private Object mutex = new Object(); + + /** + * Make sure mandatory properties are set. + * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() + */ + public void afterPropertiesSet() throws Exception { + Assert.notNull(source); + } + + /** + * Setter for the input source. Mandatory with no default. + * @param source + */ + public void setSource(FieldSetInputSource source) { + this.source = source; + } + + /** + * Get the next field set from the input source, and then call + * {@link #transform(FieldSet)} on the result. Synchronizes access to the + * input source using an internal mutex as a lock. + * + * @see org.springframework.batch.item.ItemProvider#next() + */ + public final Object next() { + FieldSet fieldSet; + synchronized (mutex) { + fieldSet = this.source.readFieldSet(); + if (fieldSet != null) { + return transform(fieldSet); + } + } + return null; + } + + protected abstract Object transform(FieldSet fieldSet); +} diff --git a/infrastructure/src/main/java/org/springframework/batch/item/provider/AbstractItemProvider.java b/infrastructure/src/main/java/org/springframework/batch/item/provider/AbstractItemProvider.java new file mode 100644 index 000000000..7ca6cc20a --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/item/provider/AbstractItemProvider.java @@ -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; + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/item/provider/JmsItemProvider.java b/infrastructure/src/main/java/org/springframework/batch/item/provider/JmsItemProvider.java new file mode 100644 index 000000000..d150b57f1 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/item/provider/JmsItemProvider.java @@ -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; + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/item/provider/ListItemProvider.java b/infrastructure/src/main/java/org/springframework/batch/item/provider/ListItemProvider.java new file mode 100644 index 000000000..58f027c68 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/item/provider/ListItemProvider.java @@ -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; + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/item/provider/package.html b/infrastructure/src/main/java/org/springframework/batch/item/provider/package.html new file mode 100644 index 000000000..3fde969fe --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/item/provider/package.html @@ -0,0 +1,7 @@ + + +

+Infrastructure implementations of item provider concerns. +

+ + diff --git a/infrastructure/src/main/java/org/springframework/batch/item/validator/SpringValidator.java b/infrastructure/src/main/java/org/springframework/batch/item/validator/SpringValidator.java new file mode 100644 index 000000000..e6e4a1100 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/item/validator/SpringValidator.java @@ -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(); + } +} diff --git a/infrastructure/src/main/java/org/springframework/batch/item/validator/Validator.java b/infrastructure/src/main/java/org/springframework/batch/item/validator/Validator.java new file mode 100644 index 000000000..0d1830e3b --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/item/validator/Validator.java @@ -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.item.validator; + +import org.springframework.batch.io.exception.ValidationException; + +/** + * Interface used to validate objects. + * + * @author tomas.slanina + * + */ +public interface Validator { + /** + * Method used to validate if the value is valid. + * + * @param value object to be validated + */ + void validate(Object value) throws ValidationException; +} diff --git a/infrastructure/src/main/java/org/springframework/batch/item/validator/package.html b/infrastructure/src/main/java/org/springframework/batch/item/validator/package.html new file mode 100644 index 000000000..26c82e9c1 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/item/validator/package.html @@ -0,0 +1,7 @@ + + +

+Infrastructure implementations of item validator concerns. +

+ + diff --git a/infrastructure/src/main/java/org/springframework/batch/package.html b/infrastructure/src/main/java/org/springframework/batch/package.html new file mode 100644 index 000000000..5137c3e68 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/package.html @@ -0,0 +1,7 @@ + + +

+Infrastructure implementations of . concerns. +

+ + diff --git a/infrastructure/src/main/java/org/springframework/batch/repeat/CompletionPolicy.java b/infrastructure/src/main/java/org/springframework/batch/repeat/CompletionPolicy.java new file mode 100644 index 000000000..84e9b314b --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/repeat/CompletionPolicy.java @@ -0,0 +1,79 @@ +/* + * 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.repeat; + +/** + * Interface for batch completion policies, to enable batch operations to + * strategise normal completion conditions. Stateful implementations of batch + * iterators should only update state using the update method. If you + * need custom behaviour consider extending an existing implementation or using + * the composite provided. + * + * @author Dave Syer + * + */ +public interface CompletionPolicy { + + /** + * Determine whether a batch is complete given the latest result from the + * callback. If this method returns true then + * {@link #isComplete(RepeatContext)} should also (but not necessarily vice + * versa, since the answer here depends on the result). + * + * @param context the current batch context. + * @param result the result of the latest batch item processing. + * + * @return true if the batch should terminate. + * + * @see #isComplete(RepeatContext) + */ + boolean isComplete(RepeatContext context, Object result); + + /** + * Allow policy to signal completion according to internal state, without + * having to wait for the callback to complete. + * + * @param context the current batch context. + * + * @return true if the batch should terminate. + */ + boolean isComplete(RepeatContext context); + + /** + * Create a new context for the execution of a batch. N.B. implementations + * should not return the parent from this method - they must + * create a new context to meet the specific needs of the policy. The best + * way to do this might be to override an existing implementation and use + * the {@link RepeatContext} to store state in its attributes. + * + * @param parent the current context if one is already in progress. + * @return a context object that can be used by the implementation to store + * internal state for a batch. + */ + RepeatContext start(RepeatContext parent); + + /** + * Give implementations the opportunity to update the state of the current + * batch. Will be called once per callback, after it has been + * launched, but not necessarily after it completes (if the batch is + * asynchronous). + * + * @param context the value returned by start. + */ + void update(RepeatContext context); + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/repeat/ExitStatus.java b/infrastructure/src/main/java/org/springframework/batch/repeat/ExitStatus.java new file mode 100644 index 000000000..f6bf3c602 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/repeat/ExitStatus.java @@ -0,0 +1,125 @@ +/* + * 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.repeat; + +/** + * Value object used to carry information about the status of a + * {@link RepeatOperations}. + * + * @author Dave Syer + * + */ +public class ExitStatus { + + /** + * Convenient constant value representing unfinished processing. + */ + public static ExitStatus CONTINUABLE = new ExitStatus(true); + + /** + * Convenient constant value representing finished processing. + */ + public static ExitStatus FINISHED = new ExitStatus(false); + + /** + * Convenient constant value representing finished processing with an error. + */ + public static ExitStatus FAILED = new ExitStatus(false, -1); + + private final boolean continuable; + + private final int exitCode; + + public ExitStatus(boolean continuable) { + this(continuable, 0); + } + + public ExitStatus(boolean continuable, int exitCode) { + super(); + this.continuable = continuable; + this.exitCode = exitCode; + } + + /** + * Flag to signal that processing can continue. This is distinct from any + * flag that might indicate that a batch is complete, or terminated, since a + * batch might be only a small part of a larger whole, which is still not + * finished. + * + * @return true if processing can continue. + */ + public boolean isContinuable() { + return continuable; + } + + /** + * Getter for the exit code (defaults to 0). + * @return the exit code. + */ + public int getExitCode() { + return exitCode; + } + + /** + * Create a new {@link ExitStatus} with a logical combination of the + * continuable flag. + * @param continuable true if the caller thinks it is safe to continue. + * @return a new {@link ExitStatus} with {@link #isContinuable()} the + * logical and of the current value and the argument provided. + */ + public ExitStatus and(boolean continuable) { + return and(new ExitStatus(continuable)); + } + + /** + * Create a new {@link ExitStatus} with a logical combination of the + * continuable flag and adding the other exit code. + * @param other an other {@link ExitStatus}. + * @return a new {@link ExitStatus} with {@link #isContinuable()} the + * logical and of the current value and the other's. + */ + public ExitStatus and(ExitStatus other) { + return new ExitStatus(this.continuable && other.isContinuable(), exitCode).addExitCode(other.getExitCode()); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#toString() + */ + public String toString() { + return "continuable=" + continuable + ";exitCode=" + exitCode; + } + + /** + * Duplicates the existing status except that a new exit code is added as + * per the argument. If both codes (the current and the new one provided) + * are negative then the lower of the two is returned, otherwise the higher + * or the two. + * + * @param exitCode the new value of the exitCode + * @return a new {@link ExitStatus} instance + */ + public ExitStatus addExitCode(int exitCode) { + if (this.exitCode < 0 || exitCode < 0) { + exitCode = Math.min(this.exitCode, exitCode); + } + else { + exitCode = Math.max(this.exitCode, exitCode); + } + return new ExitStatus(this.continuable, exitCode); + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/repeat/RepeatCallback.java b/infrastructure/src/main/java/org/springframework/batch/repeat/RepeatCallback.java new file mode 100644 index 000000000..5fc1cfd72 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/repeat/RepeatCallback.java @@ -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.repeat; + +/** + * Callback interface for batch operations. Many simple batch processes will be + * able to use off-the-shelf implementations of this interface, e.g. + * {@link org.springframework.batch.repeat.callback.ItemProviderRepeatCallback}, + * enabling the batch developer to concentrate on business logic. + * + * @see RepeatOperations + * + * @author Dave Syer + * + */ +public interface RepeatCallback { + + /** + * Implementations return true if they can continue processing - e.g. there + * is a datasource that is not yet exhausted. Exceptions are not necessarily + * fatal - the batch might continue depending on the Exception type and the + * implementation of the caller. + * + * @param context the current context passed in by the caller. + * @return true if there is (or may be) more data to process. + * @throws Exception if there is a problem with the processing. + */ + ExitStatus doInIteration(RepeatContext context) throws Exception; +} diff --git a/infrastructure/src/main/java/org/springframework/batch/repeat/RepeatContext.java b/infrastructure/src/main/java/org/springframework/batch/repeat/RepeatContext.java new file mode 100644 index 000000000..4ece5f36e --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/repeat/RepeatContext.java @@ -0,0 +1,91 @@ +/* + * 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.repeat; + +import org.springframework.core.AttributeAccessor; + +/** + * Base interface for context which controls the state and completion / + * termination of a batch step. A new context is created for each call to the + * {@link RepeatOperations}. Within a batch callback code can communicate via + * the {@link AttributeAccessor} interface. + * + * @author Dave Syer + * + * @see RepeatOperations#iterate(RepeatCallback) + * + */ +public interface RepeatContext extends AttributeAccessor { + + /** + * If batches are nested, then the inner batch will be created with the + * outer one as a parent. This is an accessor for the parent if it exists. + * + * @return the parent context or null if there is none + */ + RepeatContext getParent(); + + /** + * Public access to a counter for the number of operations attempted. + * + * @return the number of batch operations started. + */ + int getStartedCount(); + + /** + * Signal to the framework that the current batch should complete normally, + * independent of the current {@link CompletionPolicy}. + */ + void setCompleteOnly(); + + /** + * Public accessor for the complete flag. + */ + boolean isCompleteOnly(); + + /** + * Signal to the framework that the current batch should complete + * abnormally, independent of the current {@link CompletionPolicy}. + */ + void setTerminateOnly(); + + /** + * Public accessor for the termination flag. If this flag is set then the + * complete flag will also be. + */ + boolean isTerminateOnly(); + + /** + * Register a callback to be executed on close, associated with the + * attribute having the given name. The {@link Runnable} callback should not + * throw any exceptions. + * + * @param name the name of the attribute to associated this callback with. + * If this attribute is removed the callback should never be called. + * @param callback a {@link Runnable} to execute when the context is closed. + */ + void registerDestructionCallback(String name, Runnable callback); + + /** + * Allow resources to be cleared, especially in destruction callbacks. + * Implementations should ensure that any registered destruction callbacks + * are executed here, as long as the corresponding attribute is still + * available. + */ + void close(); + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/repeat/RepeatInterceptor.java b/infrastructure/src/main/java/org/springframework/batch/repeat/RepeatInterceptor.java new file mode 100644 index 000000000..4bbfe005f --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/repeat/RepeatInterceptor.java @@ -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.repeat; + +/** + * Interface for interceptors in the batch process. Implementers can provide + * enhance the behaviour of a batch in small cross-cutting modules. The + * framework provides callbacks at key points in the processing. + * + * @author Dave Syer + * + */ +public interface RepeatInterceptor { + /** + * Called by the framework before each batch item. Implementers can halt a + * batch by setting the complete flag on the context. + * + * @param context the current batch context. + */ + void before(RepeatContext context); + + /** + * Called by the framework after each item has been processed, including if + * the item processing results in an exception, in which case result is an + * Exception. This method is called as soon as the result is known, whereas + * {@link #onError(RepeatContext, Throwable)} is only guaranteed to be + * called at some time after the failure occurred. + * + * @param context the current batch context + * @param result the result of the callback item - ExitStatus in normal + * circumstances, but might be null or an Exception in abnormal cases. + */ + void after(RepeatContext context, Object result); + + /** + * Called once at the start of a complete batch, before any items are + * processed. Implementers can use this method to acquire any resources that + * might be needed during processing. Implementers can halt the current + * operation by setting the complete flag on the context. To halt all + * enclosing batches (the whole job), the would need to use the parent + * context (recursively). + * + * @param context the current batch context + */ + void open(RepeatContext context); + + /** + * Called at the end of a batch if any callback fails by throwing an + * exception. There will be one call to this method for each exception + * thrown during a repeat operation (e.g. a chunk).
+ * + * There is no need to re-throw the exception here - that will be done by + * the enclosing framework. + * + * @param context the current batch context + * @param e the error that was encountered in an item callback. + */ + void onError(RepeatContext context, Throwable e); + + /** + * Called once at the end of a complete batch, after normal or abnormal + * completion (i.e. even after an exception). Implementers can use this + * method to clean up any resources. + * + * @param context the current batch context. + * @return TODO + */ + ExitStatus close(RepeatContext context); +} diff --git a/infrastructure/src/main/java/org/springframework/batch/repeat/RepeatOperations.java b/infrastructure/src/main/java/org/springframework/batch/repeat/RepeatOperations.java new file mode 100644 index 000000000..5adc82ef5 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/repeat/RepeatOperations.java @@ -0,0 +1,46 @@ +/* + * 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.repeat; + +/** + * The main interface providing access to batch operations. The batch client is + * the {@link RepeatCallback}, where a single item or record is processed. The + * batch behaviour, boundary conditions, transactions etc, are dealt with by the + * {@link RepeatOperations} in such as way that the client does not need to know + * about them. The client may have access to framework abstractions, like + * template data sources, but these should work the same whether they are in a + * batch or not. + * + * @author Dave Syer + * + */ +public interface RepeatOperations { + + /** + * Execute the callback repeatedly, until a decision can be made to + * complete. The decision about how many times to execute or when to + * complete, and what to do in the case of an error is delegated to a + * {@link CompletionPolicy}. + * + * @param callback the batch callback. + * @return the aggregate of the result of all the callback operations. An + * indication of whether the {@link RepeatOperations} can continue + * processing if this method is called again. + */ + ExitStatus iterate(RepeatCallback callback); + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/repeat/aop/RepeatOperationsInterceptor.java b/infrastructure/src/main/java/org/springframework/batch/repeat/aop/RepeatOperationsInterceptor.java new file mode 100644 index 000000000..03b460afe --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/repeat/aop/RepeatOperationsInterceptor.java @@ -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.repeat.aop; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.springframework.batch.repeat.RepeatCallback; +import org.springframework.batch.repeat.RepeatContext; +import org.springframework.batch.repeat.RepeatOperations; +import org.springframework.batch.repeat.ExitStatus; +import org.springframework.batch.repeat.exception.RepeatException; +import org.springframework.batch.repeat.support.RepeatTemplate; +import org.springframework.util.Assert; + +/** + * @author Dave Syer + * @since 2.1 + */ +public class RepeatOperationsInterceptor implements MethodInterceptor { + + private RepeatOperations batchTempate = new RepeatTemplate(); + + /** + * Setter for the {@link RepeatOperations}. + * @param batchTempate + * @throws IllegalArgumentException if the argument is null. + */ + public void setRepeatOperations(RepeatOperations batchTempate) { + Assert.notNull(batchTempate, "'batchTemplate' cannot be null."); + this.batchTempate = batchTempate; + } + + /** + * Invoke the proceeding method call repeatedly, according to the properties + * of the injected {@link RepeatOperations}. + * + * @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation) + */ + public Object invoke(final MethodInvocation methodInvocation) throws Throwable { + + batchTempate.iterate(new RepeatCallback() { + + public ExitStatus doInIteration(RepeatContext context) throws Exception { + try { + // N.B. discards return value if there is one + return new ExitStatus(methodInvocation.proceed() != null); + } + catch (Throwable e) { + if (e instanceof Exception) { + throw (Exception) e; + } + else { + throw new RepeatException("Unexpected error in batch interceptor", e); + } + } + } + + }); + + return null; + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/repeat/aop/package.html b/infrastructure/src/main/java/org/springframework/batch/repeat/aop/package.html new file mode 100644 index 000000000..bb348cede --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/repeat/aop/package.html @@ -0,0 +1,7 @@ + + +

+Infrastructure implementations of repeat aop concerns. +

+ + diff --git a/infrastructure/src/main/java/org/springframework/batch/repeat/callback/ItemProviderRepeatCallback.java b/infrastructure/src/main/java/org/springframework/batch/repeat/callback/ItemProviderRepeatCallback.java new file mode 100644 index 000000000..4c01ae319 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/repeat/callback/ItemProviderRepeatCallback.java @@ -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.repeat.callback; + +import org.springframework.batch.item.ItemProcessor; +import org.springframework.batch.item.ItemProvider; +import org.springframework.batch.repeat.RepeatCallback; +import org.springframework.batch.repeat.RepeatContext; +import org.springframework.batch.repeat.ExitStatus; + +/** + * Simple wrapper for two business interfaces: get the next item from a + * ItemProvider and apply the given processor to the result (if not null). + * + * @author Dave Syer + * + */ +public class ItemProviderRepeatCallback implements RepeatCallback { + + ItemProvider provider; + + ItemProcessor processor; + + public ItemProviderRepeatCallback(ItemProvider provider, ItemProcessor processor) { + super(); + this.provider = provider; + this.processor = processor; + } + + /** + * Default processor is null, in which case we do nothing - subclasses can + * extend this behaviour, but must be careful to actually exhaust the + * provider by calling next(). + * @param provider + */ + public ItemProviderRepeatCallback(ItemProvider provider) { + this(provider, null); + } + + /** + * Use the processor to process the next item if there is one. Return the + * item processed, or null if nothing was available. + * @see org.springframework.batch.repeat.RepeatCallback#doInIteration(org.springframework.batch.item.BatchContextAdapter) + * @param context the current context. + * @return null if the data provider is exhausted. + */ + public ExitStatus doInIteration(RepeatContext context) throws Exception { + + ExitStatus result = ExitStatus.FINISHED; + Object item = provider.next(); + + if (processor != null) { + if (item != null) { + processor.process(item); + result = ExitStatus.CONTINUABLE; + } + item = null; + } + + return result; + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/repeat/callback/NestedRepeatCallback.java b/infrastructure/src/main/java/org/springframework/batch/repeat/callback/NestedRepeatCallback.java new file mode 100644 index 000000000..a0883bcb8 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/repeat/callback/NestedRepeatCallback.java @@ -0,0 +1,61 @@ +/* + * 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.repeat.callback; + +import org.springframework.batch.repeat.RepeatCallback; +import org.springframework.batch.repeat.RepeatContext; +import org.springframework.batch.repeat.RepeatOperations; +import org.springframework.batch.repeat.ExitStatus; + +/** + * Callback that delegates to another callback, via a {@link RepeatOperations} instance. + * Useful when nesting or composing batches in one another, e.g. for breaking a + * batch down into chunks. + * + * @author Dave Syer + * + */ +public class NestedRepeatCallback implements RepeatCallback { + + private RepeatOperations template; + + private RepeatCallback callback; + + /** + * Constructor setting mandatory fields. + * + * @param template the {@link RepeatOperations} to use when calling the delegate + * callback + * @param callback the {@link RepeatCallback} delegate + */ + public NestedRepeatCallback(RepeatOperations template, RepeatCallback callback) { + super(); + this.template = template; + this.callback = callback; + } + + /** + * Simply calls template.execute(callback). Clients can use this to repeat a + * batch process, or to break a process up into smaller chunks (e.g. to + * change the transaction boundaries). + * + * @see org.springframework.batch.repeat.RepeatCallback#doInIteration(org.springframework.batch.support.BatchContextAdapter) + */ + public ExitStatus doInIteration(RepeatContext context) throws Exception { + return template.iterate(callback); + } +} diff --git a/infrastructure/src/main/java/org/springframework/batch/repeat/callback/package.html b/infrastructure/src/main/java/org/springframework/batch/repeat/callback/package.html new file mode 100644 index 000000000..a25a6cf38 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/repeat/callback/package.html @@ -0,0 +1,7 @@ + + +

+Infrastructure implementations of repeat callback concerns. +

+ + diff --git a/infrastructure/src/main/java/org/springframework/batch/repeat/context/RepeatContextCounter.java b/infrastructure/src/main/java/org/springframework/batch/repeat/context/RepeatContextCounter.java new file mode 100644 index 000000000..5b1bbf080 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/repeat/context/RepeatContextCounter.java @@ -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.repeat.context; + +import org.springframework.batch.repeat.RepeatContext; +import org.springframework.util.Assert; + +import edu.emory.mathcs.backport.java.util.concurrent.atomic.AtomicInteger; + +/** + * Helper class for policies that need to count the number of occurrences of + * some event (e.g. an exception type in the context) in the scope of a batch. + * The value of the counter can be stored between batches in a nested context, + * so that the termination decision is based on the aggregate of a number of + * sibling batches. + * + * @author Dave Syer + * + */ +public class RepeatContextCounter { + + private String countKey; + + /** + * Flag to indicate whether the count is stored at the level of the parent + * context, or just local to the current context. Default value is false. + */ + private boolean useParent = false; + + private RepeatContext context; + + /** + * Increment the counter. + * + * @param delta the amount by which to increment the counter. + */ + final public void increment(int delta) { + AtomicInteger count = getCounter(); + count.addAndGet(delta); + } + + /** + * Increment by 1. + */ + final public void increment() { + increment(1); + } + + /** + * Convenience constructor with {@link #useParent}=false. + * @param context the current context. + * @param countKey the key to use to store the counter in the context. + */ + public RepeatContextCounter(RepeatContext context, String countKey) { + this(context, countKey, false); + } + + /** + * Construct a new {@link RepeatContextCounter}. + * + * @param context the current context. + * @param countKey the key to use to store the counter in the context. + * @param useParent true if the counter is to be shared between siblings. + * The state will be stored in the parent of the context (if it exists) + * instead of the context itself. + */ + public RepeatContextCounter(RepeatContext context, String countKey, boolean useParent) { + + super(); + + Assert.notNull(context, "The context must be provided"); + + this.countKey = countKey; + this.useParent = useParent; + + RepeatContext parent = context.getParent(); + + if (this.useParent && parent != null) { + this.context = parent; + } + else { + this.context = context; + } + if (!this.context.hasAttribute(countKey)) { + this.context.setAttribute(countKey, new AtomicInteger(0)); + } + + } + + /** + * @return the current value of the counter + */ + public int getCount() { + return getCounter().intValue(); + } + + private AtomicInteger getCounter() { + return ((AtomicInteger) context.getAttribute(countKey)); + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/repeat/context/RepeatContextSupport.java b/infrastructure/src/main/java/org/springframework/batch/repeat/context/RepeatContextSupport.java new file mode 100644 index 000000000..d55bca3dd --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/repeat/context/RepeatContextSupport.java @@ -0,0 +1,172 @@ +/* + * 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.repeat.context; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.batch.repeat.RepeatContext; + +public class RepeatContextSupport extends SynchronizedAttributeAccessor implements RepeatContext { + + private RepeatContext parent; + + private int count; + + private volatile boolean completeOnly; + + private volatile boolean terminateOnly; + + private Map callbacks = new HashMap(); + + /** + * Constructor for {@link RepeatContextSupport}. The parent can be null, + * but should be set to the enclosing repeat context if there is one, e.g. + * if this context is an inner loop. + * @param parent + */ + public RepeatContextSupport(RepeatContext parent) { + super(); + this.parent = parent; + } + + /* + * (non-Javadoc) + * @see org.springframework.batch.repeat.RepeatContext#isCompleteOnly() + */ + public boolean isCompleteOnly() { + return completeOnly; + } + + /* + * (non-Javadoc) + * @see org.springframework.batch.repeat.RepeatContext#setCompleteOnly() + */ + public void setCompleteOnly() { + completeOnly = true; + } + + /* + * (non-Javadoc) + * @see org.springframework.batch.repeat.RepeatContext#isTerminateOnly() + */ + public boolean isTerminateOnly() { + return terminateOnly; + } + + /* + * (non-Javadoc) + * @see org.springframework.batch.repeat.RepeatContext#setTerminateOnly() + */ + public void setTerminateOnly() { + terminateOnly = true; + setCompleteOnly(); + } + + /* + * (non-Javadoc) + * @see org.springframework.batch.repeat.RepeatContext#getParent() + */ + public RepeatContext getParent() { + return parent; + } + + /** + * Used by clients to increment the started count. + */ + public synchronized void increment() { + count++; + } + + /* + * (non-Javadoc) + * @see org.springframework.batch.repeat.RepeatContext#getStartedCount() + */ + public synchronized int getStartedCount() { + return count; + } + + /* + * (non-Javadoc) + * @see org.springframework.batch.repeat.RepeatContext#registerDestructionCallback(java.lang.String, + * java.lang.Runnable) + */ + public void registerDestructionCallback(String name, Runnable callback) { + synchronized (callbacks) { + Set set = (Set) callbacks.get(name); + if (set == null) { + set = new HashSet(); + callbacks.put(name, set); + } + set.add(callback); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.batch.repeat.RepeatContext#close() + */ + public void close() { + + List errors = new ArrayList(); + + Set copy; + + synchronized (callbacks) { + copy = new HashSet(callbacks.entrySet()); + } + + for (Iterator iter = copy.iterator(); iter.hasNext();) { + Map.Entry entry = (Map.Entry) iter.next(); + Set set = (Set) entry.getValue(); + for (Iterator iterator = set.iterator(); iterator.hasNext();) { + Runnable callback = (Runnable) iterator.next(); + /* + * Potentially we could check here if there is an attribute with + * the given name - if it has been removed, maybe the callback + * is invalid. On the other hand it is less surprising for the + * callback register if it is always executed. + */ + if (callback != null) { + /* + * The documentation of the interface says that these + * callbacks must not throw exceptions, but we don't trust + * them necessarily... + */ + try { + callback.run(); + } + catch (RuntimeException t) { + errors.add(t); + } + } + } + } + + if (errors.isEmpty()) { + return; + } + + throw (RuntimeException) errors.get(0); + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/repeat/context/SynchronizedAttributeAccessor.java b/infrastructure/src/main/java/org/springframework/batch/repeat/context/SynchronizedAttributeAccessor.java new file mode 100644 index 000000000..1e345fa1e --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/repeat/context/SynchronizedAttributeAccessor.java @@ -0,0 +1,161 @@ +/* + * 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.repeat.context; + +import org.springframework.core.AttributeAccessor; +import org.springframework.core.AttributeAccessorSupport; + +/** + * An {@link AttributeAccessor} that synchronizes on a mutex (not this) before + * modifying or accessing the underlying attributes. + * + * @author Dave Syer + * + */ +public class SynchronizedAttributeAccessor implements AttributeAccessor { + + /** + * All methods are delegated to this support object. + */ + AttributeAccessorSupport support = new AttributeAccessorSupport() { + /** + * Generated serial UID. + */ + private static final long serialVersionUID = -7664290016506582290L; + }; + + /* + * (non-Javadoc) + * @see org.springframework.core.AttributeAccessor#attributeNames() + */ + public String[] attributeNames() { + synchronized (support) { + return support.attributeNames(); + } + } + + /* + * (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + public boolean equals(Object other) { + if (this == other) { + return true; + } + AttributeAccessorSupport that; + if (other instanceof SynchronizedAttributeAccessor) { + that = ((SynchronizedAttributeAccessor) other).support; + } + else if (other instanceof AttributeAccessorSupport) { + that = (AttributeAccessorSupport) other; + } + else { + return false; + } + synchronized (support) { + return support.equals(that); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.core.AttributeAccessor#getAttribute(java.lang.String) + */ + public Object getAttribute(String name) { + synchronized (support) { + return support.getAttribute(name); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.core.AttributeAccessor#hasAttribute(java.lang.String) + */ + public boolean hasAttribute(String name) { + synchronized (support) { + return support.hasAttribute(name); + } + } + + /* + * (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + public int hashCode() { + return support.hashCode(); + } + + /* + * (non-Javadoc) + * @see org.springframework.core.AttributeAccessor#removeAttribute(java.lang.String) + */ + public Object removeAttribute(String name) { + synchronized (support) { + return support.removeAttribute(name); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.core.AttributeAccessor#setAttribute(java.lang.String, + * java.lang.Object) + */ + public void setAttribute(String name, Object value) { + synchronized (support) { + support.setAttribute(name, value); + } + } + + /** + * Additional support for atomic put if absent. + * @param name the key for the attribute name + * @param value the value of the attribute + * @return null if the attribute was not already set, the existing value + * otherwise. + */ + public Object setAttributeIfAbsent(String name, Object value) { + synchronized (support) { + Object old = getAttribute(name); + if (old != null) { + return old; + } + setAttribute(name, value); + } + return null; + } + + /* + * (non-Javadoc) + * @see java.lang.Object#toString() + */ + public String toString() { + StringBuffer buffer = new StringBuffer("SynchronizedAttributeAccessor: ["); + synchronized (support) { + String[] names = attributeNames(); + for (int i = 0; i < names.length; i++) { + String name = names[i]; + buffer.append(names[i] + "=" + getAttribute(name)); + if (i < names.length - 1) { + buffer.append(", "); + } + } + buffer.append("]"); + return buffer.toString(); + } + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/repeat/context/package.html b/infrastructure/src/main/java/org/springframework/batch/repeat/context/package.html new file mode 100644 index 000000000..7bb43facd --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/repeat/context/package.html @@ -0,0 +1,7 @@ + + +

+Infrastructure implementations of repeat context concerns. +

+ + diff --git a/infrastructure/src/main/java/org/springframework/batch/repeat/exception/RepeatException.java b/infrastructure/src/main/java/org/springframework/batch/repeat/exception/RepeatException.java new file mode 100644 index 000000000..cfbacca4d --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/repeat/exception/RepeatException.java @@ -0,0 +1,31 @@ +/* + * 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.repeat.exception; + +import org.springframework.core.NestedRuntimeException; + +public class RepeatException extends NestedRuntimeException { + + public RepeatException(String msg) { + super(msg); + } + + public RepeatException(String msg, Throwable t) { + super(msg, t); + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/repeat/exception/handler/CompositeExceptionHandler.java b/infrastructure/src/main/java/org/springframework/batch/repeat/exception/handler/CompositeExceptionHandler.java new file mode 100644 index 000000000..28d14199d --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/repeat/exception/handler/CompositeExceptionHandler.java @@ -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.repeat.exception.handler; + +import java.util.Collection; + +import org.springframework.batch.repeat.RepeatContext; + +/** + * Composiste {@link ExceptionHandler} that loops though a list of delegates. + * + * @author Dave Syer + * + */ +public class CompositeExceptionHandler implements ExceptionHandler { + + private ExceptionHandler[] handlers = new ExceptionHandler[0]; + + public void setHandlers(ExceptionHandler[] handlers) { + this.handlers = handlers; + } + + /** + * Iterate over the handlers delegating the call to each in turn. The chain + * ends if an exception is thrown. + * + * @see ExceptionHandler#handleExceptions(RepeatContext, Collection) + */ + public void handleExceptions(RepeatContext context, Collection throwables) throws RuntimeException { + for (int i = 0; i < handlers.length; i++) { + ExceptionHandler handler = handlers[i]; + handler.handleExceptions(context, throwables); + } + } +} diff --git a/infrastructure/src/main/java/org/springframework/batch/repeat/exception/handler/DefaultExceptionHandler.java b/infrastructure/src/main/java/org/springframework/batch/repeat/exception/handler/DefaultExceptionHandler.java new file mode 100644 index 000000000..6f30f2775 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/repeat/exception/handler/DefaultExceptionHandler.java @@ -0,0 +1,65 @@ +/* + * 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.repeat.exception.handler; + +import java.util.Collection; + +import org.springframework.batch.repeat.RepeatContext; +import org.springframework.batch.repeat.exception.RepeatException; + +/** + * Default implementation of {@link ExceptionHandler} - just re-throws the first + * exception it encounters. + * + * @author Dave Syer + * + */ +public class DefaultExceptionHandler implements ExceptionHandler { + + /** + * Rethrow the first throwable in the collection's iterator. Wrap in a + * {@link RepeatException} if the first instance is not a + * {@link RuntimeException}. + * + * @see org.springframework.batch.repeat.exception.handler.ExceptionHandler#handleExceptions(RepeatContext, + * java.util.Collection) + */ + public void handleExceptions(RepeatContext context, Collection throwables) throws RuntimeException { + + Throwable t = (Throwable) throwables.iterator().next(); + rethrow(t); + + } + + /** + * Convenience method to rethrow the Throwable instance. Wraps it in a + * {@link RepeatException} if it is not a {@link RuntimeException}. + * + * @param throwable a Throwable. + * @throws RuntimeException if the throwable is a RuntimeException just + * rethrow, otherwise wrap in a {@link RepeatException} + */ + public static void rethrow(Throwable throwable) throws RuntimeException { + if (throwable instanceof RuntimeException) { + throw (RuntimeException) throwable; + } + else { + throw new RepeatException("Exception in batch process", throwable); + } + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/repeat/exception/handler/ExceptionHandler.java b/infrastructure/src/main/java/org/springframework/batch/repeat/exception/handler/ExceptionHandler.java new file mode 100644 index 000000000..74e9d432e --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/repeat/exception/handler/ExceptionHandler.java @@ -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.repeat.exception.handler; + +import java.util.Collection; + +import org.springframework.batch.repeat.CompletionPolicy; +import org.springframework.batch.repeat.RepeatContext; + +/** + * Policy to allow strategies for rethrowing exceptions in the case of + * termination. Normally a {@link CompletionPolicy} will be used to decide + * whether to end a batch including when there is an exception, and the + * {@link ExceptionHandler} is used to distinguish between normal and abnormal + * ending. An abnormal ending would normally result in an + * {@link ExceptionHandler} throwing an exception. + * + * @author Dave Syer + * + */ +public interface ExceptionHandler { + + /** + * Deal with a collection of throwables accumulated during a batch. The + * collection may consist of RuntimeExceptions or other unchecked + * exceptions. + * @param context the current {@link RepeatContext}. Can be used to store + * state (via attributes), for example to count the number of occurrences of + * a particular exception type and implement a threshold policy. + * @param throwables a collection of non-checked exceptions. + * + * @throws any or all of the exception types passed in, or a wrapped + * exception if one is an error or checked exception. + */ + void handleExceptions(RepeatContext context, Collection throwables) throws RuntimeException; + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/repeat/exception/handler/LogOrRethrowExceptionHandler.java b/infrastructure/src/main/java/org/springframework/batch/repeat/exception/handler/LogOrRethrowExceptionHandler.java new file mode 100644 index 000000000..2d811b1ef --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/repeat/exception/handler/LogOrRethrowExceptionHandler.java @@ -0,0 +1,111 @@ +/* + * 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.repeat.exception.handler; + +import java.util.Collection; +import java.util.Iterator; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.batch.common.ExceptionClassifier; +import org.springframework.batch.common.ExceptionClassifierSupport; +import org.springframework.batch.repeat.RepeatContext; +import org.springframework.batch.repeat.exception.RepeatException; + +/** + * Implementation of {@link ExceptionHandler} based on an + * {@link ExceptionClassifier}. The classifier determines whether to log the + * exception or rethrow it. The keys in the classifier must be the same as the + * static contants in this class. + * + * @author Dave Syer + * + */ +public class LogOrRethrowExceptionHandler implements ExceptionHandler { + + /** + * Key for {@link ExceptionClassifier} signalling that the throwable should + * be rethrown. If the throwable is not a RuntimeException it is wrapped in + * a {@link RepeatException}. + */ + public static final String RETHROW = "rethrow"; + + /** + * Key for {@link ExceptionClassifier} signalling that the throwable should + * be logged at debug level. + */ + public static final String DEBUG = "debug"; + + /** + * Key for {@link ExceptionClassifier} signalling that the throwable should + * be logged at warn level. + */ + public static final String WARN = "warn"; + + /** + * Key for {@link ExceptionClassifier} signalling that the throwable should + * be logged at error level. + */ + public static final String ERROR = "error"; + + protected final Log logger = LogFactory.getLog(LogOrRethrowExceptionHandler.class); + + private ExceptionClassifier exceptionClassifier = new ExceptionClassifierSupport() { + public Object classify(Throwable throwable) { + return RETHROW; + } + }; + + /** + * Setter for the {@link ExceptionClassifier} used by this handler. The + * default is to map all throwable instances to {@value #RETHROW}. + * + * @param exceptionClassifier + */ + public void setExceptionClassifier(ExceptionClassifier exceptionClassifier) { + this.exceptionClassifier = exceptionClassifier; + } + + /** + * Classify the throwables and decide whether to rethrow based on the + * result. The context is not used. + * @throws Exception + * + * @see {@link ExceptionHandler#handleExceptions(RepeatContext, Collection)} + */ + public void handleExceptions(RepeatContext context, Collection throwables) throws RuntimeException { + + for (Iterator iter = throwables.iterator(); iter.hasNext();) { + Throwable throwable = (Throwable) iter.next(); + Object key = exceptionClassifier.classify(throwable); + if (ERROR.equals(key)) { + logger.error("Exception encountered in batch repeat.", throwable); + } + if (WARN.equals(key)) { + logger.warn("Exception encountered in batch repeat.", throwable); + } + if (DEBUG.equals(key) && logger.isDebugEnabled()) { + logger.debug("Exception encountered in batch repeat.", throwable); + } + if (RETHROW.equals(key)) { + DefaultExceptionHandler.rethrow(throwable); + } + } + + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/repeat/exception/handler/RethrowOnThresholdExceptionHandler.java b/infrastructure/src/main/java/org/springframework/batch/repeat/exception/handler/RethrowOnThresholdExceptionHandler.java new file mode 100644 index 000000000..1dbe85365 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/repeat/exception/handler/RethrowOnThresholdExceptionHandler.java @@ -0,0 +1,133 @@ +/* + * 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.repeat.exception.handler; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.batch.common.ExceptionClassifier; +import org.springframework.batch.common.ExceptionClassifierSupport; +import org.springframework.batch.repeat.RepeatContext; +import org.springframework.batch.repeat.context.RepeatContextCounter; +import org.springframework.util.Assert; + +/** + * Implementation of {@link ExceptionHandler} that rethrows when exceptions of a + * given type reach a threshold. Requires an {@link ExceptionClassifier} that + * maps exception types to unique keys, and also a map from those keys to + * threshold values (Integer type). + * + * @author Dave Syer + * + */ +public class RethrowOnThresholdExceptionHandler implements ExceptionHandler { + + protected final Log logger = LogFactory.getLog(RethrowOnThresholdExceptionHandler.class); + + private ExceptionClassifier exceptionClassifier = new ExceptionClassifierSupport(); + + private Map thresholds = new HashMap(); + + private boolean useParent = false; + + /** + * Flag to indicate the the exception counters should be shared between + * sibling contexts in a nested batch. Default is false. + * + * @param useParent true if the parent context should be used to store the + * counters. + */ + public void setUseParent(boolean useParent) { + this.useParent = useParent; + } + + /** + * Set up the exception handler. Creates a default exception handler and + * threshold that maps all exceptions to a threshold of 0 - all exceptions + * are rethrown by default. + */ + public RethrowOnThresholdExceptionHandler() { + super(); + thresholds.put(ExceptionClassifierSupport.DEFAULT, new Integer(0)); + } + + /** + * A map from classifier keys to a threshold value of type Integer. The keys + * are usually String literals, depending on the {@link ExceptionClassifier} + * implementation used. + * + * @param thresholds the threshold value map. + */ + public void setThresholds(Map thresholds) { + for (Iterator iter = thresholds.entrySet().iterator(); iter.hasNext();) { + Map.Entry entry = (Map.Entry) iter.next(); + if (!(entry.getKey() instanceof String)) { + logger.warn("Key in thresholds map is not of type String: " + entry.getKey()); + } + Assert.state(entry.getValue() instanceof Integer, "Threshold value must be of type Integer. " + + "Try using the value-type attribute if you care configuring this map via xml."); + } + this.thresholds = thresholds; + } + + /** + * Setter for the {@link ExceptionClassifier} used by this handler. The + * default is to map all throwable instances to + * {@value ExceptionClassifierSupport#DEFAULT}, which are then mapped to a + * threshold of 0 by the {@link #setThresholds(Map)} map. + * + * @param exceptionClassifier + */ + public void setExceptionClassifier(ExceptionClassifier exceptionClassifier) { + this.exceptionClassifier = exceptionClassifier; + } + + /** + * Classify the throwables and decide whether to rethrow based on the + * result. The context is used to accumulate the number of exceptions of the + * same type according to the classifier. + * @throws Exception + * + * @see {@link ExceptionHandler#handleExceptions(RepeatContext, Collection)} + */ + public void handleExceptions(RepeatContext context, Collection throwables) throws RuntimeException { + + for (Iterator iter = throwables.iterator(); iter.hasNext();) { + Throwable throwable = (Throwable) iter.next(); + Object key = exceptionClassifier.classify(throwable); + RepeatContextCounter counter = getCounter(context, key); + counter.increment(); + int count = counter.getCount(); + Integer threshold = (Integer) thresholds.get(key); + if (threshold == null || count > threshold.intValue()) { + DefaultExceptionHandler.rethrow(throwable); + } + } + + } + + private RepeatContextCounter getCounter(RepeatContext context, Object key) { + String attribute = RethrowOnThresholdExceptionHandler.class + "." + key.toString(); + // Creates a new counter and stores it in the correct context: + return new RepeatContextCounter(context, attribute, useParent); + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/repeat/exception/handler/SimpleLimitExceptionHandler.java b/infrastructure/src/main/java/org/springframework/batch/repeat/exception/handler/SimpleLimitExceptionHandler.java new file mode 100644 index 000000000..f88ee8718 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/repeat/exception/handler/SimpleLimitExceptionHandler.java @@ -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.repeat.exception.handler; + +import java.util.Collection; +import java.util.HashMap; + +import org.springframework.batch.common.ExceptionClassifierSupport; +import org.springframework.batch.io.exception.TransactionInvalidException; +import org.springframework.batch.repeat.RepeatContext; + +/** + * Simple implementation of exception handler which looks for one exception + * type. If it is found in the Collection of throwables then a counter is + * incremented and the a limit is checked to determine if it has been exceeded + * and the Throwable should be re-thrown. + * + * @author Dave Syer + */ +public class SimpleLimitExceptionHandler implements ExceptionHandler { + + /** + * Name of exception classifier key for the + * {@link TransactionInvalidException}. + */ + private static final String TX_INVALID = "TX_INVALID"; + + private RethrowOnThresholdExceptionHandler delegate = new RethrowOnThresholdExceptionHandler(); + + private Class type = TransactionInvalidException.class; + + /** + * Flag to indicate the the exception counters should be shared between + * sibling contexts in a nested batch (i.e. inner loop). Default is false. + * Set this flag to true if you want to count exceptions for the whole + * (outer) loop in a typical container. + * + * @param useParent true if the parent context should be used to store the + * counters. + */ + public void setUseParent(boolean useParent) { + delegate.setUseParent(useParent); + } + + /** + * Default constructor for the {@link SimpleLimitExceptionHandler}. + */ + public SimpleLimitExceptionHandler() { + super(); + delegate.setExceptionClassifier(new ExceptionClassifierSupport() { + public Object classify(Throwable throwable) { + if (type.isAssignableFrom(throwable.getClass())) { + return TX_INVALID; + } + return super.classify(throwable); + } + }); + } + + /** + * Rethrows only if the limit is breached for this context on the exception + * type specified. + * + * @see #setType(Class) + * @see #setLimit(int) + * + * @see org.springframework.batch.repeat.exception.handler.ExceptionHandler#handleExceptions(org.springframework.batch.repeat.RepeatContext, + * java.util.Collection) + */ + public void handleExceptions(RepeatContext context, Collection throwables) throws RuntimeException { + delegate.handleExceptions(context, throwables); + } + + /** + * The limit on the given exception type within a single context before it + * is rethrown. + * + * @param limit + */ + public void setLimit(final int limit) { + delegate.setThresholds(new HashMap() { + { + put(ExceptionClassifierSupport.DEFAULT, new Integer(0)); + put(TX_INVALID, new Integer(limit)); + } + }); + } + + /** + * Setter for the Throwable type that this handler counts. Defaults to + * {@link TransactionInvalidException}. + * + * @param type + */ + public void setType(Class type) { + this.type = type; + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/repeat/exception/handler/package.html b/infrastructure/src/main/java/org/springframework/batch/repeat/exception/handler/package.html new file mode 100644 index 000000000..edbc051c8 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/repeat/exception/handler/package.html @@ -0,0 +1,7 @@ + + +

+Infrastructure implementations of repeat exception handler concerns. +

+ + diff --git a/infrastructure/src/main/java/org/springframework/batch/repeat/exception/package.html b/infrastructure/src/main/java/org/springframework/batch/repeat/exception/package.html new file mode 100644 index 000000000..cf291e781 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/repeat/exception/package.html @@ -0,0 +1,7 @@ + + +

+Infrastructure implementations of repeat exception concerns. +

+ + diff --git a/infrastructure/src/main/java/org/springframework/batch/repeat/interceptor/ApplicationEventPublisherRepeatInterceptor.java b/infrastructure/src/main/java/org/springframework/batch/repeat/interceptor/ApplicationEventPublisherRepeatInterceptor.java new file mode 100644 index 000000000..d8a4136fc --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/repeat/interceptor/ApplicationEventPublisherRepeatInterceptor.java @@ -0,0 +1,95 @@ +/* + * 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.repeat.interceptor; + +import org.springframework.batch.repeat.RepeatContext; +import org.springframework.batch.repeat.RepeatInterceptor; +import org.springframework.batch.repeat.ExitStatus; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; + +/** + * @author Dave Syer + * + */ +public class ApplicationEventPublisherRepeatInterceptor implements ApplicationEventPublisherAware, RepeatInterceptor { + + private ApplicationEventPublisher applicationEventPublisher; + + /* + * (non-Javadoc) + * @see org.springframework.context.ApplicationEventPublisherAware#setApplicationEventPublisher(org.springframework.context.ApplicationEventPublisher) + */ + public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { + this.applicationEventPublisher = applicationEventPublisher; + } + + /* + * (non-Javadoc) + * @see org.springframework.batch.repeat.RepeatInterceptor#after(org.springframework.batch.repeat.RepeatContext, + * java.lang.Object) + */ + public void after(RepeatContext context, Object result) { + publish(context, "After repeat callback with result=[" + result + "]", RepeatOperationsApplicationEvent.AFTER); + } + + /* + * (non-Javadoc) + * @see org.springframework.batch.repeat.RepeatInterceptor#before(org.springframework.batch.repeat.RepeatContext) + */ + public void before(RepeatContext context) { + publish(context, "Before repeat callback", RepeatOperationsApplicationEvent.BEFORE); + } + + /* + * (non-Javadoc) + * @see org.springframework.batch.repeat.RepeatInterceptor#close(org.springframework.batch.repeat.RepeatContext) + */ + public ExitStatus close(RepeatContext context) { + publish(context, "Closed repeat context with batch complete", RepeatOperationsApplicationEvent.CLOSE); + return ExitStatus.CONTINUABLE; + } + + /* + * (non-Javadoc) + * @see org.springframework.batch.repeat.RepeatInterceptor#onError(org.springframework.batch.repeat.RepeatContext, + * java.lang.Throwable) + */ + public void onError(RepeatContext context, Throwable e) { + publish(context, "Error in repeat operations with Throwable type=["+e.getClass()+"], message=["+e.getMessage()+"]", RepeatOperationsApplicationEvent.ERROR); + } + + /* + * (non-Javadoc) + * @see org.springframework.batch.repeat.RepeatInterceptor#open(org.springframework.batch.repeat.RepeatContext) + */ + public void open(RepeatContext context) { + publish(context, "Repeat operations opened", RepeatOperationsApplicationEvent.OPEN); + } + + /** + * Publish a {@link RepeatOperationsApplicationEvent} with the given + * parameters. + * + * @param context the current batch context + * @param message the message to publish + * @param type the type of event to publish + */ + private void publish(RepeatContext context, String message, int type) { + applicationEventPublisher.publishEvent(new RepeatOperationsApplicationEvent(context, message, type)); + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/repeat/interceptor/RepeatInterceptorAdapter.java b/infrastructure/src/main/java/org/springframework/batch/repeat/interceptor/RepeatInterceptorAdapter.java new file mode 100644 index 000000000..fee8c70d1 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/repeat/interceptor/RepeatInterceptorAdapter.java @@ -0,0 +1,41 @@ +/* + * 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.repeat.interceptor; + +import org.springframework.batch.repeat.RepeatContext; +import org.springframework.batch.repeat.RepeatInterceptor; +import org.springframework.batch.repeat.ExitStatus; + +public class RepeatInterceptorAdapter implements RepeatInterceptor { + + public void before(RepeatContext context) { + } + + public void after(RepeatContext context, Object result) { + } + + public ExitStatus close(RepeatContext context) { + return ExitStatus.CONTINUABLE; + } + + public void onError(RepeatContext context, Throwable e) { + } + + public void open(RepeatContext context) { + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/repeat/interceptor/RepeatOperationsApplicationEvent.java b/infrastructure/src/main/java/org/springframework/batch/repeat/interceptor/RepeatOperationsApplicationEvent.java new file mode 100644 index 000000000..463656d9f --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/repeat/interceptor/RepeatOperationsApplicationEvent.java @@ -0,0 +1,69 @@ +/* + * 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.repeat.interceptor; + +import org.springframework.batch.repeat.RepeatContext; +import org.springframework.context.ApplicationEvent; +import org.springframework.util.ClassUtils; + +/** + * @author Dave Syer + * + */ +public class RepeatOperationsApplicationEvent extends ApplicationEvent { + + public static final int AFTER = 3; + + public static final int BEFORE = 2; + + public static final int CLOSE = 4; + + public static final int OPEN = 1; + + public static final int ERROR = 5; + + final private int type; + + final private String message; + + /** + * Constructor for {@link RepeatOperationsApplicationEvent}. + * + * @param source the source of the event. Normally the current + * {@link RepeatContext} if there is one. + */ + public RepeatOperationsApplicationEvent(Object source, String message, int type) { + super(source); + this.message = message; + this.type = type; + } + + public String getMessage() { + return message; + } + + public int getType() { + return type; + } + + /* (non-Javadoc) + * @see java.util.EventObject#toString() + */ + public String toString() { + return ClassUtils.getShortName(getClass())+": type="+type+"; message="+message; + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/repeat/interceptor/package.html b/infrastructure/src/main/java/org/springframework/batch/repeat/interceptor/package.html new file mode 100644 index 000000000..8a95d8382 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/repeat/interceptor/package.html @@ -0,0 +1,7 @@ + + +

+Infrastructure implementations of repeat interceptor concerns. +

+ + diff --git a/infrastructure/src/main/java/org/springframework/batch/repeat/package.html b/infrastructure/src/main/java/org/springframework/batch/repeat/package.html new file mode 100644 index 000000000..531b1f8c4 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/repeat/package.html @@ -0,0 +1,7 @@ + + +

+Infrastructure implementations of repeat concerns. +

+ + diff --git a/infrastructure/src/main/java/org/springframework/batch/repeat/policy/CompletionPolicySupport.java b/infrastructure/src/main/java/org/springframework/batch/repeat/policy/CompletionPolicySupport.java new file mode 100644 index 000000000..ca85cad30 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/repeat/policy/CompletionPolicySupport.java @@ -0,0 +1,70 @@ +/* + * 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.repeat.policy; + +import org.springframework.batch.repeat.RepeatContext; +import org.springframework.batch.repeat.CompletionPolicy; +import org.springframework.batch.repeat.context.RepeatContextSupport; + +/** + * Very simple base class for {@link CompletionPolicy} implementations. + * + * @author Dave Syer + * + */ +public class CompletionPolicySupport implements CompletionPolicy { + + /** + * Delegate to {@link #isComplete(RepeatContext)}. + * + * @see org.springframework.batch.repeat.CompletionPolicy#isComplete(org.springframework.batch.repeat.RepeatContext, + * java.lang.Object) + */ + public boolean isComplete(RepeatContext context, Object result) { + return isComplete(context); + } + + /** + * Always true. + * + * @see org.springframework.batch.repeat.CompletionPolicy#isComplete(org.springframework.batch.repeat.RepeatContext) + */ + public boolean isComplete(RepeatContext context) { + return true; + } + + /** + * Build a new {@link RepeatContextSupport} and return it. + * + * @see org.springframework.batch.repeat.CompletionPolicy#start(RepeatContext) + */ + public RepeatContext start(RepeatContext context) { + return new RepeatContextSupport(context); + } + + /** + * Increment the context so the counter is up to date. Do nothing else. + * + * @see org.springframework.batch.repeat.CompletionPolicy#update(org.springframework.batch.repeat.RepeatContext) + */ + public void update(RepeatContext context) { + if (context instanceof RepeatContextSupport) { + ((RepeatContextSupport) context).increment(); + } + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/repeat/policy/CompositeCompletionPolicy.java b/infrastructure/src/main/java/org/springframework/batch/repeat/policy/CompositeCompletionPolicy.java new file mode 100644 index 000000000..6d2d70095 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/repeat/policy/CompositeCompletionPolicy.java @@ -0,0 +1,131 @@ +/* + * 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.repeat.policy; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.batch.repeat.RepeatContext; +import org.springframework.batch.repeat.CompletionPolicy; +import org.springframework.batch.repeat.context.RepeatContextSupport; + +/** + * Composite policy that loops through a list of delegate policies and answers + * calls by a concensus. + * + * @author Dave Syer + * + */ +public class CompositeCompletionPolicy implements CompletionPolicy { + + CompletionPolicy[] policies = new CompletionPolicy[0]; + + /** + * Setter for the policies. + * + * @param policies + */ + public void setPolicies(CompletionPolicy[] policies) { + this.policies = policies; + } + + /** + * This policy is complete if any of the composed policies is complete. + * + * @see org.springframework.batch.repeat.CompletionPolicy#isComplete(org.springframework.batch.repeat.RepeatContext, + * java.lang.Object) + */ + public boolean isComplete(RepeatContext context, Object result) { + RepeatContext[] contexts = ((CompositeBatchContext) context).contexts; + CompletionPolicy[] policies = ((CompositeBatchContext) context).policies; + for (int i = 0; i < policies.length; i++) { + if (policies[i].isComplete(contexts[i], result)) { + return true; + } + } + return false; + } + + /** + * This policy is complete if any of the composed policies is complete. + * + * @see org.springframework.batch.repeat.CompletionPolicy#isComplete(org.springframework.batch.repeat.RepeatContext) + */ + public boolean isComplete(RepeatContext context) { + RepeatContext[] contexts = ((CompositeBatchContext) context).contexts; + CompletionPolicy[] policies = ((CompositeBatchContext) context).policies; + for (int i = 0; i < policies.length; i++) { + if (policies[i].isComplete(contexts[i])) { + return true; + } + } + return false; + } + + /** + * Create a new composite context from all the available policies. + * + * @see org.springframework.batch.repeat.CompletionPolicy#start(RepeatContext) + */ + public RepeatContext start(RepeatContext context) { + List list = new ArrayList(); + for (int i = 0; i < policies.length; i++) { + list.add(policies[i].start(context)); + } + return new CompositeBatchContext(context, list); + + } + + /** + * Update all the composed contexts, and also increment the parent context. + * + * @see org.springframework.batch.repeat.CompletionPolicy#update(org.springframework.batch.repeat.RepeatContext) + */ + public void update(RepeatContext context) { + RepeatContext[] contexts = ((CompositeBatchContext) context).contexts; + CompletionPolicy[] policies = ((CompositeBatchContext) context).policies; + for (int i = 0; i < policies.length; i++) { + policies[i].update(contexts[i]); + } + ((RepeatContextSupport) context).increment(); + } + + /** + * Composite context that knows about the policies and contexts is was + * created with. + * + * @author Dave Syer + * + */ + protected class CompositeBatchContext extends RepeatContextSupport { + + private RepeatContext[] contexts; + + // Save a reference to the policies when we were created - gives some + // protection against reference changes (e.g. if the number of policies + // change). + private CompletionPolicy[] policies; + + public CompositeBatchContext(RepeatContext context, List contexts) { + super(context); + this.contexts = (RepeatContext[]) contexts.toArray(new RepeatContext[contexts.size()]); + this.policies = CompositeCompletionPolicy.this.policies; + } + + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/repeat/policy/CountingCompletionPolicy.java b/infrastructure/src/main/java/org/springframework/batch/repeat/policy/CountingCompletionPolicy.java new file mode 100644 index 000000000..ea09e6eda --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/repeat/policy/CountingCompletionPolicy.java @@ -0,0 +1,132 @@ +/* + * 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.repeat.policy; + +import org.springframework.batch.repeat.RepeatContext; +import org.springframework.batch.repeat.context.RepeatContextCounter; +import org.springframework.batch.repeat.context.RepeatContextSupport; + +/** + * Abstract base class for policies that need to count the number of occurrences + * of some event (e.g. an exception type in the context), and terminate based on + * a limit for the counter. The value of the counter can be stored between + * batches in a nested context, so that the termination decision is based on the + * aggregate of a number of sibling batches. + * + * @author Dave Syer + * + */ +public abstract class CountingCompletionPolicy extends CompletionPolicySupport { + + /** + * Session key for global counter. + */ + public static final String COUNT = CountingCompletionPolicy.class + ".COUNT"; + + private boolean useParent = false; + + private int maxCount = 0; + + /** + * Flag to indicate whether the count is at the level of the parent context, + * or just local to the context. If true then the count is aggregated among + * siblings in a nested batch. + * + * @param useParent whether to use the parent context to cache the total + * count. Default value is false. + */ + public void setUseParent(boolean useParent) { + this.useParent = useParent; + } + + /** + * Setter for maximum value of count before termination. + * + * @param maxCount the maximum number of counts before termination. Default + * 0 so termination is immediate. + */ + public void setMaxCount(int maxCount) { + this.maxCount = maxCount; + } + + /** + * Extension point for subclasses. Obtain the value of the count in the + * current context. Subclasses can count the number of attempts or + * violations and store the result in their context. This policy base class + * will take care of the termination contract and aggregating at the level + * of the session if required. + * + * @param context the current context, specific to the subclass. + * @return the value of the counter in the context. + */ + protected abstract int getCount(RepeatContext context); + + /** + * Extension point for subclasses. Inspect the context and update the state + * of a counter in whatever way is appropriate. This will be added to the + * session-level counter if {@link #useParent} is true. + * + * @param context the current context. + * + * @return the change in the value of the counter (default 0). + */ + protected int doUpdate(RepeatContext context) { + return 0; + } + + /* + * (non-Javadoc) + * @see org.springframework.batch.repeat.policy.CompletionPolicySupport#isComplete(org.springframework.batch.repeat.BatchContext) + */ + final public boolean isComplete(RepeatContext context) { + int count = ((CountingBatchContext) context).getCounter().getCount(); + return count >= maxCount; + } + + /* + * (non-Javadoc) + * @see org.springframework.batch.repeat.policy.CompletionPolicySupport#start(org.springframework.batch.repeat.BatchContext) + */ + public RepeatContext start(RepeatContext parent) { + return new CountingBatchContext(parent); + } + + /* + * (non-Javadoc) + * @see org.springframework.batch.repeat.policy.CompletionPolicySupport#update(org.springframework.batch.repeat.BatchContext) + */ + final public void update(RepeatContext context) { + super.update(context); + int delta = doUpdate(context); + ((CountingBatchContext) context).getCounter().increment(delta); + } + + protected class CountingBatchContext extends RepeatContextSupport { + + RepeatContextCounter counter; + + public CountingBatchContext(RepeatContext parent) { + super(parent); + counter = new RepeatContextCounter(this, COUNT, useParent); + } + + public RepeatContextCounter getCounter() { + return counter; + } + + } +} diff --git a/infrastructure/src/main/java/org/springframework/batch/repeat/policy/DefaultResultCompletionPolicy.java b/infrastructure/src/main/java/org/springframework/batch/repeat/policy/DefaultResultCompletionPolicy.java new file mode 100644 index 000000000..1342eb2c5 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/repeat/policy/DefaultResultCompletionPolicy.java @@ -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.repeat.policy; + +import org.springframework.batch.repeat.RepeatContext; +import org.springframework.batch.repeat.CompletionPolicy; +import org.springframework.batch.repeat.ExitStatus; + +/** + * Very simple {@link CompletionPolicy} that bases its decision on the result of + * a batch operation. If the result is Boolean.FALSE, null or an instance of + * Throwable the batch is complete, otherwise not. + * + * @author Dave Syer + * + */ +public class DefaultResultCompletionPolicy extends CompletionPolicySupport { + + /** + * True if the result is null, a {@link ExitStatus} indicating completion, + * or an instance of Throwable. + * + * @see org.springframework.batch.repeat.CompletionPolicy#isComplete(org.springframework.batch.repeat.RepeatContext, + * java.lang.Object) + */ + public boolean isComplete(RepeatContext context, Object result) { + return (result == null || (result instanceof Throwable) || (result instanceof ExitStatus && !((ExitStatus) result) + .isContinuable())); + } + + /** + * Always false. + * + * @see org.springframework.batch.repeat.CompletionPolicy#isComplete(org.springframework.batch.repeat.RepeatContext) + */ + public boolean isComplete(RepeatContext context) { + return false; + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/repeat/policy/SimpleCompletionPolicy.java b/infrastructure/src/main/java/org/springframework/batch/repeat/policy/SimpleCompletionPolicy.java new file mode 100644 index 000000000..a550c34b0 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/repeat/policy/SimpleCompletionPolicy.java @@ -0,0 +1,106 @@ +/* + * 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.repeat.policy; + +import org.springframework.batch.repeat.RepeatContext; +import org.springframework.batch.repeat.context.RepeatContextSupport; +import org.springframework.batch.repeat.support.RepeatTemplate; + +/** + * Policy for terminating a batch after a fixed number of operations. Internal + * state is maintained and a counter incremented, so successful use of this + * policy requires that isComplete() is only called once per batch item. Using + * the standard {@link RepeatTemplate} should ensure this contract is kept, but it needs + * to be carefully monitored. + * + * @author Dave Syer + * + */ +public class SimpleCompletionPolicy extends DefaultResultCompletionPolicy { + + public static final int DEFAULT_CHUNK_SIZE = 5; + + int chunkSize = 0; + + public SimpleCompletionPolicy() { + this(DEFAULT_CHUNK_SIZE); + } + + public SimpleCompletionPolicy(int chunkSize) { + super(); + this.chunkSize = chunkSize; + } + + public void setChunkSize(int chunkSize) { + this.chunkSize = chunkSize; + } + + /** + * Reset the counter. + * + * @see org.springframework.batch.repeat.CompletionPolicy#start(RepeatContext) + */ + public RepeatContext start(RepeatContext context) { + return new SimpleTerminationContext(context); + } + + /** + * Terminate if the chunk size has been reached, or the result is null. + * + * @see org.springframework.batch.repeat.CompletionPolicy#isComplete(RepeatContext, + * Object) + * @throws Exception (normally terminating the batch) if the result is + * itself an exception. + */ + public boolean isComplete(RepeatContext context, Object result) { + return super.isComplete(context, result) || ((SimpleTerminationContext) context).isComplete(); + } + + /** + * Terminate if the chunk size has been reached. + * + * @see org.springframework.batch.repeat.CompletionPolicy#isComplete(RepeatContext) + */ + public boolean isComplete(RepeatContext context) { + return ((SimpleTerminationContext) context).isComplete(); + } + + /** + * Increment the counter in the context. + * + * @see org.springframework.batch.repeat.CompletionPolicy#update(RepeatContext) + */ + public void update(RepeatContext context) { + ((SimpleTerminationContext) context).update(); + } + + protected class SimpleTerminationContext extends RepeatContextSupport { + + public SimpleTerminationContext(RepeatContext context) { + super(context); + } + + public void update() { + increment(); + } + + public boolean isComplete() { + return getStartedCount() >= chunkSize; + } + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/repeat/policy/TimeoutTerminationPolicy.java b/infrastructure/src/main/java/org/springframework/batch/repeat/policy/TimeoutTerminationPolicy.java new file mode 100644 index 000000000..3a024f7fa --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/repeat/policy/TimeoutTerminationPolicy.java @@ -0,0 +1,96 @@ +/* + * 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.repeat.policy; + +import org.springframework.batch.repeat.RepeatContext; +import org.springframework.batch.repeat.context.RepeatContextSupport; + +/** + * Termination policy that times out after a fixed period. Allows graceful exit + * from a batch if the latest result comes in after the timeout expires (i.e. + * does not throw a timeout exception).
+ * + * N.B. It may often be the case that the batch governed by this policy will be + * transactional, and the transaction might have its own timeout. In this case + * the transaction might throw a timeout exception on commit if its timeout + * threshold is lower than the termination policy. + * + * @author Dave Syer + * + */ +public class TimeoutTerminationPolicy extends CompletionPolicySupport { + + /** + * Default timeout value in millisecs (the value equivalent to 30 seconds). + */ + public static final long DEFAULT_TIMEOUT = 30000L; + + private long timeout = DEFAULT_TIMEOUT; + + /** + * Default constructor. + */ + public TimeoutTerminationPolicy() { + super(); + } + + /** + * Construct a {@link TimeoutTerminationPolicy} with the specified timeout + * value (in milliseconds). + * + * @param timeout + */ + public TimeoutTerminationPolicy(long timeout) { + super(); + this.timeout = timeout; + } + + /** + * Check the timeout and complete gracefully if it has expires. + * + * @see org.springframework.batch.repeat.CompletionPolicy#isComplete(org.springframework.batch.repeat.RepeatContext) + */ + public boolean isComplete(RepeatContext context) { + return ((TimeoutBatchContext) context).isComplete(); + } + + /** + * Start the clock on the timeout. + * + * @see org.springframework.batch.repeat.CompletionPolicy#start(RepeatContext) + */ + public RepeatContext start(RepeatContext context) { + return new TimeoutBatchContext(context); + } + + protected class TimeoutBatchContext extends RepeatContextSupport { + + private volatile long time = System.currentTimeMillis(); + + private final long timeout = TimeoutTerminationPolicy.this.timeout; + + public TimeoutBatchContext(RepeatContext context) { + super(context); + } + + public boolean isComplete() { + return (System.currentTimeMillis() - time) > timeout; + } + + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/repeat/policy/package.html b/infrastructure/src/main/java/org/springframework/batch/repeat/policy/package.html new file mode 100644 index 000000000..e30f24f4e --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/repeat/policy/package.html @@ -0,0 +1,7 @@ + + +

+Infrastructure implementations of repeat policy concerns. +

+ + diff --git a/infrastructure/src/main/java/org/springframework/batch/repeat/support/RepeatInternalState.java b/infrastructure/src/main/java/org/springframework/batch/repeat/support/RepeatInternalState.java new file mode 100644 index 000000000..8a8e83983 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/repeat/support/RepeatInternalState.java @@ -0,0 +1,25 @@ +/* + * 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.repeat.support; + +import java.util.Collection; + +public interface RepeatInternalState { + + Collection getThrowables(); + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/repeat/support/RepeatInternalStateSupport.java b/infrastructure/src/main/java/org/springframework/batch/repeat/support/RepeatInternalStateSupport.java new file mode 100644 index 000000000..06224b7d1 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/repeat/support/RepeatInternalStateSupport.java @@ -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.repeat.support; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +public class RepeatInternalStateSupport implements RepeatInternalState { + + // Accumulation of failed results. + Set throwables = new HashSet(); + + /* (non-Javadoc) + * @see org.springframework.batch.repeat.support.BatchInternalState#getThrowables() + */ + public Collection getThrowables() { + return throwables; + } + + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/repeat/support/RepeatTemplate.java b/infrastructure/src/main/java/org/springframework/batch/repeat/support/RepeatTemplate.java new file mode 100644 index 000000000..1b0ec7526 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/repeat/support/RepeatTemplate.java @@ -0,0 +1,409 @@ +/* + * 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.repeat.support; + +import java.util.Collection; +import java.util.Iterator; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.batch.repeat.RepeatCallback; +import org.springframework.batch.repeat.RepeatContext; +import org.springframework.batch.repeat.RepeatInterceptor; +import org.springframework.batch.repeat.RepeatOperations; +import org.springframework.batch.repeat.CompletionPolicy; +import org.springframework.batch.repeat.ExitStatus; +import org.springframework.batch.repeat.exception.handler.DefaultExceptionHandler; +import org.springframework.batch.repeat.exception.handler.ExceptionHandler; +import org.springframework.batch.repeat.policy.DefaultResultCompletionPolicy; +import org.springframework.batch.repeat.synch.RepeatSynchronizationManager; +import org.springframework.util.Assert; + +/** + * Simple implementation and base class for batch templates implementing + * {@link RepeatOperations}. Provides a framework including interceptors and + * policies. Subclasses just need to provide a method that gets the next result + * and one that waits for all the results to be returned from concurrent + * processes or threads.
+ * + * N.B. the template accumulates thrown exceptions during the iteration, and + * they are all processed together when the main loop ends (i.e. finished + * processing the items). Clients that do not want to stop execution when an + * exception is thrown can use a specific {@link CompletionPolicy} that does not + * finish when exceptions are received. This is not the default behaviour.
+ * + * Clients that want to take some business action when an exception is thrown by + * the {@link RepeatCallback} can consider using a custom + * {@link RepeatInterceptor} instead of trying to customise the + * {@link CompletionPolicy}. This is generally a friendlier interface to + * implement, and the {@link RepeatInterceptor#after(RepeatContext, Object)} + * method is passed in the result of the callback, which would be an instance of + * {@link Throwable} if the business processing had thrown an exception. If the + * exception is not to be propagated to the caller, then a non-default + * {@link CompletionPolicy} needs to be provided as well, but that could be off + * the shelf, with the business action implemented only in the interceptor. + * + * @author Dave Syer + * + */ +public class RepeatTemplate implements RepeatOperations { + + protected Log logger = LogFactory.getLog(getClass()); + + private RepeatInterceptor[] interceptors = new RepeatInterceptor[] {}; + + private CompletionPolicy completionPolicy = new DefaultResultCompletionPolicy(); + + private ExceptionHandler exceptionHandler = new DefaultExceptionHandler(); + + public void setInterceptors(RepeatInterceptor[] interceptors) { + this.interceptors = interceptors; + } + + public void setInterceptor(RepeatInterceptor interceptor) { + interceptors = new RepeatInterceptor[] { interceptor }; + } + + /** + * Setter for exception handler strategy. The exception handler is called at + * the end of a batch, after the {@link CompletionPolicy} has determined + * that the batch is complete. By default all exceptions are re-thrown. + * + * @see ExceptionHandler + * @see DefaultExceptionHandler + * @see #setCompletionPolicy(CompletionPolicy) + * + * @param exceptionHandler the {@link ExceptionHandler} to use. + */ + public void setExceptionHandler(ExceptionHandler exceptionHandler) { + this.exceptionHandler = exceptionHandler; + } + + /** + * Setter for policy to decide when the batch is complete. The default is to + * complete normally when the callback returns a {@link ExitStatus} which is + * not marked as continuable, and abnormally when the callback throws an + * exception (but the decision to re-throw the exception is deferred to the + * {@link ExceptionHandler}). + * + * @see #setExceptionHandler(ExceptionHandler) + * + * @param terminationPolicy a TerminationPolicy. + * @throws IllegalArgumentException if the argument is null + */ + public void setCompletionPolicy(CompletionPolicy terminationPolicy) { + Assert.notNull(terminationPolicy); + this.completionPolicy = terminationPolicy; + } + + /** + * Execute the batch callback until the completion policy decides that we + * are finished. Wait for the whole batch to finish before returning even if + * the task executor is asynchronous. + * + * @see org.springframework.batch.repeat.RepeatOperations#iterate(org.springframework.batch.repeat.RepeatCallback) + */ + public ExitStatus iterate(RepeatCallback callback) { + + RepeatContext outer = RepeatSynchronizationManager.getContext(); + + ExitStatus result = ExitStatus.CONTINUABLE; + try { + // This works with an asynchronous TaskExecutor: the + // interceptors have to wait for the child processes. + result = executeInternal(callback); + } + finally { + RepeatSynchronizationManager.clear(); + if (outer != null) { + RepeatSynchronizationManager.register(outer); + } + } + + return result; + } + + /** + * Internal convenience method to loop over interceptors and batch + * callbacks. + * + * @param callback the callback to process each element of the loop. + * + * @return the aggregate of {@link ContinuationPolicy#canContinue(Object)} + * for all the results from the callback. + * + */ + private ExitStatus executeInternal(final RepeatCallback callback) { + + // Reset the termination policy if there is one... + RepeatContext context = start(); + + // Make sure if we are already marked complete before we start then no + // processing takes place. + boolean running = !isMarkedComplete(context); + + for (int i = 0; i < interceptors.length; i++) { + RepeatInterceptor interceptor = interceptors[i]; + interceptor.open(context); + running = running && !isMarkedComplete(context); + if (!running) + break; + } + + // Return value, default is to allow continued processing. + ExitStatus result = ExitStatus.CONTINUABLE; + + RepeatInternalState state = createInternalState(context); + Collection throwables = state.getThrowables(); + + try { + + while (running) { + + /* + * Run the before interceptors here, not in the task executor so + * that they all happen in the same thread - it's easier for + * tracking batch status, amongst other things. + */ + for (int i = 0; i < interceptors.length; i++) { + RepeatInterceptor interceptor = interceptors[i]; + interceptor.before(context); + // Allow before interceptors to veto the batch by setting + // flag. + running = running && !isMarkedComplete(context); + } + + // Check that we are still running... + if (running) { + + logger.debug("Batch operation about to start at count=" + context.getStartedCount()); + + Object value = getNextResult(context, callback, state); + + // Save a throwable for later... + // TODO: hide this in internal state? + if (value instanceof Throwable) { + throwables.add(value); + } + + executeAfterInterceptors(context, value); + // An exception alone is not sufficient grounds for not + // continuing + result = result.and(canContinue(value)); + + // N.B. the order may be important here: + if (isComplete(context, value) || isMarkedComplete(context)) { + running = false; + } + } + + } + + result = result.and(waitForResults(state)); + + // Explicitly drop any references to internal state... + state = null; + + } + /* + * No need for explicit catch here - if the business processing threw an + * exception it was already handled by the helper methods. An exception + * here is necessarily fatal. + */ + finally { + + try { + for (Iterator iter = throwables.iterator(); iter.hasNext();) { + Throwable t = (Throwable) iter.next(); + for (int i = interceptors.length; i-- > 0;) { + RepeatInterceptor interceptor = interceptors[i]; + interceptor.onError(context, t); + logger.error("Exception intercepted (" + (i + 1) + " of " + interceptors.length + ")", t); + } + } + + if (!throwables.isEmpty()) { + exceptionHandler.handleExceptions(context, throwables); + } + } + finally { + + try { + for (int i = interceptors.length; i-- > 0;) { + RepeatInterceptor interceptor = interceptors[i]; + result = result.and(interceptor.close(context)); + } + } + finally { + // TODO: extend this to the completion policy? + context.close(); + } + } + + } + + return result; + + } + + /** + * Create an internal state object that is used to store data needed + * internally in the scope of an iteration. Used by subclasses to manage the + * queueing and retrieval of asynchronous results. The default just provides + * an accumulation of Throwable instances for processing at the end of the + * batch. + * + * @param context the current {@link RepeatContext} + * @return a {@link RepeatInternalState} instance. + */ + protected RepeatInternalState createInternalState(RepeatContext context) { + return new RepeatInternalStateSupport(); + } + + /** + * Get the next completed result, possibly executing several callbacks until + * one finally finishes. + * + * @param context current BatchContext. + * @param callback the callback to execute. + * @param state maintained by the implementation. + * @return a finished result (possibly a Throwable instance if there is an + * error). + * + * @see {@link #isComplete(RepeatContext)} + */ + protected Object getNextResult(RepeatContext context, RepeatCallback callback, RepeatInternalState state) { + try { + update(context); + return callback.doInIteration(context); + } + catch (Throwable t) { + return t; + } + } + + /** + * If necessary, wait for results to come back from remote or concurrent + * processes. By default does nothing and returns true. + * + * @param state the internal state. + * @return true if {@link #canContinue(Object)} is true for all results + * retrieved. + */ + protected boolean waitForResults(RepeatInternalState state) { + // no-op by default + return true; + } + + /** + * Check return value from batch operation. It's either RepeatStatus or + * Throwable at this point, so we just check the type and continue as + * expected. + * @param value the last callback result. + * @return true if the value is Throwable or RepeatStatus.CONTINUABLE. + */ + protected final boolean canContinue(Object value) { + if (value instanceof ExitStatus) { + return ((ExitStatus) value).isContinuable(); + } + return true; // it's an exception + } + + private boolean isMarkedComplete(RepeatContext context) { + boolean complete = context.isCompleteOnly(); + if (context.getParent() != null) { + complete = complete || isMarkedComplete(context.getParent()); + } + if (complete) { + logger.debug("Batch is complete according to context alone."); + } + return complete; + + } + + /** + * Convenience method to execute after interceptors on a callback result. + * + * @param context the current batch context. + * @param value the result of the callback to process. + */ + protected void executeAfterInterceptors(final RepeatContext context, Object value) { + + // Don't re-throw exceptions here: let the exception handler deal with + // that... + + if (value != null + && ((value instanceof ExitStatus) && ((ExitStatus) value).isContinuable() || (value instanceof Throwable))) { + for (int i = interceptors.length; i-- > 0;) { + RepeatInterceptor interceptor = interceptors[i]; + interceptor.after(context, value); + } + + } + + } + + /** + * Delegate to the {@link CompletionPolicy}. + * + * @see org.springframework.batch.repeat.CompletionPolicy#isComplete(RepeatContext, + * Object) + */ + public boolean isComplete(RepeatContext context, Object result) { + boolean complete = completionPolicy.isComplete(context, result); + if (complete) { + logger.debug("Batch is complete according to policy and result value."); + } + return complete; + } + + /** + * Delegate to terminationPolicy. + * + * @see org.springframework.batch.repeat.CompletionPolicy#isComplete(RepeatContext) + */ + public boolean isComplete(RepeatContext context) { + boolean complete = completionPolicy.isComplete(context); + if (complete) { + logger.debug("Batch is complete according to policy alone not including result."); + } + return complete; + } + + /** + * Delegate to the {@link CompletionPolicy}. + * + * @see org.springframework.batch.repeat.CompletionPolicy#start(RepeatContext) + */ + public RepeatContext start() { + RepeatContext parent = RepeatSynchronizationManager.getContext(); + RepeatContext context = completionPolicy.start(parent); + RepeatSynchronizationManager.register(context); + logger.debug("Starting batch step."); + return context; + } + + /** + * Delegate to the {@link CompletionPolicy}. + * + * @see org.springframework.batch.repeat.CompletionPolicy#update(RepeatContext) + */ + public void update(RepeatContext context) { + completionPolicy.update(context); + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/repeat/support/TaskExecutorRepeatTemplate.java b/infrastructure/src/main/java/org/springframework/batch/repeat/support/TaskExecutorRepeatTemplate.java new file mode 100644 index 000000000..08b927199 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/repeat/support/TaskExecutorRepeatTemplate.java @@ -0,0 +1,370 @@ +/* + * 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.repeat.support; + +import java.util.List; + +import org.springframework.batch.repeat.RepeatCallback; +import org.springframework.batch.repeat.RepeatContext; +import org.springframework.batch.repeat.RepeatOperations; +import org.springframework.batch.repeat.exception.RepeatException; +import org.springframework.core.task.SyncTaskExecutor; +import org.springframework.core.task.TaskExecutor; +import org.springframework.util.Assert; + +import edu.emory.mathcs.backport.java.util.concurrent.ArrayBlockingQueue; +import edu.emory.mathcs.backport.java.util.concurrent.BlockingQueue; +import edu.emory.mathcs.backport.java.util.concurrent.Semaphore; + +/** + * Provides {@link RepeatOperations} support including interceptors that can be + * used to modify or monitor the batch behaviour at run time.
+ * + * This implementation is sufficient to be used to configure transactional + * behaviour for each batch item by making the {@link RepeatCallback} + * transactional, or for the whole batch by making the execute method + * transactional (but only then if the task executor is synchronous). + * Intermediate transactional chunks can be implemented using custom callbacks.
+ * + * This class is thread safe if its collaborators are thread safe (interceptors, + * terminationPolicy, callback). Normally this will be the case, but clients + * need to be aware that if the task executor is asynchronous, then the other + * collaborators should be also. In particular the {@link RepeatCallback} that + * is wrapped in the execute method must be thread safe - often it is based on + * some form of data source, which itself should be both thread safe and + * transactional (multiple threads could be accessing it at any given time, and + * each thread would have its own transaction).
+ * + * @author Dave Syer + * + */ +public class TaskExecutorRepeatTemplate extends RepeatTemplate { + + /** + * Default limit for maximum number of concurrent unfinished results allowed + * by the template. + * {@link #getNextResult(RepeatContext, RepeatCallback, TerminationContext, List)}. + */ + public static final int DEFAULT_THROTTLE_LIMIT = 4; + + private int throttleLimit = DEFAULT_THROTTLE_LIMIT; + + private TaskExecutor taskExecutor = new SyncTaskExecutor(); + + /** + * Setter for task executor to be used to run the individual item callbacks. + * + * @param taskExecutor a TaskExecutor + * @throws IllegalArgumentException if the argument is null + */ + public void setTaskExecutor(TaskExecutor taskExecutor) { + Assert.notNull(taskExecutor); + this.taskExecutor = taskExecutor; + } + + /** + * Use the {@link #taskExecutor} to generate a result. The internal state in + * this case is a queue of unfinished result holders of type + * {@link ResultHolder}. The holder with the return value should not be on + * the queue when this method exits. The queue is scoped in the calling + * method so there is no need to synchronize access. + * + * @see org.springframework.batch.repeat.support.AbstracBatchemplate#getNextResult(org.springframework.batch.item.RepeatContext, + * org.springframework.batch.repeat.RepeatCallback, + * org.springframework.batch.TerminationContext, java.util.List) + */ + protected Object getNextResult(RepeatContext context, RepeatCallback callback, RepeatInternalState state) { + + ExecutingRunnable runnable = null; + + ResultQueue queue = (ResultQueue) state; + + do { + + /* + * Wrap the callback in a runnable that will add its result to the + * queue when it is ready. + */ + runnable = new ExecutingRunnable(callback, context, queue); + + /* + * Start the task possibly concurrently / in the future. + */ + taskExecutor.execute(runnable); + + /* + * Allow termination policy to update its state. This must happen + * immediately before or after the call to the task executor. + */ + update(context); + + /* + * Keep going until we get a result that is finished, or early + * termination... + */ + } while (queue.isEmpty() && !isComplete(context)); + + Object result; + try { + result = queue.take().getResult(); + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + result = e; + } + return result; + } + + /** + * Wait for all the results to appear on the queue and execute the after + * interceptors for each one. + * + * @see org.springframework.batch.repeat.support.RepeatTemplate#waitForResults(org.springframework.batch.repeat.support.RepeatInternalState) + */ + protected boolean waitForResults(RepeatInternalState state) { + + ResultQueue futures = (ResultQueue) state; + + boolean result = true; + + while (futures.isExpecting()) { + + /* + * Careful that no runnables that are not going to finish ever get + * onto the queue, else this may block forever. + */ + ResultHolder future = (ResultHolder) futures.take(); + + Object value; + try { + value = future.getResult(); + } + catch (InterruptedException e) { + // TODO: cancel batch? + Thread.currentThread().interrupt(); + value = e; + } + if (value instanceof Throwable) { + state.getThrowables().add(value); + } + + executeAfterInterceptors(future.getContext(), value); + result = result && canContinue(value); + + } + + Assert.state(futures.isEmpty(), "Future results should be empty at end of batch."); + + return result; + } + + protected RepeatInternalState createInternalState(RepeatContext context) { + // Queue of pending results: + return new ResultQueue(); + } + + /** + * A runnable that puts its result on a queue when it is done. + * + * @author Dave Syer + * + */ + private static class ExecutingRunnable implements Runnable, ResultHolder { + RepeatCallback callback; + + RepeatContext context; + + ResultQueue queue; + + Object result; + + public ExecutingRunnable(RepeatCallback callback, RepeatContext context, ResultQueue queue) { + + super(); + + this.callback = callback; + this.context = context; + this.queue = queue; + + /* + * Tell the queue to expect a result. + */ + queue.expect(); + + } + + /** + * Execute the batch callback, and store the result, or any exception + * that is thrown for retrieval later by caller. + * + * @see java.lang.Runnable#run() + */ + public void run() { + try { + result = callback.doInIteration(context); + } + catch (Exception e) { + result = e; + } + finally { + queue.put(this); + } + } + + // TODO: Should we support cancellations? + + /** + * Get the result - never blocks because the queue manages waiting for + * the task to finish. + * + * @throws InterruptedException if the thread is interrupted. + */ + public Object getResult() throws InterruptedException { + return result; + } + + /** + * Getter for the context. + */ + public RepeatContext getContext() { + return this.context; + } + + } + + /** + * Abstraction for queue of {@link ResultHolder} objects. Acts as a + * BlockingQueue with the ability to count the number of items it expects to + * ever hold. When clients schedule an item to be added they call + * {@link #expect()}, and then when the result is collected the queue is + * notified that it no longer expects another. + * + * @author Dave Syer + * + */ + public class ResultQueue extends RepeatInternalStateSupport { + + // Accumulation of result objects as they finish. + private BlockingQueue results = new ArrayBlockingQueue(throttleLimit); + + // Accumulation of dummy objects flagging expected results in the + // future. + private Semaphore waits = new Semaphore(throttleLimit); + + // Arbitrary lock object. + private Object lock = new Object(); + + // Counter to monitor the difference between expected and actually + // collected results. When this reaches zero there are really no more + // results. + private volatile int count = 0; + + public boolean isEmpty() { + return results.isEmpty(); + } + + public boolean isExpecting() { + synchronized (lock) { + // Base the decision about whether we expect more results on a + // counter of the number of expected results actually collected. + return count > 0; + } + } + + public void expect() { + try { + waits.acquire(); + synchronized (lock) { + count++; + } + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RepeatException("InterruptedException waiting for to acquire lock on input."); + } + } + + public void put(ResultHolder holder) { + // There should be no need to block here: + results.add(holder); + // Take from the waits queue now to allow another result to + // accumulate. But don't decrement the counter. + waits.release(); + } + + public ResultHolder take() { + ResultHolder value; + try { + value = (ResultHolder) results.take(); + synchronized (lock) { + // Decrement the counter only when the result is collected. + count--; + } + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RepeatException("Interrupted while waiting for result."); + } + return value; + } + + } + + /** + * Interface for result holder. Should be implemented by subclasses so that + * the contract for + * {@link AbstracBatchemplate#getNextResult(RepeatContext, RepeatCallback, TerminationContext, List)} + * can be satisfied. + * + * @author Dave Syer + */ + public interface ResultHolder { + /** + * Get the result for client from this holder, blocking if necessary + * until it is ready. + * + * @return the result. + * @throws InterruptedException if the thread is interrupted while + * waiting for the result. + * @throws IllegalStateException + */ + Object getResult() throws InterruptedException; + + /** + * Get the context in which the result evaluation is execututing. + * + * @return the context of the result evaluation. + */ + RepeatContext getContext(); + } + + /** + * Public setter for the throttle limit. The throttle limit is the largest + * number of concurrent tasks that can be executing at one time - if a new + * task arrives and the throttle limit is breached we wait for one of the + * executing tasks to finish before submitting the new one to the + * {@link TaskExecutor}. Default value is {@value #DEFAULT_THROTTLE_LIMIT}. + * N.B. when used with a thread pooled {@link TaskExecutor} it doesn't make + * sense for the throttle limit to be less than the thread pool size. + * + * @param throttleLimit the throttleLimit to set. + */ + public void setThrottleLimit(int throttleLimit) { + this.throttleLimit = throttleLimit; + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/repeat/support/package.html b/infrastructure/src/main/java/org/springframework/batch/repeat/support/package.html new file mode 100644 index 000000000..6e3f0eef5 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/repeat/support/package.html @@ -0,0 +1,7 @@ + + +

+Infrastructure implementations of repeat support concerns. +

+ + diff --git a/infrastructure/src/main/java/org/springframework/batch/repeat/synch/BatchTransactionSynchronizationManager.java b/infrastructure/src/main/java/org/springframework/batch/repeat/synch/BatchTransactionSynchronizationManager.java new file mode 100644 index 000000000..d4e25f399 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/repeat/synch/BatchTransactionSynchronizationManager.java @@ -0,0 +1,185 @@ +/* + * 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.repeat.synch; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.springframework.batch.repeat.RepeatContext; +import org.springframework.core.AttributeAccessor; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +/** + *

+ * Contains static methods for registering objects for transaction + * synchronization. Because there are many non-standard inputs that need to be + * aware of transactions, such as file input, this facade provides a hook into + * Spring TransactionSyncrhonization. The spring class + * TransactionSynchronizationManager has public static methods that are used by + * the AbstractPlatformTransactionManager to ensure other resources are + * synchronizaed with it's transaction. This means that any spring transaction + * manager which extends the afore mentioned abstract class will be notified of + * changes in a transaction. For more information on the type of transaction + * events that can be handled, please see the TransactionSynchronization + * interface. + *

+ * + *

+ * Spring's intended use for the TransactionSyncrhonizationManager is that any + * class that wishes can register for the current transaction only. When commit + * or rollback is called, this list will be used to notify interested classes. + * However, once a new transaction is obtained, this list will be cleared. This + * is problematic for batch processing, since input templates need to always be + * made aware of transaction events, without being forced to register every + * time. To solve this issue, classes should register with the + * BatchTransactionFacade, which will ensure that any time a new transaction is + * obtained, they are re-registered with spring's transaction synchronization. + *

+ * + * @author Lucas Ward + * @author Dave Syer + * + * @see TransactionSynchronizationManager + * @see RepeatSynchronizationManager + * @see TransactionSynchronization + */ +public class BatchTransactionSynchronizationManager { + + /** + * The key in the context attributes for the list of synchronizations. + */ + private static final String SYNCHS_ATTR_KEY = BatchTransactionSynchronizationManager.class + ".SYNCHRONIZATIONS"; + + /** + * Static method to register synchronizations. A TransactionSyncrhonization + * object will be added to the internal list within a threadLocal. After + * ensuring that there is a reference for later re-synchronization, the + * object is added to spring's TransactionSynchronizationManager. + */ + public static void registerSynchronization(TransactionSynchronization synchronization) { + List synchs = (List) getSynchronizations(); + + if (!synchs.contains(synchronization)) { + synchs.add(synchronization); + if (TransactionSynchronizationManager.isSynchronizationActive() + && !TransactionSynchronizationManager.getSynchronizations().contains(synchronization)) { + TransactionSynchronizationManager.registerSynchronization(synchronization); + } + } + + } + + /** + * The internal list of synchronizations is iterated, and each + * synchronization object is registered with the + * TransactionSynchronizationManager again. This is necessary because any + * call to PlatformTransactionManager.getTransaction() will result in a + * clearing of the synchronizationManager's list. + */ + public static void resynchronize() { + List batchSynchs = (List) getSynchronizations(); + + if (batchSynchs != null) { + for (Iterator it = batchSynchs.iterator(); it.hasNext();) { + TransactionSynchronization synchronization = (TransactionSynchronization) it.next(); + if (TransactionSynchronizationManager.isSynchronizationActive() + && !TransactionSynchronizationManager.getSynchronizations().contains(synchronization)) { + TransactionSynchronizationManager.registerSynchronization(synchronization); + } + } + } + } + + /** + * Set the synchronizations list to null. Usually called when the step is + * complete, to ensure no issues when the next step is called within the + * same thread, which should only happen when running out of container. Does + * not throw an exception if there is no batch context. + */ + public static void clearSynchronizations() { + AttributeAccessor context = getContext(); + if (context == null) { + return; // Nothing to do + } + setSynchronizations(null); + } + + /** + * Set the current synchronizations to the given list. + * @param synchs a list of {@link TransactionSynchronization} instances. + * @throws IllegalStateException if there is no batch context available. + */ + private static void setSynchronizations(List synchs) { + AttributeAccessor context = getContext(); + if (context == null) { + return; + } + context.setAttribute(SYNCHS_ATTR_KEY, synchs); + } + + /** + * Get the current list of synchronizations if there is one. + * + * @return a list of {@link TransactionSynchronization} instances or null. + * + * @throws IllegalStateException if there is no batch context available. + */ + private static List getSynchronizations() { + + AttributeAccessor context = getContext(); + if (context == null) { + // TODO: this should return null or unmodifiable - there is no + // context for it + return new ArrayList(); + } + List synchs = (List) context.getAttribute(SYNCHS_ATTR_KEY); + + if (synchs == null) { + synchs = new ArrayList(); + } + + setSynchronizations(synchs); + + return synchs; + } + + /** + * @return the current context as an {@link AttributeAccessor}. + */ + private static AttributeAccessor getContext() { + RepeatContext context = RepeatSynchronizationManager.getContext(); + return getSynchContext(context); + } + + /** + * Locate the context that will contain the synchronisations by walking up + * the context hierarchy until the synchronisations are found or the top is + * reached. + * + * @param context + * @return + */ + private static AttributeAccessor getSynchContext(RepeatContext context) { + if (context == null || context.hasAttribute(SYNCHS_ATTR_KEY) || context.getParent() == null) { + return context; + } + return getSynchContext(context.getParent()); + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/repeat/synch/RepeatSynchronizationManager.java b/infrastructure/src/main/java/org/springframework/batch/repeat/synch/RepeatSynchronizationManager.java new file mode 100644 index 000000000..c2eb302be --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/repeat/synch/RepeatSynchronizationManager.java @@ -0,0 +1,98 @@ +/* + * 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.repeat.synch; + +import org.springframework.batch.repeat.RepeatCallback; +import org.springframework.batch.repeat.RepeatContext; +import org.springframework.batch.repeat.RepeatOperations; + +/** + * Global variable support for batch clients. Normally it is not necessary for + * batch clients to be aware of the surrounding batch environment because a + * {@link RepeatCallback} can always use the context it is passed by the + * enclosing {@link RepeatOperations}. But occasionally it might be helpful to + * have lower level access to the ongoing {@link RepeatContext} so we provide a + * global accessor here. + * + * @author Dave Syer + * + */ +public class RepeatSynchronizationManager { + + private static final ThreadLocal contextHolder = new ThreadLocal(); + + /** + * Getter for the current context. A context is shared by all items in the + * batch, so this method is intended to return the same context object + * independent of whether the callback is running synchronously or + * asynchronously with the surrounding {@link RepeatOperations}. + * + * @return the current {@link RepeatContext} or null if there is none (if we + * are not in a batch). + */ + public static RepeatContext getContext() { + return (RepeatContext) contextHolder.get(); + } + + /** + * Convenience method to set the current batch session to complete. + */ + public static void setCompleteOnly() { + RepeatContext context = getContext(); + if (context != null) { + context.setCompleteOnly(); + } + } + + /** + * Method for registering a context - should only be used by + * {@link RepeatOperations} implementations to ensure that + * {@link #getContext()} always returns the correct value. + * + * @param context a new context at the start of a batch. + * @return the old value if there was one. + */ + public static RepeatContext register(RepeatContext context) { + RepeatContext oldSession = getContext(); + RepeatSynchronizationManager.contextHolder.set(context); + return oldSession; + } + + /** + * Used internally by {@link RepeatOperations} implementations to clear the + * current context at the end of a batch. + * + * @return the old value if there was one. + */ + public static RepeatContext clear() { + RepeatContext context = getContext(); + RepeatSynchronizationManager.contextHolder.set(null); + return context; + } + + /** + * Set current session and all ancestors (via parent) to complete., + */ + public static void setAncestorsCompleteOnly() { + RepeatContext context = getContext(); + while (context != null) { + context.setCompleteOnly(); + context = context.getParent(); + } + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/repeat/synch/package.html b/infrastructure/src/main/java/org/springframework/batch/repeat/synch/package.html new file mode 100644 index 000000000..6494f15c5 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/repeat/synch/package.html @@ -0,0 +1,7 @@ + + +

+Infrastructure implementations of repeat synch concerns. +

+ + diff --git a/infrastructure/src/main/java/org/springframework/batch/restart/GenericRestartData.java b/infrastructure/src/main/java/org/springframework/batch/restart/GenericRestartData.java new file mode 100644 index 000000000..016f8956a --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/restart/GenericRestartData.java @@ -0,0 +1,33 @@ +/* + * 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.restart; + +import java.util.Properties; + +public class GenericRestartData implements RestartData { + + private Properties data; + + public GenericRestartData(Properties data){ + this.data = data; + } + + public Properties getProperties(){ + return data; + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/restart/RestartData.java b/infrastructure/src/main/java/org/springframework/batch/restart/RestartData.java new file mode 100644 index 000000000..475ac1982 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/restart/RestartData.java @@ -0,0 +1,27 @@ +/* + * 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.restart; + +import java.util.Properties; + +/** + * Interface for representing data necessary to recover state after restart. + */ +public interface RestartData { + + Properties getProperties(); +} diff --git a/infrastructure/src/main/java/org/springframework/batch/restart/Restartable.java b/infrastructure/src/main/java/org/springframework/batch/restart/Restartable.java new file mode 100644 index 000000000..9f5177d38 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/restart/Restartable.java @@ -0,0 +1,25 @@ +/* + * 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.restart; + + +public interface Restartable { + + RestartData getRestartData(); + + void restoreFrom(RestartData data); +} diff --git a/infrastructure/src/main/java/org/springframework/batch/restart/package.html b/infrastructure/src/main/java/org/springframework/batch/restart/package.html new file mode 100644 index 000000000..cea03cee0 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/restart/package.html @@ -0,0 +1,7 @@ + + +

+Infrastructure implementations of restart concerns. +

+ + diff --git a/infrastructure/src/main/java/org/springframework/batch/retry/RetryCallback.java b/infrastructure/src/main/java/org/springframework/batch/retry/RetryCallback.java new file mode 100644 index 000000000..79c3a4339 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/retry/RetryCallback.java @@ -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.retry; + +/** + * Callback interface for an operation that can be retried using a + * {@link RetryOperations}. + * + * @since 2.1 + * @author Rob Harrop + */ +public interface RetryCallback { + + /** + * Execute an operation with retry semantics. Operations should generally be + * idempotent, but implementations may choose to implement compensation + * semantics when an operation is retried. + * @param context the current retry context. + * @return the result of the successful operation. + */ + Object doWithRetry(RetryContext context) throws Throwable; +} diff --git a/infrastructure/src/main/java/org/springframework/batch/retry/RetryContext.java b/infrastructure/src/main/java/org/springframework/batch/retry/RetryContext.java new file mode 100644 index 000000000..a4800c1d7 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/retry/RetryContext.java @@ -0,0 +1,69 @@ +/* + * 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.retry; + +import org.springframework.core.AttributeAccessor; + +/** + * Low-level access to ongoing retry operation. Normally not needed by clients, + * but can be used to alter the course of the retry, e.g. force an early + * termination. + * + * @author Dave Syer + * + */ +public interface RetryContext extends AttributeAccessor { + + /** + * Signal to the framework that no more attempts should be made to try or + * retry the current {@link RetryCallback}. + */ + void setExhaustedOnly(); + + /** + * Public accessor for the exhausted flag {@link #setExhaustedOnly()}. + * + * @return true if the flag has been set. + */ + boolean isExhaustedOnly(); + + /** + * Accesssor for the parent context if retry blocks are nested. + * + * @return the parent or null if there is none. + */ + RetryContext getParent(); + + /** + * Counts the number of retry attempts. Before the first attempt this + * counter is zero, and before the first and subsequent attempts it should + * increment accordingly. + * + * @return the number of retries. + */ + int getRetryCount(); + + /** + * Accessor for the exception object that caused the current retry. + * + * @return the last exception that caused a retry, or possibly null. It will + * be null if this is the first attempt, but also if the enclosing policy + * decides not to provide it (e.g. because of concerns about memory usage). + */ + Throwable getLastThrowable(); + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/retry/RetryInterceptor.java b/infrastructure/src/main/java/org/springframework/batch/retry/RetryInterceptor.java new file mode 100644 index 000000000..7491d4d6f --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/retry/RetryInterceptor.java @@ -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.retry; + +import org.springframework.batch.retry.exception.TerminatedRetryException; + +/** + * Interface for interceptor that can be used to add behaviour to a retry. + * Implementations of {@link RetryOperations} can chose to issue callbacks to an + * interceptor during the retry lifecycle. + * + * @author Dave Syer + * + */ +public interface RetryInterceptor { + + /** + * Called before the first attempt in a retry. For instance, implementers + * can set up state that is needed by the policies in the + * {@link RetryOperations}. The whole retry can be vetoed by returning + * false from this method, in which case a {@link TerminatedRetryException} + * will be thrown. + * + * @param context the current {@link RetryContext}. + * @param callback the current {@link RetryCallback}. + * @return true if the retry should proceed. + */ + boolean open(RetryContext context, RetryCallback callback); + + /** + * Called after the final attempt (successful or not). Allow the interceptor + * to clean up any resource it is holding before control returns to the + * retry caller. + * + * @param context the current {@link RetryContext}. + * @param callback the current {@link RetryCallback}. + * @param throwable the last exception that was thrown by the callback. + */ + void close(RetryContext context, RetryCallback callback, Throwable throwable); + + /** + * Called after every unsuccessful attempt at a retry. + * + * @param context the current {@link RetryContext}. + * @param callback the current {@link RetryCallback}. + * @param throwable the last exception that was thrown by the callback. + */ + void onError(RetryContext context, RetryCallback callback, Throwable throwable); +} diff --git a/infrastructure/src/main/java/org/springframework/batch/retry/RetryOperations.java b/infrastructure/src/main/java/org/springframework/batch/retry/RetryOperations.java new file mode 100644 index 000000000..0c8355513 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/retry/RetryOperations.java @@ -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.retry; + +/** + * Defines the basic set of operations implemented by {@link RetryOperations} to + * execute operations with configurable retry behaviour. + * + * @author Rob Harrop + * @author Dave Syer + */ +public interface RetryOperations { + + /** + * Execute the supplied {@link RetryCallback} with the configured retry + * semantics. See implementations for configuration details. + * + * @return the value returned by the {@link RetryCallback} upon successful + * invocation. + * @throws Exception any {@link Exception} raised by the + * {@link RetryCallback} upon unsuccessful retry. + */ + Object execute(RetryCallback retryCallback) throws Exception; + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/retry/RetryPolicy.java b/infrastructure/src/main/java/org/springframework/batch/retry/RetryPolicy.java new file mode 100644 index 000000000..2acd7a331 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/retry/RetryPolicy.java @@ -0,0 +1,85 @@ +/* + * 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.retry; + +import org.springframework.batch.retry.exception.TerminatedRetryException; + +/** + * A {@link RetryPolicy} is responsible for allocating and managing resources + * needed by {@link RetryOperations}. The {@link RetryPolicy} allows retry + * operations to be aware of their context. Context can be internal to the retry + * framework, e.g. to support nested retries. Context can also be external, and + * the {@link RetryPolicy} provides a uniform API for a range of different + * platforms for the external context. + * + * @author Dave Syer + * + */ +public interface RetryPolicy { + + /** + * @param context the current retry status + * @return true if the operation can proceed + */ + boolean canRetry(RetryContext context); + + /** + * @param context the current context. + * @return true if the policy determines that the last exception should be + * rethrown. + */ + boolean shouldRethrow(RetryContext context); + + /** + * Acquire resources needed for the retry operation. The callback is passed + * in so that marker interfaces can be used and a manager can collaborate + * with the callback to set up some state in the status token. + * + * @param callback the {@link RetryCallback} that will execute the unit of + * work for this retry. + * @return a {@link RetryContext} object specific to this manager. + * + */ + RetryContext open(RetryCallback callback); + + /** + * @param status a retry status crated by the {@link #open(RetryCallback)} + * method of this manager. + */ + void close(RetryContext context); + + /** + * Called once per retry attempt, after the callback fails. + * + * @param context the current status object. + * + * @throws TerminatedRetryException if the status is set to terminate only. + * + */ + void registerThrowable(RetryContext context, Throwable throwable) throws TerminatedRetryException; + + /** + * Handle an exhausted retry. Default will be to throw an exception, but + * implementations may provide recovery path. + * + * @param context the current retry context. + * @return an appropriate value possibly from the callback. + * + * @throws Exception if there is no recovery path. + */ + Object handleRetryExhausted(RetryContext context) throws Exception; +} diff --git a/infrastructure/src/main/java/org/springframework/batch/retry/RetryStatistics.java b/infrastructure/src/main/java/org/springframework/batch/retry/RetryStatistics.java new file mode 100644 index 000000000..caf352a80 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/retry/RetryStatistics.java @@ -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.retry; + +/** + * Interface for statistics reporting of retry attempts. Counts the number of + * retry attempts, successes, errors (including retries), and aborts. + * + * @author Dave Syer + * + */ +public interface RetryStatistics { + + /** + * @return the number of completed retry attempts (successful or not). + */ + int getCompleteCount(); + + /** + * Get the number of times a retry block has been entered, irrespective of + * how many times the operation was retried. + * + * @return the number of retry blocks started. + */ + int getStartedCount(); + + /** + * Get the number of errors detected, whether or not they resulted in a + * retry. + * + * @return the number of errors detected. + */ + int getErrorCount(); + + /** + * Get the number of times a block failed to complete successfully, even + * after retry. + * + * @return the number of retry attempts that failed overall. + */ + int getAbortCount(); + + /** + * Get an identifier for the retry block for reporting purposes. + * + * @return an identifier for the block. + */ + String getName(); + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/retry/aop/RetryOperationsInterceptor.java b/infrastructure/src/main/java/org/springframework/batch/retry/aop/RetryOperationsInterceptor.java new file mode 100644 index 000000000..cada48ec0 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/retry/aop/RetryOperationsInterceptor.java @@ -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.retry.aop; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.springframework.batch.retry.RetryCallback; +import org.springframework.batch.retry.RetryContext; +import org.springframework.batch.retry.RetryOperations; +import org.springframework.batch.retry.support.RetryTemplate; +import org.springframework.util.Assert; + +/** + * @author Rob Harrop + * @author Dave Syer + * @since 2.1 + */ +public class RetryOperationsInterceptor implements MethodInterceptor { + + private RetryOperations retryTemplate = new RetryTemplate(); + + public void setRetryTemplate(RetryOperations retryTemplate) { + Assert.notNull(retryTemplate, "'retryTemplate' cannot be null."); + this.retryTemplate = retryTemplate; + } + + public Object invoke(final MethodInvocation methodInvocation) throws Throwable { + // TODO: use the method name to initialise a statistics context + return this.retryTemplate.execute(new RetryCallback() { + + public Object doWithRetry(RetryContext context) throws Throwable { + return methodInvocation.proceed(); + } + + }); + } +} diff --git a/infrastructure/src/main/java/org/springframework/batch/retry/aop/package.html b/infrastructure/src/main/java/org/springframework/batch/retry/aop/package.html new file mode 100644 index 000000000..2c8937847 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/retry/aop/package.html @@ -0,0 +1,7 @@ + + +

+Infrastructure implementations of retry aop concerns. +

+ + diff --git a/infrastructure/src/main/java/org/springframework/batch/retry/backoff/BackOffContext.java b/infrastructure/src/main/java/org/springframework/batch/retry/backoff/BackOffContext.java new file mode 100644 index 000000000..3a070f46b --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/retry/backoff/BackOffContext.java @@ -0,0 +1,25 @@ +/* + * 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.retry.backoff; + +/** + * @author Rob Harrop + * @since 2.1 + */ +public interface BackOffContext { + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/retry/backoff/BackOffPolicy.java b/infrastructure/src/main/java/org/springframework/batch/retry/backoff/BackOffPolicy.java new file mode 100644 index 000000000..fbed4521a --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/retry/backoff/BackOffPolicy.java @@ -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.retry.backoff; + +import org.springframework.batch.retry.RetryContext; +import org.springframework.batch.retry.exception.BackOffInterruptedException; + +/** + * Strategy interface to control back off between attempts in a single + * {@link org.springframework.batch.retry.support.RetryTemplate retry operation}. + *

Implementations are expected to be thread-safe and should be designed + * for concurrent access. Configuration for each implementation is also expected + * to be thread-safe but need not be suitable for high load concurrent access. + *

For each block of retry operations the {@link #start} method is called + * and implementations can return an implementation-specific + * {@link BackOffContext} that can be used to track state through subsequent + * back off invocations.

Each back off process is handled via a call to + * {@link #backOff}. The + * {@link org.springframework.batch.retry.support.RetryTemplate} will pass in + * the corresponding {@link BackOffContext} object created by the call to + * {@link #start}. + * + * @since 2.1 + * @author Rob Harrop + * @author Dave Syer + */ +public interface BackOffPolicy { + + /** + * Start a new block of back off operations. Implementations can choose to + * pause when this method is called, but normally it returns immediately. + * + * @param context the current retry context, which might contain information + * that we can use to decide how to proceed. + * @return the implementation-specific {@link BackOffContext} or 'null'. + */ + BackOffContext start(RetryContext context); + + /** + * Back off/pause in an implementation-specific fashion. The passed in + * {@link BackOffContext} corresponds to the one created by the call to + * {@link #start} for a given retry operation set. + * + * @throws BackOffInterruptedException if the attempt at back off is + * interrupted. + */ + void backOff(BackOffContext backOffContext) throws BackOffInterruptedException; + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/retry/backoff/ExponentialBackOffPolicy.java b/infrastructure/src/main/java/org/springframework/batch/retry/backoff/ExponentialBackOffPolicy.java new file mode 100644 index 000000000..bbb770ae6 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/retry/backoff/ExponentialBackOffPolicy.java @@ -0,0 +1,151 @@ +/* + * 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.retry.backoff; + +import org.springframework.batch.retry.RetryContext; +import org.springframework.batch.retry.exception.BackOffInterruptedException; +import org.springframework.util.ClassUtils; + +/** + * Implementation of {@link BackOffPolicy} that increases the back off period + * for each retry attempt in a given set using the {@link Math#exp exponential} + * function.

This implementation is thread-safe and suitable for concurrent + * access. Modifications to the configuration do not affect any retry sets that + * are already in progress.

The {@link #setExpSeed expSeed} property + * controls the initial value passed to {@link Math#exp} and the + * {@link #setIncrement increment} property controls by how much this value is + * increased for each subsequent attempt. + * + * @author Rob Harrop + * @author Dave Syer + * @since 2.1 + */ +public class ExponentialBackOffPolicy implements BackOffPolicy { + + /** + * The default 'initialInterval' value - 100 millisecs. Coupled with the + * default 'multiplier' value this gives a useful initial spread of pauses + * for 1-5 retries. + */ + public static final long DEFAULT_INITIAL_INTERVAL = 100L; + + /** + * The default maximum backoff time (30 seconds). + */ + public static final long DEFAULT_MAX_INTERVAL = 30000L; + + /** + * The default 'multiplier' value - value 2 (100% increase per backoff). + */ + public static final double DEFAULT_MULTIPLIER = 2; + + /** + * The initial sleep interval. + */ + private volatile long initialInterval = DEFAULT_INITIAL_INTERVAL; + + /** + * The maximum value of the backoff period in milliseconds. + */ + private volatile long maxInterval = DEFAULT_MAX_INTERVAL; + + /** + * The value to increment the exp seed with for each retry attempt. + */ + private volatile double multiplier = DEFAULT_MULTIPLIER; + + /** + * Set the initial sleep interval value. Default is 1 + * millisecond. Cannot be set to a value less than one. + */ + public void setInitialInterval(long initialInterval) { + this.initialInterval = (initialInterval > 1 ? initialInterval : 1); + } + + /** + * Set the multiplier value. Default is '1.0'. + */ + public void setMultiplier(double multiplier) { + this.multiplier = (multiplier > 1.0 ? multiplier : 1.0); + } + + /** + * Setter for maximum back off period. Default is 30000 (30 seconds). the + * value will be reset to 1 if this method is called with a value less than + * 1. + * + * @param maxInterval in milliseconds. + */ + public void setMaxInterval(long maxInterval) { + this.maxInterval = maxInterval > 0 ? maxInterval : 1; + } + + /** + * Returns a new instance of {@link ExponentialBackOffContext} configured + * with the 'expSeed' and 'increment' values. + */ + public BackOffContext start(RetryContext context) { + return new ExponentialBackOffContext(this.initialInterval, this.multiplier, this.maxInterval); + } + + /** + * Pause for a length of time equal to 'exp(backOffContext.expSeed)'. + */ + public void backOff(BackOffContext backOffContext) throws BackOffInterruptedException { + ExponentialBackOffContext context = (ExponentialBackOffContext) backOffContext; + try { + synchronized (context) { + context.wait(context.getSleepAndIncrement()); + } + } + catch (InterruptedException e) { + throw new BackOffInterruptedException("Thread interrupted while sleeping", e); + } + } + + private static class ExponentialBackOffContext implements BackOffContext { + + private final double multiplier; + + private long interval; + + private long maxInterval; + + public ExponentialBackOffContext(long expSeed, double multiplier, long maxInterval) { + this.interval = expSeed; + this.multiplier = multiplier; + this.maxInterval = maxInterval; + } + + public long getSleepAndIncrement() { + long sleep = this.interval; + if (sleep > maxInterval) { + sleep = (long) maxInterval; + } + else { + this.interval *= this.multiplier; + } + return sleep; + } + } + + public String toString() { + return ClassUtils.getShortName(getClass()) + "[initialInterval=" + initialInterval + ", multiplier=" + + multiplier + ", maxInterval=" + maxInterval + "]"; + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/retry/backoff/FixedBackOffPolicy.java b/infrastructure/src/main/java/org/springframework/batch/retry/backoff/FixedBackOffPolicy.java new file mode 100644 index 000000000..ec534dae5 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/retry/backoff/FixedBackOffPolicy.java @@ -0,0 +1,66 @@ +/* + * 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.retry.backoff; + +import org.springframework.batch.retry.exception.BackOffInterruptedException; + +/** + * Implementation of {@link BackOffPolicy} that pauses for a fixed period of + * time before continuing. A pause is implemented using {@link Thread#sleep}. + *

{@link #backOff} is thread-safe and it is safe to call + * {@link #setBackOffPeriod} during execution from multiple threads, however + * this may cause a single retry operation to have pauses of different + * intervals. + * @author Rob Harrop + * @since 2.1 + */ +public class FixedBackOffPolicy extends StatelessBackOffPolicy { + + /** + * Default back off period - 1000ms. + */ + private static final long DEFAULT_BACK_OFF_PERIOD = 1000L; + + /** + * The back off period in milliseconds. Defaults to 1000ms. + */ + private volatile long backOffPeriod = DEFAULT_BACK_OFF_PERIOD; + + /** + * Set the back off period in milliseconds. Cannot be < 1. Default value + * is 1000ms. + */ + public void setBackOffPeriod(long backOffPeriod) { + this.backOffPeriod = (backOffPeriod > 0 ? backOffPeriod : 1); + } + + /** + * Pause for the {@link #backOffPeriod} using {@link Thread#sleep}. + * @throws BackOffInterruptedException if interrupted during sleep. + */ + protected void doBackOff() throws BackOffInterruptedException { + try { + Object mutex = new Object(); + synchronized (mutex) { + mutex.wait(this.backOffPeriod); + } + } + catch (InterruptedException e) { + throw new BackOffInterruptedException("Thread interrupted while sleeping", e); + } + } +} diff --git a/infrastructure/src/main/java/org/springframework/batch/retry/backoff/NoBackOffPolicy.java b/infrastructure/src/main/java/org/springframework/batch/retry/backoff/NoBackOffPolicy.java new file mode 100644 index 000000000..594f8146a --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/retry/backoff/NoBackOffPolicy.java @@ -0,0 +1,32 @@ +/* + * 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.retry.backoff; + +import org.springframework.batch.retry.exception.BackOffInterruptedException; + +/** + * Implementation of {@link BackOffPolicy} that performs a no-op and as such all + * retry operation in a given set proceed one after the other with no pause. + * + * @author Rob Harrop + * @since 2.1 + */ +public class NoBackOffPolicy extends StatelessBackOffPolicy { + + protected void doBackOff() throws BackOffInterruptedException { + } +} diff --git a/infrastructure/src/main/java/org/springframework/batch/retry/backoff/StatelessBackOffPolicy.java b/infrastructure/src/main/java/org/springframework/batch/retry/backoff/StatelessBackOffPolicy.java new file mode 100644 index 000000000..8146fa4d7 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/retry/backoff/StatelessBackOffPolicy.java @@ -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.retry.backoff; + +import org.springframework.batch.retry.RetryContext; +import org.springframework.batch.retry.exception.BackOffInterruptedException; + +/** + * Simple base class for {@link BackOffPolicy} implementations that maintain no + * state across invocations. + * + * @author Rob Harrop + * @author Dave Syer + * @since 2.1 + */ +public abstract class StatelessBackOffPolicy implements BackOffPolicy { + + /** + * Delegates directly to the {@link #doBackOff()} method without passing on + * the {@link BackOffContext} argument which is not needed for stateless + * implementations. + */ + public final void backOff(BackOffContext backOffContext) throws BackOffInterruptedException { + doBackOff(); + } + + /** + * Returns 'null'. Subclasses can add behaviour, e.g. + * initial sleep before first attempt. + */ + public BackOffContext start(RetryContext status) { + return null; + } + + /** + * Sub-classes should implement this method to perform the actual back off. + */ + protected abstract void doBackOff() throws BackOffInterruptedException; +} diff --git a/infrastructure/src/main/java/org/springframework/batch/retry/backoff/package.html b/infrastructure/src/main/java/org/springframework/batch/retry/backoff/package.html new file mode 100644 index 000000000..53837ce90 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/retry/backoff/package.html @@ -0,0 +1,7 @@ + + +

+Infrastructure implementations of retry backoff concerns. +

+ + diff --git a/infrastructure/src/main/java/org/springframework/batch/retry/callback/ItemProviderRetryCallback.java b/infrastructure/src/main/java/org/springframework/batch/retry/callback/ItemProviderRetryCallback.java new file mode 100644 index 000000000..638dbec77 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/retry/callback/ItemProviderRetryCallback.java @@ -0,0 +1,101 @@ +/* + * 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.retry.callback; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.batch.item.ItemProcessor; +import org.springframework.batch.item.ItemProvider; +import org.springframework.batch.retry.RetryCallback; +import org.springframework.batch.retry.RetryContext; +import org.springframework.batch.retry.RetryPolicy; +import org.springframework.batch.retry.exception.ExhaustedRetryException; +import org.springframework.batch.retry.exception.RetryException; +import org.springframework.batch.retry.policy.ItemProviderRetryPolicy; + +/** + * A {@link RetryCallback} that knows about and caches the value from an + * {@link ItemProvider}. Used by the {@link ItemProviderRetryPolicy} to enable + * external retry of the item processing. + * + * @author Dave Syer + * + * @see ItemProviderRetryPolicy + * @see RetryPolicy#handleRetryExhausted(RetryContext) + * + */ +public class ItemProviderRetryCallback implements RetryCallback { + + private final static Log logger = LogFactory.getLog(ItemProviderRetryCallback.class); + + public static final String ITEM = ItemProviderRetryCallback.class + ".ITEM"; + + private ItemProvider provider; + + private ItemProcessor processor; + + public ItemProviderRetryCallback(ItemProvider provider, ItemProcessor processor) { + super(); + this.provider = provider; + this.processor = processor; + } + + public Object doWithRetry(RetryContext context) throws Throwable { + // This requires a collaboration with the RetryPolicy... + if (!context.isExhaustedOnly()) { + return process(context); + } + throw new RetryException("Recovery path requested in retry callback."); + } + + public Object next(RetryContext context) { + Object item = context.getAttribute(ITEM); + if (item == null) { + try { + item = provider.next(); + } + catch (Exception e) { + throw new ExhaustedRetryException("Unexpected end of item provider", e); + } + if (item == null) { + // This is probably not fatal: in a batch we want to + // exit gracefully... + logger.info("ItemProvider exhausted during retry."); + } + context.setAttribute(ITEM, item); + } + return item; + } + + private Object process(RetryContext context) throws Exception { + Object item = next(context); + if (item != null) { + processor.process(item); + } + context.removeAttribute(ITEM); // if successful + return item; + } + + /** + * Accessor for the {@link ItemProvider}. + * @return the provider. + */ + public ItemProvider getProvider() { + return provider; + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/retry/callback/package.html b/infrastructure/src/main/java/org/springframework/batch/retry/callback/package.html new file mode 100644 index 000000000..509e41ff5 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/retry/callback/package.html @@ -0,0 +1,7 @@ + + +

+Infrastructure implementations of retry callback concerns. +

+ + diff --git a/infrastructure/src/main/java/org/springframework/batch/retry/context/RetryContextSupport.java b/infrastructure/src/main/java/org/springframework/batch/retry/context/RetryContextSupport.java new file mode 100644 index 000000000..3d5e62456 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/retry/context/RetryContextSupport.java @@ -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.retry.context; + +import org.springframework.batch.retry.RetryContext; +import org.springframework.batch.retry.RetryPolicy; +import org.springframework.core.AttributeAccessorSupport; + +public class RetryContextSupport extends AttributeAccessorSupport implements RetryContext { + + private boolean terminate = false; + + private int count; + + private Throwable lastException; + + private RetryContext parent; + + public RetryContextSupport(RetryContext parent) { + super(); + this.parent = parent; + } + + public RetryContext getParent() { + return this.parent; + } + + public boolean isExhaustedOnly() { + return terminate; + } + + public void setExhaustedOnly() { + terminate = true; + } + + public int getRetryCount() { + return count; + } + + public Throwable getLastThrowable() { + return lastException; + } + + /** + * Set the exception for the public interface {@link RetryContext}, and + * also increment the retry count if the throwable is non-null.
+ * + * All {@link RetryPolicy} implementations should use this method when they + * register the throwable. It should only be called once per retry attempt + * because it increments the conter.
+ * + * Use of this method is not enforced by the framework - it is a service + * provider contract for authors of policies. + * + * @param throwable the exception that caused the current retry attempt to + * fail. + */ + public void registerThrowable(Throwable throwable) { + this.lastException = throwable; + if (throwable != null) + count++; + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/retry/context/package.html b/infrastructure/src/main/java/org/springframework/batch/retry/context/package.html new file mode 100644 index 000000000..d27756320 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/retry/context/package.html @@ -0,0 +1,7 @@ + + +

+Infrastructure implementations of retry context concerns. +

+ + diff --git a/infrastructure/src/main/java/org/springframework/batch/retry/exception/BackOffInterruptedException.java b/infrastructure/src/main/java/org/springframework/batch/retry/exception/BackOffInterruptedException.java new file mode 100644 index 000000000..72907b4cb --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/retry/exception/BackOffInterruptedException.java @@ -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.retry.exception; + +import org.springframework.batch.retry.backoff.BackOffPolicy; + +/** + * Exception class signifiying that an attempt to back off using a + * {@link BackOffPolicy} was interrupted, most likely by an + * {@link InterruptedException} during a call to {@link Thread#sleep}. + * + * @author Rob Harrop + * @since 2.1 + */ +public class BackOffInterruptedException extends RetryException { + + public BackOffInterruptedException(String msg) { + super(msg); + } + + public BackOffInterruptedException(String msg, Throwable cause) { + super(msg, cause); + } +} diff --git a/infrastructure/src/main/java/org/springframework/batch/retry/exception/ExhaustedRetryException.java b/infrastructure/src/main/java/org/springframework/batch/retry/exception/ExhaustedRetryException.java new file mode 100644 index 000000000..abdfae126 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/retry/exception/ExhaustedRetryException.java @@ -0,0 +1,29 @@ +/* + * 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.retry.exception; + +public class ExhaustedRetryException extends RetryException { + + public ExhaustedRetryException(String msg, Throwable cause) { + super(msg, cause); + } + + public ExhaustedRetryException(String msg) { + super(msg); + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/retry/exception/RetryException.java b/infrastructure/src/main/java/org/springframework/batch/retry/exception/RetryException.java new file mode 100644 index 000000000..50772f96c --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/retry/exception/RetryException.java @@ -0,0 +1,31 @@ +/* + * 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.retry.exception; + +import org.springframework.core.NestedRuntimeException; + +public class RetryException extends NestedRuntimeException { + + public RetryException(String msg, Throwable cause) { + super(msg, cause); + } + + public RetryException(String msg) { + super(msg); + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/retry/exception/TerminatedRetryException.java b/infrastructure/src/main/java/org/springframework/batch/retry/exception/TerminatedRetryException.java new file mode 100644 index 000000000..a79816e37 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/retry/exception/TerminatedRetryException.java @@ -0,0 +1,29 @@ +/* + * 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.retry.exception; + +public class TerminatedRetryException extends RetryException { + + public TerminatedRetryException(String msg, Throwable cause) { + super(msg, cause); + } + + public TerminatedRetryException(String msg) { + super(msg); + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/retry/exception/package.html b/infrastructure/src/main/java/org/springframework/batch/retry/exception/package.html new file mode 100644 index 000000000..91c6d6288 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/retry/exception/package.html @@ -0,0 +1,7 @@ + + +

+Infrastructure implementations of retry exception concerns. +

+ + diff --git a/infrastructure/src/main/java/org/springframework/batch/retry/interceptor/RetryInterceptorSupport.java b/infrastructure/src/main/java/org/springframework/batch/retry/interceptor/RetryInterceptorSupport.java new file mode 100644 index 000000000..95101e10a --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/retry/interceptor/RetryInterceptorSupport.java @@ -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.retry.interceptor; + +import org.springframework.batch.retry.RetryCallback; +import org.springframework.batch.retry.RetryContext; +import org.springframework.batch.retry.RetryInterceptor; + +public class RetryInterceptorSupport implements RetryInterceptor { + + public void close(RetryContext context, RetryCallback callback, Throwable throwable) { + } + + public void onError(RetryContext context, RetryCallback callback, Throwable throwable) { + } + + public boolean open(RetryContext context, RetryCallback callback) { + return true; + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/retry/interceptor/StatisticsRetryInterceptor.java b/infrastructure/src/main/java/org/springframework/batch/retry/interceptor/StatisticsRetryInterceptor.java new file mode 100644 index 000000000..9cd530d2e --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/retry/interceptor/StatisticsRetryInterceptor.java @@ -0,0 +1,87 @@ +/* + * 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.retry.interceptor; + +import org.springframework.batch.retry.RetryCallback; +import org.springframework.batch.retry.RetryContext; +import org.springframework.batch.retry.RetryInterceptor; +import org.springframework.batch.retry.RetryStatistics; + +/** + * A {@link RetryInterceptor} that counts the number of attempts, errors and + * successful retry operations. + * + * @author Dave Syer + * + */ +public class StatisticsRetryInterceptor extends RetryInterceptorSupport implements RetryStatistics { + + private int startedCount; + + private int completeCount; + + private int errorCount; + + private int abortCount; + + private String name; + + public synchronized int getAbortCount() { + return abortCount; + } + + public synchronized int getCompleteCount() { + return completeCount; + } + + public synchronized int getErrorCount() { + return errorCount; + } + + public synchronized int getStartedCount() { + return startedCount; + } + + public String getName() { + return name == null ? this.toString() : name; + } + + public void setName(String name) { + this.name = name; + } + + public synchronized boolean open(RetryContext context, RetryCallback callback) { + startedCount++; + return super.open(context, callback); + } + + public synchronized void onError(RetryContext context, RetryCallback callback, Throwable throwable) { + errorCount++; + super.onError(context, callback, throwable); + } + + public synchronized void close(RetryContext context, RetryCallback callback, Throwable throwable) { + if (throwable != null) { + abortCount++; + } + else { + completeCount++; + } + super.close(context, callback, throwable); + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/retry/interceptor/package.html b/infrastructure/src/main/java/org/springframework/batch/retry/interceptor/package.html new file mode 100644 index 000000000..f75aebfc2 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/retry/interceptor/package.html @@ -0,0 +1,7 @@ + + +

+Infrastructure implementations of retry interceptor concerns. +

+ + diff --git a/infrastructure/src/main/java/org/springframework/batch/retry/package.html b/infrastructure/src/main/java/org/springframework/batch/retry/package.html new file mode 100644 index 000000000..eb1cc9dc1 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/retry/package.html @@ -0,0 +1,7 @@ + + +

+Infrastructure implementations of retry concerns. +

+ + diff --git a/infrastructure/src/main/java/org/springframework/batch/retry/policy/AbstractStatefulRetryPolicy.java b/infrastructure/src/main/java/org/springframework/batch/retry/policy/AbstractStatefulRetryPolicy.java new file mode 100644 index 000000000..b764585b8 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/retry/policy/AbstractStatefulRetryPolicy.java @@ -0,0 +1,130 @@ +/* + * 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.retry.policy; + +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import org.springframework.batch.retry.RetryContext; +import org.springframework.batch.retry.RetryPolicy; + +/** + * Base class for stateful retry policies: those that operate in the context of + * a callback that is called once per retry execution (usually to enforce that + * it is only called once per transaction). Stateful policies need to remember + * the context for the operation that failed (e.g. the data item that was being + * processed), and decide based on its history what to do in the current + * context. For example: the retry operation includes receiving a message, and + * we need it to roll back and be re-delivered so that we can have another crack + * at it. + * + * @see RetryPolicy#handleRetryExhausted(RetryContext) + * @see AbstractStatelessRetryPolicy + * + * @author Dave Syer + * + */ +public abstract class AbstractStatefulRetryPolicy implements RetryPolicy { + + private volatile Set recoverableExceptionClasses = new HashSet(); + + protected RetryContextCache retryContextCache = new MapRetryContextCache(); + + /** + * Optional setter for the retry context cache. The default value is a + * {@link MapRetryContextCache}. + * + * @param retryContextCache + */ + public void setRetryContextCache(RetryContextCache retryContextCache) { + this.retryContextCache = retryContextCache; + } + + /** + * Return null. Subclasses should provide a recovery path if possible. + * + * @see org.springframework.batch.retry.RetryPolicy#handleRetryExhausted(org.springframework.batch.retry.RetryContext) + */ + public Object handleRetryExhausted(RetryContext context) throws Exception { + return null; + } + + /** + * For a stateful policy the default is to always rethrow. This is the + * cautious approach: we assume that the failed processing may have written + * data to a transactional resource, so we rethrow and force a rollback. Any + * recovery path that may be available has to be taken on the next attempt, + * before any processing has taken place. + * + * @return true unless the last exception registered was recoverable. + */ + public boolean shouldRethrow(RetryContext context) { + return !recoverForException(context.getLastThrowable()); + } + + /** + * Set the recoverable exceptions. Any exception on the list, or subclasses + * thereof, will be recoverable. If it is encountered in a retry block it + * will not be rethrown. Others will be rethrown. The recovery action (if + * any) is left to subclasses - normally they would override + * {@link #handleRetryExhausted(RetryContext)}. + * + * @param retryableExceptionClasses defaults to {@link Exception}. + */ + public final void setRecoverableExceptionClasses(Class[] retryableExceptionClasses) { + Set temp = new HashSet(); + for (int i = 0; i < retryableExceptionClasses.length; i++) { + addRecoverableExceptionClass(retryableExceptionClasses[i], temp); + } + this.recoverableExceptionClasses = temp; + } + + private void addRecoverableExceptionClass(Class retryableExceptionClass, Set set) { + if (!Throwable.class.isAssignableFrom(retryableExceptionClass)) { + throw new IllegalArgumentException("Class '" + retryableExceptionClass.getName() + + "' is not a subtype of Throwable."); + } + set.add(retryableExceptionClass); + } + + protected boolean recoverForException(Throwable ex) { + + // Default is false (but this shouldn't really happen in practice - + // maybe in tests): + if (ex == null) { + return false; + } + + Class exceptionClass = ex.getClass(); + if (recoverableExceptionClasses.contains(exceptionClass)) { + return true; + } + + // check for subclasses + for (Iterator iterator = recoverableExceptionClasses.iterator(); iterator.hasNext();) { + Class cls = (Class) iterator.next(); + if (cls.isAssignableFrom(exceptionClass)) { + addRecoverableExceptionClass(exceptionClass, this.recoverableExceptionClasses); + return true; + } + } + + return false; + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/retry/policy/AbstractStatelessRetryPolicy.java b/infrastructure/src/main/java/org/springframework/batch/retry/policy/AbstractStatelessRetryPolicy.java new file mode 100644 index 000000000..9bd74e891 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/retry/policy/AbstractStatelessRetryPolicy.java @@ -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.retry.policy; + +import org.springframework.batch.retry.RetryContext; +import org.springframework.batch.retry.RetryPolicy; +import org.springframework.batch.retry.exception.ExhaustedRetryException; + +/** + * Base class for "normal" retry policies: those that operate in the context of + * a callback that is called repeatedly in a loop until it succeeds, or the + * policy decides to terminate. There is no need for such policies to store + * state outside the context. + * + * @see RetryPolicy#handleRetryExhausted(RetryContext) + * @see AbstractStatefulRetryPolicy + * + * @author Dave Syer + * + */ +public abstract class AbstractStatelessRetryPolicy implements RetryPolicy { + + /** + * Just returns the negative of {@link RetryPolicy#canRetry(RetryContext)}, + * i.e. if we cannot retry then the exception should be thrown. + * + * @see org.springframework.batch.retry.RetryPolicy#shouldRethrow(org.springframework.batch.retry.RetryContext) + */ + public boolean shouldRethrow(RetryContext context) { + return !canRetry(context); + } + + /** + * Throw an exception. + * + * @see org.springframework.batch.retry.RetryPolicy#handleRetryExhausted(org.springframework.batch.retry.RetryContext) + */ + public Object handleRetryExhausted(RetryContext context) throws Exception { + throw new ExhaustedRetryException("Retry exhausted after last attempt with no recovery path.", context + .getLastThrowable()); + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/retry/policy/AlwaysRetryPolicy.java b/infrastructure/src/main/java/org/springframework/batch/retry/policy/AlwaysRetryPolicy.java new file mode 100644 index 000000000..f2aa4798a --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/retry/policy/AlwaysRetryPolicy.java @@ -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.retry.policy; + +import org.springframework.batch.retry.RetryContext; +import org.springframework.batch.retry.RetryPolicy; + +/** + * A {@link RetryPolicy} that always permits a retry. Can also be used as a base + * class for other policies, e.g. for test purposes as a stub. + * + * @author Dave Syer + * + */ +public class AlwaysRetryPolicy extends NeverRetryPolicy { + + /** + * Always returns true. + * + * @see org.springframework.batch.retry.RetryPolicy#canRetry(org.springframework.batch.retry.RetryContext) + */ + public boolean canRetry(RetryContext context) { + return true; + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/retry/policy/CompositeRetryPolicy.java b/infrastructure/src/main/java/org/springframework/batch/retry/policy/CompositeRetryPolicy.java new file mode 100644 index 000000000..7cce15821 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/retry/policy/CompositeRetryPolicy.java @@ -0,0 +1,125 @@ +/* + * 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.retry.policy; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.batch.retry.RetryCallback; +import org.springframework.batch.retry.RetryContext; +import org.springframework.batch.retry.RetryPolicy; +import org.springframework.batch.retry.context.RetryContextSupport; +import org.springframework.batch.retry.exception.TerminatedRetryException; +import org.springframework.batch.retry.synch.RetrySynchronizationManager; + +/** + * A {@link RetryPolicy} that composes a list of other policies and delegates + * calls to them in order. + * + * @author Dave Syer + * + */ +public class CompositeRetryPolicy extends AbstractStatelessRetryPolicy { + + RetryPolicy[] policies = new RetryPolicy[0]; + + /** + * Setter for policies. + * + * @param policies + */ + public void setPolicies(RetryPolicy[] policies) { + this.policies = policies; + } + + /** + * Delegate to the policies that were in operation when the context was + * created. If any of them cannot retry then return false, oetherwise return + * true. + * + * @see org.springframework.batch.retry.RetryPolicy#canRetry(org.springframework.batch.retry.RetryContext) + */ + public boolean canRetry(RetryContext context) { + RetryContext[] contexts = ((CompositeRetryContext) context).contexts; + RetryPolicy[] policies = ((CompositeRetryContext) context).policies; + for (int i = 0; i < contexts.length; i++) { + if (!policies[i].canRetry(contexts[i])) { + return false; + } + } + return true; + } + + /** + * Delegate to the policies that were in operation when the context was + * created. + * + * @see org.springframework.batch.retry.RetryPolicy#close(org.springframework.batch.retry.RetryContext) + */ + public void close(RetryContext context) { + RetryContext[] contexts = ((CompositeRetryContext) context).contexts; + RetryPolicy[] policies = ((CompositeRetryContext) context).policies; + // TODO: throw some sort of composite exception if any of the close + // methods fail? + for (int i = 0; i < contexts.length; i++) { + policies[i].close(contexts[i]); + } + } + + /** + * Creates a new context that copies the existing policies and keeps a list + * of the contexts from each one. + * + * @see org.springframework.batch.retry.RetryPolicy#open(org.springframework.batch.retry.RetryCallback) + */ + public RetryContext open(RetryCallback callback) { + List list = new ArrayList(); + for (int i = 0; i < policies.length; i++) { + list.add(policies[i].open(callback)); + } + return new CompositeRetryContext(list); + } + + /** + * Delegate to the policies that were in operation when the context was + * created. + * + * @see org.springframework.batch.retry.RetryPolicy#close(org.springframework.batch.retry.RetryContext) + */ + public void registerThrowable(RetryContext context, Throwable throwable) throws TerminatedRetryException { + RetryContext[] contexts = ((CompositeRetryContext) context).contexts; + RetryPolicy[] policies = ((CompositeRetryContext) context).policies; + for (int i = 0; i < contexts.length; i++) { + policies[i].registerThrowable(contexts[i], throwable); + } + ((RetryContextSupport) context).registerThrowable(throwable); + } + + private class CompositeRetryContext extends RetryContextSupport { + RetryContext[] contexts; + + RetryPolicy[] policies; + + public CompositeRetryContext(List contexts) { + super(RetrySynchronizationManager.getContext()); + this.contexts = (RetryContext[]) contexts.toArray(new RetryContext[0]); + this.policies = CompositeRetryPolicy.this.policies; + } + + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/retry/policy/ExceptionClassifierRetryPolicy.java b/infrastructure/src/main/java/org/springframework/batch/retry/policy/ExceptionClassifierRetryPolicy.java new file mode 100644 index 000000000..909cc5df3 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/retry/policy/ExceptionClassifierRetryPolicy.java @@ -0,0 +1,187 @@ +/* + * 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.retry.policy; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import org.springframework.batch.common.ExceptionClassifier; +import org.springframework.batch.common.ExceptionClassifierSupport; +import org.springframework.batch.retry.RetryCallback; +import org.springframework.batch.retry.RetryContext; +import org.springframework.batch.retry.RetryPolicy; +import org.springframework.batch.retry.context.RetryContextSupport; +import org.springframework.batch.retry.exception.TerminatedRetryException; +import org.springframework.batch.retry.synch.RetrySynchronizationManager; +import org.springframework.util.Assert; + +/** + * A {@link RetryPolicy} that dynamically adapts to one of a set of injected + * policies according to the value of the latest exception. + * + * @author Dave Syer + * + */ +public class ExceptionClassifierRetryPolicy extends AbstractStatelessRetryPolicy { + + private ExceptionClassifier exceptionClassifier = new ExceptionClassifierSupport(); + + private Map policyMap = new HashMap(); + + public ExceptionClassifierRetryPolicy() { + policyMap.put(ExceptionClassifierSupport.DEFAULT, new NeverRetryPolicy()); + } + + /** + * Setter for policy map. This property should not be changed dynamically - + * set it once, e.g. in configuration, and then don't change it during a + * running application. + * + * @param policyMap a map of String to {@link RetryPolicy} that will be + * applied to the result of the {@link ExceptionClassifier} to locate a + * policy. + */ + public void setPolicyMap(Map policyMap) { + this.policyMap = policyMap; + } + + /** + * Setter for an exception classifier. The classifier is responsible for + * translating exceptions to keys in the policy map. + * + * @param exceptionClassifier + */ + public void setExceptionClassifier(ExceptionClassifier exceptionClassifier) { + this.exceptionClassifier = exceptionClassifier; + } + + /** + * Delegate to the policy currently activated in the context. + * + * @see org.springframework.batch.retry.RetryPolicy#canRetry(org.springframework.batch.retry.RetryContext) + */ + public boolean canRetry(RetryContext context) { + RetryPolicy policy = (RetryPolicy) context; + return policy.canRetry(context); + } + + /** + * Delegate to the policy currently activated in the context. + * + * @see org.springframework.batch.retry.RetryPolicy#close(org.springframework.batch.retry.RetryContext) + */ + public void close(RetryContext context) { + RetryPolicy policy = (RetryPolicy) context; + policy.close(context); + } + + /** + * Create an active context that proxies a retry policy by chosing a target + * from the policy map. + * + * @see org.springframework.batch.retry.RetryPolicy#open(org.springframework.batch.retry.RetryCallback) + */ + public RetryContext open(RetryCallback callback) { + return new ExceptionClassifierRetryContext(exceptionClassifier).open(callback); + } + + /** + * Delegate to the policy currently activated in the context. + * + * @see org.springframework.batch.retry.RetryPolicy#registerThrowable(org.springframework.batch.retry.RetryContext, + * java.lang.Throwable) + */ + public void registerThrowable(RetryContext context, Throwable throwable) throws TerminatedRetryException { + RetryPolicy policy = (RetryPolicy) context; + policy.registerThrowable(context, throwable); + ((RetryContextSupport) context).registerThrowable(throwable); + } + + private class ExceptionClassifierRetryContext extends RetryContextSupport implements RetryPolicy { + + private ExceptionClassifier exceptionClassifier; + + // Dynamic: depends on the latest exception: + RetryPolicy policy; + + // Dynamic: depends on the policy: + RetryContext context; + + // The same for the life of the context: + RetryCallback callback; + + Map contexts = new HashMap(); + + public ExceptionClassifierRetryContext(ExceptionClassifier exceptionClassifier) { + super(RetrySynchronizationManager.getContext()); + this.exceptionClassifier = exceptionClassifier; + Object key = exceptionClassifier.getDefault(); + policy = getPolicy(key); + Assert.notNull(policy, "Could not locate default policy: key=[" + key + "]."); + } + + public boolean canRetry(RetryContext context) { + return policy.canRetry(this.context); + } + + public boolean shouldRethrow(RetryContext context) { + return policy.shouldRethrow(context); + } + + public void close(RetryContext context) { + // Only close those policies that have been used (opened): + for (Iterator iter = contexts.keySet().iterator(); iter.hasNext();) { + RetryPolicy policy = (RetryPolicy) iter.next(); + policy.close(getContext(policy)); + } + } + + public RetryContext open(RetryCallback callback) { + this.callback = callback; + return this; + } + + public void registerThrowable(RetryContext context, Throwable throwable) throws TerminatedRetryException { + policy = getPolicy(exceptionClassifier.classify(throwable)); + this.context = getContext(policy); + policy.registerThrowable(this.context, throwable); + } + + private RetryContext getContext(RetryPolicy policy) { + RetryContext context = (RetryContext) contexts.get(policy); + if (context == null) { + context = policy.open(callback); + contexts.put(policy, context); + } + return context; + } + + private RetryPolicy getPolicy(Object key) { + RetryPolicy result = (RetryPolicy) policyMap.get(key); + Assert.notNull(result, "Could not locate policy for key=[" + key + "]."); + return result; + } + + public Object handleRetryExhausted(RetryContext context) throws Exception { + // Not called... + throw new UnsupportedOperationException("Not supported - this code should be unreachable."); + } + + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/retry/policy/ItemProviderRetryPolicy.java b/infrastructure/src/main/java/org/springframework/batch/retry/policy/ItemProviderRetryPolicy.java new file mode 100644 index 000000000..643f53eb5 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/retry/policy/ItemProviderRetryPolicy.java @@ -0,0 +1,231 @@ +/* + * 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.retry.policy; + +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.repeat.synch.RepeatSynchronizationManager; +import org.springframework.batch.retry.RetryCallback; +import org.springframework.batch.retry.RetryContext; +import org.springframework.batch.retry.RetryPolicy; +import org.springframework.batch.retry.callback.ItemProviderRetryCallback; +import org.springframework.batch.retry.context.RetryContextSupport; +import org.springframework.batch.retry.exception.TerminatedRetryException; +import org.springframework.batch.retry.synch.RetrySynchronizationManager; +import org.springframework.util.Assert; + +/** + * A {@link RetryPolicy} that detects an {@link ItemProviderRetryCallback} when + * it opens a new context, and uses it to make sure the item is in place for + * later decisions about how to retry or backoff. The callback should be an + * instance of {@link ItemProviderRetryCallback} otherwise an exception will be + * thrown when the context is created. + * + * @author Dave Syer + * + */ +public class ItemProviderRetryPolicy extends AbstractStatefulRetryPolicy { + + protected Log logger = LogFactory.getLog(getClass()); + + public static final String EXHAUSTED = ItemProviderRetryPolicy.class + ".EXHAUSTED"; + + private RetryPolicy delegate; + + /** + * Convenience constructor to set delegate on init. + * + * @param delegate + */ + public ItemProviderRetryPolicy(RetryPolicy delegate) { + super(); + this.delegate = delegate; + } + + /** + * Default constructor. Creates a new {@link SimpleRetryPolicy} for the + * delegate. + */ + public ItemProviderRetryPolicy() { + this(new SimpleRetryPolicy()); + } + + /** + * Setter for delegate. + * + * @param delegate + */ + public void setDelegate(RetryPolicy delegate) { + this.delegate = delegate; + } + + /** + * Check the history of this item, and if it has reached the retry limit, + * then return false. + * + * @see org.springframework.batch.retry.RetryPolicy#canRetry(org.springframework.batch.retry.RetryContext) + */ + public boolean canRetry(RetryContext context) { + return ((RetryPolicy) context).canRetry(context); + } + + /** + * Delegates to the delegate context. + * + * @see org.springframework.batch.retry.RetryPolicy#close(org.springframework.batch.retry.RetryContext) + */ + public void close(RetryContext context) { + ((RetryPolicy) context).close(context); + } + + /** + * Create a new context for the execution of the callback, which must be an + * instance of {@link ItemProviderRetryCallback}. + * + * @see org.springframework.batch.retry.RetryPolicy#open(org.springframework.batch.retry.RetryCallback) + * + * @throws IllegalStateException if the callback is not of the required + * type. + */ + public RetryContext open(RetryCallback callback) { + Assert.state(callback instanceof ItemProviderRetryCallback, "Callback must be ItemProviderRetryCallback"); + ItemProviderRetryContext context = new ItemProviderRetryContext((ItemProviderRetryCallback) callback); + context.open(callback); + return context; + } + + /** + * If {@link #canRetry(RetryContext)} is false then take remedial action (if + * implemented by subclasses), and remove the current item from the history. + * + * @see org.springframework.batch.retry.RetryPolicy#registerThrowable(org.springframework.batch.retry.RetryContext, + * java.lang.Throwable) + */ + public void registerThrowable(RetryContext context, Throwable throwable) throws TerminatedRetryException { + ((RetryPolicy) context).registerThrowable(context, throwable); + // The throwable is stored in the delegate context. + } + + /** + * Call recovery path (if any) and clean up context history. + * + * @see org.springframework.batch.retry.policy.AbstractStatefulRetryPolicy#handleRetryExhausted(org.springframework.batch.retry.RetryContext) + */ + public Object handleRetryExhausted(RetryContext context) throws Exception { + return ((RetryPolicy) context).handleRetryExhausted(context); + } + + private class ItemProviderRetryContext extends RetryContextSupport implements RetryPolicy { + + private Object item; + + // The delegate context... + private RetryContext delegateContext; + + private ItemProvider provider; + + public ItemProviderRetryContext(ItemProviderRetryCallback callback) { + super(RetrySynchronizationManager.getContext()); + item = callback.next(this); + this.provider = callback.getProvider(); + } + + public boolean canRetry(RetryContext context) { + return delegate.canRetry(this.delegateContext); + } + + public void close(RetryContext context) { + delegate.close(this.delegateContext); + } + + public RetryContext open(RetryCallback callback) { + if (hasFailed(provider, item)) { + this.delegateContext = retryContextCache.get(provider.getKey(item)); + } + if (this.delegateContext == null) { + // Only create a new context if we don't know the history of + // this item: + this.delegateContext = delegate.open(callback); + } + // The return value shouldn't be used... + return null; + } + + public void registerThrowable(RetryContext context, Throwable throwable) throws TerminatedRetryException { + retryContextCache.put(provider.getKey(item), this.delegateContext); + delegate.registerThrowable(this.delegateContext, throwable); + } + + public boolean isExternal() { + // Not called... + throw new UnsupportedOperationException("Not supported - this code should be unreachable."); + } + + public boolean shouldRethrow(RetryContext context) { + // Not called... + throw new UnsupportedOperationException("Not supported - this code should be unreachable."); + } + + public Object handleRetryExhausted(RetryContext context) throws Exception { + // If there is no going back, then we can remove the history + retryContextCache.remove(provider.getKey(item)); + RepeatSynchronizationManager.setCompleteOnly(); + boolean success = provider.recover(item, context.getLastThrowable()); + if (!success) { + //TODO if context was null, there would be exception while getting success value + String count = context != null ? "" + context.getRetryCount() : "unknown"; + logger.error("Could not recover from error after retry exhausted after [" + count + "] attempts.", + context.getLastThrowable()); + } + return item; + } + + public Throwable getLastThrowable() { + return delegateContext.getLastThrowable(); + } + + public int getRetryCount() { + return delegateContext.getRetryCount(); + } + + } + + /** + * Extension point for cases where it is possible to avoid a cache hit by + * inspecting the item to determine if could ever have been seen before. In + * a messaging environment where the item is a message, it can be inspected + * to see if it has been delivered before.
+ * + * The default implementation of this method checks the provider for a mixin + * interface {@link FailedItemIdentifier}. If the interface is present the + * decision is delegated to the provider. Otherwise we just check the cache + * for the item key. + * + * @param provider + * @param item + * @return + */ + protected boolean hasFailed(ItemProvider provider, Object item) { + if (provider instanceof FailedItemIdentifier) { + return ((FailedItemIdentifier) provider).hasFailed(item); + } + return retryContextCache.containsKey(provider.getKey(item)); + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/retry/policy/MapRetryContextCache.java b/infrastructure/src/main/java/org/springframework/batch/retry/policy/MapRetryContextCache.java new file mode 100644 index 000000000..06ce25c11 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/retry/policy/MapRetryContextCache.java @@ -0,0 +1,52 @@ +/* + * 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.retry.policy; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.batch.retry.RetryContext; + +/** + * Map-based implementation of {@link RetryContextCache}. The map backing the + * cache of contexts is sytchronized. + * + * @author Dave Syer + * + */ +public class MapRetryContextCache implements RetryContextCache { + + private Map map = Collections.synchronizedMap(new HashMap()); + + public boolean containsKey(Object key) { + return map.containsKey(key); + } + + public RetryContext get(Object key) { + return (RetryContext) map.get(key); + } + + public void put(Object key, RetryContext context) { + map.put(key, context); + } + + public void remove(Object key) { + map.remove(key); + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/retry/policy/NeverRetryPolicy.java b/infrastructure/src/main/java/org/springframework/batch/retry/policy/NeverRetryPolicy.java new file mode 100644 index 000000000..e9e12fbc6 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/retry/policy/NeverRetryPolicy.java @@ -0,0 +1,102 @@ +/* + * 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.retry.policy; + +import org.springframework.batch.retry.RetryCallback; +import org.springframework.batch.retry.RetryContext; +import org.springframework.batch.retry.RetryPolicy; +import org.springframework.batch.retry.context.RetryContextSupport; +import org.springframework.batch.retry.exception.TerminatedRetryException; +import org.springframework.batch.retry.synch.RetrySynchronizationManager; + +/** + * A {@link RetryPolicy} that allows the first attempt but never permits a + * retry. Also be used as a base class for other policies, e.g. for test + * purposes as a stub. + * + * @author Dave Syer + * + */ +public class NeverRetryPolicy extends AbstractStatelessRetryPolicy { + + /** + * Returns false after the first exception. So there is always one try, and + * then the retry is prevented. + * + * @see org.springframework.batch.retry.RetryPolicy#canRetry(org.springframework.batch.retry.RetryContext) + */ + public boolean canRetry(RetryContext context) { + return !((NeverRetryContext) context).isFinished(); + } + + /** + * Do nothing. + * + * @see org.springframework.batch.retry.RetryPolicy#close(org.springframework.batch.retry.RetryContext) + */ + public void close(RetryContext context) { + // no-op + } + + /** + * Return a context that can respond to early termination requests, but does + * nothing else. + * + * @see org.springframework.batch.retry.RetryPolicy#open(org.springframework.batch.retry.RetryCallback) + */ + public RetryContext open(RetryCallback callback) { + return new NeverRetryContext(RetrySynchronizationManager.getContext()); + } + + /** + * Do nothing. + * @see org.springframework.batch.retry.RetryPolicy#registerThrowable(org.springframework.batch.retry.RetryContext, + * java.lang.Throwable) + */ + public void registerThrowable(RetryContext context, Throwable throwable) throws TerminatedRetryException { + ((NeverRetryContext) context).setFinished(); + ((RetryContextSupport) context).registerThrowable(throwable); + } + + /** + * Special context object for {@link NeverRetryPolicy}. Implements a flag + * with a similar function to {@link RetryContext#isExhaustedOnly()}, but + * kept separate so that if subclasses of {@link NeverRetryPolicy} need to + * they can modify the behaviour of + * {@link NeverRetryPolicy#canRetry(RetryContext)} without affecting + * {@link RetryContext#isExhaustedOnly()}. + * + * @author Dave Syer + * + */ + private static class NeverRetryContext extends RetryContextSupport { + private boolean finished = false; + + public NeverRetryContext(RetryContext parent) { + super(parent); + } + + public boolean isFinished() { + return finished; + } + + public void setFinished() { + this.finished = true; + } + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/retry/policy/RetryContextCache.java b/infrastructure/src/main/java/org/springframework/batch/retry/policy/RetryContextCache.java new file mode 100644 index 000000000..b67bf8f20 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/retry/policy/RetryContextCache.java @@ -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.retry.policy; + +import org.springframework.batch.retry.RetryContext; + +/** + * Simple map-like bstraction for stateful retry policies to use when storing + * and retrieving {@link RetryContext} instances. + * + * @author Dave Syer + * + * @see MapRetryContextCache + * + */ +public interface RetryContextCache { + + RetryContext get(Object key); + + void put(Object key, RetryContext context); + + void remove(Object key); + + boolean containsKey(Object key); + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/retry/policy/SimpleRetryPolicy.java b/infrastructure/src/main/java/org/springframework/batch/retry/policy/SimpleRetryPolicy.java new file mode 100644 index 000000000..cac2f5cf4 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/retry/policy/SimpleRetryPolicy.java @@ -0,0 +1,159 @@ +/* + * 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.retry.policy; + +import org.springframework.batch.common.BinaryExceptionClassifier; +import org.springframework.batch.repeat.RepeatContext; +import org.springframework.batch.retry.RetryCallback; +import org.springframework.batch.retry.RetryContext; +import org.springframework.batch.retry.context.RetryContextSupport; +import org.springframework.batch.retry.synch.RetrySynchronizationManager; + +/** + * + * Simple retry policy that retries a fixed number of times for a set of named + * exceptions (and subclasses). The number of attempts includes the initial try, + * so e.g. + * + *
+ * retryTemplate = new RetryTemplate(new SimpleRetryPolicy(3));
+ * retryTemplate.execute(callback);
+ * 
+ * + * will execute the callback at least once, and as many as 3 times. + * + * @author Dave Syer + * @author Rob Harrop + * + */ +public class SimpleRetryPolicy extends AbstractStatelessRetryPolicy { + + /** + * The default limit to the number of attempts for a new policy. + */ + public final static int DEFAULT_MAX_ATTEMPTS = 3; + + private volatile int maxAttempts; + + private BinaryExceptionClassifier classifier = new BinaryExceptionClassifier(); + + /** + * Create a {@link SimpleRetryPolicy} with the default number of retry + * attempts. + */ + public SimpleRetryPolicy() { + this(DEFAULT_MAX_ATTEMPTS); + } + + /** + * Create a {@link SimpleRetryPolicy} with the specified number of retry + * attempts, and default exceptions to retry. + * + * @param maxAttempts + */ + public SimpleRetryPolicy(int maxAttempts) { + super(); + setRetryableExceptionClasses(new Class[] { Exception.class }); + this.maxAttempts = maxAttempts; + } + + /** + * Setter for retry attempts. + * @param retryAttempts the number of attempts before a retry becomes + * impossible. + */ + public void setMaxAttempts(int retryAttempts) { + this.maxAttempts = retryAttempts; + } + + /** + * Test for retryable operation based on the status. + * @see org.springframework.batch.retry.RetryPolicy#canRetry(org.springframework.batch.retry.RetryContext) + * + * @return true if the last exception was retryable and the number of + * attempts so far is less than the limit. + */ + public boolean canRetry(RetryContext context) { + SimpleRetryContext simpleContext = ((SimpleRetryContext) context); + Throwable t = simpleContext.getLastThrowable(); + // N.B. since the contract is defined to include the initial attempt + // in the count, we have to subtract one from the max attempts in this + // test + return (t == null || retryForException(t)) && simpleContext.getRetryCount() < maxAttempts; + } + + /** + * Set the retryable exceptions. Any exception on the list, or subclasses + * thereof, will be retryable. Others will be rethrown without retry. + * + * @param retryableExceptionClasses defaults to {@link Exception}. + */ + public final void setRetryableExceptionClasses(Class[] retryableExceptionClasses) { + classifier.setExceptionClasses(retryableExceptionClasses); + } + + /** + * @see org.springframework.batch.retry.RetryManager#close(RepeatContext) + */ + public void close(RetryContext status) { + } + + /** + * Update the status with another attempted retry and the latest exception. + * + * @see org.springframework.batch.retry.RetryPolicy#registerThrowable(org.springframework.batch.retry.RetryContext, + * java.lang.Throwable) + */ + public void registerThrowable(RetryContext context, Throwable throwable) { + SimpleRetryContext simpleContext = ((SimpleRetryContext) context); + simpleContext.registerThrowable(throwable); + } + + /** + * Get a status object that can be used to track the current operation + * according to this policy. Has to be aware of the latest exception and the + * number of attempts. + * @see org.springframework.batch.retry.RetryPolicy#open(org.springframework.batch.retry.RetryCallback) + */ + public RetryContext open(RetryCallback callback) { + return new SimpleRetryContext(); + } + + private static class SimpleRetryContext extends RetryContextSupport { + + public SimpleRetryContext() { + this(RetrySynchronizationManager.getContext()); + + } + + public SimpleRetryContext(RetryContext parent) { + super(parent); + } + + } + + /** + * Delegates to an exception classifier. + * + * @param ex + * @return true if this exception or its ancestors have been registered as + * retryable. + */ + private boolean retryForException(Throwable ex) { + return !classifier.isDefault(ex); + } +} diff --git a/infrastructure/src/main/java/org/springframework/batch/retry/policy/TimeoutRetryPolicy.java b/infrastructure/src/main/java/org/springframework/batch/retry/policy/TimeoutRetryPolicy.java new file mode 100644 index 000000000..ac1b4574e --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/retry/policy/TimeoutRetryPolicy.java @@ -0,0 +1,88 @@ +/* + * 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.retry.policy; + +import org.springframework.batch.retry.RetryCallback; +import org.springframework.batch.retry.RetryContext; +import org.springframework.batch.retry.RetryPolicy; +import org.springframework.batch.retry.context.RetryContextSupport; +import org.springframework.batch.retry.exception.TerminatedRetryException; +import org.springframework.batch.retry.synch.RetrySynchronizationManager; + +/** + * A {@link RetryPolicy} that allows a retry only if it hasn't timed out. The + * clock is started on a call to {@link #open(RetryCallback)}. + * + * @author Dave Syer + * + */ +public class TimeoutRetryPolicy extends AbstractStatelessRetryPolicy { + + /** + * Default value for timeout (milliseconds). + */ + public static final long DEFAULT_TIMEOUT = 1000; + + private long timeout = DEFAULT_TIMEOUT; + + /** + * Setter for timeout. Default is {@value #DEFAULT_TIMEOUT}. + * @param timeout + */ + public void setTimeout(long timeout) { + this.timeout = timeout; + } + + /** + * Only permits a retry if the timeout has not expired. Does not check the + * exception at all. + * + * @see org.springframework.batch.retry.RetryPolicy#canRetry(org.springframework.batch.retry.RetryContext) + */ + public boolean canRetry(RetryContext context) { + return ((TimeoutRetryContext) context).isAlive(); + } + + public void close(RetryContext context) { + } + + public RetryContext open(RetryCallback callback) { + return new TimeoutRetryContext(timeout); + } + + public void registerThrowable(RetryContext context, Throwable throwable) throws TerminatedRetryException { + ((RetryContextSupport) context).registerThrowable(throwable); + // otherwise no-op - we only time out, otherwise retry everything... + } + + private static class TimeoutRetryContext extends RetryContextSupport { + private long timeout; + + private long start; + + public TimeoutRetryContext(long timeout) { + super(RetrySynchronizationManager.getContext()); + this.start = System.currentTimeMillis(); + this.timeout = timeout; + } + + public boolean isAlive() { + return (System.currentTimeMillis() - start) <= timeout; + } + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/retry/policy/package.html b/infrastructure/src/main/java/org/springframework/batch/retry/policy/package.html new file mode 100644 index 000000000..765200937 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/retry/policy/package.html @@ -0,0 +1,7 @@ + + +

+Infrastructure implementations of retry policy concerns. +

+ + diff --git a/infrastructure/src/main/java/org/springframework/batch/retry/support/RetryTemplate.java b/infrastructure/src/main/java/org/springframework/batch/retry/support/RetryTemplate.java new file mode 100644 index 000000000..fe5fd6dfd --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/retry/support/RetryTemplate.java @@ -0,0 +1,252 @@ +/* + * 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.retry.support; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.batch.retry.RetryCallback; +import org.springframework.batch.retry.RetryContext; +import org.springframework.batch.retry.RetryInterceptor; +import org.springframework.batch.retry.RetryOperations; +import org.springframework.batch.retry.RetryPolicy; +import org.springframework.batch.retry.backoff.BackOffContext; +import org.springframework.batch.retry.backoff.BackOffPolicy; +import org.springframework.batch.retry.backoff.NoBackOffPolicy; +import org.springframework.batch.retry.exception.BackOffInterruptedException; +import org.springframework.batch.retry.exception.TerminatedRetryException; +import org.springframework.batch.retry.policy.SimpleRetryPolicy; +import org.springframework.batch.retry.synch.RetrySynchronizationManager; + +/** + * Template class that simplifies the execution of operations with retry + * semantics.
Retryable operations are encapsulated in implementations of + * the {@link RetryCallback} interface and are executed using one of the + * supplied {@link #execute} methods.
+ * + * By default, an operation is retried if is throws any {@link Exception} or + * subclass of {@link Exception}. This behaviour can be changed by using the + * {@link #setRetryableExceptionClasses} method to specify exactly which + * {@link Exception} classes to retry for.
+ * + * Also by default, each operation is retried for a maximum of three attempts + * with no back off in between. This behaviour can be configured using the + * {@link #setRetryAttempts retryAttempts} and + * {@link #setBackOffStrategy backOffStrategy} properties. The + * {@link org.springframework.batch.retry.backoff.BackOffPolicy} controls how + * long the pause is between each individual retry attempt.
+ * + * This class is thread-safe and suitable for concurrent access when executing + * operations and when performing configuration changes. As such, it is possible + * to change the number of retries on the fly, as well as the + * {@link BackOffPolicy} used and no in progress retryable operations will be + * affected. + * + * @author Rob Harrop + * @author Dave Syer + * @since 2.1 + */ +public class RetryTemplate implements RetryOperations { + + protected final Log logger = LogFactory.getLog(getClass()); + + private volatile BackOffPolicy backOffPolicy = new NoBackOffPolicy(); + + private volatile RetryPolicy retryPolicy = new SimpleRetryPolicy(); + + private volatile RetryInterceptor[] interceptors = new RetryInterceptor[0]; + + /** + * Setter for interceptors. The interceptors are executed before and after a + * retry block (i.e. before and after all the attempts), and on an error + * (every attempt). + * @param interceptors + * @see RetryInterceptor + */ + public void setInterceptors(RetryInterceptor[] interceptors) { + this.interceptors = interceptors; + } + + /** + * Setter for single interceptor if there is only one. + * @param interceptor + * @see #setInterceptors(RetryInterceptor[]) + */ + public void setInterceptor(RetryInterceptor interceptor) { + this.interceptors = new RetryInterceptor[] { interceptor }; + } + + /** + * Setter for {@link BackOffPolicy}. + * @param backOffPolicy + */ + public void setBackOffPolicy(BackOffPolicy backOffPolicy) { + this.backOffPolicy = backOffPolicy; + } + + /** + * Setter for {@link RetryPolicy}. + * + * @param retryPolicy + */ + public void setRetryPolicy(RetryPolicy retryPolicy) { + this.retryPolicy = retryPolicy; + } + + /** + * Keep executing the callback until it eiether succeeds or the policy + * dictates that we stop, in which case the most recent exception thrown by + * the callback will be rethrown. + * + * @see org.springframework.batch.retry.RetryOperations#execute(org.springframework.batch.retry.RetryCallback) + * + * @throws TerminatedRetryException if the retry has been manually + * terminated through the {@link RetryContext}. + */ + public final Object execute(RetryCallback callback) throws Exception { + + /* + * Read all needed data into local variables to prevent any + * reference/primitive changes on other threads affecting this retry + * attempt. + */ + BackOffPolicy backOffPolicy = this.backOffPolicy; + RetryPolicy retryPolicy = this.retryPolicy; + + // Allow the retry policy to initialise itself... + // TODO: catch and rethrow abnormal retry exception? + RetryContext context = retryPolicy.open(callback); + + // Make sure the context is available globally for clients who need + // it... + RetrySynchronizationManager.register(context); + + Throwable lastException = null; + + try { + + // Give clients a chance to enhance the context... + boolean running = doOpenInterceptors(callback, context); + + if (!running) { + throw new TerminatedRetryException("Retry terminated abnormally by interceptor before first attempt"); + } + + // Start the backoff context... + BackOffContext backOffContext = backOffPolicy.start(context); + + /* + * We allow the whole loop to be skipped if the policy or context + * already forbid the first try. This is used in the case of + * external retry to allow a recovery in handleRetryExhausted + * without the callback processing (which would throw an exception). + */ + while (retryPolicy.canRetry(context) && !isMarkedExhausted(context)) { + + try { + logger.debug("Retry: count=" + context.getRetryCount()); + // Reset the last exception, so if we are successful + // the close interceptors will not think we failed... + lastException = null; + return callback.doWithRetry(context); + } + catch (Throwable e) { + + lastException = e; + + doOnErrorInterceptors(callback, context, e); + + retryPolicy.registerThrowable(context, e); + + if (retryPolicy.shouldRethrow(context)) { + logger.debug("Abort retry for policy: count=" + context.getRetryCount()); + unwrapAndThrow(e); + } + + } + + try { + backOffPolicy.backOff(backOffContext); + } + catch (BackOffInterruptedException e) { + lastException = e; + // back off was prevented by another thread - fail the + // retry + logger.debug("Abort retry because interrupted: count=" + context.getRetryCount()); + unwrapAndThrow(e); + } + + /* + * An external policy that can retry should have rethrown the + * exception by now - i.e. we shouldn't get this far for an + * external policy if it can retry. + */ + } + + logger.debug("Retry failed last attempt: count=" + context.getRetryCount()); + return retryPolicy.handleRetryExhausted(context); + + } + finally { + retryPolicy.close(context); + doCloseInterceptors(callback, context, lastException); + RetrySynchronizationManager.clear(); + } + } + + /** + * Check if client has marked the context to end the retry attempts. + * @param context + * @return + */ + private boolean isMarkedExhausted(RetryContext context) { + return context.isExhaustedOnly(); + } + + private boolean doOpenInterceptors(RetryCallback callback, RetryContext context) { + + boolean result = true; + + for (int i = 0; i < interceptors.length; i++) { + result = result && interceptors[i].open(context, callback); + } + + return result; + + } + + private void doCloseInterceptors(RetryCallback callback, RetryContext context, Throwable lastException) { + for (int i = interceptors.length; i-- > 0;) { + interceptors[i].close(context, callback, lastException); + } + } + + private void doOnErrorInterceptors(RetryCallback callback, RetryContext context, Throwable throwable) { + for (int i = interceptors.length; i-- > 0;) { + interceptors[i].onError(context, callback, throwable); + } + } + + private void unwrapAndThrow(Throwable ex) throws Exception { + if (ex instanceof Exception) { + throw (Exception) ex; + } + else if (ex instanceof Error) { + throw (Error) ex; + } + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/retry/support/package.html b/infrastructure/src/main/java/org/springframework/batch/retry/support/package.html new file mode 100644 index 000000000..549ec8289 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/retry/support/package.html @@ -0,0 +1,7 @@ + + +

+Infrastructure implementations of retry support concerns. +

+ + diff --git a/infrastructure/src/main/java/org/springframework/batch/retry/synch/RetrySynchronizationManager.java b/infrastructure/src/main/java/org/springframework/batch/retry/synch/RetrySynchronizationManager.java new file mode 100644 index 000000000..b3e3fbdc6 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/retry/synch/RetrySynchronizationManager.java @@ -0,0 +1,56 @@ +/* + * 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.retry.synch; + +import org.springframework.batch.retry.RetryContext; + +public class RetrySynchronizationManager { + + private RetrySynchronizationManager() { + } + + private static final ThreadLocal context = new ThreadLocal(); + + public static RetryContext getContext() { + RetryContext result = (RetryContext) context.get(); + return result; + } + + public static RetryContext register(RetryContext context) { + RetryContext oldContext = getContext(); + RetrySynchronizationManager.context.set(context); + return oldContext; + } + + public static RetryContext clear() { + RetryContext value = getContext(); + RetryContext parent = value == null ? null : value.getParent(); + RetrySynchronizationManager.context.set(parent); + return value; + } + + public static RetryContext clearAll() { + RetryContext result = null; + RetryContext context = clear(); + while (context != null) { + result = context; + context = clear(); + } + return result; + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/retry/synch/package.html b/infrastructure/src/main/java/org/springframework/batch/retry/synch/package.html new file mode 100644 index 000000000..77200b44a --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/retry/synch/package.html @@ -0,0 +1,7 @@ + + +

+Infrastructure implementations of retry synch concerns. +

+ + diff --git a/infrastructure/src/main/java/org/springframework/batch/statistics/StatisticsProvider.java b/infrastructure/src/main/java/org/springframework/batch/statistics/StatisticsProvider.java new file mode 100644 index 000000000..db6bbf908 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/statistics/StatisticsProvider.java @@ -0,0 +1,31 @@ +/* + * 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.statistics; + +import java.util.Properties; + +/** + * Provides statistics for a given module run. Any class that implements + * this interface is garunteeing that it will provide Statistics. + * + * @author Lucas Ward + * + */ +public interface StatisticsProvider { + + Properties getStatistics(); +} diff --git a/infrastructure/src/main/java/org/springframework/batch/statistics/package.html b/infrastructure/src/main/java/org/springframework/batch/statistics/package.html new file mode 100644 index 000000000..bb5fbbea8 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/statistics/package.html @@ -0,0 +1,7 @@ + + +

+Infrastructure implementations of statistics concerns. +

+ + diff --git a/infrastructure/src/main/java/org/springframework/batch/support/IntArrayPropertyEditor.java b/infrastructure/src/main/java/org/springframework/batch/support/IntArrayPropertyEditor.java new file mode 100644 index 000000000..63a2c2420 --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/support/IntArrayPropertyEditor.java @@ -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.support; + +import java.beans.PropertyEditorSupport; + +import org.springframework.util.StringUtils; + +public class IntArrayPropertyEditor extends PropertyEditorSupport { + + public void setAsText(String text) throws IllegalArgumentException { + String[] strs = StringUtils.commaDelimitedListToStringArray(text); + int[] value = new int[strs.length]; + for (int i = 0; i < value.length; i++) { + value[i] = Integer.valueOf(strs[i].trim()).intValue(); + } + setValue(value); + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/support/PropertiesConverter.java b/infrastructure/src/main/java/org/springframework/batch/support/PropertiesConverter.java new file mode 100644 index 000000000..bd8e135fc --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/support/PropertiesConverter.java @@ -0,0 +1,110 @@ +/* + * 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.support; + +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.util.Properties; + +import org.springframework.util.DefaultPropertiesPersister; +import org.springframework.util.PropertiesPersister; + +/** + * Utility to convert a Properties object to a String and back. Ideally this + * utility should have been used to convert to string in order to convert that + * string back to a Properties Object. Attempting to convert a string obtained + * by calling Properties.toString() will return an invalid Properties object. + * The format of Properties is that used by {@link PropertiesPersister} from the + * Spring Core, so a String in the correct format for a Spring property editor + * is fine (key=value pairs separated by new lines). + * + * @author Lucas Ward + * @author Dave Syer + * + * @see PropertiesPersister + */ +public final class PropertiesConverter { + + private static final PropertiesPersister propertiesPersister = new DefaultPropertiesPersister(); + + // prevents the class from being instantiated + private PropertiesConverter() { + }; + + /** + * Parse a String to a Properties object. If string is null, an empty + * Properties object will be returned. + * + * @param stringToParse String to parse. + * @return Properties parsed from each string. + * @see PropertiesPersister + * @throws StringIOException + */ + public static Properties stringToProperties(String stringToParse) { + + if (stringToParse == null) { + return new Properties(); + } + + StringReader stringReader = new StringReader(stringToParse); + + Properties properties = new Properties(); + + try { + propertiesPersister.load(properties, stringReader); + // Exception is only thrown by StringReader after it is closed, + // so never in this case. + } + catch (IOException ex) { + throw new IllegalStateException("Error while trying to parse String to java.util.Properties," + + " given String: " + properties); + } + + return properties; + } + + /** + * Convert Properties object to String. This is only necessary for + * compatibility with converting the String back to a properties object. If + * an empty properties object is passed in, a blank string is returned, + * otherwise it's string representation is returned. + * + * @param propertiesToParse + * @return String representation of properties object + * @throws StringIOException if IOException is thrown from StringWriter + */ + public static String propertiesToString(Properties propertiesToParse) { + + // If properties is empty, return a blank string. + if (propertiesToParse == null || propertiesToParse.size() == 0) { + return ""; + } + + StringWriter stringWriter = new StringWriter(); + + try { + propertiesPersister.store(propertiesToParse, stringWriter, null); + } + catch (IOException ex) { + // Exception is never thrown by StringWriter + throw new IllegalStateException("Error while trying to convert properties to string"); + } + + return stringWriter.toString(); + } +} diff --git a/infrastructure/src/main/java/org/springframework/batch/support/package.html b/infrastructure/src/main/java/org/springframework/batch/support/package.html new file mode 100644 index 000000000..3da319dce --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/support/package.html @@ -0,0 +1,7 @@ + + +

+Infrastructure implementations of support concerns. +

+ + diff --git a/infrastructure/src/main/java/org/springframework/batch/support/transaction/ResourcelessTransactionManager.java b/infrastructure/src/main/java/org/springframework/batch/support/transaction/ResourcelessTransactionManager.java new file mode 100644 index 000000000..f313c857c --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/support/transaction/ResourcelessTransactionManager.java @@ -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.support.transaction; + +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.support.AbstractPlatformTransactionManager; +import org.springframework.transaction.support.DefaultTransactionStatus; + +public class ResourcelessTransactionManager extends AbstractPlatformTransactionManager { + + protected void doBegin(Object transaction, TransactionDefinition definition) throws TransactionException { + } + + protected void doCommit(DefaultTransactionStatus status) throws TransactionException { + } + + protected Object doGetTransaction() throws TransactionException { + return new Object(); + } + + protected void doRollback(DefaultTransactionStatus status) throws TransactionException { + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/support/transaction/TransactionAwareProxyFactory.java b/infrastructure/src/main/java/org/springframework/batch/support/transaction/TransactionAwareProxyFactory.java new file mode 100644 index 000000000..b8639180a --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/support/transaction/TransactionAwareProxyFactory.java @@ -0,0 +1,162 @@ +/* + * 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.support.transaction; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationAdapter; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +/** + * Factory for transaction aware objects (like lists, sets, maps). If a + * transaction is active when a method is called on an instance created by the + * factory, it makes a copy of the target object and carries out all operations + * on the copy. Only when the transaction commits is the target re-initialised + * with the copy.
+ * + * Works well with collections and maps for testing transactional behaviour + * without needing a database. The base implementation handles lists, sets and + * maps. Subclasses can implement {@link #begin(Object)} and + * {@link #commit(Object, Object)} to provide support for other resources. + * + * @author Dave Syer + * + */ +public class TransactionAwareProxyFactory { + + private Object target; + + public TransactionAwareProxyFactory(Object target) { + super(); + this.target = begin(target); + } + + /** + * Make a copy of the target that can be used inside a transaction to + * isolate changes from the original. Also called from the factory + * constructor to isolate the target from the original value passed in. + * + * @param target the target object (List, Set or Map) + * @return an independent copy + */ + protected final Object begin(Object target) { + if (target instanceof List) { + return new ArrayList((List) target); + } + else if (target instanceof Set) { + return new HashSet((Set) target); + } + else if (target instanceof Map) { + return new HashMap((Map) target); + } + else { + throw new UnsupportedOperationException("Cannot copy target for this type: " + target.getClass()); + } + } + + /** + * Take the working copy state and commit it back to the original target. + * The target then reflects all the changes applied to the copy during a + * transaction. + * + * @param copy the working copy. + * @param target the original target of the factory. + */ + protected void commit(Object copy, Object target) { + if (target instanceof Collection) { + ((Collection) target).clear(); + ((Collection) target).addAll((Collection) copy); + } + else { + ((Map) target).clear(); + ((Map) target).putAll((Map) copy); + } + } + + public Object createInstance() { + ProxyFactory factory = new ProxyFactory(target); + factory.addAdvice(new MethodInterceptor() { + public Object invoke(MethodInvocation invocation) throws Throwable { + + if (!TransactionSynchronizationManager.isActualTransactionActive()) { + return invocation.proceed(); + } + + Object cache; + + if (!TransactionSynchronizationManager.hasResource(this)) { + cache = begin(target); + TransactionSynchronizationManager.bindResource(this, cache); + TransactionSynchronizationManager.registerSynchronization(new TargetSynchronization(this, cache)); + } + else { + cache = TransactionSynchronizationManager.getResource(this); + } + + return invocation.getMethod().invoke(cache, invocation.getArguments()); + + } + }); + return factory.getProxy(); + } + + public static Map createTransactionalMap() { + return (Map) new TransactionAwareProxyFactory(new HashMap()).createInstance(); + } + + public static Set createTransactionalSet() { + return (Set) new TransactionAwareProxyFactory(new HashSet()).createInstance(); + } + + public static List createTransactionalList() { + return (List) new TransactionAwareProxyFactory(new ArrayList()).createInstance(); + } + + private class TargetSynchronization extends TransactionSynchronizationAdapter { + + Object cache; + + Object key; + + public TargetSynchronization(Object key, Object cache) { + super(); + this.cache = cache; + this.key = key; + } + + public void afterCompletion(int status) { + super.afterCompletion(status); + if (status == TransactionSynchronization.STATUS_COMMITTED) { + synchronized (target) { + commit(cache, target); + } + } + TransactionSynchronizationManager.unbindResource(key); + } + } + +} diff --git a/infrastructure/src/main/java/org/springframework/batch/support/transaction/package.html b/infrastructure/src/main/java/org/springframework/batch/support/transaction/package.html new file mode 100644 index 000000000..77cef996a --- /dev/null +++ b/infrastructure/src/main/java/org/springframework/batch/support/transaction/package.html @@ -0,0 +1,7 @@ + + +

+Infrastructure implementations of support transaction concerns. +

+ + diff --git a/infrastructure/src/main/java/overview.html b/infrastructure/src/main/java/overview.html new file mode 100644 index 000000000..ad566f8bc --- /dev/null +++ b/infrastructure/src/main/java/overview.html @@ -0,0 +1,16 @@ + + +

+Infrastructure components are low-level re-usable abstractions that +help with optimisation or common ETL-style problems. Optimisations +include repeating an operation automatically until a policy determines +that the iteration is over. Combining this with a transaction +boundary optimises throughput by widening the transaction and sharing +the resources amongst all the operations. ETL support includes +input/output operations like flat file parsing, and transaction +synchronisations to make file access pseudo-transactional (e.g. return +to last good line if a transaction rolls back). There are also useful +abstractions for generic input and output. +

+ + diff --git a/infrastructure/src/site/apt/changelog.apt b/infrastructure/src/site/apt/changelog.apt new file mode 100644 index 000000000..7c41b85a2 --- /dev/null +++ b/infrastructure/src/site/apt/changelog.apt @@ -0,0 +1,7 @@ +Changelog: Spring Batch Infrastructure + +* 1.0-M2 + +** 2007/07/12 + + * No-one uses this file: we should just switch to auto-generated changelogs? diff --git a/infrastructure/src/site/apt/index.apt b/infrastructure/src/site/apt/index.apt new file mode 100644 index 000000000..9cd5c3e62 --- /dev/null +++ b/infrastructure/src/site/apt/index.apt @@ -0,0 +1,50 @@ + ------ + Spring Batch Prototype + ------ + Dave Syer + ------ + December 2006 + +Introduction + + This module provides framework code for the Spring Batch project. + The core interfaces are <<>> and + <<>>. The main implementations are + <<>> and <<>>. Example usage: + ++--- +RepeatTemplate template = new RepeatTemplate(); + +template.setCompletionPolicy(new FixedChunkSizeCompletionPolicy(2)); + +template.iterate(new RepeatCallback() { + + public boolean doInIteration(RepeatContext context) { + // Do stuff in batch... + return true; // Return false to signal exhausted data + } + +}); ++--- + + The callback is executed repeatedly, until the termination policy + determines that the batch should end. + + The framework provides <<>> for automatic retry of + a business operation. This is independent of the batching support, + but will often be used in conjunction with it. Example usage: + ++--- +RetryTemplate template = new RetryTemplate(); + +template.setRetryPolicy(new TimeoutRetryPolicy(30000L)); + +Object result = template.execute(new RetryCallback() { + + public Object doWithRetry(RetryContext context) { + // Do stuff that might fail, e.g. webservice operation + return result; + } + +}); ++--- diff --git a/infrastructure/src/site/site.xml b/infrastructure/src/site/site.xml new file mode 100644 index 000000000..0d7558c3e --- /dev/null +++ b/infrastructure/src/site/site.xml @@ -0,0 +1,38 @@ + + + + + Spring Batch: ${project.name} + + + + images/shim.gif + + + + + + + + org.springframework.maven.skins + maven-spring-skin + 1.0.3 + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/infrastructure/src/test/java/org/springframework/batch/common/BinaryExceptionClassifierTests.java b/infrastructure/src/test/java/org/springframework/batch/common/BinaryExceptionClassifierTests.java new file mode 100644 index 000000000..9640a103f --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/common/BinaryExceptionClassifierTests.java @@ -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.common; + +import junit.framework.TestCase; + +public class BinaryExceptionClassifierTests extends TestCase { + + BinaryExceptionClassifier classifier = new BinaryExceptionClassifier(); + + public void testClassifyNullIsDefault() { + assertTrue(classifier.isDefault(null)); + } + + public void testClassifyRandomException() { + assertTrue(classifier.isDefault(new IllegalStateException("foo"))); + } + + public void testClassifyExactMatch() { + classifier.setExceptionClasses(new Class[] {IllegalStateException.class}); + assertEquals(false, classifier.isDefault(new IllegalStateException("Foo"))); + } + +} diff --git a/infrastructure/src/test/java/org/springframework/batch/common/ExceptionClassifierSupportTests.java b/infrastructure/src/test/java/org/springframework/batch/common/ExceptionClassifierSupportTests.java new file mode 100644 index 000000000..eb6e7ecaf --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/common/ExceptionClassifierSupportTests.java @@ -0,0 +1,33 @@ +/* + * 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 junit.framework.TestCase; + +public class ExceptionClassifierSupportTests extends TestCase { + + public void testClassifyNullIsDefault() { + ExceptionClassifierSupport classifier = new ExceptionClassifierSupport(); + assertEquals(classifier.classify(null), classifier.getDefault()); + } + + public void testClassifyRandomException() { + ExceptionClassifierSupport classifier = new ExceptionClassifierSupport(); + assertEquals(classifier.classify(new IllegalStateException("Foo")), classifier.getDefault()); + } + +} diff --git a/infrastructure/src/test/java/org/springframework/batch/common/SubclassExceptionClassifierTests.java b/infrastructure/src/test/java/org/springframework/batch/common/SubclassExceptionClassifierTests.java new file mode 100644 index 000000000..3fd08b671 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/common/SubclassExceptionClassifierTests.java @@ -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.Collections; +import java.util.LinkedHashMap; + +import junit.framework.TestCase; + +public class SubclassExceptionClassifierTests extends TestCase { + + SubclassExceptionClassifier classifier = new SubclassExceptionClassifier(); + + public void testClassifyNullIsDefault() { + assertEquals(classifier.classify(null), classifier.getDefault()); + } + + public void testClassifyRandomException() { + assertEquals(classifier.classify(new IllegalStateException("Foo")), classifier.getDefault()); + } + + public void testIllegalMapWithNonClass() { + try { + classifier.setTypeMap(Collections.singletonMap("bar", "foo")); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + // expected + } + } + + public void testIllegalMapWithClass() { + try { + classifier.setTypeMap(Collections.singletonMap(String.class, "foo")); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + // expected + } + } + + public void testClassifyExactMatch() { + classifier.setTypeMap(Collections.singletonMap(IllegalStateException.class, "foo")); + assertEquals("foo", classifier.classify(new IllegalStateException("Foo"))); + } + + public void testClassifySubclassMatch() { + classifier.setTypeMap(Collections.singletonMap(RuntimeException.class, "foo")); + assertEquals("foo", classifier.classify(new IllegalStateException("Foo"))); + } + + public void testClassifySuperclassDoesNotMatch() { + classifier.setTypeMap(Collections.singletonMap(IllegalStateException.class, "foo")); + assertEquals(classifier.getDefault(), classifier.classify(new RuntimeException("Foo"))); + } + + public void testClassifyAncestorMatch() { + classifier.setTypeMap(new LinkedHashMap() {{ + put(Exception.class, "bar"); + put(IllegalArgumentException.class, "foo"); + put(RuntimeException.class, "bucket"); + }}); + assertEquals("bucket", classifier.classify(new IllegalStateException("Foo"))); + } +} diff --git a/infrastructure/src/test/java/org/springframework/batch/io/exception/AbstractBatchCriticalExceptionTests.java b/infrastructure/src/test/java/org/springframework/batch/io/exception/AbstractBatchCriticalExceptionTests.java new file mode 100644 index 000000000..ab3478e37 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/io/exception/AbstractBatchCriticalExceptionTests.java @@ -0,0 +1,44 @@ +/* + * 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; + +import junit.framework.TestCase; + +public abstract class AbstractBatchCriticalExceptionTests extends TestCase { + + public void testExceptionString() throws Exception { + Exception exception = getException("foo"); + assertEquals("foo", exception.getMessage()); + } + + public void testExceptionThrowable() throws Exception { + Exception exception = getException(new RuntimeException("foo")); + assertEquals("foo", exception.getCause().getMessage().substring(0, 3)); + } + + public void testExceptionStringThrowable() throws Exception { + Exception exception = getException("foo", new IllegalStateException()); + assertEquals("foo", exception.getMessage().substring(0, 3)); + } + + public abstract Exception getException(String msg) throws Exception; + + public abstract Exception getException(Throwable exception) throws Exception; + + public abstract Exception getException(String msg, Throwable t) throws Exception; + +} diff --git a/infrastructure/src/test/java/org/springframework/batch/io/exception/AbstractExceptionTests.java b/infrastructure/src/test/java/org/springframework/batch/io/exception/AbstractExceptionTests.java new file mode 100644 index 000000000..545e420b6 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/io/exception/AbstractExceptionTests.java @@ -0,0 +1,44 @@ +/* + * 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; + +import junit.framework.TestCase; + +public abstract class AbstractExceptionTests extends TestCase { + + public void testExceptionString() throws Exception { + Exception exception = getException("foo"); + assertEquals("foo", exception.getMessage()); + } + + public void testExceptionThrowable() throws Exception { + Exception exception = getException(new RuntimeException("foo")); + assertEquals("foo", exception.getCause().getMessage()); + } + + public void testExceptionStringThrowable() throws Exception { + Exception exception = getException("foo", new IllegalStateException()); + assertEquals("foo", exception.getMessage().substring(0, 3)); + } + + public abstract Exception getException(String msg) throws Exception; + + public abstract Exception getException(Throwable t) throws Exception; + + public abstract Exception getException(String msg, Throwable t) throws Exception; + +} diff --git a/infrastructure/src/test/java/org/springframework/batch/io/exception/BatchConfigurationExceptionTests.java b/infrastructure/src/test/java/org/springframework/batch/io/exception/BatchConfigurationExceptionTests.java new file mode 100644 index 000000000..31ae3b00d --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/io/exception/BatchConfigurationExceptionTests.java @@ -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.exception; + +import org.springframework.batch.io.exception.BatchConfigurationException; + +public class BatchConfigurationExceptionTests extends AbstractBatchCriticalExceptionTests { + + public Exception getException(String msg) throws Exception { + return new BatchConfigurationException(msg); + } + + public Exception getException(Throwable t) throws Exception { + return new BatchConfigurationException(t); + } + + public Exception getException(String msg, Throwable t) throws Exception { + return new BatchConfigurationException(msg, t); + } + +} diff --git a/infrastructure/src/test/java/org/springframework/batch/io/exception/BatchCriticalExceptionTests.java b/infrastructure/src/test/java/org/springframework/batch/io/exception/BatchCriticalExceptionTests.java new file mode 100644 index 000000000..54d1d1c1d --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/io/exception/BatchCriticalExceptionTests.java @@ -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; + + +public class BatchCriticalExceptionTests extends AbstractBatchCriticalExceptionTests { + + public Exception getException(String msg) throws Exception { + return new BatchCriticalException(msg); + } + + public Exception getException(Throwable t) throws Exception { + return new BatchCriticalException(t); + } + + public Exception getException(String msg, Throwable t) throws Exception { + return new BatchCriticalException(msg, t); + } + +} diff --git a/infrastructure/src/test/java/org/springframework/batch/io/exception/BatchEnvironmentExceptionTests.java b/infrastructure/src/test/java/org/springframework/batch/io/exception/BatchEnvironmentExceptionTests.java new file mode 100644 index 000000000..102195791 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/io/exception/BatchEnvironmentExceptionTests.java @@ -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; + +import org.springframework.batch.io.exception.BatchCriticalException; +import org.springframework.batch.io.exception.BatchEnvironmentException; + +public class BatchEnvironmentExceptionTests extends AbstractBatchCriticalExceptionTests { + + public Exception getException(String msg) throws Exception { + return new BatchEnvironmentException(msg); + } + + public Exception getException(Throwable t) throws Exception { + return new BatchCriticalException(t); + } + + public Exception getException(String msg, Throwable t) throws Exception { + return new BatchEnvironmentException(msg, t); + } + +} diff --git a/infrastructure/src/test/java/org/springframework/batch/io/exception/TransactionInvalidExceptionTests.java b/infrastructure/src/test/java/org/springframework/batch/io/exception/TransactionInvalidExceptionTests.java new file mode 100644 index 000000000..ca01b21ad --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/io/exception/TransactionInvalidExceptionTests.java @@ -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; + + +public class TransactionInvalidExceptionTests extends AbstractExceptionTests { + + public Exception getException(String msg) throws Exception { + return new TransactionInvalidException(msg); + } + + public Exception getException(Throwable t) throws Exception { + return new TransactionInvalidException(t); + } + + public Exception getException(String msg, Throwable t) throws Exception { + return new TransactionInvalidException(msg, t); + } + + public void testNothing() throws Exception { + // fool coverage tools... + } +} diff --git a/infrastructure/src/test/java/org/springframework/batch/io/exception/TransactionValidExceptionTests.java b/infrastructure/src/test/java/org/springframework/batch/io/exception/TransactionValidExceptionTests.java new file mode 100644 index 000000000..ae8e70d0d --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/io/exception/TransactionValidExceptionTests.java @@ -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; + + +public class TransactionValidExceptionTests extends AbstractExceptionTests { + + public Exception getException(String msg) throws Exception { + return new TransactionValidException(msg); + } + + public Exception getException(Throwable t) throws Exception { + return new TransactionValidException(t); + } + + public Exception getException(String msg, Throwable t) throws Exception { + return new TransactionValidException(msg, t); + } + + public void testNothing() throws Exception { + // fool coverage tools... + } +} diff --git a/infrastructure/src/test/java/org/springframework/batch/io/exception/ValidationExceptionTests.java b/infrastructure/src/test/java/org/springframework/batch/io/exception/ValidationExceptionTests.java new file mode 100644 index 000000000..f5a8fbebb --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/io/exception/ValidationExceptionTests.java @@ -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; + +import org.springframework.batch.repeat.exception.AbstractExceptionTests; + +public class ValidationExceptionTests extends AbstractExceptionTests { + + public Exception getException(String msg) throws Exception { + return new ValidationException(msg); + } + + public Exception getException(String msg, Throwable t) throws Exception { + return new ValidationException(msg, t); + } + + public void testNothing() throws Exception { + // fool coverage tools... + } +} diff --git a/infrastructure/src/test/java/org/springframework/batch/io/file/support/DefaultFlatFileInputSourceTests.java b/infrastructure/src/test/java/org/springframework/batch/io/file/support/DefaultFlatFileInputSourceTests.java new file mode 100644 index 000000000..abdda38f6 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/io/file/support/DefaultFlatFileInputSourceTests.java @@ -0,0 +1,197 @@ +/* + * 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 junit.framework.TestCase; + +import org.springframework.batch.io.file.FieldSet; +import org.springframework.batch.io.file.support.DefaultFlatFileInputSource; +import org.springframework.batch.io.file.support.transform.LineTokenizer; +import org.springframework.batch.restart.RestartData; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.Resource; +import org.springframework.transaction.support.TransactionSynchronization; + +/** + * Tests for {@link DefaultFlatFileInputSource} + * + * @author robert.kasanicky + * + * TODO only regular reading is tested currently, add exception cases, restart, + * skip, validation... + */ +public class DefaultFlatFileInputSourceTests extends TestCase { + + // object under test + private DefaultFlatFileInputSource template = new DefaultFlatFileInputSource(); + + // common value used for writing to a file + private String TEST_STRING = "FlatFileInputTemplate-TestData"; + + // simple stub instead of a realistic tokenizer + private LineTokenizer tokenizer = new LineTokenizer() { + public FieldSet tokenize(String line) { + return new FieldSet(new String[]{line}); + } + }; + + /** + * Create inputFile, inject mock/stub dependencies for tested object, + * initialize the tested object + */ + protected void setUp() throws Exception { + + template.setResource(getInputResource(TEST_STRING)); + template.setTokenizer(tokenizer); + + // context argument is necessary only for the FileLocator, which + // is mocked + template.open(); + } + + /** + * Release resources and delete the temporary file + */ + protected void tearDown() throws Exception { + template.close(); + } + + private Resource getInputResource(String input) { + return new ByteArrayResource(input.getBytes()); + } + + /** + * Test skip and skipRollback functionality + * @throws IOException + */ + public void testSkip() throws IOException { + + template.close(); + template.setResource(getInputResource("testLine1\ntestLine2\ntestLine3\ntestLine4\ntestLine5\ntestLine6")); + template.open(); + + // read some records + template.readFieldSet(); // #1 + template.readFieldSet(); // #2 + // commit them + template.getTransactionSynchronization().afterCompletion(TransactionSynchronization.STATUS_COMMITTED); + // read next record + template.readFieldSet(); // # 3 + // mark record as skipped + template.skip(); + // read next records + template.getTransactionSynchronization().afterCompletion(TransactionSynchronization.STATUS_ROLLED_BACK); + + // we should now process all records after first commit point, that are + // not marked as skipped + assertEquals("[testLine4]", template.readFieldSet().toString()); + + // TODO update + // Map statistics = template.getStatistics(); + // assertEquals("6", + // statistics.get(FlatFileInputTemplate.READ_STATISTICS_NAME)); + // assertEquals("2", + // statistics.get(FlatFileInputTemplate.SKIPPED_STATISTICS_NAME)); + + } + + /** + * Test skip and skipRollback functionality + * @throws IOException + */ + public void testTransactionSynchronizationUnknown() throws IOException { + + template.close(); + template.setResource(getInputResource("testLine1\ntestLine2\ntestLine3\ntestLine4\ntestLine5\ntestLine6")); + template.open(); + + // read some records + template.readFieldSet(); + template.skip(); + template.readFieldSet(); + // TODO + // statistics = template.getStatistics(); + // skipped = (String) + // statistics.get(FlatFileInputTemplate.SKIPPED_STATISTICS_NAME); + // read = (String) + // statistics.get(FlatFileInputTemplate.READ_STATISTICS_NAME); + + // call unknown, which has no influence and therefore statistics should + // be the same + template.getTransactionSynchronization().afterCompletion(TransactionSynchronization.STATUS_UNKNOWN); + // TODO + // statistics = template.getStatistics(); + // assertEquals(skipped, (String) + // statistics.get(FlatFileInputTemplate.SKIPPED_STATISTICS_NAME)); + // assertEquals(read, (String) + // statistics.get(FlatFileInputTemplate.READ_STATISTICS_NAME)); + } + + public void testRestartFromNullData() throws Exception { + template.restoreFrom(null); + assertEquals("[FlatFileInputTemplate-TestData]", template.readFieldSet().toString()); + } + + public void testRestartWithNullReader() throws Exception { + template = new DefaultFlatFileInputSource(); + template.setResource(getInputResource(TEST_STRING)); + // do not open the template... + template.restoreFrom(template.getRestartData()); + assertEquals("[FlatFileInputTemplate-TestData]", template.readFieldSet().toString()); + } + + public void testRestart() throws IOException { + + template.close(); + template.setResource(getInputResource("testLine1\ntestLine2\ntestLine3\ntestLine4\ntestLine5\ntestLine6")); + template.open(); + + // read some records + template.readFieldSet(); + template.readFieldSet(); + // commit them + template.getTransactionSynchronization().afterCompletion(TransactionSynchronization.STATUS_COMMITTED); + // read next two records + template.readFieldSet(); + template.readFieldSet(); + + // get restart data + RestartData restartData = template.getRestartData(); + // TODO + // assertEquals("4", (String) restartData); + // close input + template.close(); + + template.setResource(getInputResource("testLine1\ntestLine2\ntestLine3\ntestLine4\ntestLine5\ntestLine6")); + + // init for restart + template.open(); + template.restoreFrom(restartData); + + // read remaining records + assertEquals("[testLine5]", template.readFieldSet().toString()); + assertEquals("[testLine6]", template.readFieldSet().toString()); + + // TODO + // Map statistics = template.getStatistics(); + // assertEquals("6", + // statistics.get(FlatFileInputTemplate.READ_STATISTICS_NAME)); + } + +} diff --git a/infrastructure/src/test/java/org/springframework/batch/io/file/support/FieldSetTests.java b/infrastructure/src/test/java/org/springframework/batch/io/file/support/FieldSetTests.java new file mode 100644 index 000000000..c6559d3c9 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/io/file/support/FieldSetTests.java @@ -0,0 +1,371 @@ +/* + * 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.math.BigDecimal; +import java.text.ParseException; + +import junit.framework.TestCase; + +import org.springframework.batch.io.file.FieldSet; + +public class FieldSetTests extends TestCase { + FieldSet fieldSet; + + String[] tokens; + + String[] names; + + protected void setUp() throws Exception { + super.setUp(); + + tokens = new String[] { "TestString", "true", "C", "10", "-472", "354224", "543", "124.3", "424.3", "324", + null, "2007-10-12", "12-10-2007" }; + names = new String[] { "String", "Boolean", "Char", "Byte", "Short", "Integer", "Long", "Float", "Double", + "BigDecimal", "Null", "Date", "DatePattern" }; + + fieldSet = new FieldSet(tokens, names); + assertTrue(fieldSet.getFieldCount() == 13); + + } + + public void testReadString() throws ParseException { + + assertEquals(fieldSet.readString(0), "TestString"); + assertEquals(fieldSet.readString("String"), "TestString"); + + } + + public void testReadChar() throws Exception { + + assertTrue(fieldSet.readChar(2) == 'C'); + assertTrue(fieldSet.readChar("Char") == 'C'); + + } + + public void testReadBooleanTrue() throws Exception { + + assertTrue(fieldSet.readBoolean(1)); + assertTrue(fieldSet.readBoolean("Boolean")); + + } + + public void testReadByte() throws Exception { + + assertTrue(fieldSet.readByte(3) == 10); + assertTrue(fieldSet.readByte("Byte") == 10); + + } + + public void testReadShort() throws Exception { + + assertTrue(fieldSet.readShort(4) == -472); + assertTrue(fieldSet.readShort("Short") == -472); + + } + + public void testReadFloat() throws Exception { + + assertTrue(fieldSet.readFloat(7) == 124.3F); + assertTrue(fieldSet.readFloat("Float") == 124.3F); + + } + + public void testReadDouble() throws Exception { + + assertTrue(fieldSet.readDouble(8) == 424.3); + assertTrue(fieldSet.readDouble("Double") == 424.3); + + } + + public void testReadBigDecimal() throws Exception { + + BigDecimal bd = new BigDecimal(324); + assertEquals(fieldSet.readBigDecimal(9), bd); + assertEquals(fieldSet.readBigDecimal("BigDecimal"), bd); + + } + + public void testReadBigDecimalWithDefaultvalue() throws Exception { + + BigDecimal bd = new BigDecimal(324); + assertEquals(bd, fieldSet.readBigDecimal(10, bd)); + assertEquals(bd, fieldSet.readBigDecimal("Null", bd)); + + } + + public void testReadNonExistentField() throws Exception { + + try { + fieldSet.readString("something"); + fail("field set returns value even value was never put in!"); + } + catch (IllegalArgumentException e) { + assertTrue(e.getMessage().indexOf("something") > 0); + } + + } + + public void testReadIndexOutOfRange() throws Exception { + + try { + fieldSet.readShort(-1); + fail("field set returns value even index is out of range!"); + } + catch (IndexOutOfBoundsException e) { + assertTrue(true); + } + + try { + fieldSet.readShort(99); + fail("field set returns value even index is out of range!"); + } + catch (Exception e) { + assertTrue(true); + } + } + + public void testReadBooleanWithTrueValue() { + assertTrue(fieldSet.readBoolean(1, "true")); + assertFalse(fieldSet.readBoolean(1, "incorrect trueValue")); + + assertTrue(fieldSet.readBoolean("Boolean", "true")); + assertFalse(fieldSet.readBoolean("Boolean", "incorrect trueValue")); + } + + public void testReadBooleanFalse() { + fieldSet = new FieldSet(new String[] { "false" }); + assertFalse(fieldSet.readBoolean(0)); + } + + public void testReadCharException() { + try { + fieldSet.readChar(1); + fail("the value read was not a character, exception expected"); + } + catch (IllegalArgumentException expected) { + assertTrue(true); + } + + try { + fieldSet.readChar("Boolean"); + fail("the value read was not a character, exception expected"); + } + catch (IllegalArgumentException expected) { + assertTrue(true); + } + } + + public void testReadInt() throws Exception { + assertEquals(354224, fieldSet.readInt(5)); + assertEquals(354224, fieldSet.readInt("Integer")); + } + + public void testReadLong() throws Exception { + assertEquals(543, fieldSet.readLong(6)); + assertEquals(543, fieldSet.readLong("Long")); + } + + public void testReadIntWithNullValue() { + assertEquals(5, fieldSet.readInt(10, 5)); + assertEquals(5, fieldSet.readInt("Null", 5)); + } + + public void testReadIntWithDefaultAndNotNull() throws Exception { + assertEquals(354224, fieldSet.readInt(5, 5)); + assertEquals(354224, fieldSet.readInt("Integer", 5)); + } + + public void testReadLongWithNullValue() { + int defaultValue = 5; + int indexOfNull = 10; + int indexNotNull = 6; + String nameNull = "Null"; + String nameNotNull = "Long"; + long longValueAtIndex = 543; + + assertEquals(fieldSet.readLong(indexOfNull, defaultValue), defaultValue); + assertEquals(fieldSet.readLong(indexNotNull, defaultValue), longValueAtIndex); + + assertEquals(fieldSet.readLong(nameNull, defaultValue), defaultValue); + assertEquals(fieldSet.readLong(nameNotNull, defaultValue), longValueAtIndex); + } + + public void testReadBigDecimalInvalid() { + int index = 0; + + try { + fieldSet.readBigDecimal(index); + fail("field value is not a number, exception expected"); + } + catch (IllegalArgumentException e) { + assertTrue(e.getMessage().indexOf("TestString") > 0); + } + + } + + public void testReadBigDecimalByNameInvalid() throws Exception { + try { + fieldSet.readBigDecimal("String"); + fail("field value is not a number, exception expected"); + } + catch (IllegalArgumentException e) { + assertTrue(e.getMessage().indexOf("TestString") > 0); + assertTrue(e.getMessage().indexOf("name: [String]") > 0); + } + } + + public void testReadDate() throws Exception { + assertNotNull(fieldSet.readDate(11)); + assertNotNull(fieldSet.readDate("Date")); + } + + public void testReadDateInvalid() throws Exception { + + try { + fieldSet.readDate(0); + fail("field value is not a date, exception expected"); + } + catch (IllegalArgumentException e) { + assertTrue(e.getMessage().indexOf("TestString") > 0); + } + + } + + public void testReadDateInvalidByName() throws Exception { + + try { + fieldSet.readDate("String"); + fail("field value is not a date, exception expected"); + } + catch (IllegalArgumentException e) { + assertTrue(e.getMessage().indexOf("name: [String]") > 0); + } + + } + + public void testReadDateInvalidWithPattern() throws Exception { + + try { + fieldSet.readDate(0, "dd-MM-yyyy"); + fail("field value is not a date, exception expected"); + } + catch (IllegalArgumentException e) { + assertTrue(e.getMessage().indexOf("dd-MM-yyyy") > 0); + } + } + + public void testReadDateByNameInvalidWithPattern() throws Exception { + + try { + fieldSet.readDate("String", "dd-MM-yyyy"); + fail("field value is not a date, exception expected"); + } + catch (IllegalArgumentException e) { + assertTrue(e.getMessage().indexOf("dd-MM-yyyy") > 0); + assertTrue(e.getMessage().indexOf("String") > 0); + } + } + + public void testEquals() { + + assertEquals(fieldSet, fieldSet); + assertEquals(fieldSet, new FieldSet(tokens)); + + String[] tokens1 = new String[] { "token1" }; + String[] tokens2 = new String[] { "token1" }; + FieldSet fs1 = new FieldSet(tokens1); + FieldSet fs2 = new FieldSet(tokens2); + assertEquals(fs1, fs2); + } + + public void testEqualsNull() { + assertFalse(fieldSet.equals(null)); + } + + public void testEqualsNullTokens() { + assertFalse(new FieldSet(null).equals(fieldSet)); + } + + public void testEqualsNotEqual() throws Exception { + + String[] tokens1 = new String[] { "token1" }; + String[] tokens2 = new String[] { "token1", "token2" }; + FieldSet fs1 = new FieldSet(tokens1); + FieldSet fs2 = new FieldSet(tokens2); + assertFalse(fs1.equals(fs2)); + + } + + public void testHashCode() throws Exception { + assertEquals(fieldSet.hashCode(), new FieldSet(tokens).hashCode()); + } + + public void testHashCodeWithNullTokens() throws Exception { + assertEquals(0, new FieldSet(null).hashCode()); + } + + public void testConstructor() throws Exception { + try { + new FieldSet(new String[] { "1", "2" }, new String[] { "a" }); + fail("Expected IllegalArgumentException"); + } + catch (IllegalArgumentException e) { + // expected + } + } + + public void testToStringWithNames() throws Exception { + fieldSet = new FieldSet(new String[] { "foo", "bar" }, new String[] { "Foo", "Bar" }); + assertTrue(fieldSet.toString().indexOf("Foo=foo") >= 0); + } + + public void testToStringWithoutNames() throws Exception { + fieldSet = new FieldSet(new String[] { "foo", "bar" }); + assertTrue(fieldSet.toString().indexOf("foo") >= 0); + } + + public void testToStringNullTokens() throws Exception { + fieldSet = new FieldSet(null); + assertEquals(null, fieldSet.toString()); + } + + public void testProperties() throws Exception { + assertEquals("foo", new FieldSet(new String[] { "foo", "bar" }, new String[] { "Foo", "Bar" }).getProperties() + .getProperty("Foo")); + } + + public void testPropertiesWithNoNames() throws Exception { + try { + new FieldSet(new String[] { "foo", "bar" }).getProperties(); + fail("Expected IllegalStateException"); + } + catch (IllegalStateException e) { + // expected + } + } + + public void testAccessByNameWhenNamesMissing() throws Exception { + try { + new FieldSet(new String[] { "1", "2" }).readInt("a"); + fail("Expected IllegalArgumentException"); + } + catch (IllegalArgumentException e) { + // expected + } + } +} diff --git a/infrastructure/src/test/java/org/springframework/batch/io/file/support/FlatFileOutputSourceTests.java b/infrastructure/src/test/java/org/springframework/batch/io/file/support/FlatFileOutputSourceTests.java new file mode 100644 index 000000000..b46fd3ebc --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/io/file/support/FlatFileOutputSourceTests.java @@ -0,0 +1,345 @@ +/* + * 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.File; +import java.io.FileReader; +import java.io.IOException; +import java.util.Collections; + +import junit.framework.TestCase; + +import org.springframework.batch.io.file.support.transform.Converter; +import org.springframework.batch.restart.RestartData; +import org.springframework.core.io.FileSystemResource; +import org.springframework.transaction.support.TransactionSynchronization; + +/** + * Tests of regular usage for {@link FlatFileOutputSource} Exception cases will + * be in separate TestCase classes with different setUp and + * tearDown methods + * + * @author robert.kasanicky + * @author Dave Syer + * + */ +public class FlatFileOutputSourceTests extends TestCase { + + // object under test + private FlatFileOutputSource template = new FlatFileOutputSource(); + + // String to be written into file by the FlatFileInputTemplate + private static final String TEST_STRING = "FlatFileOutputTemplateTest-OutputData"; + + // temporary output file + private File outputFile; + + // reads the output file to check the result + private BufferedReader reader; + + /** + * Create temporary output file, define mock behaviour, set dependencies + * and initialize the object under test + */ + protected void setUp() throws Exception { + + outputFile = File.createTempFile("flatfile-output-", ".tmp"); + outputFile.createNewFile(); + + template.setResource(new FileSystemResource(outputFile)); + template.afterPropertiesSet(); + + template.open(); + + reader = new BufferedReader(new FileReader(outputFile)); + } + + /** + * Release resources and delete the temporary output file + */ + protected void tearDown() throws Exception { + reader.close(); + template.close(); + outputFile.delete(); + } + + /** + * Regular usage of write(String) method + */ + public void testWriteString() throws IOException { + template.write(TEST_STRING); + + String lineFromFile = reader.readLine(); + assertEquals(TEST_STRING, lineFromFile); + } + + /** + * Regular usage of write(String) method + */ + public void testWriteCollection() throws IOException { + template.write(Collections.singleton(TEST_STRING)); + + String lineFromFile = reader.readLine(); + assertEquals(TEST_STRING, lineFromFile); + } + + /** + * Regular usage of write(String) method + */ + public void testWriteWithConverter() throws IOException { + template.setConverter(new Converter() { + public Object convert(Object input) { + return "FOO:" + input; + } + }); + Object data = new Object(); + template.write(data); + + String lineFromFile = reader.readLine(); + // converter not used if input is String + assertEquals("FOO:" + data.toString(), lineFromFile); + } + + /** + * Regular usage of write(String) method + */ + public void testWriteWithConverterAndInfiniteLoop() throws IOException { + template.setConverter(new Converter() { + public Object convert(Object input) { + return "FOO:" + input; + } + }); + Object data = new Object(); + template.write(data); + + String lineFromFile = reader.readLine(); + // converter not used if input is String + assertEquals("FOO:" + data.toString(), lineFromFile); + } + + /** + * Regular usage of write(String) method + */ + public void testWriteWithConverterAndInfiniteLoopInCollection() throws IOException { + template.setConverter(new Converter() { + public Object convert(Object input) { + return "FOO:" + input; + } + }); + Object data = new Object(); + template.write(new Object[] { data, data }); + + String lineFromFile = reader.readLine(); + assertEquals("FOO:" + data.toString(), lineFromFile); + lineFromFile = reader.readLine(); + assertEquals("FOO:" + data.toString(), lineFromFile); + } + + /** + * Regular usage of write(String) method + */ + public void testWriteWithConverterAndInfiniteLoopInConvertedCollection() throws IOException { + template.setConverter(new Converter() { + boolean converted = false; + public Object convert(Object input) { + if (converted) { + return input; + } + converted = true; + return new Object[] { input, input }; + } + }); + Object data = new Object(); + try { + template.write(data); + fail("Expected IllegalStateException"); + } catch (IllegalStateException e) { + // expected + assertTrue("Wrong message: "+e, e.getMessage().toLowerCase().indexOf("infinite")>=0); + } + + String lineFromFile = reader.readLine(); + assertNull(lineFromFile); + } + + /** + * Regular usage of write(String) method + */ + public void testWriteWithConverterAndString() throws IOException { + template.setConverter(new Converter() { + public Object convert(Object input) { + return "FOO:" + input; + } + }); + template.write(Collections.singleton(TEST_STRING)); + + String lineFromFile = reader.readLine(); + // converter not used if input is String + assertEquals(TEST_STRING, lineFromFile); + } + + /** + * Regular usage of write(String) method + */ + public void testWriteWithConverterAndCollectionOfString() throws IOException { + template.setConverter(new Converter() { + public Object convert(Object input) { + return "FOO:" + input; + } + }); + template.write(TEST_STRING); + + String lineFromFile = reader.readLine(); + // converter not used if input is String + assertEquals(TEST_STRING, lineFromFile); + } + + /** + * Regular usage of write(String) method + */ + public void testWriteArray() throws IOException { + template.write(new String[] { TEST_STRING, TEST_STRING }); + + String lineFromFile = reader.readLine(); + assertEquals(TEST_STRING, lineFromFile); + lineFromFile = reader.readLine(); + assertEquals(TEST_STRING, lineFromFile); + } + + /** + * Regular usage of write(String[], LineDescriptor) method + */ + public void testWriteRecord() throws IOException { + String args = "1"; + + // AggregatorStub ignores the LineDescriptor, so we pass null + template.write(args); + + String lineFromFile = reader.readLine(); + assertEquals(args, lineFromFile); + } + + public void testRollback() throws Exception { + template.write("testLine1"); + // rollback + template.getTransactionSynchronization().afterCompletion(TransactionSynchronization.STATUS_ROLLED_BACK); + String lineFromFile = reader.readLine(); + assertEquals(null, lineFromFile); + } + + public void testCommit() throws Exception { + template.write("testLine1"); + // rollback + template.getTransactionSynchronization().afterCompletion(TransactionSynchronization.STATUS_COMMITTED); + String lineFromFile = reader.readLine(); + assertEquals("testLine1", lineFromFile); + } + + public void testUnknown() throws Exception { + template.write("testLine1"); + // rollback + template.getTransactionSynchronization().afterCompletion(TransactionSynchronization.STATUS_UNKNOWN); + String lineFromFile = reader.readLine(); + assertEquals("testLine1", lineFromFile); + } + + public void testRestart() throws IOException { + + // write some lines + template.write("testLine1"); + template.write("testLine2"); + template.write("testLine3"); + + // commit + template.getTransactionSynchronization().afterCompletion(TransactionSynchronization.STATUS_COMMITTED); + + // this will be rolled back... + template.write("this will be rolled back"); + + // rollback + template.getTransactionSynchronization().afterCompletion(TransactionSynchronization.STATUS_ROLLED_BACK); + + // write more lines + template.write("testLine4"); + template.write("testLine5"); + + // commit + template.getTransactionSynchronization().afterCompletion(TransactionSynchronization.STATUS_COMMITTED); + + // get restart data + RestartData restartData = template.getRestartData(); + // close template + template.close(); + + // init for restart + template.setBufferSize(0); + template.open(); + + // try empty restart data... + try { + template.restoreFrom(null); + assertTrue(true); + } + catch (IllegalArgumentException iae) { + fail("null restart data should be handled gracefully"); + } + + // init with correct data + template.restoreFrom(restartData); + + // write more lines + template.write("testLine6"); + template.write("testLine7"); + template.write("testLine8"); + + // close template + template.close(); + + // verify what was written to the file + for (int i = 1; i < 9; i++) { + assertEquals("testLine" + i, reader.readLine()); + } + + // get statistics + // Statistics statistics = template.getStatistics(); + // 3 lines were written to the file after restart + // TODO + // assertEquals("3", + // statistics.get(FlatFileOutputTemplate.WRITTEN_STATISTICS_NAME)); + + } + + public void testAfterPropertiesSetChecksMandatory() throws Exception { + template = new FlatFileOutputSource(); + try { + template.afterPropertiesSet(); + fail("Expected IllegalArgumentException"); + } + catch (IllegalArgumentException e) { + // expected + } + } + + public void testDefaultRestartData() throws Exception { + template = new FlatFileOutputSource(); + RestartData restartData = template.getRestartData(); + assertNotNull(restartData); + // TODO: assert the properties of the default restart data + assertEquals(1, restartData.getProperties().size()); + } +} diff --git a/infrastructure/src/test/java/org/springframework/batch/io/file/support/ResourceLineReaderTests.java b/infrastructure/src/test/java/org/springframework/batch/io/file/support/ResourceLineReaderTests.java new file mode 100644 index 000000000..f53f5a966 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/io/file/support/ResourceLineReaderTests.java @@ -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.file.support; + +import java.io.IOException; +import java.io.InputStream; + +import junit.framework.TestCase; + +import org.springframework.batch.io.exception.BatchEnvironmentException; +import org.springframework.batch.io.file.support.ResourceLineReader; +import org.springframework.batch.io.file.support.separator.SuffixRecordSeparatorPolicy; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.InputStreamResource; +import org.springframework.core.io.Resource; + +/** + * @author Rob Harrop + */ +public class ResourceLineReaderTests extends TestCase { + + public void testBadResource() throws Exception { + ResourceLineReader reader = new ResourceLineReader(new InputStreamResource(new InputStream() { + public int read() throws IOException { + throw new IOException("Foo"); + } + })); + try { + reader.read(); + fail("Expected InputException"); + } + catch (BatchEnvironmentException e) { + // expected + assertTrue(e.getMessage().startsWith("Unable to read")); + } + } + + public void testRead() throws Exception { + Resource resource = new ByteArrayResource("a,b,c\n1,2,3".getBytes()); + ResourceLineReader reader = new ResourceLineReader(resource); + int count = 0; + String line; + while ((line = (String) reader.read()) != null) { + count++; + assertNotNull(line); + } + + assertEquals(2, count); + } + + public void testCloseTwice() throws Exception { + Resource resource = new ByteArrayResource("a,b,c\n1,2,3".getBytes()); + ResourceLineReader reader = new ResourceLineReader(resource); + reader.open(); + reader.close(); + try { + reader.close(); // just closing a BufferedReader twice should be fine + } catch (Exception e) { + fail("Unexpected Exception "+e); + } + assertEquals("a,b,c", reader.read()); + } + + public void testEncoding() throws Exception { + Resource resource = new ByteArrayResource("a,b,c\n1,2,3".getBytes()); + ResourceLineReader reader = new ResourceLineReader(resource, "UTF-8"); + int count = 0; + String line; + while ((line = (String) reader.read()) != null) { + count++; + assertNotNull(line); + } + + assertEquals(2, count); + } + + public void testLineCount() throws Exception { + Resource resource = new ByteArrayResource("1,2,\"3\n4\"\n5,6,7".getBytes()); + ResourceLineReader reader = new ResourceLineReader(resource); + reader.read(); + assertEquals(2, reader.getCurrentLineCount()); + } + + public void testLineEndings() throws Exception { + Resource resource = new ByteArrayResource("1\n2\r\n3".getBytes()); + ResourceLineReader reader = new ResourceLineReader(resource); + reader.read(); + String line = (String) reader.read(); + assertEquals("2", line); + assertEquals(2, reader.getCurrentLineCount()); + line = (String) reader.read(); + assertEquals("3", line); + assertEquals(3, reader.getCurrentLineCount()); + } + + public void testDefaultComments() throws Exception { + Resource resource = new ByteArrayResource("1\n# 2\n3".getBytes()); + ResourceLineReader reader = new ResourceLineReader(resource); + reader.read(); + String line = (String) reader.read(); + assertEquals("3", line); + } + + public void testComments() throws Exception { + Resource resource = new ByteArrayResource("1\n-- 2\n3".getBytes()); + ResourceLineReader reader = new ResourceLineReader(resource); + reader.setComments(new String[] {"//", "--"}); + reader.read(); + String line = (String) reader.read(); + assertEquals("3", line); + } + + public void testResetNewReader() throws Exception { + Resource resource = new ByteArrayResource("1\n4\n5".getBytes()); + ResourceLineReader reader = new ResourceLineReader(resource); + reader.reset(); + assertEquals(0, reader.getCurrentLineCount()); + } + + public void testMarkReset() throws Exception { + Resource resource = new ByteArrayResource("1\n4\n5".getBytes()); + ResourceLineReader reader = new ResourceLineReader(resource); + reader.read(); + assertEquals(1, reader.getCurrentLineCount()); + reader.mark(); + reader.read(); + assertEquals(2, reader.getCurrentLineCount()); + reader.reset(); + reader.read(); + assertEquals(2, reader.getCurrentLineCount()); + } + + public void testNonDefaultRecordSeparatorPolicy() throws Exception { + Resource resource = new ByteArrayResource("1\n\"4\n5\"; \n6".getBytes()); + ResourceLineReader reader = new ResourceLineReader(resource); + reader.setRecordSeparatorPolicy(new SuffixRecordSeparatorPolicy()); + assertEquals(0, reader.getCurrentLineCount()); + String line = (String) reader.read(); + assertEquals("1\"4\n5\"", line); + } +} diff --git a/infrastructure/src/test/java/org/springframework/batch/io/file/support/SimpleFlatFileInputSourceTests.java b/infrastructure/src/test/java/org/springframework/batch/io/file/support/SimpleFlatFileInputSourceTests.java new file mode 100644 index 000000000..50b770561 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/io/file/support/SimpleFlatFileInputSourceTests.java @@ -0,0 +1,223 @@ +/* + * 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 java.util.ArrayList; +import java.util.List; + +import junit.framework.TestCase; + +import org.springframework.batch.io.exception.BatchEnvironmentException; +import org.springframework.batch.io.exception.ValidationException; +import org.springframework.batch.io.file.FieldSet; +import org.springframework.batch.io.file.support.separator.DefaultRecordSeparatorPolicy; +import org.springframework.batch.io.file.support.transform.LineTokenizer; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; + +/** + * Tests for {@link SimpleFlatFileInputSourceTests} + * + * @author Dave Syer + * + */ +public class SimpleFlatFileInputSourceTests extends TestCase { + + // object under test + private SimpleFlatFileInputSource template = new SimpleFlatFileInputSource(); + + // common value used for writing to a file + private String TEST_STRING = "FlatFileInputTemplate-TestData"; + + // simple stub instead of a realistic tokenizer + private LineTokenizer tokenizer = new LineTokenizer() { + public FieldSet tokenize(String line) { + return new FieldSet(new String[] { line }); + } + }; + + /** + * Create inputFile, inject mock/stub dependencies for tested object, + * initialize the tested object + */ + protected void setUp() throws Exception { + + template.setResource(getInputResource(TEST_STRING)); + template.setTokenizer(tokenizer); + template.afterPropertiesSet(); + + // context argument is necessary only for the FileLocator, which + // is mocked + template.open(); + } + + /** + * Release resources. + */ + protected void tearDown() throws Exception { + template.close(); + } + + private Resource getInputResource(String input) { + return new ByteArrayResource(input.getBytes()); + } + + /** + * Regular usage of read method + */ + public void testReadFieldSet() throws IOException { + assertEquals("[FlatFileInputTemplate-TestData]", template.readFieldSet().toString()); + } + + /** + * Regular usage of read method + */ + public void testRead() throws IOException { + assertEquals("[FlatFileInputTemplate-TestData]", template.read().toString()); + } + + /** + * Regular usage of read method + */ + public void testReadExhausted() throws IOException { + assertEquals("[FlatFileInputTemplate-TestData]", template.read().toString()); + assertEquals(null, template.read()); + } + + /** + * Regular usage of read method + */ + public void testReadWithError() throws IOException { + template.setTokenizer(new LineTokenizer() { + public FieldSet tokenize(String line) { + throw new RuntimeException("foo"); + } + }); + try { + template.read(); + fail("Expected ValidationException"); + } catch (ValidationException e) { + assertTrue(e.getMessage().indexOf("at line")>=0); + assertTrue(e.getMessage().indexOf("at line 1")>=0); + } + } + + public void testReadBeforeOpen() throws Exception { + template = new SimpleFlatFileInputSource(); + template.setResource(getInputResource(TEST_STRING)); + assertEquals("[FlatFileInputTemplate-TestData]", template.readFieldSet().toString()); + } + + public void testCloseBeforeOpen() throws Exception { + template = new SimpleFlatFileInputSource(); + template.setResource(getInputResource(TEST_STRING)); + template.close(); + // The open still happens automatically on a read... + assertEquals("[FlatFileInputTemplate-TestData]", template.readFieldSet().toString()); + } + + public void testCloseOnDestroy() throws Exception { + final List list = new ArrayList(); + template = new SimpleFlatFileInputSource() { + public void close() { + list.add("close"); + } + }; + template.destroy(); + assertEquals(1, list.size()); + } + + public void testInitializationWithNullResource() throws Exception { + template = new SimpleFlatFileInputSource(); + try { + template.afterPropertiesSet(); + fail("Expected IllegalArgumentException"); + } + catch (IllegalArgumentException e) { + // expected + } + } + + public void testOpenTwiceHasNoEffect() throws Exception { + template.open(); + testRead(); + } + + public void testSetValidEncoding() throws Exception { + template = new SimpleFlatFileInputSource(); + template.setEncoding("UTF-8"); + template.setResource(getInputResource(TEST_STRING)); + testRead(); + } + + public void testSetNullEncoding() throws Exception { + template = new SimpleFlatFileInputSource(); + template.setEncoding(null); + template.setResource(getInputResource(TEST_STRING)); + try { + template.open(); + fail("Expected IllegalArgumentException"); + } + catch (IllegalArgumentException e) { + // expected + } + } + + public void testSetInvalidEncoding() throws Exception { + template = new SimpleFlatFileInputSource(); + template.setEncoding("foo"); + template.setResource(getInputResource(TEST_STRING)); + try { + template.open(); + fail("Expected BatchEnvironmentException"); + } + catch (BatchEnvironmentException e) { + // expected + assertEquals("foo", e.getCause().getMessage()); + } + } + + public void testEncoding() throws Exception { + template.setEncoding("UTF-8"); + testRead(); + } + + public void testRecordSeparator() throws Exception { + template.setRecordSeparatorPolicy(new DefaultRecordSeparatorPolicy()); + testRead(); + } + + public void testInvalidFile() throws IOException { + DefaultFlatFileInputSource ffit = new DefaultFlatFileInputSource(); + + FileSystemResource resource = new FileSystemResource("FooDummy.txt"); + assertTrue(!resource.exists()); + ffit.setResource(resource); + + try { + ffit.open(); + fail("File is not existing but exception was not thrown."); + } + catch (BatchEnvironmentException e) { + assertEquals("FooDummy", e.getCause().getMessage().substring(0,8)); + } + + } + +} diff --git a/infrastructure/src/test/java/org/springframework/batch/io/file/support/mapping/BeanWrapperFieldSetMapperTests.java b/infrastructure/src/test/java/org/springframework/batch/io/file/support/mapping/BeanWrapperFieldSetMapperTests.java new file mode 100644 index 000000000..8d78a0dea --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/io/file/support/mapping/BeanWrapperFieldSetMapperTests.java @@ -0,0 +1,347 @@ +/* + * 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.ArrayList; +import java.util.List; +import java.util.Properties; + +import junit.framework.TestCase; + +import org.springframework.batch.io.file.FieldSet; +import org.springframework.batch.io.file.support.mapping.BeanWrapperFieldSetMapper; +import org.springframework.batch.support.IntArrayPropertyEditor; +import org.springframework.beans.BeanWrapperImpl; +import org.springframework.beans.NotWritablePropertyException; +import org.springframework.beans.propertyeditors.PropertiesEditor; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.context.support.StaticApplicationContext; + +public class BeanWrapperFieldSetMapperTests extends TestCase { + + public void testMapperWithSingleton() throws Exception { + BeanWrapperFieldSetMapper mapper = new BeanWrapperFieldSetMapper(); + StaticApplicationContext context = new StaticApplicationContext(); + mapper.setBeanFactory(context); + context.getBeanFactory().registerSingleton("bean", new TestObject()); + mapper.setPrototypeBeanName("bean"); + + FieldSet fieldSet = new FieldSet(new String[] { "This is some dummy string", "true", "C" }, new String[] { + "varString", "varBoolean", "varChar" }); + TestObject result = (TestObject) mapper.mapLine(fieldSet); + assertEquals("This is some dummy string", result.getVarString()); + assertEquals(true, result.isVarBoolean()); + assertEquals('C', result.getVarChar()); + } + + public void testPropertyNameMatching() throws Exception { + BeanWrapperFieldSetMapper mapper = new BeanWrapperFieldSetMapper(); + StaticApplicationContext context = new StaticApplicationContext(); + mapper.setBeanFactory(context); + context.getBeanFactory().registerSingleton("bean", new TestObject()); + mapper.setPrototypeBeanName("bean"); + + FieldSet fieldSet = new FieldSet(new String[] { "This is some dummy string", "true", "C" }, new String[] { + "VarString", "VAR_BOOLEAN", "VAR_CHAR" }); + TestObject result = (TestObject) mapper.mapLine(fieldSet); + assertEquals("This is some dummy string", result.getVarString()); + assertEquals(true, result.isVarBoolean()); + assertEquals('C', result.getVarChar()); + } + + public void testMapperWithPrototype() throws Exception { + ApplicationContext context = new ClassPathXmlApplicationContext("bean-wrapper.xml", getClass()); + + BeanWrapperFieldSetMapper mapper = (BeanWrapperFieldSetMapper) context.getBean("fieldSetMapper"); + + FieldSet fieldSet = new FieldSet(new String[] { "This is some dummy string", "true", "C" }, new String[] { + "varString", "varBoolean", "varChar" }); + TestObject result = (TestObject) mapper.mapLine(fieldSet); + assertEquals("This is some dummy string", result.getVarString()); + assertEquals(true, result.isVarBoolean()); + assertEquals('C', result.getVarChar()); + + } + + public void testMapperWithNestedBeanPaths() throws Exception { + TestNestedA testNestedA = new TestNestedA(); + TestNestedB testNestedB = new TestNestedB(); + testNestedA.setTestObjectB(testNestedB); + testNestedB.setTestObjectC(new TestNestedC()); + + BeanWrapperFieldSetMapper mapper = new BeanWrapperFieldSetMapper(); + StaticApplicationContext context = new StaticApplicationContext(); + mapper.setBeanFactory(context); + context.getBeanFactory().registerSingleton("bean", testNestedA); + mapper.setPrototypeBeanName("bean"); + + FieldSet fieldSet = new FieldSet(new String[] { "This is some dummy string", "1", "Another dummy", "2" }, + new String[] { "valueA", "valueB", "testObjectB.valueA", "testObjectB.testObjectC.value" }); + + TestNestedA result = (TestNestedA) mapper.mapLine(fieldSet); + + assertEquals("This is some dummy string", result.getValueA()); + assertEquals(1, result.getValueB()); + assertEquals("Another dummy", result.getTestObjectB().getValueA()); + assertEquals(2, result.getTestObjectB().getTestObjectC().getValue()); + } + + public void testMapperWithSimilarNamePropertyMatches() throws Exception { + TestNestedA testNestedA = new TestNestedA(); + + BeanWrapperFieldSetMapper mapper = new BeanWrapperFieldSetMapper(); + StaticApplicationContext context = new StaticApplicationContext(); + mapper.setBeanFactory(context); + context.getBeanFactory().registerSingleton("bean", testNestedA); + mapper.setPrototypeBeanName("bean"); + + FieldSet fieldSet = new FieldSet(new String[] { "This is some dummy string", "1" }, new String[] { "VALUE_A", + "VALUE_B" }); + + TestNestedA result = (TestNestedA) mapper.mapLine(fieldSet); + + assertEquals("This is some dummy string", result.getValueA()); + assertEquals(1, result.getValueB()); + } + + public void testMapperWithNotVerySimilarNamePropertyMatches() throws Exception { + TestNestedC testNestedC = new TestNestedC(); + + BeanWrapperFieldSetMapper mapper = new BeanWrapperFieldSetMapper(); + StaticApplicationContext context = new StaticApplicationContext(); + mapper.setBeanFactory(context); + context.getBeanFactory().registerSingleton("bean", testNestedC); + mapper.setPrototypeBeanName("bean"); + + FieldSet fieldSet = new FieldSet(new String[] { "1" }, new String[] { + "foo" }); + + TestNestedC result = (TestNestedC) mapper.mapLine(fieldSet); + + // "foo" is similar enough to "value" that it matches - but only because + // nothing else does... + assertEquals(1, result.getValue()); + } + + public void testMapperWithNestedBeanPathsAndPropertyMatches() throws Exception { + TestNestedA testNestedA = new TestNestedA(); + TestNestedB testNestedB = new TestNestedB(); + testNestedA.setTestObjectB(testNestedB); + testNestedB.setTestObjectC(new TestNestedC()); + + BeanWrapperFieldSetMapper mapper = new BeanWrapperFieldSetMapper(); + StaticApplicationContext context = new StaticApplicationContext(); + mapper.setBeanFactory(context); + context.getBeanFactory().registerSingleton("bean", testNestedA); + mapper.setPrototypeBeanName("bean"); + + FieldSet fieldSet = new FieldSet(new String[] { "Another dummy", "2" }, new String[] { "TestObjectB.ValueA", + "TestObjectB.TestObjectC.Value" }); + + TestNestedA result = (TestNestedA) mapper.mapLine(fieldSet); + + assertEquals("Another dummy", result.getTestObjectB().getValueA()); + assertEquals(2, result.getTestObjectB().getTestObjectC().getValue()); + } + + public void testMapperWithNestedBeanPathsAndPropertyMisMatches() throws Exception { + TestNestedA testNestedA = new TestNestedA(); + TestNestedB testNestedB = new TestNestedB(); + testNestedA.setTestObjectB(testNestedB); + + BeanWrapperFieldSetMapper mapper = new BeanWrapperFieldSetMapper(); + StaticApplicationContext context = new StaticApplicationContext(); + mapper.setBeanFactory(context); + context.getBeanFactory().registerSingleton("bean", testNestedA); + mapper.setPrototypeBeanName("bean"); + + FieldSet fieldSet = new FieldSet(new String[] { "Another dummy" }, new String[] { "TestObjectB.foo" }); + + try { + mapper.mapLine(fieldSet); + fail("Expected NotWritablePropertyException"); + } + catch (NotWritablePropertyException e) { + // expected + } + } + + public void testMapperWithNestedBeanPathsAndPropertyPrefixMisMatches() throws Exception { + TestNestedA testNestedA = new TestNestedA(); + TestNestedB testNestedB = new TestNestedB(); + testNestedA.setTestObjectB(testNestedB); + + BeanWrapperFieldSetMapper mapper = new BeanWrapperFieldSetMapper(); + StaticApplicationContext context = new StaticApplicationContext(); + mapper.setBeanFactory(context); + context.getBeanFactory().registerSingleton("bean", testNestedA); + mapper.setPrototypeBeanName("bean"); + + FieldSet fieldSet = new FieldSet(new String[] { "2" }, new String[] { "TestObjectA.garbage" }); + + try { + mapper.mapLine(fieldSet); + fail("Expected NotWritablePropertyException"); + } + catch (NotWritablePropertyException e) { + // expected + } + } + + public void testPlainBeanWrapper() throws Exception { + TestObject result = new TestObject(); + BeanWrapperImpl wrapper = new BeanWrapperImpl(result); + PropertiesEditor editor = new PropertiesEditor(); + editor.setAsText("varString=This is some dummy string\nvarBoolean=true\nvarChar=C"); + Properties props = (Properties) editor.getValue(); + wrapper.setPropertyValues(props); + assertEquals("This is some dummy string", result.getVarString()); + assertEquals(true, result.isVarBoolean()); + assertEquals('C', result.getVarChar()); + } + + public void testIntArray() throws Exception { + BeanWithIntArray result = new BeanWithIntArray(); + BeanWrapperImpl wrapper = new BeanWrapperImpl(result); + wrapper.registerCustomEditor(int[].class, new IntArrayPropertyEditor()); + PropertiesEditor editor = new PropertiesEditor(); + editor.setAsText("numbers=1,2,3, 4"); + Properties props = (Properties) editor.getValue(); + wrapper.setPropertyValues(props); + assertEquals(4, result.numbers[3]); + } + + //BeanWrapperFieldSetMapper doesn't currently support nesting with collections. + public void testNestedList(){ + + TestNestedList nestedList = new TestNestedList(); + List nestedC = new ArrayList(); + nestedC.add(new TestNestedC()); + nestedC.add(new TestNestedC()); + nestedC.add(new TestNestedC()); + nestedList.setNestedC(nestedC); + + BeanWrapperFieldSetMapper mapper = new BeanWrapperFieldSetMapper(); + StaticApplicationContext context = new StaticApplicationContext(); + mapper.setBeanFactory(context); + context.getBeanFactory().registerSingleton("bean", nestedList); + mapper.setPrototypeBeanName("bean"); + + FieldSet fieldSet = new FieldSet(new String[]{ "1", "2", "3"}, new String[]{"NestedC[0].Value", "NestedC[1].Value", "NestedC[2].Value"}); + + mapper.mapLine(fieldSet); + + assertEquals(((TestNestedC) nestedList.getNestedC().get(0)).getValue(), 1); + assertEquals(((TestNestedC) nestedList.getNestedC().get(1)).getValue(), 2); + assertEquals(((TestNestedC) nestedList.getNestedC().get(2)).getValue(), 3); + + } + + private static class BeanWithIntArray { + private int[] numbers; + + public void setNumbers(int[] numbers) { + this.numbers = numbers; + } + } + + private static class TestNestedList { + + List nestedC; + + public List getNestedC() { + return nestedC; + } + + public void setNestedC(List nestedC) { + this.nestedC = nestedC; + } + + + + } + + private static class TestNestedA { + private String valueA; + + private int valueB; + + TestNestedB testObjectB; + + public TestNestedB getTestObjectB() { + return testObjectB; + } + + public void setTestObjectB(TestNestedB testObjectB) { + this.testObjectB = testObjectB; + } + + public String getValueA() { + return valueA; + } + + public void setValueA(String valueA) { + this.valueA = valueA; + } + + public int getValueB() { + return valueB; + } + + public void setValueB(int valueB) { + this.valueB = valueB; + } + + } + + private static class TestNestedB { + private String valueA; + + private TestNestedC testObjectC; + + public TestNestedC getTestObjectC() { + return testObjectC; + } + + public void setTestObjectC(TestNestedC testObjectC) { + this.testObjectC = testObjectC; + } + + public String getValueA() { + return valueA; + } + + public void setValueA(String valueA) { + this.valueA = valueA; + } + + } + + private static class TestNestedC { + private int value; + + public int getValue() { + return value; + } + + public void setValue(int value) { + this.value = value; + } + } + +} diff --git a/infrastructure/src/test/java/org/springframework/batch/io/file/support/mapping/PropertyMatchesTests.java b/infrastructure/src/test/java/org/springframework/batch/io/file/support/mapping/PropertyMatchesTests.java new file mode 100644 index 000000000..2769492f0 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/io/file/support/mapping/PropertyMatchesTests.java @@ -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.mapping; + +import org.springframework.batch.io.file.support.mapping.PropertyMatches; + +import junit.framework.TestCase; + +public class PropertyMatchesTests extends TestCase { + + public void setDuckSoup(String duckSoup) { + } + + public void setDuckPate(String duckPate) { + } + + public void setDuckBreast(String duckBreast) { + } + + public void testPropertyMatchesWithMaxDistance() throws Exception { + String[] matches = PropertyMatches.forProperty("DUCK_SOUP", getClass(), 2).getPossibleMatches(); + assertEquals(1, matches.length); + } + + public void testPropertyMatchesWithDefault() throws Exception { + String[] matches = PropertyMatches.forProperty("DUCK_SOUP", getClass()).getPossibleMatches(); + assertEquals(1, matches.length); + } + + public void testBuildErrorMessageNoMatches() throws Exception { + String msg = PropertyMatches.forProperty("foo", getClass(), 2).buildErrorMessage(); + assertTrue(msg.indexOf("foo")>=0); + } + + public void testBuildErrorMessagePossibleMatch() throws Exception { + String msg = PropertyMatches.forProperty("DUCKSOUP", getClass(), 1).buildErrorMessage(); + // the message contains the close match + assertTrue(msg.indexOf("duckSoup")>=0); + } + + public void testBuildErrorMessageMultiplePossibleMatches() throws Exception { + String msg = PropertyMatches.forProperty("DUCKCRAP", getClass(), 4).buildErrorMessage(); + // the message contains the close matches + assertTrue(msg.indexOf("duckSoup")>=0); + assertTrue(msg.indexOf("duckPate")>=0); + } + + public void testEmptyString() throws Exception { + String[] matches = PropertyMatches.forProperty("", getClass(), 4).getPossibleMatches(); + // TestCase base class has a name property + assertEquals("name", matches[0]); + } +} diff --git a/infrastructure/src/test/java/org/springframework/batch/io/file/support/mapping/TestObject.java b/infrastructure/src/test/java/org/springframework/batch/io/file/support/mapping/TestObject.java new file mode 100644 index 000000000..a6bf6f637 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/io/file/support/mapping/TestObject.java @@ -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.mapping; + +import java.math.BigDecimal; +import java.util.Date; + +public class TestObject { + String varString; + + boolean varBoolean; + + char varChar; + + byte varByte; + + short varShort; + + int varInt; + + long varLong; + + float varFloat; + + double varDouble; + + BigDecimal varBigDecimal; + + Date varDate; + + public Date getVarDate() { + return varDate; + } + + public void setVarDate(Date varDate) { + this.varDate = varDate; + } + + public TestObject() { + } + + public BigDecimal getVarBigDecimal() { + return varBigDecimal; + } + + public void setVarBigDecimal(BigDecimal varBigDecimal) { + this.varBigDecimal = varBigDecimal; + } + + public boolean isVarBoolean() { + return varBoolean; + } + + public void setVarBoolean(boolean varBoolean) { + this.varBoolean = varBoolean; + } + + public byte getVarByte() { + return varByte; + } + + public void setVarByte(byte varByte) { + this.varByte = varByte; + } + + public char getVarChar() { + return varChar; + } + + public void setVarChar(char varChar) { + this.varChar = varChar; + } + + public double getVarDouble() { + return varDouble; + } + + public void setVarDouble(double varDouble) { + this.varDouble = varDouble; + } + + public float getVarFloat() { + return varFloat; + } + + public void setVarFloat(float varFloat) { + this.varFloat = varFloat; + } + + public long getVarLong() { + return varLong; + } + + public void setVarLong(long varLong) { + this.varLong = varLong; + } + + public short getVarShort() { + return varShort; + } + + public void setVarShort(short varShort) { + this.varShort = varShort; + } + + public String getVarString() { + return varString; + } + + public void setVarString(String varString) { + this.varString = varString; + } + + public int getVarInt() { + return varInt; + } + + public void setVarInt(int varInt) { + this.varInt = varInt; + } +} diff --git a/infrastructure/src/test/java/org/springframework/batch/io/file/support/separator/DefaultRecordSeparatorPolicyTests.java b/infrastructure/src/test/java/org/springframework/batch/io/file/support/separator/DefaultRecordSeparatorPolicyTests.java new file mode 100644 index 000000000..3ada41564 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/io/file/support/separator/DefaultRecordSeparatorPolicyTests.java @@ -0,0 +1,79 @@ +/* + * 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.batch.io.file.support.separator.DefaultRecordSeparatorPolicy; + +import junit.framework.TestCase; + +public class DefaultRecordSeparatorPolicyTests extends TestCase { + + DefaultRecordSeparatorPolicy policy = new DefaultRecordSeparatorPolicy(); + + public void testNormalLine() throws Exception { + assertTrue(policy.isEndOfRecord("a string")); + } + + public void testQuoteUnterminatedLine() throws Exception { + assertFalse(policy.isEndOfRecord("a string\"one")); + } + + public void testEmptyLine() throws Exception { + assertTrue(policy.isEndOfRecord("")); + } + + public void testNullLine() throws Exception { + assertTrue(policy.isEndOfRecord(null)); + } + + public void testPostProcess() throws Exception { + String line = "foo\nbar"; + assertEquals(line, policy.postProcess(line)); + } + + public void testPreProcessWithQuote() throws Exception { + String line = "foo\"bar"; + assertEquals(line+"\n", policy.preProcess(line)); + } + + public void testPreProcessWithNotDefaultQuote() throws Exception { + String line = "foo'bar"; + policy.setQuoteCharacter("'"); + assertEquals(line+"\n", policy.preProcess(line)); + } + + public void testPreProcessWithoutQuote() throws Exception { + String line = "foo"; + assertEquals(line, policy.preProcess(line)); + } + + public void testContinuationMarkerNotEnd() throws Exception { + String line = "foo\\"; + assertFalse(policy.isEndOfRecord(line)); + } + + public void testNotDefaultContinuationMarkerNotEnd() throws Exception { + String line = "foo bar"; + policy.setContinuation("bar"); + assertFalse(policy.isEndOfRecord(line)); + } + + public void testContinuationMarkerRemoved() throws Exception { + String line = "foo\\"; + assertEquals("foo", policy.preProcess(line)); + } +} diff --git a/infrastructure/src/test/java/org/springframework/batch/io/file/support/separator/SimpleRecordSeparatorPolicyTests.java b/infrastructure/src/test/java/org/springframework/batch/io/file/support/separator/SimpleRecordSeparatorPolicyTests.java new file mode 100644 index 000000000..620005bd3 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/io/file/support/separator/SimpleRecordSeparatorPolicyTests.java @@ -0,0 +1,48 @@ +/* + * 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.batch.io.file.support.separator.SimpleRecordSeparatorPolicy; + +import junit.framework.TestCase; + +public class SimpleRecordSeparatorPolicyTests extends TestCase { + + SimpleRecordSeparatorPolicy policy = new SimpleRecordSeparatorPolicy(); + + public void testNormalLine() throws Exception { + assertTrue(policy.isEndOfRecord("a string")); + } + + public void testEmptyLine() throws Exception { + assertTrue(policy.isEndOfRecord("")); + } + + public void testNullLine() throws Exception { + assertTrue(policy.isEndOfRecord(null)); + } + + public void testPostProcess() throws Exception { + String line = "foo\nbar"; + assertEquals(line, policy.postProcess(line)); + } + + public void testPreProcess() throws Exception { + String line = "foo\nbar"; + assertEquals(line, policy.preProcess(line)); + } +} diff --git a/infrastructure/src/test/java/org/springframework/batch/io/file/support/separator/SuffixRecordSeparatorPolicyTests.java b/infrastructure/src/test/java/org/springframework/batch/io/file/support/separator/SuffixRecordSeparatorPolicyTests.java new file mode 100644 index 000000000..1384bcbff --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/io/file/support/separator/SuffixRecordSeparatorPolicyTests.java @@ -0,0 +1,69 @@ +/* + * 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.batch.io.file.support.separator.SuffixRecordSeparatorPolicy; + +import junit.framework.TestCase; + +public class SuffixRecordSeparatorPolicyTests extends TestCase { + + private static final String LINE = "a string"; + SuffixRecordSeparatorPolicy policy = new SuffixRecordSeparatorPolicy(); + + public void testNormalLine() throws Exception { + assertFalse(policy.isEndOfRecord(LINE)); + } + + public void testNormalLineWithDefaultSuffix() throws Exception { + assertTrue(policy.isEndOfRecord(LINE+SuffixRecordSeparatorPolicy.DEFAULT_SUFFIX)); + } + + public void testNormalLineWithNonDefaultSuffix() throws Exception { + policy.setSuffix(":foo"); + assertTrue(policy.isEndOfRecord(LINE+ ":foo")); + } + + public void testNormalLineWithDefaultSuffixAndWhitespace() throws Exception { + assertTrue(policy.isEndOfRecord(LINE+SuffixRecordSeparatorPolicy.DEFAULT_SUFFIX+" ")); + } + + public void testNormalLineWithDefaultSuffixWithIgnoreWhitespace() throws Exception { + policy.setIgnoreWhitespace(false); + assertFalse(policy.isEndOfRecord(LINE+SuffixRecordSeparatorPolicy.DEFAULT_SUFFIX+" ")); + } + + public void testEmptyLine() throws Exception { + assertFalse(policy.isEndOfRecord("")); + } + + public void testNullLineIsEndOfRecord() throws Exception { + assertTrue(policy.isEndOfRecord(null)); + } + + public void testPostProcessSunnyDay() throws Exception { + String line = LINE; + String record = line+SuffixRecordSeparatorPolicy.DEFAULT_SUFFIX; + assertEquals(line, policy.postProcess(record)); + } + + public void testPostProcessNullLine() throws Exception { + String line = null; + assertEquals(null, policy.postProcess(line)); + } + +} diff --git a/infrastructure/src/test/java/org/springframework/batch/io/file/support/transform/DelimitedLineAggregatorTests.java b/infrastructure/src/test/java/org/springframework/batch/io/file/support/transform/DelimitedLineAggregatorTests.java new file mode 100644 index 000000000..2fe31d53c --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/io/file/support/transform/DelimitedLineAggregatorTests.java @@ -0,0 +1,41 @@ +/* + * 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.support.transform.DelimitedLineAggregator; + +import junit.framework.TestCase; + +/** + * Unit tests for {@link DelimitedLineAggregator} + * + * @author robert.kasanicky + */ +public class DelimitedLineAggregatorTests extends TestCase { + private DelimitedLineAggregator aggregator; + + public void testAggregate() { + aggregator = new DelimitedLineAggregator(); + aggregator.setDelimiter(":"); + + String[] args = { "a", "bc", "def" }; + String expectedResult = "a:bc:def"; + String result = aggregator.aggregate(args); + assertEquals(result, expectedResult); + } + +} diff --git a/infrastructure/src/test/java/org/springframework/batch/io/file/support/transform/DelimitedLineTokenizerTests.java b/infrastructure/src/test/java/org/springframework/batch/io/file/support/transform/DelimitedLineTokenizerTests.java new file mode 100644 index 000000000..0b2bad999 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/io/file/support/transform/DelimitedLineTokenizerTests.java @@ -0,0 +1,160 @@ +/* + * 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 junit.framework.TestCase; + +import org.springframework.batch.io.file.FieldSet; +import org.springframework.batch.io.file.support.transform.AbstractLineTokenizer; +import org.springframework.batch.io.file.support.transform.DelimitedLineTokenizer; + +public class DelimitedLineTokenizerTests extends TestCase { + + private static final String TOKEN_MATCHES = "token equals the expected string"; + + private DelimitedLineTokenizer tokenizer = new DelimitedLineTokenizer(); + + public void testTokenizeRegularUse() { + FieldSet tokens = tokenizer.tokenize("sfd,\"Well,I have no idea what to do in the afternoon\",sFj, asdf,,as\n"); + assertEquals(6, tokens.getFieldCount()); + assertTrue(TOKEN_MATCHES, tokens.readString(0).equals("sfd")); + assertTrue(TOKEN_MATCHES, tokens.readString(1).equals("Well,I have no idea what to do in the afternoon")); + assertTrue(TOKEN_MATCHES, tokens.readString(2).equals("sFj")); + assertTrue(TOKEN_MATCHES, tokens.readString(3).equals("asdf")); + assertTrue(TOKEN_MATCHES, tokens.readString(4).equals("")); + assertTrue(TOKEN_MATCHES, tokens.readString(5).equals("as")); + + tokens = tokenizer.tokenize("First string,"); + assertEquals(2, tokens.getFieldCount()); + assertTrue(TOKEN_MATCHES, tokens.readString(0).equals("First string")); + assertTrue(TOKEN_MATCHES, tokens.readString(1).equals("")); + } + + public void testInvalidConstructorArgument() { + try { + new DelimitedLineTokenizer(DelimitedLineTokenizer.DEFAULT_QUOTE_CHARACTER); + fail("Quote character can't be used as delimiter for delimited line tokenizer!"); + } + catch (Exception e) { + assertTrue(true); + } + } + + public void testDelimitedLineTokenizer() { + FieldSet line = tokenizer.tokenize("a,b,c"); + assertEquals(3, line.getFieldCount()); + } + + public void testNames() { + tokenizer.setNames(new String[] {"A", "B", "C"}); + FieldSet line = tokenizer.tokenize("a,b,c"); + assertEquals(3, line.getFieldCount()); + assertEquals("a", line.readString("A")); + } + + public void testTooFewNames() { + tokenizer.setNames(new String[] {"A", "B"}); + try { + tokenizer.tokenize("a,b,c"); + fail("Expected IllegalArgumentException"); + } + catch (IllegalArgumentException e) { + // expected + } + } + + public void testTooManyNames() { + tokenizer.setNames(new String[] {"A", "B", "C", "D"}); + FieldSet line = tokenizer.tokenize("a,b,c"); + assertEquals(4, line.getFieldCount()); + assertEquals("c", line.readString("C")); + assertEquals(null, line.readString("D")); + } + + public void testDelimitedLineTokenizerChar() { + AbstractLineTokenizer tokenizer = new DelimitedLineTokenizer(' '); + FieldSet line = tokenizer.tokenize("a b c"); + assertEquals(3, line.getFieldCount()); + } + + public void testTokenizeWithQuotes() { + FieldSet line = tokenizer.tokenize("a,b,\"c\""); + assertEquals(3, line.getFieldCount()); + assertEquals("c", line.readString(2)); + } + + public void testTokenizeWithNotDefaultQuotes() { + tokenizer.setQuoteCharacter('\''); + FieldSet line = tokenizer.tokenize("a,b,'c'"); + assertEquals(3, line.getFieldCount()); + assertEquals("c", line.readString(2)); + } + + public void testTokenizeWithEscapedQuotes() { + FieldSet line = tokenizer.tokenize("a,\"\"b,\"\"\"c\""); + assertEquals(3, line.getFieldCount()); + assertEquals("\"\"b", line.readString(1)); + assertEquals("\"c", line.readString(2)); + } + + public void testTokenizeWithUnclosedQuotes() { + tokenizer.setQuoteCharacter('\''); + FieldSet line = tokenizer.tokenize("a,\"b,c"); + assertEquals(3, line.getFieldCount()); + assertEquals("\"b", line.readString(1)); + assertEquals("c", line.readString(2)); + } + + public void testTokenizeWithDelimiterAtEnd() { + FieldSet line = tokenizer.tokenize("a,b,c,"); + assertEquals(4, line.getFieldCount()); + assertEquals("c", line.readString(2)); + assertEquals("", line.readString(3)); + } + + public void testEmptyLine() throws Exception { + FieldSet line = tokenizer.tokenize(""); + assertEquals(0, line.getFieldCount()); + } + + public void testWhitespaceLine() throws Exception { + FieldSet line = tokenizer.tokenize(" "); + // whitespace counts as text + assertEquals(1, line.getFieldCount()); + } + + public void testNullLine() throws Exception { + FieldSet line = tokenizer.tokenize(null); + // null doesn't... + assertEquals(0, line.getFieldCount()); + } + + public void testMultiLineField() throws Exception { + FieldSet line = tokenizer.tokenize("a,b,c\nrap"); + assertEquals(3, line.getFieldCount()); + assertEquals("c\nrap", line.readString(2)); + + } + + public void testMultiLineFieldWithQuotes() throws Exception { + FieldSet line = tokenizer.tokenize("a,b,\"c\nrap\""); + assertEquals(3, line.getFieldCount()); + assertEquals("c\nrap", line.readString(2)); + + } + +} diff --git a/infrastructure/src/test/java/org/springframework/batch/io/file/support/transform/FixedLengthLineAggregatorTests.java b/infrastructure/src/test/java/org/springframework/batch/io/file/support/transform/FixedLengthLineAggregatorTests.java new file mode 100644 index 000000000..8898a384a --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/io/file/support/transform/FixedLengthLineAggregatorTests.java @@ -0,0 +1,128 @@ +/* + * 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.support.transform.FixedLengthLineAggregator; + +import junit.framework.TestCase; + +/** + * Unit tests for {@link FixedLengthLineAggregator} + * + * @author robert.kasanicky + */ +public class FixedLengthLineAggregatorTests extends TestCase { + + // object under test + private FixedLengthLineAggregator aggregator = new FixedLengthLineAggregator(); + + /** + * Record descriptor is null => BatchCriticalException + */ + public void testAggregateNullRecordDescriptor() { + String[] args = { "does not matter what is here" }; + + try { + aggregator.aggregate(args); + fail("should not work with null LineDescriptor"); + } + catch (IllegalArgumentException expected) { + // expected + } + } + + /** + * Argument count does not match the number of fields in the + * LineDescriptor + */ + public void testAggregateWrongArgumentCount() { + String[] args = { "only one argument" }; + aggregator.setLengths(new int[0]); + + try { + aggregator.aggregate(args); + fail("Wrong argument count, exception exptected"); + } + catch (IllegalArgumentException expected) { + assertTrue(true); + } + } + + /** + * Argument length exceeds the length specified by FieldDescriptor + */ + public void testAggregateInvalidInputLength() { + String[] args = { "Oversize" }; + aggregator.setLengths(new int[] {args[0].length()-1}); + try { + aggregator.aggregate(args); + fail("Invalid argument length, exception should have been thrown"); + } + catch (IllegalArgumentException expected) { + // expected + } + } + + /** + * Regular use with valid LineDescriptor + */ + public void testAggregate() { + String[] args = { "Matchsize", "Smallsize" }; + aggregator.setLengths(new int[] {args[0].length(), args[1].length()}); + String result = aggregator.aggregate(args); + assertEquals("MatchsizeSmallsize", result); + } + + /** + * Regular use with valid LineDescriptor + */ + public void testAggregateFormattedRight() { + String[] args = { "Matchsize", "Smallsize" }; + aggregator.setAlignment("right"); + aggregator.setLengths(new int[] {args[0].length()+4, args[1].length()+1}); + String result = aggregator.aggregate(args); + assertEquals(result, " Matchsize Smallsize"); + } + + /** + * Regular use with valid LineDescriptor + */ + public void testAggregateFormattedCenter() { + String[] args = { "Matchsize", "Smallsize" }; + aggregator.setAlignment("center"); + aggregator.setLengths(new int[] {args[0].length()+4, args[1].length()+1}); + String result = aggregator.aggregate(args); + assertEquals(result, " Matchsize Smallsize "); + } + + /** + * If one of the passed arguments is null, string filled with spaces should + * be returned + */ + public void testAggregateNullArgument() { + String[] args = { null }; + + aggregator.setLengths(new int[] {3}); + + try { + assertEquals(" ", aggregator.aggregate(args)); + } + catch (NullPointerException unexpected) { + fail("incorrect handling of null arguments"); + } + } +} diff --git a/infrastructure/src/test/java/org/springframework/batch/io/file/support/transform/FixedLengthTokenizerTests.java b/infrastructure/src/test/java/org/springframework/batch/io/file/support/transform/FixedLengthTokenizerTests.java new file mode 100644 index 000000000..095e12a92 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/io/file/support/transform/FixedLengthTokenizerTests.java @@ -0,0 +1,112 @@ +/* + * 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 junit.framework.TestCase; + +import org.springframework.batch.io.file.FieldSet; +import org.springframework.batch.io.file.support.transform.FixedLengthTokenizer; + +public class FixedLengthTokenizerTests extends TestCase { + + private FixedLengthTokenizer tokenizer = new FixedLengthTokenizer(); + + private String line = null; + + /** + * even if null or empty string is tokenized, tokenizer returns as many + * empty tokens as defined by recordDescriptor. + */ + public void testTokenizeEmptyString() { + tokenizer.setLengths(new int[] {5,5,5}); + FieldSet tokens = tokenizer.tokenize(null); + assertEquals(0, tokens.getFieldCount()); + } + + public void testTokenizeNullString() { + tokenizer.setLengths(new int[] {5,5,5}); + FieldSet tokens = tokenizer.tokenize(""); + assertEquals(0, tokens.getFieldCount()); + } + + public void testTokenizeRegularUse() { + tokenizer.setLengths(new int[] {2,5,5}); + // test shorter line as defined by record descriptor + line = "H1"; + FieldSet tokens = tokenizer.tokenize(line); + assertEquals(3, tokens.getFieldCount()); + assertEquals("H1", tokens.readString(0)); + assertEquals("", tokens.readString(1)); + assertEquals("", tokens.readString(2)); + } + + public void testNormalLength() throws Exception { + tokenizer.setLengths(new int[] {10,15,5}); + // test shorter line as defined by record descriptor + line = "H1"; + FieldSet tokens = tokenizer.tokenize(line); + // test normal length + line = "H1 12345678 12345"; + tokens = tokenizer.tokenize(line); + assertEquals(3, tokens.getFieldCount()); + assertEquals(line.substring(0, 10).trim(), tokens.readString(0)); + assertEquals(line.substring(10, 25).trim(), tokens.readString(1)); + assertEquals(line.substring(25).trim(), tokens.readString(2)); + } + + public void testLongerLinesRestIgnored() throws Exception { + tokenizer.setLengths(new int[] {10,15,5}); + // test shorter line as defined by record descriptor + line = "H1"; + FieldSet tokens = tokenizer.tokenize(line); + // test longer lines => rest will be ignored + line = "H1 12345678 1234567890"; + tokens = tokenizer.tokenize(line); + assertEquals(3, tokens.getFieldCount()); + assertEquals(line.substring(0, 10).trim(), tokens.readString(0)); + assertEquals(line.substring(10, 25).trim(), tokens.readString(1)); + assertEquals(line.substring(25, 30).trim(), tokens.readString(2)); + } + + public void testAnotherTypeOfRecord() throws Exception { + tokenizer.setLengths(new int[] {5,10,10,2}); + // test shorter line as defined by record descriptor + line = "H1"; + FieldSet tokens = tokenizer.tokenize(line); + // test another type of record + line = "H2 123456 12345 12"; + tokens = tokenizer.tokenize(line); + assertEquals(4, tokens.getFieldCount()); + assertEquals(line.substring(0, 5).trim(), tokens.readString(0)); + assertEquals(line.substring(5, 15).trim(), tokens.readString(1)); + assertEquals(line.substring(15, 25).trim(), tokens.readString(2)); + assertEquals(line.substring(25).trim(), tokens.readString(3)); + } + + public void testTokenizerInvalidSetup() { + tokenizer.setNames(new String[] {"a", "b"}); + tokenizer.setLengths(new int[] {5,5,5,2}); + + try { + tokenizer.tokenize("McDonalds - I'm lovin' it."); + fail("tokenizer works even with invalid names!"); + } + catch (Exception e) { + assertTrue(true); + } + } +} diff --git a/infrastructure/src/test/java/org/springframework/batch/io/file/support/transform/LineAggregatorStub.java b/infrastructure/src/test/java/org/springframework/batch/io/file/support/transform/LineAggregatorStub.java new file mode 100644 index 000000000..849753aae --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/io/file/support/transform/LineAggregatorStub.java @@ -0,0 +1,41 @@ +/* + * 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.support.transform.LineAggregator; + + +/** + * Stub implementation of {@link LineAggregator} interface for testing purposes. + * + * @author robert.kasanicky + */ +public class LineAggregatorStub implements LineAggregator { + + /** + * Concatenates arguments. Ignores the LineDescriptor. + */ + public String aggregate(String[] args) { + String result = ""; + + for (int i = 1; i < args.length; i++) { + result = result + args[i]; + } + + return result; + } +} diff --git a/infrastructure/src/test/java/org/springframework/batch/io/file/support/transform/PrefixMatchingCompositeLineTokenizerTests.java b/infrastructure/src/test/java/org/springframework/batch/io/file/support/transform/PrefixMatchingCompositeLineTokenizerTests.java new file mode 100644 index 000000000..07fe1ef19 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/io/file/support/transform/PrefixMatchingCompositeLineTokenizerTests.java @@ -0,0 +1,97 @@ +/* + * 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.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +import junit.framework.TestCase; + +import org.springframework.batch.io.file.FieldSet; +import org.springframework.batch.io.file.support.transform.DelimitedLineTokenizer; +import org.springframework.batch.io.file.support.transform.LineTokenizer; +import org.springframework.batch.io.file.support.transform.PrefixMatchingCompositeLineTokenizer; + +public class PrefixMatchingCompositeLineTokenizerTests extends TestCase { + + PrefixMatchingCompositeLineTokenizer tokenizer = new PrefixMatchingCompositeLineTokenizer(); + + public void testNoTokenizers() throws Exception { + try { + tokenizer.tokenize("a line"); + fail("Expected IllegalStateException"); + } catch (IllegalStateException e) { + // expected + } + } + + public void testNullLine() throws Exception { + tokenizer.setTokenizers(Collections.singletonMap("foo", new DelimitedLineTokenizer())); + FieldSet fields = tokenizer.tokenize(null); + assertEquals(0, fields.getFieldCount()); + } + + public void testEmptyKeyMatchesAnyLine() throws Exception { + Map map = new HashMap(); + map.put("", new DelimitedLineTokenizer()); + map.put("foo", new LineTokenizer() { + public FieldSet tokenize(String line) { + return null; + } + }); + tokenizer.setTokenizers(map); + FieldSet fields = tokenizer.tokenize("abc"); + assertEquals(1, fields.getFieldCount()); + } + + public void testEmptyKeyDoesNotMatchWhenAlternativeAvailable() throws Exception { + + Map map = new LinkedHashMap(); + map.put("", new LineTokenizer() { + public FieldSet tokenize(String line) { + return null; + } + }); + map.put("foo", new DelimitedLineTokenizer()); + tokenizer.setTokenizers(map); + FieldSet fields = tokenizer.tokenize("foo,bar"); + assertEquals("bar", fields.readString(1)); + } + + public void testNoMatch() throws Exception { + tokenizer.setTokenizers(Collections.singletonMap("foo", new DelimitedLineTokenizer())); + try { + tokenizer.tokenize("nomatch"); + fail("Expected IllegalStateException"); + } catch (IllegalStateException e) { + // expected + } + } + + public void testMatchWithPrefix() throws Exception { + tokenizer.setTokenizers(Collections.singletonMap("foo", new LineTokenizer() { + public FieldSet tokenize(String line) { + return new FieldSet(new String[] {line}); + } + })); + FieldSet fields = tokenizer.tokenize("foo bar"); + assertEquals(1, fields.getFieldCount()); + assertEquals("foo bar", fields.readString(0)); + } +} diff --git a/infrastructure/src/test/java/org/springframework/batch/io/sample/domain/Customer.java b/infrastructure/src/test/java/org/springframework/batch/io/sample/domain/Customer.java new file mode 100644 index 000000000..495d39a3b --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/io/sample/domain/Customer.java @@ -0,0 +1,90 @@ +/* + * 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.sample.domain; + +import org.apache.commons.lang.builder.EqualsBuilder; +import org.apache.commons.lang.builder.HashCodeBuilder; +import org.apache.commons.lang.builder.ToStringBuilder; + +/** + * An XML customer. + * + * This is a complex type. + */ +public class Customer { + private String name; + + private String address; + + private int age; + + private int moo; + + private int poo; + + public String getAddress() { + return address; + } + + public void setAddress(String address) { + this.address = address; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getMoo() { + return moo; + } + + public void setMoo(int moo) { + this.moo = moo; + } + + public int getPoo() { + return poo; + } + + public void setPoo(int poo) { + this.poo = poo; + } + + public boolean equals(Object obj) { + return EqualsBuilder.reflectionEquals(obj, this); + } + + public int hashCode() { + return HashCodeBuilder.reflectionHashCode(this); + } + + public String toString() { + return ToStringBuilder.reflectionToString(this); + } +} diff --git a/infrastructure/src/test/java/org/springframework/batch/io/sample/domain/LineItem.java b/infrastructure/src/test/java/org/springframework/batch/io/sample/domain/LineItem.java new file mode 100644 index 000000000..f8717f82e --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/io/sample/domain/LineItem.java @@ -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.sample.domain; + +import org.apache.commons.lang.builder.EqualsBuilder; +import org.apache.commons.lang.builder.HashCodeBuilder; +import org.apache.commons.lang.builder.ToStringBuilder; + +/** + * An XML line-item. + * + * This is a complex type. + */ +public class LineItem { + private String description; + + private double perUnitOunces; + + private double price; + + private int quantity; + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public double getPerUnitOunces() { + return perUnitOunces; + } + + public void setPerUnitOunces(double perUnitOunces) { + this.perUnitOunces = perUnitOunces; + } + + public double getPrice() { + return price; + } + + public void setPrice(double price) { + this.price = price; + } + + public int getQuantity() { + return quantity; + } + + public void setQuantity(int quantity) { + this.quantity = quantity; + } + + public boolean equals(Object obj) { + return EqualsBuilder.reflectionEquals(obj, this); + } + + public int hashCode() { + return HashCodeBuilder.reflectionHashCode(this); + } + + public String toString() { + return ToStringBuilder.reflectionToString(this); + } +} diff --git a/infrastructure/src/test/java/org/springframework/batch/io/sample/domain/Order.java b/infrastructure/src/test/java/org/springframework/batch/io/sample/domain/Order.java new file mode 100644 index 000000000..1d010cacd --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/io/sample/domain/Order.java @@ -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.sample.domain; + +import java.util.Date; +import java.util.List; + +import org.apache.commons.lang.builder.EqualsBuilder; +import org.apache.commons.lang.builder.HashCodeBuilder; +import org.apache.commons.lang.builder.ToStringBuilder; + +/** + * An XML order. + * + * This is a complex type. + */ +public class Order { + private Customer customer; + + private Date date; + + private List lineItems; + + private Shipper shipper; + + public Customer getCustomer() { + return customer; + } + + public void setCustomer(Customer customer) { + this.customer = customer; + } + + public Date getDate() { + return date; + } + + public void setDate(Date date) { + this.date = date; + } + + public List getLineItems() { + return lineItems; + } + + public void setLineItems(List lineItems) { + this.lineItems = lineItems; + } + + public Shipper getShipper() { + return shipper; + } + + public void setShipper(Shipper shipper) { + this.shipper = shipper; + } + + public boolean equals(Object obj) { + return EqualsBuilder.reflectionEquals(obj, this); + } + + public int hashCode() { + return HashCodeBuilder.reflectionHashCode(this); + } + + public String toString() { + return ToStringBuilder.reflectionToString(this); + } +} diff --git a/infrastructure/src/test/java/org/springframework/batch/io/sample/domain/Shipper.java b/infrastructure/src/test/java/org/springframework/batch/io/sample/domain/Shipper.java new file mode 100644 index 000000000..0edcb77c0 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/io/sample/domain/Shipper.java @@ -0,0 +1,60 @@ +/* + * 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.sample.domain; + +import org.apache.commons.lang.builder.EqualsBuilder; +import org.apache.commons.lang.builder.HashCodeBuilder; +import org.apache.commons.lang.builder.ToStringBuilder; + +/** + * An XML shipper. + * + * This is a complex type. + */ +public class Shipper { + private String name; + + private double perOunceRate; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public double getPerOunceRate() { + return perOunceRate; + } + + public void setPerOunceRate(double perOunceRate) { + this.perOunceRate = perOunceRate; + } + + public boolean equals(Object obj) { + return EqualsBuilder.reflectionEquals(obj, this); + } + + public int hashCode() { + return HashCodeBuilder.reflectionHashCode(this); + } + + public String toString() { + return ToStringBuilder.reflectionToString(this); + } +} diff --git a/infrastructure/src/test/java/org/springframework/batch/io/sql/SingleKeySqlDrivingQueryInputSourceIntegrationTests.java b/infrastructure/src/test/java/org/springframework/batch/io/sql/SingleKeySqlDrivingQueryInputSourceIntegrationTests.java new file mode 100644 index 000000000..97c70f6b5 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/io/sql/SingleKeySqlDrivingQueryInputSourceIntegrationTests.java @@ -0,0 +1,152 @@ +package org.springframework.batch.io.sql; + +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.springframework.batch.repeat.synch.BatchTransactionSynchronizationManager; +import org.springframework.batch.restart.RestartData; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.test.AbstractTransactionalDataSourceSpringContextTests; + +public class SingleKeySqlDrivingQueryInputSourceIntegrationTests + extends AbstractTransactionalDataSourceSpringContextTests { + + SingleKeySqlDrivingQueryInputSource sqlInputSource; + + protected String[] getConfigLocations(){ + return new String[] { "org/springframework/batch/io/sql/data-source-context.xml"}; + } + + protected void onSetUp()throws Exception{ + super.onSetUp(); + + sqlInputSource = createInputSource(); + } + + protected SingleKeySqlDrivingQueryInputSource createInputSource(){ + + SingleKeySqlDrivingQueryInputSource inputSource = new SingleKeySqlDrivingQueryInputSource(); + inputSource.setDrivingQuery("SELECT ID from T_FOOS order by ID"); + inputSource.setDetailsQuery("SELECT NAME, VALUE from T_FOOS where ID = ?"); + inputSource.setRestartQuery("SELECT ID from T_FOOS where ID > ? order by ID"); + inputSource.setMapper(new FooMapper()); + inputSource.setDataSource(super.getJdbcTemplate().getDataSource()); + return inputSource; + } + + protected void onTearDown()throws Exception{ + + BatchTransactionSynchronizationManager.clearSynchronizations(); + sqlInputSource.close(); + super.onTearDown(); + } + + public void testNormalProcessing(){ + + Foo foo = (Foo)sqlInputSource.read(); + assertEquals(1, foo.value); + + foo = (Foo)sqlInputSource.read(); + assertEquals(2, foo.value); + + foo = (Foo)sqlInputSource.read(); + assertEquals(3, foo.value); + + foo = (Foo)sqlInputSource.read(); + assertEquals(4, foo.value); + + foo = (Foo)sqlInputSource.read(); + assertEquals(5, foo.value); + + assertNull(sqlInputSource.read()); + } + +/* public void testRollback(){ + + SqlIdentityKey key = sqlInputSource.readKey(); + assertEquals("1", key.getKeyValue("id")); + + key = sqlInputSource.readKey(); + assertEquals("2", key.getKeyValue("id")); + + super.setComplete(); + super.endTransaction(); + super.startNewTransaction(); + BatchTransactionSynchronizationManager.resynchronize(); + + key = sqlInputSource.readKey(); + assertEquals("3", key.getKeyValue("id")); + + key = sqlInputSource.readKey(); + assertEquals("4", key.getKeyValue("id")); + + super.endTransaction(); + super.startNewTransaction(); + + key = sqlInputSource.readKey(); + assertEquals("3", key.getKeyValue("id")); + + }*/ + + public void testRestart(){ + + Foo foo = (Foo)sqlInputSource.read(); + assertEquals(1, foo.value); + + foo = (Foo)sqlInputSource.read(); + assertEquals(2, foo.value); + + RestartData restartData = sqlInputSource.getRestartData(); + + //create new input source + sqlInputSource = createInputSource(); + + sqlInputSource.restoreFrom(restartData); + + foo = (Foo)sqlInputSource.read(); + assertEquals(3, foo.value); + } + + //test that reading from an input source and then trying to restore causes an error. + public void testInvalidRestore(){ + + Foo foo = (Foo)sqlInputSource.read(); + assertEquals(1, foo.value); + + foo = (Foo)sqlInputSource.read(); + assertEquals(2, foo.value); + + RestartData restartData = sqlInputSource.getRestartData(); + + //create new input source + sqlInputSource = createInputSource(); + + foo = (Foo)sqlInputSource.read(); + assertEquals(1, foo.value); + + try{ + sqlInputSource.restoreFrom(restartData); + fail(); + } + catch(IllegalStateException ex){ + //expected + } + } + + private class Foo { + String name; + int value; + } + + private class FooMapper implements RowMapper{ + + public Object mapRow(ResultSet rs, int rowNum) throws SQLException { + + Foo foo = new Foo(); + foo.name = rs.getString(1); + foo.value = rs.getInt(2); + return foo; + } + + } +} diff --git a/infrastructure/src/test/java/org/springframework/batch/io/sql/SqlCursorInputSourceIntegrationTests.java b/infrastructure/src/test/java/org/springframework/batch/io/sql/SqlCursorInputSourceIntegrationTests.java new file mode 100644 index 000000000..35cfcce6b --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/io/sql/SqlCursorInputSourceIntegrationTests.java @@ -0,0 +1,240 @@ +package org.springframework.batch.io.sql; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Properties; + +import org.springframework.batch.repeat.context.RepeatContextSupport; +import org.springframework.batch.repeat.synch.BatchTransactionSynchronizationManager; +import org.springframework.batch.repeat.synch.RepeatSynchronizationManager; +import org.springframework.batch.restart.RestartData; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.test.AbstractTransactionalDataSourceSpringContextTests; + +/** + * + * @author Lucas Ward + * + */ +public class SqlCursorInputSourceIntegrationTests extends AbstractTransactionalDataSourceSpringContextTests { + + protected SqlCursorInputSource sqlCursorInputSource; + + private RowMapper mapper = new SqlRowMapper(); + + private static final String CURRENT_PROCESSED_ROW = "sqlCursorInput.lastProcessedRowNum"; + private static final String SKIP_COUNT = "sqlCursorInput.skippedRrecordCount"; + + protected String[] getConfigLocations(){ + return new String[] { "org/springframework/batch/io/sql/data-source-context.xml"}; + } + + protected void onSetUp()throws Exception{ + sqlCursorInputSource = getNewInputSource(); + sqlCursorInputSource.setMapper(mapper); + RepeatSynchronizationManager.register(new RepeatContextSupport(null)); + super.onSetUp(); + } + + protected void onTearDown()throws Exception{ + //cursor must be closed between each test, and transaction synchronization + //list must be cleared. + BatchTransactionSynchronizationManager.clearSynchronizations(); + RepeatSynchronizationManager.clear(); + sqlCursorInputSource.close(); + super.onTearDown(); + } + + protected SqlCursorInputSource getNewInputSource(){ + + return new SqlCursorInputSource(super.getJdbcTemplate().getDataSource(), + "SELECT * from T_FOOS"); + } + + public void testNormalReading(){ + + int fooCount = 0; + + for(;;){ + Foo foo = (Foo)sqlCursorInputSource.read(); + + if( foo == null){ + break; + } + + fooCount++; + validateFoo(fooCount, "bar" + fooCount, fooCount, foo); + + } + + assertEquals(5, fooCount ); + } + + public void testRestart(){ + + sqlCursorInputSource.read(); + Foo foo = (Foo)sqlCursorInputSource.read(); + + validateFoo(2, "bar2", 2, foo); + + RestartData restartData = sqlCursorInputSource.getRestartData(); + + sqlCursorInputSource = getNewInputSource(); + sqlCursorInputSource.setMapper(mapper); + + foo = (Foo)sqlCursorInputSource.read(); + validateFoo(1, "bar1", 1, foo); + + sqlCursorInputSource.restoreFrom(restartData); + + foo = (Foo)sqlCursorInputSource.read(); + + validateFoo(3, "bar3", 3, foo); + } + + public void testReadWithNullMapper(){ + + //calling read without a mapper should throw an exception. + sqlCursorInputSource.setMapper(null); + try{ + sqlCursorInputSource.read(); + fail(); + } + catch(IllegalStateException ex){ + //expected + } + } + + public void testStatistics(){ + + Properties statistics = sqlCursorInputSource.getStatistics(); + assertEquals("0", statistics.getProperty(CURRENT_PROCESSED_ROW)); + sqlCursorInputSource.read(); + + statistics = sqlCursorInputSource.getStatistics(); + assertEquals("1", statistics.getProperty(CURRENT_PROCESSED_ROW)); + } + + public void testSkipCountStatistics(){ + + sqlCursorInputSource.read(); + Foo foo = (Foo)sqlCursorInputSource.read(); + validateFoo(2, "bar2", 2, foo); + + Properties statistics = sqlCursorInputSource.getStatistics(); + assertEquals("0", statistics.getProperty(SKIP_COUNT)); + + sqlCursorInputSource.skip(); + + statistics = sqlCursorInputSource.getStatistics(); + assertEquals("1", statistics.getProperty(SKIP_COUNT)); + + sqlCursorInputSource.read(); + + sqlCursorInputSource.read(); + sqlCursorInputSource.skip(); + + statistics = sqlCursorInputSource.getStatistics(); + assertEquals("2", statistics.getProperty(SKIP_COUNT)); + + super.endTransaction(); + super.startNewTransaction(); + + sqlCursorInputSource.read(); + + statistics = sqlCursorInputSource.getStatistics(); + assertEquals("2", statistics.getProperty(SKIP_COUNT)); + } + + public void testRollback(){ + + sqlCursorInputSource.read(); + Foo foo = (Foo)sqlCursorInputSource.read(); + validateFoo(2, "bar2", 2, foo); + + super.setComplete(); + super.endTransaction(); + super.startNewTransaction(); + BatchTransactionSynchronizationManager.resynchronize(); + + sqlCursorInputSource.read(); + foo = (Foo)sqlCursorInputSource.read(); + validateFoo(4, "bar4", 4, foo); + + super.endTransaction(); + super.startNewTransaction(); + + foo = (Foo)sqlCursorInputSource.read(); + validateFoo(3, "bar3", 3, foo); + } + + public void testSkip(){ + + sqlCursorInputSource.read(); + Foo foo = (Foo)sqlCursorInputSource.read(); + validateFoo(2, "bar2", 2, foo); + + sqlCursorInputSource.skip(); + + foo = (Foo)sqlCursorInputSource.read(); + validateFoo(3, "bar3", 3, foo); + + super.endTransaction(); + super.startNewTransaction(); + + foo = (Foo)sqlCursorInputSource.read(); + validateFoo(1, "bar1", 1, foo); + + foo = (Foo)sqlCursorInputSource.read(); + validateFoo(3, "bar3", 3, foo); + } + + public void testSucessiveSkip(){ + + sqlCursorInputSource.read(); + Foo foo = (Foo)sqlCursorInputSource.read(); + validateFoo(2, "bar2", 2, foo); + sqlCursorInputSource.skip(); + + foo = (Foo)sqlCursorInputSource.read(); + validateFoo(3, "bar3", 3, foo); + sqlCursorInputSource.skip(); + + super.endTransaction(); + super.startNewTransaction(); + + foo = (Foo)sqlCursorInputSource.read(); + validateFoo(1, "bar1", 1, foo); + + foo = (Foo)sqlCursorInputSource.read(); + validateFoo(4, "bar4", 4, foo); + } + + private void validateFoo(int id, String name, int value, Foo foo){ + + assertEquals(id, foo.id); + assertEquals(name, foo.name); + assertEquals(value, foo.value); + } + + private class SqlRowMapper implements RowMapper { + + public Object mapRow(ResultSet rs, int rowNum) throws SQLException { + + Foo foo = new Foo(); + foo.id = rs.getInt(1); + foo.name = rs.getString(2); + foo.value = rs.getInt(3); + + return foo; + } + + } + + private class Foo{ + + private int id; + private String name; + private int value; + } +} diff --git a/infrastructure/src/test/java/org/springframework/batch/io/xml/ObjectInputWrapperTests.java b/infrastructure/src/test/java/org/springframework/batch/io/xml/ObjectInputWrapperTests.java new file mode 100644 index 000000000..160376621 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/io/xml/ObjectInputWrapperTests.java @@ -0,0 +1,167 @@ +/* + * 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.xml; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.ObjectInput; + +import javax.xml.stream.Location; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; + +import junit.framework.TestCase; + +import org.easymock.MockControl; +import org.springframework.batch.io.xml.xstream.XStreamFactory.ObjectInputWrapper; +import org.springframework.dao.DataAccessResourceFailureException; + +/** + * Unit tests for {@link ObjectInputWrapper}. + * @author peter.zozom + */ +public class ObjectInputWrapperTests extends TestCase { + + private ObjectInputWrapper wrapper; + + private MockControl readerControl; + + private XMLStreamReader reader; + + private MockControl oiControl; + + private ObjectInput input; + + public void setUp() throws FileNotFoundException, XMLStreamException { + + // create mock reader + readerControl = MockControl.createControl(XMLStreamReader.class); + reader = (XMLStreamReader) readerControl.getMock(); + + // create mock for java.io.ObjectInput + oiControl = MockControl.createControl(ObjectInput.class); + input = (ObjectInput) oiControl.getMock(); + + // create ObjectInputWrapper + wrapper = new ObjectInputWrapper(reader, input); + } + + /** + * Test {@link ObjectInputWrapper#position()}. + */ + public void testPosition() { + + // create mock for Location + MockControl locationControl = MockControl.createControl(Location.class); + Location location = (Location) locationControl.getMock(); + location.getLineNumber(); + locationControl.setReturnValue(104); + locationControl.replay(); + + // set up reader mock + reader.getLocation(); + readerControl.setReturnValue(location); + readerControl.replay(); + + assertEquals(104, wrapper.position()); + + readerControl.verify(); + locationControl.verify(); + } + + /** + * Test {@link ObjectInputWrapper#readObject()} + * @throws ClassNotFoundException + * @throws IOException + */ + public void testReadObject() throws ClassNotFoundException, IOException { + + // set up objectInput mock + input.readObject(); + oiControl.setReturnValue(this); + oiControl.replay(); + + // read object + assertSame(this, wrapper.readObject()); + + oiControl.verify(); + } + + public void testClose() throws XMLStreamException, IOException { + + // TEST CLOSE + + // set up reader mock + reader.close(); + readerControl.replay(); + + // set up objectInput mock + input.close(); + oiControl.replay(); + + wrapper.close(); + + readerControl.verify(); + oiControl.verify(); + + // TEST CLOSE WITH XMLStreamException + + // set up reader mock + readerControl.reset(); + reader.close(); + readerControl.setThrowable(new XMLStreamException()); + readerControl.replay(); + + // set up objectInput mock + oiControl.reset(); + input.close(); + oiControl.replay(); + + try { + wrapper.close(); + fail("BatchCriticalException was expected"); + } + catch (DataAccessResourceFailureException darfe) { + assertTrue(darfe.getCause() instanceof XMLStreamException); + } + + readerControl.verify(); + oiControl.verify(); + + // TEST CLOSE WITH IOException + // set up reader mock + readerControl.reset(); + readerControl.replay(); + + // set up objectInput mock + oiControl.reset(); + input.close(); + oiControl.setThrowable(new IOException()); + oiControl.replay(); + + try { + wrapper.close(); + fail("BatchCriticalException was expected"); + } + catch (DataAccessResourceFailureException darfe) { + assertTrue(darfe.getCause() instanceof IOException); + } + + readerControl.verify(); + oiControl.verify(); + } +} diff --git a/infrastructure/src/test/java/org/springframework/batch/io/xml/ObjectOutputWrapperTests.java b/infrastructure/src/test/java/org/springframework/batch/io/xml/ObjectOutputWrapperTests.java new file mode 100644 index 000000000..68c16026d --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/io/xml/ObjectOutputWrapperTests.java @@ -0,0 +1,398 @@ +/* + * 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.xml; + +import java.io.IOException; +import java.io.ObjectOutput; +import java.nio.ByteBuffer; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.channels.FileLock; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.WritableByteChannel; + +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; + +import junit.framework.TestCase; + +import org.easymock.MockControl; +import org.springframework.batch.io.xml.xstream.XStreamFactory.ObjectOutputWrapper; +import org.springframework.dao.DataAccessResourceFailureException; + +/** + * Unit tests for {@link ObjectOutputWrapper}. + * @author peter.zozom + */ +public class ObjectOutputWrapperTests extends TestCase { + + private ObjectOutputWrapper wrapper; + + private MockControl writerControl; + + private XMLStreamWriter writer; + + private MockControl ooControl; + + private ObjectOutput output; + + private MockFileChannel channel; + + public void setUp() { + + // create mock for xml writer + writerControl = MockControl.createControl(XMLStreamWriter.class); + writer = (XMLStreamWriter) writerControl.getMock(); + + // create mock for java.io.objectOutput + ooControl = MockControl.createControl(ObjectOutput.class); + output = (ObjectOutput) ooControl.getMock(); + + // create mock for file channel + channel = new MockFileChannel(); + + // create wrapper + wrapper = new ObjectOutputWrapper(writer, channel, output); + } + + public void testAfterRestart() throws XMLStreamException, IOException { + + // set up writer mock + writer.writeComment(""); + writerControl.replay(); + + // set up objectOutput mock + output.flush(); + ooControl.replay(); + + // call after restart + wrapper.afterRestart(new Long(99)); + + // check size and position + assertEquals(99, channel.size()); + assertEquals(99, channel.position()); + + // TEST EXCEPTION HANDLING + + // set up writer mock + writerControl.reset(); + writer.writeComment(""); + writerControl.setThrowable(new XMLStreamException()); + writerControl.replay(); + + try { + wrapper.afterRestart(new Long(74)); + fail("BatchCriticalException was expected"); + } + catch (DataAccessResourceFailureException bce) { + assertTrue(bce.getCause() instanceof XMLStreamException); + } + + // set up writer mock + writerControl.reset(); + writer.writeComment(""); + writerControl.replay(); + + // set up objectOutput mock + ooControl.reset(); + output.flush(); + ooControl.setThrowable(new IOException()); + ooControl.replay(); + + try { + wrapper.afterRestart(new Long(63)); + fail("BatchCriticalException was expected"); + } + catch (DataAccessResourceFailureException bce) { + assertTrue(bce.getCause() instanceof IOException); + } + } + + public void testClose() throws IOException, XMLStreamException { + + // set up objectOutput mock + output.close(); + ooControl.replay(); + + // set up writer mock + writer.close(); + writerControl.replay(); + + wrapper.close(); + + // test whether channel, writer and output were closed + assertTrue(channel.isClosed()); + ooControl.verify(); + writerControl.verify(); + + // TEST EXCEPTION HANDLING + + ooControl.reset(); + output.close(); + ooControl.setThrowable(new IOException()); + ooControl.replay(); + + try { + wrapper.close(); + fail("BatchCriticalException was expected"); + } + catch (DataAccessResourceFailureException bce) { + assertTrue(bce.getCause() instanceof IOException); + } + + ooControl.reset(); + output.close(); + ooControl.replay(); + + writerControl.reset(); + writer.close(); + writerControl.setThrowable(new XMLStreamException()); + writerControl.replay(); + + try { + wrapper.close(); + fail("BatchCriticalException was expected"); + } + catch (DataAccessResourceFailureException bce) { + assertTrue(bce.getCause() instanceof XMLStreamException); + } + + } + + /** + * Test flush() method. + * @throws IOException + */ + public void testFlush() throws IOException { + + // set up objectOutput mock (second call of flush() method will throw an + // IOException) + output.flush(); + output.flush(); + ooControl.setThrowable(new IOException()); + ooControl.replay(); + + // call flush() twice + wrapper.flush(); + try { + wrapper.flush(); + fail("BatchCriticalException was expected"); + } + catch (DataAccessResourceFailureException bce) { + assertTrue(bce.getCause() instanceof IOException); + } + + // verify method calls + ooControl.verify(); + } + + /** + * Test position() and position(int) methods. + * @throws IOException + */ + public void testPosition() throws IOException { + + // set up fileChannel mock + channel.position(35); + + // test position() + assertEquals(35, wrapper.position()); + + // test position(int) + wrapper.position(93); + assertEquals(93, channel.position()); + + // set exception + channel.setThrowable(new IOException()); + + // test exception handling + try { + wrapper.position(); + fail("BatchEnviromentException was expected"); + } + catch (DataAccessResourceFailureException bee) { + assertTrue(bee.getCause() instanceof IOException); + } + + try { + wrapper.position(33); + fail("BatchEnviromentException was expected"); + } + catch (DataAccessResourceFailureException bee) { + assertTrue(bee.getCause() instanceof IOException); + } + } + + /** + * Test size() and truncate() methods. + * @throws IOException + */ + public void testSizeAndTruncate() throws IOException { + + // set up fileChannel mock + channel.truncate(53); + + // test size() + assertEquals(53, wrapper.size()); + + // test truncate(int) + wrapper.truncate(39); + assertEquals(39, channel.size()); + + // set exception + channel.setThrowable(new IOException()); + + // test exception handling + try { + wrapper.size(); + fail("BatchEnviromentException was expected"); + } + catch (DataAccessResourceFailureException bee) { + assertTrue(bee.getCause() instanceof IOException); + } + + try { + wrapper.truncate(66); + fail("BatchEnviromentException was expected"); + } + catch (DataAccessResourceFailureException bee) { + assertTrue(bee.getCause() instanceof IOException); + } + + } + + /** + * Test writeObject() method. + * @throws IOException + */ + public void testWriteObject() throws IOException { + //TODO why is "this" used as argument to writeObject? + + // set up objectOutput mock + output.writeObject(this); + ooControl.replay(); + + // write object + wrapper.writeObject(this); + + // verify method calls + ooControl.verify(); + } + + /* + * Mock for FileChannel + */ + private static class MockFileChannel extends FileChannel { + + private long position; + + private long size; + + private boolean closed = false; + + private IOException throwable; + + public void setThrowable(IOException throwable) { + this.throwable = throwable; + } + + public long position() throws IOException { + if (throwable != null) { + throw throwable; + } + return position; + } + + public FileChannel position(long newPosition) throws IOException { + if (throwable != null) { + throw throwable; + } + this.position = newPosition; + return null; + } + + public long size() throws IOException { + if (throwable != null) { + throw throwable; + } + return size; + } + + public FileChannel truncate(long size) throws IOException { + if (throwable != null) { + throw throwable; + } + this.size = size; + return null; + } + + protected void implCloseChannel() throws IOException { + closed = true; + } + + public boolean isClosed() { + return closed; + } + + public void force(boolean metaData) throws IOException { + } + + public FileLock lock(long position, long size, boolean shared) throws IOException { + return null; + } + + public MappedByteBuffer map(MapMode mode, long position, long size) throws IOException { + return null; + } + + public int read(ByteBuffer dst) throws IOException { + return 0; + } + + public int read(ByteBuffer dst, long position) throws IOException { + return 0; + } + + public long read(ByteBuffer[] dsts, int offset, int length) throws IOException { + return 0; + } + + public long transferFrom(ReadableByteChannel src, long position, long count) throws IOException { + return 0; + } + + public long transferTo(long position, long count, WritableByteChannel target) throws IOException { + return 0; + } + + public FileLock tryLock(long position, long size, boolean shared) throws IOException { + return null; + } + + public int write(ByteBuffer src) throws IOException { + return 0; + } + + public int write(ByteBuffer src, long position) throws IOException { + return 0; + } + + public long write(ByteBuffer[] srcs, int offset, int length) throws IOException { + return 0; + } + } +} diff --git a/infrastructure/src/test/java/org/springframework/batch/io/xml/XmlErrorHandlerTests.java b/infrastructure/src/test/java/org/springframework/batch/io/xml/XmlErrorHandlerTests.java new file mode 100644 index 000000000..a8c785bb7 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/io/xml/XmlErrorHandlerTests.java @@ -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.xml; + +import junit.framework.TestCase; + +import org.springframework.batch.io.xml.XmlErrorHandler; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; + +/** + * Unit test for XmlErrorHandler + * @author peter.zozom + */ +public class XmlErrorHandlerTests extends TestCase { + + XmlErrorHandler handler; + + SAXParseException spe; + + public void setUp() { + handler = new XmlErrorHandler(); + spe = new SAXParseException("test", "pid", "sid", 1, 1); + } + + public void testWarning() { + try { + handler.warning(spe); + } + catch (SAXException se) { + assertSame(spe, se.getException()); + } + } + + public void testError() { + try { + handler.error(spe); + } + catch (SAXException se) { + assertSame(spe, se.getException()); + } + } + + public void testFatalError() { + try { + handler.fatalError(spe); + } + catch (SAXException se) { + assertSame(spe, se.getException()); + } + } + +} diff --git a/infrastructure/src/test/java/org/springframework/batch/io/xml/XmlInputSource2Tests.java b/infrastructure/src/test/java/org/springframework/batch/io/xml/XmlInputSource2Tests.java new file mode 100644 index 000000000..cbcffafa8 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/io/xml/XmlInputSource2Tests.java @@ -0,0 +1,124 @@ +package org.springframework.batch.io.xml; + +import java.io.IOException; + +import javax.xml.transform.Source; +import javax.xml.transform.sax.SAXSource; + +import junit.framework.TestCase; + +import org.easymock.MockControl; +import org.springframework.batch.restart.RestartData; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.oxm.Unmarshaller; +import org.springframework.oxm.XmlMappingException; +import org.springframework.transaction.support.TransactionSynchronization; + +/** + * Unit tests for {@link XmlInputSource2} + * + * @author Robert Kasanicky + */ +public class XmlInputSource2Tests extends TestCase { + + private XmlInputSource2 inputSource = new XmlInputSource2(); + + + private Resource getInputResource() throws IOException { + return new FileSystemResource("src/test/resources/org/springframework/batch/io/xml/test1.xml"); + } + + //@Override + protected void setUp() throws Exception { + inputSource.setRecordElementName("book"); + inputSource.setResource(getInputResource()); + inputSource.setUnmarshaller(new UnmarshallerStub()); + inputSource.setUseSaxParser(true); + } + + + + /** + * Regular usage scenario. + * The actual xml-to-object mapping is delegated to the injected unmarshaller. + */ + public void testRead() throws XmlMappingException, IOException { + MockControl umControl = MockControl.createControl(Unmarshaller.class); + Unmarshaller unmarshaller = (Unmarshaller) umControl.getMock(); + Object expectedDomainObject = new Object(); + unmarshaller.unmarshal(null); + umControl.setDefaultMatcher(MockControl.ALWAYS_MATCHER); + umControl.setDefaultReturnValue(expectedDomainObject); + umControl.replay(); + + inputSource.setUnmarshaller(unmarshaller); + + //there are two records in the input file + assertSame(expectedDomainObject, inputSource.read()); + assertSame(expectedDomainObject, inputSource.read()); + assertNull(inputSource.read()); + } + + public void testReadUntilEnd() { + + } + + /** + * In case of rollback uncommited records are read again. + */ + public void testRollback() { + Object uncommited = inputSource.read(); + inputSource.getSynchronization().afterCompletion(TransactionSynchronization.STATUS_ROLLED_BACK); + Object afterRollback = inputSource.read(); + + assertEquals(uncommited, afterRollback); + } + + /** + * Records once marked to be skipped are not returned when read again. + */ + public void testSkip() { + Object first = inputSource.read(); + inputSource.skip(); + inputSource.getSynchronization().afterCompletion(TransactionSynchronization.STATUS_ROLLED_BACK); + + Object second = inputSource.read(); + assertFalse(second.equals(first)); + } + + /** + * In case of restart the input source should continue from the position when restart data was saved. + */ + public void testRestart() { + inputSource.read(); + RestartData commitPoint = inputSource.getRestartData(); + Object firstAfterCommit = inputSource.read(); + inputSource.restoreFrom(commitPoint); + assertEquals(firstAfterCommit, inputSource.read()); + + } + + /** + * Returns a fixed-length prefix of the original xml string instead of mapped object. + * + * @author Robert Kasanicky + */ + private static class UnmarshallerStub implements Unmarshaller { + + private static final int PREFIX_LENGTH = 10000; + + public boolean supports(Class clazz) { + return true; + } + + public Object unmarshal(Source source) throws XmlMappingException, IOException { + char[] input = new char[PREFIX_LENGTH]; + SAXSource saxSource = (SAXSource) source; + saxSource.getInputSource().getCharacterStream().read(input); + + return String.valueOf(input); + } + + } +} diff --git a/infrastructure/src/test/java/org/springframework/batch/io/xml/XmlInputSourceIntegrationTests.java b/infrastructure/src/test/java/org/springframework/batch/io/xml/XmlInputSourceIntegrationTests.java new file mode 100644 index 000000000..e0d17d6e8 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/io/xml/XmlInputSourceIntegrationTests.java @@ -0,0 +1,428 @@ +/* + * 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.xml; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.List; + +import junit.framework.TestCase; + +import org.springframework.batch.io.exception.BatchEnvironmentException; +import org.springframework.batch.io.sample.domain.LineItem; +import org.springframework.batch.io.sample.domain.Order; +import org.springframework.batch.io.sample.domain.Shipper; +import org.springframework.batch.io.xml.xstream.FieldAlias; +import org.springframework.batch.io.xml.xstream.Mapping; +import org.springframework.batch.io.xml.xstream.XStreamConfiguration; +import org.springframework.batch.io.xml.xstream.XStreamFactory; +import org.springframework.batch.restart.RestartData; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.util.ClassUtils; + +/** + * Integration test for XmlInputTemplate. It tests reading, xml validation, skip + * and restart functionality. + * @author peter.zozom + */ +public class XmlInputSourceIntegrationTests extends TestCase { + + private final static String INPUT_NAME = "xmlInputTemplate"; + + private XmlInputSource xmlInput; + + private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z"); + + /** + * Set up XmlInputTemplate: create mock for FileLocator and create + * XStreamConfiguration object. + * @throws Exception + */ + public void setUp() throws Exception { + + // create mock for file locator + Resource resource = new ClassPathResource(ClassUtils.addResourcePathToPackagePath(getClass(), + "20070125.testStream.xmlFileStep.xml")); + + // Set up XStreamCfg: + XStreamConfiguration streamConfiguration = new XStreamConfiguration(); + + // Step 1: set field aliases + List aliases = new ArrayList(); + + FieldAlias alias = new FieldAlias(); + alias.setAliasName("org.springframework.batch.io.sample.domain.Customer"); + alias.setType("org.springframework.batch.io.sample.domain.Order"); + alias.setFieldName("customer"); + aliases.add(alias); + + alias = new FieldAlias(); + alias.setAliasName("org.springframework.batch.io.sample.domain.Shipper"); + alias.setType("org.springframework.batch.io.sample.domain.Order"); + alias.setFieldName("shipper"); + aliases.add(alias); + + streamConfiguration.setFieldAliases(aliases); + + // Step 2: set mappings + List mappings = new ArrayList(); + + Mapping mapping = new Mapping(); + mapping.setClassName("org.springframework.batch.io.sample.domain.Order"); + mapping.setNamespaceURI("http://adsj.accenture.com/purchaseorders"); + mapping.setLocalPart("order"); + mapping.setPrefix(""); + mappings.add(mapping); + + mapping = new Mapping(); + mapping.setClassName("org.springframework.batch.io.sample.domain.Customer"); + mapping.setNamespaceURI("http://adsj.accenture.com/purchaseorders"); + mapping.setLocalPart("customer"); + mapping.setPrefix(""); + mappings.add(mapping); + + mapping = new Mapping(); + mapping.setClassName("org.springframework.batch.io.sample.domain.LineItem"); + mapping.setNamespaceURI("http://adsj.accenture.com/purchaseorders"); + mapping.setLocalPart("lineItem"); + mapping.setPrefix(""); + mappings.add(mapping); + + mapping = new Mapping(); + mapping.setClassName("org.springframework.batch.io.sample.domain.Shipper"); + mapping.setNamespaceURI("http://adsj.accenture.com/purchaseorders"); + mapping.setLocalPart("shipper"); + mapping.setPrefix(""); + mappings.add(mapping); + + streamConfiguration.setMappings(mappings); + + // Set up input template + xmlInput = new XmlInputSource() { + public void registerSynchronization() { + } + }; + + xmlInput.setResource(resource); + xmlInput.setEncoding("UTF-8"); + xmlInput.setName(INPUT_NAME); + XStreamFactory factory = new XStreamFactory(); + factory.setConfig(streamConfiguration); + xmlInput.setInputFactory(factory); + } + + public void tearDown() { + } + + /** + * Test read functionality. + * @throws ParseException + */ + public void testRead() throws ParseException { + + xmlInput.setValidating(false); + xmlInput.open(); + + // READ FIRST RECORD + Object result = xmlInput.read(); + + // is it Order? + assertTrue(result instanceof Order); + Order order = (Order) result; + // verify customer + assertNotNull(order.getCustomer()); + assertEquals("Gladys Kravitz", order.getCustomer().getName()); + assertEquals("Anytown, PA", order.getCustomer().getAddress()); + assertEquals(34, order.getCustomer().getAge()); + assertEquals(0, order.getCustomer().getMoo()); + assertEquals(0, order.getCustomer().getPoo()); + // verify date + assertEquals(sdf.parse("2003-01-07 14:16:00 GMT"), order.getDate()); + // verify line items + List items = order.getLineItems(); + assertEquals(2, items.size()); + LineItem item = (LineItem) items.get(0); + assertEquals("Burnham's Celestial Handbook, Vol 1", item.getDescription()); + assertEquals(5.0, item.getPerUnitOunces(), 0.0); + assertEquals(21.79, item.getPrice(), 0.0); + assertEquals(2, item.getQuantity()); + item = (LineItem) items.get(1); + assertEquals("Burnham's Celestial Handbook, Vol 2", item.getDescription()); + assertEquals(5.0, item.getPerUnitOunces(), 0.0); + assertEquals(19.89, item.getPrice(), 0.0); + assertEquals(2, item.getQuantity()); + // verify shipper + Shipper shipper = order.getShipper(); + assertEquals("ZipShip", shipper.getName()); + assertEquals(0.74, shipper.getPerOunceRate(), 0.0); + + // READ SECOND RECORD + result = xmlInput.read(); + + // is it Order? + assertTrue(result instanceof Order); + order = (Order) result; + // verify customer + assertNotNull(order.getCustomer()); + assertEquals("John Smith", order.getCustomer().getName()); + assertEquals("Chicago, IL", order.getCustomer().getAddress()); + assertEquals(46, order.getCustomer().getAge()); + assertEquals(0, order.getCustomer().getMoo()); + assertEquals(0, order.getCustomer().getPoo()); + // verify date + assertEquals(sdf.parse("2003-01-07 14:16:02 GMT"), order.getDate()); + // verify line items + items = order.getLineItems(); + assertEquals(3, items.size()); + item = (LineItem) items.get(0); + assertEquals("XmlBeans in Action", item.getDescription()); + assertEquals(3.0, item.getPerUnitOunces(), 0.0); + assertEquals(41.29, item.getPrice(), 0.0); + assertEquals(1, item.getQuantity()); + item = (LineItem) items.get(1); + assertEquals("JSR-173", item.getDescription()); + assertEquals(1.0, item.getPerUnitOunces(), 0.0); + assertEquals(11.99, item.getPrice(), 0.0); + assertEquals(5, item.getQuantity()); + item = (LineItem) items.get(2); + assertEquals("Teach Yourself XML in 21 days", item.getDescription()); + assertEquals(1.0, item.getPerUnitOunces(), 0.0); + assertEquals(35.49, item.getPrice(), 0.0); + assertEquals(1, item.getQuantity()); + // verify shipper + shipper = order.getShipper(); + assertEquals("ZipShip", shipper.getName()); + assertEquals(0.74, shipper.getPerOunceRate(), 0.0); + + // READ LAST RECORD + result = xmlInput.read(); + + // is it Order? + assertTrue(result instanceof Order); + order = (Order) result; + // verify customer + assertNotNull(order.getCustomer()); + assertEquals("Peter Newman", order.getCustomer().getName()); + assertEquals("Cleveland, OH", order.getCustomer().getAddress()); + assertEquals(23, order.getCustomer().getAge()); + assertEquals(0, order.getCustomer().getMoo()); + assertEquals(0, order.getCustomer().getPoo()); + // verify date + assertEquals(sdf.parse("2003-01-07 14:16:35 GMT"), order.getDate()); + // verify line items + items = order.getLineItems(); + assertEquals(1, items.size()); + item = (LineItem) items.get(0); + assertEquals("Java 6", item.getDescription()); + assertEquals(2.0, item.getPerUnitOunces(), 0.0); + assertEquals(12.79, item.getPrice(), 0.0); + assertEquals(3, item.getQuantity()); + // verify shipper + shipper = order.getShipper(); + assertEquals("UPS", shipper.getName()); + assertEquals(0.69, shipper.getPerOunceRate(), 0.0); + + // all records were processed already + assertNull(xmlInput.read()); + + // verify statistics TODO + // Map statistics = xmlInput.getStatistics(); + // assertEquals("4", + // statistics.get(XmlInputTemplate.READ_STATISTICS_NAME)); + + xmlInput.close(); + } + + /** + * Test XML validation + */ + public void testValidation() { + + // turn on xml validation + xmlInput.setValidating(true); + // TEST 1: parse valid xml + xmlInput.open(); + xmlInput.close(); + + } + + public void testInvalidXml() throws Exception { + + xmlInput.setValidating(true); + + // TEST 2: parse invalid xml + xmlInput.setResource(new ByteArrayResource("".getBytes())); + + try { + xmlInput.open(); + fail("Parsing invalid xml file. Exception should be thrown."); + } + catch (BatchEnvironmentException bee) { + assertTrue(true); + } + + } + + /** + * Test skip functioanlity. + * @throws ParseException + */ + public void testSkip() throws ParseException { + + xmlInput.setValidating(false); + xmlInput.open(); + + // read first record + xmlInput.read(); + // mark it as skipped + xmlInput.skip(); + // read second record + xmlInput.read(); + // read third record + xmlInput.read(); + // mark it as skipped and rollback + xmlInput.skip(); + xmlInput.afterCompletion(TransactionSynchronization.STATUS_ROLLED_BACK); + + // read second record again (first was skipped) + Object result = xmlInput.read(); + + // is it Order? + assertTrue(result instanceof Order); + Order order = (Order) result; + // verify customer + assertNotNull(order.getCustomer()); + assertEquals("John Smith", order.getCustomer().getName()); + assertEquals("Chicago, IL", order.getCustomer().getAddress()); + assertEquals(46, order.getCustomer().getAge()); + assertEquals(0, order.getCustomer().getMoo()); + assertEquals(0, order.getCustomer().getPoo()); + // verify date + assertEquals(sdf.parse("2003-01-07 14:16:02 GMT"), order.getDate()); + // verify line items + List items = order.getLineItems(); + assertEquals(3, items.size()); + LineItem item = (LineItem) items.get(0); + assertEquals("XmlBeans in Action", item.getDescription()); + assertEquals(3.0, item.getPerUnitOunces(), 0.0); + assertEquals(41.29, item.getPrice(), 0.0); + assertEquals(1, item.getQuantity()); + item = (LineItem) items.get(1); + assertEquals("JSR-173", item.getDescription()); + assertEquals(1.0, item.getPerUnitOunces(), 0.0); + assertEquals(11.99, item.getPrice(), 0.0); + assertEquals(5, item.getQuantity()); + item = (LineItem) items.get(2); + assertEquals("Teach Yourself XML in 21 days", item.getDescription()); + assertEquals(1.0, item.getPerUnitOunces(), 0.0); + assertEquals(35.49, item.getPrice(), 0.0); + assertEquals(1, item.getQuantity()); + // verify shipper + Shipper shipper = order.getShipper(); + assertEquals("ZipShip", shipper.getName()); + assertEquals(0.74, shipper.getPerOunceRate(), 0.0); + + // No records left, third record should be skipped + assertNull(xmlInput.read()); + + // verify statistics TODO + // Map statistics = xmlInput.getStatistics(); + // assertEquals("4", + // statistics.get(XmlInputTemplate.READ_STATISTICS_NAME)); + } + + /** + * Test restart functionality. + * @throws ParseException + */ + public void testRestart() throws ParseException { + + xmlInput.open(); + + // read first record and commit it + xmlInput.read(); + xmlInput.afterCompletion(TransactionSynchronization.STATUS_COMMITTED); + // read second record and commit it + xmlInput.read(); + xmlInput.afterCompletion(TransactionSynchronization.STATUS_COMMITTED); + // read third record + xmlInput.read(); + xmlInput.afterCompletion(TransactionSynchronization.STATUS_ROLLED_BACK); + RestartData restartData = xmlInput.getRestartData(); + + xmlInput.close(); + + xmlInput.open(); + xmlInput.restoreFrom(restartData); + Object result = xmlInput.read(); + + // is it Order? + assertTrue(result instanceof Order); + Order order = (Order) result; + // verify customer + assertNotNull(order.getCustomer()); + assertEquals("Peter Newman", order.getCustomer().getName()); + assertEquals("Cleveland, OH", order.getCustomer().getAddress()); + assertEquals(23, order.getCustomer().getAge()); + assertEquals(0, order.getCustomer().getMoo()); + assertEquals(0, order.getCustomer().getPoo()); + // verify date + assertEquals(sdf.parse("2003-01-07 14:16:35 GMT"), order.getDate()); + // verify line items + List items = order.getLineItems(); + assertEquals(1, items.size()); + LineItem item = (LineItem) items.get(0); + assertEquals("Java 6", item.getDescription()); + assertEquals(2.0, item.getPerUnitOunces(), 0.0); + assertEquals(12.79, item.getPrice(), 0.0); + assertEquals(3, item.getQuantity()); + // verify shipper + Shipper shipper = order.getShipper(); + assertEquals("UPS", shipper.getName()); + assertEquals(0.69, shipper.getPerOunceRate(), 0.0); + + // all records were processed already + assertNull(xmlInput.read()); + + // verify statistics TODO + // Map statistics = xmlInput.getStatistics(); + // assertEquals("4", + // statistics.get(XmlInputTemplate.READ_STATISTICS_NAME)); + } + + /** + * Tests null resource + * @throws Exception + */ + public void testGetFileLocatorStrategyWithNullParam() throws Exception { + + // set file locator strategy to null + xmlInput.setResource(null); + try { + xmlInput.afterPropertiesSet(); + fail("Expected IllegalArgumentException"); + } + catch (IllegalArgumentException e) { + // expected + } + } + +} diff --git a/infrastructure/src/test/java/org/springframework/batch/io/xml/XmlInputSourceTests.java b/infrastructure/src/test/java/org/springframework/batch/io/xml/XmlInputSourceTests.java new file mode 100644 index 000000000..9b909787a --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/io/xml/XmlInputSourceTests.java @@ -0,0 +1,284 @@ +/* + * 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.xml; + +import java.io.File; +import java.io.IOException; + +import javax.xml.parsers.FactoryConfigurationError; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; + +import junit.framework.TestCase; + +import org.easymock.MockControl; +import org.springframework.batch.io.exception.BatchCriticalException; +import org.springframework.batch.io.exception.BatchEnvironmentException; +import org.springframework.batch.io.xml.ObjectInput; +import org.springframework.batch.io.xml.ObjectInputFactory; +import org.springframework.batch.io.xml.XmlInputSource; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.util.ClassUtils; +import org.xml.sax.Parser; +import org.xml.sax.SAXException; +import org.xml.sax.SAXNotRecognizedException; +import org.xml.sax.SAXNotSupportedException; +import org.xml.sax.XMLReader; +import org.xml.sax.helpers.DefaultHandler; + +/** + * Additional unit tests, which test feuatores not tested with + * XmlInputTemplateIntegrationTest + * @author peter.zozom + */ +public class XmlInputSourceTests extends TestCase { + + private MockControl oifControl; + + private MockControl oiControl; + + private ObjectInputFactory objectInputFactory; + + private ObjectInput objectInput; + + private XmlInputSource input; + + /** + * Set up XmlInputTemplate: create mock for FileLocator, + * ObjectInputFactory and ObjectInput + */ + public void setUp() { + + Resource resource = new ClassPathResource(ClassUtils.addResourcePathToPackagePath(getClass(), "20070125.testStream.xmlFileStep.xml")); + + // create mock for ObjectInput + oiControl = MockControl.createControl(ObjectInput.class); + objectInput = (ObjectInput) oiControl.getMock(); + + // create mock for ObjectInputFactory + oifControl = MockControl.createControl(ObjectInputFactory.class); + objectInputFactory = (ObjectInputFactory) oifControl.getMock(); + objectInputFactory.createObjectInput(resource, "UTF-8"); + oifControl.setReturnValue(objectInput, 1); + oifControl.replay(); + + // create input template + input = new XmlInputSource() { + protected void registerSynchronization() { + } + + protected SAXParserFactory getSaxFactory() throws FactoryConfigurationError { + return new MockSAXFactory(); + } + }; + + // set up input template + input.setValidating(false); + input.setName("test_name"); + input.setInputFactory(objectInputFactory); + + input.setResource(resource); + } + + /** + * Test init called twice (2nd call should do nothing) + */ + public void testDoubleInit() { + + // set up objectInput mock + objectInput.position(); + oiControl.setReturnValue(3, 1); + oiControl.replay(); + + // call init + input.open(); + + // call init again - nothing should happen + input.open(); + + // verify method calls for each mock object + oifControl.verify(); + oiControl.verify(); + } + + /** + * Test exception handling in validateInputFile() method + */ + public void testExceptionsInValidationMethod() { + + oifControl.reset(); + oifControl.replay(); + // set up objectInput mock + oiControl.replay(); + input.setValidating(true); + + try { + // call init again - nothing should happen + input.open(); + fail("ParserConfigurationException was expected"); + } + catch (BatchEnvironmentException bee) { + // ParserConfigurationException is expected + assertTrue(bee.getCause() instanceof ParserConfigurationException); + } + + FileSystemResource resource = new FileSystemResource("FooDummy.xml"); + assertTrue(!resource.exists()); + input.setResource(resource); + + try { + // call init again - nothing should happen + input.open(); + fail("BatchCriticalException was expected"); + } + catch (BatchCriticalException bee) { + // IOException is expected + assertTrue(bee.getCause() instanceof IOException); + } + + // verify method calls for each mock object + oifControl.verify(); + oiControl.verify(); + + } + + /** + * Test exception handling in read() method + * @throws ClassNotFoundException + * @throws IOException + */ + public void testExceptionsInReadMethod() throws ClassNotFoundException, IOException { + + // set up objectInput mock + objectInput.position(); + oiControl.setReturnValue(3, 1); + objectInput.readObject(); + oiControl.setThrowable(new IOException()); + objectInput.readObject(); + oiControl.setThrowable(new ClassNotFoundException()); + oiControl.replay(); + + // call init + input.open(); + + try { + input.read(); + fail("BatchCriticalException caused by IOException was expected"); + } + catch (BatchCriticalException bce) { + assertTrue(bce.getCause() instanceof IOException); + } + + try { + input.read(); + fail("BatchCriticalException caused by ClassNotFoundException was expected"); + } + catch (BatchCriticalException bee) { + assertTrue(bee.getCause() instanceof ClassNotFoundException); + } + + // verify method calls for each mock object + oifControl.verify(); + oiControl.verify(); + } + + /** + * Test afterCompletition() method with transaction status "UNKNOWN" + */ + public void testTransactionUnknownStatus() { + + // set up ObjectInput mock + objectInput.position(); + oiControl.setReturnValue(3, 1); + oiControl.replay(); + + input.open(); + + // call afterCompletition method with unknown status - nothing should + // happen + input.afterCompletion(TransactionSynchronization.STATUS_UNKNOWN); + + // verify method calls for each mock object + oifControl.verify(); + oiControl.verify(); + } + + /* + * Mock for SAXParserFactory, which either throws + * ParserConfigurationException or returns MockSAXParser + */ + private static class MockSAXFactory extends SAXParserFactory { + + private static int counter = 0; + + public boolean getFeature(String name) throws ParserConfigurationException, SAXNotRecognizedException, + SAXNotSupportedException { + return false; + } + + public SAXParser newSAXParser() throws ParserConfigurationException, SAXException { + counter++; + if (counter % 2 != 0) { + throw new ParserConfigurationException(); + } + return new MockSAXParser(); + } + + public void setFeature(String name, boolean value) throws ParserConfigurationException, + SAXNotRecognizedException, SAXNotSupportedException { + } + } + + /* + * Mock for SAXParser, which throws IOException in parse(java.io.File, + * org.xml.sax.helpers.DefaultHandler) method + */ + private static class MockSAXParser extends SAXParser { + + public void parse(File f, DefaultHandler dh) throws SAXException, IOException { + throw new IOException(); + } + + public Parser getParser() throws SAXException { + return null; + } + + public Object getProperty(String name) throws SAXNotRecognizedException, SAXNotSupportedException { + return null; + } + + public XMLReader getXMLReader() throws SAXException { + return null; + } + + public boolean isNamespaceAware() { + return false; + } + + public boolean isValidating() { + return false; + } + + public void setProperty(String name, Object value) throws SAXNotRecognizedException, SAXNotSupportedException { + } + + } +} diff --git a/infrastructure/src/test/java/org/springframework/batch/io/xml/XmlOutputSourceTests.java b/infrastructure/src/test/java/org/springframework/batch/io/xml/XmlOutputSourceTests.java new file mode 100644 index 000000000..94435b222 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/io/xml/XmlOutputSourceTests.java @@ -0,0 +1,274 @@ +/* + * 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.xml; + +import java.io.File; +import java.io.IOException; +import java.security.AccessController; +import java.util.Random; + +import junit.framework.TestCase; + +import org.easymock.MockControl; +import org.easymock.internal.Range; +import org.springframework.batch.io.xml.ObjectOutput; +import org.springframework.batch.io.xml.ObjectOutputFactory; +import org.springframework.batch.io.xml.XmlOutputSource; +import org.springframework.batch.restart.RestartData; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.transaction.support.TransactionSynchronization; + +import sun.security.action.GetPropertyAction; + +/** + * Unit tests for XmlOutputTemplate + * @author peter.zozom + * + */ +public class XmlOutputSourceTests extends TestCase { + + private final static String OUTPUT_NAME = "xmlOutputTemplate"; + + private XmlOutputSource xmlOutput; + + private MockControl ooControl; + + private ObjectOutput objectOutput; + + private MockControl oofControl; + + private ObjectOutputFactory objectOutputFactory; + + private String getTempDir() { + GetPropertyAction a = new GetPropertyAction("java.io.tmpdir"); + return ((String) AccessController.doPrivileged(a)); + } + + /** + * Set up XmlOutputTemplate: create mock for FileLocator, + * ObjectOutputFactory and ObjectOutput. + */ + public void setUp() throws Exception { + + // create File object + Resource file = new FileSystemResource(new File(getTempDir() + "test" + Integer.toString(new Random().nextInt() & 0xffff) + ".tmp")); + + // Create mock for ObjectOutput + ooControl = MockControl.createControl(ObjectOutput.class); + objectOutput = (ObjectOutput) ooControl.getMock(); + + // Create mock for ObjectOutputFactory, which will return mock + // ObjectOutput + oofControl = MockControl.createControl(ObjectOutputFactory.class); + objectOutputFactory = (ObjectOutputFactory) oofControl.getMock(); + objectOutputFactory.createObjectOutput(file, "UTF-8"); + oofControl.setReturnValue(objectOutput, new Range(1,2)); + oofControl.replay(); + + // Create output template + xmlOutput = new XmlOutputSource() { + protected void registerSynchronization() { + } + }; + + // Set up output template + xmlOutput.setResource(file); + xmlOutput.setEncoding("UTF-8"); + xmlOutput.setName(OUTPUT_NAME); + xmlOutput.setOutputFactory(objectOutputFactory); + } + + /** + * Tests write and close method. Also tests statistics. + * @throws IOException + */ + public void testWrite() throws IOException { + + // initialize xmlOutput + xmlOutput.open(); + + // set up ObjectOutput mock + objectOutput.writeObject(this); + objectOutput.writeObject(this); + objectOutput.close(); + ooControl.replay(); + + // call write method + xmlOutput.write(this); + + // verify statistics TODO +// Map statistics = xmlOutput.getStatistics(); +// assertEquals("1", statistics.get(XmlOutputTemplate.WRITTEN_STATISTICS_NAME)); + + // call write method again + xmlOutput.write(this); + + // call close method + xmlOutput.close(); + + // verify statistics again: count of written objects should be changed TODO +// statistics = xmlOutput.getStatistics(); +// assertEquals("2", statistics.get(XmlOutputTemplate.WRITTEN_STATISTICS_NAME)); + + // verify method calls for each mock + oofControl.verify(); + ooControl.verify(); + + } + + /** + * Tests handling IOException raised within write() method + * @throws IOException + */ + public void testWriteWithIOException() throws IOException { + + // initialize xmlOutput + xmlOutput.open(); + + // set up ObjectOutput mock + objectOutput.writeObject(this); + IOException ioe = new IOException("test"); + ooControl.setThrowable(ioe); + objectOutput.close(); + ooControl.replay(); + + try { + xmlOutput.write(this); + fail("BatchCriticalException was expected"); + } + catch (DataAccessResourceFailureException bce) { + // exceptiow was expected: caused by ioe + assertSame(ioe, bce.getCause()); + } + + } + + /** + * Tests commit and rollback functionality. + * @throws IOException + */ + public void testCommitAndRollback() throws IOException { + + // Set up ObjectOutput mock: + objectOutput.writeObject(null); + + // STEP1: commit: flush output and remember commit position + objectOutput.flush(); + objectOutput.position(); + ooControl.setReturnValue(102); + // STEP2: rollback: check size, truncate output and set new position + objectOutput.size(); + ooControl.setReturnValue(500); // size(=500) > newSize(=102) + objectOutput.position(102); + objectOutput.truncate(102); + // STEP3: rollback with bad output size + objectOutput.size(); + ooControl.setReturnValue(10); // size(=10) < newSize(=102) + ooControl.replay(); + + // initialize xmlOutput + xmlOutput.open(); + xmlOutput.write(null); // because output writer is initialized in write method. + + // test commit and rollback + // STEP1: commit + xmlOutput.afterCompletion(TransactionSynchronization.STATUS_COMMITTED); + // STEP2: rollback + xmlOutput.afterCompletion(TransactionSynchronization.STATUS_ROLLED_BACK); + // STEP3: rollback with bad output size + try { + xmlOutput.afterCompletion(TransactionSynchronization.STATUS_ROLLED_BACK); + fail("Exception was expected because of bad output size"); + } + catch (IllegalStateException bce) { + // exception is expected + assertTrue(true); + } + + // STEP4: call afterCompletition with status "UNKNOWN" - nothing should + // happen + xmlOutput.afterCompletion(TransactionSynchronization.STATUS_UNKNOWN); + + // verify method calls for each mock + oofControl.verify(); + ooControl.verify(); + } + + /** + * Tests restart functionality. + * @throws IOException + */ + public void testRestart() throws IOException { + + // Set up ObjectOutput mock: + objectOutput.writeObject(null); + // - set position (=restartData) + objectOutput.position(); + ooControl.setReturnValue(23001); + objectOutput.close(); + + objectOutput.writeObject(null); + + // - after restart should be called with restart data + objectOutput.afterRestart(new Long(23001)); + // - and finaly set size (it should be verified: size > + // newSize(=restartData)) + objectOutput.size(); + ooControl.setReturnValue(54301); + ooControl.replay(); + + // initialize xmlOutput + xmlOutput.open(); + + xmlOutput.write(null); // because output writer is initialized in write method. + + // get restart data + RestartData restartData = xmlOutput.getRestartData(); + assertEquals("23001", restartData.getProperties().getProperty(XmlOutputSource.RESTART_DATA_NAME)); + xmlOutput.close(); + + // init for restart + xmlOutput.open(); + + xmlOutput.restoreFrom(restartData); + + xmlOutput.write(null); // because output writer is initialized in write method. + + // verify method calls for each mock + oofControl.verify(); + ooControl.verify(); + } + + /** + * Tests getFileLocatorStrategy() with fileLocatorStrategy = null + */ + public void testGetFileLocatorStrategyWithNullParam() throws Exception { + + // set file locator strategy to null + xmlOutput.setResource(null); + try { + xmlOutput.afterPropertiesSet(); + fail("Expected IllegalArgumentException"); + } + catch (IllegalArgumentException e) { + // expected + } + } + +} diff --git a/infrastructure/src/test/java/org/springframework/batch/io/xml/xstream/XStreamConfigurationFactoryBeanIntegrationTests.java b/infrastructure/src/test/java/org/springframework/batch/io/xml/xstream/XStreamConfigurationFactoryBeanIntegrationTests.java new file mode 100644 index 000000000..d8a6e3006 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/io/xml/xstream/XStreamConfigurationFactoryBeanIntegrationTests.java @@ -0,0 +1,234 @@ +/* + * 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.xml.xstream; + +import java.util.List; +import java.util.Map; + +import junit.framework.TestCase; + +import org.springframework.batch.io.exception.BatchEnvironmentException; +import org.springframework.core.io.ClassPathResource; + +/** + * Integration tests for XStreamConfigurationFactory. + * @author peter.zozom + * @author Dave Syer + */ +public class XStreamConfigurationFactoryBeanIntegrationTests extends TestCase { + + XStreamConfigurationFactoryBean factory; + + public void setUp() { + factory = new XStreamConfigurationFactoryBean(); + } + + /** + * Test getXStreamCofiguration() method. + * @throws Exception + */ + public void testGetXStreamConfiguration() throws Exception { + + // set config file + factory.setConfigFile(new ClassPathResource("xstream-config-test.xml", getClass())); + // get XStreamConfiguration + factory.afterPropertiesSet(); + XStreamConfiguration config = (XStreamConfiguration) factory.getObject(); + + // test mode + assertEquals(1003, config.getMode()); + + // test root element name + assertEquals("root_test", config.getRootElementName()); + + // test root element attributes + Map rea = config.getRootElementAttributes(); + assertNotNull(rea); + assertEquals(2, rea.size()); + assertEquals("root-elementAttr_value1", rea.get("root-elementAttr_key1")); + assertEquals("root-elementAttr_value2", rea.get("root-elementAttr_key2")); + + // test class aliases + List aliases = config.getClassAliases(); + assertNotNull(aliases); + assertEquals(2, aliases.size()); + + ClassAlias classAlias = (ClassAlias) aliases.get(0); + assertEquals("class-alias_name1", classAlias.getName()); + assertEquals("class-alias_type1", classAlias.getType()); + assertEquals("class-alias_di1", classAlias.getDefaultImplementation()); + + classAlias = (ClassAlias) aliases.get(1); + assertEquals("class-alias_name2", classAlias.getName()); + assertEquals("class-alias_type2", classAlias.getType()); + assertEquals("class-alias_di2", classAlias.getDefaultImplementation()); + + // test type aliases + aliases = config.getTypeAliases(); + assertNotNull(aliases); + assertEquals(2, aliases.size()); + + TypeAlias typeAlias = (TypeAlias) aliases.get(0); + assertEquals("type-alias_name1", typeAlias.getName()); + assertEquals("type-alias_type1", typeAlias.getType()); + + typeAlias = (TypeAlias) aliases.get(1); + assertEquals("type-alias_name2", typeAlias.getName()); + assertEquals("type-alias_type2", typeAlias.getType()); + + // test field aliases + aliases = config.getFieldAliases(); + assertNotNull(aliases); + assertEquals(2, aliases.size()); + + FieldAlias fieldAlias = (FieldAlias) aliases.get(0); + assertEquals("field-alias_name1", fieldAlias.getAliasName()); + assertEquals("field-alias_type1", fieldAlias.getType()); + assertEquals("field1", fieldAlias.getFieldName()); + + fieldAlias = (FieldAlias) aliases.get(1); + assertEquals("field-alias_name2", fieldAlias.getAliasName()); + assertEquals("field-alias_type2", fieldAlias.getType()); + assertEquals("field2", fieldAlias.getFieldName()); + + // test attribute alias + aliases = config.getAttributeAliases(); + assertNotNull(aliases); + assertEquals(2, aliases.size()); + + AttributeAlias attributeAlias = (AttributeAlias) aliases.get(0); + assertEquals("attribute-alias_name1", attributeAlias.getAttributeName()); + assertEquals("attribute-alias_alias1", attributeAlias.getAlias()); + + attributeAlias = (AttributeAlias) aliases.get(1); + assertEquals("attribute-alias_name2", attributeAlias.getAttributeName()); + assertEquals("attribute-alias_alias2", attributeAlias.getAlias()); + + // test attribute properties + List properties = config.getAttributes(); + assertNotNull(properties); + assertEquals(2, properties.size()); + + AttributeProperties attributeProperties = (AttributeProperties) properties.get(0); + assertEquals("attribute-properties_type1", attributeProperties.getType()); + assertEquals("attribute-properties_field1", attributeProperties.getFieldName()); + + attributeProperties = (AttributeProperties) properties.get(1); + assertEquals("attribute-properties_type2", attributeProperties.getType()); + assertEquals("attribute-properties_field2", attributeProperties.getFieldName()); + + // test converters + properties = config.getConverters(); + assertNotNull(properties); + assertEquals(2, properties.size()); + + ConverterProperties converterProperties = (ConverterProperties) properties.get(0); + assertEquals("converter.class-name1", converterProperties.getClassName()); + assertEquals(-50, converterProperties.getPriority()); + + converterProperties = (ConverterProperties) properties.get(1); + assertEquals("converter.class-name2", converterProperties.getClassName()); + assertEquals(750, converterProperties.getPriority()); + + // test implicit collections + List collections = config.getImplicitCollections(); + assertNotNull(collections); + assertEquals(2, collections.size()); + + ImplicitCollection implicitCollection = (ImplicitCollection) collections.get(0); + assertEquals("ic_owner-type1", implicitCollection.getOwnerType()); + assertEquals("ic_field-name1", implicitCollection.getFieldName()); + assertEquals("ic_itemField-name1", implicitCollection.getItemFieldName()); + assertEquals("ic_item-type1", implicitCollection.getItemType()); + + implicitCollection = (ImplicitCollection) collections.get(1); + assertEquals("ic_owner-type2", implicitCollection.getOwnerType()); + assertEquals("ic_field-name2", implicitCollection.getFieldName()); + assertNull(implicitCollection.getItemFieldName()); + assertEquals("ic_item-type2", implicitCollection.getItemType()); + + // test ommited fields + List fields = config.getOmmitedFields(); + assertNotNull(fields); + assertEquals(2, fields.size()); + + OmmitedField ommitedField = (OmmitedField) fields.get(0); + assertEquals("ommited-field_type1", ommitedField.getType()); + assertEquals("ommited-field_field1", ommitedField.getFieldName()); + + ommitedField = (OmmitedField) fields.get(1); + assertEquals("ommited-field_type2", ommitedField.getType()); + assertEquals("ommited-field_field2", ommitedField.getFieldName()); + + // test immutable types + List types = config.getImmutableTypes(); + assertNotNull(types); + assertEquals(2, types.size()); + assertEquals("immutable-type1", types.get(0)); + assertEquals("immutable-type2", types.get(1)); + + // test default implementations + List implementations = config.getDefaultImplementations(); + assertNotNull(implementations); + assertEquals(2, implementations.size()); + + DefaultImplementation defaultImplementation = (DefaultImplementation) implementations.get(0); + assertEquals("default-implementation1", defaultImplementation.getDefaultImpl()); + assertEquals("type1", defaultImplementation.getType()); + + defaultImplementation = (DefaultImplementation) implementations.get(1); + assertEquals("default-implementation2", defaultImplementation.getDefaultImpl()); + assertEquals("type2", defaultImplementation.getType()); + + // test mappings + List mappings = config.getMappings(); + assertNotNull(mappings); + assertEquals(2, mappings.size()); + + Mapping mapping = (Mapping) mappings.get(0); + assertEquals("uri1", mapping.getNamespaceURI()); + assertEquals("localpart1", mapping.getLocalPart()); + assertEquals("prefix1", mapping.getPrefix()); + assertEquals("classname1", mapping.getClassName()); + + mapping = (Mapping) mappings.get(1); + assertEquals("uri2", mapping.getNamespaceURI()); + assertEquals("localpart2", mapping.getLocalPart()); + assertEquals("prefix2", mapping.getPrefix()); + assertEquals("classname2", mapping.getClassName()); + + } + + /** + * Test getXStreamConfiguration with non-existing config file. + * @throws Exception + */ + public void testNonExistingConfigFile() throws Exception { + + // set config file to non-existing file + factory.setConfigFile(new ClassPathResource("nonexisting-xstream-config-file.xml")); + + // try to get XStreamConfiguration + try { + factory.afterPropertiesSet(); + fail("BatchEnvironmentException was expected"); + } + catch (BatchEnvironmentException bee) { + assertTrue(true); + } + } +} diff --git a/infrastructure/src/test/java/org/springframework/batch/io/xml/xstream/XStreamFactoryIntegrationTests.java b/infrastructure/src/test/java/org/springframework/batch/io/xml/xstream/XStreamFactoryIntegrationTests.java new file mode 100644 index 000000000..69c49f465 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/io/xml/xstream/XStreamFactoryIntegrationTests.java @@ -0,0 +1,901 @@ +/* + * 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.xml.xstream; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import junit.framework.TestCase; + +import org.springframework.batch.io.exception.BatchEnvironmentException; +import org.springframework.batch.io.xml.ObjectInput; +import org.springframework.batch.io.xml.ObjectOutput; +import org.springframework.batch.io.xml.xstream.AttributeAlias; +import org.springframework.batch.io.xml.xstream.AttributeProperties; +import org.springframework.batch.io.xml.xstream.ClassAlias; +import org.springframework.batch.io.xml.xstream.DefaultImplementation; +import org.springframework.batch.io.xml.xstream.FieldAlias; +import org.springframework.batch.io.xml.xstream.ImplicitCollection; +import org.springframework.batch.io.xml.xstream.OmmitedField; +import org.springframework.batch.io.xml.xstream.TypeAlias; +import org.springframework.batch.io.xml.xstream.XStreamConfiguration; +import org.springframework.batch.io.xml.xstream.XStreamFactory; +import org.springframework.core.io.FileSystemResource; + +import com.thoughtworks.xstream.XStream; + +/** + * Integretion test for XStreamFactory. + * + * @author peter.zozom + */ +public class XStreamFactoryIntegrationTests extends TestCase { + + private XStream stream; + + private XStreamConfiguration config; + + private XStreamFactory factory; + + public void testAddDefaultImplementations() throws ClassNotFoundException { + + // override tested methods + class XStreamExt extends XStream { + + private String diName; + + private String otName; + + private boolean test = false; + + public void init(String diName, String otName) { + test = true; + this.diName = diName; + this.otName = otName; + } + + public void addDefaultImplementation(Class defaultImplementation, Class ofType) { + if (test) { + assertEquals(diName, defaultImplementation.getName()); + assertEquals(otName, ofType.getName()); + } + } + } + + // TEST1: test adding default implementation + + // create new XStream + stream = new XStreamExt(); + // set expected values + ((XStreamExt) stream).init("java.util.ArrayList", "java.util.List"); + + // create DefaultImplemetation object + DefaultImplementation di = new DefaultImplementation(); + di.setDefaultImpl("java.util.ArrayList"); + di.setType("java.util.List"); + + // add it to list of defaultImplementations + List defaultImplementations = new ArrayList(); + defaultImplementations.add(di); + + // create configuration object + config = new XStreamConfiguration(); + // set list of defaultImplementations + config.setDefaultImplementations(defaultImplementations); + + // create factory + factory = new XStreamFactory(); + // set config object + factory.setConfig(config); + // call set-up method for XStream + factory.setUpXStream(stream); + + // TEST2: ClassNotFoundException for 'defaultImplementation' parameter + + // set defaultImplementation class name to some non-existing class name + di.setDefaultImpl("test.some.nonexisting.ClassName"); + // call set-up method for XStream - BatchEnvironmentException is + // expected + try { + factory.setUpXStream(stream); + fail("BatchEnvironmentException was expected"); + } + catch (BatchEnvironmentException bee) { + assertTrue(bee.getCause() instanceof ClassNotFoundException); + } + + // TEST3: ClassNotFoundException for 'ofType' parameter + + // set ofType class name to some non-existing class name + di.setType("test.some.nonexisting.ClassName"); + di.setDefaultImpl("java.util.List"); + + // call set-up method for XStream - BatchEnvironmentException is + // expected + try { + factory.setUpXStream(stream); + fail("BatchEnvironmentException was expected"); + } + catch (BatchEnvironmentException bee) { + assertTrue(bee.getCause() instanceof ClassNotFoundException); + } + } + + public void testSetClassAliases() { + + // override tested methods + class XStreamExt extends XStream { + + private String shortName; + + private String typeName; + + private String diName; + + private boolean test; + + public void init(String shortName, String typeName, String diName) { + test = true; + this.shortName = shortName; + this.typeName = typeName; + this.diName = diName; + } + + public void alias(String name, Class type, Class defaultImplementation) { + if (test) { + assertEquals(shortName, name); + assertEquals(typeName, type.getName()); + assertEquals(diName, defaultImplementation.getName()); + } + } + + public void alias(String name, Class type) { + if (test) { + assertEquals(shortName, name); + assertEquals(typeName, type.getName()); + } + } + } + + // TEST1: test setting class alias with method alias(String,Class,Class) + + // create new XStream + stream = new XStreamExt(); + // set expected values + ((XStreamExt) stream).init("testAlias", "java.util.List", "java.util.ArrayList"); + + // create classAlias + ClassAlias classAlias = new ClassAlias(); + classAlias.setName("testAlias"); + classAlias.setType("java.util.List"); + classAlias.setDefaultImplementation("java.util.ArrayList"); + + // add it to the list of aliases + List classAliases = new ArrayList(); + classAliases.add(classAlias); + + // create configuration object + config = new XStreamConfiguration(); + // set list of classAliases + config.setClassAliases(classAliases); + + // create factory + factory = new XStreamFactory(); + // set config object + factory.setConfig(config); + // call set-up method for XStream + factory.setUpXStream(stream); + + // TEST2: test setting class alias with method alias(String,Class) + classAlias.setDefaultImplementation(null); + factory.setUpXStream(stream); + + // TEST3: ClassNotFoundException for 'defaultImplementation' parameter + classAlias.setDefaultImplementation("test.some.nonexisting.ClassName"); + // call set-up method for XStream - BatchEnvironmentException is + // expected + try { + factory.setUpXStream(stream); + fail("BatchEnvironmentException was expected"); + } + catch (BatchEnvironmentException bee) { + assertTrue(bee.getCause() instanceof ClassNotFoundException); + } + + // TEST4: ClassNotFoundException for 'type' parameter + classAlias.setDefaultImplementation(null); + classAlias.setType("test.some.nonexisting.ClassName"); + // call set-up method for XStream - BatchEnvironmentException is + // expected + try { + factory.setUpXStream(stream); + fail("BatchEnvironmentException was expected"); + } + catch (BatchEnvironmentException bee) { + assertTrue(bee.getCause() instanceof ClassNotFoundException); + } + + } + + public void testSetTypeAliases() { + + // override tested methods + class XStreamExt extends XStream { + + private String shortName; + + private String typeName; + + private boolean test; + + public void init(String shortName, String typeName) { + test = true; + this.shortName = shortName; + this.typeName = typeName; + } + + public void aliasType(String name, Class type) { + if (test) { + assertEquals(shortName, name); + assertEquals(typeName, type.getName()); + } + } + } + + // TEST1: test setting type alias with method alias(String,Class) + + // create new XStream + stream = new XStreamExt(); + // set expected values + ((XStreamExt) stream).init("testAlias", "java.util.List"); + + // create classAlias + TypeAlias typeAlias = new TypeAlias(); + typeAlias.setName("testAlias"); + typeAlias.setType("java.util.List"); + + // add it to the list of aliases + List typeAliases = new ArrayList(); + typeAliases.add(typeAlias); + + // create configuration object + config = new XStreamConfiguration(); + // set list of classAliases + config.setTypeAliases(typeAliases); + + // create factory + factory = new XStreamFactory(); + // set config object + factory.setConfig(config); + // call set-up method for XStream + factory.setUpXStream(stream); + + // TEST2: ClassNotFoundException for 'type' parameter + typeAlias.setType("test.some.nonexisting.ClassName"); + // call set-up method for XStream - BatchEnvironmentException is + // expected + try { + factory.setUpXStream(stream); + fail("BatchEnvironmentException was expected"); + } + catch (BatchEnvironmentException bee) { + assertTrue(bee.getCause() instanceof ClassNotFoundException); + } + } + + public void testSetFieldAliases() { + + // override tested methods + class XStreamExt extends XStream { + + private String aliasName; + + private String typeName; + + private String fieldName; + + private boolean test; + + public void init(String aliasName, String typeName, String fieldName) { + test = true; + this.aliasName = aliasName; + this.typeName = typeName; + this.fieldName = fieldName; + } + + public void aliasField(String aliasName, Class type, String fieldName) { + if (test) { + assertEquals(this.aliasName, aliasName); + assertEquals(typeName, type.getName()); + assertEquals(this.fieldName, fieldName); + } + } + } + + // TEST1: test setting type alias with method alias(String,Class) + + // create new XStream + stream = new XStreamExt(); + // set expected values + ((XStreamExt) stream).init("testAlias", "java.util.List", "list"); + + // create classAlias + FieldAlias fieldAlias = new FieldAlias(); + fieldAlias.setAliasName("testAlias"); + fieldAlias.setType("java.util.List"); + fieldAlias.setFieldName("list"); + + // add it to the list of aliases + List fieldAliases = new ArrayList(); + fieldAliases.add(fieldAlias); + + // create configuration object + config = new XStreamConfiguration(); + // set list of classAliases + config.setFieldAliases(fieldAliases); + + // create factory + factory = new XStreamFactory(); + // set config object + factory.setConfig(config); + // call set-up method for XStream + factory.setUpXStream(stream); + + // TEST2: ClassNotFoundException for 'type' parameter + fieldAlias.setType("test.some.nonexisting.ClassName"); + // call set-up method for XStream - BatchEnvironmentException is + // expected + try { + factory.setUpXStream(stream); + fail("BatchEnvironmentException was expected"); + } + catch (BatchEnvironmentException bee) { + assertTrue(bee.getCause() instanceof ClassNotFoundException); + } + } + + public void testSetAttributeAliases() { + + // override tested methods + class XStreamExt extends XStream { + + private String alias; + + private String attributeName; + + private boolean test = false; + + public void init(String alias, String attributeName) { + test = true; + this.alias = alias; + this.attributeName = attributeName; + } + + public void aliasAttribute(String alias, String attributeName) { + if (test) { + assertEquals(this.alias, alias); + assertEquals(this.attributeName, attributeName); + } + } + } + + // TEST1: test adding attribute aliases + + // create new XStream + stream = new XStreamExt(); + // set expected values + ((XStreamExt) stream).init("alias", "attribute"); + + AttributeAlias alias = new AttributeAlias(); + alias.setAlias("alias"); + alias.setAttributeName("attribute"); + + List aliases = new ArrayList(); + aliases.add(alias); + + // create configuration object + config = new XStreamConfiguration(); + // set list of classAliases + config.setAttributeAliases(aliases); + + // create factory + factory = new XStreamFactory(); + // set config object + factory.setConfig(config); + // call set-up method for XStream + factory.setUpXStream(stream); + } + + public void testSetAttributes() { + + // override tested methods + class XStreamExt extends XStream { + + private String type; + + private String fieldName; + + private boolean test = false; + + public void init(String type, String fieldName) { + test = true; + this.type = type; + this.fieldName = fieldName; + } + + public void useAttributeFor(Class type) { + if (test) { + assertEquals(this.type, type.getName()); + } + } + + public void useAttributeFor(String fieldName, Class type) { + if (test) { + assertEquals(this.fieldName, fieldName); + assertEquals(this.type, type.getName()); + } + } + } + + // TEST1: test adding attribute properties with + // useAttributeFor(String,Class) + + // create new XStream + stream = new XStreamExt(); + // set expected values + ((XStreamExt) stream).init("java.util.List", "fieldName"); + + AttributeProperties props = new AttributeProperties(); + props.setFieldName("fieldName"); + props.setType("java.util.List"); + + List properties = new ArrayList(); + properties.add(props); + + // create configuration object + config = new XStreamConfiguration(); + // set list of classAliases + config.setAttributes(properties); + + // create factory + factory = new XStreamFactory(); + // set config object + factory.setConfig(config); + // call set-up method for XStream + factory.setUpXStream(stream); + + // TEST2: test adding attribute properties with useAttributeFor(String) + props.setFieldName(null); + factory.setUpXStream(stream); + + // TEST3: ClassNotFoundException for 'type' parameter + props.setType("test.some.nonexisting.ClassName"); + // call set-up method for XStream - BatchEnvironmentException is + // expected + try { + factory.setUpXStream(stream); + fail("BatchEnvironmentException was expected"); + } + catch (BatchEnvironmentException bee) { + assertTrue(bee.getCause() instanceof ClassNotFoundException); + } + } + +// TODO different results for JDK1.4 and JDK1.5 +// public void testRegisterConverters() { +// +// // override tested methods +// class XStreamExt extends XStream { +// +// private String className; +// +// private int priority; +// +// private boolean test = false; +// +// public void init(String className, int priority) { +// test = true; +// this.className = className; +// this.priority = priority; +// } +// +// public void registerConverter(Converter converter, int priority) { +// if (test) { +// assertEquals(className, converter.getClass().getName()); +// assertEquals(this.priority, priority); +// } +// } +// +// public void registerConverter(SingleValueConverter converter, int priority) { +// if (test) { +// assertEquals(className, converter.getClass().getName()); +// assertEquals(this.priority, priority); +// } +// } +// } +// +// // TEST1: test registering single value converter +// +// // create new XStream +// stream = new XStreamExt(); +// // set expected values +// ((XStreamExt) stream).init("com.thoughtworks.xstream.converters.basic.FloatConverter", 10); +// +// ConverterProperties cp = new ConverterProperties(); +// cp.setConverterClassName("com.thoughtworks.xstream.converters.basic.FloatConverter"); +// cp.setPriority(10); +// +// List converters = new ArrayList(); +// converters.add(cp); +// +// // create configuration object +// config = new XStreamConfiguration(); +// // set list of classAliases +// config.setConverters(converters); +// +// // create factory +// factory = new XStreamFactory(); +// // set config object +// factory.setConfig(config); +// // call set-up method for XStream +// factory.setUpXStream(stream); +// +// // TEST2: test registering converter +// cp.setConverterClassName("com.thoughtworks.xstream.converters.basic.NullConverter"); +// ((XStreamExt) stream).init("com.thoughtworks.xstream.converters.basic.NullConverter", 10); +// factory.setUpXStream(stream); +// +// // TEST3: BatchEnviromentException due to invalid type (not assignable +// // to SingleValueConverter or Converter) +// cp.setConverterClassName("java.util.List"); +// try { +// factory.setUpXStream(stream); +// fail("BatchEnvironmentException was expected"); +// } +// catch (BatchEnvironmentException bee) { +// assertNull(bee.getCause()); +// } +// +// // TEST4: ClassNotFoundException +// cp.setConverterClassName("test.some.nonexisting.ClassName"); +// try { +// factory.setUpXStream(stream); +// fail("BatchEnvironmentException was expected"); +// } +// catch (BatchEnvironmentException bee) { +// assertTrue(bee.getCause() instanceof ClassNotFoundException); +// } +// +// // TEST5: InstantiationException +// // set interface as className +// cp.setConverterClassName("com.thoughtworks.xstream.converters.Converter"); +// try { +// factory.setUpXStream(stream); +// fail("BatchEnvironmentException was expected"); +// } +// catch (BatchEnvironmentException bee) { +// assertTrue(bee.getCause() instanceof InstantiationException); +// } +// } + + public void testSetMode() { + + // override tested methods + class XStreamExt extends XStream { + + private int mode; + + private boolean test = false; + + public void init(int mode) { + test = true; + this.mode = mode; + } + + public void setMode(int mode) { + if (test) { + assertEquals(this.mode, mode); + } + } + } + + // create new XStream + stream = new XStreamExt(); + ((XStreamExt) stream).init(1001); + + // create configuration object + config = new XStreamConfiguration(); + // set list of classAliases + config.setMode(1001); + + // create factory + factory = new XStreamFactory(); + // set config object + factory.setConfig(config); + // call set-up method for XStream + factory.setUpXStream(stream); + } + + public void testAddImplicitCollections() { + + // override tested methods + class XStreamExt extends XStream { + + private String ownerType; + + private String fieldName; + + private String itemFieldName; + + private String itemType; + + private boolean test = false; + + /* + * Set expected values + */ + public void init(String ownerType, String fieldName, String itemFieldName, String itemType) { + test = true; + this.ownerType = ownerType; + this.fieldName = fieldName; + this.itemFieldName = itemFieldName; + this.itemType = itemType; + } + + public void addImplicitCollection(Class ownerType, String fieldName, Class itemType) { + if (test) { + assertEquals(this.ownerType, ownerType.getName()); + assertEquals(this.fieldName, fieldName); + assertEquals(this.itemType, itemType.getName()); + } + } + + public void addImplicitCollection(Class ownerType, String fieldName, String itemFieldName, Class itemType) { + if (test) { + assertEquals(this.ownerType, ownerType.getName()); + assertEquals(this.fieldName, fieldName); + assertEquals(this.itemFieldName, itemFieldName); + assertEquals(this.itemType, itemType.getName()); + } + } + + public void addImplicitCollection(Class ownerType, String fieldName) { + if (test) { + assertEquals(this.ownerType, ownerType.getName()); + assertEquals(this.fieldName, fieldName); + } + } + } + + // create new XStream + stream = new XStreamExt(); + ((XStreamExt) stream).init("java.util.List", "fieldName", "itemFieldName", "java.util.Map"); + + // TEST1: test adding implicit collection with + // addImplicitCollection(Class, String) + ImplicitCollection implicitCollection = new ImplicitCollection(); + implicitCollection.setOwnerType("java.util.List"); + implicitCollection.setFieldName("fieldName"); + + List implicitCollections = new ArrayList(); + implicitCollections.add(implicitCollection); + + // create configuration object + config = new XStreamConfiguration(); + // set list of classAliases + config.setImplicitCollections(implicitCollections); + + // create factory + factory = new XStreamFactory(); + // set config object + factory.setConfig(config); + // call set-up method for XStream + factory.setUpXStream(stream); + + // TEST2: test adding implicit collection with + // addImplicitCollection(Class, String, String) + implicitCollection.setItemType("java.util.Map"); + factory.setUpXStream(stream); + + // TEST3: test adding implicit collection with + // addImplicitCollection(Class, String, String, String) + implicitCollection.setItemFieldName("itemFieldName"); + factory.setUpXStream(stream); + + // TEST4: ClassNotFoundException due to non-existing class name in + // itemType + implicitCollection.setItemType("test.some.nonexisting.ClassName"); + try { + factory.setUpXStream(stream); + fail("BatchEnvironmentException was expected"); + } + catch (BatchEnvironmentException bee) { + assertTrue(bee.getCause() instanceof ClassNotFoundException); + } + + // TEST5: ClassNotFoundException due to non-existing class name in + // ownerType + implicitCollection.setOwnerType("test.some.nonexisting.ClassName"); + try { + factory.setUpXStream(stream); + fail("BatchEnvironmentException was expected"); + } + catch (BatchEnvironmentException bee) { + assertTrue(bee.getCause() instanceof ClassNotFoundException); + } + } + + public void testSetOmittedFields() { + + // override tested methods + class XStreamExt extends XStream { + + private String type; + + private String fieldName; + + private boolean test = false; + + public void init(String type, String fieldName) { + test = true; + this.type = type; + this.fieldName = fieldName; + } + + public void omitField(Class type, String fieldName) { + if (test) { + assertEquals(this.type, type.getName()); + assertEquals(this.fieldName, fieldName); + } + } + } + + // TEST1: test adding ommited fields + + // create new XStream + stream = new XStreamExt(); + // set expected values + ((XStreamExt) stream).init("java.util.List", "fieldName"); + + OmmitedField ommitedField = new OmmitedField(); + ommitedField.setType("java.util.List"); + ommitedField.setFieldName("fieldName"); + + List ommitedFields = new ArrayList(); + ommitedFields.add(ommitedField); + + // create configuration object + config = new XStreamConfiguration(); + // set list of classAliases + config.setOmmitedFields(ommitedFields); + + // create factory + factory = new XStreamFactory(); + // set config object + factory.setConfig(config); + // call set-up method for XStream + factory.setUpXStream(stream); + + // TEST2: ClassNotFoundException + ommitedField.setType("test.some.nonexisting.ClassName"); + try { + factory.setUpXStream(stream); + fail("BatchEnvironmentException was expected"); + } + catch (BatchEnvironmentException bee) { + assertTrue(bee.getCause() instanceof ClassNotFoundException); + } + } + + public void testAddImmutableTypes() { + + // override tested methods + class XStreamExt extends XStream { + + private String type; + + private boolean test = false; + + public void init(String type) { + test = true; + this.type = type; + + } + + public void addImmutableType(Class type) { + if (test) { + assertEquals(this.type, type.getName()); + } + } + } + + // create new XStream + stream = new XStreamExt(); + // set expected values + ((XStreamExt) stream).init("java.util.List"); + + List immutableTypes = new ArrayList(); + immutableTypes.add("java.util.List"); + + // create configuration object + config = new XStreamConfiguration(); + // set list of classAliases + config.setImmutableTypes(immutableTypes); + + // create factory + factory = new XStreamFactory(); + // set config object + factory.setConfig(config); + // call set-up method for XStream + factory.setUpXStream(stream); + + // TEST2: ClassNotFoundException + immutableTypes.clear(); + immutableTypes.add("test.some.nonexisting.ClassName"); + try { + factory.setUpXStream(stream); + fail("BatchEnvironmentException was expected"); + } + catch (BatchEnvironmentException bee) { + assertTrue(bee.getCause() instanceof ClassNotFoundException); + } + } + + public void testWriteAndRead() throws IOException, ClassNotFoundException { + + // create file + File file = File.createTempFile("test", ".xml"); + // create factory and set empty configuration + XStreamFactory factory = new XStreamFactory(); + factory.setConfig(new XStreamConfiguration()); + + // define test class + class TestValueObject { + String param1; + + int param2; + + Long param3; + } + + TestValueObject valueObject = new TestValueObject(); + valueObject.param1 = "test"; + valueObject.param2 = 392; + valueObject.param3 = new Long(632); + + // just a simple test for object output and input: write object to XML + // and read it back + + ObjectOutput output = factory.createObjectOutput(new FileSystemResource(file), "UTF-8"); + output.writeObject(valueObject); + output.close(); + + ObjectInput input = factory.createObjectInput(new FileSystemResource(file), "UTF-8"); + Object result = input.readObject(); + input.close(); + file.delete(); + + // is result instance of TestValueObject? + assertTrue(result instanceof TestValueObject); + // is result equal to written object? + assertEquals(valueObject.param1, ((TestValueObject) result).param1); + assertEquals(valueObject.param2, ((TestValueObject) result).param2); + assertEquals(valueObject.param3, ((TestValueObject) result).param3); + } +} diff --git a/infrastructure/src/test/java/org/springframework/batch/item/ItemProviderTests.java b/infrastructure/src/test/java/org/springframework/batch/item/ItemProviderTests.java new file mode 100644 index 000000000..a7c4736f3 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/item/ItemProviderTests.java @@ -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.item; + +import junit.framework.TestCase; + +import org.springframework.batch.item.provider.AbstractItemProvider; + +public class ItemProviderTests extends TestCase { + + ItemProvider provider = new AbstractItemProvider() { + public Object next() { + return "foo"; + } + }; + + public void testNext() throws Exception { + assertEquals("foo", provider.next()); + } + + public void testRecover() throws Exception { + try { + provider.recover("foo", null); + } + catch (Exception e) { + fail("Unexpected Exception"); + } + } +} diff --git a/infrastructure/src/test/java/org/springframework/batch/item/exception/UnexpectedInputExceptionTests.java b/infrastructure/src/test/java/org/springframework/batch/item/exception/UnexpectedInputExceptionTests.java new file mode 100644 index 000000000..1098b5a93 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/item/exception/UnexpectedInputExceptionTests.java @@ -0,0 +1,32 @@ +/* + * 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; + +import org.springframework.batch.item.exception.UnexpectedInputException; +import org.springframework.batch.repeat.exception.AbstractExceptionTests; + +public class UnexpectedInputExceptionTests extends AbstractExceptionTests { + + public Exception getException(String msg) throws Exception { + return new UnexpectedInputException(msg, null); + } + + public Exception getException(String msg, Throwable t) throws Exception { + return new UnexpectedInputException(msg, t); + } + +} diff --git a/infrastructure/src/test/java/org/springframework/batch/item/provider/AbstractFieldSetItemProviderTests.java b/infrastructure/src/test/java/org/springframework/batch/item/provider/AbstractFieldSetItemProviderTests.java new file mode 100644 index 000000000..0f313ed08 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/item/provider/AbstractFieldSetItemProviderTests.java @@ -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 junit.framework.TestCase; + +import org.springframework.batch.io.file.FieldSet; +import org.springframework.batch.io.file.FieldSetInputSource; +import org.springframework.batch.io.file.support.SimpleFlatFileInputSource; +import org.springframework.core.io.ByteArrayResource; + +/** + * @author Dave Syer + * + */ +public class AbstractFieldSetItemProviderTests extends TestCase { + + public void testNotOpen() throws Exception { + TestItemProvider provider = new TestItemProvider(); + provider.setSource(getInputSource("one\ntwo\nthree")); + assertNotNull(provider.next()); + } + + public void testAfterPropertiesSet() throws Exception { + TestItemProvider provider = new TestItemProvider(); + try { + provider.afterPropertiesSet(); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + // expected + } + } + + private static class TestItemProvider extends AbstractFieldSetItemProvider { + protected Object transform(FieldSet fieldSet) { + return fieldSet.toString(); + } + } + + private FieldSetInputSource getInputSource(String data) throws Exception { + SimpleFlatFileInputSource template = new SimpleFlatFileInputSource(); + template.setResource(new ByteArrayResource(data.getBytes())); + template.afterPropertiesSet(); + return template; + } +} diff --git a/infrastructure/src/test/java/org/springframework/batch/item/provider/FieldSetItemProviderTests.java b/infrastructure/src/test/java/org/springframework/batch/item/provider/FieldSetItemProviderTests.java new file mode 100644 index 000000000..6c5cf7bdc --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/item/provider/FieldSetItemProviderTests.java @@ -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.item.provider; + +import junit.framework.TestCase; + +import org.springframework.batch.io.file.FieldSet; +import org.springframework.batch.io.file.support.SimpleFlatFileInputSource; +import org.springframework.core.io.ByteArrayResource; + +public class FieldSetItemProviderTests extends TestCase { + + public void testNotOpen() throws Exception { + TestItemProvider provider = new TestItemProvider("one\ntwo\nthree"); + assertNotNull(provider.next()); + } + + public void testNext() throws Exception { + TestItemProvider provider = new TestItemProvider("one\ntwo\nthree"); + assertEquals("[one]", provider.next()); + assertEquals("[two]", provider.next()); + assertEquals("[three]", provider.next()); + assertEquals(null, provider.next()); + } + + private static class TestItemProvider extends AbstractFieldSetItemProvider { + public TestItemProvider(String data) throws Exception { + super(); + SimpleFlatFileInputSource template = new SimpleFlatFileInputSource(); + template.setResource(new ByteArrayResource(data.getBytes())); + template.afterPropertiesSet(); + setSource(template); + } + + protected Object transform(FieldSet fieldSet) { + return fieldSet.toString(); + } + } + +} diff --git a/infrastructure/src/test/java/org/springframework/batch/item/provider/JmsItemProviderTests.java b/infrastructure/src/test/java/org/springframework/batch/item/provider/JmsItemProviderTests.java new file mode 100644 index 000000000..28c50e2ce --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/item/provider/JmsItemProviderTests.java @@ -0,0 +1,181 @@ +/* + * 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.Date; + +import javax.jms.Destination; +import javax.jms.Message; +import javax.jms.Queue; + +import junit.framework.TestCase; + +import org.easymock.MockControl; +import org.springframework.jms.core.JmsOperations; + +public class JmsItemProviderTests extends TestCase { + + JmsItemProvider itemProvider = new JmsItemProvider(); + + public void testNoItemTypeSunnyDay() { + MockControl templateControl = MockControl.createControl(JmsOperations.class); + JmsOperations jmsTemplate = (JmsOperations) templateControl.getMock(); + templateControl.expectAndReturn(jmsTemplate.receiveAndConvert(), "foo"); + templateControl.replay(); + + itemProvider.setJmsTemplate(jmsTemplate); + assertEquals("foo", itemProvider.next()); + templateControl.verify(); + } + + public void testSetItemTypeSunnyDay() { + MockControl templateControl = MockControl.createControl(JmsOperations.class); + JmsOperations jmsTemplate = (JmsOperations) templateControl.getMock(); + templateControl.expectAndReturn(jmsTemplate.receiveAndConvert(), "foo"); + templateControl.replay(); + + itemProvider.setJmsTemplate(jmsTemplate); + itemProvider.setItemType(String.class); + assertEquals("foo", itemProvider.next()); + templateControl.verify(); + } + + public void testSetItemSubclassTypeSunnyDay() { + MockControl templateControl = MockControl.createControl(JmsOperations.class); + JmsOperations jmsTemplate = (JmsOperations) templateControl.getMock(); + + Date date = new java.sql.Date(0L); + templateControl.expectAndReturn(jmsTemplate.receiveAndConvert(), date); + templateControl.replay(); + + itemProvider.setJmsTemplate(jmsTemplate); + itemProvider.setItemType(Date.class); + assertEquals(date, itemProvider.next()); + templateControl.verify(); + } + + public void testSetItemTypeMismatch() { + MockControl templateControl = MockControl.createControl(JmsOperations.class); + JmsOperations jmsTemplate = (JmsOperations) templateControl.getMock(); + templateControl.expectAndReturn(jmsTemplate.receiveAndConvert(), "foo"); + templateControl.replay(); + + itemProvider.setJmsTemplate(jmsTemplate); + itemProvider.setItemType(Date.class); + try { + itemProvider.next(); + fail("Expected IllegalStateException"); + } + catch (IllegalStateException e) { + // expected + assertTrue(e.getMessage().indexOf("wrong type") >= 0); + } + templateControl.verify(); + } + + public void testNextMessageSunnyDay() { + MockControl templateControl = MockControl.createControl(JmsOperations.class); + MockControl messageControl = MockControl.createControl(Message.class); + JmsOperations jmsTemplate = (JmsOperations) templateControl.getMock(); + Message message = (Message) messageControl.getMock(); + templateControl.expectAndReturn(jmsTemplate.receive(), message); + templateControl.replay(); + + itemProvider.setJmsTemplate(jmsTemplate); + itemProvider.setItemType(Message.class); + assertEquals(message, itemProvider.next()); + templateControl.verify(); + } + + public void testRecoverWithNoDestination() throws Exception { + MockControl templateControl = MockControl.createControl(JmsOperations.class); + JmsOperations jmsTemplate = (JmsOperations) templateControl.getMock(); + templateControl.replay(); + + itemProvider.setJmsTemplate(jmsTemplate); + itemProvider.setItemType(String.class); + itemProvider.recover("foo", null); + + templateControl.verify(); + } + + public void testErrorQueueWithDestinationName() throws Exception { + MockControl templateControl = MockControl.createControl(JmsOperations.class); + JmsOperations jmsTemplate = (JmsOperations) templateControl.getMock(); + jmsTemplate.convertAndSend("queue", "foo"); + templateControl.setVoidCallable(); + templateControl.replay(); + + itemProvider.setJmsTemplate(jmsTemplate); + itemProvider.setItemType(String.class); + itemProvider.setErrorDestinationName("queue"); + itemProvider.recover("foo", null); + templateControl.verify(); + } + + public void testErrorQueueWithDestination() throws Exception { + MockControl templateControl = MockControl.createControl(JmsOperations.class); + MockControl queueControl = MockControl.createControl(Queue.class); + + Destination queue = (Destination) queueControl.getMock(); + queueControl.replay(); + + JmsOperations jmsTemplate = (JmsOperations) templateControl.getMock(); + jmsTemplate.convertAndSend(queue, "foo"); + templateControl.setVoidCallable(); + templateControl.replay(); + + itemProvider.setJmsTemplate(jmsTemplate); + itemProvider.setItemType(String.class); + itemProvider.setErrorDestination(queue); + itemProvider.recover("foo", null); + templateControl.verify(); + } + + public void testGetKeyFromMessage() throws Exception { + MockControl messageControl = MockControl.createControl(Message.class); + Message message = (Message) messageControl.getMock(); + messageControl.expectAndReturn(message.getJMSMessageID(), "foo"); + messageControl.replay(); + + itemProvider.setItemType(Message.class); + assertEquals("foo", itemProvider.getKey(message)); + messageControl.verify(); + + } + + public void testGetKeyFromNonMessage() throws Exception { + itemProvider.setItemType(String.class); + assertEquals("foo", itemProvider.getKey("foo")); + } + + public void testIsNewForMessage() throws Exception { + MockControl messageControl = MockControl.createControl(Message.class); + Message message = (Message) messageControl.getMock(); + messageControl.expectAndReturn(message.getJMSRedelivered(), true); + messageControl.replay(); + + itemProvider.setItemType(Message.class); + assertEquals(true, itemProvider.hasFailed(message)); + messageControl.verify(); + } + + public void testIsNewForNonMessage() throws Exception { + itemProvider.setItemType(String.class); + assertEquals(true, itemProvider.hasFailed("foo")); + } +} diff --git a/infrastructure/src/test/java/org/springframework/batch/item/provider/ListItemProviderTests.java b/infrastructure/src/test/java/org/springframework/batch/item/provider/ListItemProviderTests.java new file mode 100644 index 000000000..871aec2e6 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/item/provider/ListItemProviderTests.java @@ -0,0 +1,44 @@ +/* + * 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.Arrays; +import java.util.List; + +import junit.framework.TestCase; + +public class ListItemProviderTests extends TestCase { + + ListItemProvider provider = new ListItemProvider(Arrays.asList(new String[] { "a", "b", "c" })); + + public void testNext() throws Exception { + assertEquals("a", provider.next()); + assertEquals("b", provider.next()); + assertEquals("c", provider.next()); + assertEquals(null, provider.next()); + } + + public void testChangeList() throws Exception { + List list = new ArrayList(Arrays.asList(new String[] { "a", "b", "c" })); + provider = new ListItemProvider(list); + assertEquals("a", provider.next()); + list.clear(); + assertEquals(0, list.size()); + assertEquals("b", provider.next()); + } +} diff --git a/infrastructure/src/test/java/org/springframework/batch/item/provider/TransactionAwareListItemProviderTests.java b/infrastructure/src/test/java/org/springframework/batch/item/provider/TransactionAwareListItemProviderTests.java new file mode 100644 index 000000000..4e492dbc7 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/item/provider/TransactionAwareListItemProviderTests.java @@ -0,0 +1,124 @@ +/* + * 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.Arrays; +import java.util.List; + +import junit.framework.TestCase; + +import org.springframework.batch.support.transaction.ResourcelessTransactionManager; +import org.springframework.batch.support.transaction.TransactionAwareProxyFactory; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.TransactionCallback; +import org.springframework.transaction.support.TransactionTemplate; + +public class TransactionAwareListItemProviderTests extends TestCase { + + // TransactionAwareListItemProvider provider = new + // TransactionAwareListItemProvider(Arrays.asList(new String[] { "a", + // "b", "c" })); + ListItemProvider provider; + + protected void setUp() throws Exception { + super.setUp(); + TransactionAwareProxyFactory factory = new TransactionAwareProxyFactory(Arrays.asList(new String[] { "a", "b", + "c" })); + provider = new ListItemProvider((List) factory.createInstance()); + } + + public void testNext() throws Exception { + assertEquals("a", provider.next()); + assertEquals("b", provider.next()); + assertEquals("c", provider.next()); + assertEquals(null, provider.next()); + } + + public void testCommit() throws Exception { + PlatformTransactionManager transactionManager = new ResourcelessTransactionManager(); + final List taken = new ArrayList(); + try { + new TransactionTemplate(transactionManager).execute(new TransactionCallback() { + public Object doInTransaction(TransactionStatus status) { + taken.add(provider.next()); + return null; + } + }); + } + catch (RuntimeException e) { + fail("Unexpected RuntimeException"); + assertEquals("Rollback!", e.getMessage()); + } + assertEquals(1, taken.size()); + assertEquals("a", taken.get(0)); + taken.clear(); + Object next = provider.next(); + while (next != null) { + taken.add(next); + next = provider.next(); + } + // System.err.println(taken); + assertFalse(taken.contains("a")); + } + + public void testTransactionalExhausted() throws Exception { + PlatformTransactionManager transactionManager = new ResourcelessTransactionManager(); + final List taken = new ArrayList(); + new TransactionTemplate(transactionManager).execute(new TransactionCallback() { + public Object doInTransaction(TransactionStatus status) { + Object next = provider.next(); + while (next != null) { + taken.add(next); + next = provider.next(); + } + return null; + } + }); + assertEquals(3, taken.size()); + assertEquals("a", taken.get(0)); + } + + public void testRollback() throws Exception { + PlatformTransactionManager transactionManager = new ResourcelessTransactionManager(); + final List taken = new ArrayList(); + try { + new TransactionTemplate(transactionManager).execute(new TransactionCallback() { + public Object doInTransaction(TransactionStatus status) { + taken.add(provider.next()); + throw new RuntimeException("Rollback!"); + } + }); + fail("Expected RuntimeException"); + } + catch (RuntimeException e) { + assertEquals("Rollback!", e.getMessage()); + } + assertEquals(1, taken.size()); + assertEquals("a", taken.get(0)); + taken.clear(); + Object next = provider.next(); + while (next != null) { + taken.add(next); + next = provider.next(); + } + System.err.println(taken); + assertTrue(taken.contains("a")); + } + +} diff --git a/infrastructure/src/test/java/org/springframework/batch/item/validator/SpringValidatorTests.java b/infrastructure/src/test/java/org/springframework/batch/item/validator/SpringValidatorTests.java new file mode 100644 index 000000000..fc146d9a4 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/item/validator/SpringValidatorTests.java @@ -0,0 +1,150 @@ +/* + * 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 junit.framework.TestCase; + +import org.springframework.batch.io.exception.ValidationException; +import org.springframework.batch.item.validator.SpringValidator; +import org.springframework.validation.Errors; +import org.springframework.validation.Validator; + +public class SpringValidatorTests extends TestCase { + private SpringValidator validator = new SpringValidator(); + + private Validator mockValidator; + + protected void setUp() throws Exception { + mockValidator = new MockSpringValidator(); + validator.setValidator(mockValidator); + } + + /** + * Validator property is not set + */ + public void testValidateNullValidator() { + validator.setValidator(null); + + try { + validator.validate(MockSpringValidator.ACCEPT_VALUE); + fail("must not validate with null validator"); + } + catch (ValidationException expected) { + assertTrue(true); + } + } + + /** + * Validator does not know how to validate object of the given class + */ + public void testValidateUnsupportedType() { + try { + validator.validate(new Integer(1)); // only strings are supported + fail("must not validate unsupported classes"); + } + catch (ValidationException expected) { + assertTrue(true); + } + } + + /** + * Typical successful validation + */ + public void testValidateSuccessfully() { + try { + validator.validate(MockSpringValidator.ACCEPT_VALUE); + assertTrue(true); + } + catch (ValidationException unexpected) { + throw unexpected; + } + } + + /** + * Typical failed validation + */ + public void testValidateFailure() { + try { + validator.validate(MockSpringValidator.REJECT_VALUE); + fail("exception should have been thrown on invalid value"); + } + catch (ValidationException expected) { + assertTrue(true); + } + } + + /** + * Typical failed validation + */ + public void testValidateFailureWithFields() { + try { + validator.validate(MockSpringValidator.REJECT_MULTI_VALUE); + fail("exception should have been thrown on invalid value"); + } + catch (ValidationException expected) { + assertTrue("Wonrg message: "+expected.getMessage(), expected.getMessage().indexOf("foo, bar")>=0); + } + } + + static class MockSpringValidator implements Validator { + public static final TestBean ACCEPT_VALUE = new TestBean(); + + public static final TestBean REJECT_VALUE = new TestBean(); + + public static final TestBean REJECT_MULTI_VALUE = new TestBean("foo", "bar"); + + public boolean supports(Class clazz) { + return clazz.isAssignableFrom(TestBean.class); + } + + public void validate(Object value, Errors errors) { + if (value.equals(ACCEPT_VALUE)) { + return; // return without adding errors + } + + if (value.equals(REJECT_VALUE)) { + errors.reject("bad.value"); + return; + } + if (value.equals(REJECT_MULTI_VALUE)) { + errors.rejectValue("foo", "bad.value"); + errors.rejectValue("bar", "bad.value"); + return; + } + } + } + + static class TestBean { + private String foo; + private String bar; + public String getFoo() { + return foo; + } + public String getBar() { + return bar; + } + public TestBean() { + super(); + } + public TestBean(String foo, String bar) { + this(); + this.foo = foo; + this.bar = bar; + } + + } +} diff --git a/infrastructure/src/test/java/org/springframework/batch/repeat/ExitStatusTests.java b/infrastructure/src/test/java/org/springframework/batch/repeat/ExitStatusTests.java new file mode 100644 index 000000000..e97917b82 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/repeat/ExitStatusTests.java @@ -0,0 +1,104 @@ +/* + * 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.repeat; + +import junit.framework.TestCase; + +/** + * @author Dave Syer + * + */ +public class ExitStatusTests extends TestCase { + + /** + * Test method for {@link org.springframework.batch.repeat.ExitStatus#ExitStatus(boolean, int)}. + */ + public void testExitStatusBooleanInt() { + ExitStatus status = new ExitStatus(true, 10); + assertTrue(status.isContinuable()); + assertEquals(10, status.getExitCode()); + } + + /** + * Test method for {@link org.springframework.batch.repeat.ExitStatus#ExitStatus(boolean, int)}. + */ + public void testExitStatusConstantsContinuable() { + ExitStatus status = ExitStatus.CONTINUABLE; + assertTrue(status.isContinuable()); + assertEquals(0, status.getExitCode()); + } + + /** + * Test method for {@link org.springframework.batch.repeat.ExitStatus#ExitStatus(boolean, int)}. + */ + public void testExitStatusConstantsFinished() { + ExitStatus status = ExitStatus.FINISHED; + assertFalse(status.isContinuable()); + assertEquals(0, status.getExitCode()); + } + + /** + * Test method for {@link org.springframework.batch.repeat.ExitStatus#and(boolean)}. + */ + public void testAndBoolean() { + assertTrue(ExitStatus.CONTINUABLE.and(true).isContinuable()); + assertFalse(ExitStatus.CONTINUABLE.and(false).isContinuable()); + } + + /** + * Test method for {@link org.springframework.batch.repeat.ExitStatus#and(org.springframework.batch.repeat.ExitStatus)}. + */ + public void testAndExitStatus() { + assertTrue(ExitStatus.CONTINUABLE.and(ExitStatus.CONTINUABLE).isContinuable()); + assertFalse(ExitStatus.CONTINUABLE.and(ExitStatus.FINISHED).isContinuable()); + } + + /** + * Test method for {@link org.springframework.batch.repeat.ExitStatus#addExitCode(int)}. + */ + public void testAddExitCode() { + ExitStatus status = ExitStatus.FINISHED.addExitCode(10); + assertEquals(10, status.getExitCode()); + assertFalse(status.isContinuable()); + } + + /** + * Test method for {@link org.springframework.batch.repeat.ExitStatus#addExitCode(int)}. + */ + public void testAddExitCodeBothNegative() { + ExitStatus status = ExitStatus.FINISHED.addExitCode(-2); + assertEquals(-2, status.getExitCode()); + assertEquals(-3, status.addExitCode(-3).getExitCode()); + } + + /** + * Test method for {@link org.springframework.batch.repeat.ExitStatus#addExitCode(int)}. + */ + public void testAddExitCodeBothPositive() { + ExitStatus status = ExitStatus.FINISHED.addExitCode(2); + assertEquals(2, status.getExitCode()); + assertEquals(3, status.addExitCode(3).getExitCode()); + } + + /** + * Test method for {@link org.springframework.batch.repeat.ExitStatus#addExitCode(int)}. + */ + public void testAddExitCodeNewNegative() { + ExitStatus status = ExitStatus.FINISHED.addExitCode(2); + assertEquals(2, status.getExitCode()); + assertEquals(-3, status.addExitCode(-3).getExitCode()); + } +} diff --git a/infrastructure/src/test/java/org/springframework/batch/repeat/aop/RepeatOperationsInterceptorTests.java b/infrastructure/src/test/java/org/springframework/batch/repeat/aop/RepeatOperationsInterceptorTests.java new file mode 100644 index 000000000..7ced248d6 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/repeat/aop/RepeatOperationsInterceptorTests.java @@ -0,0 +1,120 @@ +/* + * 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.repeat.aop; + +import java.util.ArrayList; +import java.util.List; + +import junit.framework.TestCase; + +import org.springframework.aop.framework.Advised; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.batch.repeat.RepeatCallback; +import org.springframework.batch.repeat.RepeatOperations; +import org.springframework.batch.repeat.ExitStatus; +import org.springframework.batch.repeat.exception.RepeatException; + +public class RepeatOperationsInterceptorTests extends TestCase { + + private RepeatOperationsInterceptor interceptor; + + private Service service; + + private ServiceImpl target; + + protected void setUp() throws Exception { + super.setUp(); + interceptor = new RepeatOperationsInterceptor(); + target = new ServiceImpl(); + ProxyFactory factory = new ProxyFactory(RepeatOperations.class.getClassLoader()); + factory.setInterfaces(new Class[] { Service.class }); + factory.setTarget(target); + service = (Service) factory.getProxy(); + } + + public void testDefaultInterceptorSunnyDay() throws Exception { + ((Advised) service).addAdvice(interceptor); + service.service(); + assertEquals(3, target.count); + } + + public void testSetTemplate() throws Exception { + final List calls = new ArrayList(); + interceptor.setRepeatOperations(new RepeatOperations() { + public ExitStatus iterate(RepeatCallback callback) { + Object result = "1"; + calls.add(result); + return ExitStatus.CONTINUABLE; + } + }); + ((Advised) service).addAdvice(interceptor); + service.service(); + assertEquals(1, calls.size()); + } + + public void testCallbackWithException() throws Exception { + ((Advised) service).addAdvice(interceptor); + try { + service.exception(); + fail("Expected RuntimeException"); + } + catch (RuntimeException e) { + assertEquals("Duh", e.getMessage().substring(0, 3)); + } + } + + public void testCallbackWithThrowable() throws Exception { + ((Advised) service).addAdvice(interceptor); + try { + service.error(); + fail("Expected BatchException"); + } + catch (RepeatException e) { + assertEquals("Unexpected", e.getMessage().substring(0, 10)); + } + } + + private interface Service { + Object service() throws Exception; + + Object exception() throws Exception; + + Object error() throws Exception; + } + + private static class ServiceImpl implements Service { + private int count = 0; + + public Object service() throws Exception { + count++; + if (count <= 2) { + return new Integer(count); + } + else { + return null; + } + } + + public Object exception() throws Exception { + throw new RuntimeException("Duh! Stupid."); + } + + public Object error() throws Exception { + throw new Error("Duh! Stupid."); + } + } +} diff --git a/infrastructure/src/test/java/org/springframework/batch/repeat/callback/ItemProviderRepeatCallbackTests.java b/infrastructure/src/test/java/org/springframework/batch/repeat/callback/ItemProviderRepeatCallbackTests.java new file mode 100644 index 000000000..9302fdd09 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/repeat/callback/ItemProviderRepeatCallbackTests.java @@ -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.repeat.callback; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import junit.framework.TestCase; + +import org.springframework.batch.item.ItemProcessor; +import org.springframework.batch.item.provider.ListItemProvider; + +public class ItemProviderRepeatCallbackTests extends TestCase { + + ItemProviderRepeatCallback callback; + + List list = new ArrayList(); + + public void testDoWithRepeat() throws Exception { + callback = new ItemProviderRepeatCallback(new ListItemProvider(Arrays.asList(new String[] { "foo", "bar" })), + new ItemProcessor() { + public void process(Object data) { + list.add(data); + } + }); + callback.doInIteration(null); + assertEquals(1, list.size()); + assertEquals("foo", list.get(0)); + } + + public void testDoWithRepeatNullProcessor() throws Exception { + ListItemProvider provider = new ListItemProvider(Arrays.asList(new String[] { "foo", "bar" })); + callback = new ItemProviderRepeatCallback(provider); + callback.doInIteration(null); + assertEquals(0, list.size()); + assertEquals("bar", provider.next()); + } + +} diff --git a/infrastructure/src/test/java/org/springframework/batch/repeat/callback/NestedRepeatCallbackTests.java b/infrastructure/src/test/java/org/springframework/batch/repeat/callback/NestedRepeatCallbackTests.java new file mode 100644 index 000000000..e50904db6 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/repeat/callback/NestedRepeatCallbackTests.java @@ -0,0 +1,41 @@ +/* + * 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.repeat.callback; + +import junit.framework.TestCase; + +import org.springframework.batch.repeat.RepeatCallback; +import org.springframework.batch.repeat.RepeatContext; +import org.springframework.batch.repeat.ExitStatus; +import org.springframework.batch.repeat.support.RepeatTemplate; + +public class NestedRepeatCallbackTests extends TestCase { + + int count = 0; + + public void testExecute() throws Exception { + NestedRepeatCallback callback = new NestedRepeatCallback(new RepeatTemplate(), new RepeatCallback() { + public ExitStatus doInIteration(RepeatContext context) throws Exception { + count++; + return new ExitStatus(count <= 1); + } + }); + ExitStatus result = callback.doInIteration(null); + assertEquals(2, count); + assertFalse(result.isContinuable()); // False because processing has finished + } +} diff --git a/infrastructure/src/test/java/org/springframework/batch/repeat/context/RepeatContextCounterTests.java b/infrastructure/src/test/java/org/springframework/batch/repeat/context/RepeatContextCounterTests.java new file mode 100644 index 000000000..5f94da4b8 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/repeat/context/RepeatContextCounterTests.java @@ -0,0 +1,66 @@ +/* + * 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.repeat.context; + +import junit.framework.TestCase; + +import org.springframework.batch.repeat.RepeatContext; +import org.springframework.batch.repeat.context.RepeatContextCounter; +import org.springframework.batch.repeat.context.RepeatContextSupport; + +public class RepeatContextCounterTests extends TestCase { + + RepeatContext parent = new RepeatContextSupport(null); + RepeatContext context = new RepeatContextSupport(parent); + + public void testAttributeCreated() { + new RepeatContextCounter(context, "FOO"); + assertTrue(context.hasAttribute("FOO")); + } + + public void testAttributeCreatedWithNullParent() { + new RepeatContextCounter(parent, "FOO", true); + assertTrue(parent.hasAttribute("FOO")); + } + + public void testVanillaIncrement() throws Exception { + RepeatContextCounter counter = new RepeatContextCounter(context, "FOO"); + assertEquals(0, counter.getCount()); + counter.increment(1); + assertEquals(1, counter.getCount()); + counter.increment(2); + assertEquals(3, counter.getCount()); + } + + public void testAttributeCreatedInParent() throws Exception { + new RepeatContextCounter(context, "FOO", true); + assertFalse(context.hasAttribute("FOO")); + assertTrue(parent.hasAttribute("FOO")); + } + + public void testParentIncrement() throws Exception { + RepeatContextCounter counter = new RepeatContextCounter(context, "FOO", true); + assertEquals(0, counter.getCount()); + counter.increment(1); + // now get new context with same parent + counter = new RepeatContextCounter(new RepeatContextSupport(parent), "FOO", true); + assertEquals(1, counter.getCount()); + counter.increment(2); + assertEquals(3, counter.getCount()); + } + +} diff --git a/infrastructure/src/test/java/org/springframework/batch/repeat/context/RepeatContextSupportTests.java b/infrastructure/src/test/java/org/springframework/batch/repeat/context/RepeatContextSupportTests.java new file mode 100644 index 000000000..dced65425 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/repeat/context/RepeatContextSupportTests.java @@ -0,0 +1,93 @@ +/* + * 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.repeat.context; + +import java.util.ArrayList; +import java.util.List; + +import junit.framework.TestCase; + +/** + * @author dsyer + * + */ +public class RepeatContextSupportTests extends TestCase { + + private List list = new ArrayList(); + + /** + * Test method for {@link org.springframework.batch.repeat.context.RepeatContextSupport#registerDestructionCallback(java.lang.String, java.lang.Runnable)}. + */ + public void testDestructionCallbackSunnyDay() throws Exception { + RepeatContextSupport context = new RepeatContextSupport(null); + context.setAttribute("foo", "FOO"); + context.registerDestructionCallback("foo", new Runnable() { + public void run() { + list.add("bar"); + } + }); + context.close(); + assertEquals(1, list.size()); + assertEquals("bar", list.get(0)); + } + + /** + * Test method for {@link org.springframework.batch.repeat.context.RepeatContextSupport#registerDestructionCallback(java.lang.String, java.lang.Runnable)}. + */ + public void testDestructionCallbackMissingAttribute() throws Exception { + RepeatContextSupport context = new RepeatContextSupport(null); + context.registerDestructionCallback("foo", new Runnable() { + public void run() { + list.add("bar"); + } + }); + context.close(); + // No check for the attribute before executing callback + assertEquals(1, list.size()); + } + + /** + * Test method for {@link org.springframework.batch.repeat.context.RepeatContextSupport#registerDestructionCallback(java.lang.String, java.lang.Runnable)}. + */ + public void testDestructionCallbackWithException() throws Exception { + RepeatContextSupport context = new RepeatContextSupport(null); + context.setAttribute("foo", "FOO"); + context.setAttribute("bar", "BAR"); + context.registerDestructionCallback("bar", new Runnable() { + public void run() { + list.add("spam"); + throw new RuntimeException("fail!"); + } + }); + context.registerDestructionCallback("foo", new Runnable() { + public void run() { + list.add("bar"); + throw new RuntimeException("fail!"); + } + }); + try { + context.close(); + fail("Expected RuntimeException"); + } catch (RuntimeException e) { + // We don't care which one was thrown... + assertEquals("fail!", e.getMessage()); + } + // ...but we do care that both were executed: + assertEquals(2, list.size()); + assertTrue(list.contains("bar")); + assertTrue(list.contains("spam")); + } +} diff --git a/infrastructure/src/test/java/org/springframework/batch/repeat/context/SynchronizedAttributeAccessorTests.java b/infrastructure/src/test/java/org/springframework/batch/repeat/context/SynchronizedAttributeAccessorTests.java new file mode 100644 index 000000000..2f2e60acd --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/repeat/context/SynchronizedAttributeAccessorTests.java @@ -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.repeat.context; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import junit.framework.TestCase; + +import org.springframework.batch.repeat.context.SynchronizedAttributeAccessor; +import org.springframework.core.AttributeAccessorSupport; + +public class SynchronizedAttributeAccessorTests extends TestCase { + + SynchronizedAttributeAccessor accessor = new SynchronizedAttributeAccessor(); + + public void testHashCode() { + SynchronizedAttributeAccessor another = new SynchronizedAttributeAccessor(); + accessor.setAttribute("foo", "bar"); + another.setAttribute("foo", "bar"); + assertEquals(accessor.hashCode(), another.hashCode()); + } + + public void testToStringWithNoAttributes() throws Exception { + assertNotNull(accessor.toString()); + } + + public void testToStringWithAttributes() throws Exception { + accessor.setAttribute("foo", "bar"); + accessor.setAttribute("spam", "bucket"); + assertNotNull(accessor.toString()); + } + + public void testAttributeNames() { + accessor.setAttribute("foo", "bar"); + accessor.setAttribute("spam", "bucket"); + List list = Arrays.asList(accessor.attributeNames()); + assertEquals(2, list.size()); + assertTrue(list.contains("foo")); + } + + public void testEqualsSameType() { + SynchronizedAttributeAccessor another = new SynchronizedAttributeAccessor(); + accessor.setAttribute("foo", "bar"); + another.setAttribute("foo", "bar"); + assertEquals(accessor, another); + } + + public void testEqualsSelf() { + accessor.setAttribute("foo", "bar"); + assertEquals(accessor, accessor); + } + + public void testEqualsWrongType() { + accessor.setAttribute("foo", "bar"); + Map another = Collections.singletonMap("foo", "bar"); + + //TODO accessor and another are instances of unrelated classes, they can never be equal + assertFalse(accessor.equals(another)); + } + + public void testEqualsSupport() { + AttributeAccessorSupport another = new AttributeAccessorSupport() { + }; + accessor.setAttribute("foo", "bar"); + another.setAttribute("foo", "bar"); + assertEquals(accessor, another); + } + + public void testGetAttribute() { + accessor.setAttribute("foo", "bar"); + assertEquals("bar", accessor.getAttribute("foo")); + } + + public void testSetAttributeIfAbsentWhenAlreadyPresent() { + accessor.setAttribute("foo", "bar"); + assertEquals("bar", accessor.setAttributeIfAbsent("foo", "spam")); + } + + public void testSetAttributeIfAbsentWhenNotAlreadyPresent() { + assertEquals(null, accessor.setAttributeIfAbsent("foo", "bar")); + assertEquals("bar", accessor.getAttribute("foo")); + } + + public void testHasAttribute() { + accessor.setAttribute("foo", "bar"); + assertEquals(true, accessor.hasAttribute("foo")); + } + + public void testRemoveAttribute() { + accessor.setAttribute("foo", "bar"); + assertEquals("bar", accessor.getAttribute("foo")); + accessor.removeAttribute("foo"); + assertEquals(null, accessor.getAttribute("foo")); + } + +} diff --git a/infrastructure/src/test/java/org/springframework/batch/repeat/exception/AbstractExceptionTests.java b/infrastructure/src/test/java/org/springframework/batch/repeat/exception/AbstractExceptionTests.java new file mode 100644 index 000000000..efbadfdb5 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/repeat/exception/AbstractExceptionTests.java @@ -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.repeat.exception; + +import junit.framework.TestCase; + +public abstract class AbstractExceptionTests extends TestCase { + + public void testExceptionString() throws Exception { + Exception exception = getException("foo"); + assertEquals("foo", exception.getMessage()); + } + + public void testExceptionStringThrowable() throws Exception { + Exception exception = getException("foo", new IllegalStateException()); + assertEquals("foo", exception.getMessage().substring(0, 3)); + } + + public abstract Exception getException(String msg) throws Exception; + + public abstract Exception getException(String msg, Throwable t) throws Exception; +} diff --git a/infrastructure/src/test/java/org/springframework/batch/repeat/exception/RepeatExceptionTests.java b/infrastructure/src/test/java/org/springframework/batch/repeat/exception/RepeatExceptionTests.java new file mode 100644 index 000000000..c08653ed0 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/repeat/exception/RepeatExceptionTests.java @@ -0,0 +1,32 @@ +/* + * 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.repeat.exception; + +public class RepeatExceptionTests extends AbstractExceptionTests { + + public Exception getException(String msg) throws Exception { + return new RepeatException(msg); + } + + public Exception getException(String msg, Throwable t) throws Exception { + return new RepeatException(msg, t); + } + + public void testNothing() throws Exception { + // fool coverage tools... + } +} diff --git a/infrastructure/src/test/java/org/springframework/batch/repeat/exception/handler/CompositeExceptionHandlerTests.java b/infrastructure/src/test/java/org/springframework/batch/repeat/exception/handler/CompositeExceptionHandlerTests.java new file mode 100644 index 000000000..ad6dd02bd --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/repeat/exception/handler/CompositeExceptionHandlerTests.java @@ -0,0 +1,60 @@ +/* + * 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.repeat.exception.handler; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import junit.framework.TestCase; + +import org.springframework.batch.repeat.RepeatContext; + +public class CompositeExceptionHandlerTests extends TestCase { + + private CompositeExceptionHandler handler = new CompositeExceptionHandler(); + + public void testNewHandler() throws Exception { + try { + handler.handleExceptions(null, Collections.singleton(new RuntimeException())); + } + catch (RuntimeException e) { + fail("Unexpected RuntimeException"); + } + } + + public void testDelegation() throws Exception { + final List list = new ArrayList(); + handler.setHandlers(new ExceptionHandler[] { + new ExceptionHandler() { + public void handleExceptions(RepeatContext context, Collection throwables) { + list.add("1"); + } + }, + new ExceptionHandler() { + public void handleExceptions(RepeatContext context, Collection throwables) { + list.add("2"); + } + } + }); + handler.handleExceptions(null, Collections.singleton(new RuntimeException())); + assertEquals(2, list.size()); + assertEquals("1", list.get(0)); + assertEquals("2", list.get(1)); + } +} diff --git a/infrastructure/src/test/java/org/springframework/batch/repeat/exception/handler/DefaultExceptionHandlerTests.java b/infrastructure/src/test/java/org/springframework/batch/repeat/exception/handler/DefaultExceptionHandlerTests.java new file mode 100644 index 000000000..43c7d8b2a --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/repeat/exception/handler/DefaultExceptionHandlerTests.java @@ -0,0 +1,48 @@ +/* + * 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.repeat.exception.handler; + +import java.util.Collections; + +import junit.framework.TestCase; + +import org.springframework.batch.repeat.RepeatContext; +import org.springframework.batch.repeat.exception.RepeatException; + +public class DefaultExceptionHandlerTests extends TestCase { + + private DefaultExceptionHandler handler = new DefaultExceptionHandler(); + private RepeatContext context = null; + + public void testRuntimeException() throws Exception { + try { + handler.handleExceptions(context, Collections.singleton(new RuntimeException("Foo"))); + fail("Expected RuntimeException"); + } catch (RuntimeException e) { + assertEquals("Foo", e.getMessage()); + } + } + + public void testError() throws Exception { + try { + handler.handleExceptions(context, Collections.singleton(new Error("Foo"))); + fail("Expected BatchException"); + } catch (RepeatException e) { + assertEquals("Foo", e.getCause().getMessage()); + } + } +} diff --git a/infrastructure/src/test/java/org/springframework/batch/repeat/exception/handler/LogOrRethrowExceptionHandlerTests.java b/infrastructure/src/test/java/org/springframework/batch/repeat/exception/handler/LogOrRethrowExceptionHandlerTests.java new file mode 100644 index 000000000..54c7ca52e --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/repeat/exception/handler/LogOrRethrowExceptionHandlerTests.java @@ -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.repeat.exception.handler; + +import java.io.StringWriter; +import java.util.Collections; + +import junit.framework.TestCase; + +import org.apache.log4j.Level; +import org.apache.log4j.Logger; +import org.apache.log4j.SimpleLayout; +import org.apache.log4j.WriterAppender; +import org.springframework.batch.common.ExceptionClassifierSupport; +import org.springframework.batch.repeat.RepeatContext; +import org.springframework.batch.repeat.exception.RepeatException; + +public class LogOrRethrowExceptionHandlerTests extends TestCase { + + private LogOrRethrowExceptionHandler handler = new LogOrRethrowExceptionHandler(); + private StringWriter writer; + private RepeatContext context = null; + + protected void setUp() throws Exception { + super.setUp(); + Logger logger = Logger.getLogger(LogOrRethrowExceptionHandler.class); + logger.setLevel(Level.DEBUG); + writer = new StringWriter(); + logger.removeAllAppenders(); + logger.getParent().removeAllAppenders(); + logger.addAppender(new WriterAppender(new SimpleLayout(), writer)); + } + + public void testRuntimeException() throws Exception { + try { + handler.handleExceptions(context, Collections.singleton(new RuntimeException("Foo"))); + fail("Expected RuntimeException"); + } catch (RuntimeException e) { + assertEquals("Foo", e.getMessage()); + } + } + + public void testError() throws Exception { + try { + handler.handleExceptions(context, Collections.singleton(new Error("Foo"))); + fail("Expected BatchException"); + } catch (RepeatException e) { + assertEquals("Foo", e.getCause().getMessage()); + } + } + + public void testNotRethrownErrorLevel() throws Exception { + handler.setExceptionClassifier(new ExceptionClassifierSupport() { + public Object classify(Throwable throwable) { + return LogOrRethrowExceptionHandler.ERROR; + } + }); + // No exception... + handler.handleExceptions(context, Collections.singleton(new Error("Foo"))); + assertNotNull(writer.toString()); + } + + public void testNotRethrownWarnLevel() throws Exception { + handler.setExceptionClassifier(new ExceptionClassifierSupport() { + public Object classify(Throwable throwable) { + return LogOrRethrowExceptionHandler.WARN; + } + }); + // No exception... + handler.handleExceptions(context, Collections.singleton(new Error("Foo"))); + assertNotNull(writer.toString()); + } + + public void testNotRethrownDebugLevel() throws Exception { + handler.setExceptionClassifier(new ExceptionClassifierSupport() { + public Object classify(Throwable throwable) { + return LogOrRethrowExceptionHandler.DEBUG; + } + }); + // No exception... + handler.handleExceptions(context, Collections.singleton(new Error("Foo"))); + assertNotNull(writer.toString()); + } + +} diff --git a/infrastructure/src/test/java/org/springframework/batch/repeat/exception/handler/RethrowOnThresholdExceptionHandlerTests.java b/infrastructure/src/test/java/org/springframework/batch/repeat/exception/handler/RethrowOnThresholdExceptionHandlerTests.java new file mode 100644 index 000000000..6b6cde816 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/repeat/exception/handler/RethrowOnThresholdExceptionHandlerTests.java @@ -0,0 +1,145 @@ +/* + * 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.repeat.exception.handler; + +import java.util.Collections; + +import junit.framework.TestCase; + +import org.springframework.batch.common.ExceptionClassifierSupport; +import org.springframework.batch.repeat.RepeatContext; +import org.springframework.batch.repeat.context.RepeatContextCounter; +import org.springframework.batch.repeat.context.RepeatContextSupport; +import org.springframework.batch.repeat.exception.RepeatException; + +public class RethrowOnThresholdExceptionHandlerTests extends TestCase { + + private RethrowOnThresholdExceptionHandler handler = new RethrowOnThresholdExceptionHandler(); + private RepeatContext parent = new RepeatContextSupport(null); + private RepeatContext context = new RepeatContextSupport(parent); + + public void testRuntimeException() throws Exception { + try { + handler.handleExceptions(context, Collections.singleton(new RuntimeException("Foo"))); + fail("Expected RuntimeException"); + } catch (RuntimeException e) { + assertEquals("Foo", e.getMessage()); + } + } + + public void testError() throws Exception { + try { + handler.handleExceptions(context, Collections.singleton(new Error("Foo"))); + fail("Expected BatchException"); + } catch (RepeatException e) { + assertEquals("Foo", e.getCause().getMessage()); + } + } + + public void testNotRethrownWithThreshold() throws Exception { + handler.setExceptionClassifier(new ExceptionClassifierSupport() { + public Object classify(Throwable throwable) { + return "RuntimeException"; + } + }); + handler.setThresholds(Collections.singletonMap("RuntimeException", new Integer(1))); + // No exception... + handler.handleExceptions(context, Collections.singleton(new RuntimeException("Foo"))); + RepeatContextCounter counter = new RepeatContextCounter(context, RethrowOnThresholdExceptionHandler.class + ".RuntimeException"); + assertNotNull(counter); + assertEquals(1, counter.getCount()); + } + + public void testRethrowOnThreshold() throws Exception { + handler.setExceptionClassifier(new ExceptionClassifierSupport() { + public Object classify(Throwable throwable) { + return "RuntimeException"; + } + }); + handler.setThresholds(Collections.singletonMap("RuntimeException", new Integer(1))); + // No exception... + handler.handleExceptions(context, Collections.singleton(new RuntimeException("Foo"))); + try { + handler.handleExceptions(context, Collections.singleton(new RuntimeException("Foo"))); + fail("Expected RuntimeException"); + } + catch (RuntimeException e) { + assertEquals("Foo", e.getMessage()); + } + } + + public void testNonIntegerAsThreshold() throws Exception { + try { + handler.setThresholds(Collections.singletonMap("RuntimeException", new Long(1))); + fail("Expected IllegalStateException"); + } + catch (IllegalStateException e) { + // expected + } + } + + public void testNotUseParent() throws Exception { + handler.setExceptionClassifier(new ExceptionClassifierSupport() { + public Object classify(Throwable throwable) { + return "RuntimeException"; + } + }); + handler.setThresholds(Collections.singletonMap("RuntimeException", new Integer(1))); + // No exception... + handler.handleExceptions(context, Collections.singleton(new RuntimeException("Foo"))); + context = new RepeatContextSupport(parent); + try { + // No exception again - context is changed... + handler.handleExceptions(context, Collections.singleton(new RuntimeException("Foo"))); + } + catch (RuntimeException e) { + fail("Unexpected Error"); + } + } + + public void testUseParent() throws Exception { + handler.setExceptionClassifier(new ExceptionClassifierSupport() { + public Object classify(Throwable throwable) { + return "RuntimeException"; + } + }); + handler.setThresholds(Collections.singletonMap("RuntimeException", new Integer(1))); + handler.setUseParent(true); + // No exception... + handler.handleExceptions(context, Collections.singleton(new RuntimeException("Foo"))); + context = new RepeatContextSupport(parent); + try { + handler.handleExceptions(context, Collections.singleton(new RuntimeException("Foo"))); + fail("Expected Error"); + } + catch (RuntimeException e) { + assertEquals("Foo", e.getMessage()); + } + } + + public void testNotStringAsKey() throws Exception { + try { + handler.setThresholds(Collections.singletonMap(RuntimeException.class, new Integer(1))); + // It's not an error, but not advised... + } + catch (RuntimeException e) { + throw e; + } + + } + +} diff --git a/infrastructure/src/test/java/org/springframework/batch/repeat/exception/handler/SimpleLimitExceptionHandlerTests.java b/infrastructure/src/test/java/org/springframework/batch/repeat/exception/handler/SimpleLimitExceptionHandlerTests.java new file mode 100644 index 000000000..be06da072 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/repeat/exception/handler/SimpleLimitExceptionHandlerTests.java @@ -0,0 +1,179 @@ +/* + * 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.repeat.exception.handler; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import junit.framework.TestCase; + +import org.springframework.batch.io.exception.TransactionInvalidException; +import org.springframework.batch.repeat.context.RepeatContextSupport; +import org.springframework.batch.repeat.exception.handler.SimpleLimitExceptionHandler; + +/** + * Unit tests for {@link SimpleLimitExceptionHandler} + * + * @author Robert Kasanicky + * @author Dave Syer + */ +public class SimpleLimitExceptionHandlerTests extends TestCase { + + // object under test + private SimpleLimitExceptionHandler handler = new SimpleLimitExceptionHandler(); + + public void testInitializeWithNullContext() throws Exception { + try { + handler.handleExceptions(null, Collections.singleton(new RuntimeException("foo"))); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + // expected + } + } + + public void testInitializeWithNullContextAndEmptyList() throws Exception { + try { + handler.handleExceptions(null, Collections.EMPTY_LIST); + } catch (Exception e) { + fail("Unexpected IllegalArgumentException"); + } + } + + /** + * Other than TransactionInvalidException should be rethrown, ignoring the exception limit. + */ + public void testNormalExceptionThrown() { + List throwables = Collections.singletonList(new RuntimeException("foo")); + + final int MORE_THAN_ZERO = 1; + handler.setLimit(MORE_THAN_ZERO); + + try{ + handler.handleExceptions(new RepeatContextSupport(null), throwables); + fail("Exception swallowed."); + } catch (RuntimeException expected) { + assertTrue("Exception is rethrown, ignoring the exception limit",true); + assertSame(throwables.get(0), expected); + } + } + + /** + * TransactionInvalidException should only be rethrown below the exception limit. + */ + public void testLimitedExceptionTypeNotThrown() { + List throwables = Collections.singletonList(new RuntimeException("foo")); + + final int MORE_THAN_ZERO = 1; + handler.setLimit(MORE_THAN_ZERO); + handler.setType(RuntimeException.class); + + try{ + handler.handleExceptions(new RepeatContextSupport(null), throwables); + } catch (RuntimeException expected) { + fail("Unexpected exception."); + } + } + + /** + * TransactionInvalidException should only be rethrown below the exception limit. + */ + public void testLimitedExceptionNotThrownFromSiblings() { + List throwables = Collections.singletonList(new RuntimeException("foo")); + + final int MORE_THAN_ZERO = 1; + handler.setLimit(MORE_THAN_ZERO); + handler.setType(RuntimeException.class); + + RepeatContextSupport parent = new RepeatContextSupport(null); + + try{ + RepeatContextSupport context = new RepeatContextSupport(parent); + handler.handleExceptions(context, throwables); + context = new RepeatContextSupport(parent); + handler.handleExceptions(context, throwables); + } catch (RuntimeException expected) { + fail("Unexpected exception."); + } + } + + /** + * TransactionInvalidException should only be rethrown below the exception limit. + */ + public void testLimitedExceptionThrownFromSiblingsWhenUsingParent() { + List throwables = Collections.singletonList(new RuntimeException("foo")); + + final int MORE_THAN_ZERO = 1; + handler.setLimit(MORE_THAN_ZERO); + handler.setType(RuntimeException.class); + handler.setUseParent(true); + + RepeatContextSupport parent = new RepeatContextSupport(null); + + try{ + RepeatContextSupport context = new RepeatContextSupport(parent); + handler.handleExceptions(context, throwables); + context = new RepeatContextSupport(parent); + handler.handleExceptions(context, throwables); + fail("Expected exception."); + } catch (RuntimeException expected) { + assertSame(throwables.get(0), expected); + } + } + + /** + * TransactionInvalidExceptions are swallowed until the exception limit is exceeded. + * After the limit is exceeded exceptions are rethrown as BatchCriticalExceptions + */ + public void testExceptionThrownAboveLimit() { + + final int EXCEPTION_LIMIT = 3; + handler.setLimit(EXCEPTION_LIMIT); + + List throwables = new ArrayList() {{ + for (int i = 0; i < (EXCEPTION_LIMIT); i++) { + add(new TransactionInvalidException("below exception limit")); + } + }}; + + RepeatContextSupport context = new RepeatContextSupport(null); + + try { + handler.handleExceptions(context, throwables); + assertTrue("exceptions up to limit are swallowed", true); + } catch (RuntimeException unexpected) { + fail("exception rethrown although exception limit was not exceeded"); + } + + + throwables = new ArrayList() {{ + add(new TransactionInvalidException("above exception limit")); + }}; + + // after reaching the limit, behaviour should be idempotent + final int ARBITRARY_REPEAT_COUNT = 2; + for (int i = 0; i < ARBITRARY_REPEAT_COUNT; i++) { + try { + handler.handleExceptions(context, throwables); + fail("exception above exception limit swallowed"); + } catch (TransactionInvalidException expected) { + assertSame(throwables.get(0), expected); + } + } + } + +} diff --git a/infrastructure/src/test/java/org/springframework/batch/repeat/interceptor/ApplicationEventPublisherRepeatInterceptorTests.java b/infrastructure/src/test/java/org/springframework/batch/repeat/interceptor/ApplicationEventPublisherRepeatInterceptorTests.java new file mode 100644 index 000000000..ba45f7bb6 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/repeat/interceptor/ApplicationEventPublisherRepeatInterceptorTests.java @@ -0,0 +1,107 @@ +/* + * 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.repeat.interceptor; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.batch.repeat.RepeatContext; +import org.springframework.batch.repeat.context.RepeatContextSupport; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationEventPublisher; + +import junit.framework.TestCase; + +/** + * @author Dave Syer + * + */ +public class ApplicationEventPublisherRepeatInterceptorTests extends TestCase { + + private ApplicationEventPublisherRepeatInterceptor interceptor = new ApplicationEventPublisherRepeatInterceptor(); + + private List list = new ArrayList(); + + private RepeatContext context = new RepeatContextSupport(null); + + /* (non-Javadoc) + * @see junit.framework.TestCase#setUp() + */ + protected void setUp() throws Exception { + super.setUp(); + interceptor.setApplicationEventPublisher(new ApplicationEventPublisher() { + public void publishEvent(ApplicationEvent event) { + list.add(event); + } + }); + } + + /** + * Test method for {@link org.springframework.batch.repeat.interceptor.ApplicationEventPublisherRepeatInterceptor#after(org.springframework.batch.repeat.RepeatContext, java.lang.Object)}. + */ + public void testAfter() { + interceptor.after(context, Boolean.TRUE); + assertEquals(1, list.size()); + RepeatOperationsApplicationEvent event = (RepeatOperationsApplicationEvent) list.get(0); + assertEquals(RepeatOperationsApplicationEvent.AFTER, event.getType()); + assertTrue(event.getMessage().toLowerCase().indexOf("after")>=0); + } + + /** + * Test method for {@link org.springframework.batch.repeat.interceptor.ApplicationEventPublisherRepeatInterceptor#before(org.springframework.batch.repeat.RepeatContext)}. + */ + public void testBefore() { + interceptor.before(context); + assertEquals(1, list.size()); + RepeatOperationsApplicationEvent event = (RepeatOperationsApplicationEvent) list.get(0); + assertEquals(RepeatOperationsApplicationEvent.BEFORE, event.getType()); + assertTrue(event.getMessage().toLowerCase().indexOf("before")>=0); + } + + /** + * Test method for {@link org.springframework.batch.repeat.interceptor.ApplicationEventPublisherRepeatInterceptor#close(org.springframework.batch.repeat.RepeatContext)}. + */ + public void testClose() { + interceptor.close(context); + assertEquals(1, list.size()); + RepeatOperationsApplicationEvent event = (RepeatOperationsApplicationEvent) list.get(0); + assertEquals(RepeatOperationsApplicationEvent.CLOSE, event.getType()); + assertTrue(event.getMessage().toLowerCase().indexOf("close")>=0); + } + + /** + * Test method for {@link org.springframework.batch.repeat.interceptor.ApplicationEventPublisherRepeatInterceptor#onError(org.springframework.batch.repeat.RepeatContext, java.lang.Throwable)}. + */ + public void testOnError() { + interceptor.onError(context, new RuntimeException("foo")); + assertEquals(1, list.size()); + RepeatOperationsApplicationEvent event = (RepeatOperationsApplicationEvent) list.get(0); + assertEquals(RepeatOperationsApplicationEvent.ERROR, event.getType()); + assertTrue(event.getMessage().toLowerCase().indexOf("foo")>=0); + } + + /** + * Test method for {@link org.springframework.batch.repeat.interceptor.ApplicationEventPublisherRepeatInterceptor#open(org.springframework.batch.repeat.RepeatContext)}. + */ + public void testOpen() { + interceptor.open(context); + assertEquals(1, list.size()); + RepeatOperationsApplicationEvent event = (RepeatOperationsApplicationEvent) list.get(0); + assertEquals(RepeatOperationsApplicationEvent.OPEN, event.getType()); + assertTrue(event.getMessage().toLowerCase().indexOf("open")>=0); + } + +} diff --git a/infrastructure/src/test/java/org/springframework/batch/repeat/interceptor/RepeatInterceptorTests.java b/infrastructure/src/test/java/org/springframework/batch/repeat/interceptor/RepeatInterceptorTests.java new file mode 100644 index 000000000..ef2d8a37f --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/repeat/interceptor/RepeatInterceptorTests.java @@ -0,0 +1,287 @@ +/* + * 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.repeat.interceptor; + +import java.util.ArrayList; +import java.util.List; + +import junit.framework.TestCase; + +import org.springframework.batch.repeat.RepeatCallback; +import org.springframework.batch.repeat.RepeatContext; +import org.springframework.batch.repeat.RepeatInterceptor; +import org.springframework.batch.repeat.ExitStatus; +import org.springframework.batch.repeat.support.RepeatTemplate; +import org.springframework.batch.repeat.support.TaskExecutorRepeatTemplate; +import org.springframework.core.task.SimpleAsyncTaskExecutor; + +public class RepeatInterceptorTests extends TestCase { + + int count = 0; + + public void testBeforeInterceptors() throws Exception { + RepeatTemplate template = new RepeatTemplate(); + final List calls = new ArrayList(); + template.setInterceptors(new RepeatInterceptor[] { new RepeatInterceptorAdapter() { + public void before(RepeatContext context) { + calls.add("1"); + } + }, new RepeatInterceptorAdapter() { + public void before(RepeatContext context) { + calls.add("2"); + } + } }); + template.iterate(new RepeatCallback() { + public ExitStatus doInIteration(RepeatContext context) throws Exception { + count++; + return new ExitStatus(count <= 1); + } + }); + // 2 calls: the second time there is no processing + // (despite the fact that the callback returned null and batch was + // complete). Is this OK? + assertEquals(2, count); + // ... but the interceptor before() was called: + assertEquals("[1, 2, 1, 2]", calls.toString()); + } + + public void testBeforeInterceptorCanVeto() throws Exception { + RepeatTemplate template = new RepeatTemplate(); + final List calls = new ArrayList(); + template.setInterceptor(new RepeatInterceptorAdapter() { + public void before(RepeatContext context) { + calls.add("1"); + context.setCompleteOnly(); + } + }); + template.iterate(new RepeatCallback() { + public ExitStatus doInIteration(RepeatContext context) throws Exception { + count++; + return ExitStatus.FINISHED; + } + }); + assertEquals(0, count); + // ... but the interceptor before() was called: + assertEquals("[1]", calls.toString()); + } + + public void testAfterInterceptors() throws Exception { + RepeatTemplate template = new RepeatTemplate(); + final List calls = new ArrayList(); + template.setInterceptors(new RepeatInterceptor[] { new RepeatInterceptorAdapter() { + public void after(RepeatContext context, Object result) { + calls.add("1"); + } + }, new RepeatInterceptorAdapter() { + public void after(RepeatContext context, Object result) { + calls.add("2"); + } + } }); + template.iterate(new RepeatCallback() { + public ExitStatus doInIteration(RepeatContext context) throws Exception { + count++; + return new ExitStatus(count <= 1); + } + }); + // 2 calls to the callback, and the second one had no processing... + assertEquals(2, count); + // ... so the interceptor after() is not called: + assertEquals("[2, 1]", calls.toString()); + } + + public void testOpenInterceptors() throws Exception { + RepeatTemplate template = new RepeatTemplate(); + final List calls = new ArrayList(); + template.setInterceptors(new RepeatInterceptor[] { new RepeatInterceptorAdapter() { + public void open(RepeatContext context) { + calls.add("1"); + } + }, new RepeatInterceptorAdapter() { + public void open(RepeatContext context) { + calls.add("2"); + context.setCompleteOnly(); + } + } }); + template.iterate(new RepeatCallback() { + public ExitStatus doInIteration(RepeatContext context) throws Exception { + count++; + return ExitStatus.CONTINUABLE; + } + }); + assertEquals(0, count); + assertEquals("[1, 2]", calls.toString()); + } + + public void testSingleOpenInterceptor() throws Exception { + RepeatTemplate template = new RepeatTemplate(); + final List calls = new ArrayList(); + template.setInterceptor(new RepeatInterceptorAdapter() { + public void open(RepeatContext context) { + calls.add("1"); + } + }); + template.iterate(new RepeatCallback() { + public ExitStatus doInIteration(RepeatContext context) throws Exception { + count++; + context.setCompleteOnly(); + return ExitStatus.FINISHED; + } + }); + assertEquals(1, count); + assertEquals("[1]", calls.toString()); + } + + public void testCloseInterceptors() throws Exception { + RepeatTemplate template = new RepeatTemplate(); + final List calls = new ArrayList(); + template.setInterceptors(new RepeatInterceptor[] { new RepeatInterceptorAdapter() { + public ExitStatus close(RepeatContext context) { + calls.add("1"); + return ExitStatus.CONTINUABLE; + } + }, new RepeatInterceptorAdapter() { + public ExitStatus close(RepeatContext context) { + calls.add("2"); + return ExitStatus.CONTINUABLE; + } + } }); + template.iterate(new RepeatCallback() { + public ExitStatus doInIteration(RepeatContext context) throws Exception { + count++; + return new ExitStatus(count < 2); + } + }); + // Test that more than one call comes in to the callback... + assertEquals(2, count); + // ... but the interceptor is only called once. + assertEquals("[2, 1]", calls.toString()); + } + + public void testCloseInterceptorsCanChangeExitCode() throws Exception { + RepeatTemplate template = new RepeatTemplate(); + final List calls = new ArrayList(); + template.setInterceptors(new RepeatInterceptor[] { new RepeatInterceptorAdapter() { + public ExitStatus close(RepeatContext context) { + calls.add("1"); + return ExitStatus.FINISHED.addExitCode(1); + } + }, new RepeatInterceptorAdapter() { + public ExitStatus close(RepeatContext context) { + calls.add("2"); + return ExitStatus.CONTINUABLE; + } + } }); + ExitStatus status = template.iterate(new RepeatCallback() { + public ExitStatus doInIteration(RepeatContext context) throws Exception { + count++; + return new ExitStatus(count < 2); + } + }); + // Test that only one call comes in to the callback... + assertEquals(2, count); + // ... and the interceptor is called once. + assertEquals("[2, 1]", calls.toString()); + assertEquals(1, status.getExitCode()); + } + + public void testOnErrorInterceptors() throws Exception { + RepeatTemplate template = new RepeatTemplate(); + final List calls = new ArrayList(); + template.setInterceptors(new RepeatInterceptor[] { new RepeatInterceptorAdapter() { + public void onError(RepeatContext context, Throwable t) { + calls.add("1"); + } + }, new RepeatInterceptorAdapter() { + public void onError(RepeatContext context, Throwable t) { + calls.add("2"); + } + } }); + try { + template.iterate(new RepeatCallback() { + public ExitStatus doInIteration(RepeatContext context) throws Exception { + throw new IllegalStateException("Bogus"); + } + }); + fail("Expected IllegalStateException"); + } + catch (IllegalStateException e) { + // expected + } + assertEquals(0, count); + assertEquals("[2, 1]", calls.toString()); + } + + public void testOnErrorInterceptorsPrecedence() throws Exception { + RepeatTemplate template = new RepeatTemplate(); + final List calls = new ArrayList(); + template.setInterceptors(new RepeatInterceptor[] { new RepeatInterceptorAdapter() { + public void after(RepeatContext context, Object result) { + calls.add("1"); + } + }, new RepeatInterceptorAdapter() { + public void onError(RepeatContext context, Throwable t) { + calls.add("2"); + } + } }); + try { + template.iterate(new RepeatCallback() { + public ExitStatus doInIteration(RepeatContext context) throws Exception { + throw new IllegalStateException("Bogus"); + } + }); + fail("Expected IllegalStateException"); + } + catch (IllegalStateException e) { + // expected + } + assertEquals(0, count); + // The after is executed, then the on error... + assertEquals("[1, 2]", calls.toString()); + } + + public void testAsynchronousOnErrorInterceptorsPrecedence() throws Exception { + TaskExecutorRepeatTemplate template = new TaskExecutorRepeatTemplate(); + template.setTaskExecutor(new SimpleAsyncTaskExecutor()); + final List calls = new ArrayList(); + final List fails = new ArrayList(); + template.setInterceptors(new RepeatInterceptor[] { new RepeatInterceptorAdapter() { + public void after(RepeatContext context, Object result) { + calls.add("1"); + } + }, new RepeatInterceptorAdapter() { + public void onError(RepeatContext context, Throwable t) { + calls.add("2"); + fails.add("2"); + } + } }); + try { + template.iterate(new RepeatCallback() { + public ExitStatus doInIteration(RepeatContext context) throws Exception { + throw new IllegalStateException("Bogus"); + } + }); + fail("Expected IllegalStateException"); + } + catch (IllegalStateException e) { + // expected + } + assertEquals(0, count); + // The after is executed, then the on error... + assertEquals(calls.lastIndexOf("1") + 1, calls.indexOf("2")); + assertEquals(fails.size() * 2, calls.size()); + } +} diff --git a/infrastructure/src/test/java/org/springframework/batch/repeat/policy/CompositeCompletionPolicyTests.java b/infrastructure/src/test/java/org/springframework/batch/repeat/policy/CompositeCompletionPolicyTests.java new file mode 100644 index 000000000..797824bd2 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/repeat/policy/CompositeCompletionPolicyTests.java @@ -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.repeat.policy; + +import junit.framework.TestCase; + +import org.springframework.batch.repeat.RepeatContext; +import org.springframework.batch.repeat.CompletionPolicy; + +public class CompositeCompletionPolicyTests extends TestCase { + + public void testEmptyPolicies() throws Exception { + CompositeCompletionPolicy policy = new CompositeCompletionPolicy(); + RepeatContext context = policy.start(null); + assertNotNull(context); + assertFalse(policy.isComplete(context)); + } + + public void testTrivialPolicies() throws Exception { + CompositeCompletionPolicy policy = new CompositeCompletionPolicy(); + policy.setPolicies(new CompletionPolicy[] { new MockCompletionPolicySupport(), + new MockCompletionPolicySupport() }); + RepeatContext context = policy.start(null); + assertEquals(0, context.getStartedCount()); + assertFalse(policy.isComplete(context)); + assertFalse(policy.isComplete(context, null)); + policy.update(context); + assertEquals(1, context.getStartedCount()); + } + + public void testNonTrivialPolicies() throws Exception { + CompositeCompletionPolicy policy = new CompositeCompletionPolicy(); + policy.setPolicies(new CompletionPolicy[] { new MockCompletionPolicySupport(), + new MockCompletionPolicySupport() { + public boolean isComplete(RepeatContext context) { + return true; + } + } }); + RepeatContext context = policy.start(null); + assertTrue(policy.isComplete(context)); + } + + public void testNonTrivialPoliciesWithResult() throws Exception { + CompositeCompletionPolicy policy = new CompositeCompletionPolicy(); + policy.setPolicies(new CompletionPolicy[] { new MockCompletionPolicySupport(), + new MockCompletionPolicySupport() { + public boolean isComplete(RepeatContext context, Object result) { + return true; + } + } }); + RepeatContext context = policy.start(null); + assertTrue(policy.isComplete(context, null)); + } +} diff --git a/infrastructure/src/test/java/org/springframework/batch/repeat/policy/CountingCompletionPolicyTests.java b/infrastructure/src/test/java/org/springframework/batch/repeat/policy/CountingCompletionPolicyTests.java new file mode 100644 index 000000000..80e35c4e0 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/repeat/policy/CountingCompletionPolicyTests.java @@ -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.repeat.policy; + +import junit.framework.TestCase; + +import org.springframework.batch.repeat.RepeatContext; +import org.springframework.batch.repeat.context.RepeatContextSupport; + +public class CountingCompletionPolicyTests extends TestCase { + + public void testDefaultBehaviour() throws Exception { + CountingCompletionPolicy policy = new CountingCompletionPolicy() { + protected int getCount(RepeatContext context) { + return 1; + }; + }; + RepeatContext context = policy.start(null); + assertTrue(policy.isComplete(context)); + } + + public void testDefaultBehaviourWithUpdate() throws Exception { + CountingCompletionPolicy policy = new CountingCompletionPolicy() { + int count = 0; + + protected int getCount(RepeatContext context) { + return count; + }; + + protected int doUpdate(RepeatContext context) { + count++; + return 1; + } + }; + policy.setMaxCount(2); + RepeatContext context = policy.start(null); + policy.update(context); + assertFalse(policy.isComplete(context)); + policy.update(context); + assertTrue(policy.isComplete(context)); + } + + public void testUpdateNotSavedAcrossSession() throws Exception { + CountingCompletionPolicy policy = new CountingCompletionPolicy() { + int count = 0; + + protected int getCount(RepeatContext context) { + return count; + }; + + protected int doUpdate(RepeatContext context) { + super.doUpdate(context); + count++; + return 1; + } + + public RepeatContext start(RepeatContext context) { + count = 0; + return super.start(context); + } + }; + policy.setMaxCount(2); + RepeatContextSupport session = new RepeatContextSupport(null); + RepeatContext context = policy.start(session); + policy.update(context); + assertFalse(policy.isComplete(context)); + context = policy.start(session); + policy.update(context); + assertFalse(policy.isComplete(context)); + } + + public void testUpdateSavedAcrossSession() throws Exception { + CountingCompletionPolicy policy = new CountingCompletionPolicy() { + int count = 0; + + protected int getCount(RepeatContext context) { + return count; + }; + + protected int doUpdate(RepeatContext context) { + super.doUpdate(context); + count++; + return 1; + } + + public RepeatContext start(RepeatContext context) { + count = 0; + return super.start(context); + } + }; + policy.setMaxCount(2); + policy.setUseParent(true); + RepeatContextSupport session = new RepeatContextSupport(null); + RepeatContext context = policy.start(session); + policy.update(context); + assertFalse(policy.isComplete(context)); + context = policy.start(session); + policy.update(context); + assertTrue(policy.isComplete(context)); + } +} diff --git a/infrastructure/src/test/java/org/springframework/batch/repeat/policy/MockCompletionPolicySupport.java b/infrastructure/src/test/java/org/springframework/batch/repeat/policy/MockCompletionPolicySupport.java new file mode 100644 index 000000000..6f1231567 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/repeat/policy/MockCompletionPolicySupport.java @@ -0,0 +1,27 @@ +/* + * 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.repeat.policy; + +import org.springframework.batch.repeat.RepeatContext; + +public class MockCompletionPolicySupport extends CompletionPolicySupport { + + public boolean isComplete(RepeatContext context) { + return false; + } + +} diff --git a/infrastructure/src/test/java/org/springframework/batch/repeat/policy/SimpleCompletionPolicyTests.java b/infrastructure/src/test/java/org/springframework/batch/repeat/policy/SimpleCompletionPolicyTests.java new file mode 100644 index 000000000..942ef34d5 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/repeat/policy/SimpleCompletionPolicyTests.java @@ -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.repeat.policy; + +import junit.framework.TestCase; + +import org.springframework.batch.repeat.RepeatContext; + +public class SimpleCompletionPolicyTests extends TestCase { + + SimpleCompletionPolicy policy = new SimpleCompletionPolicy(); + + RepeatContext context; + + Object dummy = "foo"; + + protected void setUp() throws Exception { + super.setUp(); + context = policy.start(null); + } + + public void testTerminationAfterDefaultSize() throws Exception { + for (int i = 0; i < SimpleCompletionPolicy.DEFAULT_CHUNK_SIZE - 1; i++) { + policy.update(context); + assertFalse(policy.isComplete(context, dummy)); + } + policy.update(context); + assertTrue(policy.isComplete(context, dummy)); + } + + public void testTerminationAfterExplicitChunkSize() throws Exception { + int chunkSize = 2; + policy.setChunkSize(chunkSize); + for (int i = 0; i < chunkSize - 1; i++) { + policy.update(context); + assertFalse(policy.isComplete(context, dummy)); + } + policy.update(context); + assertTrue(policy.isComplete(context, dummy)); + } + + public void testTerminationAfterNullResult() throws Exception { + policy.update(context); + assertFalse(policy.isComplete(context, dummy)); + policy.update(context); + assertTrue(policy.isComplete(context, null)); + } + + public void testTerminationAfterException() throws Exception { + policy.update(context); + try { + assertTrue(policy.isComplete(context, new IllegalStateException("foo"))); + } + catch (IllegalStateException e) { + assertEquals("foo", e.getMessage()); + fail("Unxpected IllegalStateException"); + } + } + + public void testReset() throws Exception { + policy.setChunkSize(2); + policy.update(context); + assertFalse(policy.isComplete(context, dummy)); + policy.update(context); + assertTrue(policy.isComplete(context, dummy)); + context = policy.start(null); + policy.update(context); + assertFalse(policy.isComplete(context, dummy)); + } +} diff --git a/infrastructure/src/test/java/org/springframework/batch/repeat/policy/TimeoutCompletionPolicyTests.java b/infrastructure/src/test/java/org/springframework/batch/repeat/policy/TimeoutCompletionPolicyTests.java new file mode 100644 index 000000000..80bd884cf --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/repeat/policy/TimeoutCompletionPolicyTests.java @@ -0,0 +1,92 @@ +/* + * 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.repeat.policy; + +import junit.framework.TestCase; + +import org.springframework.batch.repeat.RepeatContext; + +public class TimeoutCompletionPolicyTests extends TestCase { + + public void testSimpleTimeout() throws Exception { + TimeoutTerminationPolicy policy = new TimeoutTerminationPolicy(20L); + RepeatContext context = policy.start(null); + assertFalse(policy.isComplete(context)); + Thread.sleep(50L); + assertTrue(policy.isComplete(context)); + } + + public void testSuccessfulResult() throws Exception { + TimeoutTerminationPolicy policy = new TimeoutTerminationPolicy(); + RepeatContext context = policy.start(null); + assertFalse(policy.isComplete(context, null)); + } + + public void testUpdate() throws Exception { + TimeoutTerminationPolicy policy = new TimeoutTerminationPolicy(20L); + RepeatContext context = policy.start(null); + assertFalse(policy.isComplete(context)); + Thread.sleep(50L); + assertTrue(policy.isComplete(context)); + policy.update(context); + // update doesn't change completeness + assertTrue(policy.isComplete(context)); + } + + public void testException() throws Exception { + TimeoutTerminationPolicy policy = new TimeoutTerminationPolicy(); + RepeatContext context = policy.start(null); + assertFalse(policy.isComplete(context)); + try { + policy.isComplete(context, new RuntimeException("foo")); + } + catch (RuntimeException e) { + // expected + assertEquals("foo", e.getMessage()); + } + assertFalse(policy.isComplete(context)); + } + + public void testError() throws Exception { + TimeoutTerminationPolicy policy = new TimeoutTerminationPolicy(); + RepeatContext context = policy.start(null); + assertFalse(policy.isComplete(context)); + try { + policy.isComplete(context, new Error("foo")); + } + catch (Error e) { + // expected + assertEquals("foo", e.getMessage()); + } + assertFalse(policy.isComplete(context)); + } + + public void testThrowable() throws Exception { + TimeoutTerminationPolicy policy = new TimeoutTerminationPolicy(); + RepeatContext context = policy.start(null); + assertFalse(policy.isComplete(context)); + try { + policy.isComplete(context, new Throwable("foo")); + } + catch (Throwable e) { + // expected + assertEquals("foo", e.getCause().getMessage()); + } + assertFalse(policy.isComplete(context)); + } + +} diff --git a/infrastructure/src/test/java/org/springframework/batch/repeat/support/AbstractTradeBatchTests.java b/infrastructure/src/test/java/org/springframework/batch/repeat/support/AbstractTradeBatchTests.java new file mode 100644 index 000000000..8f265abb8 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/repeat/support/AbstractTradeBatchTests.java @@ -0,0 +1,75 @@ +/* + * 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.repeat.support; + +import junit.framework.TestCase; + +import org.springframework.batch.io.file.FieldSet; +import org.springframework.batch.io.file.support.SimpleFlatFileInputSource; +import org.springframework.batch.item.ItemProcessor; +import org.springframework.batch.item.provider.AbstractFieldSetItemProvider; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; + +/** + * Base class for simple tests with small trade data set. + * + * @author Dave Syer + * + */ +public abstract class AbstractTradeBatchTests extends TestCase { + + public static final int NUMBER_OF_ITEMS = 5; + + Resource resource = new ClassPathResource("trades.csv", getClass()); + + protected TradeProcessor executor = new TradeProcessor(); + + protected TradeItemProvider provider; + + protected void setUp() throws Exception { + super.setUp(); + provider = new TradeItemProvider(resource); + } + + protected static class TradeItemProvider extends AbstractFieldSetItemProvider { + + protected TradeItemProvider(Resource resource) throws Exception { + super(); + SimpleFlatFileInputSource template = new SimpleFlatFileInputSource(); + template.setResource(resource); + template.afterPropertiesSet(); + setSource(template); + } + + protected Object transform(FieldSet fieldSet) { + return new Trade(fieldSet); + } + } + + protected static class TradeProcessor implements ItemProcessor { + int count = 0; + + // This has to be synchronized because we are going to test the state + // (count) at the end of a concurrent batch run. + public synchronized void process(Object data) { + count++; + System.out.println("Executing trade '" + data + "'"); + } + } + +} diff --git a/infrastructure/src/test/java/org/springframework/batch/repeat/support/AsynchronousRepeatTests.java b/infrastructure/src/test/java/org/springframework/batch/repeat/support/AsynchronousRepeatTests.java new file mode 100644 index 000000000..8b01b7f83 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/repeat/support/AsynchronousRepeatTests.java @@ -0,0 +1,103 @@ +/* + * 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.repeat.support; + +import java.util.HashSet; +import java.util.Set; + +import org.springframework.batch.repeat.RepeatCallback; +import org.springframework.batch.repeat.RepeatContext; +import org.springframework.batch.repeat.ExitStatus; +import org.springframework.batch.repeat.callback.ItemProviderRepeatCallback; +import org.springframework.core.task.SimpleAsyncTaskExecutor; + +public class AsynchronousRepeatTests extends AbstractTradeBatchTests { + + /** + * Run a batch with a single template that itself has an asynch task + * executor. The result is a batch that runs in multiple threads (up to the + * throttle limit of the template). + * + * @throws Exception + */ + public void testMultiThreadAsynchronousExecution() throws Exception { + TaskExecutorRepeatTemplate template = new TaskExecutorRepeatTemplate(); + template.setTaskExecutor(new SimpleAsyncTaskExecutor()); + + final String threadName = Thread.currentThread().getName(); + final Set threadNames = new HashSet(); + + final RepeatCallback callback = new ItemProviderRepeatCallback(provider, executor) { + public ExitStatus doInIteration(RepeatContext context) throws Exception { + assertNotSame(threadName, Thread.currentThread().getName()); + threadNames.add(Thread.currentThread().getName()); + Thread.sleep(100); + return super.doInIteration(context); + } + }; + + template.iterate(callback); + // Shouldn't be necessary to wait: + // Thread.sleep(500); + assertEquals(NUMBER_OF_ITEMS, executor.count); + assertTrue(threadNames.size() > 1); + } + + /** + * Wrap an otherwise synchronous batch in a callback to an asynchronous + * template. + * + * @throws Exception + */ + public void testSingleThreadAsynchronousExecution() throws Exception { + TaskExecutorRepeatTemplate jobTemplate = new TaskExecutorRepeatTemplate(); + final RepeatTemplate stepTemplate = new RepeatTemplate(); + SimpleAsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor(); + jobTemplate.setTaskExecutor(taskExecutor); + + final String threadName = Thread.currentThread().getName(); + final Set threadNames = new HashSet(); + + final RepeatCallback stepCallback = new ItemProviderRepeatCallback(provider, executor) { + public ExitStatus doInIteration(RepeatContext context) throws Exception { + assertNotSame(threadName, Thread.currentThread().getName()); + threadNames.add(Thread.currentThread().getName()); + Thread.sleep(100); + return super.doInIteration(context); + } + }; + RepeatCallback jobCallback = new RepeatCallback() { + public ExitStatus doInIteration(RepeatContext context) throws Exception { + stepTemplate.iterate(stepCallback); + return ExitStatus.FINISHED; + } + }; + + jobTemplate.iterate(jobCallback); + // Shouldn't be necessary to wait: + // Thread.sleep(500); + assertEquals(NUMBER_OF_ITEMS, executor.count); + // Because of the throttling and queing internally to a TaskExecutor, + // more than one thread wil be used - the number used is (as of writing) + // one less than the throttle limit of the template. + // TODO: see if we can get it to use only one thread? + assertTrue(threadNames.size() >= 1); + } + + // TODO: test transactional callback with asynch template. + +} diff --git a/infrastructure/src/test/java/org/springframework/batch/repeat/support/ChunkedRepeatTests.java b/infrastructure/src/test/java/org/springframework/batch/repeat/support/ChunkedRepeatTests.java new file mode 100644 index 000000000..24d6f0092 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/repeat/support/ChunkedRepeatTests.java @@ -0,0 +1,180 @@ +/* + * 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.repeat.support; + +import org.springframework.batch.item.ItemProvider; +import org.springframework.batch.item.provider.AbstractItemProvider; +import org.springframework.batch.repeat.RepeatCallback; +import org.springframework.batch.repeat.RepeatContext; +import org.springframework.batch.repeat.ExitStatus; +import org.springframework.batch.repeat.callback.ItemProviderRepeatCallback; +import org.springframework.batch.repeat.callback.NestedRepeatCallback; +import org.springframework.batch.repeat.policy.SimpleCompletionPolicy; +import org.springframework.core.task.SimpleAsyncTaskExecutor; + +/** + * Test various approaches to chunking of a batch. Not really a unit test, but + * it should be fast. + * + * @author Dave Syer + * + */ +public class ChunkedRepeatTests extends AbstractTradeBatchTests { + + int count = 0; + + /** + * Chunking using a dedicated TerminationPolicy. Transactions would be laid + * on at the level of chunkTemplate.execute() or the surrounding callback. + * + * @throws Exception + */ + public void testChunkedBatchWithTerminationPolicy() throws Exception { + + RepeatTemplate repeatTemplate = new RepeatTemplate(); + final RepeatCallback callback = new ItemProviderRepeatCallback(provider, executor); + + final RepeatTemplate chunkTemplate = new RepeatTemplate(); + // The policy is resettable so we only have to resolve this dependency + // once + chunkTemplate.setCompletionPolicy(new SimpleCompletionPolicy(2)); + + ExitStatus result = repeatTemplate.iterate(new NestedRepeatCallback(chunkTemplate, callback) { + + public ExitStatus doInIteration(RepeatContext context) throws Exception { + count++; // for test assertion + return super.doInIteration(context); + } + + }); + + assertEquals(NUMBER_OF_ITEMS, executor.count); + // The chunk executes 3 times because the last one + // returns false. We terminate the main batch when + // we encounter a partially empty chunk. + assertEquals(3, count); + assertFalse(result.isContinuable()); + + } + + /** + * Chunking with an asynchronous taskExecutor in the chunks. Transactions + * have to be at the level of the business callback. + * + * @throws Exception + */ + public void testAsynchronousChunkedBatchWithTerminationPolicy() throws Exception { + + RepeatTemplate repeatTemplate = new RepeatTemplate(); + final RepeatCallback callback = new ItemProviderRepeatCallback(provider, executor); + + final TaskExecutorRepeatTemplate chunkTemplate = new TaskExecutorRepeatTemplate(); + // The policy is resettable so we only have to resolve this dependency + // once + chunkTemplate.setCompletionPolicy(new SimpleCompletionPolicy(2)); + chunkTemplate.setTaskExecutor(new SimpleAsyncTaskExecutor()); + + ExitStatus result = repeatTemplate.iterate(new NestedRepeatCallback(chunkTemplate, callback) { + + public ExitStatus doInIteration(RepeatContext context) throws Exception { + count++; // for test assertion + return super.doInIteration(context); + } + + }); + + assertEquals(NUMBER_OF_ITEMS, executor.count); + assertFalse(result.isContinuable()); + assertEquals(3, count); + + } + + /** + * Explicit chunking of input data. Transactions would be laid on at the + * level of template.execute(). + * + * @throws Exception + */ + public void testChunksWithTruncatedItemProvider() throws Exception { + + RepeatTemplate template = new RepeatTemplate(); + + // This pattern would work with an asynchronous callback as well + // (but non-transactional in that case). + + class Chunker { + boolean ready = false; + + int count = 0; + + void set() { + ready = true; + } + + boolean ready() { + return ready; + } + + boolean first() { + return count == 0; + } + + void reset() { + count = 0; + ready = false; + } + + void increment() { + count++; + } + } + ; + + final Chunker chunker = new Chunker(); + + while (!chunker.ready()) { + + ItemProvider truncated = new AbstractItemProvider() { + int count = 0; + + public Object next() throws Exception { + if (count++ < 2) + return provider.next(); + return null; + } + }; + chunker.reset(); + template.iterate(new ItemProviderRepeatCallback(truncated, executor) { + + public ExitStatus doInIteration(RepeatContext context) throws Exception { + ExitStatus result = super.doInIteration(context); + if (!result.isContinuable() && chunker.first()) { + chunker.set(); + } + chunker.increment(); + return result; + } + + }); + + } + + assertEquals(NUMBER_OF_ITEMS, executor.count); + + } + +} diff --git a/infrastructure/src/test/java/org/springframework/batch/repeat/support/SimpleRepeatTemplateTests.java b/infrastructure/src/test/java/org/springframework/batch/repeat/support/SimpleRepeatTemplateTests.java new file mode 100644 index 000000000..002af0b20 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/repeat/support/SimpleRepeatTemplateTests.java @@ -0,0 +1,391 @@ +/* + * 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.repeat.support; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.springframework.batch.repeat.RepeatCallback; +import org.springframework.batch.repeat.RepeatContext; +import org.springframework.batch.repeat.ExitStatus; +import org.springframework.batch.repeat.callback.ItemProviderRepeatCallback; +import org.springframework.batch.repeat.callback.NestedRepeatCallback; +import org.springframework.batch.repeat.context.RepeatContextSupport; +import org.springframework.batch.repeat.exception.handler.ExceptionHandler; +import org.springframework.batch.repeat.policy.CompletionPolicySupport; +import org.springframework.batch.repeat.policy.SimpleCompletionPolicy; +import org.springframework.batch.repeat.synch.RepeatSynchronizationManager; + +/** + * @author Dave Syer + */ +public class SimpleRepeatTemplateTests extends AbstractTradeBatchTests { + + RepeatTemplate template = getRepeatTemplate(); + + int count = 0; + + public RepeatTemplate getRepeatTemplate() { + return new RepeatTemplate(); + } + + public void testExecute() throws Exception { + template.iterate(new ItemProviderRepeatCallback(provider, executor)); + assertEquals(NUMBER_OF_ITEMS, executor.count); + } + + /** + * Check that a dedicated TerminationPolicy can terminate the batch. + * + * @throws Exception + */ + public void testEarlyCompletionWithPolicy() throws Exception { + + template.setCompletionPolicy(new SimpleCompletionPolicy(2)); + + template.iterate(new ItemProviderRepeatCallback(provider, executor)); + + assertEquals(2, executor.count); + + } + + /** + * Check that a dedicated TerminationPolicy can terminate the batch. + * + * @throws Exception + */ + public void testEarlyCompletionWithException() throws Exception { + + try { + template.iterate(new RepeatCallback() { + public ExitStatus doInIteration(RepeatContext context) throws Exception { + count++; + throw new IllegalStateException("foo!"); + } + }); + fail("Expected IllegalStateException"); + } + catch (IllegalStateException e) { + assertEquals("foo!", e.getMessage()); + } + + assertEquals(1, count); + + } + + /** + * Check that the context is closed. + * + * @throws Exception + */ + public void testContextClosedOnNormalCompletion() throws Exception { + + final List list = new ArrayList(); + + final RepeatContext context = new RepeatContextSupport(null) { + public void close() { + super.close(); + list.add("close"); + } + }; + template.setCompletionPolicy(new CompletionPolicySupport() { + public RepeatContext start(RepeatContext c) { + return context; + } + }); + template.iterate(new RepeatCallback() { + public ExitStatus doInIteration(RepeatContext context) throws Exception { + count++; + return new ExitStatus(count < 1); + } + }); + + assertEquals(1, count); + assertEquals(1, list.size()); + + } + + /** + * Check that the context is closed. + * + * @throws Exception + */ + public void testContextClosedOnAbnormalCompletion() throws Exception { + + final List list = new ArrayList(); + + final RepeatContext context = new RepeatContextSupport(null) { + public void close() { + super.close(); + list.add("close"); + } + }; + template.setCompletionPolicy(new CompletionPolicySupport() { + public RepeatContext start(RepeatContext c) { + return context; + } + }); + + try { + template.iterate(new RepeatCallback() { + public ExitStatus doInIteration(RepeatContext context) throws Exception { + count++; + throw new RuntimeException("foo"); + } + }); + } + catch (RuntimeException e) { + assertEquals("foo", e.getMessage()); + } + + assertEquals(1, count); + assertEquals(1, list.size()); + + } + + /** + * Check that the exception handler is called. + * + * @throws Exception + */ + public void testExceptionHandlerCalledOnAbnormalCompletion() throws Exception { + + final List list = new ArrayList(); + + template.setExceptionHandler(new ExceptionHandler() { + public void handleExceptions(RepeatContext context, Collection throwables) { + list.addAll(throwables); + } + }); + + try { + template.iterate(new RepeatCallback() { + public ExitStatus doInIteration(RepeatContext context) throws Exception { + count++; + throw new RuntimeException("foo"); + } + }); + } + catch (RuntimeException e) { + assertEquals("foo", e.getMessage()); + } + + assertEquals(1, count); + assertEquals(1, list.size()); + + } + + /** + * Check that a the context can be used to signal early completion. + * + * @throws Exception + */ + public void testEarlyCompletionWithContext() throws Exception { + + ExitStatus result = template.iterate(new ItemProviderRepeatCallback(provider, executor) { + + public ExitStatus doInIteration(RepeatContext context) throws Exception { + ExitStatus result = super.doInIteration(context); + if (executor.count >= 2) { + context.setCompleteOnly(); + // If we return null the batch will terminate anyway + // without an exception... + } + return result; + } + }); + + // 2 items were processed before completion signalled + assertEquals(2, executor.count); + + // Not all items processed + assertTrue(result.isContinuable()); + + } + + /** + * Check that a the context can be used to signal early completion. + * + * @throws Exception + */ + public void testEarlyCompletionWithContextTerminated() throws Exception { + + ExitStatus result = template.iterate(new ItemProviderRepeatCallback(provider, executor) { + + public ExitStatus doInIteration(RepeatContext context) throws Exception { + ExitStatus result = super.doInIteration(context); + if (executor.count >= 2) { + context.setTerminateOnly(); + // If we return null the batch will terminate anyway + // without an exception... + } + return result; + } + }); + + // 2 items were processed before completion signalled + assertEquals(2, executor.count); + + // Not all items processed + assertTrue(result.isContinuable()); + + } + + public void testNestedSession() throws Exception { + RepeatTemplate outer = getRepeatTemplate(); + RepeatTemplate inner = getRepeatTemplate(); + outer.iterate(new NestedRepeatCallback(inner, new RepeatCallback() { + public ExitStatus doInIteration(RepeatContext context) throws Exception { + count++; + assertNotNull(context); + assertNotSame("Nested batch should have new session", context, context.getParent()); + assertSame(context, RepeatSynchronizationManager.getContext()); + return ExitStatus.FINISHED; + } + }) { + public ExitStatus doInIteration(RepeatContext context) throws Exception { + count++; + // TODO parameter is rewritten and then compared to value it has + // just been assigned + context = RepeatSynchronizationManager.getContext(); + assertSame(context, RepeatSynchronizationManager.getContext()); + return super.doInIteration(context); + } + }); + assertEquals(2, count); + } + + public void testNestedSessionTerminatesBeforeIteration() throws Exception { + RepeatTemplate outer = getRepeatTemplate(); + RepeatTemplate inner = getRepeatTemplate(); + outer.iterate(new NestedRepeatCallback(inner, new RepeatCallback() { + public ExitStatus doInIteration(RepeatContext context) throws Exception { + count++; + assertEquals(2, count); + fail("Nested batch should not have been executed"); + return ExitStatus.FINISHED; + } + }) { + public ExitStatus doInIteration(RepeatContext context) throws Exception { + count++; + context.setCompleteOnly(); + return super.doInIteration(context); + } + }); + assertEquals(1, count); + } + + public void testOuterContextPreserved() throws Exception { + RepeatTemplate outer = getRepeatTemplate(); + outer.setCompletionPolicy(new SimpleCompletionPolicy(2)); + RepeatTemplate inner = getRepeatTemplate(); + outer.iterate(new NestedRepeatCallback(inner, new RepeatCallback() { + public ExitStatus doInIteration(RepeatContext context) throws Exception { + count++; + assertNotNull(context); + assertNotSame("Nested batch should have new session", context, context.getParent()); + assertSame(context, RepeatSynchronizationManager.getContext()); + return ExitStatus.FINISHED; + } + }) { + public ExitStatus doInIteration(RepeatContext context) throws Exception { + count++; + // TODO parameter is rewritten and then compared to value it has + // just been assigned + context = RepeatSynchronizationManager.getContext(); + assertSame(context, RepeatSynchronizationManager.getContext()); + super.doInIteration(context); + return ExitStatus.CONTINUABLE; + } + }); + assertEquals(4, count); + } + + /** + * Test that a result is returned from the batch. + * @throws Exception + */ + public void testResult() throws Exception { + ExitStatus result = template.iterate(new ItemProviderRepeatCallback(provider, executor)); + assertEquals(NUMBER_OF_ITEMS, executor.count); + // We are complete - do not expect to be called again + assertFalse(result.isContinuable()); + } + + public void testExceptionThrownOnLastItem() throws Exception { + template.setCompletionPolicy(new SimpleCompletionPolicy(2)); + try { + template.iterate(new RepeatCallback() { + public ExitStatus doInIteration(RepeatContext context) throws Exception { + count++; + if (count < 2) { + return ExitStatus.CONTINUABLE; + } + throw new RuntimeException("Barf second try count=" + count); + } + }); + fail("Expected exception on last item in batch"); + } + catch (Exception e) { + // expected + assertEquals("Barf second try count=2", e.getMessage()); + } + } + + /** + * Check that a the session can be used to signal early completion, but an + * exception takes precedence. + * + * @throws Exception + */ + public void testEarlyCompletionWithSessionAndException() throws Exception { + + template.setCompletionPolicy(new SimpleCompletionPolicy(4)); + + ExitStatus result = ExitStatus.FINISHED; + + try { + result = template.iterate(new ItemProviderRepeatCallback(provider, executor) { + + public ExitStatus doInIteration(RepeatContext context) throws Exception { + ExitStatus result = super.doInIteration(context); + if (executor.count >= 2) { + context.setCompleteOnly(); + throw new RuntimeException("Barf second try count=" + executor.count); + } + return result; + } + }); + fail("Expected exception on last item in batch"); + } + catch (RuntimeException e) { + // expected + assertEquals("Barf second try count=2", e.getMessage()); + } + + // 2 items were processed before completion signalled + assertEquals(2, executor.count); + + System.err.println(result); + + // An exception was thrown by the template so result is still false + assertFalse(result.isContinuable()); + + } + +} diff --git a/infrastructure/src/test/java/org/springframework/batch/repeat/support/TaskExecutorRepeatTemplateTests.java b/infrastructure/src/test/java/org/springframework/batch/repeat/support/TaskExecutorRepeatTemplateTests.java new file mode 100644 index 000000000..ee9e3efae --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/repeat/support/TaskExecutorRepeatTemplateTests.java @@ -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.repeat.support; + + +/** + * @author Dave Syer + */ +public class TaskExecutorRepeatTemplateTests extends SimpleRepeatTemplateTests { + + public RepeatTemplate getRepeatTemplate() { + return new TaskExecutorRepeatTemplate(); + } + + public void testSetThrottleLimit() throws Exception { + try { + new TaskExecutorRepeatTemplate().setThrottleLimit(-1); + } catch (Exception e) { + // unexpected - no check for illegal values + fail("Unexpected Exception setting throttle limit"); + } + } + +} diff --git a/infrastructure/src/test/java/org/springframework/batch/repeat/support/Trade.java b/infrastructure/src/test/java/org/springframework/batch/repeat/support/Trade.java new file mode 100644 index 000000000..5aead930e --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/repeat/support/Trade.java @@ -0,0 +1,55 @@ +/* + * 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.repeat.support; + +import java.math.BigDecimal; + +import org.springframework.batch.io.file.FieldSet; + +/** + * @author Rob Harrop + */ +public class Trade { + + private String isin; + + private long quantity; + + private BigDecimal price; + + Trade(FieldSet fieldSet) { + this.isin = fieldSet.readString(0); + this.quantity = fieldSet.readLong(1); + this.price = fieldSet.readBigDecimal(2); + } + + public String getIsin() { + return isin; + } + + public BigDecimal getPrice() { + return price; + } + + public long getQuantity() { + return quantity; + } + + public String toString() { + return "Trade: [isin=" + isin + ",quantity=" + quantity + ",price=" + price + "]"; + } +} diff --git a/infrastructure/src/test/java/org/springframework/batch/repeat/synch/BatchTransactionSynchronizationManagerTests.java b/infrastructure/src/test/java/org/springframework/batch/repeat/synch/BatchTransactionSynchronizationManagerTests.java new file mode 100644 index 000000000..671f5694c --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/repeat/synch/BatchTransactionSynchronizationManagerTests.java @@ -0,0 +1,143 @@ +/* + * 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.repeat.synch; + +import java.util.List; + +import junit.framework.TestCase; + +import org.springframework.batch.repeat.context.RepeatContextSupport; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationAdapter; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +public class BatchTransactionSynchronizationManagerTests extends TestCase { + + private TransactionSynchronization synchronization = new TransactionSynchronizationAdapter() { + }; + + protected void setUp() throws Exception { + super.setUp(); + BatchTransactionSynchronizationManager.clearSynchronizations(); + if (TransactionSynchronizationManager.isSynchronizationActive()) { + TransactionSynchronizationManager.clearSynchronization(); + } + TransactionSynchronizationManager.initSynchronization(); + RepeatSynchronizationManager.register(new RepeatContextSupport(null)); + } + + /* + * (non-Javadoc) + * @see junit.framework.TestCase#tearDown() + */ + protected void tearDown() throws Exception { + super.tearDown(); + RepeatSynchronizationManager.clear(); + } + + public void testRegisterWhenContextMissing() throws Exception { + RepeatSynchronizationManager.clear(); + try { + BatchTransactionSynchronizationManager.registerSynchronization(synchronization); + List synchronizations = TransactionSynchronizationManager.getSynchronizations(); + assertEquals(1, synchronizations.size()); + } + catch (IllegalStateException e) { + fail("Unexpected IllegalStateException"); + // Unexpected - + } + } + + public void testRegisterSynchronization() { + + BatchTransactionSynchronizationManager.registerSynchronization(synchronization); + + // There should be only one transaction synchronization object in the + // list. + List synchronizations = TransactionSynchronizationManager.getSynchronizations(); + assertEquals(1, synchronizations.size()); + assertSame(synchronizations.get(0), synchronization); + + if (RepeatSynchronizationManager.getContext() != null) { + assertEquals(1, RepeatSynchronizationManager.getContext().attributeNames().length); + } + } + + public void testRegisterSynchronizationWithParentContext() { + + RepeatSynchronizationManager.register(new RepeatContextSupport(RepeatSynchronizationManager.getContext())); + + BatchTransactionSynchronizationManager.registerSynchronization(synchronization); + + // There should be only one transaction synchronization object in the + // list. + List synchronizations = TransactionSynchronizationManager.getSynchronizations(); + assertEquals(1, synchronizations.size()); + assertSame(synchronizations.get(0), synchronization); + + assertEquals(0, RepeatSynchronizationManager.getContext().attributeNames().length); + assertEquals(1, RepeatSynchronizationManager.getContext().getParent().attributeNames().length); + } + + public void testSynchronizeTwiceWithSameObject() throws Exception { + BatchTransactionSynchronizationManager.registerSynchronization(synchronization); + testRegisterSynchronization(); + } + + public void testSynchronizeTwiceWithSameObjectAndNoContext() throws Exception { + RepeatSynchronizationManager.clear(); + BatchTransactionSynchronizationManager.registerSynchronization(synchronization); + testRegisterSynchronization(); + } + + public void testReregisterSynchronization() { + BatchTransactionSynchronizationManager.registerSynchronization(synchronization); + TransactionSynchronizationManager.clearSynchronization(); + + TransactionSynchronizationManager.initSynchronization(); + BatchTransactionSynchronizationManager.resynchronize(); + + // There should be only one transaction synchronization object in the + // list. + List synchronizations = TransactionSynchronizationManager.getSynchronizations(); + assertEquals(1, synchronizations.size()); + assertSame(synchronizations.get(0), synchronization); + + } + + public void testResynchronizeWithNoSynchronizations() throws Exception { + BatchTransactionSynchronizationManager.resynchronize(); + List synchronizations = TransactionSynchronizationManager.getSynchronizations(); + assertEquals(0, synchronizations.size()); + } + + public void testSynchronizeWhenNotInTransaction() throws Exception { + + TransactionSynchronizationManager.clearSynchronization(); + BatchTransactionSynchronizationManager.registerSynchronization(synchronization); + TransactionSynchronizationManager.initSynchronization(); + + BatchTransactionSynchronizationManager.resynchronize(); + + // There should be only one transaction synchronization object in the + // list. + List synchronizations = TransactionSynchronizationManager.getSynchronizations(); + assertEquals(1, synchronizations.size()); + assertSame(synchronizations.get(0), synchronization); + } + +} diff --git a/infrastructure/src/test/java/org/springframework/batch/repeat/synch/RepeatSynchronizationManagerTests.java b/infrastructure/src/test/java/org/springframework/batch/repeat/synch/RepeatSynchronizationManagerTests.java new file mode 100644 index 000000000..3f348e117 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/repeat/synch/RepeatSynchronizationManagerTests.java @@ -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.repeat.synch; + +import junit.framework.TestCase; + +import org.springframework.batch.repeat.RepeatContext; +import org.springframework.batch.repeat.context.RepeatContextSupport; +import org.springframework.batch.repeat.synch.RepeatSynchronizationManager; + +public class RepeatSynchronizationManagerTests extends TestCase { + + private RepeatContext context = new RepeatContextSupport(null); + + protected void setUp() throws Exception { + RepeatSynchronizationManager.clear(); + } + + protected void tearDown() throws Exception { + RepeatSynchronizationManager.clear(); + } + + public void testGetContext() { + RepeatSynchronizationManager.register(context); + assertEquals(context, RepeatSynchronizationManager.getContext()); + } + + public void testSetSessionCompleteOnly() { + assertNull(RepeatSynchronizationManager.getContext()); + RepeatSynchronizationManager.register(context); + assertFalse(RepeatSynchronizationManager.getContext().isCompleteOnly()); + RepeatSynchronizationManager.setCompleteOnly(); + assertTrue(RepeatSynchronizationManager.getContext().isCompleteOnly()); + } + + public void testSetSessionCompleteOnlyWithParent() { + assertNull(RepeatSynchronizationManager.getContext()); + RepeatContext child = new RepeatContextSupport(context); + RepeatSynchronizationManager.register(child); + assertFalse(child.isCompleteOnly()); + RepeatSynchronizationManager.setAncestorsCompleteOnly(); + assertTrue(child.isCompleteOnly()); + assertTrue(context.isCompleteOnly()); + } + + public void testClear() { + RepeatSynchronizationManager.register(context); + assertEquals(context, RepeatSynchronizationManager.getContext()); + RepeatSynchronizationManager.clear(); + assertEquals(null, RepeatSynchronizationManager.getContext()); + } + +} diff --git a/infrastructure/src/test/java/org/springframework/batch/retry/aop/RetryOperationsInterceptorTests.java b/infrastructure/src/test/java/org/springframework/batch/retry/aop/RetryOperationsInterceptorTests.java new file mode 100644 index 000000000..9cad3a7a6 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/retry/aop/RetryOperationsInterceptorTests.java @@ -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.retry.aop; + +import junit.framework.TestCase; + +import org.springframework.aop.framework.Advised; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.aop.target.SingletonTargetSource; +import org.springframework.batch.retry.policy.NeverRetryPolicy; +import org.springframework.batch.retry.support.RetryTemplate; + +public class RetryOperationsInterceptorTests extends TestCase { + + private RetryOperationsInterceptor interceptor; + + private Service service; + + private ServiceImpl target; + + protected void setUp() throws Exception { + super.setUp(); + interceptor = new RetryOperationsInterceptor(); + target = new ServiceImpl(); + service = (Service) ProxyFactory.getProxy(Service.class, new SingletonTargetSource(target)); + } + + public void testDefaultInterceptorSunnyDay() throws Exception { + ((Advised) service).addAdvice(interceptor); + service.service(); + assertEquals(2, target.count); + } + + public void testRetryExceptionAfterTooManyAttempts() throws Exception { + ((Advised) service).addAdvice(interceptor); + RetryTemplate template = new RetryTemplate(); + template.setRetryPolicy(new NeverRetryPolicy()); + interceptor.setRetryTemplate(template); + try { + service.service(); + fail("Expected Exception."); + } + catch (Exception e) { + assertTrue(e.getMessage().startsWith("Not enough calls")); + } + assertEquals(1, target.count); + } + + private interface Service { + void service() throws Exception; + } + + private static class ServiceImpl implements Service { + private int count = 0; + + public void service() throws Exception { + count++; + if (count < 2) { + throw new Exception("Not enough calls: " + count); + } + } + } +} diff --git a/infrastructure/src/test/java/org/springframework/batch/retry/backoff/ExponentialBackOffPolicyTests.java b/infrastructure/src/test/java/org/springframework/batch/retry/backoff/ExponentialBackOffPolicyTests.java new file mode 100644 index 000000000..f3974d9c3 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/retry/backoff/ExponentialBackOffPolicyTests.java @@ -0,0 +1,95 @@ +/* + * 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.retry.backoff; + +import junit.framework.TestCase; + +/** + * @author Rob Harrop + * @author Dave Syer + * @since 2.1 + */ +public class ExponentialBackOffPolicyTests extends TestCase { + + public void testSetMaxInterval() throws Exception { + ExponentialBackOffPolicy strategy = new ExponentialBackOffPolicy(); + strategy.setMaxInterval(1000); + assertTrue(strategy.toString().indexOf("maxInterval=1000") >= 0); + strategy.setMaxInterval(0); + // The minimum value for the max interval is 1 + assertTrue(strategy.toString().indexOf("maxInterval=1") >= 0); + } + + public void testSetInitialInterval() throws Exception { + ExponentialBackOffPolicy strategy = new ExponentialBackOffPolicy(); + strategy.setInitialInterval(10000); + assertTrue(strategy.toString().indexOf("initialInterval=10000,") >= 0); + strategy.setInitialInterval(0); + assertTrue(strategy.toString().indexOf("initialInterval=1,") >= 0); + } + + public void testSetMultiplier() throws Exception { + ExponentialBackOffPolicy strategy = new ExponentialBackOffPolicy(); + strategy.setMultiplier(3.); + assertTrue(strategy.toString().indexOf("multiplier=3.") >= 0); + strategy.setMultiplier(.5); + assertTrue(strategy.toString().indexOf("multiplier=1.") >= 0); + } + + public void testSingleBackOff() throws Exception { + ExponentialBackOffPolicy strategy = new ExponentialBackOffPolicy(); + BackOffContext context = strategy.start(null); + long before = System.currentTimeMillis(); + strategy.backOff(context); + long after = System.currentTimeMillis(); + assertEqualsApprox(ExponentialBackOffPolicy.DEFAULT_INITIAL_INTERVAL, after - before, 30); + } + + public void testMaximumBackOff() throws Exception { + ExponentialBackOffPolicy strategy = new ExponentialBackOffPolicy(); + strategy.setMaxInterval(50); + BackOffContext context = strategy.start(null); + long before = System.currentTimeMillis(); + strategy.backOff(context); + long after = System.currentTimeMillis(); + assertEqualsApprox(50L, after - before, 15); + } + + public void testMultiBackOff() throws Exception { + ExponentialBackOffPolicy strategy = new ExponentialBackOffPolicy(); + long seed = 40; // not too small or Windoze won't resolve the difference + double multiplier = 1.2; // not too large or the test takes ages! + strategy.setInitialInterval(seed); + strategy.setMultiplier(multiplier); + BackOffContext context = strategy.start(null); + for (int x = 0; x < 5; x++) { + long before = System.currentTimeMillis(); + strategy.backOff(context); + long after = System.currentTimeMillis(); + assertFalse(after == before); + assertEqualsApprox(seed, after - before, 20); + seed *= multiplier; + } + } + + private void assertEqualsApprox(long desired, long actual, long variance) { + long lower = desired - variance; + long upper = desired + 2 * variance; + assertTrue("Expected value to be between '" + lower + "' and '" + upper + "' but was '" + actual + "'", + lower <= actual && actual <= upper); + } +} diff --git a/infrastructure/src/test/java/org/springframework/batch/retry/backoff/FixedBackOffPolicyTests.java b/infrastructure/src/test/java/org/springframework/batch/retry/backoff/FixedBackOffPolicyTests.java new file mode 100644 index 000000000..b25923369 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/retry/backoff/FixedBackOffPolicyTests.java @@ -0,0 +1,66 @@ +/* + * 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.retry.backoff; + +import junit.framework.TestCase; + +/** + * @author Rob Harrop + * @author Dave Syer + * @since 2.1 + */ +public class FixedBackOffPolicyTests extends TestCase { + + public void testSetBackoffPeriodNegative() throws Exception { + FixedBackOffPolicy strategy = new FixedBackOffPolicy(); + strategy.setBackOffPeriod(-1000L); + long before = System.currentTimeMillis(); + strategy.backOff(null); + long after = System.currentTimeMillis(); + // We should see a zero backoff if we try to set it negative + assertEqualsApprox(0, after - before, 25); + } + + public void testSingleBackOff() throws Exception { + int backOffPeriod = 50; + FixedBackOffPolicy strategy = new FixedBackOffPolicy(); + strategy.setBackOffPeriod(backOffPeriod); + long before = System.currentTimeMillis(); + strategy.backOff(null); + long after = System.currentTimeMillis(); + assertEqualsApprox(backOffPeriod, after - before, 25); + } + + public void testManyBackOffCalls() throws Exception { + int backOffPeriod = 50; + FixedBackOffPolicy strategy = new FixedBackOffPolicy(); + strategy.setBackOffPeriod(backOffPeriod); + for (int x = 0; x < 10; x++) { + long before = System.currentTimeMillis(); + strategy.backOff(null); + long after = System.currentTimeMillis(); + assertEqualsApprox(backOffPeriod, after - before, 25); + } + } + + private void assertEqualsApprox(long desired, long actual, long variance) { + long lower = desired - variance; + long upper = desired + 2 * variance; + assertTrue("Expected value to be between '" + lower + "' and '" + upper + "' but was '" + actual + "'", + lower <= actual && actual <= upper); + } +} diff --git a/infrastructure/src/test/java/org/springframework/batch/retry/callback/ItemProviderRetryCallbackTests.java b/infrastructure/src/test/java/org/springframework/batch/retry/callback/ItemProviderRetryCallbackTests.java new file mode 100644 index 000000000..f2b88ad06 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/retry/callback/ItemProviderRetryCallbackTests.java @@ -0,0 +1,168 @@ +/* + * 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.retry.callback; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import junit.framework.TestCase; + +import org.springframework.batch.item.ItemProcessor; +import org.springframework.batch.item.provider.ListItemProvider; +import org.springframework.batch.retry.RetryContext; +import org.springframework.batch.retry.context.RetryContextSupport; +import org.springframework.batch.retry.exception.RetryException; +import org.springframework.batch.retry.exception.TerminatedRetryException; +import org.springframework.batch.retry.policy.NeverRetryPolicy; +import org.springframework.batch.retry.support.RetryTemplate; + +public class ItemProviderRetryCallbackTests extends TestCase { + + List calls = new ArrayList(); + + int count = 0; + + RetryTemplate template; + + ListItemProvider provider; + + ItemProviderRetryCallback callback; + + protected void setUp() throws Exception { + super.setUp(); + template = new RetryTemplate(); + provider = new ListItemProvider(Arrays.asList(new String[] { "foo", "bar" })) { + public boolean recover(Object data, Throwable cause) { + count++; + calls.add(data); + return true; + } + + public Object getKey(Object item) { + return "key" + (count++); + } + }; + callback = new ItemProviderRetryCallback(provider, new ItemProcessor() { + public void process(Object data) { + count++; + if (data.equals("bar")) { + throw new IllegalStateException("Bar detected"); + } + } + }); + } + + public void testDoWithRetrySuccessfulFirstTime() throws Exception { + template.execute(callback); + assertEquals(1, count); + } + + public void testDataExhausted() throws Exception { + provider.next(); + provider.next(); // line up a null data item... + + try { + template.execute(callback); + } + catch (RetryException e) { + fail("Unexpected RetryException"); + } + + // The item is null, and is not processed: + assertEquals(0, count); + } + + public void testContextInitializedWithItemAndCanRetry() throws Exception { + // We can use the policy to intercept the context and do something with + // the item... + provider.next(); // line up an unsuccessful call... + assertEquals(0, calls.size()); + template.setRetryPolicy(new NeverRetryPolicy() { + public boolean canRetry(RetryContext context) { + // ...register the failed item + calls.add("item(" + count + ")=" + context.getAttribute(ItemProviderRetryCallback.ITEM)); + // Do not call the base class method - the attempt counts as + // successful now + if (count < 2) // only retry once + return true; + return false; + } + }); + try { + template.execute(callback); + fail("Expected IllegalStateException"); + } + catch (IllegalStateException e) { + // expected + } + System.err.println(calls); + assertEquals(2, count); + // Two from initial attempt (one in shouldRethrow and one at start of + // do loop), two from the final attempt (same)... + assertEquals(4, calls.size()); + assertEquals("item(1)=bar", calls.get(1)); + } + + public void testContextInitializedWithItemAndRegisterThrowable() throws Exception { + // We can use the policy to intercept the context and do something with + // the item... + provider.next(); // line up an unsuccessful call... + assertEquals(0, calls.size()); + template.setRetryPolicy(new NeverRetryPolicy() { + public void registerThrowable(RetryContext context, Throwable throwable) throws TerminatedRetryException { + // ...register the failed item + calls.add("item=" + context.getAttribute(ItemProviderRetryCallback.ITEM)); + // Call the base class method so that the next attempt is a + // failure. + super.registerThrowable(context, throwable); + } + }); + try { + template.execute(callback); + fail("Expected IllegalStateException"); + } + catch (IllegalStateException e) { + // expected + } + // One call from the callback itslef and one from the retry policy + assertEquals(1, count); + assertEquals(1, calls.size()); + assertEquals("item=bar", calls.get(0)); + } + + public void testContextMarkedExhausted() throws Exception { + RetryContext context = new RetryContextSupport(null); + context.setExhaustedOnly(); + try { + callback.doWithRetry(context); + } + catch (Throwable t) { + assertTrue(t instanceof RetryException); + } + } + + public void testGetKey() throws Exception { + assertEquals("key0", callback.getProvider().getKey("foo")); + } + + public void testRecoverWithoutSession() throws Exception { + callback.getProvider().recover("foo", null); + assertEquals(1, count); + assertEquals(1, calls.size()); + } +} diff --git a/infrastructure/src/test/java/org/springframework/batch/retry/exception/AbstractExceptionTests.java b/infrastructure/src/test/java/org/springframework/batch/retry/exception/AbstractExceptionTests.java new file mode 100644 index 000000000..f432c03f7 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/retry/exception/AbstractExceptionTests.java @@ -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.retry.exception; + +import junit.framework.TestCase; + +public abstract class AbstractExceptionTests extends TestCase { + + public void testExceptionString() throws Exception { + Exception exception = getException("foo"); + assertEquals("foo", exception.getMessage()); + } + + public void testExceptionStringThrowable() throws Exception { + Exception exception = getException("foo", new IllegalStateException()); + assertEquals("foo", exception.getMessage().substring(0, 3)); + } + + public abstract Exception getException(String msg) throws Exception; + + public abstract Exception getException(String msg, Throwable t) throws Exception; +} diff --git a/infrastructure/src/test/java/org/springframework/batch/retry/exception/BackOffInterruptedExceptionTests.java b/infrastructure/src/test/java/org/springframework/batch/retry/exception/BackOffInterruptedExceptionTests.java new file mode 100644 index 000000000..08a4658e7 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/retry/exception/BackOffInterruptedExceptionTests.java @@ -0,0 +1,32 @@ +/* + * 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.retry.exception; + +public class BackOffInterruptedExceptionTests extends AbstractExceptionTests { + + public Exception getException(String msg) throws Exception { + return new BackOffInterruptedException(msg); + } + + public Exception getException(String msg, Throwable t) throws Exception { + return new BackOffInterruptedException(msg, t); + } + + public void testNothing() throws Exception { + // fool coverage tools... + } +} diff --git a/infrastructure/src/test/java/org/springframework/batch/retry/exception/ExhaustedRetryExceptionTests.java b/infrastructure/src/test/java/org/springframework/batch/retry/exception/ExhaustedRetryExceptionTests.java new file mode 100644 index 000000000..653ed60a0 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/retry/exception/ExhaustedRetryExceptionTests.java @@ -0,0 +1,32 @@ +/* + * 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.retry.exception; + +public class ExhaustedRetryExceptionTests extends AbstractExceptionTests { + + public Exception getException(String msg) throws Exception { + return new ExhaustedRetryException(msg); + } + + public Exception getException(String msg, Throwable t) throws Exception { + return new ExhaustedRetryException(msg, t); + } + + public void testNothing() throws Exception { + // fool coverage tools... + } +} diff --git a/infrastructure/src/test/java/org/springframework/batch/retry/exception/RetryExceptionTests.java b/infrastructure/src/test/java/org/springframework/batch/retry/exception/RetryExceptionTests.java new file mode 100644 index 000000000..a0b62fcf1 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/retry/exception/RetryExceptionTests.java @@ -0,0 +1,32 @@ +/* + * 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.retry.exception; + +public class RetryExceptionTests extends AbstractExceptionTests { + + public Exception getException(String msg) throws Exception { + return new RetryException(msg); + } + + public Exception getException(String msg, Throwable t) throws Exception { + return new RetryException(msg, t); + } + + public void testNothing() throws Exception { + // fool coverage tools... + } +} diff --git a/infrastructure/src/test/java/org/springframework/batch/retry/exception/TerminatedRetryExceptionTests.java b/infrastructure/src/test/java/org/springframework/batch/retry/exception/TerminatedRetryExceptionTests.java new file mode 100644 index 000000000..42c24389e --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/retry/exception/TerminatedRetryExceptionTests.java @@ -0,0 +1,32 @@ +/* + * 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.retry.exception; + +public class TerminatedRetryExceptionTests extends AbstractExceptionTests { + + public Exception getException(String msg) throws Exception { + return new TerminatedRetryException(msg); + } + + public Exception getException(String msg, Throwable t) throws Exception { + return new TerminatedRetryException(msg, t); + } + + public void testNothing() throws Exception { + // fool coverage tools... + } +} diff --git a/infrastructure/src/test/java/org/springframework/batch/retry/interceptor/RetryInterceptorSupportTests.java b/infrastructure/src/test/java/org/springframework/batch/retry/interceptor/RetryInterceptorSupportTests.java new file mode 100644 index 000000000..b0c4845cf --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/retry/interceptor/RetryInterceptorSupportTests.java @@ -0,0 +1,48 @@ +/* + * 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.retry.interceptor; + +import junit.framework.TestCase; + +public class RetryInterceptorSupportTests extends TestCase { + + public void testClose() { + RetryInterceptorSupport support = new RetryInterceptorSupport(); + try { + support.close(null, null, null); + } + catch (Exception e) { + fail("Unexpected exception"); + } + } + + public void testOnError() { + RetryInterceptorSupport support = new RetryInterceptorSupport(); + try { + support.onError(null, null, null); + } + catch (Exception e) { + fail("Unexpected exception"); + } + } + + public void testOpen() { + RetryInterceptorSupport support = new RetryInterceptorSupport(); + assertTrue(support.open(null, null)); + } + +} diff --git a/infrastructure/src/test/java/org/springframework/batch/retry/interceptor/RetryInterceptorTests.java b/infrastructure/src/test/java/org/springframework/batch/retry/interceptor/RetryInterceptorTests.java new file mode 100644 index 000000000..0b7b69221 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/retry/interceptor/RetryInterceptorTests.java @@ -0,0 +1,162 @@ +/* + * 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.retry.interceptor; + +import java.util.ArrayList; +import java.util.List; + +import junit.framework.TestCase; + +import org.springframework.batch.retry.RetryCallback; +import org.springframework.batch.retry.RetryContext; +import org.springframework.batch.retry.RetryInterceptor; +import org.springframework.batch.retry.exception.TerminatedRetryException; +import org.springframework.batch.retry.policy.NeverRetryPolicy; +import org.springframework.batch.retry.support.RetryTemplate; + +public class RetryInterceptorTests extends TestCase { + + RetryTemplate template = new RetryTemplate(); + + int count = 0; + + List list = new ArrayList(); + + public void testOpenInterceptors() throws Exception { + template.setInterceptors(new RetryInterceptor[] { new RetryInterceptorSupport() { + public boolean open(RetryContext context, RetryCallback callback) { + count++; + list.add("1:" + count); + return true; + } + }, new RetryInterceptorSupport() { + public boolean open(RetryContext context, RetryCallback callback) { + count++; + list.add("2:" + count); + return true; + } + } }); + template.execute(new RetryCallback() { + public Object doWithRetry(RetryContext context) throws Throwable { + return null; + } + }); + assertEquals(2, count); + assertEquals(2, list.size()); + assertEquals("1:1", list.get(0)); + } + + public void testOpenCanVetoRetry() throws Exception { + template.setInterceptor(new RetryInterceptorSupport() { + public boolean open(RetryContext context, RetryCallback callback) { + list.add("1"); + return false; + } + }); + try { + template.execute(new RetryCallback() { + public Object doWithRetry(RetryContext context) throws Throwable { + count++; + return null; + } + }); + fail("Expected TerminatedRetryException"); + } + catch (TerminatedRetryException e) { + // expected + } + assertEquals(0, count); + assertEquals(1, list.size()); + assertEquals("1", list.get(0)); + } + + public void testCloseInterceptors() throws Exception { + template.setInterceptors(new RetryInterceptor[] { new RetryInterceptorSupport() { + public void close(RetryContext context, RetryCallback callback, Throwable t) { + count++; + list.add("1:" + count); + } + }, new RetryInterceptorSupport() { + public void close(RetryContext context, RetryCallback callback, Throwable t) { + count++; + list.add("2:" + count); + } + } }); + template.execute(new RetryCallback() { + public Object doWithRetry(RetryContext context) throws Throwable { + return null; + } + }); + assertEquals(2, count); + assertEquals(2, list.size()); + // interceptors are called in reverse order on close... + assertEquals("2:1", list.get(0)); + } + + public void testOnError() throws Exception { + template.setRetryPolicy(new NeverRetryPolicy()); + template.setInterceptors(new RetryInterceptor[] { new RetryInterceptorSupport() { + public void onError(RetryContext context, RetryCallback callback, Throwable throwable) { + list.add("1"); + } + }, new RetryInterceptorSupport() { + public void onError(RetryContext context, RetryCallback callback, Throwable throwable) { + list.add("2"); + } + } }); + try { + template.execute(new RetryCallback() { + public Object doWithRetry(RetryContext context) throws Throwable { + count++; + throw new IllegalStateException("foo"); + } + }); + fail("Expected TerminatedRetryException"); + } + catch (IllegalStateException e) { + assertEquals("foo", e.getMessage()); + } + // never retry so callback is executed once + assertEquals(1, count); + assertEquals(2, list.size()); + // interceptors are called in reverse order on error... + assertEquals("2", list.get(0)); + + } + + public void testCloseInterceptorsAfterRetry() throws Exception { + template.setInterceptor(new RetryInterceptorSupport() { + public void close(RetryContext context, RetryCallback callback, Throwable t) { + list.add("" + count); + // The last attempt should have been successful: + assertNull(t); + } + }); + template.execute(new RetryCallback() { + public Object doWithRetry(RetryContext context) throws Throwable { + if (count++ < 1) + throw new RuntimeException("Retry!"); + return null; + } + }); + assertEquals(2, count); + // The close interceptor was only called once: + assertEquals(1, list.size()); + // We succeeded on the second try: + assertEquals("2", list.get(0)); + } +} diff --git a/infrastructure/src/test/java/org/springframework/batch/retry/interceptor/StatisticsRetryInterceptorTests.java b/infrastructure/src/test/java/org/springframework/batch/retry/interceptor/StatisticsRetryInterceptorTests.java new file mode 100644 index 000000000..293faa4ba --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/retry/interceptor/StatisticsRetryInterceptorTests.java @@ -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.retry.interceptor; + +import junit.framework.TestCase; + +public class StatisticsRetryInterceptorTests extends TestCase { + + StatisticsRetryInterceptor interceptor = new StatisticsRetryInterceptor(); + + public void testGetAbortCount() { + assertEquals(0, interceptor.getAbortCount()); + interceptor.close(null, null, null); + assertEquals(0, interceptor.getAbortCount()); + interceptor.close(null, null, new Exception()); + assertEquals(1, interceptor.getAbortCount()); + } + + public void testGetCompleteCount() { + assertEquals(0, interceptor.getCompleteCount()); + interceptor.close(null, null, null); + assertEquals(1, interceptor.getCompleteCount()); + } + + public void testGetErrorCount() { + assertEquals(0, interceptor.getErrorCount()); + interceptor.onError(null, null, null); + assertEquals(1, interceptor.getErrorCount()); + } + + public void testGetStartedCount() { + assertEquals(0, interceptor.getStartedCount()); + interceptor.open(null, null); + assertEquals(1, interceptor.getStartedCount()); + } + + public void testGetName() { + assertNotNull(interceptor.getName()); + interceptor.setName("foo"); + assertEquals("foo", interceptor.getName()); + } + +} diff --git a/infrastructure/src/test/java/org/springframework/batch/retry/policy/AlwaysRetryPolicyTests.java b/infrastructure/src/test/java/org/springframework/batch/retry/policy/AlwaysRetryPolicyTests.java new file mode 100644 index 000000000..f84166b83 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/retry/policy/AlwaysRetryPolicyTests.java @@ -0,0 +1,56 @@ +/* + * 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.retry.policy; + +import junit.framework.TestCase; + +import org.springframework.batch.retry.RetryContext; +import org.springframework.batch.retry.synch.RetrySynchronizationManager; + +public class AlwaysRetryPolicyTests extends TestCase { + + public void testSimpleOperations() throws Exception { + AlwaysRetryPolicy policy = new AlwaysRetryPolicy(); + RetryContext context = policy.open(null); + assertNotNull(context); + assertTrue(policy.canRetry(context)); + policy.registerThrowable(context, null); + assertTrue(policy.canRetry(context)); + policy.close(context); + assertTrue(policy.canRetry(context)); + } + + public void testRetryCount() throws Exception { + AlwaysRetryPolicy policy = new AlwaysRetryPolicy(); + RetryContext context = policy.open(null); + assertNotNull(context); + policy.registerThrowable(context, null); + assertEquals(0, context.getRetryCount()); + policy.registerThrowable(context, new RuntimeException("foo")); + assertEquals(1, context.getRetryCount()); + assertEquals("foo", context.getLastThrowable().getMessage()); + } + + public void testParent() throws Exception { + AlwaysRetryPolicy policy = new AlwaysRetryPolicy(); + RetryContext context = policy.open(null); + RetrySynchronizationManager.register(context); + RetryContext child = policy.open(null); + assertNotSame(child, context); + assertSame(context, child.getParent()); + } +} diff --git a/infrastructure/src/test/java/org/springframework/batch/retry/policy/CompositeRetryPolicyTests.java b/infrastructure/src/test/java/org/springframework/batch/retry/policy/CompositeRetryPolicyTests.java new file mode 100644 index 000000000..2fc039524 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/retry/policy/CompositeRetryPolicyTests.java @@ -0,0 +1,120 @@ +/* + * 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.retry.policy; + +import java.util.ArrayList; +import java.util.List; + +import junit.framework.TestCase; + +import org.springframework.batch.retry.RetryContext; +import org.springframework.batch.retry.RetryPolicy; +import org.springframework.batch.retry.exception.TerminatedRetryException; +import org.springframework.batch.retry.synch.RetrySynchronizationManager; + +public class CompositeRetryPolicyTests extends TestCase { + + public void testEmptyPolicies() throws Exception { + CompositeRetryPolicy policy = new CompositeRetryPolicy(); + RetryContext context = policy.open(null); + assertNotNull(context); + assertTrue(policy.canRetry(context)); + } + + public void testTrivialPolicies() throws Exception { + CompositeRetryPolicy policy = new CompositeRetryPolicy(); + policy.setPolicies(new RetryPolicy[] { new MockRetryPolicySupport(), new MockRetryPolicySupport() }); + RetryContext context = policy.open(null); + assertNotNull(context); + assertTrue(policy.canRetry(context)); + } + + public void testNonTrivialPolicies() throws Exception { + CompositeRetryPolicy policy = new CompositeRetryPolicy(); + policy.setPolicies(new RetryPolicy[] { new MockRetryPolicySupport(), new MockRetryPolicySupport() { + public boolean canRetry(RetryContext context) { + return false; + } + } }); + RetryContext context = policy.open(null); + assertNotNull(context); + assertFalse(policy.canRetry(context)); + } + + public void testNonTrivialPoliciesWithThrowable() throws Exception { + CompositeRetryPolicy policy = new CompositeRetryPolicy(); + policy.setPolicies(new RetryPolicy[] { new MockRetryPolicySupport(), new MockRetryPolicySupport() { + boolean errorRegistered = false; + + public boolean canRetry(RetryContext context) { + return !errorRegistered; + } + + public void registerThrowable(RetryContext context, Throwable throwable) throws TerminatedRetryException { + errorRegistered = true; + } + } }); + RetryContext context = policy.open(null); + assertNotNull(context); + assertTrue(policy.canRetry(context)); + policy.registerThrowable(context, null); + assertFalse(policy.canRetry(context)); + } + + public void testNonTrivialPoliciesClose() throws Exception { + final List list = new ArrayList(); + CompositeRetryPolicy policy = new CompositeRetryPolicy(); + policy.setPolicies(new RetryPolicy[] { new MockRetryPolicySupport() { + public void close(RetryContext context) { + list.add("1"); + // TODO: test that all close methods are called if this + // happens... + // throw new RuntimeException("Pah!"); + } + }, new MockRetryPolicySupport() { + public void close(RetryContext context) { + list.add("2"); + } + } }); + RetryContext context = policy.open(null); + assertNotNull(context); + policy.close(context); + assertEquals(2, list.size()); + } + + public void testRetryCount() throws Exception { + CompositeRetryPolicy policy = new CompositeRetryPolicy(); + policy.setPolicies(new RetryPolicy[] { new MockRetryPolicySupport(), new MockRetryPolicySupport() }); + RetryContext context = policy.open(null); + assertNotNull(context); + policy.registerThrowable(context, null); + assertEquals(0, context.getRetryCount()); + policy.registerThrowable(context, new RuntimeException("foo")); + assertEquals(1, context.getRetryCount()); + assertEquals("foo", context.getLastThrowable().getMessage()); + } + + public void testParent() throws Exception { + CompositeRetryPolicy policy = new CompositeRetryPolicy(); + RetryContext context = policy.open(null); + RetrySynchronizationManager.register(context); + RetryContext child = policy.open(null); + assertNotSame(child, context); + assertSame(context, child.getParent()); + } + +} diff --git a/infrastructure/src/test/java/org/springframework/batch/retry/policy/ExceptionClassifierRetryPolicyTests.java b/infrastructure/src/test/java/org/springframework/batch/retry/policy/ExceptionClassifierRetryPolicyTests.java new file mode 100644 index 000000000..c6cb6aa8b --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/retry/policy/ExceptionClassifierRetryPolicyTests.java @@ -0,0 +1,134 @@ +/* + * 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.retry.policy; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import junit.framework.TestCase; + +import org.springframework.batch.common.ExceptionClassifierSupport; +import org.springframework.batch.retry.RetryContext; +import org.springframework.batch.retry.synch.RetrySynchronizationManager; + +public class ExceptionClassifierRetryPolicyTests extends TestCase { + + ExceptionClassifierRetryPolicy policy = new ExceptionClassifierRetryPolicy(); + + public void testDefaultPolicies() throws Exception { + RetryContext context = policy.open(null); + assertNotNull(context); + } + + public void testTrivialPolicies() throws Exception { + policy.setPolicyMap(Collections.singletonMap(ExceptionClassifierSupport.DEFAULT, new MockRetryPolicySupport())); + RetryContext context = policy.open(null); + assertNotNull(context); + assertTrue(policy.canRetry(context)); + } + + public void testNullPolicies() throws Exception { + policy.setPolicyMap(new HashMap()); + try { + policy.open(null); + fail("Expected IllegalArgumentException"); + } + catch (IllegalArgumentException e) { + // expected + } + } + + public void testClassifierOperates() throws Exception { + + Map map = new HashMap(); + map.put(ExceptionClassifierSupport.DEFAULT, new AlwaysRetryPolicy()); + map.put("foo", new NeverRetryPolicy()); + policy.setPolicyMap(map); + + RetryContext context = policy.open(null); + assertNotNull(context); + + assertTrue(policy.canRetry(context)); + policy.registerThrowable(context, new IllegalArgumentException()); + assertTrue(policy.canRetry(context)); + + policy.setExceptionClassifier(new ExceptionClassifierSupport() { + public Object classify(Throwable throwable) { + //TODO probably (==null) and (!=null) branches are interchanged? + if (throwable != null) { + return "foo"; + } + return super.classify(throwable); + } + }); + + // The context saves the classifier, so changing it now has no effect + assertTrue(policy.canRetry(context)); + policy.registerThrowable(context, new IllegalArgumentException()); + assertTrue(policy.canRetry(context)); + + // But now the classifier will be active in the new context... + context = policy.open(null); + assertTrue(policy.canRetry(context)); + policy.registerThrowable(context, new IllegalArgumentException()); + assertFalse(policy.canRetry(context)); + + } + + int count = 0; + + public void testClose() throws Exception { + policy.setPolicyMap(Collections.singletonMap(ExceptionClassifierSupport.DEFAULT, new MockRetryPolicySupport() { + public void close(RetryContext context) { + count++; + } + })); + RetryContext context = policy.open(null); + + // The mapped (child) policy hasn't been used yet, so if we close now + // we don't incur the possible expense of ceating the child context. + policy.close(context); + assertEquals(0, count); // not classified yet + // This forces a child context to be created and the child policy is + // then closed + policy.registerThrowable(context, new IllegalStateException()); + policy.close(context); + assertEquals(1, count); // now classified + } + + public void testRetryCount() throws Exception { + ExceptionClassifierRetryPolicy policy = new ExceptionClassifierRetryPolicy(); + RetryContext context = policy.open(null); + assertNotNull(context); + policy.registerThrowable(context, null); + assertEquals(0, context.getRetryCount()); + policy.registerThrowable(context, new RuntimeException("foo")); + assertEquals(1, context.getRetryCount()); + assertEquals("foo", context.getLastThrowable().getMessage()); + } + + public void testParent() throws Exception { + ExceptionClassifierRetryPolicy policy = new ExceptionClassifierRetryPolicy(); + RetryContext context = policy.open(null); + RetrySynchronizationManager.register(context); + RetryContext child = policy.open(null); + assertNotSame(child, context); + assertSame(context, child.getParent()); + } + +} diff --git a/infrastructure/src/test/java/org/springframework/batch/retry/policy/ExternalRetryPolicyTests.java b/infrastructure/src/test/java/org/springframework/batch/retry/policy/ExternalRetryPolicyTests.java new file mode 100644 index 000000000..c896ec349 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/retry/policy/ExternalRetryPolicyTests.java @@ -0,0 +1,212 @@ +/* + * 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.retry.policy; + +import java.util.regex.Pattern; + +import junit.framework.TestCase; + +import org.springframework.batch.retry.RetryCallback; +import org.springframework.batch.retry.RetryContext; +import org.springframework.batch.retry.context.RetryContextSupport; +import org.springframework.batch.retry.exception.TerminatedRetryException; +import org.springframework.batch.retry.support.RetryTemplate; + +public class ExternalRetryPolicyTests extends TestCase { + + public void testExternalRetryStopsLoop() throws Exception { + MockRetryCallback callback = new MockRetryCallback(); + callback.setExceptionToThrow(new IllegalArgumentException()); + + RetryTemplate retryTemplate = new RetryTemplate(); + retryTemplate.setRetryPolicy(new MockExternalRetryPolicy(3)); + + Object result = "start_foo"; + try { + result = retryTemplate.execute(callback); + // If template is external and retry is still permitted, then + // we expect the exception to be propagated. + fail("Expected IllegalArgumentException"); + } + catch (IllegalArgumentException e) { + assertNull(e.getMessage()); + } + assertEquals(1, callback.attempts); + assertEquals("start_foo", result); + } + + public void testExternalRetryWithFailAndNoRetry() throws Exception { + MockRetryCallback callback = new MockRetryCallback(); + callback.setExceptionToThrow(new IllegalArgumentException()); + + RetryTemplate retryTemplate = new RetryTemplate(); + + // Allow one unsuccessful attempt (plus one for recovery): + retryTemplate.setRetryPolicy(new MockExternalRetryPolicy(1)); + + Object result = "start_foo"; + try { + result = retryTemplate.execute(callback); + // The first failed attempt we expect to retry... + fail("Expected IllegalArgumentException"); + } + catch (IllegalArgumentException e) { + assertNull(e.getMessage()); + } + + try { + result = retryTemplate.execute(callback); + // We always get a second attempt... + } + catch (IllegalArgumentException e) { + // This is now the "exhausted" message: + assertNotNull(e.getMessage()); + // But if template is external we should + // swallow the exception when retry is impossible. + fail("Did not expect IllegalArgumentException"); + } + // Callback is called once: the recovery path should be called in + // handleRetryExhausted (so not in this test)... + assertEquals(1, callback.attempts); + assertEquals(null, result); + } + + public void testNonThrowableIsNotRecoverable() throws Exception { + + try { + MockExternalRetryPolicy policy = new MockExternalRetryPolicy(1); + policy.setRecoverableExceptionClasses(new Class[] { String.class }); + fail("Expected IllegalArgumentException"); + } + catch (IllegalArgumentException e) { + // Expected + System.err.println(e.getMessage()); + assertTrue(Pattern.matches(".*not.*Throwable.*", e.getMessage())); + } + + } + + public void testSuclassIsRecoverable() throws Exception { + + MockExternalRetryPolicy policy = new MockExternalRetryPolicy(1); + policy.setRecoverableExceptionClasses(new Class[] { IllegalArgumentException.class }); + + RetryContextSupport context = new RetryContextSupport(null); + + assertTrue(policy.shouldRethrow(context)); + + context.registerThrowable(new IllegalStateException()); + assertTrue(policy.shouldRethrow(context)); + + context.registerThrowable(new IllegalArgumentException()); + assertFalse(policy.shouldRethrow(context)); + + context.registerThrowable(new IllegalArgumentException() { + // subclass + }); + assertFalse(policy.shouldRethrow(context)); + + } + + public void testRecoverableException() throws Exception { + MockRetryCallback callback = new MockRetryCallback(); + callback.setExceptionToThrow(new IllegalArgumentException()); + + RetryTemplate retryTemplate = new RetryTemplate(); + + // Allow one unsuccessful attempt (should take recovery path): + MockExternalRetryPolicy policy = new MockExternalRetryPolicy(1); + policy + .setRecoverableExceptionClasses(new Class[] { IllegalArgumentException.class, + IllegalStateException.class }); + retryTemplate.setRetryPolicy(policy); + + Object result = "start_foo"; + try { + result = retryTemplate.execute(callback); + } + catch (IllegalArgumentException e) { + // This is the "exhausted" message: + assertNotNull(e.getMessage()); + // But if template is external we should + // swallow the exception when retry is impossible. + fail("Did not expect IllegalArgumentException"); + } + // Callback is called once: the recovery path should be called in + // handleRetryExhausted (so not in this test)... + assertEquals(1, callback.attempts); + assertEquals(null, result); + } + + private static class MockRetryCallback implements RetryCallback { + + private int attempts; + + public static String EXHAUSTED = "complete"; + + private Exception exceptionToThrow = new Exception(); + + public Object doWithRetry(RetryContext context) throws Exception { + this.attempts++; + if (((Boolean) context.getAttribute(EXHAUSTED)).booleanValue()) { + // This is now a recovery step... + return null; + } + // Otherwise just barf... + throw this.exceptionToThrow; + } + + public void setExceptionToThrow(Exception exceptionToThrow) { + this.exceptionToThrow = exceptionToThrow; + } + } + + private static class MockExternalRetryPolicy extends AbstractStatefulRetryPolicy { + + private int retryLimit = 0; + + // This one is stateful for testing only - normally this state would + // have to be managed by the context. + private int attempts = 0; + + public MockExternalRetryPolicy(int retryLimit) { + super(); + this.retryLimit = retryLimit; + } + + public boolean canRetry(RetryContext context) { + return attempts < retryLimit; + } + + public void close(RetryContext context) { + // do nothing + } + + public RetryContext open(RetryCallback callback) { + RetryContextSupport context = new RetryContextSupport(null); + context.setAttribute(MockRetryCallback.EXHAUSTED, Boolean.valueOf(!canRetry(context))); + return context; + } + + public void registerThrowable(RetryContext context, Throwable throwable) throws TerminatedRetryException { + ((RetryContextSupport) context).registerThrowable(throwable); + attempts++; + } + + } + +} diff --git a/infrastructure/src/test/java/org/springframework/batch/retry/policy/ItemProviderRetryPolicyTests.java b/infrastructure/src/test/java/org/springframework/batch/retry/policy/ItemProviderRetryPolicyTests.java new file mode 100644 index 000000000..3563da3ff --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/retry/policy/ItemProviderRetryPolicyTests.java @@ -0,0 +1,320 @@ +/* + * 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.retry.policy; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import junit.framework.TestCase; + +import org.springframework.batch.item.FailedItemIdentifier; +import org.springframework.batch.item.ItemProcessor; +import org.springframework.batch.item.ItemProvider; +import org.springframework.batch.item.provider.ListItemProvider; +import org.springframework.batch.repeat.RepeatContext; +import org.springframework.batch.repeat.context.RepeatContextSupport; +import org.springframework.batch.repeat.synch.RepeatSynchronizationManager; +import org.springframework.batch.retry.RetryCallback; +import org.springframework.batch.retry.RetryContext; +import org.springframework.batch.retry.callback.ItemProviderRetryCallback; +import org.springframework.batch.retry.context.RetryContextSupport; +import org.springframework.batch.retry.support.RetryTemplate; + +public class ItemProviderRetryPolicyTests extends TestCase { + + private ItemProviderRetryPolicy policy = new ItemProviderRetryPolicy(); + + private ItemProvider provider; + + private int count = 0; + + private List list = new ArrayList(); + + protected void setUp() throws Exception { + super.setUp(); + // The list simulates a failed delivery, redelivery of the same message, + // then a new message... + provider = new ListItemProvider(Arrays.asList(new String[] { "foo", "foo", "bar" })) { + public boolean recover(Object data, Throwable cause) { + count++; + list.add(data); + return true; + } + }; + } + + public void testOpenSunnyDay() throws Exception { + RetryContext context = policy.open(new ItemProviderRetryCallback(provider, new ItemProcessor() { + public void process(Object data) { + count++; + list.add(data); + } + })); + assertNotNull(context); + // we haven't called the processor yet... + assertEquals(0, count); + // but the provider has been accessed: + assertEquals("foo", provider.next()); + assertEquals("bar", provider.next()); + } + + public void testOpenWithWrongCallbackType() { + try { + policy.open(new RetryCallback() { + public Object doWithRetry(RetryContext context) throws Throwable { + return null; + } + }); + fail("Expected IllegalStateException"); + } + catch (IllegalStateException e) { + assertTrue(e.getMessage().indexOf("must be ItemProvider") >= 0); + } + } + + public void testCanRetry() { + policy.setDelegate(new AlwaysRetryPolicy()); + + RetryContext context = policy.open(new ItemProviderRetryCallback(provider, new ItemProcessor() { + public void process(Object data) { + count++; + } + })); + assertNotNull(context); + + // We can always retry if delegate says so... + assertTrue(policy.canRetry(context)); + } + + public void testRegisterThrowable() { + policy.setDelegate(new NeverRetryPolicy()); + RetryContext context = policy.open(new ItemProviderRetryCallback(provider, new ItemProcessor() { + public void process(Object data) { + count++; + list.add(data); + } + })); + assertNotNull(context); + policy.registerThrowable(context, new Exception()); + assertFalse(policy.canRetry(context)); + } + + public void testClose() throws Exception { + policy.setDelegate(new NeverRetryPolicy()); + RetryContext context = policy.open(new ItemProviderRetryCallback(provider, new ItemProcessor() { + public void process(Object data) { + count++; + list.add(data); + } + })); + assertNotNull(context); + policy.registerThrowable(context, new Exception()); + assertFalse(policy.canRetry(context)); + policy.close(context); + // still can't retry, even if policy is closed + // (not that this would happen in practice)... + assertFalse(policy.canRetry(context)); + // The provider has been accessed only once: + assertEquals("foo", provider.next()); + assertEquals("bar", provider.next()); + } + + public void testOpenTwice() throws Exception { + ItemProviderRetryCallback callback = new ItemProviderRetryCallback(provider, new ItemProcessor() { + public void process(Object data) { + count++; + list.add(data); + } + }); + policy.setDelegate(new SimpleRetryPolicy(2)); + + // First call... + RetryContext context = policy.open(callback); + assertNotNull(context); + policy.registerThrowable(context, new Exception()); + assertTrue(policy.canRetry(context)); + policy.close(context); + + // Second call... + context = policy.open(callback); + assertNotNull(context); + policy.registerThrowable(context, new Exception()); + assertFalse(policy.canRetry(context)); + policy.close(context); + + // The provider has been accessed twice, so this + // mimics a message receive by repeating the value of the first + // message... + assertEquals("bar", provider.next()); + } + + public void testRecover() throws Exception { + policy = new ItemProviderRetryPolicy(); + policy.setDelegate(new SimpleRetryPolicy(1)); + ItemProviderRetryCallback callback = new ItemProviderRetryCallback(provider, new ItemProcessor() { + public void process(Object data) { + } + }); + RetryContext context = policy.open(callback); + assertNotNull(context); + assertTrue(policy.canRetry(context)); + policy.registerThrowable(context, new Exception()); + assertFalse(policy.canRetry(context)); + assertEquals(0, count); + context = policy.open(callback); + // On the second retry, the recovery path is taken... + Object result = policy.handleRetryExhausted(context); + assertNotNull(result); // default result is null + assertEquals(1, count); + assertFalse(policy.canRetry(context)); + assertEquals("foo", list.get(0)); + } + + public void testRecoverWithParent() throws Exception { + RepeatContext parent = new RepeatContextSupport(null); + RepeatSynchronizationManager.register(new RepeatContextSupport(parent)); + testRecover(); + assertFalse(parent.isCompleteOnly()); + RepeatSynchronizationManager.clear(); + } + + public void testFailedItemIdentifier() throws Exception { + policy = new ItemProviderRetryPolicy(); + policy.setDelegate(new SimpleRetryPolicy(1)); + MockFailedItemProvider provider = new MockFailedItemProvider(Collections.EMPTY_LIST); + ItemProviderRetryCallback callback = new ItemProviderRetryCallback(provider, null); + policy.open(callback); + assertEquals(1, provider.hasFailedCount); + } + + public void testRecoverWithTemplate() throws Exception { + policy = new ItemProviderRetryPolicy(); + policy.setDelegate(new SimpleRetryPolicy(1)); + ItemProviderRetryCallback callback = new ItemProviderRetryCallback(provider, new ItemProcessor() { + public void process(Object data) { + throw new RuntimeException("Barf!"); + } + }); + RetryTemplate template = new RetryTemplate(); + template.setRetryPolicy(policy); + Object result = null; + try { + result = template.execute(callback); + fail("Expected exception on first try"); + } + catch (Exception e) { + // expected... + } + // On the second retry, the recovery path is taken... + result = template.execute(callback); + assertNotNull(result); // default result is last item processed + assertEquals(1, count); + assertEquals("foo", list.get(0)); + } + + public void testExhaustedClearsHistoryAfterLastAttempt() throws Exception { + ItemProviderRetryCallback callback = new ItemProviderRetryCallback(provider, new ItemProcessor() { + public void process(Object data) { + count++; + list.add(data); + } + }); + policy.setDelegate(new SimpleRetryPolicy(1)); + + RetryContext context = policy.open(callback); + assertNotNull(context); + + assertEquals(0, count); + policy.registerThrowable(context, new Exception()); + + // False before close... + assertFalse(policy.canRetry(context)); + policy.close(context); + Object result = policy.handleRetryExhausted(context); + assertEquals("foo", result); // default result is last item + + context = policy.open(callback); + // True after exhausted - the history is reset... + assertTrue(policy.canRetry(context)); + } + + public void testRetryCount() throws Exception { + policy = new ItemProviderRetryPolicy(); + policy.setDelegate(new SimpleRetryPolicy(1)); + RetryContext context = policy.open(new ItemProviderRetryCallback(provider, new ItemProcessor() { + public void process(Object data) { + count++; + list.add(data); + } + })); + assertNotNull(context); + policy.registerThrowable(context, null); + assertEquals(0, context.getRetryCount()); + policy.registerThrowable(context, new RuntimeException("foo")); + assertEquals(1, context.getRetryCount()); + assertEquals("foo", context.getLastThrowable().getMessage()); + } + + public void testRetryCountPreservedBetweenRetries() throws Exception { + ItemProviderRetryCallback callback = new ItemProviderRetryCallback(provider, new ItemProcessor() { + public void process(Object data) { + count++; + list.add(data); + } + }); + + policy = new ItemProviderRetryPolicy(); + policy.setDelegate(new SimpleRetryPolicy(1)); + RetryContext context = policy.open(callback); + assertNotNull(context); + policy.registerThrowable(context, new RuntimeException("foo")); + assertEquals(1, context.getRetryCount()); + context = policy.open(callback); + assertEquals(1, context.getRetryCount()); + policy.registerThrowable(context, new RuntimeException("foo")); + assertEquals(2, context.getRetryCount()); + } + + public void testSetCacheAndHasFailed() throws Exception { + MapRetryContextCache cache = new MapRetryContextCache(); + policy.setRetryContextCache(cache); + cache.put("foo", new RetryContextSupport(null)); + assertTrue(policy.hasFailed(provider, "foo")); + } + + private static class MockFailedItemProvider extends ListItemProvider implements FailedItemIdentifier { + + private int hasFailedCount = 0; + + public MockFailedItemProvider(List list) { + super(list); + } + + public boolean hasFailed(Object item) { + hasFailedCount++; + return false; + } + + public Object getKey(Object item) { + throw new UnsupportedOperationException("Should not call this method"); + } + + } + +} diff --git a/infrastructure/src/test/java/org/springframework/batch/retry/policy/MapRetryContextCacheTests.java b/infrastructure/src/test/java/org/springframework/batch/retry/policy/MapRetryContextCacheTests.java new file mode 100644 index 000000000..2100d3611 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/retry/policy/MapRetryContextCacheTests.java @@ -0,0 +1,42 @@ +/* + * 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.retry.policy; + +import junit.framework.TestCase; + +import org.springframework.batch.retry.context.RetryContextSupport; + +public class MapRetryContextCacheTests extends TestCase { + + MapRetryContextCache cache = new MapRetryContextCache(); + + public void testPut() { + RetryContextSupport context = new RetryContextSupport(null); + cache.put("foo", context); + assertEquals(context, cache.get("foo")); + } + + public void testRemove() { + assertFalse(cache.containsKey("foo")); + RetryContextSupport context = new RetryContextSupport(null); + cache.put("foo", context); + assertTrue(cache.containsKey("foo")); + cache.remove("foo"); + assertFalse(cache.containsKey("foo")); + } + +} diff --git a/infrastructure/src/test/java/org/springframework/batch/retry/policy/MockRetryPolicySupport.java b/infrastructure/src/test/java/org/springframework/batch/retry/policy/MockRetryPolicySupport.java new file mode 100644 index 000000000..c89ce1893 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/retry/policy/MockRetryPolicySupport.java @@ -0,0 +1,21 @@ +/* + * 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.retry.policy; + +public class MockRetryPolicySupport extends AlwaysRetryPolicy { + +} diff --git a/infrastructure/src/test/java/org/springframework/batch/retry/policy/NeverRetryPolicyTests.java b/infrastructure/src/test/java/org/springframework/batch/retry/policy/NeverRetryPolicyTests.java new file mode 100644 index 000000000..befab5f3d --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/retry/policy/NeverRetryPolicyTests.java @@ -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.retry.policy; + +import junit.framework.TestCase; + +import org.springframework.batch.retry.RetryContext; +import org.springframework.batch.retry.synch.RetrySynchronizationManager; + +public class NeverRetryPolicyTests extends TestCase { + + public void testSimpleOperations() throws Exception { + NeverRetryPolicy policy = new NeverRetryPolicy(); + RetryContext context = policy.open(null); + assertNotNull(context); + // We can retry until the first exception is registered... + assertTrue(policy.canRetry(context)); + assertTrue(policy.canRetry(context)); + policy.registerThrowable(context, null); + assertFalse(policy.canRetry(context)); + policy.close(context); + assertFalse(policy.canRetry(context)); + } + + public void testRetryCount() throws Exception { + NeverRetryPolicy policy = new NeverRetryPolicy(); + RetryContext context = policy.open(null); + assertNotNull(context); + policy.registerThrowable(context, null); + assertEquals(0, context.getRetryCount()); + policy.registerThrowable(context, new RuntimeException("foo")); + assertEquals(1, context.getRetryCount()); + assertEquals("foo", context.getLastThrowable().getMessage()); + } + + public void testParent() throws Exception { + NeverRetryPolicy policy = new NeverRetryPolicy(); + RetryContext context = policy.open(null); + RetrySynchronizationManager.register(context); + RetryContext child = policy.open(null); + assertNotSame(child, context); + assertSame(context, child.getParent()); + } + +} diff --git a/infrastructure/src/test/java/org/springframework/batch/retry/policy/SimpleRetryPolicyTests.java b/infrastructure/src/test/java/org/springframework/batch/retry/policy/SimpleRetryPolicyTests.java new file mode 100644 index 000000000..6cbba7452 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/retry/policy/SimpleRetryPolicyTests.java @@ -0,0 +1,95 @@ +/* + * 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.retry.policy; + +import junit.framework.TestCase; + +import org.springframework.batch.retry.RetryContext; +import org.springframework.batch.retry.synch.RetrySynchronizationManager; + +public class SimpleRetryPolicyTests extends TestCase { + + public void testSetInvalidExceptionClass() throws Exception { + try { + new SimpleRetryPolicy().setRetryableExceptionClasses(new Class[] { String.class }); + fail("Should only be able to set Exception classes."); + } + catch (IllegalArgumentException ex) { + + } + } + + public void testCanRetryIfNoException() throws Exception { + SimpleRetryPolicy policy = new SimpleRetryPolicy(); + RetryContext context = policy.open(null); + assertTrue(policy.canRetry(context)); + } + + public void testEmptyExceptionsNeverRetry() throws Exception { + + SimpleRetryPolicy policy = new SimpleRetryPolicy(); + RetryContext context = policy.open(null); + + // We can't retry any exceptions... + policy.setRetryableExceptionClasses(new Class[0]); + + // ...so we can't retry this one... + policy.registerThrowable(context, new IllegalStateException()); + assertFalse(policy.canRetry(context)); + } + + public void testRetryLimitInitialState() throws Exception { + SimpleRetryPolicy policy = new SimpleRetryPolicy(); + RetryContext context = policy.open(null); + assertTrue(policy.canRetry(context)); + policy.setMaxAttempts(0); + context = policy.open(null); + assertFalse(policy.canRetry(context)); + } + + public void testRetryLimitSubsequentState() throws Exception { + SimpleRetryPolicy policy = new SimpleRetryPolicy(); + RetryContext context = policy.open(null); + policy.setMaxAttempts(2); + assertTrue(policy.canRetry(context)); + policy.registerThrowable(context, new Exception()); + assertTrue(policy.canRetry(context)); + policy.registerThrowable(context, new Exception()); + assertFalse(policy.canRetry(context)); + } + + public void testRetryCount() throws Exception { + SimpleRetryPolicy policy = new SimpleRetryPolicy(); + RetryContext context = policy.open(null); + assertNotNull(context); + policy.registerThrowable(context, null); + assertEquals(0, context.getRetryCount()); + policy.registerThrowable(context, new RuntimeException("foo")); + assertEquals(1, context.getRetryCount()); + assertEquals("foo", context.getLastThrowable().getMessage()); + } + + public void testParent() throws Exception { + SimpleRetryPolicy policy = new SimpleRetryPolicy(); + RetryContext context = policy.open(null); + RetrySynchronizationManager.register(context); + RetryContext child = policy.open(null); + assertNotSame(child, context); + assertSame(context, child.getParent()); + } + +} diff --git a/infrastructure/src/test/java/org/springframework/batch/retry/policy/TimeoutRetryPolicyTests.java b/infrastructure/src/test/java/org/springframework/batch/retry/policy/TimeoutRetryPolicyTests.java new file mode 100644 index 000000000..b93626beb --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/retry/policy/TimeoutRetryPolicyTests.java @@ -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.retry.policy; + +import junit.framework.TestCase; + +import org.springframework.batch.retry.RetryContext; +import org.springframework.batch.retry.synch.RetrySynchronizationManager; + +public class TimeoutRetryPolicyTests extends TestCase { + + public void testTimeoutPreventsRetry() throws Exception { + TimeoutRetryPolicy policy = new TimeoutRetryPolicy(); + policy.setTimeout(100); + RetryContext context = policy.open(null); + policy.registerThrowable(context, new Exception()); + assertTrue(policy.canRetry(context)); + Thread.sleep(200); + assertFalse(policy.canRetry(context)); + policy.close(context); + } + + public void testRetryCount() throws Exception { + TimeoutRetryPolicy policy = new TimeoutRetryPolicy(); + RetryContext context = policy.open(null); + assertNotNull(context); + policy.registerThrowable(context, null); + assertEquals(0, context.getRetryCount()); + policy.registerThrowable(context, new RuntimeException("foo")); + assertEquals(1, context.getRetryCount()); + assertEquals("foo", context.getLastThrowable().getMessage()); + } + + public void testParent() throws Exception { + TimeoutRetryPolicy policy = new TimeoutRetryPolicy(); + RetryContext context = policy.open(null); + RetrySynchronizationManager.register(context); + RetryContext child = policy.open(null); + assertNotSame(child, context); + assertSame(context, child.getParent()); + } + +} diff --git a/infrastructure/src/test/java/org/springframework/batch/retry/support/RetryTemplateTests.java b/infrastructure/src/test/java/org/springframework/batch/retry/support/RetryTemplateTests.java new file mode 100644 index 000000000..277e61708 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/retry/support/RetryTemplateTests.java @@ -0,0 +1,274 @@ +/* + * 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.retry.support; + +import junit.framework.TestCase; + +import org.springframework.batch.retry.RetryCallback; +import org.springframework.batch.retry.RetryContext; +import org.springframework.batch.retry.backoff.BackOffContext; +import org.springframework.batch.retry.backoff.BackOffPolicy; +import org.springframework.batch.retry.backoff.StatelessBackOffPolicy; +import org.springframework.batch.retry.exception.BackOffInterruptedException; +import org.springframework.batch.retry.exception.ExhaustedRetryException; +import org.springframework.batch.retry.policy.NeverRetryPolicy; +import org.springframework.batch.retry.policy.SimpleRetryPolicy; +import org.springframework.batch.retry.synch.RetrySynchronizationManager; + +/** + * @author Rob Harrop + * @since 2.1 + */ +public class RetryTemplateTests extends TestCase { + + RetryContext context; + + int count = 0; + + public void testSuccessfulRetry() throws Exception { + for (int x = 1; x <= 10; x++) { + MockRetryCallback callback = new MockRetryCallback(); + callback.setAttemptsBeforeSuccess(x); + RetryTemplate retryTemplate = new RetryTemplate(); + retryTemplate.setRetryPolicy(new SimpleRetryPolicy(x)); + retryTemplate.execute(callback); + assertEquals(x, callback.attempts); + } + } + + public void testAlwaysTryAtLeastOnce() throws Exception { + MockRetryCallback callback = new MockRetryCallback(); + RetryTemplate retryTemplate = new RetryTemplate(); + retryTemplate.setRetryPolicy(new NeverRetryPolicy()); + retryTemplate.execute(callback); + assertEquals(1, callback.attempts); + } + + public void testNoSuccessRetry() throws Exception { + MockRetryCallback callback = new MockRetryCallback(); + // Somthing that won't be thrwon by JUnit... + callback.setExceptionToThrow(new IllegalArgumentException()); + callback.setAttemptsBeforeSuccess(Integer.MAX_VALUE); + RetryTemplate retryTemplate = new RetryTemplate(); + int retryAttempts = 2; + retryTemplate.setRetryPolicy(new SimpleRetryPolicy(retryAttempts)); + try { + retryTemplate.execute(callback); + fail("Expected IllegalArgumentException"); + } + catch (IllegalArgumentException e) { + assertNotNull(e); + assertEquals(retryAttempts, callback.attempts); + return; + } + fail("Expected IllegalArgumentException"); + } + + public void testDefaultConfigWithExceptionSubclass() throws Exception { + MockRetryCallback callback = new MockRetryCallback(); + int attempts = 3; + callback.setAttemptsBeforeSuccess(attempts); + callback.setExceptionToThrow(new IllegalArgumentException()); + + RetryTemplate retryTemplate = new RetryTemplate(); + retryTemplate.setRetryPolicy(new SimpleRetryPolicy(attempts)); + retryTemplate.execute(callback); + assertEquals(attempts, callback.attempts); + } + + public void testSetExceptions() throws Exception { + RetryTemplate template = new RetryTemplate(); + SimpleRetryPolicy policy = new SimpleRetryPolicy(); + template.setRetryPolicy(policy); + policy.setRetryableExceptionClasses(new Class[] { RuntimeException.class }); + + int attempts = 3; + + MockRetryCallback callback = new MockRetryCallback(); + callback.setAttemptsBeforeSuccess(attempts); + + try { + template.execute(callback); + } + catch (Exception e) { + assertNotNull(e); + assertEquals(1, callback.attempts); + } + callback.setExceptionToThrow(new RuntimeException()); + + template.execute(callback); + assertEquals(attempts, callback.attempts); + } + + public void testBackOffInvoked() throws Exception { + for (int x = 1; x <= 10; x++) { + MockRetryCallback callback = new MockRetryCallback(); + MockBackOffStrategy backOff = new MockBackOffStrategy(); + callback.setAttemptsBeforeSuccess(x); + RetryTemplate retryTemplate = new RetryTemplate(); + retryTemplate.setBackOffPolicy(backOff); + retryTemplate.setRetryPolicy(new SimpleRetryPolicy(x)); + retryTemplate.execute(callback); + assertEquals(x, callback.attempts); + assertEquals(1, backOff.startCalls); + assertEquals(x - 1, backOff.backOffCalls); + } + } + + public void testEarlyTermination() throws Exception { + try { + RetryTemplate retryTemplate = new RetryTemplate(); + retryTemplate.execute(new RetryCallback() { + public Object doWithRetry(RetryContext status) throws Throwable { + status.setExhaustedOnly(); + throw new IllegalStateException("Retry this operation"); + } + }); + fail("Expected TerminatedRetryException"); + } + catch (ExhaustedRetryException ex) { + // Expected for internal retry policy (external would recover + // gracefully) + assertEquals("Retry this operation", ex.getCause().getMessage()); + } + } + + public void testNestedContexts() throws Exception { + RetryTemplate outer = new RetryTemplate(); + final RetryTemplate inner = new RetryTemplate(); + outer.execute(new RetryCallback() { + public Object doWithRetry(RetryContext status) throws Throwable { + context = status; + count++; + Object result = inner.execute(new RetryCallback() { + public Object doWithRetry(RetryContext status) throws Throwable { + count++; + assertNotNull(context); + assertNotSame(status, context); + assertSame(context, status.getParent()); + assertSame("The context should be the child", status, RetrySynchronizationManager.getContext()); + return null; + } + }); + assertSame("The context should be restored", status, RetrySynchronizationManager.getContext()); + return result; + } + }); + assertEquals(2, count); + } + + public void testRethrowError() throws Exception { + RetryTemplate retryTemplate = new RetryTemplate(); + retryTemplate.setRetryPolicy(new NeverRetryPolicy()); + try { + retryTemplate.execute(new RetryCallback() { + public Object doWithRetry(RetryContext context) throws Throwable { + throw new Error("Realllly bad!"); + } + }); + fail("Expected Error"); + } + catch (Error e) { + assertEquals("Realllly bad!", e.getMessage()); + } + } + + public void testBackOffInterrupted() throws Exception { + RetryTemplate retryTemplate = new RetryTemplate(); + retryTemplate.setBackOffPolicy(new StatelessBackOffPolicy() { + protected void doBackOff() throws BackOffInterruptedException { + throw new BackOffInterruptedException("foo"); + } + }); + try { + retryTemplate.execute(new RetryCallback() { + public Object doWithRetry(RetryContext context) throws Throwable { + throw new RuntimeException("Bad!"); + } + }); + fail("Expected RuntimeException"); + } + catch (BackOffInterruptedException e) { + assertEquals("foo", e.getMessage()); + } + } + + public void testFallThroughToEndUnsuccessfully() throws Exception { + MockRetryCallback callback = new MockRetryCallback(); + int attempts = 3; + callback.setAttemptsBeforeSuccess(attempts); + callback.setExceptionToThrow(new IllegalArgumentException()); + + RetryTemplate retryTemplate = new RetryTemplate(); + retryTemplate.setRetryPolicy(new NeverRetryPolicy() { + public boolean shouldRethrow(RetryContext context) { + // The opposite of normal... + // cause the retry to drop through to the end + // neither throwing exception nor returning successfully. + return false; + } + }); + try { + retryTemplate.execute(callback); + fail("Expected ExhaustedRetryException"); + } + catch (ExhaustedRetryException e) { + assertTrue(e.getMessage().indexOf("exhausted") >= 0); + } + } + + private static class MockRetryCallback implements RetryCallback { + + private int attempts; + + private int attemptsBeforeSuccess; + + private Exception exceptionToThrow = new Exception(); + + public Object doWithRetry(RetryContext status) throws Exception { + this.attempts++; + if (attempts < attemptsBeforeSuccess) { + throw this.exceptionToThrow; + } + return null; + } + + public void setAttemptsBeforeSuccess(int attemptsBeforeSuccess) { + this.attemptsBeforeSuccess = attemptsBeforeSuccess; + } + + public void setExceptionToThrow(Exception exceptionToThrow) { + this.exceptionToThrow = exceptionToThrow; + } + } + + private static class MockBackOffStrategy implements BackOffPolicy { + + public int backOffCalls; + + public int startCalls; + + public BackOffContext start(RetryContext status) { + startCalls++; + return null; + } + + public void backOff(BackOffContext backOffContext) throws BackOffInterruptedException { + backOffCalls++; + } + } +} diff --git a/infrastructure/src/test/java/org/springframework/batch/retry/synch/RetrySynchronizationManagerTests.java b/infrastructure/src/test/java/org/springframework/batch/retry/synch/RetrySynchronizationManagerTests.java new file mode 100644 index 000000000..2cab6ab2c --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/retry/synch/RetrySynchronizationManagerTests.java @@ -0,0 +1,82 @@ +/* + * 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.retry.synch; + +import junit.framework.TestCase; + +import org.springframework.batch.retry.RetryCallback; +import org.springframework.batch.retry.RetryContext; +import org.springframework.batch.retry.context.RetryContextSupport; +import org.springframework.batch.retry.support.RetryTemplate; + +/** + * @author Dave Syer + */ +public class RetrySynchronizationManagerTests extends TestCase { + + int count = 0; + + RetryTemplate template = new RetryTemplate(); + + protected void setUp() throws Exception { + super.setUp(); + RetrySynchronizationManager.clearAll(); + RetryContext status = RetrySynchronizationManager.getContext(); + assertNull(status); + } + + public void testStatusIsStoredByTemplate() throws Exception { + + RetryContext status = RetrySynchronizationManager.getContext(); + assertNull(status); + + template.execute(new RetryCallback() { + public Object doWithRetry(RetryContext status) throws Exception { + RetryContext global = RetrySynchronizationManager.getContext(); + assertNotNull(status); + assertEquals(global, status); + return null; + } + }); + + status = RetrySynchronizationManager.getContext(); + assertNull(status); + } + + public void testStatusRegistration() throws Exception { + RetryContext status = new RetryContextSupport(null); + RetryContext value = RetrySynchronizationManager.register(status); + assertNull(value); + value = RetrySynchronizationManager.register(status); + assertEquals(status, value); + } + + public void testClear() throws Exception { + RetryContext status = new RetryContextSupport(null); + RetryContext value = RetrySynchronizationManager.register(status); + assertNull(value); + RetrySynchronizationManager.clear(); + value = RetrySynchronizationManager.register(status); + assertNull(value); + } + + public void testParent() throws Exception { + RetryContext parent = new RetryContextSupport(null); + RetryContext child = new RetryContextSupport(parent); + assertSame(parent, child.getParent()); + } +} diff --git a/infrastructure/src/test/java/org/springframework/batch/support/PropertiesConverterTests.java b/infrastructure/src/test/java/org/springframework/batch/support/PropertiesConverterTests.java new file mode 100644 index 000000000..c38e4b48d --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/support/PropertiesConverterTests.java @@ -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.support; + +import java.util.Properties; + +import org.springframework.batch.support.PropertiesConverter; + +import junit.framework.TestCase; + +/** + * Unit tests for {@link PropertiesConverter} + * + * @author Robert Kasanicky + */ +public class PropertiesConverterTests extends TestCase { + + //convenience attributes for storing results of conversions + private Properties props = null; + private String string = null; + + /** + * Check that Properties can be converted to String and back correctly. + */ + public void testRegularConversion() { + + Properties storedProps = new Properties(); + storedProps.setProperty("key1", "value1"); + storedProps.setProperty("key2", "value2"); + + props = PropertiesConverter.stringToProperties(PropertiesConverter.propertiesToString(storedProps)); + + assertEquals(storedProps, props); + } + + /** + * Converting a String to Properties and back does not return equal String! + * See {@link PropertiesConverter} javadoc for more details. + */ + public void testInvalidConversion() { + String value = "key=value"; + string = PropertiesConverter.propertiesToString(PropertiesConverter.stringToProperties(value)); + assertFalse(value.equals(string)); + } + + /** + * Null String should be converted to empty Properties + */ + public void testStringToPropertiesNull() { + props = PropertiesConverter.stringToProperties(null); + assertNotNull(props); + assertEquals("properties are empty", 0, props.size()); + } + + /** + * Null or empty properties should be converted to empty String + */ + public void testPropertiesToStringNull() { + string = PropertiesConverter.propertiesToString(null); + assertEquals("", string); + + string = PropertiesConverter.propertiesToString(new Properties()); + assertEquals("", string); + } + +} diff --git a/infrastructure/src/test/java/org/springframework/batch/support/transaction/ResourcelessTransactionManagerTests.java b/infrastructure/src/test/java/org/springframework/batch/support/transaction/ResourcelessTransactionManagerTests.java new file mode 100644 index 000000000..3fdcc6c4a --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/support/transaction/ResourcelessTransactionManagerTests.java @@ -0,0 +1,69 @@ +/* + * 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.support.transaction; + +import junit.framework.TestCase; + +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.TransactionCallback; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationAdapter; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.transaction.support.TransactionTemplate; + +public class ResourcelessTransactionManagerTests extends TestCase { + + ResourcelessTransactionManager transactionManager = new ResourcelessTransactionManager(); + + int txStatus = Integer.MIN_VALUE; + + public void testCommit() throws Exception { + new TransactionTemplate(transactionManager).execute(new TransactionCallback() { + public Object doInTransaction(TransactionStatus status) { + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() { + public void afterCompletion(int status) { + super.afterCompletion(status); + txStatus = status; + } + }); + return null; + } + }); + assertEquals(TransactionSynchronization.STATUS_COMMITTED, txStatus); + } + + public void testRollback() throws Exception { + try { + new TransactionTemplate(transactionManager).execute(new TransactionCallback() { + public Object doInTransaction(TransactionStatus status) { + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() { + public void afterCompletion(int status) { + super.afterCompletion(status); + txStatus = status; + } + }); + throw new RuntimeException("Rollback!"); + } + }); + fail("Expected RuntimeException"); + } + catch (RuntimeException e) { + assertEquals("Rollback!", e.getMessage()); + } + assertEquals(TransactionSynchronization.STATUS_ROLLED_BACK, txStatus); + } +} diff --git a/infrastructure/src/test/java/org/springframework/batch/support/transaction/TransactionAwareListFactoryTests.java b/infrastructure/src/test/java/org/springframework/batch/support/transaction/TransactionAwareListFactoryTests.java new file mode 100644 index 000000000..00a545b6e --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/support/transaction/TransactionAwareListFactoryTests.java @@ -0,0 +1,137 @@ +/* + * 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.support.transaction; + +import java.util.Arrays; +import java.util.List; + +import junit.framework.TestCase; + +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.TransactionCallback; +import org.springframework.transaction.support.TransactionTemplate; + +public class TransactionAwareListFactoryTests extends TestCase { + + TransactionAwareProxyFactory factory = new TransactionAwareProxyFactory(Arrays.asList(new String[] { "foo", + "bar", "spam" })); + + TransactionTemplate transactionTemplate = new TransactionTemplate(new ResourcelessTransactionManager()); + + List list; + + protected void setUp() throws Exception { + list = (List) factory.createInstance(); + } + + public void testAdd() { + assertEquals(3, list.size()); + list.add("bucket"); + assertTrue(list.contains("bucket")); + } + + public void testRemove() { + assertEquals(3, list.size()); + assertTrue(list.contains("spam")); + list.remove("spam"); + assertFalse(list.contains("spam")); + } + + public void testClear() { + assertEquals(3, list.size()); + list.clear(); + assertEquals(0, list.size()); + } + + public void testTransactionalAdd() throws Exception { + transactionTemplate.execute(new TransactionCallback() { + public Object doInTransaction(TransactionStatus status) { + testAdd(); + return null; + } + }); + assertEquals(4, list.size()); + } + + public void testTransactionalRemove() throws Exception { + transactionTemplate.execute(new TransactionCallback() { + public Object doInTransaction(TransactionStatus status) { + testRemove(); + return null; + } + }); + assertEquals(2, list.size()); + } + + public void testTransactionalClear() throws Exception { + transactionTemplate.execute(new TransactionCallback() { + public Object doInTransaction(TransactionStatus status) { + testClear(); + return null; + } + }); + assertEquals(0, list.size()); + } + + public void testTransactionalAddWithRollback() throws Exception { + try { + transactionTemplate.execute(new TransactionCallback() { + public Object doInTransaction(TransactionStatus status) { + testAdd(); + throw new RuntimeException("Rollback!"); + } + }); + fail("Expected RuntimeException"); + } + catch (RuntimeException e) { + assertEquals("Rollback!", e.getMessage()); + } + assertEquals(3, list.size()); + } + + public void testTransactionalRemoveWithRollback() throws Exception { + try { + transactionTemplate.execute(new TransactionCallback() { + public Object doInTransaction(TransactionStatus status) { + testRemove(); + throw new RuntimeException("Rollback!"); + } + }); + fail("Expected RuntimeException"); + } + catch (RuntimeException e) { + assertEquals("Rollback!", e.getMessage()); + } + assertEquals(3, list.size()); + } + + public void testTransactionalClearWithRollback() throws Exception { + try { + transactionTemplate.execute(new TransactionCallback() { + public Object doInTransaction(TransactionStatus status) { + testClear(); + throw new RuntimeException("Rollback!"); + } + }); + fail("Expected RuntimeException"); + } + catch (RuntimeException e) { + assertEquals("Rollback!", e.getMessage()); + } + assertEquals(3, list.size()); + } +} diff --git a/infrastructure/src/test/java/org/springframework/batch/support/transaction/TransactionAwareMapFactoryTests.java b/infrastructure/src/test/java/org/springframework/batch/support/transaction/TransactionAwareMapFactoryTests.java new file mode 100644 index 000000000..baefd006a --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/support/transaction/TransactionAwareMapFactoryTests.java @@ -0,0 +1,141 @@ +/* + * 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.support.transaction; + +import java.util.HashMap; +import java.util.Map; + +import junit.framework.TestCase; + +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.TransactionCallback; +import org.springframework.transaction.support.TransactionTemplate; + +public class TransactionAwareMapFactoryTests extends TestCase { + + TransactionAwareProxyFactory factory; + + TransactionTemplate transactionTemplate = new TransactionTemplate(new ResourcelessTransactionManager()); + + Map map; + + protected void setUp() throws Exception { + Map seed = new HashMap(); + seed.put("foo", "oof"); + seed.put("bar", "bar"); + seed.put("spam", "maps"); + factory = new TransactionAwareProxyFactory(seed); + map = (Map) factory.createInstance(); + } + + public void testAdd() { + assertEquals(3, map.size()); + map.put("bucket", "crap"); + assertTrue(map.keySet().contains("bucket")); + } + + public void testRemove() { + assertEquals(3, map.size()); + assertTrue(map.keySet().contains("spam")); + map.remove("spam"); + assertFalse(map.keySet().contains("spam")); + } + + public void testClear() { + assertEquals(3, map.size()); + map.clear(); + assertEquals(0, map.size()); + } + + public void testTransactionalAdd() throws Exception { + transactionTemplate.execute(new TransactionCallback() { + public Object doInTransaction(TransactionStatus status) { + testAdd(); + return null; + } + }); + assertEquals(4, map.size()); + } + + public void testTransactionalRemove() throws Exception { + transactionTemplate.execute(new TransactionCallback() { + public Object doInTransaction(TransactionStatus status) { + testRemove(); + return null; + } + }); + assertEquals(2, map.size()); + } + + public void testTransactionalClear() throws Exception { + transactionTemplate.execute(new TransactionCallback() { + public Object doInTransaction(TransactionStatus status) { + testClear(); + return null; + } + }); + assertEquals(0, map.size()); + } + + public void testTransactionalAddWithRollback() throws Exception { + try { + transactionTemplate.execute(new TransactionCallback() { + public Object doInTransaction(TransactionStatus status) { + testAdd(); + throw new RuntimeException("Rollback!"); + } + }); + fail("Expected RuntimeException"); + } + catch (RuntimeException e) { + assertEquals("Rollback!", e.getMessage()); + } + assertEquals(3, map.size()); + } + + public void testTransactionalRemoveWithRollback() throws Exception { + try { + transactionTemplate.execute(new TransactionCallback() { + public Object doInTransaction(TransactionStatus status) { + testRemove(); + throw new RuntimeException("Rollback!"); + } + }); + fail("Expected RuntimeException"); + } + catch (RuntimeException e) { + assertEquals("Rollback!", e.getMessage()); + } + assertEquals(3, map.size()); + } + + public void testTransactionalClearWithRollback() throws Exception { + try { + transactionTemplate.execute(new TransactionCallback() { + public Object doInTransaction(TransactionStatus status) { + testClear(); + throw new RuntimeException("Rollback!"); + } + }); + fail("Expected RuntimeException"); + } + catch (RuntimeException e) { + assertEquals("Rollback!", e.getMessage()); + } + assertEquals(3, map.size()); + } +} diff --git a/infrastructure/src/test/java/org/springframework/batch/support/transaction/TransactionAwareProxyFactoryTests.java b/infrastructure/src/test/java/org/springframework/batch/support/transaction/TransactionAwareProxyFactoryTests.java new file mode 100644 index 000000000..d5ff8d343 --- /dev/null +++ b/infrastructure/src/test/java/org/springframework/batch/support/transaction/TransactionAwareProxyFactoryTests.java @@ -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.support.transaction; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import junit.framework.TestCase; + +public class TransactionAwareProxyFactoryTests extends TestCase { + + public void testCreateList() throws Exception { + List list = TransactionAwareProxyFactory.createTransactionalList(); + list.add("foo"); + assertEquals(1, list.size()); + } + + public void testCreateSet() throws Exception { + Set set = TransactionAwareProxyFactory.createTransactionalSet(); + set.add("foo"); + assertEquals(1, set.size()); + } + + public void testCreateMap() throws Exception { + Map map = TransactionAwareProxyFactory.createTransactionalMap(); + map.put("foo", "bar"); + assertEquals(1, map.size()); + } + + public void testCreateUnsupported() throws Exception { + try { + new TransactionAwareProxyFactory(new Object()).createInstance(); + fail("Expected UnsupportedOperationException"); + } catch (UnsupportedOperationException e) { + // expected + } + } +} diff --git a/infrastructure/src/test/java/test/jdbc/datasource/InitializingDataSourceFactoryBean.java b/infrastructure/src/test/java/test/jdbc/datasource/InitializingDataSourceFactoryBean.java new file mode 100644 index 000000000..210d73b57 --- /dev/null +++ b/infrastructure/src/test/java/test/jdbc/datasource/InitializingDataSourceFactoryBean.java @@ -0,0 +1,131 @@ +/* + * 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 test.jdbc.datasource; + +import java.io.IOException; +import java.util.Iterator; +import java.util.List; + +import javax.sql.DataSource; + +import org.apache.commons.io.IOUtils; +import org.springframework.beans.factory.BeanInitializationException; +import org.springframework.beans.factory.config.AbstractFactoryBean; +import org.springframework.core.io.Resource; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.TransactionCallback; +import org.springframework.transaction.support.TransactionTemplate; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +public class InitializingDataSourceFactoryBean extends AbstractFactoryBean { + + private Resource initScript; + + private Resource destroyScript; + + DataSource dataSource; + + public void destroy() throws Exception { + super.destroy(); + + try { + doExecuteScript(destroyScript); + } + catch (Exception e) { + logger.warn("Could not execute destroy script [" + destroyScript + "]", e); + } + } + + public void afterPropertiesSet() throws Exception { + Assert.notNull(dataSource); + super.afterPropertiesSet(); + } + + protected Object createInstance() throws Exception { + Assert.notNull(dataSource); + try { + doExecuteScript(destroyScript); + } + catch (Exception e) { + logger.debug("Could not execute destroy script [" + destroyScript + "]", e); + } + doExecuteScript(initScript); + return dataSource; + } + + private void doExecuteScript(final Resource scriptResource) { + if (scriptResource == null || !scriptResource.exists()) + return; + TransactionTemplate transactionTemplate = new TransactionTemplate(new DataSourceTransactionManager(dataSource)); + if (initScript != null) { + transactionTemplate.execute(new TransactionCallback() { + + public Object doInTransaction(TransactionStatus status) { + JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); + String[] scripts; + try { + scripts = StringUtils.delimitedListToStringArray(stripComments(IOUtils.readLines(scriptResource + .getInputStream())), ";"); + } + catch (IOException e) { + throw new BeanInitializationException("Cannot load script from [" + initScript + "]", e); + } + for (int i = 0; i < scripts.length; i++) { + String script = scripts[i].trim(); + if (StringUtils.hasText(script)) { + jdbcTemplate.execute(scripts[i]); + } + } + return null; + } + + }); + + } + } + + private String stripComments(List list) { + StringBuffer buffer = new StringBuffer(); + for (Iterator iter = list.iterator(); iter.hasNext();) { + String line = (String) iter.next(); + if (!line.startsWith("//") && !line.startsWith("--")) { + buffer.append(line + "\n"); + } + } + return buffer.toString(); + } + + public Class getObjectType() { + return DataSource.class; + } + + public void setInitScript(Resource initScript) { + this.initScript = initScript; + } + + public void setDestroyScript(Resource destroyScript) { + this.destroyScript = destroyScript; + } + + public void setDataSource(DataSource dataSource) { + this.dataSource = dataSource; + } + +} diff --git a/infrastructure/src/test/resources/log4j.properties b/infrastructure/src/test/resources/log4j.properties new file mode 100644 index 000000000..8e19982a3 --- /dev/null +++ b/infrastructure/src/test/resources/log4j.properties @@ -0,0 +1,7 @@ +log4j.rootCategory=INFO, stdout + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - <%m>%n + +log4j.category.org.apache.activemq=ERROR diff --git a/infrastructure/src/test/resources/org/springframework/batch/io/file/support/mapping/bean-wrapper.xml b/infrastructure/src/test/resources/org/springframework/batch/io/file/support/mapping/bean-wrapper.xml new file mode 100644 index 000000000..7a58d250d --- /dev/null +++ b/infrastructure/src/test/resources/org/springframework/batch/io/file/support/mapping/bean-wrapper.xml @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/infrastructure/src/test/resources/org/springframework/batch/io/sql/data-source-context.xml b/infrastructure/src/test/resources/org/springframework/batch/io/sql/data-source-context.xml new file mode 100644 index 000000000..083019ae6 --- /dev/null +++ b/infrastructure/src/test/resources/org/springframework/batch/io/sql/data-source-context.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/infrastructure/src/test/resources/org/springframework/batch/io/sql/destroy-foo-schema-hsqldb.sql b/infrastructure/src/test/resources/org/springframework/batch/io/sql/destroy-foo-schema-hsqldb.sql new file mode 100644 index 000000000..7124ced3e --- /dev/null +++ b/infrastructure/src/test/resources/org/springframework/batch/io/sql/destroy-foo-schema-hsqldb.sql @@ -0,0 +1 @@ +DROP TABLE T_FOOS; \ No newline at end of file diff --git a/infrastructure/src/test/resources/org/springframework/batch/io/sql/init-foo-schema-hsqldb.sql b/infrastructure/src/test/resources/org/springframework/batch/io/sql/init-foo-schema-hsqldb.sql new file mode 100644 index 000000000..2890a4823 --- /dev/null +++ b/infrastructure/src/test/resources/org/springframework/batch/io/sql/init-foo-schema-hsqldb.sql @@ -0,0 +1,13 @@ +CREATE TABLE T_FOOS ( + ID BIGINT NOT NULL, + NAME VARCHAR(45), + VALUE INTEGER +); + +ALTER TABLE T_FOOS ADD PRIMARY KEY (ID); + +INSERT INTO t_foos (id, name, value) VALUES (1, 'bar1', 1); +INSERT INTO t_foos (id, name, value) VALUES (2, 'bar2', 2); +INSERT INTO t_foos (id, name, value) VALUES (3, 'bar3', 3); +INSERT INTO t_foos (id, name, value) VALUES (4, 'bar4', 4); +INSERT INTO t_foos (id, name, value) VALUES (5, 'bar5', 5); diff --git a/infrastructure/src/test/resources/org/springframework/batch/io/xml/20070125.testStream.xmlFileStep.xml b/infrastructure/src/test/resources/org/springframework/batch/io/xml/20070125.testStream.xmlFileStep.xml new file mode 100644 index 000000000..73ec2ad43 --- /dev/null +++ b/infrastructure/src/test/resources/org/springframework/batch/io/xml/20070125.testStream.xmlFileStep.xml @@ -0,0 +1,88 @@ + + + + Gladys Kravitz +
Anytown, PA
+ 34 + 0 + 0 +
+ 2003-01-07 14:16:00 GMT + + + Burnham's Celestial Handbook, Vol 1 + 5 + 21.79 + 2 + + + Burnham's Celestial Handbook, Vol 2 + 5 + 19.89 + 2 + + + + ZipShip + 0.74 + +
+ + + John Smith +
Chicago, IL
+ 46 + 0 + 0 +
+ 2003-01-07 14:16:02 GMT + + + XmlBeans in Action + 3 + 41.29 + 1 + + + JSR-173 + 1 + 11.99 + 5 + + + Teach Yourself XML in 21 days + 1 + 35.49 + 1 + + + + ZipShip + 0.74 + +
+ + + Peter Newman +
Cleveland, OH
+ 23 + 0 + 0 +
+ 2003-01-07 14:16:35 GMT + + + Java 6 + 2 + 12.79 + 3 + + + + UPS + 0.69 + +
+
diff --git a/infrastructure/src/test/resources/org/springframework/batch/io/xml/purchaseorders.xsd b/infrastructure/src/test/resources/org/springframework/batch/io/xml/purchaseorders.xsd new file mode 100644 index 000000000..1536a9859 --- /dev/null +++ b/infrastructure/src/test/resources/org/springframework/batch/io/xml/purchaseorders.xsd @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/infrastructure/src/test/resources/org/springframework/batch/io/xml/test1.xml b/infrastructure/src/test/resources/org/springframework/batch/io/xml/test1.xml new file mode 100644 index 000000000..744ef658a --- /dev/null +++ b/infrastructure/src/test/resources/org/springframework/batch/io/xml/test1.xml @@ -0,0 +1,10 @@ + + Programming Java + John Smith + 480 + + + Programming C# + Kate Newman + 579 + \ No newline at end of file diff --git a/infrastructure/src/test/resources/org/springframework/batch/io/xml/test2.xml b/infrastructure/src/test/resources/org/springframework/batch/io/xml/test2.xml new file mode 100644 index 000000000..5ff36d92a --- /dev/null +++ b/infrastructure/src/test/resources/org/springframework/batch/io/xml/test2.xml @@ -0,0 +1,12 @@ + + + Programming Java + John Smith + 480 + + + Programming C# + Kate Newman + 579 + + \ No newline at end of file diff --git a/infrastructure/src/test/resources/org/springframework/batch/io/xml/xstream/xstream-config-test.xml b/infrastructure/src/test/resources/org/springframework/batch/io/xml/xstream/xstream-config-test.xml new file mode 100644 index 000000000..5d7e97925 --- /dev/null +++ b/infrastructure/src/test/resources/org/springframework/batch/io/xml/xstream/xstream-config-test.xml @@ -0,0 +1,130 @@ + + + 1003 + root_test + + + root-elementAttr_key1 + root-elementAttr_value1 + + + root-elementAttr_key2 + root-elementAttr_value2 + + + + + class-alias_name1 + class-alias_type1 + class-alias_di1 + + + class-alias_name2 + class-alias_type2 + class-alias_di2 + + + + + type-alias_name1 + type-alias_type1 + + + type-alias_name2 + type-alias_type2 + + + + + field-alias_name1 + field-alias_type1 + field1 + + + field-alias_name2 + field-alias_type2 + field2 + + + + + attribute-alias_name1 + attribute-alias_alias1 + + + attribute-alias_name2 + attribute-alias_alias2 + + + + + attribute-properties_type1 + attribute-properties_field1 + + + attribute-properties_type2 + attribute-properties_field2 + + + + + converter.class-name1 + -50 + + + converter.class-name2 + 750 + + + + + ic_owner-type1 + ic_field-name1 + ic_itemField-name1 + ic_item-type1 + + + ic_owner-type2 + ic_field-name2 + ic_item-type2 + + + + + ommited-field_type1 + ommited-field_field1 + + + ommited-field_field2 + ommited-field_type2 + + + + immutable-type1 + immutable-type2 + + + + default-implementation1 + type1 + + + default-implementation2 + type2 + + + + + uri1 + localpart1 + prefix1 + classname1 + + + uri2 + localpart2 + prefix2 + classname2 + + + diff --git a/infrastructure/src/test/resources/org/springframework/batch/repeat/support/trades.csv b/infrastructure/src/test/resources/org/springframework/batch/repeat/support/trades.csv new file mode 100644 index 000000000..fe3d2ece2 --- /dev/null +++ b/infrastructure/src/test/resources/org/springframework/batch/repeat/support/trades.csv @@ -0,0 +1,5 @@ +UK21341EAH45,978,98.34 +UK21341EAH46,112,18.12 +UK21341EAH47,245,12.78 +UK21341EAH48,108,109.25 +UK21341EAH49,854,123.39 \ No newline at end of file diff --git a/integration/.classpath b/integration/.classpath new file mode 100644 index 000000000..ff2b9f569 --- /dev/null +++ b/integration/.classpath @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/integration/.project b/integration/.project new file mode 100644 index 000000000..e3d128c80 --- /dev/null +++ b/integration/.project @@ -0,0 +1,29 @@ + + + batch-integration + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.springframework.ide.eclipse.core.springbuilder + + + + + org.maven.ide.eclipse.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.maven.ide.eclipse.maven2Nature + org.springframework.ide.eclipse.core.springnature + + diff --git a/integration/.springBeans b/integration/.springBeans new file mode 100644 index 000000000..eb3bec74c --- /dev/null +++ b/integration/.springBeans @@ -0,0 +1,42 @@ + + + + xml + + + src/test/resources/org/springframework/batch/jms/jms-context.xml + src/test/resources/org/springframework/jms/asynch.xml + src/test/resources/org/springframework/jms/synch.xml + src/test/resources/org/springframework/jms/tx.xml + src/test/resources/data-source.xml + + + + + true + false + + src/test/resources/data-source.xml + src/test/resources/org/springframework/jms/synch.xml + + + + + true + false + + src/test/resources/data-source.xml + src/test/resources/org/springframework/batch/jms/jms-context.xml + + + + + true + false + + src/test/resources/data-source.xml + src/test/resources/org/springframework/jms/asynch.xml + + + + diff --git a/integration/pom.xml b/integration/pom.xml new file mode 100644 index 000000000..e3156ff55 --- /dev/null +++ b/integration/pom.xml @@ -0,0 +1,80 @@ + + + 4.0.0 + spring-batch-integration + jar + Integration Tests + Integration tests for the Spring Batch Infrastructure + + + org.springframework.batch + 1.0-m2-SNAPSHOT + spring-batch + .. + + + + + org.springframework.batch + spring-batch-infrastructure + ${project.version} + + + hsqldb + hsqldb + 1.8.0.7 + test + + + commons-io + commons-io + 1.2 + + + org.apache.derby + derby + 10.2.1.6 + + + org.apache.activemq + activemq-core + 4.2-incubator-SNAPSHOT + + + commons-logging + commons-logging + + + mx4j + mx4j + + + test + + + easymock + easymock + 1.1 + test + + + + org.apache.geronimo.specs + geronimo-jms_1.1_spec + 1.0 + + + mockobjects + mockobjects-jdk1.4-j2ee1.3 + + + mockobjects + mockobjects-core + + + + + + + diff --git a/integration/src/main/java/org/springframework/batch/container/jms/BatchMessageListenerContainer.java b/integration/src/main/java/org/springframework/batch/container/jms/BatchMessageListenerContainer.java new file mode 100644 index 000000000..a85689926 --- /dev/null +++ b/integration/src/main/java/org/springframework/batch/container/jms/BatchMessageListenerContainer.java @@ -0,0 +1,202 @@ +/* + * 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.container.jms; + +import javax.jms.JMSException; +import javax.jms.Message; +import javax.jms.MessageConsumer; +import javax.jms.Session; + +import org.springframework.batch.repeat.RepeatCallback; +import org.springframework.batch.repeat.RepeatContext; +import org.springframework.batch.repeat.RepeatOperations; +import org.springframework.batch.repeat.ExitStatus; +import org.springframework.jms.connection.TransactionAwareConnectionFactoryProxy; +import org.springframework.jms.listener.DefaultMessageListenerContainer; +import org.springframework.jms.listener.adapter.MessageListenerAdapter; + +/** + * Message listener container adapted for batching the message processing. + * Instead of receiving a single message and processing it, we use a + * {@link RepeatOperations} to receive multiple messages in the same thread. Use + * with a transactional {@link RepeatOperations} and either an XA connection + * factory, or the {@link TransactionAwareConnectionFactoryProxy} to synchronize + * the JMS session with an ongoing transaction. + * + * @author Dave Syer + * + */ +public class BatchMessageListenerContainer extends DefaultMessageListenerContainer { + + private RepeatOperations template; + + private ThreadLocal messageHolder = new ThreadLocal(); + + /** + * Create a new {@link BatchMessageListenerContainer}. The container is set + * with auto startup = false (not the default of the parent container). + * + * @param template a {@link RepeatOperations}. It is advisable to set the + * {@link RepeatOperations} with a sensible termination policy, like a small + * fixed chunk size. + */ + public BatchMessageListenerContainer(RepeatOperations template) { + super(); + this.template = template; + setAutoStartup(false); + // Avoid error on startup... + // http://opensource.atlassian.com/projects/spring/browse/SPR-3154 + setMessageListener(new MessageListenerAdapter()); + } + + /** + * Override base class method to store message in a thread local for later + * use. + * + * @see org.springframework.jms.listener.AbstractPollingMessageListenerContainer#receiveMessage(javax.jms.MessageConsumer) + */ + protected Message receiveMessage(MessageConsumer consumer) throws JMSException { + Message message = super.receiveMessage(consumer); + if (message!=null) { + messageHolder.set(message); + } + return message; + } + + /** + * Override base class method to enable the message holder to be reset, + * signalling that a rollback has occurred. + * + * @see org.springframework.jms.listener.AbstractMessageListenerContainer#rollbackOnExceptionIfNecessary(javax.jms.Session, + * java.lang.Throwable) + */ + protected void rollbackOnExceptionIfNecessary(Session session, Throwable ex) throws JMSException { + super.rollbackOnExceptionIfNecessary(session, ex); + if (session.getTransacted() && isSessionTransacted()) { + messageHolder.set(null); + } + } + + /** + * Override base class to allow extra processing in the case of exception, + * with knowledge of the message. + * + * @see org.springframework.jms.listener.AbstractMessageListenerContainer#doExecuteListener(javax.jms.Session, + * javax.jms.Message) + */ + protected void doExecuteListener(Session session, Message message) throws JMSException { + try { + super.doExecuteListener(session, message); + } + catch (Throwable ex) { + handleListenerException(session, message, ex); + } + } + + /** + * Extension point for subclasses. Do anything necessary to recover from the + * exception, which was raised when the message was being processed. + * @param session the current JMS session. + * @param message the message just receieved and failed to process. + * @param ex the exception thrown during message processing. + */ + protected void recover(Session session, Message message, Throwable ex) throws JMSException { + // do nothing... + } + + /** + * Used to provide a recovery path - delegates to + * {@link #recover(Session, Message, Throwable)}. TODO: Could be merged + * into base class? + * @param session the JMS session + * @param message the last message + * @param ex the exception thrown by listener + * @see #doExecuteListener(Session, Message) + * @see #recover(Session, Message, Throwable) + */ + protected final void handleListenerException(Session session, Message message, Throwable ex) throws JMSException { + // Call out to recovery path... + recover(session, message, ex); + if (ex instanceof RuntimeException) { + // We need to rethrow so that an enclosing non-JMS transaction can + // rollback... + throw (RuntimeException) ex; + } + else if (ex instanceof Error) { + // Just re-throw Error instances because otherwise unit tests just + // swallow expections from EasyMock and JUnit. + throw (Error) ex; + } + } + + /** + * Override base class method to wrap call in a batch. + * @see org.springframework.jms.listener.AbstractPollingMessageListenerContainer#receiveAndExecute(javax.jms.Session, + * javax.jms.MessageConsumer) + */ + protected boolean receiveAndExecute(final Session session, final MessageConsumer consumer) throws JMSException { + + template.iterate(new RepeatCallback() { + public ExitStatus doInIteration(RepeatContext context) throws Exception { + return doBatchCallBack(session, consumer); + } + }); + + if (messageHolder.get()==null) { + return false; + } + + messageHolder.set(null); + return true; + } + + /** + * Wraps a call to {@link #receiveAndExecute(Session, MessageConsumer)}, + * retrieving the message from thread local. + * + * @param session + * @param consumer + * @return + * @throws JMSException + * + * @see {@link #receiveMessage(MessageConsumer)} + */ + protected ExitStatus doBatchCallBack(Session session, MessageConsumer consumer) throws JMSException { + /* + * The base class receiveAndExecute is transactional (if configured). We + * could extend the tx boundary to the whole batch by making the + * template.execute transactional, and either switch off the tx manager + * in this object, or live with its default propagation=REQUIRED + * behaviour. + * + * But if the super class transaction manager is a + * JmsTransactionManager, which is the normal choice for a message + * listener container (see + * http://opensource.atlassian.com/projects/spring/browse/SPR-3156), + * then it will not behave as expected. In particular since the + * JmsTransactionManager is not aware of the batch template (execute or + * callback) transactions, it will commit message sessions that should + * be rolled back when a batch fails. + */ + if (super.receiveAndExecute(session, consumer)) { + Object message = messageHolder.get(); + return new ExitStatus(message!=null); + } + return ExitStatus.FINISHED; + } + +} diff --git a/integration/src/main/java/test/jdbc/datasource/DerbyDataSourceFactoryBean.java b/integration/src/main/java/test/jdbc/datasource/DerbyDataSourceFactoryBean.java new file mode 100644 index 000000000..82da4b7b1 --- /dev/null +++ b/integration/src/main/java/test/jdbc/datasource/DerbyDataSourceFactoryBean.java @@ -0,0 +1,109 @@ +package test.jdbc.datasource; + +import java.io.File; +import java.io.IOException; + +import javax.sql.DataSource; + +import org.apache.commons.io.IOUtils; +import org.apache.derby.jdbc.EmbeddedDataSource; +import org.springframework.beans.factory.BeanInitializationException; +import org.springframework.beans.factory.config.AbstractFactoryBean; +import org.springframework.core.io.Resource; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.TransactionCallback; +import org.springframework.transaction.support.TransactionTemplate; +import org.springframework.util.StringUtils; + +public class DerbyDataSourceFactoryBean extends AbstractFactoryBean { + + private String dataDirectory = "derby-home"; + + private Resource initScript; + + private Resource destroyScript; + + DataSource dataSource; + + public void destroy() throws Exception { + super.destroy(); + + try { + doExecuteScript(destroyScript); + } + catch (Exception e) { + logger.warn("Could not execute destroy script [" + destroyScript + "]", e); + } + } + + public void setDataDirectory(String dataDirectory) { + this.dataDirectory = dataDirectory; + } + + protected Object createInstance() throws Exception { + File directory = new File(dataDirectory); + System.setProperty("derby.system.home", directory.getCanonicalPath()); + System.setProperty("derby.storage.fileSyncTransactionLog", "true"); + System.setProperty("derby.storage.pageCacheSize", "100"); + + final EmbeddedDataSource ds = new EmbeddedDataSource(); + ds.setDatabaseName("derbydb"); + ds.setCreateDatabase("create"); + dataSource = ds; + + try { + doExecuteScript(destroyScript); + } + catch (Exception e) { + logger.debug("Could not execute destroy script [" + destroyScript + "]", e); + } + doExecuteScript(initScript); + return ds; + } + + private void doExecuteScript(final Resource scriptResource) { + if (scriptResource == null || !scriptResource.exists()) + return; + TransactionTemplate transactionTemplate = new TransactionTemplate(new DataSourceTransactionManager(dataSource)); + if (initScript != null) { + transactionTemplate.execute(new TransactionCallback() { + + public Object doInTransaction(TransactionStatus status) { + JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); + String[] scripts; + try { + scripts = StringUtils.delimitedListToStringArray(IOUtils.toString(scriptResource + .getInputStream()), ";"); + } + catch (IOException e) { + throw new BeanInitializationException("Cannot load script from [" + initScript + "]", e); + } + for (int i = 0; i < scripts.length; i++) { + String script = scripts[i].trim(); + if (StringUtils.hasText(script)) { + jdbcTemplate.execute(scripts[i]); + } + } + return null; + } + + }); + + } + } + + public Class getObjectType() { + return DataSource.class; + } + + public void setInitScript(Resource initScript) { + this.initScript = initScript; + } + + public void setDestroyScript(Resource destroyScript) { + this.destroyScript = destroyScript; + } + +} diff --git a/integration/src/main/java/test/jdbc/datasource/InitializingDataSourceFactoryBean.java b/integration/src/main/java/test/jdbc/datasource/InitializingDataSourceFactoryBean.java new file mode 100644 index 000000000..210d73b57 --- /dev/null +++ b/integration/src/main/java/test/jdbc/datasource/InitializingDataSourceFactoryBean.java @@ -0,0 +1,131 @@ +/* + * 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 test.jdbc.datasource; + +import java.io.IOException; +import java.util.Iterator; +import java.util.List; + +import javax.sql.DataSource; + +import org.apache.commons.io.IOUtils; +import org.springframework.beans.factory.BeanInitializationException; +import org.springframework.beans.factory.config.AbstractFactoryBean; +import org.springframework.core.io.Resource; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.TransactionCallback; +import org.springframework.transaction.support.TransactionTemplate; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +public class InitializingDataSourceFactoryBean extends AbstractFactoryBean { + + private Resource initScript; + + private Resource destroyScript; + + DataSource dataSource; + + public void destroy() throws Exception { + super.destroy(); + + try { + doExecuteScript(destroyScript); + } + catch (Exception e) { + logger.warn("Could not execute destroy script [" + destroyScript + "]", e); + } + } + + public void afterPropertiesSet() throws Exception { + Assert.notNull(dataSource); + super.afterPropertiesSet(); + } + + protected Object createInstance() throws Exception { + Assert.notNull(dataSource); + try { + doExecuteScript(destroyScript); + } + catch (Exception e) { + logger.debug("Could not execute destroy script [" + destroyScript + "]", e); + } + doExecuteScript(initScript); + return dataSource; + } + + private void doExecuteScript(final Resource scriptResource) { + if (scriptResource == null || !scriptResource.exists()) + return; + TransactionTemplate transactionTemplate = new TransactionTemplate(new DataSourceTransactionManager(dataSource)); + if (initScript != null) { + transactionTemplate.execute(new TransactionCallback() { + + public Object doInTransaction(TransactionStatus status) { + JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); + String[] scripts; + try { + scripts = StringUtils.delimitedListToStringArray(stripComments(IOUtils.readLines(scriptResource + .getInputStream())), ";"); + } + catch (IOException e) { + throw new BeanInitializationException("Cannot load script from [" + initScript + "]", e); + } + for (int i = 0; i < scripts.length; i++) { + String script = scripts[i].trim(); + if (StringUtils.hasText(script)) { + jdbcTemplate.execute(scripts[i]); + } + } + return null; + } + + }); + + } + } + + private String stripComments(List list) { + StringBuffer buffer = new StringBuffer(); + for (Iterator iter = list.iterator(); iter.hasNext();) { + String line = (String) iter.next(); + if (!line.startsWith("//") && !line.startsWith("--")) { + buffer.append(line + "\n"); + } + } + return buffer.toString(); + } + + public Class getObjectType() { + return DataSource.class; + } + + public void setInitScript(Resource initScript) { + this.initScript = initScript; + } + + public void setDestroyScript(Resource destroyScript) { + this.destroyScript = destroyScript; + } + + public void setDataSource(DataSource dataSource) { + this.dataSource = dataSource; + } + +} diff --git a/integration/src/site/site.xml b/integration/src/site/site.xml new file mode 100644 index 000000000..b3fe5eca5 --- /dev/null +++ b/integration/src/site/site.xml @@ -0,0 +1,31 @@ + + + + Spring Batch: ${project.name} + + + images/shim.gif + + + + + + org.springframework.maven.skins + maven-spring-skin + 1.0.2 + + + + + + + + + + + + + + + + diff --git a/integration/src/test/java/org/springframework/batch/config/DatasourceTests.java b/integration/src/test/java/org/springframework/batch/config/DatasourceTests.java new file mode 100644 index 000000000..405e09fbb --- /dev/null +++ b/integration/src/test/java/org/springframework/batch/config/DatasourceTests.java @@ -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.config; + +import org.springframework.test.AbstractTransactionalDataSourceSpringContextTests; + +public class DatasourceTests extends AbstractTransactionalDataSourceSpringContextTests { + + protected String[] getConfigLocations() { + return new String[] { "/org/springframework/batch/jms/jms-context.xml" }; + } + + public void testTemplate() throws Exception { + System.err.println(System.getProperty("java.class.path")); + jdbcTemplate.execute("delete from T_FOOS"); + int count = jdbcTemplate.queryForInt("select count(*) from T_FOOS"); + assertEquals(0, count); + + jdbcTemplate.update("INSERT into T_FOOS (id,name,foo_date) values (?,?,null)", new Object[] { Integer.valueOf(0), + "foo" }); + } +} diff --git a/integration/src/test/java/org/springframework/batch/config/MessagingTests.java b/integration/src/test/java/org/springframework/batch/config/MessagingTests.java new file mode 100644 index 000000000..e9d3a3c42 --- /dev/null +++ b/integration/src/test/java/org/springframework/batch/config/MessagingTests.java @@ -0,0 +1,62 @@ +/* + * 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.config; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.jms.core.JmsTemplate; +import org.springframework.test.AbstractDependencyInjectionSpringContextTests; + +public class MessagingTests extends AbstractDependencyInjectionSpringContextTests { + + private JmsTemplate jmsTemplate; + + public void setJmsTemplate(JmsTemplate jmsTemplate) { + this.jmsTemplate = jmsTemplate; + } + + protected String[] getConfigLocations() { + return new String[] { "/org/springframework/batch/jms/jms-context.xml" }; + } + + protected void onSetUp() throws Exception { + super.onSetUp(); + Thread.sleep(100L); + getMessages(); // drain queue + jmsTemplate.convertAndSend("queue", "foo"); + jmsTemplate.convertAndSend("queue", "bar"); + } + + public void testMessaging() throws Exception { + List list = getMessages(); + System.err.println(list); + assertEquals(2, list.size()); + assertTrue(list.contains("foo")); + } + + private List getMessages() { + String next = ""; + List msgs = new ArrayList(); + while (next != null) { + next = (String) jmsTemplate.receiveAndConvert("queue"); + if (next != null) + msgs.add(next); + } + return msgs; + } +} diff --git a/integration/src/test/java/org/springframework/batch/container/jms/BatchMessageListenerContainerTests.java b/integration/src/test/java/org/springframework/batch/container/jms/BatchMessageListenerContainerTests.java new file mode 100644 index 000000000..224a50a57 --- /dev/null +++ b/integration/src/test/java/org/springframework/batch/container/jms/BatchMessageListenerContainerTests.java @@ -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.container.jms; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import javax.jms.JMSException; +import javax.jms.Message; +import javax.jms.MessageConsumer; +import javax.jms.MessageListener; +import javax.jms.Session; + +import junit.framework.TestCase; + +import org.easymock.MockControl; +import org.springframework.batch.container.jms.BatchMessageListenerContainer; +import org.springframework.batch.repeat.RepeatCallback; +import org.springframework.batch.repeat.ExitStatus; +import org.springframework.batch.repeat.policy.SimpleCompletionPolicy; +import org.springframework.batch.repeat.support.RepeatTemplate; +import org.springframework.util.ReflectionUtils; + +public class BatchMessageListenerContainerTests extends TestCase { + + BatchMessageListenerContainer container; + + int count = 0; + + public void testReceiveAndExecuteWithNoCallback() throws Exception { + RepeatTemplate template = new RepeatTemplate() { + public ExitStatus iterate(RepeatCallback callback) { + count++; + return ExitStatus.CONTINUABLE; // means we can continue to operate, but no message is received + } + }; + container = new BatchMessageListenerContainer(template); + boolean received = doExecute(null, null); + assertEquals(1, count); + assertFalse("Message received", received); + } + + public void testReceiveAndExecuteWithCallback() throws Exception { + RepeatTemplate template = new RepeatTemplate(); + template.setCompletionPolicy(new SimpleCompletionPolicy(2)); + container = new BatchMessageListenerContainer(template); + + MockControl sessionControl = MockControl.createNiceControl(Session.class); + MockControl consumerControl = MockControl.createControl(MessageConsumer.class); + MockControl messageControl = MockControl.createControl(Message.class); + + Session session = (Session) sessionControl.getMock(); + MessageConsumer consumer = (MessageConsumer) consumerControl.getMock(); + Message message = (Message) messageControl.getMock(); + + // Expect two calls to consumer (chunk size)... + consumerControl.expectAndReturn(consumer.receive(1000), message); + consumerControl.expectAndReturn(consumer.receive(1000), message); + + sessionControl.replay(); + consumerControl.replay(); + messageControl.replay(); + + boolean received = doExecute(session, consumer); + assertTrue("Message not received", received); + + sessionControl.verify(); + consumerControl.verify(); + messageControl.verify(); + + } + + public void testReceiveAndExecuteWithCallbackReturningNull() throws Exception { + RepeatTemplate template = new RepeatTemplate(); + template.setCompletionPolicy(new SimpleCompletionPolicy(2)); + container = new BatchMessageListenerContainer(template); + + MockControl sessionControl = MockControl.createNiceControl(Session.class); + MockControl consumerControl = MockControl.createControl(MessageConsumer.class); + + Session session = (Session) sessionControl.getMock(); + MessageConsumer consumer = (MessageConsumer) consumerControl.getMock(); + Message message = null; + + // Expect one call to consumer (chunk size is 2 but terminates on + // first)... + consumerControl.expectAndReturn(consumer.receive(1000), message); + + sessionControl.replay(); + consumerControl.replay(); + + boolean received = doExecute(session, consumer); + assertFalse("Message not received", received); + + sessionControl.verify(); + consumerControl.verify(); + + } + + public void testTransactionalReceiveAndExecuteWithCallbackThrowingException() throws Exception { + RepeatTemplate template = new RepeatTemplate(); + template.setCompletionPolicy(new SimpleCompletionPolicy(2)); + container = new BatchMessageListenerContainer(template); + container.setSessionTransacted(true); + boolean received = doTestWithException(new IllegalStateException("No way!"), true, 2); + assertFalse("Message received", received); + } + + public void testNonTransactionalReceiveAndExecuteWithCallbackThrowingException() throws Exception { + RepeatTemplate template = new RepeatTemplate(); + template.setCompletionPolicy(new SimpleCompletionPolicy(2)); + container = new BatchMessageListenerContainer(template); + container.setSessionTransacted(false); + boolean received = doTestWithException(new IllegalStateException("No way!"), false, 2); + assertTrue("Message not received", received); + } + + public void testNonTransactionalReceiveAndExecuteWithCallbackThrowingError() throws Exception { + RepeatTemplate template = new RepeatTemplate(); + template.setCompletionPolicy(new SimpleCompletionPolicy(2)); + container = new BatchMessageListenerContainer(template); + container.setSessionTransacted(false); + try { + boolean received = doTestWithException(new RuntimeException("No way!"), false, 2); + assertTrue("Message not received", received); + } + catch (RuntimeException e) { + assertEquals("No way!", e.getMessage()); + fail("Unexpected Error - should be swallowed"); + } + } + + private boolean doTestWithException(final Throwable t, boolean expectRollback, int expectGetTransactionCount) + throws JMSException, IllegalAccessException { + container.setAcceptMessagesWhileStopping(true); + container.setMessageListener(new MessageListener() { + public void onMessage(Message arg0) { + if (t instanceof RuntimeException) + throw (RuntimeException) t; + else + throw (Error) t; + } + }); + + MockControl sessionControl = MockControl.createNiceControl(Session.class); + MockControl consumerControl = MockControl.createNiceControl(MessageConsumer.class); + MockControl messageControl = MockControl.createNiceControl(Message.class); + + Session session = (Session) sessionControl.getMock(); + MessageConsumer consumer = (MessageConsumer) consumerControl.getMock(); + Message message = (Message) messageControl.getMock(); + + sessionControl.expectAndReturn(session.getTransacted(), true, expectGetTransactionCount); + + // Expect only one call to consumer (chunk size is 2, but first one + // rolls back terminating batch)... + consumerControl.expectAndReturn(consumer.receive(1000), message); + if (expectRollback) { + session.rollback(); + sessionControl.setVoidCallable(); + } + + sessionControl.replay(); + consumerControl.replay(); + messageControl.replay(); + + boolean received = doExecute(session, consumer); + + sessionControl.verify(); + consumerControl.verify(); + messageControl.verify(); + return received; + } + + private boolean doExecute(Session session, MessageConsumer consumer) throws IllegalAccessException { + Method method = ReflectionUtils.findMethod(container.getClass(), "receiveAndExecute", new Class[] { + Session.class, MessageConsumer.class }); + method.setAccessible(true); + boolean received; + try { + received = ((Boolean) method.invoke(container, new Object[] { session, consumer })).booleanValue(); + } + catch (InvocationTargetException e) { + if (e.getCause() instanceof RuntimeException) { + throw (RuntimeException) e.getCause(); + } else { + throw (Error) e.getCause(); + } + } + return received; + } + +} diff --git a/integration/src/test/java/org/springframework/batch/jms/ExternalRetryInBatchTests.java b/integration/src/test/java/org/springframework/batch/jms/ExternalRetryInBatchTests.java new file mode 100644 index 000000000..519eff2ec --- /dev/null +++ b/integration/src/test/java/org/springframework/batch/jms/ExternalRetryInBatchTests.java @@ -0,0 +1,196 @@ +/* + * 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.jms; + +import java.util.ArrayList; +import java.util.List; + +import javax.sql.DataSource; + +import org.springframework.batch.item.ItemProcessor; +import org.springframework.batch.item.ItemProvider; +import org.springframework.batch.item.provider.AbstractItemProvider; +import org.springframework.batch.repeat.RepeatCallback; +import org.springframework.batch.repeat.RepeatContext; +import org.springframework.batch.repeat.ExitStatus; +import org.springframework.batch.repeat.policy.SimpleCompletionPolicy; +import org.springframework.batch.repeat.support.RepeatTemplate; +import org.springframework.batch.retry.callback.ItemProviderRetryCallback; +import org.springframework.batch.retry.policy.ItemProviderRetryPolicy; +import org.springframework.batch.retry.policy.SimpleRetryPolicy; +import org.springframework.batch.retry.support.RetryTemplate; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jms.core.JmsTemplate; +import org.springframework.test.AbstractDependencyInjectionSpringContextTests; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.TransactionCallback; +import org.springframework.transaction.support.TransactionTemplate; + +public class ExternalRetryInBatchTests extends AbstractDependencyInjectionSpringContextTests { + private JmsTemplate jmsTemplate; + + private RetryTemplate retryTemplate; + + private RepeatTemplate repeatTemplate; + + private ItemProvider provider; + + private JdbcTemplate jdbcTemplate; + + private PlatformTransactionManager transactionManager; + + public void setDataSource(DataSource dataSource) { + jdbcTemplate = new JdbcTemplate(dataSource); + } + + public void setTransactionManager(PlatformTransactionManager transactionManager) { + this.transactionManager = transactionManager; + } + + public void setRepeatTemplate(RepeatTemplate repeatTemplate) { + this.repeatTemplate = repeatTemplate; + } + + public void setJmsTemplate(JmsTemplate jmsTemplate) { + this.jmsTemplate = jmsTemplate; + } + + protected String[] getConfigLocations() { + return new String[] { "/org/springframework/batch/jms/jms-context.xml" }; + } + + protected void onSetUp() throws Exception { + super.onSetUp(); + getMessages(); // drain queue + jdbcTemplate.execute("delete from T_FOOS"); + jmsTemplate.convertAndSend("queue", "foo"); + jmsTemplate.convertAndSend("queue", "bar"); + provider = new AbstractItemProvider() { + public Object next() { + String text = (String) jmsTemplate.receiveAndConvert("queue"); + list.add(text); + return text; + } + + public boolean recover(Object data, Throwable cause) { + recovered.add(data); + return true; + } + }; + retryTemplate = new RetryTemplate(); + } + + protected void onTearDown() throws Exception { + getMessages(); // drain queue + jdbcTemplate.execute("delete from T_FOOS"); + } + + private void assertInitialState() { + int count = jdbcTemplate.queryForInt("select count(*) from T_FOOS"); + assertEquals(0, count); + } + + private List list = new ArrayList(); + + private List recovered = new ArrayList(); + + public void testExternalRetryRecoveryInBatch() throws Exception { + assertInitialState(); + + retryTemplate.setRetryPolicy(new ItemProviderRetryPolicy(new SimpleRetryPolicy(1))); + + final ItemProviderRetryCallback callback = new ItemProviderRetryCallback(provider, new ItemProcessor() { + public void process(final Object text) { + // No need for transaction here: the whole batch will roll + // back. When it comes back for recovery this code is not + // executed... + jdbcTemplate.update("INSERT into T_FOOS (id,name,foo_date) values (?,?,null)", new Object[] { + Integer.valueOf(list.size()), text }); + throw new RuntimeException("Rollback!"); + } + }); + + repeatTemplate.setCompletionPolicy(new SimpleCompletionPolicy(2)); + + // In a real container this could be an outer retry loop with an + // *internal* retry policy. + for (int i = 0; i < 4; i++) { + try { + new TransactionTemplate(transactionManager).execute(new TransactionCallback() { + public Object doInTransaction(TransactionStatus status) { + try { + + repeatTemplate.iterate(new RepeatCallback() { + + public ExitStatus doInIteration(RepeatContext context) throws Exception { + return new ExitStatus(retryTemplate.execute(callback)!=null); + } + + }); + return null; + + } + catch (Exception e) { + throw new RuntimeException(e.getMessage(), e); + } + } + }); + } + catch (Exception e) { + + if (i == 0 || i == 2) { + assertEquals("Rollback!", e.getMessage()); + } + else { + throw e; + } + + } + finally { + System.err.println(i + ": " + recovered); + } + } + + List msgs = getMessages(); + + System.err.println(msgs); + + assertEquals(2, recovered.size()); + + // The database portion committed once... + int count = jdbcTemplate.queryForInt("select count(*) from T_FOOS"); + assertEquals(0, count); + + // ... and so did the message session. + // Both messages were failed and recovered after last retry attempt: + assertEquals("[]", msgs.toString()); + assertEquals("[foo, bar]", recovered.toString()); + + } + + private List getMessages() { + String next = ""; + List msgs = new ArrayList(); + while (next != null) { + next = (String) jmsTemplate.receiveAndConvert("queue"); + if (next != null) + msgs.add(next); + } + return msgs; + } +} diff --git a/integration/src/test/java/org/springframework/batch/repeat/jms/AsynchronousTests.java b/integration/src/test/java/org/springframework/batch/repeat/jms/AsynchronousTests.java new file mode 100644 index 000000000..9aabe82fa --- /dev/null +++ b/integration/src/test/java/org/springframework/batch/repeat/jms/AsynchronousTests.java @@ -0,0 +1,160 @@ +/* + * 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.repeat.jms; + +import java.util.ArrayList; +import java.util.List; + +import javax.jms.JMSException; +import javax.jms.Message; +import javax.jms.Session; +import javax.jms.TextMessage; + +import org.springframework.batch.container.jms.BatchMessageListenerContainer; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jms.core.JmsTemplate; +import org.springframework.jms.listener.SessionAwareMessageListener; +import org.springframework.test.AbstractDependencyInjectionSpringContextTests; + +public class AsynchronousTests extends AbstractDependencyInjectionSpringContextTests { + + protected String[] getConfigLocations() { + return new String[] { "/org/springframework/batch/jms/jms-context.xml" }; + } + + private BatchMessageListenerContainer container; + + private JmsTemplate jmsTemplate; + + private JdbcTemplate jdbcTemplate; + + public void setJdbcTemplate(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + public void setJmsTemplate(JmsTemplate jmsTemplate) { + this.jmsTemplate = jmsTemplate; + } + + public void setContainer(BatchMessageListenerContainer container) { + this.container = container; + } + + protected void onSetUp() throws Exception { + super.onSetUp(); + String foo = ""; + int count = 0; + while (foo != null && count < 100) { + foo = (String) jmsTemplate.receiveAndConvert("queue"); + count++; + } + jdbcTemplate.execute("delete from T_FOOS"); + + // Queue is now drained... + assertNull(foo); + + // Add a couple of messages... + jmsTemplate.convertAndSend("queue", "foo"); + jmsTemplate.convertAndSend("queue", "bar"); + } + + protected void onTearDown() throws Exception { + super.onTearDown(); + container.stop(); + // Need to give the container time to shutdown + Thread.sleep(1000L); + } + + List list = new ArrayList(); + + private void assertInitialState() { + int count = jdbcTemplate.queryForInt("select count(*) from T_FOOS"); + assertEquals(0, count); + } + + public void testSunnyDay() throws Exception { + + assertInitialState(); + + container.setMessageListener(new SessionAwareMessageListener() { + public void onMessage(Message message, Session session) throws JMSException { + list.add(message.toString()); + String text = ((TextMessage) message).getText(); + jdbcTemplate.update("INSERT into T_FOOS (id,name,foo_date) values (?,?,null)", new Object[] { + Integer.valueOf(list.size()), text }); + } + }); + + container.start(); + + // Need to sleep for at least a second here... + Thread.sleep(1000L); + + System.err.println(jdbcTemplate.queryForList("select * from T_FOOS")); + + assertEquals(2, list.size()); + + String foo = (String) jmsTemplate.receiveAndConvert("queue"); + assertEquals(null, foo); + + int count = jdbcTemplate.queryForInt("select count(*) from T_FOOS"); + assertEquals(2, count); + + } + + public void testRollback() throws Exception { + + assertInitialState(); + + container.setMessageListener(new SessionAwareMessageListener() { + public void onMessage(Message message, Session session) throws JMSException { + list.add(message.toString()); + final String text = ((TextMessage) message).getText(); + jdbcTemplate.update("INSERT into T_FOOS (id,name,foo_date) values (?,?,null)", new Object[] { + new Integer(list.size()), text }); + // This causes the DB to rollback but not the message + if (text.equals("bar")) { + throw new RuntimeException("Rollback!"); + } + } + }); + + container.start(); + + // Need to sleep for at least a second here... + Thread.sleep(1000L); + + // We rolled back so the messages might come in many times... + assertTrue(list.size() >= 1); + + System.err.println(jdbcTemplate.queryForList("select * from T_FOOS")); + + String text = ""; + List msgs = new ArrayList(); + while (text != null) { + text = (String) jmsTemplate.receiveAndConvert("queue"); + msgs.add(text); + } + System.err.println(msgs); + + int count = jdbcTemplate.queryForInt("select count(*) from T_FOOS"); + assertEquals(0, count); + + assertTrue("Foo not on queue", msgs.contains("foo")); + + } +} diff --git a/integration/src/test/java/org/springframework/batch/repeat/jms/SynchronousTests.java b/integration/src/test/java/org/springframework/batch/repeat/jms/SynchronousTests.java new file mode 100644 index 000000000..1fb086655 --- /dev/null +++ b/integration/src/test/java/org/springframework/batch/repeat/jms/SynchronousTests.java @@ -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.repeat.jms; + +import java.util.ArrayList; +import java.util.List; + +import javax.jms.ConnectionFactory; +import javax.jms.JMSException; +import javax.jms.Session; + +import org.springframework.batch.repeat.ExitStatus; +import org.springframework.batch.repeat.RepeatCallback; +import org.springframework.batch.repeat.RepeatContext; +import org.springframework.batch.repeat.support.RepeatTemplate; +import org.springframework.jms.connection.SessionProxy; +import org.springframework.jms.core.JmsTemplate; +import org.springframework.jms.core.SessionCallback; +import org.springframework.test.AbstractTransactionalDataSourceSpringContextTests; +import org.springframework.transaction.support.TransactionSynchronizationAdapter; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +public class SynchronousTests extends AbstractTransactionalDataSourceSpringContextTests { + + private JmsTemplate jmsTemplate; + + private RepeatTemplate repeatTemplate; + + public void setJmsTemplate(JmsTemplate jmsTemplate) { + this.jmsTemplate = jmsTemplate; + } + + public void setRepeatTemplate(RepeatTemplate repeatTemplate) { + this.repeatTemplate = repeatTemplate; + } + + protected String[] getConfigLocations() { + return new String[] { "/org/springframework/batch/jms/jms-context.xml" }; + } + + protected void onSetUpBeforeTransaction() throws Exception { + super.onSetUpBeforeTransaction(); + String foo = ""; + int count = 0; + while (foo != null && count < 100) { + foo = (String) jmsTemplate.receiveAndConvert("queue"); + count++; + } + jdbcTemplate.execute("delete from T_FOOS"); + jmsTemplate.convertAndSend("queue", "foo"); + jmsTemplate.convertAndSend("queue", "bar"); + } + + protected void onSetUpInTransaction() throws Exception { + super.onSetUpInTransaction(); + } + + private void assertInitialState() { + int count = jdbcTemplate.queryForInt("select count(*) from T_FOOS"); + assertEquals(0, count); + } + + List list = new ArrayList(); + + public void testCommit() throws Exception { + + assertInitialState(); + + repeatTemplate.iterate(new RepeatCallback() { + public ExitStatus doInIteration(RepeatContext context) throws Exception { + String text = (String) jmsTemplate.receiveAndConvert("queue"); + list.add(text); + jdbcTemplate.update("INSERT into T_FOOS (id,name,foo_date) values (?,?,null)", new Object[] { + Integer.valueOf(list.size()), text }); + return new ExitStatus(text != null); + } + }); + + // force commit... + setComplete(); + endTransaction(); + startNewTransaction(); + + System.err.println(jdbcTemplate.queryForList("select * from T_FOOS")); + + // Database committed so this resord should be there... + int count = jdbcTemplate.queryForInt("select count(*) from T_FOOS"); + assertEquals(2, count); + + // ... the commit should also have cleared the queue, so this should now + // be null + String text = (String) jmsTemplate.receiveAndConvert("queue"); + assertEquals(null, text); + + } + + public void testFullRollback() throws Exception { + + assertInitialState(); + repeatTemplate.iterate(new RepeatCallback() { + public ExitStatus doInIteration(RepeatContext context) throws Exception { + String text = (String) jmsTemplate.receiveAndConvert("queue"); + list.add(text); + jdbcTemplate.update("INSERT into T_FOOS (id,name,foo_date) values (?,?,null)", new Object[] { + Integer.valueOf(list.size()), text }); + return new ExitStatus(text != null); + } + }); + + // force rollback... + endTransaction(); + startNewTransaction(); + + String text = ""; + List msgs = new ArrayList(); + while (text != null) { + text = (String) jmsTemplate.receiveAndConvert("queue"); + msgs.add(text); + } + + // The database portion rolled back... + int count = jdbcTemplate.queryForInt("select count(*) from T_FOOS"); + assertEquals(0, count); + + // ... and so did the message session. The rollback should have restored + // the queue, so this should now be non-null + assertTrue("Foo not on queue", msgs.contains("foo")); + } + + public void testPartialRollback() throws Exception { + + // The JmsTemplate is used elsewhere outside a transaction, so + // we need to use one here that is transaction aware. + final JmsTemplate jmsTemplate = new JmsTemplate((ConnectionFactory) applicationContext + .getBean("txAwareConnectionFactory")); + jmsTemplate.setReceiveTimeout(100L); + jmsTemplate.setSessionTransacted(true); + + assertInitialState(); + repeatTemplate.iterate(new RepeatCallback() { + public ExitStatus doInIteration(RepeatContext context) throws Exception { + String text = (String) jmsTemplate.receiveAndConvert("queue"); + list.add(text); + jdbcTemplate.update("INSERT into T_FOOS (id,name,foo_date) values (?,?,null)", new Object[] { + Integer.valueOf(list.size()), text }); + return new ExitStatus(text != null); + } + }); + + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() { + public void beforeCommit(boolean readOnly) { + // Simulate a message system failure before the main transaction + // commits... + jmsTemplate.execute(new SessionCallback() { + public Object doInJms(Session session) throws JMSException { + try { + assertTrue("Not a SessionProxy - wrong spring version?", session instanceof SessionProxy); + ((SessionProxy) session).getTargetSession().rollback(); + } + catch (JMSException e) { + throw e; + } + catch (Exception e) { + // swallow it + e.printStackTrace(); + } + return null; + } + }); + } + }); + // force commit... + setComplete(); + endTransaction(); + startNewTransaction(); + + String text = ""; + List msgs = new ArrayList(); + while (text != null) { + text = (String) jmsTemplate.receiveAndConvert("queue"); + msgs.add(text); + } + + // The database portion committed... + int count = jdbcTemplate.queryForInt("select count(*) from T_FOOS"); + assertEquals(2, count); + + // ...but the JMS session rolled back, so the message is still there + assertTrue("Foo not on queue", msgs.contains("foo")); + assertTrue("Bar not on queue", msgs.contains("bar")); + + } +} diff --git a/integration/src/test/java/org/springframework/batch/retry/jms/ExternalRetryTests.java b/integration/src/test/java/org/springframework/batch/retry/jms/ExternalRetryTests.java new file mode 100644 index 000000000..820d7896c --- /dev/null +++ b/integration/src/test/java/org/springframework/batch/retry/jms/ExternalRetryTests.java @@ -0,0 +1,233 @@ +/* + * 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.retry.jms; + +import java.util.ArrayList; +import java.util.List; + +import javax.sql.DataSource; + +import org.springframework.batch.item.ItemProcessor; +import org.springframework.batch.item.ItemProvider; +import org.springframework.batch.item.provider.AbstractItemProvider; +import org.springframework.batch.retry.callback.ItemProviderRetryCallback; +import org.springframework.batch.retry.policy.ItemProviderRetryPolicy; +import org.springframework.batch.retry.support.RetryTemplate; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jms.core.JmsTemplate; +import org.springframework.test.AbstractDependencyInjectionSpringContextTests; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.TransactionCallback; +import org.springframework.transaction.support.TransactionTemplate; + +public class ExternalRetryTests extends AbstractDependencyInjectionSpringContextTests { + + private JmsTemplate jmsTemplate; + + private RetryTemplate retryTemplate; + + private ItemProvider provider; + + private JdbcTemplate jdbcTemplate; + + private PlatformTransactionManager transactionManager; + + public void setDataSource(DataSource dataSource) { + jdbcTemplate = new JdbcTemplate(dataSource); + } + + public void setTransactionManager(PlatformTransactionManager transactionManager) { + this.transactionManager = transactionManager; + } + + public void setJmsTemplate(JmsTemplate jmsTemplate) { + this.jmsTemplate = jmsTemplate; + } + + protected String[] getConfigLocations() { + return new String[] { "/org/springframework/batch/jms/jms-context.xml" }; + } + + protected void onSetUp() throws Exception { + super.onSetUp(); + getMessages(); // drain queue + jdbcTemplate.execute("delete from T_FOOS"); + jmsTemplate.convertAndSend("queue", "foo"); + provider = new AbstractItemProvider() { + public Object next() { + String text = (String) jmsTemplate.receiveAndConvert("queue"); + list.add(text); + return text; + } + + public boolean recover(Object data, Throwable cause) { + recovered.add(data); + return true; + } + }; + retryTemplate = new RetryTemplate(); + } + + private void assertInitialState() { + int count = jdbcTemplate.queryForInt("select count(*) from T_FOOS"); + assertEquals(0, count); + } + + private List list = new ArrayList(); + + private List recovered = new ArrayList(); + + /** + * Message processing is successful on the second attempt but must receive + * the message again. + * + * @throws Exception + */ + public void testExternalRetrySuccessOnSecondAttempt() throws Exception { + + assertInitialState(); + + retryTemplate.setRetryPolicy(new ItemProviderRetryPolicy()); + + final ItemProviderRetryCallback callback = new ItemProviderRetryCallback(provider, new ItemProcessor() { + public void process(final Object text) { + jdbcTemplate.update("INSERT into T_FOOS (id,name,foo_date) values (?,?,null)", new Object[] { + Integer.valueOf(list.size()), text }); + if (list.size() == 1) { + throw new RuntimeException("Rollback!"); + } + + } + }); + + try { + new TransactionTemplate(transactionManager).execute(new TransactionCallback() { + public Object doInTransaction(TransactionStatus status) { + try { + return retryTemplate.execute(callback); + } + catch (Exception e) { + throw new RuntimeException(e.getMessage(), e); + } + } + }); + fail("Expected Exception"); + } + catch (Exception e) { + + assertEquals("Rollback!", e.getMessage()); + + // Client of retry template has to take care of rollback. This would + // be a message listener container in the MDP case. + + } + + new TransactionTemplate(transactionManager).execute(new TransactionCallback() { + public Object doInTransaction(TransactionStatus status) { + try { + return retryTemplate.execute(callback); + } + catch (Exception e) { + throw new RuntimeException(e.getMessage(), e); + } + } + }); + + List msgs = getMessages(); + + // The database portion committed once... + int count = jdbcTemplate.queryForInt("select count(*) from T_FOOS"); + assertEquals(1, count); + + // ... and so did the message session. + assertEquals("[]", msgs.toString()); + } + + /** + * Message processing fails on both attempts. + * + * @throws Exception + */ + public void testExternalRetryWithRecovery() throws Exception { + + assertInitialState(); + + retryTemplate.setRetryPolicy(new ItemProviderRetryPolicy()); + + final ItemProviderRetryCallback callback = new ItemProviderRetryCallback(provider, new ItemProcessor() { + public void process(final Object text) { + jdbcTemplate.update("INSERT into T_FOOS (id,name,foo_date) values (?,?,null)", new Object[] { + Integer.valueOf(list.size()), text }); + throw new RuntimeException("Rollback!"); + } + }); + + Object result = "start"; + + for (int i = 0; i < 4; i++) { + try { + result = new TransactionTemplate(transactionManager).execute(new TransactionCallback() { + public Object doInTransaction(TransactionStatus status) { + try { + return retryTemplate.execute(callback); + } + catch (Exception e) { + throw new RuntimeException(e.getMessage(), e); + } + } + }); + } + catch (Exception e) { + + if (i < 3) + assertEquals("Rollback!", e.getMessage()); + + // Client of retry template has to take care of rollback. This + // would + // be a message listener container in the MDP case. + + } + } + + // Last attempt should return last item. + assertEquals("foo", result); + + List msgs = getMessages(); + + assertEquals(1, recovered.size()); + + // The database portion committed once... + int count = jdbcTemplate.queryForInt("select count(*) from T_FOOS"); + assertEquals(0, count); + + // ... and so did the message session. + assertEquals("[]", msgs.toString()); + + } + + private List getMessages() { + String next = ""; + List msgs = new ArrayList(); + while (next != null) { + next = (String) jmsTemplate.receiveAndConvert("queue"); + if (next != null) + msgs.add(next); + } + return msgs; + } +} diff --git a/integration/src/test/java/org/springframework/batch/retry/jms/SynchronousTests.java b/integration/src/test/java/org/springframework/batch/retry/jms/SynchronousTests.java new file mode 100644 index 000000000..fabfcef20 --- /dev/null +++ b/integration/src/test/java/org/springframework/batch/retry/jms/SynchronousTests.java @@ -0,0 +1,371 @@ +/* + * 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.retry.jms; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.batch.item.ItemProcessor; +import org.springframework.batch.item.provider.JmsItemProvider; +import org.springframework.batch.retry.RetryCallback; +import org.springframework.batch.retry.RetryContext; +import org.springframework.batch.retry.callback.ItemProviderRetryCallback; +import org.springframework.batch.retry.support.RetryTemplate; +import org.springframework.jms.core.JmsTemplate; +import org.springframework.test.AbstractTransactionalDataSourceSpringContextTests; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.TransactionCallback; +import org.springframework.transaction.support.TransactionTemplate; + +public class SynchronousTests extends AbstractTransactionalDataSourceSpringContextTests { + + private JmsTemplate jmsTemplate; + + private RetryTemplate retryTemplate; + + public void setJmsTemplate(JmsTemplate jmsTemplate) { + this.jmsTemplate = jmsTemplate; + } + + protected String[] getConfigLocations() { + return new String[] { "/org/springframework/batch/jms/jms-context.xml" }; + } + + protected void onSetUpBeforeTransaction() throws Exception { + super.onSetUpBeforeTransaction(); + String foo = ""; + int count = 0; + while (foo != null && count < 100) { + foo = (String) jmsTemplate.receiveAndConvert("queue"); + count++; + } + jdbcTemplate.execute("delete from T_FOOS"); + jmsTemplate.convertAndSend("queue", "foo"); + jmsTemplate.convertAndSend("queue", "foo"); + final String text = (String) jmsTemplate.receiveAndConvert("queue"); + assertNotNull(text); + retryTemplate = new RetryTemplate(); + } + + protected void onSetUpInTransaction() throws Exception { + super.onSetUpInTransaction(); + } + + private void assertInitialState() { + int count = jdbcTemplate.queryForInt("select count(*) from T_FOOS"); + assertEquals(0, count); + } + + List list = new ArrayList(); + + /** + * Message processing is successful on the second attempt without having to + * receive the message again. + * + * @throws Exception + */ + public void testInternalRetrySuccessOnSecondAttempt() throws Exception { + + assertInitialState(); + + /* + * We either want the JMS receive to be outside a transaction, or we + * need the database transaction in the retry to be PROPAGATION_NESTED. + * Otherwise JMS will roll back when the retry callback is eventually + * successful because of the previous exception. + * PROPAGATION_REQUIRES_NEW is wrong because it doesn't allow the outer + * transaction to fail and rollback the inner one. + */ + final String text = (String) jmsTemplate.receiveAndConvert("queue"); + assertNotNull(text); + + retryTemplate.execute(new RetryCallback() { + public Object doWithRetry(RetryContext status) throws Throwable { + + TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager); + transactionTemplate.setPropagationBehavior(TransactionTemplate.PROPAGATION_NESTED); + return transactionTemplate.execute(new TransactionCallback() { + public Object doInTransaction(TransactionStatus status) { + + list.add(text); + System.err.println("Inserting: [" + list.size() + "," + text + "]"); + jdbcTemplate.update("INSERT into T_FOOS (id,name,foo_date) values (?,?,null)", new Object[] { + Integer.valueOf(list.size()), text }); + if (list.size() == 1) { + throw new RuntimeException("Rollback!"); + } + return text; + + } + }); + + } + }); + + // force commit... + setComplete(); + endTransaction(); + + startNewTransaction(); + + List msgs = getMessages(); + + // The database portion committed once... + int count = jdbcTemplate.queryForInt("select count(*) from T_FOOS"); + assertEquals(1, count); + + // ... and so did the message session. + assertEquals("[]", msgs.toString()); + } + + /** + * Message processing is successful on the second attempt without having to + * receive the message again - uses JmsItemProvider internally. + * + * @throws Exception + */ + public void testInternalRetrySuccessOnSecondAttemptWithItemProvider() throws Exception { + + assertInitialState(); + + JmsItemProvider provider = new JmsItemProvider(); + // provider.setItemType(Message.class); + provider.setJmsTemplate(jmsTemplate); + jmsTemplate.setDefaultDestinationName("queue"); + + retryTemplate.execute(new ItemProviderRetryCallback(provider, new ItemProcessor() { + public void process(final Object text) { + + TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager); + transactionTemplate.setPropagationBehavior(TransactionTemplate.PROPAGATION_NESTED); + transactionTemplate.execute(new TransactionCallback() { + public Object doInTransaction(TransactionStatus status) { + + list.add(text); + System.err.println("Inserting: [" + list.size() + "," + text + "]"); + jdbcTemplate.update("INSERT into T_FOOS (id,name,foo_date) values (?,?,null)", new Object[] { + Integer.valueOf(list.size()), text }); + if (list.size() == 1) { + throw new RuntimeException("Rollback!"); + } + + return text; + + } + }); + + } + })); + + // force commit... + setComplete(); + endTransaction(); + + startNewTransaction(); + + List msgs = getMessages(); + + // The database portion committed once... + int count = jdbcTemplate.queryForInt("select count(*) from T_FOOS"); + assertEquals(1, count); + + // ... and so did the message session. + assertEquals("[]", msgs.toString()); + } + + /** + * Message processing is successful on the second attempt without having to + * receive the message again. + * + * @throws Exception + */ + public void testInternalRetrySuccessOnFirstAttemptRollbackOuter() throws Exception { + + assertInitialState(); + + /* + * We either want the JMS receive to be outside a transaction, or we + * need the database transaction in the retry to be PROPAGATION_NESTED. + * Otherwise JMS will roll back when the retry callback is eventually + * successful because of the previous exception. + * PROPAGATION_REQUIRES_NEW is wrong because it doesn't allow the outer + * transaction to fail and rollback the inner one. + */ + final String text = (String) jmsTemplate.receiveAndConvert("queue"); + + retryTemplate.execute(new RetryCallback() { + public Object doWithRetry(RetryContext status) throws Throwable { + + TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager); + transactionTemplate.setPropagationBehavior(TransactionTemplate.PROPAGATION_NESTED); + return transactionTemplate.execute(new TransactionCallback() { + public Object doInTransaction(TransactionStatus status) { + + list.add(text); + System.err.println("Inserting: [" + list.size() + "," + text + "]"); + jdbcTemplate.update("INSERT into T_FOOS (id,name,foo_date) values (?,?,null)", new Object[] { + Integer.valueOf(list.size()), text }); + return text; + + } + }); + + } + }); + + // The database transaction has committed... + int count = jdbcTemplate.queryForInt("select count(*) from T_FOOS"); + assertEquals(1, count); + + // force rollback... + endTransaction(); + + startNewTransaction(); + + List msgs = getMessages(); + + // The database portion rolled back... + count = jdbcTemplate.queryForInt("select count(*) from T_FOOS"); + assertEquals(0, count); + + // ... and so did the message session. + assertEquals("[foo]", msgs.toString()); + } + + /** + * Message processing is successful on the second attempt but must receive + * the message again. + * + * @throws Exception + */ + public void testExternalRetrySuccessOnSecondAttempt() throws Exception { + + assertInitialState(); + + // force commit so that the retry executes in its own transaction (not + // nested)... + setComplete(); + endTransaction(); + + retryTemplate.execute(new RetryCallback() { + public Object doWithRetry(RetryContext status) throws Throwable { + + TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager); + return transactionTemplate.execute(new TransactionCallback() { + public Object doInTransaction(TransactionStatus status) { + + // The receive is inside the retry and the + // transaction... + final String text = (String) jmsTemplate.receiveAndConvert("queue"); + list.add(text); + jdbcTemplate.update("INSERT into T_FOOS (id,name,foo_date) values (?,?,null)", new Object[] { + Integer.valueOf(list.size()), text }); + if (list.size() == 1) { + throw new RuntimeException("Rollback!"); + } + return text; + + } + }); + + } + }); + + startNewTransaction(); + + List msgs = getMessages(); + + // The database portion committed once... + int count = jdbcTemplate.queryForInt("select count(*) from T_FOOS"); + assertEquals(1, count); + + // ... and so did the message session. + assertEquals("[]", msgs.toString()); + } + + /** + * Message processing fails. + * + * @throws Exception + */ + public void testExternalRetryFailOnSecondAttempt() throws Exception { + + assertInitialState(); + + // force commit so that the retry executes in its own transaction (not + // nested)... + setComplete(); + endTransaction(); + + try { + + retryTemplate.execute(new RetryCallback() { + public Object doWithRetry(RetryContext status) throws Throwable { + + TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager); + return transactionTemplate.execute(new TransactionCallback() { + public Object doInTransaction(TransactionStatus status) { + + // The receieve is inside the retry and the + // transaction... + final String text = (String) jmsTemplate.receiveAndConvert("queue"); + list.add(text); + jdbcTemplate.update("INSERT into T_FOOS (id,name,foo_date) values (?,?,null)", + new Object[] { Integer.valueOf(list.size()), text }); + throw new RuntimeException("Rollback!"); + + } + }); + + } + }); + + /* + * N.B. the message can be re-directed to an error queue by setting + * an error destination in a JmsItemProvider. + */ + fail("Expected RuntimeException"); + + } + catch (RuntimeException e) { + assertEquals("Rollback!", e.getMessage()); + // expected + } + + startNewTransaction(); + + List msgs = getMessages(); + + // The database portion rolled back... + int count = jdbcTemplate.queryForInt("select count(*) from T_FOOS"); + assertEquals(0, count); + + // ... and so did the message session. + assertTrue(msgs.contains("foo")); + } + + private List getMessages() { + String next = ""; + List msgs = new ArrayList(); + while (next != null) { + next = (String) jmsTemplate.receiveAndConvert("queue"); + if (next != null) + msgs.add(next); + } + return msgs; + } +} diff --git a/integration/src/test/java/org/springframework/jms/AsynchronousTests.java b/integration/src/test/java/org/springframework/jms/AsynchronousTests.java new file mode 100644 index 000000000..f2725fa4f --- /dev/null +++ b/integration/src/test/java/org/springframework/jms/AsynchronousTests.java @@ -0,0 +1,151 @@ +/* + * 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.jms; + +import java.util.ArrayList; +import java.util.List; + +import javax.jms.JMSException; +import javax.jms.Message; +import javax.jms.Session; + +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jms.core.JmsTemplate; +import org.springframework.jms.listener.DefaultMessageListenerContainer; +import org.springframework.jms.listener.SessionAwareMessageListener; +import org.springframework.test.AbstractDependencyInjectionSpringContextTests; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.TransactionCallback; +import org.springframework.transaction.support.TransactionTemplate; +import org.springframework.util.ClassUtils; + +public class AsynchronousTests extends AbstractDependencyInjectionSpringContextTests { + + protected String[] getConfigLocations() { + return new String[] { ClassUtils.classPackageAsResourcePath(getClass()) + "/asynch.xml" }; + } + + private DefaultMessageListenerContainer container; + + private JmsTemplate jmsTemplate; + + private JdbcTemplate jdbcTemplate; + + private PlatformTransactionManager transactionManager; + + public void setTransactionManager(PlatformTransactionManager transactionManager) { + this.transactionManager = transactionManager; + } + + public void setJdbcTemplate(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + public void setJmsTemplate(JmsTemplate jmsTemplate) { + this.jmsTemplate = jmsTemplate; + } + + public void setContainer(DefaultMessageListenerContainer container) { + this.container = container; + } + + protected void onSetUp() throws Exception { + super.onSetUp(); + String foo = ""; + int count = 0; + while (foo != null && count < 100) { + foo = (String) jmsTemplate.receiveAndConvert("queue"); + count++; + } + jdbcTemplate.execute("delete from T_FOOS"); + jmsTemplate.convertAndSend("queue", "foo"); + } + + protected void onTearDown() throws Exception { + super.onTearDown(); + container.stop(); + // Need to give the container time to shutdown + Thread.sleep(1000L); + } + + List list = new ArrayList(); + + private void assertInitialState() { + int count = jdbcTemplate.queryForInt("select count(*) from T_FOOS"); + assertEquals(0, count); + } + + public void testSunnyDay() throws Exception { + + assertInitialState(); + + container.setMessageListener(new SessionAwareMessageListener() { + public void onMessage(Message message, Session session) throws JMSException { + list.add(message.toString()); + jdbcTemplate.execute("INSERT into T_FOOS (id,name,foo_date) values (1,'bar',null)"); + } + }); + + container.start(); + + // Need to sleep for at least a second here... + Thread.sleep(1000L); + + assertEquals(1, list.size()); + + String foo = (String) jmsTemplate.receiveAndConvert("queue"); + assertEquals(null, foo); + + int count = jdbcTemplate.queryForInt("select count(*) from T_FOOS"); + assertEquals(1, count); + + } + + public void testRollback() throws Exception { + + assertInitialState(); + + container.setMessageListener(new SessionAwareMessageListener() { + public void onMessage(Message message, Session session) throws JMSException { + list.add(message.toString()); + new TransactionTemplate(transactionManager).execute(new TransactionCallback() { + public Object doInTransaction(TransactionStatus status) { + jdbcTemplate.execute("INSERT into T_FOOS (id,name,foo_date) values (1,'bar',null)"); + // This causes the DB to rollback but not the message + throw new RuntimeException("Rollback!"); + } + }); + } + }); + + container.start(); + + // Need to sleep for at least a second here... + Thread.sleep(1000L); + + // We rolled back so the message might come in many times... + assertTrue(list.size() > 1); + + int count = jdbcTemplate.queryForInt("select count(*) from T_FOOS"); + assertEquals(0, count); + + String foo = (String) jmsTemplate.receiveAndConvert("queue"); + assertEquals("foo", foo); + + } +} diff --git a/integration/src/test/java/org/springframework/jms/SynchronousTests.java b/integration/src/test/java/org/springframework/jms/SynchronousTests.java new file mode 100644 index 000000000..b0bc68e78 --- /dev/null +++ b/integration/src/test/java/org/springframework/jms/SynchronousTests.java @@ -0,0 +1,151 @@ +/* + * 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.jms; + +import javax.jms.JMSException; +import javax.jms.Session; + +import org.springframework.jms.connection.SessionProxy; +import org.springframework.jms.core.JmsTemplate; +import org.springframework.jms.core.SessionCallback; +import org.springframework.test.AbstractTransactionalDataSourceSpringContextTests; +import org.springframework.transaction.support.TransactionSynchronizationAdapter; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.util.ClassUtils; + +public class SynchronousTests extends AbstractTransactionalDataSourceSpringContextTests { + + private JmsTemplate jmsTemplate; + + public void setJmsTemplate(JmsTemplate jmsTemplate) { + this.jmsTemplate = jmsTemplate; + } + + protected String[] getConfigLocations() { + return new String[] { ClassUtils.classPackageAsResourcePath(getClass()) + "/synch.xml" }; + } + + protected void onSetUpBeforeTransaction() throws Exception { + super.onSetUpBeforeTransaction(); + String foo = ""; + int count = 0; + while (foo != null && count < 100) { + foo = (String) jmsTemplate.receiveAndConvert("queue"); + count++; + } + jdbcTemplate.execute("delete from T_FOOS"); + jmsTemplate.convertAndSend("queue", "foo"); + } + + protected void onSetUpInTransaction() throws Exception { + super.onSetUpInTransaction(); + } + + private void assertInitialState() { + int count = jdbcTemplate.queryForInt("select count(*) from T_FOOS"); + assertEquals(0, count); + } + + public void testCommit() throws Exception { + + assertInitialState(); + String foo = (String) jmsTemplate.receiveAndConvert("queue"); + assertEquals("foo", foo); + jdbcTemplate.execute("INSERT into T_FOOS (id,name,foo_date) values (1,'bar',null)"); + + // force commit... + setComplete(); + endTransaction(); + startNewTransaction(); + + // Database committed so this resord should be there... + int count = jdbcTemplate.queryForInt("select count(*) from T_FOOS"); + assertEquals(1, count); + + // ... the commit should also have cleared the queue, so this should now + // be null + foo = (String) jmsTemplate.receiveAndConvert("queue"); + assertEquals(null, foo); + + } + + public void testFullRollback() throws Exception { + + assertInitialState(); + String foo = (String) jmsTemplate.receiveAndConvert("queue"); + assertEquals("foo", foo); + jdbcTemplate.execute("INSERT into T_FOOS (id,name,foo_date) values (1,'bar',null)"); + + // force rollback... + endTransaction(); + startNewTransaction(); + + // The database connection rolled back... + int count = jdbcTemplate.queryForInt("select count(*) from T_FOOS"); + assertEquals(0, count); + + // ... and so did the message session. The rollback should have restored + // the queue, so this should now be non-null + foo = (String) jmsTemplate.receiveAndConvert("queue"); + assertEquals("foo", foo); + + } + + public void testPartialRollback() throws Exception { + + assertInitialState(); + String foo = (String) jmsTemplate.receiveAndConvert("queue"); + assertEquals("foo", foo); + jdbcTemplate.execute("INSERT into T_FOOS (id,name,foo_date) values (1,'bar',null)"); + + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() { + public void beforeCommit(boolean readOnly) { + // Simulate a message system failure before the main transaction + // commits... + jmsTemplate.execute(new SessionCallback() { + public Object doInJms(Session session) throws JMSException { + try { + assertTrue("Not a SessionProxy - wrong spring version?", session instanceof SessionProxy); + ((SessionProxy) session).getTargetSession().rollback(); + } + catch (JMSException e) { + throw e; + } + catch (Exception e) { + // swallow it + e.printStackTrace(); + } + return null; + } + }); + } + }); + // force commit... + setComplete(); + endTransaction(); + startNewTransaction(); + + // The database portion committed... + int count = jdbcTemplate.queryForInt("select count(*) from T_FOOS"); + assertEquals(1, count); + + // ...but the JMS session rolled back, so the message is still there + foo = (String) jmsTemplate.receiveAndConvert("queue"); + assertEquals("foo", foo); + + } +} diff --git a/integration/src/test/java/org/springframework/jms/TransactionPropagationTests.java b/integration/src/test/java/org/springframework/jms/TransactionPropagationTests.java new file mode 100644 index 000000000..ec07a1112 --- /dev/null +++ b/integration/src/test/java/org/springframework/jms/TransactionPropagationTests.java @@ -0,0 +1,161 @@ +/* + * 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.jms; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.jms.core.JmsTemplate; +import org.springframework.test.AbstractDependencyInjectionSpringContextTests; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.DefaultTransactionDefinition; +import org.springframework.transaction.support.TransactionCallback; +import org.springframework.transaction.support.TransactionTemplate; +import org.springframework.util.ClassUtils; + +public class TransactionPropagationTests extends AbstractDependencyInjectionSpringContextTests { + + protected String[] getConfigLocations() { + return new String[] { ClassUtils.classPackageAsResourcePath(getClass()) + "/tx.xml" }; + } + + private JmsTemplate jmsTemplate; + + private PlatformTransactionManager transactionManager; + + public void setTransactionManager(PlatformTransactionManager transactionManager) { + this.transactionManager = transactionManager; + } + + public void setJmsTemplate(JmsTemplate jmsTemplate) { + this.jmsTemplate = jmsTemplate; + } + + protected void onSetUp() throws Exception { + super.onSetUp(); + String foo = ""; + int count = 0; + while (foo != null && count < 100) { + foo = (String) jmsTemplate.receiveAndConvert("queue"); + count++; + } + jmsTemplate.convertAndSend("queue", "foo"); + jmsTemplate.convertAndSend("queue", "bar"); + jmsTemplate.convertAndSend("queue", "spam"); + } + + List list = new ArrayList(); + + public void testRollbackOuterTransaction() throws Exception { + + final DefaultTransactionDefinition transactionDefinition = new DefaultTransactionDefinition( + TransactionDefinition.PROPAGATION_MANDATORY); + + try { + + new TransactionTemplate(transactionManager).execute(new TransactionCallback() { + + public Object doInTransaction(TransactionStatus status) { + + new TransactionTemplate(transactionManager, transactionDefinition) + .execute(new TransactionCallback() { + public Object doInTransaction(TransactionStatus status) { + String msg = (String) jmsTemplate.receiveAndConvert("queue"); + list.add(msg); + return null; + } + }); + + new TransactionTemplate(transactionManager, transactionDefinition) + .execute(new TransactionCallback() { + public Object doInTransaction(TransactionStatus status) { + String msg = (String) jmsTemplate.receiveAndConvert("queue"); + list.add(msg); + throw new RuntimeException("Rollback!"); + } + }); + + return null; + } + }); + + fail("Expected RuntimeException"); + + } + catch (RuntimeException e) { + // Expected + assertEquals("Rollback!", e.getMessage()); + } + + List msgs = getMessages(); + System.err.println(list); + System.err.println(msgs); + + // 2 received + assertEquals(2, list.size()); + // but both rolled back... + assertEquals(3, msgs.size()); + } + + public void testRollbackSingleTransaction() throws Exception { + + try { + + new TransactionTemplate(transactionManager).execute(new TransactionCallback() { + + public Object doInTransaction(TransactionStatus status) { + + String msg = (String) jmsTemplate.receiveAndConvert("queue"); + list.add(msg); + msg = (String) jmsTemplate.receiveAndConvert("queue"); + list.add(msg); + throw new RuntimeException("Rollback!"); + + } + }); + + fail("Expected RuntimeException"); + + } + catch (RuntimeException e) { + // Expected + assertEquals("Rollback!", e.getMessage()); + } + + List msgs = getMessages(); + System.err.println(list); + System.err.println(msgs); + + // 2 received + assertEquals(2, list.size()); + // but both rolled back... + assertEquals(3, msgs.size()); + } + + private List getMessages() { + String next = ""; + List msgs = new ArrayList(); + while (next != null) { + next = (String) jmsTemplate.receiveAndConvert("queue"); + if (next != null) + msgs.add(next); + } + return msgs; + } +} diff --git a/integration/src/test/java/org/springframework/retry/jms/SynchronousTests.java b/integration/src/test/java/org/springframework/retry/jms/SynchronousTests.java new file mode 100644 index 000000000..0e33ff376 --- /dev/null +++ b/integration/src/test/java/org/springframework/retry/jms/SynchronousTests.java @@ -0,0 +1,363 @@ +/* + * 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.retry.jms; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.batch.item.ItemProcessor; +import org.springframework.batch.item.provider.JmsItemProvider; +import org.springframework.batch.retry.RetryCallback; +import org.springframework.batch.retry.RetryContext; +import org.springframework.batch.retry.callback.ItemProviderRetryCallback; +import org.springframework.batch.retry.support.RetryTemplate; +import org.springframework.jms.core.JmsTemplate; +import org.springframework.test.AbstractTransactionalDataSourceSpringContextTests; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.TransactionCallback; +import org.springframework.transaction.support.TransactionTemplate; + +public class SynchronousTests extends AbstractTransactionalDataSourceSpringContextTests { + + private JmsTemplate jmsTemplate; + + private RetryTemplate retryTemplate; + + public void setJmsTemplate(JmsTemplate jmsTemplate) { + this.jmsTemplate = jmsTemplate; + } + + protected String[] getConfigLocations() { + return new String[] { "/org/springframework/batch/jms/jms-context.xml" }; + } + + protected void onSetUpBeforeTransaction() throws Exception { + super.onSetUpBeforeTransaction(); + String foo = ""; + int count = 0; + while (foo != null && count < 100) { + foo = (String) jmsTemplate.receiveAndConvert("queue"); + count++; + } + jdbcTemplate.execute("delete from T_FOOS"); + jmsTemplate.convertAndSend("queue", "foo"); + retryTemplate = new RetryTemplate(); + } + + protected void onSetUpInTransaction() throws Exception { + super.onSetUpInTransaction(); + } + + private void assertInitialState() { + int count = jdbcTemplate.queryForInt("select count(*) from T_FOOS"); + assertEquals(0, count); + } + + List list = new ArrayList(); + + /** + * Message processing is successful on the second attempt without having to + * receive the message again. + * + * @throws Exception + */ + public void testInternalRetrySuccessOnSecondAttempt() throws Exception { + + assertInitialState(); + + /* + * We either want the JMS receive to be outside a transaction, or we + * need the database transaction in the retry to be PROPAGATION_NESTED. + * Otherwise JMS will roll back when the retry callback is eventually + * successful because of the previous exception. + * PROPAGATION_REQUIRES_NEW is wrong because it doesn't allow the outer + * transaction to fail and rollback the inner one. + */ + final String text = (String) jmsTemplate.receiveAndConvert("queue"); + + retryTemplate.execute(new RetryCallback() { + public Object doWithRetry(RetryContext status) throws Throwable { + + TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager); + transactionTemplate.setPropagationBehavior(TransactionTemplate.PROPAGATION_NESTED); + return transactionTemplate.execute(new TransactionCallback() { + public Object doInTransaction(TransactionStatus status) { + + list.add(text); + System.err.println("Inserting: [" + list.size() + "," + text + "]"); + jdbcTemplate.update("INSERT into T_FOOS (id,name,foo_date) values (?,?,null)", new Object[] { + Integer.valueOf(list.size()), text }); + if (list.size() == 1) { + throw new RuntimeException("Rollback!"); + } + return text; + + } + }); + + } + }); + + // force commit... + setComplete(); + endTransaction(); + + startNewTransaction(); + + List msgs = getMessages(); + + // The database portion committed once... + int count = jdbcTemplate.queryForInt("select count(*) from T_FOOS"); + assertEquals(1, count); + + // ... and so did the message session. + assertEquals("[]", msgs.toString()); + } + + /** + * Message processing is successful on the second attempt without having to + * receive the message again - uses JmsItemProvider internally. + * + * @throws Exception + */ + public void testInternalRetrySuccessOnSecondAttemptWithItemProvider() throws Exception { + + assertInitialState(); + + JmsItemProvider provider = new JmsItemProvider(); + // provider.setItemType(Message.class); + provider.setJmsTemplate(jmsTemplate); + jmsTemplate.setDefaultDestinationName("queue"); + + retryTemplate.execute(new ItemProviderRetryCallback(provider, new ItemProcessor() { + public void process(final Object text) { + + TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager); + transactionTemplate.setPropagationBehavior(TransactionTemplate.PROPAGATION_NESTED); + transactionTemplate.execute(new TransactionCallback() { + public Object doInTransaction(TransactionStatus status) { + + list.add(text); + System.err.println("Inserting: [" + list.size() + "," + text + "]"); + jdbcTemplate.update("INSERT into T_FOOS (id,name,foo_date) values (?,?,null)", new Object[] { + Integer.valueOf(list.size()), text }); + if (list.size() == 1) { + throw new RuntimeException("Rollback!"); + } + + return text; + + } + }); + + } + })); + + // force commit... + setComplete(); + endTransaction(); + + startNewTransaction(); + + List msgs = getMessages(); + + // The database portion committed once... + int count = jdbcTemplate.queryForInt("select count(*) from T_FOOS"); + assertEquals(1, count); + + // ... and so did the message session. + assertEquals("[]", msgs.toString()); + } + + /** + * Message processing is successful on the second attempt without having to + * receive the message again. + * + * @throws Exception + */ + public void testInternalRetrySuccessOnFirstAttemptRollbackOuter() throws Exception { + + assertInitialState(); + + /* + * We either want the JMS receive to be outside a transaction, or we + * need the database transaction in the retry to be PROPAGATION_NESTED. + * Otherwise JMS will roll back when the retry callback is eventually + * successful because of the previous exception. + * PROPAGATION_REQUIRES_NEW is wrong because it doesn't allow the outer + * transaction to fail and rollback the inner one. + */ + final String text = (String) jmsTemplate.receiveAndConvert("queue"); + + retryTemplate.execute(new RetryCallback() { + public Object doWithRetry(RetryContext status) throws Throwable { + + TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager); + transactionTemplate.setPropagationBehavior(TransactionTemplate.PROPAGATION_NESTED); + return transactionTemplate.execute(new TransactionCallback() { + public Object doInTransaction(TransactionStatus status) { + + list.add(text); + System.err.println("Inserting: [" + list.size() + "," + text + "]"); + jdbcTemplate.update("INSERT into T_FOOS (id,name,foo_date) values (?,?,null)", new Object[] { + Integer.valueOf(list.size()), text }); + return text; + + } + }); + + } + }); + + // The database transaction has committed... + int count = jdbcTemplate.queryForInt("select count(*) from T_FOOS"); + assertEquals(1, count); + + // force rollback... + endTransaction(); + + startNewTransaction(); + + List msgs = getMessages(); + + // The database portion rolled back... + count = jdbcTemplate.queryForInt("select count(*) from T_FOOS"); + assertEquals(0, count); + + // ... and so did the message session. + assertEquals("[foo]", msgs.toString()); + } + + /** + * Message processing is successful on the second attempt but must receive + * the message again. + * + * @throws Exception + */ + public void testExternalRetrySuccessOnSecondAttempt() throws Exception { + + assertInitialState(); + + // force commit so that the retry executes in its own transaction (not + // nested)... + setComplete(); + endTransaction(); + + retryTemplate.execute(new RetryCallback() { + public Object doWithRetry(RetryContext status) throws Throwable { + + TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager); + return transactionTemplate.execute(new TransactionCallback() { + public Object doInTransaction(TransactionStatus status) { + + // The receieve is inside the retry and the + // transaction... + final String text = (String) jmsTemplate.receiveAndConvert("queue"); + list.add(text); + jdbcTemplate.update("INSERT into T_FOOS (id,name,foo_date) values (?,?,null)", new Object[] { + Integer.valueOf(list.size()), text }); + if (list.size() == 1) { + throw new RuntimeException("Rollback!"); + } + return text; + + } + }); + + } + }); + + startNewTransaction(); + + List msgs = getMessages(); + + // The database portion committed once... + int count = jdbcTemplate.queryForInt("select count(*) from T_FOOS"); + assertEquals(1, count); + + // ... and so did the message session. + assertEquals("[]", msgs.toString()); + } + + /** + * Message processing fails. + * + * @throws Exception + */ + public void testExternalRetryFailOnSecondAttempt() throws Exception { + + assertInitialState(); + + // force commit so that the retry executes in its own transaction (not + // nested)... + setComplete(); + endTransaction(); + + try { + + retryTemplate.execute(new RetryCallback() { + public Object doWithRetry(RetryContext status) throws Throwable { + + TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager); + return transactionTemplate.execute(new TransactionCallback() { + public Object doInTransaction(TransactionStatus status) { + + // The receieve is inside the retry and the + // transaction... + final String text = (String) jmsTemplate.receiveAndConvert("queue"); + list.add(text); + jdbcTemplate.update("INSERT into T_FOOS (id,name,foo_date) values (?,?,null)", + new Object[] { Integer.valueOf(list.size()), text }); + throw new RuntimeException("Rollback!"); + + } + }); + + } + }); + + fail("Expected RuntimeException"); + + } + catch (RuntimeException e) { + assertEquals("Rollback!", e.getMessage()); + // expected + } + + startNewTransaction(); + + List msgs = getMessages(); + + // The database portion rolled back... + int count = jdbcTemplate.queryForInt("select count(*) from T_FOOS"); + assertEquals(0, count); + + // ... and so did the message session. + assertTrue(msgs.contains("foo")); + } + + private List getMessages() { + String next = ""; + List msgs = new ArrayList(); + while (next != null) { + next = (String) jmsTemplate.receiveAndConvert("queue"); + if (next != null) + msgs.add(next); + } + return msgs; + } +} diff --git a/integration/src/test/resources/data-source.xml b/integration/src/test/resources/data-source.xml new file mode 100644 index 000000000..d20a65734 --- /dev/null +++ b/integration/src/test/resources/data-source.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + vm://localhost + + + + + + + + + + + + \ No newline at end of file diff --git a/integration/src/test/resources/log4j.properties b/integration/src/test/resources/log4j.properties new file mode 100644 index 000000000..45e88b404 --- /dev/null +++ b/integration/src/test/resources/log4j.properties @@ -0,0 +1,8 @@ +log4j.rootCategory=INFO, stdout + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - <%m>%n + +log4j.category.org.apache.activemq=ERROR +# log4j.category.org.springframework=DEBUG diff --git a/integration/src/test/resources/org/springframework/batch/jms/destroy.sql b/integration/src/test/resources/org/springframework/batch/jms/destroy.sql new file mode 100644 index 000000000..a8f0da237 --- /dev/null +++ b/integration/src/test/resources/org/springframework/batch/jms/destroy.sql @@ -0,0 +1 @@ +DROP TABLE T_FOOS; diff --git a/integration/src/test/resources/org/springframework/batch/jms/init.sql b/integration/src/test/resources/org/springframework/batch/jms/init.sql new file mode 100644 index 000000000..1c8a78ce6 --- /dev/null +++ b/integration/src/test/resources/org/springframework/batch/jms/init.sql @@ -0,0 +1,5 @@ +create table T_FOOS ( + id integer not null primary key, + name varchar(80), + foo_date timestamp +); diff --git a/integration/src/test/resources/org/springframework/batch/jms/jms-context.xml b/integration/src/test/resources/org/springframework/batch/jms/jms-context.xml new file mode 100644 index 000000000..31492e460 --- /dev/null +++ b/integration/src/test/resources/org/springframework/batch/jms/jms-context.xml @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + vm://localhost + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.springframework.batch.repeat.RepeatOperations + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/integration/src/test/resources/org/springframework/jms/asynch.xml b/integration/src/test/resources/org/springframework/jms/asynch.xml new file mode 100644 index 000000000..2201131a9 --- /dev/null +++ b/integration/src/test/resources/org/springframework/jms/asynch.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + vm://localhost + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/integration/src/test/resources/org/springframework/jms/synch.xml b/integration/src/test/resources/org/springframework/jms/synch.xml new file mode 100644 index 000000000..1ca46ba49 --- /dev/null +++ b/integration/src/test/resources/org/springframework/jms/synch.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + vm://localhost + + + + + + + \ No newline at end of file diff --git a/integration/src/test/resources/org/springframework/jms/tx.xml b/integration/src/test/resources/org/springframework/jms/tx.xml new file mode 100644 index 000000000..30b40fe9f --- /dev/null +++ b/integration/src/test/resources/org/springframework/jms/tx.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + vm://localhost + + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index 5ce776fef..a6bd5f7f9 100644 --- a/pom.xml +++ b/pom.xml @@ -1,87 +1,121 @@ - + 4.0.0 org.springframework.batch spring-batch - pom Spring Batch - 1.0-SNAPSHOT - Spring Batch provides tools for enterprise batch or bulk processing. It can be used to wire up jobs, and track - their execution, or simply as an optimisation for repetitive processing in a transactional environment. Spring - Batch is part of the Spring Portfolio. + + - http://www.springframework.org/spring-batch + 1.0-m2-SNAPSHOT + pom + http://static.springframework.org/spring-batch + + Interface 21 + http://www.interface21.com + + + scm:svn:https://svn.interface21.com/svn/i21/spring-batch/trunk + JIRA - http://opensource2.atlassian.com/projects/spring/browse/BATCH + http://opensource.atlassian.com/projects/spring/browse/BATCH + + + Spring Batch Forum + http://forum.springframework.org/forumdisplay.php?f=41 + http://forum.springframework.org/forumdisplay.php?f=41 + + + http://lists.interface21.com/listmanager/listinfo/spring-batch-announce + http://lists.interface21.com/listmanager/listinfo/spring-batch-announce + Spring Batch Announce + http://lists.interface21.com/archives/spring-batch-announce + + Bamboo http://build.springframework.org:8085/bamboo/browse/BATCH - 2007 - - - spring-batch-announce - http://lists.interface21.com/listmanager/listinfo/spring-batch-announce - http://lists.interface21.com/listmanager/listinfo/spring-batch-announce - spring-batch-announce@lists.interface21.com - http://lists.interface21.com/archives/spring-batch-announce/ - - - - - dsyer - Dave Syer - dsyer@interface21.com - Interface21 - http://www.interface21.com - - Lead - - +1 - - - The Apache Software License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0 - repo + Apache 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt - - - scm:svn:https://springframework.svn.sourceforge.net/svnroot/springframework/spring-batch/trunk - - - scm:svn:https://springframework.svn.sourceforge.net/svnroot/springframework/spring-batch/trunk - - http://fisheye3.cenqua.com/browse/springframework/spring-batch/trunk - - - Spring Framework - http://www.springframework.org - + + + true + + + + infrastructure + core + execution + samples + integration + docs + + + + + default + + true + + + + deployment + + + static.springframework.org + + scp://static.springframework.org:/var/www/domains/springframework.org/static/htdocs/spring-batch + + + + + + strict + + false + + + + fast + + true + + + + snapshots + + + apache-snapshots + http://people.apache.org/maven-snapshot-repository + + + + + + + + staging + file:///${user.dir}/target/staging + + + - - - org.springframework.aws - spring-aws-maven - 1.1.1 - - org.apache.maven.plugins maven-compiler-plugin - 1.5 - 1.5 + 1.4 + 1.4 @@ -89,7 +123,7 @@ maven-surefire-plugin 2.3 - once + **/*Tests.java @@ -145,6 +179,11 @@ ant-junit 1.6.5 + + foundrylogic.vpp + vpp + 2.2.1 + @@ -167,19 +206,33 @@ - - - infrastructure - - - + + + + apache-snapshots + http://people.apache.org/maven-snapshot-repository + + + spring-snapshots + https://springframework.svn.sourceforge.net/svnroot/springframework/repos/repo-snapshots + + + - spring-external - Spring External Repository + spring + https://springframework.svn.sourceforge.net/svnroot/springframework/repos/repo + + + spring-ext https://springframework.svn.sourceforge.net/svnroot/springframework/repos/repo-ext + + objectstyle + http://objectstyle.org/maven2 + + true @@ -222,23 +275,18 @@ - - http://www.springframework.org/spring-batch - - spring-milestone - Spring Milestone Repository - s3://maven.springframework.org/milestone - - - spring-snapshot - Spring Snapshot Repository - s3://maven.springframework.org/snapshot - - - static.springframework.org - - scp://static.springframework.org/var/www/domains/springframework.org/static/htdocs/spring-batch/site - - - + + + + dsyer + Dave Syer + dsyer@interface21.com + + + lward + Lucas Ward + lucas.l.ward@accenture.com + + + diff --git a/samples/.classpath b/samples/.classpath new file mode 100644 index 000000000..c7aaa6fdf --- /dev/null +++ b/samples/.classpath @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/samples/.project b/samples/.project new file mode 100644 index 000000000..c9e918e64 --- /dev/null +++ b/samples/.project @@ -0,0 +1,47 @@ + + + batch-samples + + + + + + com.ibm.etools.common.migration.MigrationBuilder + + + + + org.eclipse.wst.common.project.facet.core.builder + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.wst.validation.validationbuilder + + + + + org.springframework.ide.eclipse.core.springbuilder + + + + + org.maven.ide.eclipse.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.maven.ide.eclipse.maven2Nature + org.springframework.ide.eclipse.core.springnature + org.eclipse.wst.common.project.facet.core.nature + org.eclipse.wst.common.modulecore.ModuleCoreNature + org.eclipse.jem.workbench.JavaEMFNature + + diff --git a/samples/.springBeans b/samples/.springBeans new file mode 100644 index 000000000..2a26a18aa --- /dev/null +++ b/samples/.springBeans @@ -0,0 +1,26 @@ + + + + xml + + + src/main/resources/jobs/fixedLengthImportJob.xml + src/main/resources/jobs/multilineJob.xml + src/main/resources/jobs/multilineOrderInputDescriptors.xml + src/main/resources/jobs/multilineOrderIo.xml + src/main/resources/jobs/multilineOrderJob.xml + src/main/resources/jobs/multilineOrderOutputDescriptors.xml + src/main/resources/jobs/tradeJob.xml + src/main/resources/jobs/tradeJobIo.xml + src/main/resources/jobs/xmlJob.xml + src/main/resources/jobs/restartSample.xml + src/main/resources/data-source-context.xml + src/main/resources/simple-container-definition.xml + src/main/resources/jobs/beanWrapperMapperSampleJob.xml + src/main/resources/jobs/adhocLoopJob.xml + src/main/resources/jobs/infiniteLoopJob.xml + src/main/resources/data-source-context-init.xml + + + + diff --git a/samples/20070122.testStream.CustomerReportStep.TEMP.txt b/samples/20070122.testStream.CustomerReportStep.TEMP.txt new file mode 100644 index 000000000..503b42c01 --- /dev/null +++ b/samples/20070122.testStream.CustomerReportStep.TEMP.txt @@ -0,0 +1,4 @@ +customer1 99901.66 +customer2 99969.1 +customer3 99890.75 +customer4 99876.61 diff --git a/samples/20070122.testStream.ParallelCustomerReportStep.TEMP.txt b/samples/20070122.testStream.ParallelCustomerReportStep.TEMP.txt new file mode 100644 index 000000000..5e1f0b317 --- /dev/null +++ b/samples/20070122.testStream.ParallelCustomerReportStep.TEMP.txt @@ -0,0 +1,5 @@ +Trade: [isin=UK21341EAH41,quantity=211,price=31.11,customer=customer1] +Trade: [isin=UK21341EAH42,quantity=212,price=32.11,customer=customer2] +Trade: [isin=UK21341EAH43,quantity=213,price=33.11,customer=customer3] +Trade: [isin=UK21341EAH44,quantity=214,price=34.11,customer=customer4] +Trade: [isin=UK21341EAH45,quantity=215,price=35.11,customer=customer5] diff --git a/samples/20070122.testStream.multilineStep.txt b/samples/20070122.testStream.multilineStep.txt new file mode 100644 index 000000000..e26cadbc9 --- /dev/null +++ b/samples/20070122.testStream.multilineStep.txt @@ -0,0 +1,2 @@ +[Trade: [isin=UK21341EAH45,quantity=978,price=98.34,customer=customer1], Trade: [isin=UK21341EAH46,quantity=112,price=18.12,customer=customer2]] +[Trade: [isin=UK21341EAH47,quantity=245,price=12.78,customer=customer2], Trade: [isin=UK21341EAH48,quantity=108,price=9.25,customer=customer3], Trade: [isin=UK21341EAH49,quantity=854,price=23.39,customer=customer4]] diff --git a/samples/20070122.testStream.xmlFileStep.xml b/samples/20070122.testStream.xmlFileStep.xml new file mode 100644 index 000000000..13e91dde8 --- /dev/null +++ b/samples/20070122.testStream.xmlFileStep.xml @@ -0,0 +1 @@ +Gladys Kravitz
Anytown, PA
3400
2003-01-07 14:16:00.0 GMTBurnham's Celestial Handbook, Vol 15.021.792Burnham's Celestial Handbook, Vol 25.019.892
John Smith
Chicago, IL
4600
2003-01-07 14:16:02.0 GMTXmlBeans in Action3.041.291JSR-1731.011.995Teach Yourself XML in 21 days1.035.491
Peter Newman
Cleveland, OH
2300
2003-01-07 14:16:35.0 GMTJava 62.012.793
\ No newline at end of file diff --git a/samples/20070122.teststream.multilineOrderStep.TEMP.txt b/samples/20070122.teststream.multilineOrderStep.TEMP.txt new file mode 100644 index 000000000..40214ffda --- /dev/null +++ b/samples/20070122.teststream.multilineOrderStep.TEMP.txt @@ -0,0 +1,17 @@ +BEGIN_ORDER:13100345 2007/02/15 +CUSTOMER:20014539 Peter Smith +ADDRESS:Oak Street 31/A Small Town00235 +BILLING:VISA VISA-12345678903 +ITEM:104439104137.49 +ITEM:2134776319221.99 +END_ORDER:267.34 +BEGIN_ORDER:13100346 2007/02/15 +CUSTOMER:72155919 +ADDRESS:St. Andrews Road 31 London 55342 +BILLING:AMEX AMEX-72345678903 +ITEM:10443191011070.50 +ITEM:213472721921.79 +ITEM:104433930179.95 +ITEM:213474731955.29 +ITEM:1044359501339.99 +END_ORDER:14043.74 diff --git a/samples/changelog.txt b/samples/changelog.txt new file mode 100644 index 000000000..023c8f00c --- /dev/null +++ b/samples/changelog.txt @@ -0,0 +1 @@ +Do not edit this file: use src/site/apt/changelog.apt instead. diff --git a/samples/docs/job_matrix.xls b/samples/docs/job_matrix.xls new file mode 100644 index 000000000..c2579a47c Binary files /dev/null and b/samples/docs/job_matrix.xls differ diff --git a/samples/hsql-manager.launch b/samples/hsql-manager.launch new file mode 100644 index 000000000..520b615a6 --- /dev/null +++ b/samples/hsql-manager.launch @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/samples/hsql-server.launch b/samples/hsql-server.launch new file mode 100644 index 000000000..add76ce7f --- /dev/null +++ b/samples/hsql-server.launch @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/samples/jalopy_customized.xml b/samples/jalopy_customized.xml new file mode 100644 index 000000000..459f95a14 --- /dev/null +++ b/samples/jalopy_customized.xml @@ -0,0 +1,436 @@ + + + + + 14 + + + + + true + + + [A-Z][a-zA-Z0-9]+ + [A-Z][a-zA-Z0-9]+ + + + [a-z][\w]+ + [a-z][\w]+ + [a-zA-Z][\w]+ + [a-z][\w]+ + [a-z][\w]+ + [a-zA-Z][\w]+ + [a-z][\w]+ + [a-z][\w]+ + [a-zA-Z][\w]+ + [a-z][\w]+ + [a-z][\w]+ + [a-zA-Z][\w]+ + + [A-Z][a-zA-Z0-9]+ + \w+ + + [a-z][\w]+ + [a-z][\w]+ + [a-z][\w]+ + [a-z][\w]+ + [a-z][\w]+ + [a-z][\w]+ + [a-z][\w]+ + [a-z][\w]+ + [a-z][\w]+ + [a-z][\w]+ + [a-z][\w]+ + [a-z][\w]+ + + [a-z]+(?:\.[a-z]+)* + + [a-z][\w]+ + [a-z][\w]+ + + [a-z][\w]* + + + false + false + false + false + false + false + false + false + false + false + false + false + false + false + false + false + false + false + false + + + + 6 + + + + 30000 + 30000 + 30000 + 30000 + 30000 + 30000 + + true + + + 1 + + + + true + false + true + false + false + false + + + bak + 0 + + + + 1 + 0 + 1 + 0 +
1
+
0
+ 1 + 2 + 1 + 1 +
+ + 1 + 0 + 1 + + 1 + 1 + 1 + + 1 + 1 +
0
+
0
+
+ 1 +
+ + false + + + + false + false + + + false + false + true + false + + + true + false + false + false + false + + + false + false + + + + true + true + + + + true + + + + false + true + true + + true + + 0 + 0 + 0 + 0 + + false + true + + false + + + + @task + + + /**| * DOCUMENT ME!| *| * @author $author$| * @version $Revision: 1.9 $| */ + + */ + * @throws $exceptionType$ DOCUMENT ME! + * @param $paramType$ DOCUMENT ME! + /**| * Creates a new $objectType$ object. + + /**| * DOCUMENT ME!| *| * @author $author$| * @version $Revision: 1.9 $| */ + + */ + * @throws $exceptionType$ DOCUMENT ME! + * @param $paramType$ DOCUMENT ME! + * @return DOCUMENT ME! + /**| * DOCUMENT ME! + + /** DOCUMENT ME! */ + + + + false + false + false + + + - + false + false + + Annotations + Inner Classes + Constructors + Enumerations + Instance fields + Instance initializers + Inner Interfaces + Methods + Static fields/initializers + + + + +
+ false + + 0 + false +
+
+ true + Geotools2 - OpenSource mapping toolkit + 0 + /*| * GeoTools - OpenSource mapping toolkit| * http://geotools.org| * (C) 2002-2006, GeoTools Project Managment Committee (PMC)| *| * This library is free software; you can redistribute it and/or| * modify it under the terms of the GNU Lesser General Public| * License as published by the Free Software Foundation;| * version 2.1 of the License.| *| * This library is distributed in the hope that it will be useful,| * but WITHOUT ANY WARRANTY; without even the implied warranty of| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU| * Lesser General Public License for more details.| */ + false +
+ + disabled + + + + 0 + *:0|java:1|javax:1|org.springframework:3|com.accenture:2 + + disabled + true + + + false + + true + false + + true + + + false + + + 1 + 1 + 0 + 1 + 4 + 55 + -1 + 4 + -1 + 0 + 8 + -1 + 1 + + + false + false + + + + false + false + false + true + false + true + false + + false + + + + + false + false + false + true + false + false + + false + + static|field|initializer|constructor|method|interface|class|annotation|enum + false + + + false + public=true|protected=true|private=true|abstract=true|static=true|final=true|synchronized=true|transient=true|volatile=true|native=true|strictfp=true + + + + + true + true + true + + + true + false + false + false + + false + + + false + false + true + + + + true + false + + true + true + true + true + true + true + + false + false + + + + + + 0 + false + false + false + + false + + false + false + + false + + + false + false + false + false + + + false + false + false + + + + + 2147483647 + + + true + + + + + 2147483647 + + + true + + + + + 2147483647 + + + true + + + + true + true + 100 + + + + false + false + false + + false + false + false + + + + false + + + false + + false + + false + + + +
+
diff --git a/samples/maven_checks_customized.xml b/samples/maven_checks_customized.xml new file mode 100644 index 000000000..654b243d3 --- /dev/null +++ b/samples/maven_checks_customized.xml @@ -0,0 +1,166 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/pom.xml b/samples/pom.xml new file mode 100644 index 000000000..ef389f345 --- /dev/null +++ b/samples/pom.xml @@ -0,0 +1,128 @@ + + 4.0.0 + spring-batch-samples + jar + Samples + Example batch jobs using Spring Batch Core and Execution. + + + org.springframework.batch + spring-batch + 1.0-m2-SNAPSHOT + .. + + + + + org.springframework.batch + spring-batch-execution + ${project.version} + + + aspectj + aspectjrt + 1.5.3 + + + junit + junit + 3.8.1 + test + + + org.springmodules + spring-modules-validation + 0.8 + + + org.springframework + spring + + + rhino + js + + + commons-validator + commons-validator + + + + + quartz + quartz + 1.5.1 + + + asm + asm-all + 2.2.3 + test + + + aspectj + aspectjweaver + 1.5.3 + test + + + hsqldb + hsqldb + 1.8.0.7 + test + + + commons-io + commons-io + 1.2 + test + + + commons-dbcp + commons-dbcp + 1.2.1 + + + commons-collections + commons-collections + + + test + + + geronimo-spec + geronimo-spec-j2ee + 1.4-rc4 + test + + + easymock + easymock + 1.1 + test + + + + cglib + cglib-nodep + 2.1_3 + test + + + + com.thoughtworks.xstream + xstream + 1.2.1 + test + + + + stax + stax + 1.2.0 + test + + + + diff --git a/samples/server.properties b/samples/server.properties new file mode 100644 index 000000000..7397ea159 --- /dev/null +++ b/samples/server.properties @@ -0,0 +1,5 @@ +server.port=9005 +server.trace=true + +server.database.0=file:target/hsqldb-data/samples +server.dbname.0=samples diff --git a/samples/src/main/java/org/springframework/batch/sample/FieldSetResultSetExtractor.java b/samples/src/main/java/org/springframework/batch/sample/FieldSetResultSetExtractor.java new file mode 100644 index 000000000..22e97352a --- /dev/null +++ b/samples/src/main/java/org/springframework/batch/sample/FieldSetResultSetExtractor.java @@ -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.sample; + +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +import org.springframework.batch.io.file.FieldSet; + +/** + * ResultSetExtractor implementation that returns list of FieldSets + * for given ResultSet. + * + * @author peter.zozom + * + */ +public final class FieldSetResultSetExtractor { + + // utility class not meant for instantiation + private FieldSetResultSetExtractor(){} + + /** + * Processes single row in ResultSet and returns its FieldSet representation. + * @param rs ResultSet ResultSet to extract data from. + * @return FieldSet representation of current row in ResultSet + * @throws SQLException + */ + public static FieldSet getFieldSet(ResultSet rs) throws SQLException { + ResultSetMetaData metaData = rs.getMetaData(); + int columnCount = metaData.getColumnCount(); + + FieldSet fs = null; + + List tokens = new ArrayList(); + List names = new ArrayList(); + + for (int i = 1; i <= columnCount; i++) { + tokens.add(rs.getString(i)); + names.add(metaData.getColumnName(i)); + } + + fs = new FieldSet((String[])tokens.toArray(new String[0]), (String[])names.toArray(new String[0])); + + return fs; + } + +} diff --git a/samples/src/main/java/org/springframework/batch/sample/advice/MethodExecutionLogAdvice.java b/samples/src/main/java/org/springframework/batch/sample/advice/MethodExecutionLogAdvice.java new file mode 100644 index 000000000..9e1f8cd1f --- /dev/null +++ b/samples/src/main/java/org/springframework/batch/sample/advice/MethodExecutionLogAdvice.java @@ -0,0 +1,42 @@ +/* + * 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.sample.advice; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.aspectj.lang.JoinPoint; + + +/** + * Wraps calls for 'Processing' methods which output a single Object to write + * the string representation of the object to the log. + * + * @author Lucas Ward + */ +public class MethodExecutionLogAdvice { + + private static Log log = LogFactory.getLog(MethodExecutionLogAdvice.class); + + /** + * Wraps original method and adds logging both before and after method + */ + public void doBasicLogging(JoinPoint jp) throws Throwable { + + log.info("Processed method "+jp); + } + +} diff --git a/samples/src/main/java/org/springframework/batch/sample/advice/ProcessorLogAdvice.java b/samples/src/main/java/org/springframework/batch/sample/advice/ProcessorLogAdvice.java new file mode 100644 index 000000000..9c3c57e28 --- /dev/null +++ b/samples/src/main/java/org/springframework/batch/sample/advice/ProcessorLogAdvice.java @@ -0,0 +1,52 @@ +/* + * 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.sample.advice; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.aspectj.lang.JoinPoint; + + +/** + * Wraps calls for 'Processing' methods which output a single Object to write + * the string representation of the object to the log. + * + * @author Lucas Ward + */ +public class ProcessorLogAdvice { + + private static Log log = LogFactory.getLog(ProcessorLogAdvice.class); + + /** + * Wraps original method and adds logging both before and after method + */ + public void doBasicLogging(JoinPoint pjp) throws Throwable { + Object[] args = pjp.getArgs(); + StringBuffer output = new StringBuffer(); + + for(int i = 0; i < args.length; i++){ + output.append(args[i] + " "); + } + + log.info("Processed: " + output.toString()); + } + + public void doStronglyTypedLogging(Object item){ + log.info("Processed: " + item); + } + +} diff --git a/samples/src/main/java/org/springframework/batch/sample/advice/TradeWriterLogAdvice.java b/samples/src/main/java/org/springframework/batch/sample/advice/TradeWriterLogAdvice.java new file mode 100644 index 000000000..dbf540422 --- /dev/null +++ b/samples/src/main/java/org/springframework/batch/sample/advice/TradeWriterLogAdvice.java @@ -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.sample.advice; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.aspectj.lang.JoinPoint; +import org.springframework.batch.sample.domain.Trade; + + +/** + * Wraps calls for 'Processing' methods which output a single Object to write + * the string representation of the object to the log. + * + * @author Lucas Ward + */ +public class TradeWriterLogAdvice { + + private static Log log = LogFactory.getLog(TradeWriterLogAdvice.class); + + /** + * Wraps original method and adds logging both before and after method + */ + public void doBasicLogging(JoinPoint pjp, Trade trade) throws Throwable { + + log.info("Processed: " + trade.toString()); + } + +} diff --git a/samples/src/main/java/org/springframework/batch/sample/dao/CustomerCreditWriter.java b/samples/src/main/java/org/springframework/batch/sample/dao/CustomerCreditWriter.java new file mode 100644 index 000000000..fd1e387ff --- /dev/null +++ b/samples/src/main/java/org/springframework/batch/sample/dao/CustomerCreditWriter.java @@ -0,0 +1,31 @@ +/* + * 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.sample.dao; + +import org.springframework.batch.item.ResourceLifecycle; +import org.springframework.batch.sample.domain.CustomerCredit; + +/** + * Interface for writing customer's credit information to output. + * + * @author Robert Kasanicky + */ +public interface CustomerCreditWriter extends ResourceLifecycle{ + + void write(CustomerCredit customerCredit); + +} diff --git a/samples/src/main/java/org/springframework/batch/sample/dao/CustomerDebitWriter.java b/samples/src/main/java/org/springframework/batch/sample/dao/CustomerDebitWriter.java new file mode 100644 index 000000000..0de40e558 --- /dev/null +++ b/samples/src/main/java/org/springframework/batch/sample/dao/CustomerDebitWriter.java @@ -0,0 +1,24 @@ +/* + * 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.sample.dao; + +import org.springframework.batch.sample.domain.CustomerDebit; + +public interface CustomerDebitWriter { + + void write(CustomerDebit customerDebit); +} diff --git a/samples/src/main/java/org/springframework/batch/sample/dao/FlatFileCustomerCreditWriter.java b/samples/src/main/java/org/springframework/batch/sample/dao/FlatFileCustomerCreditWriter.java new file mode 100644 index 000000000..19a0454de --- /dev/null +++ b/samples/src/main/java/org/springframework/batch/sample/dao/FlatFileCustomerCreditWriter.java @@ -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.sample.dao; + +import org.springframework.batch.io.OutputSource; +import org.springframework.batch.sample.domain.CustomerCredit; +import org.springframework.beans.factory.DisposableBean; + + +/** + * Writes customer's credit information in a file. + * + * @see CustomerCreditWriter + * @author Robert Kasanicky + */ +public class FlatFileCustomerCreditWriter implements CustomerCreditWriter, DisposableBean { + + private OutputSource outputSource; + + private String separator = "\t"; + + private volatile boolean opened = false; + + public void write(CustomerCredit customerCredit) { + + if (!opened) { + open(); + } + + String line = "" + customerCredit.getName() + separator + customerCredit.getCredit(); + + outputSource.write(line); + } + + public void setSeparator(String separator) { + this.separator = separator; + } + + public void setOutputSource(OutputSource outputSource) { + this.outputSource = outputSource; + } + + public void open() { + outputSource.open(); + opened = true; + } + + public void close() { + outputSource.close(); + } + + /* (non-Javadoc) + * @see org.springframework.beans.factory.DisposableBean#destroy() + */ + public void destroy() throws Exception { + close(); + } +} diff --git a/samples/src/main/java/org/springframework/batch/sample/dao/FlatFileOrderWriter.java b/samples/src/main/java/org/springframework/batch/sample/dao/FlatFileOrderWriter.java new file mode 100644 index 000000000..7ff3bf5d6 --- /dev/null +++ b/samples/src/main/java/org/springframework/batch/sample/dao/FlatFileOrderWriter.java @@ -0,0 +1,81 @@ +/* + * 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.sample.dao; + +import org.springframework.batch.io.OutputSource; +import org.springframework.batch.io.file.support.transform.Converter; +import org.springframework.batch.sample.domain.Order; +import org.springframework.beans.factory.DisposableBean; + + +/** + * Writes Order objects to a file. + * + * @see OrderWriter + * + * @author Dave Syer + */ +public class FlatFileOrderWriter implements OrderWriter, DisposableBean { + /** + * Takes care of writing to a file + */ + private OutputSource outputSource; + + /** + * Converter for order + */ + private Converter converter = new OrderConverter(); + + /** + * Public setter for the converter. + * + * @param converter the converter to set + */ + public void setConverter(Converter converter) { + this.converter = converter; + } + + /** + * Writes information from an Order object to a file + */ + public void write(Order data) { + outputSource.write(converter.convert(data)); + } + + public void open() { + outputSource.open(); + } + + public void close() { + outputSource.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(); + } + + public void setOutputSource(OutputSource outputSource) { + this.outputSource = outputSource; + } + +} diff --git a/samples/src/main/java/org/springframework/batch/sample/dao/JdbcCustomerDebitWriter.java b/samples/src/main/java/org/springframework/batch/sample/dao/JdbcCustomerDebitWriter.java new file mode 100644 index 000000000..ef919e7b8 --- /dev/null +++ b/samples/src/main/java/org/springframework/batch/sample/dao/JdbcCustomerDebitWriter.java @@ -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.sample.dao; + +import org.springframework.batch.sample.domain.CustomerDebit; +import org.springframework.jdbc.core.JdbcOperations; + + +/** + * Reduces customer's credit by the provided amount. + * + * @author Robert Kasanicky + */ +public class JdbcCustomerDebitWriter implements CustomerDebitWriter { + + private static final String UPDATE_CREDIT = "UPDATE customer SET credit= credit-? WHERE name=?"; + + private JdbcOperations jdbcTemplate; + + public void write(CustomerDebit customerDebit) { + jdbcTemplate.update(UPDATE_CREDIT, + new Object[] { customerDebit.getDebit(), customerDebit.getName() }); + } + + public void setJdbcTemplate(JdbcOperations jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + +} diff --git a/samples/src/main/java/org/springframework/batch/sample/dao/JdbcTradeWriter.java b/samples/src/main/java/org/springframework/batch/sample/dao/JdbcTradeWriter.java new file mode 100644 index 000000000..497945832 --- /dev/null +++ b/samples/src/main/java/org/springframework/batch/sample/dao/JdbcTradeWriter.java @@ -0,0 +1,79 @@ +/* + * 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.sample.dao; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.batch.sample.domain.Trade; +import org.springframework.jdbc.core.JdbcOperations; +import org.springframework.jdbc.support.incrementer.DataFieldMaxValueIncrementer; + + +/** + * Writes a Trade object to a database + * + * @author Robert Kasanicky + */ +public class JdbcTradeWriter implements TradeWriter { + private Log log = LogFactory.getLog(JdbcTradeWriter.class); + /** + * template for inserting a row + */ + private static final String INSERT_TRADE_RECORD = "INSERT INTO trade (id, isin, quantity, price, customer) VALUES (?, ?, ? ,?, ?)"; + + /** + * handles the processing of sql query + */ + private JdbcOperations jdbcTemplate; + + /** + * database is not expected to be setup for autoincrementation + */ + private DataFieldMaxValueIncrementer incrementer; + + /** + * @see TradeWriter + */ + public void writeTrade(Trade trade) { + Long id = new Long(incrementer.nextLongValue()); + log.debug("Processing: " + trade); + jdbcTemplate.update(INSERT_TRADE_RECORD, + new Object[] { + id, trade.getIsin(), new Long(trade.getQuantity()), trade.getPrice(), + trade.getCustomer() + }); + } + + public void setJdbcTemplate(JdbcOperations jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + public void setIncrementer(DataFieldMaxValueIncrementer incrementer) { + this.incrementer = incrementer; + } + + public void write(Object output) { + this.writeTrade((Trade)output); + } + + public void close() { + } + + public void open() { + } + +} diff --git a/samples/src/main/java/org/springframework/batch/sample/dao/OrderConverter.java b/samples/src/main/java/org/springframework/batch/sample/dao/OrderConverter.java new file mode 100644 index 000000000..fd090972d --- /dev/null +++ b/samples/src/main/java/org/springframework/batch/sample/dao/OrderConverter.java @@ -0,0 +1,117 @@ +/* + * 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.sample.dao; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.springframework.batch.io.file.support.transform.Converter; +import org.springframework.batch.io.file.support.transform.LineAggregator; +import org.springframework.batch.sample.domain.Address; +import org.springframework.batch.sample.domain.BillingInfo; +import org.springframework.batch.sample.domain.Customer; +import org.springframework.batch.sample.domain.LineItem; +import org.springframework.batch.sample.domain.Order; + + +/** + * Converts Order object to a String. + * @author Dave Syer + */ +public class OrderConverter implements Converter { + + /** + * Aggregators for all types of lines in the output file + */ + private Map aggregators; + + /** + * Converts information from an Order object to a collection of Strings for output. + */ + public Object convert(Object data) { + Order order = (Order) data; + + List result = new ArrayList(); + + result.add(getAggregator("header").aggregate(OrderFormatterUtils.headerArgs(order))); + result.add(getAggregator("customer").aggregate(OrderFormatterUtils.customerArgs(order))); + result.add(getAggregator("address").aggregate(OrderFormatterUtils.billingAddressArgs(order))); + result.add(getAggregator("billing").aggregate(OrderFormatterUtils.billingInfoArgs(order))); + + List items = order.getLineItems(); + LineItem item; + + for (int i = 0; i < items.size(); i++) { + item = (LineItem) items.get(i); + result.add(getAggregator("item").aggregate(OrderFormatterUtils.lineItemArgs(item))); + } + + result.add(getAggregator("footer").aggregate(OrderFormatterUtils.footerArgs(order))); + + return result; + } + + public void setAggregators(Map aggregators) { + this.aggregators = aggregators; + } + + private LineAggregator getAggregator(String name) { + return (LineAggregator) aggregators.get(name); + } + + /** + * Utility class encapsulating formatting of Order and its nested objects. + */ + private static class OrderFormatterUtils { + + private static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd"); + + static String[] headerArgs(Order order) { + return new String[] { "BEGIN_ORDER:", String.valueOf(order.getOrderId()), dateFormat.format(order.getOrderDate()) }; + } + + static String[] footerArgs(Order order) { + return new String[] { "END_ORDER:", order.getTotalPrice().toString() }; + } + + static String[] customerArgs(Order order) { + Customer customer = order.getCustomer(); + + return new String[] { "CUSTOMER:", String.valueOf(customer.getRegistrationId()), customer.getFirstName(), + customer.getMiddleName(), customer.getLastName() }; + } + + static String[] lineItemArgs(LineItem item) { + return new String[] { "ITEM:", String.valueOf(item.getItemId()), item.getPrice().toString() }; + } + + static String[] billingAddressArgs(Order order) { + Address address = order.getBillingAddress(); + + return new String[] { "ADDRESS:", address.getAddrLine1(), address.getCity(), address.getZipCode() }; + } + + static String[] billingInfoArgs(Order order) { + BillingInfo billingInfo = order.getBilling(); + + return new String[] { "BILLING:", billingInfo.getPaymentId(), billingInfo.getPaymentDesc() }; + } + } + +} diff --git a/samples/src/main/java/org/springframework/batch/sample/dao/OrderWriter.java b/samples/src/main/java/org/springframework/batch/sample/dao/OrderWriter.java new file mode 100644 index 000000000..df2594527 --- /dev/null +++ b/samples/src/main/java/org/springframework/batch/sample/dao/OrderWriter.java @@ -0,0 +1,29 @@ +/* + * 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.sample.dao; + +import org.springframework.batch.item.ResourceLifecycle; +import org.springframework.batch.sample.domain.Order; + +/** + * Interface for writing Order objects. + */ +public interface OrderWriter extends ResourceLifecycle { + + public void write(Order order); + +} diff --git a/samples/src/main/java/org/springframework/batch/sample/dao/TradeWriter.java b/samples/src/main/java/org/springframework/batch/sample/dao/TradeWriter.java new file mode 100644 index 000000000..f57213b15 --- /dev/null +++ b/samples/src/main/java/org/springframework/batch/sample/dao/TradeWriter.java @@ -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.sample.dao; + +import org.springframework.batch.io.OutputSource; +import org.springframework.batch.sample.domain.Trade; + + +/** + * Simple interface for writing a Trade object + * to an arbitraty output + * + * @author Robert Kasanicky + */ +public interface TradeWriter extends OutputSource{ + /** + * Write a trade object to some kind of output, + * different implementations can write to file, database etc. + */ + void writeTrade(Trade trade); + +} diff --git a/samples/src/main/java/org/springframework/batch/sample/domain/Address.java b/samples/src/main/java/org/springframework/batch/sample/domain/Address.java new file mode 100644 index 000000000..1eba53f9d --- /dev/null +++ b/samples/src/main/java/org/springframework/batch/sample/domain/Address.java @@ -0,0 +1,103 @@ +/* + * 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.sample.domain; + +import org.apache.commons.lang.builder.EqualsBuilder; +import org.apache.commons.lang.builder.HashCodeBuilder; +import org.apache.commons.lang.builder.ToStringBuilder; + +public class Address { + public static final String LINE_ID_BILLING_ADDR = "BAD"; + public static final String LINE_ID_SHIPPING_ADDR = "SAD"; + private String addressee; + private String addrLine1; + private String addrLine2; + private String city; + private String zipCode; + private String state; + private String country; + + public String getAddrLine1() { + return addrLine1; + } + + public void setAddrLine1(String addrLine1) { + this.addrLine1 = addrLine1; + } + + public String getAddrLine2() { + return addrLine2; + } + + public void setAddrLine2(String addrLine2) { + this.addrLine2 = addrLine2; + } + + public String getAddressee() { + return addressee; + } + + public void setAddressee(String addressee) { + this.addressee = addressee; + } + + public String getCity() { + return city; + } + + public void setCity(String city) { + this.city = city; + } + + public String getCountry() { + return country; + } + + public void setCountry(String country) { + this.country = country; + } + + public String getState() { + return state; + } + + public void setState(String state) { + this.state = state; + } + + public String getZipCode() { + return zipCode; + } + + public void setZipCode(String zipCode) { + this.zipCode = zipCode; + } + + public String toString() { + return ToStringBuilder.reflectionToString(this); + } + + public boolean equals(Object o) { + return EqualsBuilder.reflectionEquals(this, o); + } + + public int hashCode() { + return HashCodeBuilder.reflectionHashCode(this); + } + + +} diff --git a/samples/src/main/java/org/springframework/batch/sample/domain/BillingInfo.java b/samples/src/main/java/org/springframework/batch/sample/domain/BillingInfo.java new file mode 100644 index 000000000..d74b3c5b0 --- /dev/null +++ b/samples/src/main/java/org/springframework/batch/sample/domain/BillingInfo.java @@ -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.sample.domain; + +import org.apache.commons.lang.builder.EqualsBuilder; +import org.apache.commons.lang.builder.HashCodeBuilder; +import org.apache.commons.lang.builder.ToStringBuilder; + +public class BillingInfo { + public static final String LINE_ID_BILLING_INFO = "BIN"; + private String paymentId; + private String paymentDesc; + + public String getPaymentDesc() { + return paymentDesc; + } + + public void setPaymentDesc(String paymentDesc) { + this.paymentDesc = paymentDesc; + } + + public String getPaymentId() { + return paymentId; + } + + public void setPaymentId(String paymentId) { + this.paymentId = paymentId; + } + + public String toString() { + return ToStringBuilder.reflectionToString(this); + } + + public boolean equals(Object o) { + return EqualsBuilder.reflectionEquals(this, o); + } + + public int hashCode() { + return HashCodeBuilder.reflectionHashCode(this); + } + + +} diff --git a/samples/src/main/java/org/springframework/batch/sample/domain/Child.java b/samples/src/main/java/org/springframework/batch/sample/domain/Child.java new file mode 100644 index 000000000..3ea8ad23a --- /dev/null +++ b/samples/src/main/java/org/springframework/batch/sample/domain/Child.java @@ -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.sample.domain; + +import org.apache.commons.lang.builder.EqualsBuilder; +import org.apache.commons.lang.builder.HashCodeBuilder; +import org.apache.commons.lang.builder.ToStringBuilder; + +public class Child { + + private String name; + + public void setName(String name){ + this.name = name; + } + + public String getName(){ + return name; + } + + public String toString() { + return ToStringBuilder.reflectionToString(this); + } + + public boolean equals(Object o) { + return EqualsBuilder.reflectionEquals(this, o); + } + + public int hashCode() { + return HashCodeBuilder.reflectionHashCode(this); + } + +} diff --git a/samples/src/main/java/org/springframework/batch/sample/domain/Customer.java b/samples/src/main/java/org/springframework/batch/sample/domain/Customer.java new file mode 100644 index 000000000..c102b42d3 --- /dev/null +++ b/samples/src/main/java/org/springframework/batch/sample/domain/Customer.java @@ -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.sample.domain; + +import org.apache.commons.lang.builder.EqualsBuilder; +import org.apache.commons.lang.builder.HashCodeBuilder; +import org.apache.commons.lang.builder.ToStringBuilder; + +public class Customer { + public static final String LINE_ID_BUSINESS_CUST = "BCU"; + public static final String LINE_ID_NON_BUSINESS_CUST = "NCU"; + private boolean businessCustomer; + private boolean registered; + private long registrationId; + + //non-business customer + private String firstName; + private String lastName; + private String middleName; + private boolean vip; + + //business customer + private String companyName; + + public boolean isBusinessCustomer() { + return businessCustomer; + } + + public void setBusinessCustomer(boolean bussinessCustomer) { + this.businessCustomer = bussinessCustomer; + } + + public String getCompanyName() { + return companyName; + } + + public void setCompanyName(String companyName) { + this.companyName = companyName; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public boolean isRegistered() { + return registered; + } + + public void setRegistered(boolean registered) { + this.registered = registered; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public String getMiddleName() { + return middleName; + } + + public void setMiddleName(String middleName) { + this.middleName = middleName; + } + + public long getRegistrationId() { + return registrationId; + } + + public void setRegistrationId(long registrationId) { + this.registrationId = registrationId; + } + + public boolean isVip() { + return vip; + } + + public void setVip(boolean vip) { + this.vip = vip; + } + + public String toString() { + return ToStringBuilder.reflectionToString(this); + } + + public boolean equals(Object o) { + return EqualsBuilder.reflectionEquals(this, o); + } + + public int hashCode() { + return HashCodeBuilder.reflectionHashCode(this); + } + +} diff --git a/samples/src/main/java/org/springframework/batch/sample/domain/CustomerCredit.java b/samples/src/main/java/org/springframework/batch/sample/domain/CustomerCredit.java new file mode 100644 index 000000000..b8a7bca05 --- /dev/null +++ b/samples/src/main/java/org/springframework/batch/sample/domain/CustomerCredit.java @@ -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.sample.domain; + +import java.math.BigDecimal; + +import org.apache.commons.lang.builder.EqualsBuilder; +import org.apache.commons.lang.builder.HashCodeBuilder; + + +public class CustomerCredit { + private String name; + private BigDecimal credit; + + public String toString() { + return "CustomerCredit [name=" + name + ", credit=" + credit + "]"; + } + + public BigDecimal getCredit() { + return credit; + } + + public void setCredit(BigDecimal credit) { + this.credit = credit; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public boolean equals(Object o) { + return EqualsBuilder.reflectionEquals(this, o); + } + + public int hashCode() { + return HashCodeBuilder.reflectionHashCode(this); + } + +} diff --git a/samples/src/main/java/org/springframework/batch/sample/domain/CustomerDebit.java b/samples/src/main/java/org/springframework/batch/sample/domain/CustomerDebit.java new file mode 100644 index 000000000..5c93fd49c --- /dev/null +++ b/samples/src/main/java/org/springframework/batch/sample/domain/CustomerDebit.java @@ -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.sample.domain; + +import java.math.BigDecimal; + +import org.apache.commons.lang.builder.EqualsBuilder; +import org.apache.commons.lang.builder.HashCodeBuilder; + + +public class CustomerDebit { + private String name; + private BigDecimal debit; + + public CustomerDebit() { + } + + CustomerDebit(String name, BigDecimal debit) { + this.name = name; + this.debit = debit; + } + + public BigDecimal getDebit() { + return debit; + } + + public void setDebit(BigDecimal debit) { + this.debit = debit; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String toString() { + return "CustomerDebit [name=" + name + ", debit=" + debit + "]"; + } + + public boolean equals(Object o) { + return EqualsBuilder.reflectionEquals(this, o); + } + + public int hashCode() { + return HashCodeBuilder.reflectionHashCode(this); + } +} diff --git a/samples/src/main/java/org/springframework/batch/sample/domain/LineItem.java b/samples/src/main/java/org/springframework/batch/sample/domain/LineItem.java new file mode 100644 index 000000000..cdeb81907 --- /dev/null +++ b/samples/src/main/java/org/springframework/batch/sample/domain/LineItem.java @@ -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.sample.domain; + +import java.math.BigDecimal; + +import org.apache.commons.lang.builder.EqualsBuilder; +import org.apache.commons.lang.builder.HashCodeBuilder; +import org.apache.commons.lang.builder.ToStringBuilder; + + +public class LineItem { + public static final String LINE_ID_ITEM = "LIT"; + private long itemId; + private BigDecimal price; + private BigDecimal discountPerc; + private BigDecimal discountAmount; + private BigDecimal shippingPrice; + private BigDecimal handlingPrice; + private int quantity; + private BigDecimal totalPrice; + + public BigDecimal getDiscountAmount() { + return discountAmount; + } + + public void setDiscountAmount(BigDecimal discountAmount) { + this.discountAmount = discountAmount; + } + + public BigDecimal getDiscountPerc() { + return discountPerc; + } + + public void setDiscountPerc(BigDecimal discountPerc) { + this.discountPerc = discountPerc; + } + + public BigDecimal getHandlingPrice() { + return handlingPrice; + } + + public void setHandlingPrice(BigDecimal handlingPrice) { + this.handlingPrice = handlingPrice; + } + + public long getItemId() { + return itemId; + } + + public void setItemId(long itemId) { + this.itemId = itemId; + } + + public BigDecimal getPrice() { + return price; + } + + public void setPrice(BigDecimal price) { + this.price = price; + } + + public int getQuantity() { + return quantity; + } + + public void setQuantity(int quantity) { + this.quantity = quantity; + } + + public BigDecimal getShippingPrice() { + return shippingPrice; + } + + public void setShippingPrice(BigDecimal shippingPrice) { + this.shippingPrice = shippingPrice; + } + + public BigDecimal getTotalPrice() { + return totalPrice; + } + + public void setTotalPrice(BigDecimal totalPrice) { + this.totalPrice = totalPrice; + } + + public String toString() { + return ToStringBuilder.reflectionToString(this); + } + + public boolean equals(Object o) { + return EqualsBuilder.reflectionEquals(this, o); + } + + public int hashCode() { + return HashCodeBuilder.reflectionHashCode(this); + } + +} diff --git a/samples/src/main/java/org/springframework/batch/sample/domain/Order.java b/samples/src/main/java/org/springframework/batch/sample/domain/Order.java new file mode 100644 index 000000000..35823b9a8 --- /dev/null +++ b/samples/src/main/java/org/springframework/batch/sample/domain/Order.java @@ -0,0 +1,148 @@ +/* + * 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.sample.domain; + +import java.math.BigDecimal; +import java.util.Date; +import java.util.List; + +import org.apache.commons.lang.builder.EqualsBuilder; +import org.apache.commons.lang.builder.HashCodeBuilder; +import org.apache.commons.lang.builder.ToStringBuilder; + + +public class Order { + public static final String LINE_ID_HEADER = "HEA"; + public static final String LINE_ID_FOOTER = "FOT"; + + //header + private long orderId; + private Date orderDate; + + //footer + private int totalLines; + private int totalItems; + private BigDecimal totalPrice; + private Customer customer; + private Address billingAddress; + private Address shippingAddress; + private BillingInfo billing; + private ShippingInfo shipping; + + //order items + private List lineItems; + + public BillingInfo getBilling() { + return billing; + } + + public void setBilling(BillingInfo billing) { + this.billing = billing; + } + + public Address getBillingAddress() { + return billingAddress; + } + + public void setBillingAddress(Address billingAddress) { + this.billingAddress = billingAddress; + } + + public Customer getCustomer() { + return customer; + } + + public void setCustomer(Customer customer) { + this.customer = customer; + } + + public List getLineItems() { + return lineItems; + } + + public void setLineItems(List lineItems) { + this.lineItems = lineItems; + } + + public Date getOrderDate() { + return orderDate; + } + + public void setOrderDate(Date orderDate) { + this.orderDate = orderDate; + } + + public long getOrderId() { + return orderId; + } + + public void setOrderId(long orderId) { + this.orderId = orderId; + } + + public ShippingInfo getShipping() { + return shipping; + } + + public void setShipping(ShippingInfo shipping) { + this.shipping = shipping; + } + + public Address getShippingAddress() { + return shippingAddress; + } + + public void setShippingAddress(Address shippingAddress) { + this.shippingAddress = shippingAddress; + } + + public BigDecimal getTotalPrice() { + return totalPrice; + } + + public void setTotalPrice(BigDecimal totalPrice) { + this.totalPrice = totalPrice; + } + + public int getTotalItems() { + return totalItems; + } + + public void setTotalItems(int totalItems) { + this.totalItems = totalItems; + } + + public int getTotalLines() { + return totalLines; + } + + public void setTotalLines(int totalLines) { + this.totalLines = totalLines; + } + + public String toString() { + return ToStringBuilder.reflectionToString(this); + } + + public boolean equals(Object o) { + return EqualsBuilder.reflectionEquals(this, o); + } + + public int hashCode() { + return HashCodeBuilder.reflectionHashCode(this); + } +} diff --git a/samples/src/main/java/org/springframework/batch/sample/domain/Person.java b/samples/src/main/java/org/springframework/batch/sample/domain/Person.java new file mode 100644 index 000000000..e01edc9d1 --- /dev/null +++ b/samples/src/main/java/org/springframework/batch/sample/domain/Person.java @@ -0,0 +1,126 @@ +/* + * 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.sample.domain; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.lang.builder.EqualsBuilder; +import org.apache.commons.lang.builder.HashCodeBuilder; +import org.apache.commons.lang.builder.ToStringBuilder; + +public class Person { + + private String title = ""; + private String firstName = ""; + private String last_name = ""; + private int age = 0; + private Address address = new Address(); + private List children = new ArrayList(); + + public Person(){ + children.add(new Child()); + children.add(new Child()); + } + + /** + * @return the address + */ + public Address getAddress() { + return address; + } + /** + * @param address the address to set + */ + public void setAddress(Address address) { + this.address = address; + } + /** + * @return the age + */ + public int getAge() { + return age; + } + /** + * @param age the age to set + */ + public void setAge(int age) { + this.age = age; + } + /** + * @return the firstName + */ + public String getFirstName() { + return firstName; + } + /** + * @param firstName the firstName to set + */ + public void setFirstName(String firstName) { + this.firstName = firstName; + } + /** + * @return the children + */ + public List getChildren() { + return children; + } + /** + * @param children the children to set + */ + public void setChildren(List children) { + this.children = children; + } + /** + * Intentionally non-standard method name for testing purposes + * @return the last_name + */ + public String getLast_name() { + return last_name; + } + /** + * Intentionally non-standard method name for testing purposes + * @param last_name the last_name to set + */ + public void setLast_name(String last_name) { + this.last_name = last_name; + } + /** + * @return the person_title + */ + public String getTitle() { + return title; + } + /** + * @param person_title the person_title to set + */ + public void setTitle(String title) { + this.title = title; + } + + public String toString() { + return ToStringBuilder.reflectionToString(this); + } + + public boolean equals(Object o) { + return EqualsBuilder.reflectionEquals(this, o); + } + + public int hashCode() { + return HashCodeBuilder.reflectionHashCode(this); + } +} diff --git a/samples/src/main/java/org/springframework/batch/sample/domain/ShippingInfo.java b/samples/src/main/java/org/springframework/batch/sample/domain/ShippingInfo.java new file mode 100644 index 000000000..7feed873c --- /dev/null +++ b/samples/src/main/java/org/springframework/batch/sample/domain/ShippingInfo.java @@ -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.sample.domain; + +import org.apache.commons.lang.builder.EqualsBuilder; +import org.apache.commons.lang.builder.HashCodeBuilder; + +public class ShippingInfo { + public static final String LINE_ID_SHIPPING_INFO = "SIN"; + private String shipperId; + private String shippingTypeId; + private String shippingInfo; + + public String getShipperId() { + return shipperId; + } + + public void setShipperId(String shipperId) { + this.shipperId = shipperId; + } + + public String getShippingInfo() { + return shippingInfo; + } + + public void setShippingInfo(String shippingInfo) { + this.shippingInfo = shippingInfo; + } + + public String getShippingTypeId() { + return shippingTypeId; + } + + public void setShippingTypeId(String shippingTypeId) { + this.shippingTypeId = shippingTypeId; + } + + public boolean equals(Object o) { + return EqualsBuilder.reflectionEquals(this, o); + } + + public int hashCode() { + return HashCodeBuilder.reflectionHashCode(this); + } +} diff --git a/samples/src/main/java/org/springframework/batch/sample/domain/Trade.java b/samples/src/main/java/org/springframework/batch/sample/domain/Trade.java new file mode 100644 index 000000000..6797dea5e --- /dev/null +++ b/samples/src/main/java/org/springframework/batch/sample/domain/Trade.java @@ -0,0 +1,88 @@ +/* + * 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.sample.domain; + +import java.math.BigDecimal; + +import org.apache.commons.lang.builder.EqualsBuilder; +import org.apache.commons.lang.builder.HashCodeBuilder; + + +/** + * @author Rob Harrop + */ +public class Trade { + private String isin = ""; + private long quantity = 0; + private BigDecimal price = new BigDecimal(0); + private String customer = ""; + + public Trade() { + } + + public Trade(String isin, long quantity, BigDecimal price, String customer){ + this.isin = isin; + this.quantity = quantity; + this.price = price; + this.customer = customer; + } + + public void setCustomer(String customer) { + this.customer = customer; + } + + public void setIsin(String isin) { + this.isin = isin; + } + + public void setPrice(BigDecimal price) { + this.price = price; + } + + public void setQuantity(long quantity) { + this.quantity = quantity; + } + + public String getIsin() { + return isin; + } + + public BigDecimal getPrice() { + return price; + } + + public long getQuantity() { + return quantity; + } + + public String getCustomer() { + return customer; + } + + public String toString() { + return "Trade: [isin=" + this.isin + ",quantity=" + this.quantity + ",price=" + + this.price + ",customer=" + this.customer + "]"; + } + + public boolean equals(Object o) { + return EqualsBuilder.reflectionEquals(this, o); + } + + public int hashCode() { + return HashCodeBuilder.reflectionHashCode(this); + } +} diff --git a/samples/src/main/java/org/springframework/batch/sample/domain/xml/Customer.java b/samples/src/main/java/org/springframework/batch/sample/domain/xml/Customer.java new file mode 100644 index 000000000..0e47c9fd1 --- /dev/null +++ b/samples/src/main/java/org/springframework/batch/sample/domain/xml/Customer.java @@ -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.sample.domain.xml; + +import org.apache.commons.lang.builder.ToStringBuilder; + + +/** + * An XML customer. + * + * This is a complex type. + */ +public class Customer { + private String name; + private String address; + private int age; + private int moo; + private int poo; + + public String getAddress() { + return address; + } + + public void setAddress(String address) { + this.address = address; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getMoo() { + return moo; + } + + public void setMoo(int moo) { + this.moo = moo; + } + + public int getPoo() { + return poo; + } + + public void setPoo(int poo) { + this.poo = poo; + } + + public String toString() { + return ToStringBuilder.reflectionToString(this); + } +} diff --git a/samples/src/main/java/org/springframework/batch/sample/domain/xml/LineItem.java b/samples/src/main/java/org/springframework/batch/sample/domain/xml/LineItem.java new file mode 100644 index 000000000..ed572b215 --- /dev/null +++ b/samples/src/main/java/org/springframework/batch/sample/domain/xml/LineItem.java @@ -0,0 +1,62 @@ +/* + * 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.sample.domain.xml; + + +/** + * An XML line-item. + * + * This is a complex type. + */ +public class LineItem { + private String description; + private double perUnitOunces; + private double price; + private int quantity; + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public double getPerUnitOunces() { + return perUnitOunces; + } + + public void setPerUnitOunces(double perUnitOunces) { + this.perUnitOunces = perUnitOunces; + } + + public double getPrice() { + return price; + } + + public void setPrice(double price) { + this.price = price; + } + + public int getQuantity() { + return quantity; + } + + public void setQuantity(int quantity) { + this.quantity = quantity; + } +} diff --git a/samples/src/main/java/org/springframework/batch/sample/domain/xml/Order.java b/samples/src/main/java/org/springframework/batch/sample/domain/xml/Order.java new file mode 100644 index 000000000..9cb32a1ba --- /dev/null +++ b/samples/src/main/java/org/springframework/batch/sample/domain/xml/Order.java @@ -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.sample.domain.xml; + +import java.util.Date; +import java.util.List; + +import org.apache.commons.lang.builder.ToStringBuilder; + + +/** + * An XML order. + * + * This is a complex type. + */ +public class Order { + private Customer customer; + private Date date; + private List lineItems; + private Shipper shipper; + + public Customer getCustomer() { + return customer; + } + + public void setCustomer(Customer customer) { + this.customer = customer; + } + + public Date getDate() { + return date; + } + + public void setDate(Date date) { + this.date = date; + } + + public List getLineItems() { + return lineItems; + } + + public void setLineItems(List lineItems) { + this.lineItems = lineItems; + } + + public Shipper getShipper() { + return shipper; + } + + public void setShipper(Shipper shipper) { + this.shipper = shipper; + } + + public String toString() { + return ToStringBuilder.reflectionToString(this); + } +} diff --git a/samples/src/main/java/org/springframework/batch/sample/domain/xml/Shipper.java b/samples/src/main/java/org/springframework/batch/sample/domain/xml/Shipper.java new file mode 100644 index 000000000..b90d29a10 --- /dev/null +++ b/samples/src/main/java/org/springframework/batch/sample/domain/xml/Shipper.java @@ -0,0 +1,44 @@ +/* + * 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.sample.domain.xml; + + +/** + * An XML shipper. + * + * This is a complex type. + */ +public class Shipper { + private String name; + private double perOunceRate; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public double getPerOunceRate() { + return perOunceRate; + } + + public void setPerOunceRate(double perOunceRate) { + this.perOunceRate = perOunceRate; + } +} diff --git a/samples/src/main/java/org/springframework/batch/sample/launch/QuartzBatchLauncher.java b/samples/src/main/java/org/springframework/batch/sample/launch/QuartzBatchLauncher.java new file mode 100644 index 000000000..7b3ecfa20 --- /dev/null +++ b/samples/src/main/java/org/springframework/batch/sample/launch/QuartzBatchLauncher.java @@ -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.sample.launch; + +import java.io.IOException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.context.support.ClassPathXmlApplicationContext; + + +public class QuartzBatchLauncher { + private static Log log = LogFactory.getLog(QuartzBatchLauncher.class); + + public static void main(String[] args) throws IOException { + if (args[0] == null) { + log.error("Missing argument: provide a path to configuration file"); + System.exit(-1); + } + + new ClassPathXmlApplicationContext(args[0] + ".xml"); + + log.info("Quartz context initialized"); + } +} diff --git a/samples/src/main/java/org/springframework/batch/sample/mapping/AddressFieldSetMapper.java b/samples/src/main/java/org/springframework/batch/sample/mapping/AddressFieldSetMapper.java new file mode 100644 index 000000000..286aa6afe --- /dev/null +++ b/samples/src/main/java/org/springframework/batch/sample/mapping/AddressFieldSetMapper.java @@ -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.sample.mapping; + +import org.springframework.batch.io.file.FieldSet; +import org.springframework.batch.io.file.FieldSetMapper; +import org.springframework.batch.sample.domain.Address; + + + +public class AddressFieldSetMapper implements FieldSetMapper { + + public static final String ADDRESSEE_COLUMN = "ADDRESSEE"; + public static final String ADDRESS_LINE1_COLUMN = "ADDR_LINE1"; + public static final String ADDRESS_LINE2_COLUMN = "ADDR_LINE2"; + public static final String CITY_COLUMN = "CITY"; + public static final String ZIP_CODE_COLUMN = "ZIP_CODE"; + public static final String STATE_COLUMN = "STATE"; + public static final String COUNTRY_COLUMN = "COUNTRY"; + + + public Object mapLine(FieldSet fieldSet) { + Address address = new Address(); + + address.setAddressee(fieldSet.readString(ADDRESSEE_COLUMN)); + address.setAddrLine1(fieldSet.readString(ADDRESS_LINE1_COLUMN)); + address.setAddrLine2(fieldSet.readString(ADDRESS_LINE2_COLUMN)); + address.setCity(fieldSet.readString(CITY_COLUMN)); + address.setZipCode(fieldSet.readString(ZIP_CODE_COLUMN)); + address.setState(fieldSet.readString(STATE_COLUMN)); + address.setCountry(fieldSet.readString(COUNTRY_COLUMN)); + + return address; + } +} diff --git a/samples/src/main/java/org/springframework/batch/sample/mapping/BillingFieldSetMapper.java b/samples/src/main/java/org/springframework/batch/sample/mapping/BillingFieldSetMapper.java new file mode 100644 index 000000000..b9899ae03 --- /dev/null +++ b/samples/src/main/java/org/springframework/batch/sample/mapping/BillingFieldSetMapper.java @@ -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.sample.mapping; + +import org.springframework.batch.io.file.FieldSet; +import org.springframework.batch.io.file.FieldSetMapper; +import org.springframework.batch.sample.domain.BillingInfo; + + + +public class BillingFieldSetMapper implements FieldSetMapper { + + public static final String PAYMENT_TYPE_ID_COLUMN = "PAYMENT_TYPE_ID"; + public static final String PAYMENT_DESC_COLUMN = "PAYMENT_DESC"; + + public Object mapLine(FieldSet fieldSet) { + BillingInfo info = new BillingInfo(); + + info.setPaymentId(fieldSet.readString(PAYMENT_TYPE_ID_COLUMN)); + info.setPaymentDesc(fieldSet.readString(PAYMENT_DESC_COLUMN)); + + return info; + } +} diff --git a/samples/src/main/java/org/springframework/batch/sample/mapping/CustomerCreditRowMapper.java b/samples/src/main/java/org/springframework/batch/sample/mapping/CustomerCreditRowMapper.java new file mode 100644 index 000000000..3ad2252e8 --- /dev/null +++ b/samples/src/main/java/org/springframework/batch/sample/mapping/CustomerCreditRowMapper.java @@ -0,0 +1,23 @@ +package org.springframework.batch.sample.mapping; + +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.springframework.batch.sample.domain.CustomerCredit; +import org.springframework.jdbc.core.RowMapper; + +public class CustomerCreditRowMapper implements RowMapper { + + public static final String NAME_COLUMN = "name"; + public static final String CREDIT_COLUMN = "credit"; + + public Object mapRow(ResultSet rs, int rowNum) throws SQLException { + CustomerCredit customerCredit = new CustomerCredit(); + + customerCredit.setName(rs.getString(NAME_COLUMN)); + customerCredit.setCredit(rs.getBigDecimal(CREDIT_COLUMN)); + + return customerCredit; + } + +} diff --git a/samples/src/main/java/org/springframework/batch/sample/mapping/CustomerFieldSetMapper.java b/samples/src/main/java/org/springframework/batch/sample/mapping/CustomerFieldSetMapper.java new file mode 100644 index 000000000..3e2aa019e --- /dev/null +++ b/samples/src/main/java/org/springframework/batch/sample/mapping/CustomerFieldSetMapper.java @@ -0,0 +1,58 @@ +/* + * 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.sample.mapping; + +import org.springframework.batch.io.file.FieldSet; +import org.springframework.batch.io.file.FieldSetMapper; +import org.springframework.batch.sample.domain.Customer; + + + +public class CustomerFieldSetMapper implements FieldSetMapper { + + public static final String LINE_ID_COLUMN = "LINE_ID"; + public static final String COMPANY_NAME_COLUMN = "COMPANY_NAME"; + public static final String LAST_NAME_COLUMN = "LAST_NAME"; + public static final String FIRST_NAME_COLUMN = "FIRST_NAME"; + public static final String MIDDLE_NAME_COLUMN = "MIDDLE_NAME"; + public static final String TRUE_SYMBOL = "T"; + public static final String REGISTERED_COLUMN = "REGISTERED"; + public static final String REG_ID_COLUMN = "REG_ID"; + public static final String VIP_COLUMN = "VIP"; + + public Object mapLine(FieldSet fieldSet) { + Customer customer = new Customer(); + + if (Customer.LINE_ID_BUSINESS_CUST.equals(fieldSet.readString(LINE_ID_COLUMN))) { + customer.setCompanyName(fieldSet.readString(COMPANY_NAME_COLUMN)); + //business customer must be always registered + customer.setRegistered(true); + } + + if (Customer.LINE_ID_NON_BUSINESS_CUST.equals(fieldSet.readString(LINE_ID_COLUMN))) { + customer.setLastName(fieldSet.readString(LAST_NAME_COLUMN)); + customer.setFirstName(fieldSet.readString(FIRST_NAME_COLUMN)); + customer.setMiddleName(fieldSet.readString(MIDDLE_NAME_COLUMN)); + customer.setRegistered(TRUE_SYMBOL.equals(fieldSet.readString(REGISTERED_COLUMN))); + } + + customer.setRegistrationId(fieldSet.readLong(REG_ID_COLUMN)); + customer.setVip(TRUE_SYMBOL.equals(fieldSet.readString(VIP_COLUMN))); + + return customer; + } +} diff --git a/samples/src/main/java/org/springframework/batch/sample/mapping/CustomerUpdateMapper.java b/samples/src/main/java/org/springframework/batch/sample/mapping/CustomerUpdateMapper.java new file mode 100644 index 000000000..a5cd31201 --- /dev/null +++ b/samples/src/main/java/org/springframework/batch/sample/mapping/CustomerUpdateMapper.java @@ -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.sample.mapping; + +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.springframework.batch.sample.domain.CustomerDebit; +import org.springframework.jdbc.core.RowMapper; + + +public class CustomerUpdateMapper implements RowMapper { + + public static final String CUSTOMER_COLUMN = "customer"; + public static final String PRICE_COLUMN = "price"; + + public Object mapRow(ResultSet rs, int ignoredRowNumber) + throws SQLException { + CustomerDebit customerDebit = new CustomerDebit(); + + customerDebit.setName(rs.getString(CUSTOMER_COLUMN)); + customerDebit.setDebit(rs.getBigDecimal(PRICE_COLUMN)); + + return customerDebit; + } +} diff --git a/samples/src/main/java/org/springframework/batch/sample/mapping/HeaderFieldSetMapper.java b/samples/src/main/java/org/springframework/batch/sample/mapping/HeaderFieldSetMapper.java new file mode 100644 index 000000000..2c3d45bce --- /dev/null +++ b/samples/src/main/java/org/springframework/batch/sample/mapping/HeaderFieldSetMapper.java @@ -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.sample.mapping; + +import org.springframework.batch.io.file.FieldSet; +import org.springframework.batch.io.file.FieldSetMapper; +import org.springframework.batch.sample.domain.Order; + + + +public class HeaderFieldSetMapper implements FieldSetMapper { + + public static final String ORDER_ID_COLUMN = "ORDER_ID"; + public static final String ORDER_DATE_COLUMN = "ORDER_DATE"; + + public Object mapLine(FieldSet fieldSet) { + Order order = new Order(); + order.setOrderId(fieldSet.readLong(ORDER_ID_COLUMN)); + order.setOrderDate(fieldSet.readDate(ORDER_DATE_COLUMN)); + + return order; + } +} diff --git a/samples/src/main/java/org/springframework/batch/sample/mapping/OrderItemFieldSetMapper.java b/samples/src/main/java/org/springframework/batch/sample/mapping/OrderItemFieldSetMapper.java new file mode 100644 index 000000000..15fc5ec04 --- /dev/null +++ b/samples/src/main/java/org/springframework/batch/sample/mapping/OrderItemFieldSetMapper.java @@ -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.sample.mapping; + +import org.springframework.batch.io.file.FieldSet; +import org.springframework.batch.io.file.FieldSetMapper; +import org.springframework.batch.sample.domain.LineItem; + + +public class OrderItemFieldSetMapper implements FieldSetMapper { + + public static final String TOTAL_PRICE_COLUMN = "TOTAL_PRICE"; + public static final String QUANTITY_COLUMN = "QUANTITY"; + public static final String HANDLING_PRICE_COLUMN = "HANDLING_PRICE"; + public static final String SHIPPING_PRICE_COLUMN = "SHIPPING_PRICE"; + public static final String DISCOUNT_AMOUNT_COLUMN = "DISCOUNT_AMOUNT"; + public static final String DISCOUNT_PERC_COLUMN = "DISCOUNT_PERC"; + public static final String PRICE_COLUMN = "PRICE"; + public static final String ITEM_ID_COLUMN = "ITEM_ID"; + + + public Object mapLine(FieldSet fieldSet) { + LineItem item = new LineItem(); + + item.setItemId(fieldSet.readLong(ITEM_ID_COLUMN)); + item.setPrice(fieldSet.readBigDecimal(PRICE_COLUMN)); + item.setDiscountPerc(fieldSet.readBigDecimal(DISCOUNT_PERC_COLUMN)); + item.setDiscountAmount(fieldSet.readBigDecimal(DISCOUNT_AMOUNT_COLUMN)); + item.setShippingPrice(fieldSet.readBigDecimal(SHIPPING_PRICE_COLUMN)); + item.setHandlingPrice(fieldSet.readBigDecimal(HANDLING_PRICE_COLUMN)); + item.setQuantity(fieldSet.readInt(QUANTITY_COLUMN)); + item.setTotalPrice(fieldSet.readBigDecimal(TOTAL_PRICE_COLUMN)); + + return item; + } +} diff --git a/samples/src/main/java/org/springframework/batch/sample/mapping/ShippingFieldSetMapper.java b/samples/src/main/java/org/springframework/batch/sample/mapping/ShippingFieldSetMapper.java new file mode 100644 index 000000000..23fdb6f1e --- /dev/null +++ b/samples/src/main/java/org/springframework/batch/sample/mapping/ShippingFieldSetMapper.java @@ -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.sample.mapping; + +import org.springframework.batch.io.file.FieldSet; +import org.springframework.batch.io.file.FieldSetMapper; +import org.springframework.batch.sample.domain.ShippingInfo; + + + +public class ShippingFieldSetMapper implements FieldSetMapper { + + public static final String ADDITIONAL_SHIPPING_INFO_COLUMN = "ADDITIONAL_SHIPPING_INFO"; + public static final String SHIPPING_TYPE_ID_COLUMN = "SHIPPING_TYPE_ID"; + public static final String SHIPPER_ID_COLUMN = "SHIPPER_ID"; + + public Object mapLine(FieldSet fieldSet) { + ShippingInfo info = new ShippingInfo(); + + info.setShipperId(fieldSet.readString(SHIPPER_ID_COLUMN)); + info.setShippingTypeId(fieldSet.readString(SHIPPING_TYPE_ID_COLUMN)); + info.setShippingInfo(fieldSet.readString(ADDITIONAL_SHIPPING_INFO_COLUMN)); + + return info; + } +} diff --git a/samples/src/main/java/org/springframework/batch/sample/mapping/TradeFieldSetMapper.java b/samples/src/main/java/org/springframework/batch/sample/mapping/TradeFieldSetMapper.java new file mode 100644 index 000000000..8fcaa0b9a --- /dev/null +++ b/samples/src/main/java/org/springframework/batch/sample/mapping/TradeFieldSetMapper.java @@ -0,0 +1,42 @@ +/* + * 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.sample.mapping; + +import org.springframework.batch.io.file.FieldSet; +import org.springframework.batch.io.file.FieldSetMapper; +import org.springframework.batch.sample.domain.Trade; + + + +public class TradeFieldSetMapper implements FieldSetMapper { + + public static final int ISIN_COLUMN = 0; + public static final int QUANTITY_COLUMN = 1; + public static final int PRICE_COLUMN = 2; + public static final int CUSTOMER_COLUMN = 3; + + public Object mapLine(FieldSet fieldSet) { + + Trade trade = new Trade(); + trade.setIsin(fieldSet.readString(0)); + trade.setQuantity(fieldSet.readLong(1)); + trade.setPrice(fieldSet.readBigDecimal(2)); + trade.setCustomer(fieldSet.readString(3)); + + return trade; + } +} diff --git a/samples/src/main/java/org/springframework/batch/sample/mapping/TradeRowMapper.java b/samples/src/main/java/org/springframework/batch/sample/mapping/TradeRowMapper.java new file mode 100644 index 000000000..588473614 --- /dev/null +++ b/samples/src/main/java/org/springframework/batch/sample/mapping/TradeRowMapper.java @@ -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.sample.mapping; + +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.springframework.batch.sample.domain.Trade; +import org.springframework.jdbc.core.RowMapper; + +public class TradeRowMapper implements RowMapper { + + public static final int ISIN_COLUMN = 1; + public static final int QUANTITY_COLUMN = 2; + public static final int PRICE_COLUMN = 3; + public static final int CUSTOMER_COLUMN = 4; + + public Object mapRow(ResultSet rs, int rowNum) throws SQLException { + Trade trade = new Trade(); + + trade.setIsin(rs.getString(ISIN_COLUMN)); + trade.setQuantity(rs.getLong(QUANTITY_COLUMN)); + trade.setPrice(rs.getBigDecimal(PRICE_COLUMN)); + trade.setCustomer(rs.getString(CUSTOMER_COLUMN)); + + return trade; + } + +} diff --git a/samples/src/main/java/org/springframework/batch/sample/module/CollectionItemProvider.java b/samples/src/main/java/org/springframework/batch/sample/module/CollectionItemProvider.java new file mode 100644 index 000000000..b24a70099 --- /dev/null +++ b/samples/src/main/java/org/springframework/batch/sample/module/CollectionItemProvider.java @@ -0,0 +1,101 @@ +/* + * 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.sample.module; + +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.file.FieldSet; +import org.springframework.batch.io.file.FieldSetInputSource; +import org.springframework.batch.io.file.FieldSetMapper; +import org.springframework.batch.item.provider.AbstractItemProvider; + + +public class CollectionItemProvider extends AbstractItemProvider { + + private static final Log log = LogFactory.getLog(CollectionItemProvider.class); + + private FieldSetInputSource inputSource; + + //collects simple records + private Collection multiRecord; + + //marks we have finished reading one whole multiRecord + private boolean recordFinished; + + //mapps a sigle line to a simple record + private FieldSetMapper fieldSetMapper; + + public Object next() { + recordFinished = false; + + while (!recordFinished) { + process(inputSource.readFieldSet()); + } + + if (multiRecord != null) { + Collection result = new ArrayList(multiRecord); + multiRecord = null; + + return result; + } else { + return null; + } + } + + private void process(FieldSet fieldSet) { + //finish processing if we hit the end of file + if (fieldSet == null) { + log.debug("FINISHED"); + recordFinished = true; + multiRecord = null; + + return; + } + + //start a new collection + if (fieldSet.readString(0).equals("BEGIN")) { + log.debug("STARTING NEW RECORD"); + multiRecord = new ArrayList(); + + return; + } + + //mark we are finished with current collection + if (fieldSet.readString(0).equals("END")) { + log.debug("END OF RECORD"); + recordFinished = true; + + return; + } + + //add a simple record to the current collection + log.debug("MAPPING: " + fieldSet); + multiRecord.add(fieldSetMapper.mapLine(fieldSet)); + } + + public void setInputSource(FieldSetInputSource inputTemplate) { + this.inputSource = inputTemplate; + } + + public void setFieldSetMapper(FieldSetMapper mapper) { + this.fieldSetMapper = mapper; + } + +} diff --git a/samples/src/main/java/org/springframework/batch/sample/module/ExceptionRestartableTasklet.java b/samples/src/main/java/org/springframework/batch/sample/module/ExceptionRestartableTasklet.java new file mode 100644 index 000000000..6496c2ac3 --- /dev/null +++ b/samples/src/main/java/org/springframework/batch/sample/module/ExceptionRestartableTasklet.java @@ -0,0 +1,62 @@ +/* + * 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.sample.module; + + +import org.springframework.batch.core.tasklet.Tasklet; +import org.springframework.batch.execution.tasklet.RestartableItemProviderTasklet; +import org.springframework.batch.io.exception.BatchCriticalException; + +/** + * Hacked {@link Tasklet} that throws exception on a given record number + * (useful for testing restart). + * + * @author Robert Kasanicky + * + */ +public class ExceptionRestartableTasklet extends RestartableItemProviderTasklet { + + private int counter = 0; + private int throwExceptionOnRecordNumber = 4; + + /* (non-Javadoc) + * @see Tasklet#execute() + */ + public boolean execute() throws Exception { + + counter++; + if (counter == throwExceptionOnRecordNumber) { + throw new BatchCriticalException(); + } + + return super.execute(); + } + + /** + * @param throwExceptionOnRecordNumber The number of record on which exception should be thrown + */ + public void setThrowExceptionOnRecordNumber(int throwExceptionOnRecordNumber) { + this.throwExceptionOnRecordNumber = throwExceptionOnRecordNumber; + } + + public int getThrowExceptionOnRecordNumber() { + return throwExceptionOnRecordNumber; + } + + + +} diff --git a/samples/src/main/java/org/springframework/batch/sample/module/InfiniteLoopTasklet.java b/samples/src/main/java/org/springframework/batch/sample/module/InfiniteLoopTasklet.java new file mode 100644 index 000000000..772ba1c00 --- /dev/null +++ b/samples/src/main/java/org/springframework/batch/sample/module/InfiniteLoopTasklet.java @@ -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.sample.module; + +import java.util.Properties; + +import org.springframework.batch.core.tasklet.Tasklet; +import org.springframework.batch.statistics.StatisticsProvider; +import org.springframework.batch.support.PropertiesConverter; + +/** + * Simple module implementation that will always return true to indicate + * that processing should continue. This is useful for testing graceful + * shutdown of jobs. + * + * @author Lucas Ward + * + */ +public class InfiniteLoopTasklet implements Tasklet, StatisticsProvider { + + private int count = 0; + + /** + * + */ + public InfiniteLoopTasklet() { + super(); + } + + public boolean execute() throws Exception { + Thread.sleep(500); + count++; + return true; + } + + /* (non-Javadoc) + * @see org.springframework.batch.statistics.StatisticsProvider#getStatistics() + */ + public Properties getStatistics() { + return PropertiesConverter.stringToProperties("count="+count); + } + +} diff --git a/samples/src/main/java/org/springframework/batch/sample/module/OrderItemProvider.java b/samples/src/main/java/org/springframework/batch/sample/module/OrderItemProvider.java new file mode 100644 index 000000000..08eabcfcf --- /dev/null +++ b/samples/src/main/java/org/springframework/batch/sample/module/OrderItemProvider.java @@ -0,0 +1,209 @@ +/* + * 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.sample.module; + +import java.util.ArrayList; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.batch.core.configuration.StepConfiguration; +import org.springframework.batch.core.runtime.StepExecutionContext; +import org.springframework.batch.io.file.FieldSet; +import org.springframework.batch.io.file.FieldSetInputSource; +import org.springframework.batch.io.file.FieldSetMapper; +import org.springframework.batch.item.provider.AbstractItemProvider; +import org.springframework.batch.item.validator.Validator; +import org.springframework.batch.sample.domain.Address; +import org.springframework.batch.sample.domain.BillingInfo; +import org.springframework.batch.sample.domain.Customer; +import org.springframework.batch.sample.domain.LineItem; +import org.springframework.batch.sample.domain.Order; +import org.springframework.batch.sample.domain.ShippingInfo; + + + +/** + * @author peter.zozom + * + */ +public class OrderItemProvider extends AbstractItemProvider { + private static Log log = LogFactory.getLog(OrderItemProvider.class); + private FieldSetInputSource inputSource; + private Order order; + private boolean recordFinished; + private FieldSetMapper headerMapper; + private FieldSetMapper customerMapper; + private FieldSetMapper addressMapper; + private FieldSetMapper billingMapper; + private FieldSetMapper itemMapper; + private FieldSetMapper shippingMapper; + private Validator validator; + + /** + * @see org.springframework.batch.item.ItemProvider#next() + */ + public Object next() { + recordFinished = false; + + while (!recordFinished) { + process(inputSource.readFieldSet()); + } + + if (order!=null) { + log.info("Mapped: "+order); + validator.validate(order); + } + + Object result = order; + order = null; + + return result; + } + + /** + * @see org.springframework.batch.execution.io.FieldSetCallback#process(StepConfiguration, StepExecutionContext) + */ + private void process(FieldSet fieldSet) { + //finish processing if we hit the end of file + if (fieldSet == null) { + log.debug("FINISHED"); + recordFinished = true; + order = null; + + return; + } + + String lineId = fieldSet.readString(0); + + //start a new Order + if (Order.LINE_ID_HEADER.equals(lineId)) { + log.debug("STARTING NEW RECORD"); + order = (Order) headerMapper.mapLine(fieldSet); + + return; + } + + //mark we are finished with current Order + if (Order.LINE_ID_FOOTER.equals(lineId)) { + log.debug("END OF RECORD"); + + //Do mapping for footer here, because mapper does not allow to pass an Order object as input. + //Mapper always creates new object + order.setTotalPrice(fieldSet.readBigDecimal("TOTAL_PRICE")); + order.setTotalLines(fieldSet.readInt("TOTAL_LINE_ITEMS")); + order.setTotalItems(fieldSet.readInt("TOTAL_ITEMS")); + + recordFinished = true; + + return; + } + + if (Customer.LINE_ID_BUSINESS_CUST.equals(lineId)) { + log.debug("MAPPING CUSTOMER"); + + if (order.getCustomer() == null) { + order.setCustomer((Customer) customerMapper.mapLine(fieldSet)); + order.getCustomer().setBusinessCustomer(true); + } + + return; + } + + if (Customer.LINE_ID_NON_BUSINESS_CUST.equals(lineId)) { + log.debug("MAPPING CUSTOMER"); + + if (order.getCustomer() == null) { + order.setCustomer((Customer) customerMapper.mapLine(fieldSet)); + order.getCustomer().setBusinessCustomer(false); + } + + return; + } + + if (Address.LINE_ID_BILLING_ADDR.equals(lineId)) { + log.debug("MAPPING BILLING ADDRESS"); + order.setBillingAddress((Address) addressMapper.mapLine(fieldSet)); + return; + } + + if (Address.LINE_ID_SHIPPING_ADDR.equals(lineId)) { + log.debug("MAPPING SHIPPING ADDRESS"); + order.setShippingAddress((Address) addressMapper.mapLine(fieldSet)); + return; + } + + if (BillingInfo.LINE_ID_BILLING_INFO.equals(lineId)) { + log.debug("MAPPING BILLING INFO"); + order.setBilling((BillingInfo) billingMapper.mapLine(fieldSet)); + return; + } + + if (ShippingInfo.LINE_ID_SHIPPING_INFO.equals(lineId)) { + log.debug("MAPPING SHIPPING INFO"); + order.setShipping((ShippingInfo) shippingMapper.mapLine(fieldSet)); + return; + } + + if (LineItem.LINE_ID_ITEM.equals(lineId)) { + log.debug("MAPPING LINE ITEM"); + + if (order.getLineItems() == null) { + order.setLineItems(new ArrayList()); + } + + order.getLineItems().add(itemMapper.mapLine(fieldSet)); + + return; + } + + log.debug("Could not map LINE_ID="+lineId); + + } + + public void setAddressMapper(FieldSetMapper addressMapper) { + this.addressMapper = addressMapper; + } + + public void setBillingMapper(FieldSetMapper billingMapper) { + this.billingMapper = billingMapper; + } + + public void setCustomerMapper(FieldSetMapper customerMapper) { + this.customerMapper = customerMapper; + } + + public void setHeaderMapper(FieldSetMapper headerMapper) { + this.headerMapper = headerMapper; + } + + public void setInputSource(FieldSetInputSource inputTemplate) { + this.inputSource = inputTemplate; + } + + public void setItemMapper(FieldSetMapper itemMapper) { + this.itemMapper = itemMapper; + } + + public void setShippingMapper(FieldSetMapper shippingMapper) { + this.shippingMapper = shippingMapper; + } + + public void setValidator(Validator validator) { + this.validator = validator; + } + +} diff --git a/samples/src/main/java/org/springframework/batch/sample/module/SimpleTradeTasklet.java b/samples/src/main/java/org/springframework/batch/sample/module/SimpleTradeTasklet.java new file mode 100644 index 000000000..51070b6f5 --- /dev/null +++ b/samples/src/main/java/org/springframework/batch/sample/module/SimpleTradeTasklet.java @@ -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.sample.module; + +import java.util.Properties; + +import org.springframework.batch.execution.tasklet.ReadProcessTasklet; +import org.springframework.batch.io.file.FieldSet; +import org.springframework.batch.io.file.FieldSetMapper; +import org.springframework.batch.io.file.support.DefaultFlatFileInputSource; +import org.springframework.batch.sample.dao.TradeWriter; +import org.springframework.batch.sample.domain.Trade; +import org.springframework.batch.statistics.StatisticsProvider; + +/** + * Simple implementation of a {@link ReadProcessTasklet}, which illustrates the + * case when reading and processing of input is not separated. This can be + * viable in cases, when the input reading and processing logic need not to be + * reused in different contexts. In general it is recommended to separate these + * two concerns. + * + * Note this class is NOT thread-safe, contrast to 'standard' module + * implementations provided by the framework. + * + * @author Robert Kasanicky + * @author Lucas Ward + */ +public class SimpleTradeTasklet extends ReadProcessTasklet implements StatisticsProvider { + /** + * reads the data from input file + */ + private DefaultFlatFileInputSource inputSource; + + /** + * maps a line to a Trade object + */ + private FieldSetMapper tradeFieldSetMapper = new TradeFieldSetMapper(); + + /** + * writes a Trade object to output + */ + private TradeWriter tradeWriter; + + /** + * domain object being processed + */ + private Trade trade; + + /** + * number of trade objects processed + */ + private int tradeCount = 0; + + /** + * Read method, all reading from any input source(s) should be done here. + * The input template is read using the readAndMap method, which accepts a + * FieldSetMapper. This call returns an object (which should be a Trade + * value object) then will be stored in a class-level variable for use by + * the process method. + */ + public boolean read() { + trade = (Trade) tradeFieldSetMapper.mapLine(inputSource.readFieldSet()); + + if (trade == null) { + // no Trade object returned, reading input is finished + return false; + } + + tradeCount++; + return true; + } + + /** + * Process the data obtained during the read() method. Because this is a + * simple example job, the data is simply written out without any + * processing. + */ + public void process() { + tradeWriter.writeTrade(trade); + } + + /** + * Inner class which implements the FieldSetMapper interface. It contains + * one method, mapLine, which accepts a FieldSet as a parameter. This method + * will be called by the inputSource when it is passed in. + * + */ + private static class TradeFieldSetMapper implements FieldSetMapper { + public Object mapLine(FieldSet fieldSet) { + + if (fieldSet == null) { + return null; + } + + Trade trade = new Trade(); + trade.setIsin(fieldSet.readString("ISIN")); + trade.setQuantity(fieldSet.readLong(1)); + trade.setPrice(fieldSet.readBigDecimal(2)); + trade.setCustomer(fieldSet.readString(3)); + + return trade; + + } + } + + public void setInputSource(DefaultFlatFileInputSource inputTemplate) { + this.inputSource = inputTemplate; + } + + public void setTradeDao(TradeWriter tradeWriter) { + this.tradeWriter = tradeWriter; + } + + public Properties getStatistics() { + Properties statistics = new Properties(); + statistics.setProperty("Trade.Count", String.valueOf(tradeCount)); + statistics.putAll(inputSource.getStatistics()); + return statistics; + } + +} diff --git a/samples/src/main/java/org/springframework/batch/sample/module/SkipSampleItemProvider.java b/samples/src/main/java/org/springframework/batch/sample/module/SkipSampleItemProvider.java new file mode 100644 index 000000000..b1808b4c9 --- /dev/null +++ b/samples/src/main/java/org/springframework/batch/sample/module/SkipSampleItemProvider.java @@ -0,0 +1,66 @@ +/* + * 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.sample.module; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.batch.io.exception.TransactionInvalidException; +import org.springframework.batch.io.file.FieldSetInputSource; +import org.springframework.batch.io.file.FieldSetMapper; +import org.springframework.batch.item.provider.AbstractItemProvider; + +/** + * @author peter.zozom + * + */ +public class SkipSampleItemProvider extends AbstractItemProvider { + + private static Log log = LogFactory.getLog(SkipSampleItemProvider.class); + + private int counter = 0; + + private int exceptionOnrecordNumber = 14; + + private FieldSetInputSource inputSource; + + private FieldSetMapper fieldSetMapper; + + public Object next() { + counter++; + + if (counter == exceptionOnrecordNumber) { + // this causes rollback of current transaction + log.debug("Throwing TransactionInvalidException to cause transaction rollback..."); + throw new TransactionInvalidException("Error processing line: " + counter + ". Rollbacking..."); + } + + return fieldSetMapper.mapLine(inputSource.readFieldSet()); + } + + public void setInputSource(FieldSetInputSource inputTemplate) { + this.inputSource = inputTemplate; + } + + public void setFieldSetMapper(FieldSetMapper fieldSetMapper) { + this.fieldSetMapper = fieldSetMapper; + } + + public void setThrowExceptionOnRecordNumber(int exceptionOnrecordNumber) { + this.exceptionOnrecordNumber = exceptionOnrecordNumber; + } + +} diff --git a/samples/src/main/java/org/springframework/batch/sample/module/process/CustomerCreditUpdateProcessor.java b/samples/src/main/java/org/springframework/batch/sample/module/process/CustomerCreditUpdateProcessor.java new file mode 100644 index 000000000..9916b1578 --- /dev/null +++ b/samples/src/main/java/org/springframework/batch/sample/module/process/CustomerCreditUpdateProcessor.java @@ -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.sample.module.process; + +import org.springframework.batch.item.ItemProcessor; +import org.springframework.batch.sample.dao.CustomerCreditWriter; +import org.springframework.batch.sample.domain.CustomerCredit; + + + +public class CustomerCreditUpdateProcessor implements ItemProcessor { + private double creditFilter = 800; + private CustomerCreditWriter writer; + + public void process(Object data) { + CustomerCredit customerCredit = (CustomerCredit) data; + + if (customerCredit.getCredit().doubleValue() > creditFilter) { + writer.write(customerCredit); + } + } + + public void setCreditFilter(double creditFilter) { + this.creditFilter = creditFilter; + } + + public void setWriter(CustomerCreditWriter writer) { + this.writer = writer; + } + +} diff --git a/samples/src/main/java/org/springframework/batch/sample/module/process/CustomerUpdateProcessor.java b/samples/src/main/java/org/springframework/batch/sample/module/process/CustomerUpdateProcessor.java new file mode 100644 index 000000000..b25795ebe --- /dev/null +++ b/samples/src/main/java/org/springframework/batch/sample/module/process/CustomerUpdateProcessor.java @@ -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.sample.module.process; + +import org.springframework.batch.item.ItemProcessor; +import org.springframework.batch.sample.dao.JdbcCustomerDebitWriter; +import org.springframework.batch.sample.domain.CustomerDebit; +import org.springframework.batch.sample.domain.Trade; + + +/** + * Transforms Trade to a CustomerDebit and asks dao object to write the result. + * + * @author Robert Kasanicky + */ +public class CustomerUpdateProcessor implements ItemProcessor { + private JdbcCustomerDebitWriter dao; + + public void process(Object data) { + Trade trade = (Trade) data; + CustomerDebit customerDebit = new CustomerDebit(); + customerDebit.setName(trade.getCustomer()); + customerDebit.setDebit(trade.getPrice()); + dao.write(customerDebit); + } + + public void setDao(JdbcCustomerDebitWriter outputSource) { + this.dao = outputSource; + } + + public void close() { + } + + public void init() { + } +} diff --git a/samples/src/main/java/org/springframework/batch/sample/module/process/DefaultFlatFileProcessor.java b/samples/src/main/java/org/springframework/batch/sample/module/process/DefaultFlatFileProcessor.java new file mode 100644 index 000000000..ff50b7d68 --- /dev/null +++ b/samples/src/main/java/org/springframework/batch/sample/module/process/DefaultFlatFileProcessor.java @@ -0,0 +1,18 @@ +package org.springframework.batch.sample.module.process; + +import org.springframework.batch.io.file.support.FlatFileOutputSource; +import org.springframework.batch.item.ItemProcessor; + +public class DefaultFlatFileProcessor implements ItemProcessor{ + + private FlatFileOutputSource flatFileOutputSource; + + public void process(Object data) throws Exception { + flatFileOutputSource.write(""+data); + } + + public void setFlatFileOutputSource(FlatFileOutputSource flatFileOutputSource) { + this.flatFileOutputSource = flatFileOutputSource; + } + +} diff --git a/samples/src/main/java/org/springframework/batch/sample/module/process/DummyProcessor.java b/samples/src/main/java/org/springframework/batch/sample/module/process/DummyProcessor.java new file mode 100644 index 000000000..7a1c9ac81 --- /dev/null +++ b/samples/src/main/java/org/springframework/batch/sample/module/process/DummyProcessor.java @@ -0,0 +1,41 @@ +/* + * 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.sample.module.process; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.batch.item.ItemProcessor; + +/** + * Dummy processor useful for development and testing. + * + * @author Robert Kasanicky + */ +public class DummyProcessor implements ItemProcessor { + + private static final Log log = LogFactory.getLog(DummyProcessor.class); + + public void process(Object object) { + log.debug("PROCESSING: " + object); + } + + public void close() { + } + + public void init() { + } +} diff --git a/samples/src/main/java/org/springframework/batch/sample/module/process/OrderProcessor.java b/samples/src/main/java/org/springframework/batch/sample/module/process/OrderProcessor.java new file mode 100644 index 000000000..e6153f7eb --- /dev/null +++ b/samples/src/main/java/org/springframework/batch/sample/module/process/OrderProcessor.java @@ -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.sample.module.process; + +import org.springframework.batch.io.exception.BatchCriticalException; +import org.springframework.batch.item.ItemProcessor; +import org.springframework.batch.sample.dao.OrderWriter; +import org.springframework.batch.sample.domain.Order; + + + +public class OrderProcessor implements ItemProcessor { + private OrderWriter writer; + private Order order; + + public void process(Object data) { + if (!(data instanceof Order)) { + throw new BatchCriticalException("OrderProcessor can process only Order objects"); + } + + order = (Order) data; + writer.write(order); + } + + public void setWriter(OrderWriter reportService) { + this.writer = reportService; + } + +} diff --git a/samples/src/main/java/org/springframework/batch/sample/module/process/PersonProcessor.java b/samples/src/main/java/org/springframework/batch/sample/module/process/PersonProcessor.java new file mode 100644 index 000000000..db65f0f8f --- /dev/null +++ b/samples/src/main/java/org/springframework/batch/sample/module/process/PersonProcessor.java @@ -0,0 +1,44 @@ +/* + * 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.sample.module.process; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.batch.item.ItemProcessor; +import org.springframework.batch.sample.domain.Person; + + + +public class PersonProcessor implements ItemProcessor { + private static Log log = LogFactory.getLog(PersonProcessor.class); + + public void process(Object data) { + if (!(data instanceof Person)) { + log.warn("PersonProcessor can process only Person objects, skipping record"); + + return; + } + + log.debug("Processing: " + data); + } + + public void close() { + } + + public void init() { + } +} diff --git a/samples/src/main/java/org/springframework/batch/sample/module/process/TradeProcessor.java b/samples/src/main/java/org/springframework/batch/sample/module/process/TradeProcessor.java new file mode 100644 index 000000000..bc34b078f --- /dev/null +++ b/samples/src/main/java/org/springframework/batch/sample/module/process/TradeProcessor.java @@ -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.sample.module.process; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.batch.item.ItemProcessor; +import org.springframework.batch.sample.dao.TradeWriter; +import org.springframework.batch.sample.domain.Trade; + + + +public class TradeProcessor implements ItemProcessor { + private static Log log = LogFactory.getLog(TradeProcessor.class); + private TradeWriter writer; + + public void process(Object data) { + if (!(data instanceof Trade)) { + log.warn("TradeProcessor can process only Trade objects, skipping record"); + + return; + } + + Trade trade = (Trade) data; + log.debug(data); + + //TODO put some processing of the trade object here + writer.writeTrade(trade); + } + + public void setWriter(TradeWriter dao) { + this.writer = dao; + } + + public void close() { + } + + public void init() { + } +} diff --git a/samples/src/main/java/org/springframework/batch/sample/validation/valang/custom/FutureDateFunction.java b/samples/src/main/java/org/springframework/batch/sample/validation/valang/custom/FutureDateFunction.java new file mode 100644 index 000000000..573d0f8b8 --- /dev/null +++ b/samples/src/main/java/org/springframework/batch/sample/validation/valang/custom/FutureDateFunction.java @@ -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.sample.validation.valang.custom; + +import java.util.Date; + +import org.springmodules.validation.valang.functions.AbstractFunction; +import org.springmodules.validation.valang.functions.Function; + + +/** + * Returns Boolean.TRUE if given value is future date, else it returns Boolean.FALSE + * @author peter.zozom + */ +public class FutureDateFunction extends AbstractFunction { + /** + * @param arguments + * @param line + * @param column + */ + public FutureDateFunction(Function[] arguments, int line, int column) { + super(arguments, line, column); + definedExactNumberOfArguments(1); + } + + /** + * @see org.springmodules.validation.valang.functions.AbstractFunction#doGetResult(java.lang.Object) + */ + protected Object doGetResult(final Object target) throws Exception { + //get argument + final Object value = getArguments()[0].getResult(target); + + Boolean result = Boolean.FALSE; + + if (value instanceof Date) { + final Date now = new Date(System.currentTimeMillis()); + final Date date = (Date) value; + result = (now.compareTo(date) < 0) ? Boolean.TRUE : Boolean.FALSE; + } else { + throw new Exception("No Date value for validation"); + } + + return result; + } +} diff --git a/samples/src/main/java/org/springframework/batch/sample/validation/valang/custom/TotalOrderItemsFunction.java b/samples/src/main/java/org/springframework/batch/sample/validation/valang/custom/TotalOrderItemsFunction.java new file mode 100644 index 000000000..a98091066 --- /dev/null +++ b/samples/src/main/java/org/springframework/batch/sample/validation/valang/custom/TotalOrderItemsFunction.java @@ -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.sample.validation.valang.custom; + +import java.util.Iterator; +import java.util.List; + +import org.springframework.batch.sample.domain.LineItem; +import org.springmodules.validation.valang.functions.AbstractFunction; +import org.springmodules.validation.valang.functions.Function; + + +/** + * Validates total items count in Order. + * + * @author peter.zozom + */ +public class TotalOrderItemsFunction extends AbstractFunction { + public TotalOrderItemsFunction(Function[] arguments, int line, int column) { + super(arguments, line, column); + definedExactNumberOfArguments(2); + } + + /** + * @see org.springmodules.validation.valang.functions.AbstractFunction#doGetResult(java.lang.Object) + */ + protected Object doGetResult(Object target) throws Exception { + //get arguments + int count = ((Integer) getArguments()[0].getResult(target)).intValue(); + Object value = getArguments()[1].getResult(target); + + Boolean result; + + //count items in list of order lines + if (value instanceof List) { + int totalItems = 0; + + for (Iterator i = ((List) value).iterator(); i.hasNext();) { + LineItem item = (LineItem) i.next(); + totalItems += item.getQuantity(); + } + + result = (totalItems == count) ? Boolean.TRUE : Boolean.FALSE; + } else { + throw new Exception("No list for validation"); + } + + return result; + } +} diff --git a/samples/src/main/java/org/springframework/batch/sample/validation/valang/custom/ValidateDiscountsFunction.java b/samples/src/main/java/org/springframework/batch/sample/validation/valang/custom/ValidateDiscountsFunction.java new file mode 100644 index 000000000..b369c9e13 --- /dev/null +++ b/samples/src/main/java/org/springframework/batch/sample/validation/valang/custom/ValidateDiscountsFunction.java @@ -0,0 +1,69 @@ +/* + * 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.sample.validation.valang.custom; + +import java.math.BigDecimal; +import java.util.Iterator; +import java.util.List; + +import org.springframework.batch.sample.domain.LineItem; +import org.springmodules.validation.valang.functions.AbstractFunction; +import org.springmodules.validation.valang.functions.Function; + + +/** + * @author peter.zozom + * + */ +public class ValidateDiscountsFunction extends AbstractFunction { + private static final BigDecimal BD_0 = new BigDecimal(0.0); + private static final BigDecimal BD_PERC_MAX = new BigDecimal(100.0); + + public ValidateDiscountsFunction(Function[] arguments, int line, int column) { + super(arguments, line, column); + definedExactNumberOfArguments(1); + } + + /** + * @see org.springmodules.validation.valang.functions.AbstractFunction#doGetResult(java.lang.Object) + */ + protected Object doGetResult(Object target) throws Exception { + List lineItems = (List) getArguments()[0].getResult(target); + + for (Iterator i = lineItems.iterator(); i.hasNext();) { + LineItem item = (LineItem) i.next(); + + if (BD_0.compareTo(item.getDiscountPerc()) != 0) { + //DiscountPerc must be between 0.0 and 100.0 + if ((BD_0.compareTo(item.getDiscountPerc()) > 0) + || (BD_PERC_MAX.compareTo(item.getDiscountPerc()) < 0) + || (BD_0.compareTo(item.getDiscountAmount()) != 0)) { //only one of DiscountAmount and DiscountPerc should be non-zero + + return Boolean.FALSE; + } + } else { + //DiscountAmount must be between 0.0 and item.price + if ((BD_0.compareTo(item.getDiscountAmount()) > 0) + || (item.getPrice().compareTo(item.getDiscountAmount()) < 0)) { + return Boolean.FALSE; + } + } + } + + return Boolean.TRUE; + } +} diff --git a/samples/src/main/java/org/springframework/batch/sample/validation/valang/custom/ValidateHandlingPricesFunction.java b/samples/src/main/java/org/springframework/batch/sample/validation/valang/custom/ValidateHandlingPricesFunction.java new file mode 100644 index 000000000..c4709bc7e --- /dev/null +++ b/samples/src/main/java/org/springframework/batch/sample/validation/valang/custom/ValidateHandlingPricesFunction.java @@ -0,0 +1,58 @@ +/* + * 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.sample.validation.valang.custom; + +import java.math.BigDecimal; +import java.util.Iterator; +import java.util.List; + +import org.springframework.batch.sample.domain.LineItem; +import org.springmodules.validation.valang.functions.AbstractFunction; +import org.springmodules.validation.valang.functions.Function; + + +/** + * @author peter.zozom + * + */ +public class ValidateHandlingPricesFunction extends AbstractFunction { + private static final BigDecimal BD_MIN = new BigDecimal(0.0); + private static final BigDecimal BD_MAX = new BigDecimal(99999999.99); + + public ValidateHandlingPricesFunction(Function[] arguments, int line, int column) { + super(arguments, line, column); + definedExactNumberOfArguments(1); + } + + /** + * @see org.springmodules.validation.valang.functions.AbstractFunction#doGetResult(java.lang.Object) + */ + protected Object doGetResult(Object target) throws Exception { + List lineItems = (List) getArguments()[0].getResult(target); + + for (Iterator i = lineItems.iterator(); i.hasNext();) { + LineItem item = (LineItem) i.next(); + + if ((BD_MIN.compareTo(item.getHandlingPrice()) > 0) + || (BD_MAX.compareTo(item.getHandlingPrice()) < 0)) { + return Boolean.FALSE; + } + } + + return Boolean.TRUE; + } +} diff --git a/samples/src/main/java/org/springframework/batch/sample/validation/valang/custom/ValidateIdsFunction.java b/samples/src/main/java/org/springframework/batch/sample/validation/valang/custom/ValidateIdsFunction.java new file mode 100644 index 000000000..4344fc412 --- /dev/null +++ b/samples/src/main/java/org/springframework/batch/sample/validation/valang/custom/ValidateIdsFunction.java @@ -0,0 +1,55 @@ +/* + * 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.sample.validation.valang.custom; + +import java.util.Iterator; +import java.util.List; + +import org.springframework.batch.sample.domain.LineItem; +import org.springmodules.validation.valang.functions.AbstractFunction; +import org.springmodules.validation.valang.functions.Function; + + +/** + * @author peter.zozom + * + */ +public class ValidateIdsFunction extends AbstractFunction { + private static final long MAX_ID = 9999999999L; + + public ValidateIdsFunction(Function[] arguments, int line, int column) { + super(arguments, line, column); + definedExactNumberOfArguments(1); + } + + /** + * @see org.springmodules.validation.valang.functions.AbstractFunction#doGetResult(java.lang.Object) + */ + protected Object doGetResult(Object target) throws Exception { + List lineItems = (List) getArguments()[0].getResult(target); + + for (Iterator i = lineItems.iterator(); i.hasNext();) { + LineItem item = (LineItem) i.next(); + + if ((item.getItemId() <= 0) || (item.getItemId() > MAX_ID)) { + return Boolean.FALSE; + } + } + + return Boolean.TRUE; + } +} diff --git a/samples/src/main/java/org/springframework/batch/sample/validation/valang/custom/ValidatePricesFunction.java b/samples/src/main/java/org/springframework/batch/sample/validation/valang/custom/ValidatePricesFunction.java new file mode 100644 index 000000000..d9283fe02 --- /dev/null +++ b/samples/src/main/java/org/springframework/batch/sample/validation/valang/custom/ValidatePricesFunction.java @@ -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.sample.validation.valang.custom; + +import java.math.BigDecimal; +import java.util.Iterator; +import java.util.List; + +import org.springframework.batch.sample.domain.LineItem; +import org.springmodules.validation.valang.functions.AbstractFunction; +import org.springmodules.validation.valang.functions.Function; + + +/** + * @author peter.zozom + * + */ +public class ValidatePricesFunction extends AbstractFunction { + private static final BigDecimal BD_MIN = new BigDecimal(0.0); + private static final BigDecimal BD_MAX = new BigDecimal(99999999.99); + + public ValidatePricesFunction(Function[] arguments, int line, int column) { + super(arguments, line, column); + definedExactNumberOfArguments(1); + } + + /** + * @see org.springmodules.validation.valang.functions.AbstractFunction#doGetResult(java.lang.Object) + */ + protected Object doGetResult(Object target) throws Exception { + List lineItems = (List) getArguments()[0].getResult(target); + + for (Iterator i = lineItems.iterator(); i.hasNext();) { + LineItem item = (LineItem) i.next(); + + if ((BD_MIN.compareTo(item.getPrice()) > 0) || (BD_MAX.compareTo(item.getPrice()) < 0)) { + return Boolean.FALSE; + } + } + + return Boolean.TRUE; + } +} diff --git a/samples/src/main/java/org/springframework/batch/sample/validation/valang/custom/ValidateQuantitiesFunction.java b/samples/src/main/java/org/springframework/batch/sample/validation/valang/custom/ValidateQuantitiesFunction.java new file mode 100644 index 000000000..587b2b004 --- /dev/null +++ b/samples/src/main/java/org/springframework/batch/sample/validation/valang/custom/ValidateQuantitiesFunction.java @@ -0,0 +1,55 @@ +/* + * 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.sample.validation.valang.custom; + +import java.util.Iterator; +import java.util.List; + +import org.springframework.batch.sample.domain.LineItem; +import org.springmodules.validation.valang.functions.AbstractFunction; +import org.springmodules.validation.valang.functions.Function; + + +/** + * @author peter.zozom + * + */ +public class ValidateQuantitiesFunction extends AbstractFunction { + private static final int MAX_QUANTITY = 9999; + + public ValidateQuantitiesFunction(Function[] arguments, int line, int column) { + super(arguments, line, column); + definedExactNumberOfArguments(1); + } + + /** + * @see org.springmodules.validation.valang.functions.AbstractFunction#doGetResult(java.lang.Object) + */ + protected Object doGetResult(Object target) throws Exception { + List lineItems = (List) getArguments()[0].getResult(target); + + for (Iterator i = lineItems.iterator(); i.hasNext();) { + LineItem item = (LineItem) i.next(); + + if ((item.getQuantity() <= 0) || (item.getQuantity() > MAX_QUANTITY)) { + return Boolean.FALSE; + } + } + + return Boolean.TRUE; + } +} diff --git a/samples/src/main/java/org/springframework/batch/sample/validation/valang/custom/ValidateShippingPricesFunction.java b/samples/src/main/java/org/springframework/batch/sample/validation/valang/custom/ValidateShippingPricesFunction.java new file mode 100644 index 000000000..7b4299847 --- /dev/null +++ b/samples/src/main/java/org/springframework/batch/sample/validation/valang/custom/ValidateShippingPricesFunction.java @@ -0,0 +1,58 @@ +/* + * 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.sample.validation.valang.custom; + +import java.math.BigDecimal; +import java.util.Iterator; +import java.util.List; + +import org.springframework.batch.sample.domain.LineItem; +import org.springmodules.validation.valang.functions.AbstractFunction; +import org.springmodules.validation.valang.functions.Function; + + +/** + * @author peter.zozom + * + */ +public class ValidateShippingPricesFunction extends AbstractFunction { + private static final BigDecimal BD_MIN = new BigDecimal(0.0); + private static final BigDecimal BD_MAX = new BigDecimal(99999999.99); + + public ValidateShippingPricesFunction(Function[] arguments, int line, int column) { + super(arguments, line, column); + definedExactNumberOfArguments(1); + } + + /** + * @see org.springmodules.validation.valang.functions.AbstractFunction#doGetResult(java.lang.Object) + */ + protected Object doGetResult(Object target) throws Exception { + List lineItems = (List) getArguments()[0].getResult(target); + + for (Iterator i = lineItems.iterator(); i.hasNext();) { + LineItem item = (LineItem) i.next(); + + if ((BD_MIN.compareTo(item.getShippingPrice()) > 0) + || (BD_MAX.compareTo(item.getShippingPrice()) < 0)) { + return Boolean.FALSE; + } + } + + return Boolean.TRUE; + } +} diff --git a/samples/src/main/java/org/springframework/batch/sample/validation/valang/custom/ValidateTotalPricesFunction.java b/samples/src/main/java/org/springframework/batch/sample/validation/valang/custom/ValidateTotalPricesFunction.java new file mode 100644 index 000000000..57af7ad69 --- /dev/null +++ b/samples/src/main/java/org/springframework/batch/sample/validation/valang/custom/ValidateTotalPricesFunction.java @@ -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.sample.validation.valang.custom; + +import java.math.BigDecimal; +import java.util.Iterator; +import java.util.List; + +import org.springframework.batch.sample.domain.LineItem; +import org.springmodules.validation.valang.functions.AbstractFunction; +import org.springmodules.validation.valang.functions.Function; + + +/** + * @author peter.zozom + * + */ +public class ValidateTotalPricesFunction extends AbstractFunction { + private static final BigDecimal BD_MIN = new BigDecimal(0.0); + private static final BigDecimal BD_MAX = new BigDecimal(99999999.99); + private static final BigDecimal BD_100 = new BigDecimal(100.00); + + public ValidateTotalPricesFunction(Function[] arguments, int line, int column) { + super(arguments, line, column); + definedExactNumberOfArguments(1); + } + + /** + * @see org.springmodules.validation.valang.functions.AbstractFunction#doGetResult(java.lang.Object) + */ + protected Object doGetResult(Object target) throws Exception { + List lineItems = (List) getArguments()[0].getResult(target); + + for (Iterator i = lineItems.iterator(); i.hasNext();) { + LineItem item = (LineItem) i.next(); + + if ((BD_MIN.compareTo(item.getTotalPrice()) > 0) + || (BD_MAX.compareTo(item.getTotalPrice()) < 0)) { + return Boolean.FALSE; + } + + //calculate total price + + //discount coeficient = (100.00 - discountPerc) / 100.00 + BigDecimal coef = BD_100.subtract(item.getDiscountPerc()) + .divide(BD_100, 4, BigDecimal.ROUND_HALF_UP); + + //discountedPrice = (price * coef) - discountAmount + //at least one of discountPerc and discountAmount is 0 - this is validated by ValidateDiscountsFunction + BigDecimal discountedPrice = item.getPrice().multiply(coef) + .subtract(item.getDiscountAmount()); + + //price for single item = discountedPrice + shipping + handling + BigDecimal singleItemPrice = discountedPrice.add(item.getShippingPrice()) + .add(item.getHandlingPrice()); + + //total price = singleItemPrice * quantity + BigDecimal quantity = new BigDecimal(item.getQuantity()); + BigDecimal totalPrice = singleItemPrice.multiply(quantity) + .setScale(2, BigDecimal.ROUND_HALF_UP); + + //calculatedPrice should equal to item.totalPrice + if (totalPrice.compareTo(item.getTotalPrice()) != 0) { + return Boolean.FALSE; + } + } + + return Boolean.TRUE; + } +} diff --git a/samples/src/main/resources/batch.properties b/samples/src/main/resources/batch.properties new file mode 100644 index 000000000..a59281d72 --- /dev/null +++ b/samples/src/main/resources/batch.properties @@ -0,0 +1,23 @@ +# Placeholders batch.* +# for HSQLDB: +batch.jdbc.driver=org.hsqldb.jdbcDriver +batch.jdbc.url=jdbc:hsqldb:mem:testdb +# use this one for a separate server process (so you can inspect the results) +# batch.jdbc.url=jdbc:hsqldb:hsql://localhost:9005/samples +batch.jdbc.user=sa +batch.jdbc.password= +batch.schema= +batch.jndi.name= +batch.naming.factory.initial= +batch.naming.provider.url= +batch.database.vendor=HSQLDB +batch.database.incrementer.class=org.springframework.jdbc.support.incrementer.HsqlMaxValueIncrementer + +# Other platforms: +# org.springframework.jdbc.support.incrementer.DB2SequenceMaxValueIncrementer +# org.springframework.jdbc.support.incrementer.PostgreSQLSequenceMaxValueIncrementer + +# Bean Properties for override +# for HSQLDB: +incrementerParent.columnName=ID + diff --git a/samples/src/main/resources/beanRefContext.xml b/samples/src/main/resources/beanRefContext.xml new file mode 100644 index 000000000..1e8b9b7a5 --- /dev/null +++ b/samples/src/main/resources/beanRefContext.xml @@ -0,0 +1,11 @@ + + + + + + simple-container-definition.xml + + + + \ No newline at end of file diff --git a/samples/src/main/resources/business-schema-hsqldb.sql b/samples/src/main/resources/business-schema-hsqldb.sql new file mode 100644 index 000000000..70b690c9e --- /dev/null +++ b/samples/src/main/resources/business-schema-hsqldb.sql @@ -0,0 +1,36 @@ +DROP TABLE TRADE IF EXISTS; +DROP TABLE TRADE_SEQ IF EXISTS; +DROP TABLE CUSTOMER IF EXISTS; +DROP TABLE CUSTOMER_SEQ IF EXISTS; + +CREATE TABLE TRADE ( + ID BIGINT PRIMARY KEY, + VERSION BIGINT, + ISIN VARCHAR(45) NOT NULL, + QUANTITY BIGINT, + PRICE FLOAT, + CUSTOMER VARCHAR(45) +); + + +CREATE TABLE TRADE_SEQ ( + ID BIGINT IDENTITY +); + +CREATE TABLE CUSTOMER ( + ID INTEGER PRIMARY KEY, + VERSION BIGINT, + NAME VARCHAR(45), + CREDIT FLOAT +); + +CREATE TABLE CUSTOMER_SEQ ( + ID BIGINT IDENTITY +); + +INSERT INTO customer (id, version, name, credit) VALUES (1, 0, 'customer1', 100000); +INSERT INTO customer (id, version, name, credit) VALUES (2, 0, 'customer2', 100000); +INSERT INTO customer (id, version, name, credit) VALUES (3, 0, 'customer3', 100000); +INSERT INTO customer (id, version, name, credit) VALUES (4, 0, 'customer4', 100000); + + \ No newline at end of file diff --git a/samples/src/main/resources/data-source-context-init.xml b/samples/src/main/resources/data-source-context-init.xml new file mode 100644 index 000000000..aabd28239 --- /dev/null +++ b/samples/src/main/resources/data-source-context-init.xml @@ -0,0 +1,16 @@ + + + + + + + + schema-hsqldb.sql + business-schema-hsqldb.sql + + + + + \ No newline at end of file diff --git a/samples/src/main/resources/data-source-context.xml b/samples/src/main/resources/data-source-context.xml new file mode 100644 index 000000000..710cbe6bc --- /dev/null +++ b/samples/src/main/resources/data-source-context.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/src/main/resources/data/beanWrapperMapperSampleJob/input/20070122.teststream.ImportPersonDataStep.txt b/samples/src/main/resources/data/beanWrapperMapperSampleJob/input/20070122.teststream.ImportPersonDataStep.txt new file mode 100644 index 000000000..f3d669d54 --- /dev/null +++ b/samples/src/main/resources/data/beanWrapperMapperSampleJob/input/20070122.teststream.ImportPersonDataStep.txt @@ -0,0 +1,3 @@ +123451234567890123451234567890123456789012345123456789012345678901234567890 +Mr Tomas Slanina 29 BratislavaPeter Charles +Mr George W. Bush 31 Washington \ No newline at end of file diff --git a/samples/src/main/resources/data/beanWrapperMapperSampleJob/input/20070122.teststream.ImportTradeDataStep.txt b/samples/src/main/resources/data/beanWrapperMapperSampleJob/input/20070122.teststream.ImportTradeDataStep.txt new file mode 100644 index 000000000..2b4bbf444 --- /dev/null +++ b/samples/src/main/resources/data/beanWrapperMapperSampleJob/input/20070122.teststream.ImportTradeDataStep.txt @@ -0,0 +1,5 @@ +UK21341EAH4121131.11customer1 +UK21341EAH4221232.11customer2 +UK21341EAH4321333.11customer3 +UK21341EAH4421434.11customer4 +UK21341EAH4521535.11customer5 \ No newline at end of file diff --git a/samples/src/main/resources/data/fixedLengthImportJob/input/20070122.teststream.ImportTradeDataStep.txt b/samples/src/main/resources/data/fixedLengthImportJob/input/20070122.teststream.ImportTradeDataStep.txt new file mode 100644 index 000000000..2b4bbf444 --- /dev/null +++ b/samples/src/main/resources/data/fixedLengthImportJob/input/20070122.teststream.ImportTradeDataStep.txt @@ -0,0 +1,5 @@ +UK21341EAH4121131.11customer1 +UK21341EAH4221232.11customer2 +UK21341EAH4321333.11customer3 +UK21341EAH4421434.11customer4 +UK21341EAH4521535.11customer5 \ No newline at end of file diff --git a/samples/src/main/resources/data/multilineJob/input/20070122.teststream.multilineStep.txt b/samples/src/main/resources/data/multilineJob/input/20070122.teststream.multilineStep.txt new file mode 100644 index 000000000..70959886d --- /dev/null +++ b/samples/src/main/resources/data/multilineJob/input/20070122.teststream.multilineStep.txt @@ -0,0 +1,9 @@ +BEGIN +UK21341EAH4597898.34customer1 +UK21341EAH4611218.12customer2 +END +BEGIN +UK21341EAH4724512.78customer2 +UK21341EAH4810809.25customer3 +UK21341EAH4985423.39customer4 +END diff --git a/samples/src/main/resources/data/multilineJob/input/problematic.txt b/samples/src/main/resources/data/multilineJob/input/problematic.txt new file mode 100644 index 000000000..5aa61aa93 --- /dev/null +++ b/samples/src/main/resources/data/multilineJob/input/problematic.txt @@ -0,0 +1,20 @@ +RECORDTYPE1 +a,b,c +d,e +END + +RECORDTYPE2 +1:2:3 +4 +5:6 +END + +PERSON +john, william, smith #name +55 #age +END + +ANIMAL +tiger #spieces +1 #quantity +END diff --git a/samples/src/main/resources/data/multilineOrderJob/input/20070122.testStream.multilineOrderStep.txt b/samples/src/main/resources/data/multilineOrderJob/input/20070122.testStream.multilineOrderStep.txt new file mode 100644 index 000000000..4ae463665 --- /dev/null +++ b/samples/src/main/resources/data/multilineOrderJob/input/20070122.testStream.multilineOrderStep.txt @@ -0,0 +1,22 @@ +FHE;20070215-0001;2007-02-15 +HEA;0013100345;2007-02-15 +NCU;Smith;Peter;;T;20014539;F +BAD;;Oak Street 31/A;;Small Town;00235;IL;US +SAD;Smith, Elizabeth;Elm Street 17;;Some City;30011;FL;United States +BIN;VISA;VISA-12345678903 +LIT;1044391041;37.49;0;0;4.99;2.99;1;45.47 +LIT;2134776319;221.99;5;0;7.99;2.99;1;221.87 +SIN;UPS;EXP;DELIVER ONLY ON WEEKDAYS +FOT;2;2;267.34 +HEA;0013100346;2007-02-15 +BCU;Acme Factory of England;72155919;T +BAD;;St. Andrews Road 31;;London;55342;;UK +BIN;AMEX;AMEX-72345678903 +LIT;1044319101;1070.50;5;0;7.99;2.99;12;12335.46 +LIT;2134727219;21.79;5;0;7.99;2.99;12;380.17 +LIT;1044339301;79.95;0;5.5;4.99;2.99;4;329.72 +LIT;2134747319;55.29;10;0;7.99;2.99;6;364.45 +LIT;1044359501;339.99;10;0;7.99;2.99;2;633.94 +SIN;FEDX;AMS; +FOT;5;36;14043.74 +FFT;2;14311.08 diff --git a/samples/src/main/resources/data/multilineOrderJob/order_sample.txt b/samples/src/main/resources/data/multilineOrderJob/order_sample.txt new file mode 100644 index 000000000..d645e9b45 --- /dev/null +++ b/samples/src/main/resources/data/multilineOrderJob/order_sample.txt @@ -0,0 +1,51 @@ +# FHE;ORDER_BATCH_ID(S8-4);ORDER_BATCH_DATE(YYYY-MM-DD) +# +# HEA;ORDER_ID(N10);ORDER_DATE(YYYY-MM-DD) +# [excl] NCU;LAST_NAME(S35);FIRST_NAME(S35);MIDDLE_NAME(S35);REGISTERED(T/F);REG_ID(N8);VIP(T/F) +# [excl] BCU;COMPANY_NAME(S50);REG_ID(N8);VIP(T/F) +# BAD;ADDRESSEE(S60);ADDR_LINE1(S50);ADDR_LINE2(S50);CITY(S30);ZIP_CODE(S5);STATE(S2);COUNTRY(S50) +# [opt] SAD;ADDRESSEE(S60);ADDR_LINE1(S50);ADDR_LINE2(S50);CITY(S30);ZIP_CODE(S5);STATE(S2);COUNTRY(S50) +# BIN;PAYMENT_TYPE_ID(S4);PAYMENT_DESC(S30); +# LIT;ITEM_ID(N10);PRICE(N8.2);DISCOUNT_PERC(N3.2);DISCOUNT_AMOUNT(N8.2);SHIPPING_PRICE(N8.2);HANDLING_PRICE(N8.2);QUANTITY(N4);TOTAL_PRICE(N8.2) +# ... (1 .. n) ... +# SIN;SHIPPER_ID(S4);SHIPPING_TYPE_ID(S3);ADDITIONAL_SHIPPING_INFO(S100) +# FOT;TOTAL_LINE_ITEMS(N6);TOTAL_ITEMS(N6);TOTAL_PRICE(S8.2) +# +# FFT;TOTAL_ORDERS(N6);TOTAL_PRICE(N10.2) +# +# LINE_ID Description LINE MANDATORY OPTIONAL FIELDS* +# FHE File Header YES NONE +# HEA Record Header YES NONE +# NCU Non-Business Customer EXCL WITH BCU MIDDLE_NAME, REG_ID (if REGISTERED is 'F') +# BCU Business Customer EXCL WITH NCU NONE +# BAD Billing Address YES ADDRESSEE, ADDR_LINE2, STATE (if COUNTRY is not 'United States') +# SAD Shipping Address NO ADDR_LINE2, STATE (if COUNTRY is not 'United States') +# BIN Billing Info YES NONE +# LIT Line Item YES (1 to N lines) DISCOUNT_PERC and DISCOUNT_AMOUNT are mutualy exclusive (only one of them can be non zero) +# SIN Shipping Info YES ADDITIONAL_SHIPPING_INFO +# FOT Record Footer YES NONE +# FFT File Footer YES NONE +# +# * if field is optional at least empty field must be provided (';;') +FHE;20070215-0001;2007-02-15 +HEA;0013100345;2007-02-15 +NCU;Smith;Peter;;T;20014539;F +BAD;;Oak Street 31/A;;Small Town;00235;IL;US +SAD;Smith, Elizabeth;Elm Street 17;;Some City;30011;FL;United States +BIN;VISA;VISA-12345678903 +LIT;1044391041;37.49;0;0;4.99;2.99;1;45.47 +LIT;2134776319;221.99;5;0;7.99;2.99;1;221.87 +SIN;UPS;EXP;DELIVER ONLY ON WEEKDAYS +FOT;2;2;267.34 +HEA;0013100346;2007-02-15 +BCU;Acme Factory of England;72155919;T +BAD;;St. Andrews Road 31;;London;55342;;UK +BIN;AMEX;AMEX-72345678903 +LIT;1044319101;1070.50;5;0;7.99;2.99;12;12335.46 +LIT;2134727619;21.79;5;0;7.99;2.99;12;380.17 +LIT;1044339101;79.95;0;5.5;4.99;2.99;4;329.72 +LIT;2134747619;55.29;10;0;7.99;2.99;6;364.45 +LIT;1044359101;339.99;10;0;7.99;2.99;2;633.94 +SIN;FEDX;AMS; +FOT;5;36;14043.74 +FFT;2;14311.08 diff --git a/samples/src/main/resources/data/restartSample/input/20070122.teststream.ImportTradeDataStep.txt b/samples/src/main/resources/data/restartSample/input/20070122.teststream.ImportTradeDataStep.txt new file mode 100644 index 000000000..ea8953b3d --- /dev/null +++ b/samples/src/main/resources/data/restartSample/input/20070122.teststream.ImportTradeDataStep.txt @@ -0,0 +1,5 @@ +UK21341EAH4597898.34customer1 +UK21341EAH4611218.12customer2 +UK21341EAH4724512.78customer2 +UK21341EAH4810819.25customer3 +UK21341EAH4985423.39customer4 diff --git a/samples/src/main/resources/data/simpleSkipSample/input/20070122.teststream.ImportTradeDataStep.txt b/samples/src/main/resources/data/simpleSkipSample/input/20070122.teststream.ImportTradeDataStep.txt new file mode 100644 index 000000000..3b7bc7ba5 --- /dev/null +++ b/samples/src/main/resources/data/simpleSkipSample/input/20070122.teststream.ImportTradeDataStep.txt @@ -0,0 +1,5 @@ +UK21341EAH4597898.34customer1 +UK21341EAH4611218.12customer2 +UK21341EAH4724512.78customer2 +UK21341EAH48108109.25customer3 +UK21341EAH49854123.39customer4 \ No newline at end of file diff --git a/samples/src/main/resources/data/simpleTaskletJob/input/20070122.teststream.ImportTradeDataStep.txt b/samples/src/main/resources/data/simpleTaskletJob/input/20070122.teststream.ImportTradeDataStep.txt new file mode 100644 index 000000000..2b4bbf444 --- /dev/null +++ b/samples/src/main/resources/data/simpleTaskletJob/input/20070122.teststream.ImportTradeDataStep.txt @@ -0,0 +1,5 @@ +UK21341EAH4121131.11customer1 +UK21341EAH4221232.11customer2 +UK21341EAH4321333.11customer3 +UK21341EAH4421434.11customer4 +UK21341EAH4521535.11customer5 \ No newline at end of file diff --git a/samples/src/main/resources/data/simpleTaskletJob/input/20070207.testStream.ImportTradeDataStep.txt b/samples/src/main/resources/data/simpleTaskletJob/input/20070207.testStream.ImportTradeDataStep.txt new file mode 100644 index 000000000..ed9c7eac1 --- /dev/null +++ b/samples/src/main/resources/data/simpleTaskletJob/input/20070207.testStream.ImportTradeDataStep.txt @@ -0,0 +1,5 @@ +UK21341EAH4597898.34customer1 +UK21341EAH4611218.12customer2 +UK21341EAH4724512.78customer2 +UK21341EAH48108109.25customer3 +UK21341EAH49854123.39customer4 diff --git a/samples/src/main/resources/data/tradejob/input/20070122.teststream.ImportTradeDataStep.txt b/samples/src/main/resources/data/tradejob/input/20070122.teststream.ImportTradeDataStep.txt new file mode 100644 index 000000000..94c81c5bf --- /dev/null +++ b/samples/src/main/resources/data/tradejob/input/20070122.teststream.ImportTradeDataStep.txt @@ -0,0 +1,5 @@ +UK21341EAH45,978,98.34,customer1 +UK21341EAH46,112,18.12,customer2 +UK21341EAH47,245,12.78,customer2 +UK21341EAH48,108,109.25,customer3 +UK21341EAH49,854,123.39,customer4 \ No newline at end of file diff --git a/samples/src/main/resources/data/tradejob/input/TradeJob.csv b/samples/src/main/resources/data/tradejob/input/TradeJob.csv new file mode 100644 index 000000000..fe3d2ece2 --- /dev/null +++ b/samples/src/main/resources/data/tradejob/input/TradeJob.csv @@ -0,0 +1,5 @@ +UK21341EAH45,978,98.34 +UK21341EAH46,112,18.12 +UK21341EAH47,245,12.78 +UK21341EAH48,108,109.25 +UK21341EAH49,854,123.39 \ No newline at end of file diff --git a/samples/src/main/resources/data/xmlJob/input/20070122.testStream.xmlFileStep.xml b/samples/src/main/resources/data/xmlJob/input/20070122.testStream.xmlFileStep.xml new file mode 100644 index 000000000..73ec2ad43 --- /dev/null +++ b/samples/src/main/resources/data/xmlJob/input/20070122.testStream.xmlFileStep.xml @@ -0,0 +1,88 @@ + + + + Gladys Kravitz +
Anytown, PA
+ 34 + 0 + 0 +
+ 2003-01-07 14:16:00 GMT + + + Burnham's Celestial Handbook, Vol 1 + 5 + 21.79 + 2 + + + Burnham's Celestial Handbook, Vol 2 + 5 + 19.89 + 2 + + + + ZipShip + 0.74 + +
+ + + John Smith +
Chicago, IL
+ 46 + 0 + 0 +
+ 2003-01-07 14:16:02 GMT + + + XmlBeans in Action + 3 + 41.29 + 1 + + + JSR-173 + 1 + 11.99 + 5 + + + Teach Yourself XML in 21 days + 1 + 35.49 + 1 + + + + ZipShip + 0.74 + +
+ + + Peter Newman +
Cleveland, OH
+ 23 + 0 + 0 +
+ 2003-01-07 14:16:35 GMT + + + Java 6 + 2 + 12.79 + 3 + + + + UPS + 0.69 + +
+
diff --git a/samples/src/main/resources/data/xmlJob/input/purchaseorders.xsd b/samples/src/main/resources/data/xmlJob/input/purchaseorders.xsd new file mode 100644 index 000000000..1536a9859 --- /dev/null +++ b/samples/src/main/resources/data/xmlJob/input/purchaseorders.xsd @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/src/main/resources/data/xmlJob/output/20070122.testStream.xmlFileStep.xml b/samples/src/main/resources/data/xmlJob/output/20070122.testStream.xmlFileStep.xml new file mode 100644 index 000000000..13e91dde8 --- /dev/null +++ b/samples/src/main/resources/data/xmlJob/output/20070122.testStream.xmlFileStep.xml @@ -0,0 +1 @@ +Gladys Kravitz
Anytown, PA
3400
2003-01-07 14:16:00.0 GMTBurnham's Celestial Handbook, Vol 15.021.792Burnham's Celestial Handbook, Vol 25.019.892
John Smith
Chicago, IL
4600
2003-01-07 14:16:02.0 GMTXmlBeans in Action3.041.291JSR-1731.011.995Teach Yourself XML in 21 days1.035.491
Peter Newman
Cleveland, OH
2300
2003-01-07 14:16:35.0 GMTJava 62.012.793
\ No newline at end of file diff --git a/samples/src/main/resources/jobs/adhocLoopJob.xml b/samples/src/main/resources/jobs/adhocLoopJob.xml new file mode 100644 index 000000000..bd34c0efc --- /dev/null +++ b/samples/src/main/resources/jobs/adhocLoopJob.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/src/main/resources/jobs/beanWrapperMapperSampleJob.xml b/samples/src/main/resources/jobs/beanWrapperMapperSampleJob.xml new file mode 100644 index 000000000..6d6ae62ca --- /dev/null +++ b/samples/src/main/resources/jobs/beanWrapperMapperSampleJob.xml @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/src/main/resources/jobs/compositeProcessorSample.xml b/samples/src/main/resources/jobs/compositeProcessorSample.xml new file mode 100644 index 000000000..b4b995073 --- /dev/null +++ b/samples/src/main/resources/jobs/compositeProcessorSample.xml @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/src/main/resources/jobs/fixedLengthImportJob.xml b/samples/src/main/resources/jobs/fixedLengthImportJob.xml new file mode 100644 index 000000000..5cee051c7 --- /dev/null +++ b/samples/src/main/resources/jobs/fixedLengthImportJob.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/src/main/resources/jobs/infiniteLoopJob.xml b/samples/src/main/resources/jobs/infiniteLoopJob.xml new file mode 100644 index 000000000..6398d99fc --- /dev/null +++ b/samples/src/main/resources/jobs/infiniteLoopJob.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/src/main/resources/jobs/multilineJob.xml b/samples/src/main/resources/jobs/multilineJob.xml new file mode 100644 index 000000000..258300372 --- /dev/null +++ b/samples/src/main/resources/jobs/multilineJob.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/src/main/resources/jobs/multilineOrderInputDescriptors.xml b/samples/src/main/resources/jobs/multilineOrderInputDescriptors.xml new file mode 100644 index 000000000..5eba6614b --- /dev/null +++ b/samples/src/main/resources/jobs/multilineOrderInputDescriptors.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/src/main/resources/jobs/multilineOrderIo.xml b/samples/src/main/resources/jobs/multilineOrderIo.xml new file mode 100644 index 000000000..cfe6ab842 --- /dev/null +++ b/samples/src/main/resources/jobs/multilineOrderIo.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/src/main/resources/jobs/multilineOrderJob.xml b/samples/src/main/resources/jobs/multilineOrderJob.xml new file mode 100644 index 000000000..5c892431f --- /dev/null +++ b/samples/src/main/resources/jobs/multilineOrderJob.xml @@ -0,0 +1,152 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 AND ? <= 9999999999 : 'Incorrect order ID' : 'error.order.id' } + { orderDate : isFutureDate(?) = FALSE : 'Future date is not allowed' : 'error.order.date.future' } + { totalLines : ? = size(lineItems) : 'Bad count of order lines' : 'error.order.lines.badcount'} + + { customer.registered : customer.businessCustomer = FALSE OR ? = TRUE : 'Business customer must be registered' : 'error.customer.registration'} + { customer.companyName : customer.businessCustomer = FALSE OR ? HAS TEXT : 'Company name for business customer is mandatory' : 'error.customer.companyname'} + { customer.firstName : customer.businessCustomer = TRUE OR ? HAS TEXT : 'Firstname for non-business customer is mandatory' : 'error.customer.firstname'} + { customer.lastName : customer.businessCustomer = TRUE OR ? HAS TEXT : 'Lastname name for non-business customer is mandatory' : 'error.customer.lastname'} + { customer.registrationId : customer.registered = FALSE OR (? > 0 AND ? < 99999999) : 'Incorrect registration ID' : 'error.customer.registrationid'} + + { billingAddress.addressee : ? HAS NO TEXT OR length(?) <= 60 : 'Maximum length for Addressee is 60 characters' : 'error.baddress.addresse.length'} + { billingAddress.addrLine1 : ? HAS TEXT AND length(?) <= 50 : 'Address line1 is mandatory and maximum length for address line1 is 50 characters' : 'error.baddress.addrline1.length'} + { billingAddress.addrLine2 : ? HAS NO TEXT OR length(?) <= 50 : 'Maximum length for address line2 is 50 characters' : 'error.baddress.addrline2.length'} + { billingAddress.city : ? HAS TEXT AND length(?) <= 30 : 'City is mandatory and maximum length for city is 30 characters' : 'error.baddress.city.length'} + { billingAddress.zipCode : ? HAS TEXT AND length(?) <= 50 : 'Zipcode is mandatory and maximum length for zipcode is 5 characters' : 'error.baddress.zipcode.length'} + { billingAddress.zipCode : match('[0-9]{5}',?) = TRUE : 'ZipCode must contain exactly 5 digits' : 'error.baddress.zipcode.format'} + { billingAddress.state : (? HAS NO TEXT AND billingAddress.country != 'United States') OR (? HAS TEXT AND length(?) <= 2) : 'Maximum length for state is 2 characters' : 'error.baddress.state.length'} + { billingAddress.country : ? HAS TEXT AND length(?) <= 50 : 'Country is mandatory and maximum length for country is 50 characters' : 'error.baddress.country.length'} + + { shippingAddress.addressee : shippingAddress IS NULL OR (? HAS TEXT AND length(?) <= 60) : 'Addressee is mandatory and maximum length for addressee is 60 characters' : 'error.saddress.addresse.length'} + { shippingAddress.addrLine1 : shippingAddress IS NULL OR (? HAS TEXT AND length(?) <= 50) : 'Address line1 is mandatory and maximum length for address line1 is 50 characters' : 'error.baddress.addrline1.length'} + { shippingAddress.addrLine2 : shippingAddress IS NULL OR (? HAS NO TEXT OR length(?) <= 50) : 'Maximum length for address line2 is 50 characters' : 'error.baddress.addrline2.length'} + { shippingAddress.city : shippingAddress IS NULL OR (? HAS TEXT AND length(?) <= 30) : 'City is mandatory and maximum length for city is 30 characters' : 'error.baddress.city.length'} + { shippingAddress.zipCode : shippingAddress IS NULL OR (? HAS TEXT AND length(?) <= 50) : 'Zipcode is mandatory and maximum length for zipcode is 5 characters' : 'error.baddress.zipcode.length'} + { shippingAddress.zipCode : shippingAddress IS NULL OR (match('[0-9]{5}',?) = TRUE) : 'Zipcode must contain exactly 5 digits' : 'error.baddress.zipcode.format'} + { shippingAddress.state : shippingAddress IS NULL OR ((? HAS NO TEXT AND billingAddress.country != 'United States') OR (? HAS TEXT AND length(?) <= 2)) : 'Maximum length for state is 2 characters' : 'error.baddress.state.length'} + { shippingAddress.country : shippingAddress IS NULL OR (? HAS TEXT AND length(?) <= 50) : 'Country is mandatory and maximum length for country is 50 characters' : 'error.baddress.country.length'} + + { billing.paymentId : ? IN 'VISA','AMEX','ECMC','DCIN','PAYP' : 'Invalid payment type' : 'error.billing.type' } + { billing.paymentDesc : match('[A-Z]{4}-[0-9]{10,11}',?) = TRUE : 'Invalid format of payment description' : 'error.billing.desc' } + + { shipping.shipperId : ? IN 'FEDX', 'UPS', 'DHL', 'DPD' : 'Invalid shipper ID' : 'error.shipping.shipper'} + { shipping.shippingTypeId : ? IN 'STD', 'EXP', 'AMS', 'AME' : 'Invalid shipping type' : 'error.shipping.type' } + { shipping.shippingInfo : ? HAS NO TEXT OR length(?) <= 100 : 'Maximum length for additional shipping info is 100 characters' } + + { lineItems : validateTotalItemsCount(totalItems,?) = TRUE : 'Bad count of total line items' : 'error.lineitems.badcount' } + { lineItems : validateIds(?) = TRUE : 'One or more invalid item IDs' : 'error.lineitems.id' } + { lineItems : validatePrices(?) = TRUE : 'One or more invalid item prices' : 'error.lineitems.price' } + { lineItems : validateDiscounts(?) = TRUE : 'One or more invalid item discounts' : 'error.lineitems.discount' } + { lineItems : validateShippingPrices(?) = TRUE : 'One or more invalid item shipping prices' : 'error.lineitems.shipping' } + { lineItems : validateHandlingPrices(?) = TRUE : 'One or more invalid item handling prices' : 'error.lineitems.handling' } + { lineItems : validateQuantities(?) = TRUE : 'One or more invalid item quantities' : 'error.lineitems.quantity' } + { lineItems : validateTotalPrices(?) = TRUE : 'One or more invalid item total prices' : 'error.lineitems.totalprice' } + ]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/src/main/resources/jobs/multilineOrderOutputDescriptors.xml b/samples/src/main/resources/jobs/multilineOrderOutputDescriptors.xml new file mode 100644 index 000000000..5e76955b7 --- /dev/null +++ b/samples/src/main/resources/jobs/multilineOrderOutputDescriptors.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/src/main/resources/jobs/quartzBatch.xml b/samples/src/main/resources/jobs/quartzBatch.xml new file mode 100644 index 000000000..37875c929 --- /dev/null +++ b/samples/src/main/resources/jobs/quartzBatch.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/src/main/resources/jobs/restartSample.xml b/samples/src/main/resources/jobs/restartSample.xml new file mode 100644 index 000000000..369a53b45 --- /dev/null +++ b/samples/src/main/resources/jobs/restartSample.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/src/main/resources/jobs/simpleTaskletJob.xml b/samples/src/main/resources/jobs/simpleTaskletJob.xml new file mode 100644 index 000000000..cc0d0deb1 --- /dev/null +++ b/samples/src/main/resources/jobs/simpleTaskletJob.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/src/main/resources/jobs/tradeJob.xml b/samples/src/main/resources/jobs/tradeJob.xml new file mode 100644 index 000000000..45aefc25c --- /dev/null +++ b/samples/src/main/resources/jobs/tradeJob.xml @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/src/main/resources/jobs/tradeJobIo.xml b/samples/src/main/resources/jobs/tradeJobIo.xml new file mode 100644 index 000000000..8c13c6074 --- /dev/null +++ b/samples/src/main/resources/jobs/tradeJobIo.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/src/main/resources/jobs/xmlJob.xml b/samples/src/main/resources/jobs/xmlJob.xml new file mode 100644 index 000000000..dad3fd597 --- /dev/null +++ b/samples/src/main/resources/jobs/xmlJob.xml @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/src/main/resources/log4j.properties b/samples/src/main/resources/log4j.properties new file mode 100644 index 000000000..9f2adf936 --- /dev/null +++ b/samples/src/main/resources/log4j.properties @@ -0,0 +1,21 @@ +### direct log messages to stdout ### +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.Target=System.out +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n + +### set log levels - for more verbose logging change 'info' to 'debug' ### + +log4j.rootLogger=info, stdout + +### enable the following line if you want to track down connection ### +### leakages when using DriverManagerConnectionProvider ### +#log4j.logger.org.hibernate.connection.DriverManagerConnectionProvider=trace + +### enable spring +log4j.logger.org.springframework=error +log4j.logger.org.springframework.batch=info + +### debug your specific package or classes with the following example +log4j.logger.org.springframework.batch.sample.module.OrderDataProvider=debug +log4j.logger.org.springframework.batch.container.common.module.process.support.DefaultXmlDataProvider=debug diff --git a/samples/src/main/resources/simple-container-definition.xml b/samples/src/main/resources/simple-container-definition.xml new file mode 100644 index 000000000..0f4192a41 --- /dev/null +++ b/samples/src/main/resources/simple-container-definition.xml @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/src/main/resources/xstream-config.xml b/samples/src/main/resources/xstream-config.xml new file mode 100644 index 000000000..9926d3292 --- /dev/null +++ b/samples/src/main/resources/xstream-config.xml @@ -0,0 +1,75 @@ + + + 1003 + purchaseOrders + + + xmlns + http://adsj.accenture.com/purchaseorders + + + xmlns:xsi + http://www.w3.org/2001/XMLSchema-instance + + + xsi:schemaLocation + http://adsj.accenture.com/purchaseorders purchaseorders.xsd + + + + + + + + + org.springframework.batch.sample.domain.xml.Customer + org.springframework.batch.sample.domain.xml.Order + customer + + + org.springframework.batch.sample.domain.xml.Shipper + org.springframework.batch.sample.domain.xml.Order + shipper + + + + + + + + + + + + + + + + + + + http://adsj.accenture.com/purchaseorders + order + + org.springframework.batch.sample.domain.xml.Order + + + http://adsj.accenture.com/purchaseorders + customer + + org.springframework.batch.sample.domain.xml.Customer + + + http://adsj.accenture.com/purchaseorders + shipper + + org.springframework.batch.sample.domain.xml.Shipper + + + http://adsj.accenture.com/purchaseorders + lineItem + + org.springframework.batch.sample.domain.xml.LineItem + + + diff --git a/samples/src/site/apt/changelog.apt b/samples/src/site/apt/changelog.apt new file mode 100644 index 000000000..e67691400 --- /dev/null +++ b/samples/src/site/apt/changelog.apt @@ -0,0 +1,8 @@ +Changelog: Spring Batch Samples + +* 1.0-M2 + +** 2007/06/25 + + * Dave Syer: fixed xml sample by using scope="step" to control bean + lifecycle and stimulte call to close(). diff --git a/samples/src/site/site.xml b/samples/src/site/site.xml new file mode 100644 index 000000000..0d7558c3e --- /dev/null +++ b/samples/src/site/site.xml @@ -0,0 +1,38 @@ + + + + + Spring Batch: ${project.name} + + + + images/shim.gif + + + + + + + + org.springframework.maven.skins + maven-spring-skin + 1.0.3 + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/src/test/java/org/springframework/batch/sample/AbstractBatchBootstrapSpringContextTests.java b/samples/src/test/java/org/springframework/batch/sample/AbstractBatchBootstrapSpringContextTests.java new file mode 100644 index 000000000..f0edf5d8a --- /dev/null +++ b/samples/src/test/java/org/springframework/batch/sample/AbstractBatchBootstrapSpringContextTests.java @@ -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.sample; + +import org.springframework.batch.core.configuration.JobConfiguration; +import org.springframework.batch.execution.bootstrap.AbstractJobLauncher; +import org.springframework.test.AbstractDependencyInjectionSpringContextTests; + +/** + * Abstract unit test for running functional tests by getting context locations for + * both the container and configuration separately and having them auto wired in + * by type. This allows the two to be completely separated, and remove any + * 'configuration coupling' between the two. However, it is still purely + * theoretical until a decision is made as to how job configuration and container + * configuration files are pulled together. + * + * @author Lucas Ward + * + */ +public abstract class AbstractBatchBootstrapSpringContextTests extends AbstractDependencyInjectionSpringContextTests { + + private static final String CONTAINER_DEFINITION_LOCATION = "simple-container-definition.xml"; + + AbstractJobLauncher bootstrap; + JobConfiguration jobConfiguration; + + protected String[] getConfigLocations() { + return new String[]{CONTAINER_DEFINITION_LOCATION, getJobConfigurationContextLocation()}; + } + + public void testLifecycle(){ + bootstrap.start(); + } + + public void setBootstrap(AbstractJobLauncher bootstrap){ + this.bootstrap = bootstrap; + } + + protected abstract String getJobConfigurationContextLocation(); +} diff --git a/samples/src/test/java/org/springframework/batch/sample/AbstractBatchLauncherTests.java b/samples/src/test/java/org/springframework/batch/sample/AbstractBatchLauncherTests.java new file mode 100644 index 000000000..a92689b49 --- /dev/null +++ b/samples/src/test/java/org/springframework/batch/sample/AbstractBatchLauncherTests.java @@ -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.sample; + +import org.springframework.batch.core.configuration.JobConfiguration; +import org.springframework.batch.execution.bootstrap.JobLauncher; +import org.springframework.test.AbstractDependencyInjectionSpringContextTests; + +/** + * @author Dave Syer + * + */ +public abstract class AbstractBatchLauncherTests extends AbstractDependencyInjectionSpringContextTests { + + protected JobLauncher launcher; + private JobConfiguration jobConfiguration; + + /** + * Subclasses can provide name of job to run. We guess it by looking at the + * unique job configuration name. + */ + protected String getJobName() { + return jobConfiguration.getName(); + } + + public void setBatchContainerLauncher(JobLauncher launcher) { + this.launcher = launcher; + } + + /** + * @param jobConfiguration the jobConfiguration to set + */ + public void setJobConfiguration(JobConfiguration jobConfiguration) { + this.jobConfiguration = jobConfiguration; + } + +} diff --git a/samples/src/test/java/org/springframework/batch/sample/AbstractJobTests.java b/samples/src/test/java/org/springframework/batch/sample/AbstractJobTests.java new file mode 100644 index 000000000..19279cebe --- /dev/null +++ b/samples/src/test/java/org/springframework/batch/sample/AbstractJobTests.java @@ -0,0 +1,61 @@ +/* + * 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.sample; + +import junit.framework.TestCase; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.batch.core.configuration.JobConfiguration; +import org.springframework.batch.execution.JobExecutorFacade; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.context.ConfigurableApplicationContext; + +/** + * Only runs a job, not a real test + * + * @author robert.kasanicky + * + */ +public abstract class AbstractJobTests extends TestCase { + + public static final String JOB_CONFIGURATION_BEAN_ID = "jobConfiguration"; + + public static final String BATCH_CONTAINER_BEAN_ID = "batchContainer"; + + private static final Log log = LogFactory.getLog(AbstractJobTests.class); + + public void testRunJob() throws Exception { + BeanFactory ctx = loadContext(); + + JobConfiguration jobConfig = (JobConfiguration) ctx.getBean(JOB_CONFIGURATION_BEAN_ID); + JobExecutorFacade batchContainer = (JobExecutorFacade) ctx.getBean(BATCH_CONTAINER_BEAN_ID); + assertNotNull(jobConfig); + assertNotNull(batchContainer); + + log.info(jobConfig.getName() + " started"); +// batchContainer.start(); +// while (batchContainer.isRunning()) { +// Thread.sleep(100); +// } + log.info(jobConfig.getName() + " finished successfully"); + } + + // @Override + abstract protected ConfigurableApplicationContext loadContext(); + +} diff --git a/samples/src/test/java/org/springframework/batch/sample/AbstractLifecycleSpringContextTests.java b/samples/src/test/java/org/springframework/batch/sample/AbstractLifecycleSpringContextTests.java new file mode 100644 index 000000000..678ca21f3 --- /dev/null +++ b/samples/src/test/java/org/springframework/batch/sample/AbstractLifecycleSpringContextTests.java @@ -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.sample; + +import org.springframework.test.AbstractDependencyInjectionSpringContextTests; + +/** + * Abstract TestCase that automatically starts a Spring (@link Lifecycle) after + * obtaining it automatically via autowiring by type. It should be noted the + * getConfigLocations must be implemented for dependency injection to work + * properly. + * + * @author Lucas Ward + * @see AbstractDependencyInjectionSpringContextTests + */ +public abstract class AbstractLifecycleSpringContextTests extends AbstractBatchLauncherTests { + + public void testLifecycle() throws Exception { + validatePreConditions(); + launcher.start(getJobName()); + launcher.stop(); + validatePostConditions(); + } + + /** + * Make sure input data meets expectations + */ + protected void validatePreConditions() throws Exception { + } + + /** + * Make sure job did what it was expected to do. + */ + protected abstract void validatePostConditions() throws Exception; + +} diff --git a/samples/src/test/java/org/springframework/batch/sample/BeanWrapperMapperSampleJobFunctionalTests.java b/samples/src/test/java/org/springframework/batch/sample/BeanWrapperMapperSampleJobFunctionalTests.java new file mode 100644 index 000000000..32c8fff6a --- /dev/null +++ b/samples/src/test/java/org/springframework/batch/sample/BeanWrapperMapperSampleJobFunctionalTests.java @@ -0,0 +1,30 @@ +/* + * 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.sample; + + +public class BeanWrapperMapperSampleJobFunctionalTests extends AbstractLifecycleSpringContextTests { + + protected String[] getConfigLocations() { + return new String[]{"jobs/beanWrapperMapperSampleJob.xml"}; + } + + protected void validatePostConditions() { + // nothing to check, the job writes no output + } + +} diff --git a/samples/src/test/java/org/springframework/batch/sample/CompositeProcessorSampleFunctionalTests.java b/samples/src/test/java/org/springframework/batch/sample/CompositeProcessorSampleFunctionalTests.java new file mode 100644 index 000000000..70b0ab2ad --- /dev/null +++ b/samples/src/test/java/org/springframework/batch/sample/CompositeProcessorSampleFunctionalTests.java @@ -0,0 +1,96 @@ +package org.springframework.batch.sample; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.math.BigDecimal; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.apache.commons.io.IOUtils; +import org.springframework.batch.sample.domain.Trade; +import org.springframework.jdbc.core.JdbcOperations; +import org.springframework.jdbc.core.RowCallbackHandler; + + +public class CompositeProcessorSampleFunctionalTests extends AbstractLifecycleSpringContextTests { + + private static final String GET_TRADES = "SELECT isin, quantity, price, customer FROM trade order by isin"; + + private static final String EXPECTED_OUTPUT_FILE = + "Trade: [isin=UK21341EAH41,quantity=211,price=31.11,customer=customer1]" + + "Trade: [isin=UK21341EAH42,quantity=212,price=32.11,customer=customer2]" + + "Trade: [isin=UK21341EAH43,quantity=213,price=33.11,customer=customer3]" + + "Trade: [isin=UK21341EAH44,quantity=214,price=34.11,customer=customer4]" + + "Trade: [isin=UK21341EAH45,quantity=215,price=35.11,customer=customer5]"; + + private JdbcOperations jdbcTemplate; + + private int activeRow = 0; + + private int before; + +// @Override + protected String[] getConfigLocations() { + return new String[]{"jobs/compositeProcessorSample.xml"}; + } + + /* (non-Javadoc) + * @see org.springframework.batch.sample.AbstractLifecycleSpringContextTests#validatePreConditions() + */ + protected void validatePreConditions() throws Exception { + jdbcTemplate.update("DELETE from TRADE"); + before = jdbcTemplate.queryForInt("SELECT COUNT(*) from TRADE"); + } + + protected void validatePostConditions() throws Exception { + checkOutputFile(); + checkOutputTable(); + } + + private void checkOutputTable() { + final List trades = new ArrayList() {{ + add(new Trade("UK21341EAH41", 211, new BigDecimal("31.11"), "customer1")); + add(new Trade("UK21341EAH42", 212, new BigDecimal("32.11"), "customer2")); + add(new Trade("UK21341EAH43", 213, new BigDecimal("33.11"), "customer3")); + add(new Trade("UK21341EAH44", 214, new BigDecimal("34.11"), "customer4")); + add(new Trade("UK21341EAH45", 215, new BigDecimal("35.11"), "customer5")); + }}; + + int after = jdbcTemplate.queryForInt("SELECT COUNT(*) from TRADE"); + + assertEquals(before+5, after); + + jdbcTemplate.query(GET_TRADES, new RowCallbackHandler() { + public void processRow(ResultSet rs) throws SQLException { + Trade trade = (Trade)trades.get(activeRow++); + + assertEquals(trade.getIsin(), rs.getString(1)); + assertEquals(trade.getQuantity(), rs.getLong(2)); + assertEquals(trade.getPrice(), rs.getBigDecimal(3)); + assertEquals(trade.getCustomer(), rs.getString(4)); + } + }); + + } + + private void checkOutputFile() throws FileNotFoundException, IOException { + List outputLines = IOUtils.readLines( + new FileInputStream("20070122.testStream.ParallelCustomerReportStep.TEMP.txt")); + + String output = ""; + for (Iterator iterator = outputLines.listIterator(); iterator.hasNext();) { + String line = (String) iterator.next(); + output += line; + } + + assertEquals(EXPECTED_OUTPUT_FILE, output); + } + + public void setJdbcTemplate(JdbcOperations jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } +} diff --git a/samples/src/test/java/org/springframework/batch/sample/FixedLengthImportJobFunctionalTests.java b/samples/src/test/java/org/springframework/batch/sample/FixedLengthImportJobFunctionalTests.java new file mode 100644 index 000000000..a96e6f95a --- /dev/null +++ b/samples/src/test/java/org/springframework/batch/sample/FixedLengthImportJobFunctionalTests.java @@ -0,0 +1,97 @@ +/* + * 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.sample; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.springframework.batch.io.file.FieldSet; +import org.springframework.batch.io.file.FieldSetInputSource; +import org.springframework.core.io.Resource; +import org.springframework.jdbc.core.JdbcOperations; +import org.springframework.jdbc.core.RowCallbackHandler; + +public class FixedLengthImportJobFunctionalTests extends AbstractLifecycleSpringContextTests { + + //expected line length in input file (sum of pattern lengths + 2, because the counter is appended twice) + private static final int LINE_LENGTH = 29; + + //auto-injected attributes + private JdbcOperations jdbcTemplate; + private Resource fileLocator; + private FieldSetInputSource inputSource; + + protected void onSetUp() throws Exception { + super.onSetUp(); + jdbcTemplate.update("delete from TRADE"); + } + + protected String[] getConfigLocations() { + return new String[] {"jobs/fixedLengthImportJob.xml"}; + } + + /** + * check that records have been correctly written to database + */ + protected void validatePostConditions() { + + //ensure the input source is starts from the beginning + inputSource.close(); + inputSource.open(); + + jdbcTemplate.query("SELECT ID, ISIN, QUANTITY, PRICE, CUSTOMER FROM trade ORDER BY id", new RowCallbackHandler() { + + public void processRow(ResultSet rs) throws SQLException { + FieldSet fieldSet = inputSource.readFieldSet(); + assertEquals(fieldSet.readString(0), rs.getString(2)); + assertEquals(fieldSet.readLong(1),rs.getLong(3)); + assertEquals(fieldSet.readBigDecimal(2), rs.getBigDecimal(4)); + assertEquals(fieldSet.readString(3), rs.getString(5)); + } + + }); + + assertNull(inputSource.read()); + } + + /* + * fixed-length file is expected on input + */ + protected void validatePreConditions() throws Exception{ + BufferedReader reader = null; + + reader = new BufferedReader(new FileReader(fileLocator.getFile())); + String line; + while ((line = reader.readLine()) != null) { + assertEquals (LINE_LENGTH, line.length()); + } + } + + public void setJdbcTemplate(JdbcOperations jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + public void setFileLocator(Resource fileLocator) { + this.fileLocator = fileLocator; + } + + public void setFieldSetInputSource(FieldSetInputSource inputSource){ + this.inputSource = inputSource; + } +} diff --git a/samples/src/test/java/org/springframework/batch/sample/GracefulShutdownFunctionalTest.java b/samples/src/test/java/org/springframework/batch/sample/GracefulShutdownFunctionalTest.java new file mode 100644 index 000000000..349d6dfd7 --- /dev/null +++ b/samples/src/test/java/org/springframework/batch/sample/GracefulShutdownFunctionalTest.java @@ -0,0 +1,81 @@ +/* + * 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.sample; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.batch.repeat.exception.RepeatException; + +/** + * Functional test for graceful shutdown. A batch container is started in a new thread, + * then it's stopped via the Lifecycle interface. + * + * @author Lucas Ward + * + */ +public class GracefulShutdownFunctionalTest extends AbstractBatchLauncherTests { + + protected String[] getConfigLocations(){ + return new String[] {"jobs/infiniteLoopJob.xml"}; + } + + public void testJob()throws Exception { + final List errors = new ArrayList(); + + Thread jobThread = new Thread(){ + public void run(){ + try { + launcher.start(getJobName()); + } + catch (RepeatException e) { + if (!(e.getCause() instanceof InterruptedException)) { + errors.add(e); + } + } + catch (Exception e) { + errors.add(e); + } + } + }; + + jobThread.start(); + + //give the thread a second to start up + Thread.sleep(200); + + assertTrue(launcher.isRunning()); + assertTrue(jobThread.isAlive()); + + //stop the job + + launcher.stop(); + + //it takes a little while for it to shut down. + Thread.sleep(1000); + + assertFalse(launcher.isRunning()); + assertFalse(jobThread.isAlive()); + + if (!errors.isEmpty()) { + Exception e = (Exception) errors.get(0); + e.printStackTrace(); + fail("Unexpected Exception: "+e); + } + } + +} diff --git a/samples/src/test/java/org/springframework/batch/sample/LineAggregatorStub.java b/samples/src/test/java/org/springframework/batch/sample/LineAggregatorStub.java new file mode 100644 index 000000000..ccadc0ef9 --- /dev/null +++ b/samples/src/test/java/org/springframework/batch/sample/LineAggregatorStub.java @@ -0,0 +1,41 @@ +/* + * 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.sample; + +import org.springframework.batch.io.file.support.transform.LineAggregator; + + +/** + * Stub implementation of {@link LineAggregator} interface for testing purposes. + * + * @author robert.kasanicky + */ +public class LineAggregatorStub implements LineAggregator { + + /** + * Concatenates arguments. Ignores the LineDescriptor. + */ + public String aggregate(String[] args) { + String result = ""; + + for (int i = 1; i < args.length; i++) { + result = result + args[i]; + } + + return result; + } +} diff --git a/samples/src/test/java/org/springframework/batch/sample/MultilineJobFunctionalTests.java b/samples/src/test/java/org/springframework/batch/sample/MultilineJobFunctionalTests.java new file mode 100644 index 000000000..0e4b80ecf --- /dev/null +++ b/samples/src/test/java/org/springframework/batch/sample/MultilineJobFunctionalTests.java @@ -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.sample; + +import org.apache.commons.io.IOUtils; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.util.StringUtils; + +public class MultilineJobFunctionalTests extends AbstractLifecycleSpringContextTests { + + private final String EXPECTED_RESULT = + "[Trade: [isin=UK21341EAH45,quantity=978,price=98.34,customer=customer1], Trade: [isin=UK21341EAH46,quantity=112,price=18.12,customer=customer2]]" + + "[Trade: [isin=UK21341EAH47,quantity=245,price=12.78,customer=customer2], Trade: [isin=UK21341EAH48,quantity=108,price=9.25,customer=customer3], Trade: [isin=UK21341EAH49,quantity=854,price=23.39,customer=customer4]]"; + + private Resource output = new FileSystemResource("20070122.testStream.multilineStep.txt"); + +// @Override + protected String[] getConfigLocations() { + return new String[] {"jobs/multilineJob.xml"}; + } + + protected void validatePostConditions() throws Exception { + assertEquals(EXPECTED_RESULT, StringUtils.replace(IOUtils.toString(output.getInputStream()), System.getProperty("line.separator"), "")); + } +} diff --git a/samples/src/test/java/org/springframework/batch/sample/MultilineOrderJobFunctionalTests.java b/samples/src/test/java/org/springframework/batch/sample/MultilineOrderJobFunctionalTests.java new file mode 100644 index 000000000..5715b3a52 --- /dev/null +++ b/samples/src/test/java/org/springframework/batch/sample/MultilineOrderJobFunctionalTests.java @@ -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.sample; + +import java.io.IOException; + +import org.apache.commons.io.IOUtils; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.util.StringUtils; + +public class MultilineOrderJobFunctionalTests extends AbstractLifecycleSpringContextTests { + //private static final Log log = LogFactory.getLog(MultilineOrderJobFunctionalTests.class); + + private static final String EXPECTED_OUTPUT = + "BEGIN_ORDER:13100345 2007/02/15 "+ + "CUSTOMER:20014539 Peter Smith "+ + "ADDRESS:Oak Street 31/A Small Town00235 "+ + "BILLING:VISA VISA-12345678903 "+ + "ITEM:104439104137.49 "+ + "ITEM:2134776319221.99 "+ + "END_ORDER:267.34 "+ + "BEGIN_ORDER:13100346 2007/02/15 "+ + "CUSTOMER:72155919 "+ + "ADDRESS:St. Andrews Road 31 London 55342 "+ + "BILLING:AMEX AMEX-72345678903 "+ + "ITEM:10443191011070.50 "+ + "ITEM:213472721921.79 "+ + "ITEM:104433930179.95 "+ + "ITEM:213474731955.29 "+ + "ITEM:1044359501339.99 "+ + "END_ORDER:14043.74 "; + + + private Resource fileOutputLocator = new FileSystemResource("20070122.teststream.multilineOrderStep.TEMP.txt"); + +// @Override + protected String[] getConfigLocations() { + return new String[] {"jobs/multilineOrderJob.xml"}; + } + + /** + * Read the output file and compare it with expected string + * @throws IOException + */ + protected void validatePostConditions() throws Exception { + assertEquals(EXPECTED_OUTPUT, StringUtils.replace(IOUtils.toString(fileOutputLocator.getInputStream()), System.getProperty("line.separator"), "")); + } + +} diff --git a/samples/src/test/java/org/springframework/batch/sample/RestartFunctionalTests.java b/samples/src/test/java/org/springframework/batch/sample/RestartFunctionalTests.java new file mode 100644 index 000000000..073b67d27 --- /dev/null +++ b/samples/src/test/java/org/springframework/batch/sample/RestartFunctionalTests.java @@ -0,0 +1,87 @@ +/* + * 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.sample; + +import org.springframework.batch.io.exception.BatchCriticalException; +import org.springframework.jdbc.core.JdbcOperations; + +/** + * Simple restart scenario. + * + * @author Robert Kasanicky + * @author Dave Syer + */ +public class RestartFunctionalTests extends AbstractBatchLauncherTests { + + private static final String JOB_FILE = "jobs/restartSample.xml"; + + //auto-injected attributes + private JdbcOperations jdbcTemplate; + + /** + * Public setter for the jdbcTemplate. + * + * @param jdbcTemplate the jdbcTemplate to set + */ + public void setJdbcTemplate(JdbcOperations jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + protected String[] getConfigLocations(){ + return new String[]{JOB_FILE}; + } + + /* (non-Javadoc) + * @see org.springframework.test.AbstractSingleSpringContextTests#onTearDown() + */ + protected void onTearDown() throws Exception { + jdbcTemplate.update("DELETE FROM TRADE"); + } + + /** + * Job fails on first run, because the module throws exception after + * processing more than half of the input. On the second run, the job should + * finish successfully, because it continues execution where the previous + * run stopped (module throws exception after fixed number of processed + * records). + * @throws Exception + */ + public void testRestart() throws Exception { + + int before = jdbcTemplate.queryForInt("SELECT COUNT(*) FROM TRADE"); + + try { + runJob(); + fail("First run of the job is expected to fail."); + } + catch (BatchCriticalException expected) { + //expected + } + + runJob(); + + int after = jdbcTemplate.queryForInt("SELECT COUNT(*) FROM TRADE"); + + assertEquals(before+5, after); + } + + // load the application context and launch the job + private void runJob() throws Exception, Exception { + launcher.start(getJobName()); + } + +} diff --git a/samples/src/test/java/org/springframework/batch/sample/SimpleTaskletJobFunctionalTests.java b/samples/src/test/java/org/springframework/batch/sample/SimpleTaskletJobFunctionalTests.java new file mode 100644 index 000000000..e40767e49 --- /dev/null +++ b/samples/src/test/java/org/springframework/batch/sample/SimpleTaskletJobFunctionalTests.java @@ -0,0 +1,30 @@ +/* + * 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.sample; + +/** + * This job differs from FixedLengthImportJob only in internal job configuration, therefore + * the test is reused, only the job config location is overridden + */ +public class SimpleTaskletJobFunctionalTests extends FixedLengthImportJobFunctionalTests { + +// @Override + protected String[] getConfigLocations() { + return new String[]{"jobs/simpleTaskletJob.xml"}; + } + +} diff --git a/samples/src/test/java/org/springframework/batch/sample/TaskExecutorLauncher.java b/samples/src/test/java/org/springframework/batch/sample/TaskExecutorLauncher.java new file mode 100644 index 000000000..e1ee5f435 --- /dev/null +++ b/samples/src/test/java/org/springframework/batch/sample/TaskExecutorLauncher.java @@ -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.sample; + +import org.springframework.context.support.ClassPathXmlApplicationContext; + +/** + * @author Dave Syer + * + */ +public class TaskExecutorLauncher { + + public static void main(String[] args) throws Exception { + final String path = "jobs/adhocLoopJob.xml"; + new Thread(new Runnable() { + public void run() { + new ClassPathXmlApplicationContext(path); + }; + }).start(); + System.in.read(); + } +} diff --git a/samples/src/test/java/org/springframework/batch/sample/TradeJobFunctionalTests.java b/samples/src/test/java/org/springframework/batch/sample/TradeJobFunctionalTests.java new file mode 100644 index 000000000..8e61534ae --- /dev/null +++ b/samples/src/test/java/org/springframework/batch/sample/TradeJobFunctionalTests.java @@ -0,0 +1,196 @@ +/* + * 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.sample; + +import java.math.BigDecimal; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; + +import org.springframework.batch.sample.domain.Trade; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowCallbackHandler; + + + +public class TradeJobFunctionalTests extends AbstractLifecycleSpringContextTests { + + private static final String GET_TRADES = "SELECT isin, quantity, price, customer FROM trade"; + private static final String GET_CUSTOMERS = "SELECT name, credit FROM customer"; + + private ArrayList customers; + private ArrayList trades; + private int activeRow = 0; + + private JdbcTemplate jdbcTemplate; + + /** + * @param jdbcTemplate the jdbcTemplate to set + */ + public void setJdbcTemplate(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + protected String[] getConfigLocations() { + return new String[] {"jobs/tradeJob.xml"}; + } + + /* (non-Javadoc) + * @see org.springframework.test.AbstractSingleSpringContextTests#onSetUp() + */ + protected void onSetUp() throws Exception { + super.onSetUp(); + jdbcTemplate.update("delete from TRADE"); + } + + public void testLifecycle() throws Exception{ + super.testLifecycle(); + } + + protected void validatePostConditions() { + + // assertTrue(((Resource)applicationContext.getBean("customerFileLocator")).exists()); + + customers = new ArrayList() {{add(new Customer("customer1", (100000 - 98.34))); + add(new Customer("customer2", (100000 - 18.12 - 12.78))); + add(new Customer("customer3", (100000 - 109.25))); + add(new Customer("customer4", (100000 - 123.39)));}}; + + trades = new ArrayList() {{add(new Trade("UK21341EAH45", 978, new BigDecimal("98.34"), "customer1")); + add(new Trade("UK21341EAH46", 112, new BigDecimal("18.12"), "customer2")); + add(new Trade("UK21341EAH47", 245, new BigDecimal("12.78"), "customer2")); + add(new Trade("UK21341EAH48", 108, new BigDecimal("109.25"), "customer3")); + add(new Trade("UK21341EAH49", 854, new BigDecimal("123.39"), "customer4"));}}; + + // check content of the trade table + jdbcTemplate.query(GET_TRADES, new RowCallbackHandler() { + + public void processRow(ResultSet rs) throws SQLException { + Trade trade = (Trade)trades.get(activeRow++); + + assertTrue(trade.getIsin().equals(rs.getString(1))); + assertTrue(trade.getQuantity() == rs.getLong(2)); + assertTrue(trade.getPrice().equals(rs.getBigDecimal(3))); + assertTrue(trade.getCustomer().equals(rs.getString(4))); + } + }); + + assertTrue(trades.size() == activeRow); + + // check content of the customer table + activeRow = 0; + jdbcTemplate.query(GET_CUSTOMERS, new RowCallbackHandler() { + + public void processRow(ResultSet rs) throws SQLException { + Customer customer = (Customer)customers.get(activeRow++); + + assertTrue(customer.getName().equals(rs.getString(1))); + assertTrue(customer.getCredit() == rs.getDouble(2)); + } + }); + + assertTrue(customers.size() == activeRow); + + // check content of the output file + + +// Clean up + ((FileSystemResource)applicationContext.getBean("customerFileLocator")).getFile().delete(); + } + + protected void validatePreConditions() { + assertTrue(((Resource)applicationContext.getBean("fileLocator")).exists()); + } + + private class Customer { + private String name; + private double credit; + + public Customer(String name, double credit) { + this.name = name; + this.credit = credit; + } + + public Customer(){ + } + + /** + * @return the credit + */ + public double getCredit() { + return credit; + } + /** + * @param credit the credit to set + */ + public void setCredit(double credit) { + this.credit = credit; + } + /** + * @return the name + */ + public String getName() { + return name; + } + /** + * @param name the name to set + */ + public void setName(String name) { + this.name = name; + } + + /* (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + public int hashCode() { + final int PRIME = 31; + int result = 1; + long temp; + temp = Double.doubleToLongBits(credit); + result = PRIME * result + (int) (temp ^ (temp >>> 32)); + result = PRIME * result + ((name == null) ? 0 : name.hashCode()); + return result; + } + + /* (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + final Customer other = (Customer) obj; + if (Double.doubleToLongBits(credit) != Double.doubleToLongBits(other.credit)) + return false; + if (name == null) { + if (other.name != null) + return false; + } else if (!name.equals(other.name)) + return false; + return true; + } + + + } + + +} diff --git a/samples/src/test/java/org/springframework/batch/sample/XmlJobFunctionalTests.java b/samples/src/test/java/org/springframework/batch/sample/XmlJobFunctionalTests.java new file mode 100644 index 000000000..9a51f9a77 --- /dev/null +++ b/samples/src/test/java/org/springframework/batch/sample/XmlJobFunctionalTests.java @@ -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.sample; + +import org.apache.commons.io.IOUtils; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; + +public class XmlJobFunctionalTests extends AbstractLifecycleSpringContextTests { + //private static final Log log = LogFactory.getLog(XmlJobFunctionalTests.class); + + private Resource fileInputLocator = new ClassPathResource("data/xmlJob/output/20070122.testStream.xmlFileStep.xml"); + private Resource fileOutputLocator = new FileSystemResource("20070122.testStream.xmlFileStep.xml"); + +// @Override + protected String[] getConfigLocations() { + return new String[]{"jobs/xmlJob.xml"}; + } + + /** + * Output should be the same as input + */ + protected void validatePostConditions() throws Exception { + //TODO String comparison is inadequate, compare DOMs instead + + String input = IOUtils.toString(fileInputLocator.getInputStream()); + String output = IOUtils.toString(fileOutputLocator.getInputStream()); + + //assertEquals(input, output); + assertTrue(input.length() > 0); + assertTrue(output.length() > 0); + } + +} diff --git a/samples/src/test/java/org/springframework/batch/sample/dao/FlatFileCustomerCreditWriterTests.java b/samples/src/test/java/org/springframework/batch/sample/dao/FlatFileCustomerCreditWriterTests.java new file mode 100644 index 000000000..abd7463cf --- /dev/null +++ b/samples/src/test/java/org/springframework/batch/sample/dao/FlatFileCustomerCreditWriterTests.java @@ -0,0 +1,76 @@ +package org.springframework.batch.sample.dao; + +import java.math.BigDecimal; + +import org.springframework.batch.io.OutputSource; +import org.springframework.batch.sample.domain.CustomerCredit; + +import org.easymock.MockControl; +import junit.framework.TestCase; + +public class FlatFileCustomerCreditWriterTests extends TestCase { + + private MockControl outputControl; + private OutputSource output; + private FlatFileCustomerCreditWriter writer; + + public void setUp() throws Exception { + super.setUp(); + + //create mock for OutputSource + outputControl = MockControl.createControl(OutputSource.class); + output = (OutputSource)outputControl.getMock(); + + //create new writer + writer = new FlatFileCustomerCreditWriter(); + writer.setOutputSource(output); + } + + public void testOpen() { + + //set-up outputSource mock + output.open(); + outputControl.replay(); + + //call tested method + writer.open(); + + //verify method calls + outputControl.verify(); + } + + public void testClose() { + + //set-up outputSource mock + output.close(); + outputControl.replay(); + + //call tested method + writer.close(); + + //verify method calls + outputControl.verify(); + } + + public void testWrite() { + + //Create and set-up CustomerCredit + CustomerCredit credit = new CustomerCredit(); + credit.setCredit(new BigDecimal(1)); + credit.setName("testName"); + + //set separator + writer.setSeparator(";"); + + //set-up OutputSource mock + output.write("testName;1"); + output.open(); + outputControl.replay(); + + //call tested method + writer.write(credit); + + //verify method calls + outputControl.verify(); + } +} diff --git a/samples/src/test/java/org/springframework/batch/sample/dao/FlatFileOrderWriterTests.java b/samples/src/test/java/org/springframework/batch/sample/dao/FlatFileOrderWriterTests.java new file mode 100644 index 000000000..054d0c89f --- /dev/null +++ b/samples/src/test/java/org/springframework/batch/sample/dao/FlatFileOrderWriterTests.java @@ -0,0 +1,110 @@ +package org.springframework.batch.sample.dao; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.GregorianCalendar; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import junit.framework.TestCase; + +import org.springframework.batch.io.OutputSource; +import org.springframework.batch.io.file.support.transform.LineAggregator; +import org.springframework.batch.sample.LineAggregatorStub; +import org.springframework.batch.sample.domain.Address; +import org.springframework.batch.sample.domain.BillingInfo; +import org.springframework.batch.sample.domain.Customer; +import org.springframework.batch.sample.domain.LineItem; +import org.springframework.batch.sample.domain.Order; + +public class FlatFileOrderWriterTests extends TestCase { + + List list = new ArrayList(); + + private OutputSource output = new OutputSource() { + + public void write(Object output) { + list.add(output); + } + + public void close() { + list.add("close"); + } + + public void open() { + list.add("open"); + } + }; + + private FlatFileOrderWriter writer; + + public void setUp() throws Exception { + super.setUp(); + //create new writer + writer = new FlatFileOrderWriter(); + writer.setOutputSource(output); + } + + public void testOpen() { + + //call tested method + writer.open(); + //verify method calls + assertEquals(1, list.size()); + + } + + public void testClose() { + + //call tested method + writer.close(); + //verify method calls + assertEquals(1, list.size()); + + } + + public void testWrite() { + + //Create and set-up Order + Order order = new Order(); + + order.setOrderDate(new GregorianCalendar(2007, GregorianCalendar.JUNE, 1).getTime()); + order.setCustomer(new Customer()); + order.setBilling(new BillingInfo()); + order.setBillingAddress(new Address()); + List lineItems = new ArrayList(); + LineItem item = new LineItem(); + item.setPrice(BigDecimal.valueOf(0)); + lineItems.add(item); + lineItems.add(item); + order.setLineItems(lineItems); + order.setTotalPrice(BigDecimal.valueOf(0)); + + //create aggregator stub + LineAggregator aggregator = new LineAggregatorStub(); + + //create map of aggregators and set it to writer + Map aggregators = new HashMap(); + + OrderConverter converter = new OrderConverter(); + aggregators.put("header", aggregator); + aggregators.put("customer", aggregator); + aggregators.put("address", aggregator); + aggregators.put("billing", aggregator); + aggregators.put("item", aggregator); + aggregators.put("footer", aggregator); + converter.setAggregators(aggregators); + writer.setConverter(converter); + + //call tested method + writer.write(order); + + //verify method calls + assertEquals(1, list.size()); + assertTrue(list.get(0) instanceof List); + assertEquals("02007/06/01", ((List) list.get(0)).get(0)); + + } + +} diff --git a/samples/src/test/java/org/springframework/batch/sample/dao/JdbcCustomerDebitWriterTests.java b/samples/src/test/java/org/springframework/batch/sample/dao/JdbcCustomerDebitWriterTests.java new file mode 100644 index 000000000..252040678 --- /dev/null +++ b/samples/src/test/java/org/springframework/batch/sample/dao/JdbcCustomerDebitWriterTests.java @@ -0,0 +1,42 @@ +package org.springframework.batch.sample.dao; + +import java.math.BigDecimal; +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.springframework.batch.sample.domain.CustomerDebit; +import org.springframework.jdbc.core.RowCallbackHandler; +import org.springframework.test.AbstractTransactionalDataSourceSpringContextTests; + +public class JdbcCustomerDebitWriterTests extends AbstractTransactionalDataSourceSpringContextTests { + + protected String[] getConfigLocations() { + return new String[] { "data-source-context.xml" }; + } + + public void testWrite() { + + //insert customer credit + jdbcTemplate.execute("INSERT INTO customer VALUES (99, 0, 'testName', 100)"); + + //create writer and set jdbcTemplate + JdbcCustomerDebitWriter writer = new JdbcCustomerDebitWriter(); + writer.setJdbcTemplate(jdbcTemplate); + + //create customer debit + CustomerDebit customerDebit = new CustomerDebit(); + customerDebit.setName("testName"); + customerDebit.setDebit(BigDecimal.valueOf(5)); + + //call writer + writer.write(customerDebit); + + //verify customer credit + jdbcTemplate.query("SELECT name, credit FROM customer WHERE name = 'testName'", new RowCallbackHandler() { + public void processRow(ResultSet rs) throws SQLException { + assertEquals(95, rs.getLong("credit")); + } + }); + + } +} diff --git a/samples/src/test/java/org/springframework/batch/sample/dao/JdbcTradeWriterTests.java b/samples/src/test/java/org/springframework/batch/sample/dao/JdbcTradeWriterTests.java new file mode 100644 index 000000000..361a82e45 --- /dev/null +++ b/samples/src/test/java/org/springframework/batch/sample/dao/JdbcTradeWriterTests.java @@ -0,0 +1,44 @@ +package org.springframework.batch.sample.dao; + +import java.math.BigDecimal; +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.springframework.batch.sample.domain.Trade; +import org.springframework.jdbc.core.RowCallbackHandler; +import org.springframework.jdbc.support.incrementer.AbstractDataFieldMaxValueIncrementer; +import org.springframework.test.AbstractTransactionalDataSourceSpringContextTests; + +public class JdbcTradeWriterTests extends AbstractTransactionalDataSourceSpringContextTests { + + protected String[] getConfigLocations() { + return new String[] { "data-source-context.xml" }; + } + + public void testWrite() { + + JdbcTradeWriter writer = new JdbcTradeWriter(); + + AbstractDataFieldMaxValueIncrementer incrementer = (AbstractDataFieldMaxValueIncrementer)applicationContext.getBean("jobIncrementer"); + incrementer.setIncrementerName("TRADE_SEQ"); + + writer.setIncrementer(incrementer); + writer.setJdbcTemplate(jdbcTemplate); + + Trade trade = new Trade(); + trade.setCustomer("testCustomer"); + trade.setIsin("5647238492"); + trade.setPrice(new BigDecimal(Double.toString(99.69))); + trade.setQuantity(5); + + writer.write(trade); + + jdbcTemplate.query("SELECT * FROM TRADE WHERE ISIN = 5647238492", new RowCallbackHandler() { + public void processRow(ResultSet rs) throws SQLException { + assertEquals("testCustomer", rs.getString("CUSTOMER")); + assertEquals(new BigDecimal(Double.toString(99.69)), rs.getBigDecimal("PRICE")); + assertEquals(5,rs.getLong("QUANTITY")); + } + }); + } +} diff --git a/samples/src/test/java/org/springframework/batch/sample/dao/OrderConverterTests.java b/samples/src/test/java/org/springframework/batch/sample/dao/OrderConverterTests.java new file mode 100644 index 000000000..84eb3dc54 --- /dev/null +++ b/samples/src/test/java/org/springframework/batch/sample/dao/OrderConverterTests.java @@ -0,0 +1,62 @@ +/* + * 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.sample.dao; + +import java.math.BigDecimal; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; + +import junit.framework.TestCase; + +import org.springframework.batch.io.file.support.transform.DelimitedLineAggregator; +import org.springframework.batch.sample.domain.Address; +import org.springframework.batch.sample.domain.BillingInfo; +import org.springframework.batch.sample.domain.Customer; +import org.springframework.batch.sample.domain.Order; + +/** + * @author Dave Syer + * + */ +public class OrderConverterTests extends TestCase { + + private OrderConverter converter = new OrderConverter(); + + public void testConvert() throws Exception { + converter.setAggregators(new HashMap() { + { + put("header", new DelimitedLineAggregator()); + put("customer", new DelimitedLineAggregator()); + put("address", new DelimitedLineAggregator()); + put("billing", new DelimitedLineAggregator()); + put("item", new DelimitedLineAggregator()); + put("footer", new DelimitedLineAggregator()); + } + }); + Order order = new Order(); + order.setOrderDate(new Date()); + order.setCustomer(new Customer()); + order.setBillingAddress(new Address()); + order.setBilling(new BillingInfo()); + order.setLineItems(Collections.EMPTY_LIST); + order.setTotalPrice(BigDecimal.TEN); + Object result = converter.convert(order); + assertTrue(result instanceof Collection); + } + +} diff --git a/samples/src/test/java/org/springframework/batch/sample/mapping/AbstractFieldSetMapperTests.java b/samples/src/test/java/org/springframework/batch/sample/mapping/AbstractFieldSetMapperTests.java new file mode 100644 index 000000000..66d6130af --- /dev/null +++ b/samples/src/test/java/org/springframework/batch/sample/mapping/AbstractFieldSetMapperTests.java @@ -0,0 +1,41 @@ +package org.springframework.batch.sample.mapping; + +import org.springframework.batch.io.file.FieldSet; +import org.springframework.batch.io.file.FieldSetMapper; + +import junit.framework.TestCase; + +/** + * Encapsulates basic logic for testing custom {@link FieldSetMapper} implementations. + * + * @author Robert Kasanicky + */ +public abstract class AbstractFieldSetMapperTests extends TestCase { + + /** + * @return FieldSet used for mapping + */ + protected abstract FieldSet fieldSet(); + + /** + * @return domain object excepted as a result of mapping the FieldSet + * returned by this.fieldSet() + */ + protected abstract Object expectedDomainObject(); + + /** + * @return mapper which takes this.fieldSet() and maps it to + * domain object. + */ + protected abstract FieldSetMapper fieldSetMapper(); + + + /** + * Regular usage scenario. + * Assumes the domain object implements sensible equals(Object other) + */ + public void testRegularUse() { + assertEquals(expectedDomainObject(), fieldSetMapper().mapLine(fieldSet())); + } + +} diff --git a/samples/src/test/java/org/springframework/batch/sample/mapping/AbstractRowMapperTests.java b/samples/src/test/java/org/springframework/batch/sample/mapping/AbstractRowMapperTests.java new file mode 100644 index 000000000..bfbb2b3d4 --- /dev/null +++ b/samples/src/test/java/org/springframework/batch/sample/mapping/AbstractRowMapperTests.java @@ -0,0 +1,51 @@ +package org.springframework.batch.sample.mapping; + +import java.sql.ResultSet; +import java.sql.SQLException; + +import junit.framework.TestCase; + +import org.easymock.MockControl; +import org.springframework.jdbc.core.RowMapper; + +/** + * Encapsulates logic for testing custom {@link RowMapper} implementations. + * + * @author Robert Kasanicky + */ +public abstract class AbstractRowMapperTests extends TestCase { + + //row number should be irrelevant + private static final int IGNORED_ROW_NUMBER = 0; + + //mock result set + private MockControl rsControl = MockControl.createControl(ResultSet.class); + private ResultSet rs = (ResultSet) rsControl.getMock(); + + /** + * @return Expected result of mapping the mock ResultSet by + * the mapper being tested. + */ + abstract protected Object expectedDomainObject(); + + /** + * @return RowMapper implementation that is being tested. + */ + abstract protected RowMapper rowMapper(); + + /** + * Define the behaviour of mock ResultSet. + */ + abstract protected void setUpResultSetMock(ResultSet rs, MockControl rsControl) throws SQLException; + + + /** + * Regular usage scenario. + */ + public void testRegularUse() throws SQLException { + setUpResultSetMock(rs, rsControl); + rsControl.replay(); + + assertEquals(expectedDomainObject(), rowMapper().mapRow(rs, IGNORED_ROW_NUMBER)); + } +} diff --git a/samples/src/test/java/org/springframework/batch/sample/mapping/AddressFieldSetMapperTests.java b/samples/src/test/java/org/springframework/batch/sample/mapping/AddressFieldSetMapperTests.java new file mode 100644 index 000000000..09ed96511 --- /dev/null +++ b/samples/src/test/java/org/springframework/batch/sample/mapping/AddressFieldSetMapperTests.java @@ -0,0 +1,52 @@ +package org.springframework.batch.sample.mapping; + +import org.springframework.batch.io.file.FieldSet; +import org.springframework.batch.io.file.FieldSetMapper; +import org.springframework.batch.sample.domain.Address; +import org.springframework.batch.sample.mapping.AddressFieldSetMapper; + + +public class AddressFieldSetMapperTests extends AbstractFieldSetMapperTests { + + private static final String ADDRESSEE = "Jan Hrach"; + private static final String ADDRESS_LINE_1 = "Plynarenska 7c"; + private static final String ADDRESS_LINE_2 = ""; + private static final String CITY = "Bratislava"; + private static final String STATE = ""; + private static final String COUNTRY = "Slovakia"; + private static final String ZIP_CODE = "80000"; + + + + protected Object expectedDomainObject() { + Address address = new Address(); + address.setAddressee(ADDRESSEE); + address.setAddrLine1(ADDRESS_LINE_1); + address.setAddrLine2(ADDRESS_LINE_2); + address.setCity(CITY); + address.setState(STATE); + address.setCountry(COUNTRY); + address.setZipCode(ZIP_CODE); + return address; + } + + protected FieldSet fieldSet() { + String[] tokens = + new String[]{ADDRESSEE, ADDRESS_LINE_1, ADDRESS_LINE_2, CITY, STATE, COUNTRY, ZIP_CODE}; + String[] columnNames = + new String[]{ + AddressFieldSetMapper.ADDRESSEE_COLUMN, + AddressFieldSetMapper.ADDRESS_LINE1_COLUMN, + AddressFieldSetMapper.ADDRESS_LINE2_COLUMN, + AddressFieldSetMapper.CITY_COLUMN, + AddressFieldSetMapper.STATE_COLUMN, + AddressFieldSetMapper.COUNTRY_COLUMN, + AddressFieldSetMapper.ZIP_CODE_COLUMN }; + + return new FieldSet(tokens, columnNames); + } + + protected FieldSetMapper fieldSetMapper() { + return new AddressFieldSetMapper(); + } +} diff --git a/samples/src/test/java/org/springframework/batch/sample/mapping/BillingFieldSetMapperTests.java b/samples/src/test/java/org/springframework/batch/sample/mapping/BillingFieldSetMapperTests.java new file mode 100644 index 000000000..a3c37f974 --- /dev/null +++ b/samples/src/test/java/org/springframework/batch/sample/mapping/BillingFieldSetMapperTests.java @@ -0,0 +1,34 @@ +package org.springframework.batch.sample.mapping; + +import org.springframework.batch.io.file.FieldSet; +import org.springframework.batch.io.file.FieldSetMapper; +import org.springframework.batch.sample.domain.BillingInfo; +import org.springframework.batch.sample.mapping.BillingFieldSetMapper; + +public class BillingFieldSetMapperTests extends AbstractFieldSetMapperTests{ + + private static final String PAYMENT_ID = "777"; + private static final String PAYMENT_DESC = "My last penny"; + + protected Object expectedDomainObject() { + BillingInfo bInfo = new BillingInfo(); + bInfo.setPaymentDesc(PAYMENT_DESC); + bInfo.setPaymentId(PAYMENT_ID); + return bInfo; + } + + protected FieldSet fieldSet() { + String[] tokens = new String[]{ + PAYMENT_ID, + PAYMENT_DESC}; + String[] columnNames = new String[]{ + BillingFieldSetMapper.PAYMENT_TYPE_ID_COLUMN, + BillingFieldSetMapper.PAYMENT_DESC_COLUMN}; + return new FieldSet(tokens, columnNames); + } + + protected FieldSetMapper fieldSetMapper() { + return new BillingFieldSetMapper(); + } + +} diff --git a/samples/src/test/java/org/springframework/batch/sample/mapping/CustomerCreditRowMapperTests.java b/samples/src/test/java/org/springframework/batch/sample/mapping/CustomerCreditRowMapperTests.java new file mode 100644 index 000000000..e1f6c26ed --- /dev/null +++ b/samples/src/test/java/org/springframework/batch/sample/mapping/CustomerCreditRowMapperTests.java @@ -0,0 +1,35 @@ +package org.springframework.batch.sample.mapping; + +import java.math.BigDecimal; +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.easymock.MockControl; +import org.springframework.batch.sample.domain.CustomerCredit; +import org.springframework.batch.sample.mapping.CustomerCreditRowMapper; +import org.springframework.jdbc.core.RowMapper; + +public class CustomerCreditRowMapperTests extends AbstractRowMapperTests { + + private static final String CUSTOMER = "Jozef Mak"; + private static final BigDecimal CREDIT = new BigDecimal(0.1); + + protected Object expectedDomainObject() { + CustomerCredit credit = new CustomerCredit(); + credit.setCredit(CREDIT); + credit.setName(CUSTOMER); + return credit; + } + + protected RowMapper rowMapper() { + return new CustomerCreditRowMapper(); + } + + protected void setUpResultSetMock(ResultSet rs, MockControl rsControl) throws SQLException { + rs.getString(CustomerCreditRowMapper.NAME_COLUMN); + rsControl.setReturnValue(CUSTOMER); + rs.getBigDecimal(CustomerCreditRowMapper.CREDIT_COLUMN); + rsControl.setReturnValue(CREDIT); + } + +} diff --git a/samples/src/test/java/org/springframework/batch/sample/mapping/CustomerFieldSetMapperTests.java b/samples/src/test/java/org/springframework/batch/sample/mapping/CustomerFieldSetMapperTests.java new file mode 100644 index 000000000..5fa711806 --- /dev/null +++ b/samples/src/test/java/org/springframework/batch/sample/mapping/CustomerFieldSetMapperTests.java @@ -0,0 +1,56 @@ +package org.springframework.batch.sample.mapping; + +import org.springframework.batch.io.file.FieldSet; +import org.springframework.batch.io.file.FieldSetMapper; +import org.springframework.batch.sample.domain.Customer; +import org.springframework.batch.sample.mapping.CustomerFieldSetMapper; + +public class CustomerFieldSetMapperTests extends AbstractFieldSetMapperTests { + + private static final boolean BUSINESS_CUSTOMER = false; + //private static final String COMPANY_NAME = "Accenture"; + private static final String FIRST_NAME = "Jan"; + private static final String LAST_NAME = "Hrach"; + private static final String MIDDLE_NAME = ""; + private static final boolean REGISTERED = true; + private static final long REG_ID = 1; + private static final boolean VIP = true; + + protected Object expectedDomainObject() { + Customer cs = new Customer(); + cs.setBusinessCustomer(BUSINESS_CUSTOMER); + cs.setFirstName(FIRST_NAME); + cs.setLastName(LAST_NAME); + cs.setMiddleName(MIDDLE_NAME); + cs.setRegistered(REGISTERED); + cs.setRegistrationId(REG_ID); + cs.setVip(VIP); + return cs; + } + + protected FieldSet fieldSet() { + String[] tokens = new String[]{ + Customer.LINE_ID_NON_BUSINESS_CUST, + FIRST_NAME, + LAST_NAME, + MIDDLE_NAME, + CustomerFieldSetMapper.TRUE_SYMBOL, + String.valueOf(REG_ID), + CustomerFieldSetMapper.TRUE_SYMBOL}; + String[] columnNames = new String[]{ + CustomerFieldSetMapper.LINE_ID_COLUMN, + CustomerFieldSetMapper.FIRST_NAME_COLUMN, + CustomerFieldSetMapper.LAST_NAME_COLUMN, + CustomerFieldSetMapper.MIDDLE_NAME_COLUMN, + CustomerFieldSetMapper.REGISTERED_COLUMN, + CustomerFieldSetMapper.REG_ID_COLUMN, + CustomerFieldSetMapper.VIP_COLUMN}; + + return new FieldSet(tokens, columnNames); + } + + protected FieldSetMapper fieldSetMapper() { + return new CustomerFieldSetMapper(); + } + +} diff --git a/samples/src/test/java/org/springframework/batch/sample/mapping/HeaderFieldSetMapperTests.java b/samples/src/test/java/org/springframework/batch/sample/mapping/HeaderFieldSetMapperTests.java new file mode 100644 index 000000000..6d3c2733a --- /dev/null +++ b/samples/src/test/java/org/springframework/batch/sample/mapping/HeaderFieldSetMapperTests.java @@ -0,0 +1,41 @@ +package org.springframework.batch.sample.mapping; + +import java.util.Calendar; + +import org.springframework.batch.io.file.FieldSet; +import org.springframework.batch.io.file.FieldSetMapper; +import org.springframework.batch.sample.domain.Order; +import org.springframework.batch.sample.mapping.HeaderFieldSetMapper; + +public class HeaderFieldSetMapperTests extends AbstractFieldSetMapperTests { + + private static final long ORDER_ID = 1; + private static final String DATE = "2007-01-01"; + + protected Object expectedDomainObject() { + Order order = new Order(); + Calendar calendar = Calendar.getInstance(); + calendar.set(2007, 0, 1, 0, 0, 0); + calendar.set(Calendar.MILLISECOND, 0); + order.setOrderDate(calendar.getTime()); + order.setOrderId(ORDER_ID); + return order; + } + + protected FieldSet fieldSet() { + String[] tokens = new String[]{ + String.valueOf(ORDER_ID), + DATE + }; + String[] columnNames = new String[]{ + HeaderFieldSetMapper.ORDER_ID_COLUMN, + HeaderFieldSetMapper.ORDER_DATE_COLUMN + }; + return new FieldSet(tokens, columnNames); + } + + protected FieldSetMapper fieldSetMapper() { + return new HeaderFieldSetMapper(); + } + +} diff --git a/samples/src/test/java/org/springframework/batch/sample/mapping/OrderItemFieldSetMapperTests.java b/samples/src/test/java/org/springframework/batch/sample/mapping/OrderItemFieldSetMapperTests.java new file mode 100644 index 000000000..5005fc560 --- /dev/null +++ b/samples/src/test/java/org/springframework/batch/sample/mapping/OrderItemFieldSetMapperTests.java @@ -0,0 +1,62 @@ +package org.springframework.batch.sample.mapping; + +import java.math.BigDecimal; + +import org.springframework.batch.io.file.FieldSet; +import org.springframework.batch.io.file.FieldSetMapper; +import org.springframework.batch.sample.domain.LineItem; +import org.springframework.batch.sample.mapping.OrderItemFieldSetMapper; + +public class OrderItemFieldSetMapperTests extends AbstractFieldSetMapperTests{ + + private static final BigDecimal DISCOUNT_AMOUNT = new BigDecimal(1); + private static final BigDecimal DISCOUNT_PERC = new BigDecimal(2); + private static final BigDecimal HANDLING_PRICE = new BigDecimal(3); + private static final long ITEM_ID = 4; + private static final BigDecimal PRICE = new BigDecimal(5); + private static final int QUANTITY = 6; + private static final BigDecimal SHIPPING_PRICE = new BigDecimal(7); + private static final BigDecimal TOTAL_PRICE = new BigDecimal(8); + + protected Object expectedDomainObject() { + LineItem item = new LineItem(); + item.setDiscountAmount(DISCOUNT_AMOUNT); + item.setDiscountPerc(DISCOUNT_PERC); + item.setHandlingPrice(HANDLING_PRICE); + item.setItemId(ITEM_ID); + item.setPrice(PRICE); + item.setQuantity(QUANTITY); + item.setShippingPrice(SHIPPING_PRICE); + item.setTotalPrice(TOTAL_PRICE); + return item; + } + + protected FieldSet fieldSet() { + String[] tokens = new String[]{ + String.valueOf(DISCOUNT_AMOUNT), + String.valueOf(DISCOUNT_PERC), + String.valueOf(HANDLING_PRICE), + String.valueOf(ITEM_ID), + String.valueOf(PRICE), + String.valueOf(QUANTITY), + String.valueOf(SHIPPING_PRICE), + String.valueOf(TOTAL_PRICE) + }; + String[] columnNames = new String[]{ + OrderItemFieldSetMapper.DISCOUNT_AMOUNT_COLUMN, + OrderItemFieldSetMapper.DISCOUNT_PERC_COLUMN, + OrderItemFieldSetMapper.HANDLING_PRICE_COLUMN, + OrderItemFieldSetMapper.ITEM_ID_COLUMN, + OrderItemFieldSetMapper.PRICE_COLUMN, + OrderItemFieldSetMapper.QUANTITY_COLUMN, + OrderItemFieldSetMapper.SHIPPING_PRICE_COLUMN, + OrderItemFieldSetMapper.TOTAL_PRICE_COLUMN + }; + return new FieldSet(tokens, columnNames); + } + + protected FieldSetMapper fieldSetMapper() { + return new OrderItemFieldSetMapper(); + } + +} diff --git a/samples/src/test/java/org/springframework/batch/sample/mapping/ShippingFieldSetMapperTests.java b/samples/src/test/java/org/springframework/batch/sample/mapping/ShippingFieldSetMapperTests.java new file mode 100644 index 000000000..526d621b4 --- /dev/null +++ b/samples/src/test/java/org/springframework/batch/sample/mapping/ShippingFieldSetMapperTests.java @@ -0,0 +1,36 @@ +package org.springframework.batch.sample.mapping; + +import org.springframework.batch.io.file.FieldSet; +import org.springframework.batch.io.file.FieldSetMapper; +import org.springframework.batch.sample.domain.ShippingInfo; +import org.springframework.batch.sample.mapping.ShippingFieldSetMapper; + +public class ShippingFieldSetMapperTests extends AbstractFieldSetMapperTests{ + + private static final String SHIPPER_ID = "1"; + private static final String SHIPPING_INFO = "most interesting and informative shipping info ever"; + private static final String SHIPPING_TYPE_ID = "X"; + + protected Object expectedDomainObject() { + ShippingInfo info = new ShippingInfo(); + info.setShipperId(SHIPPER_ID); + info.setShippingInfo(SHIPPING_INFO); + info.setShippingTypeId(SHIPPING_TYPE_ID); + return info; + } + + protected FieldSet fieldSet() { + String[] tokens = new String[]{SHIPPER_ID, SHIPPING_INFO, SHIPPING_TYPE_ID}; + String[] columnNames = new String[]{ + ShippingFieldSetMapper.SHIPPER_ID_COLUMN, + ShippingFieldSetMapper.ADDITIONAL_SHIPPING_INFO_COLUMN, + ShippingFieldSetMapper.SHIPPING_TYPE_ID_COLUMN + }; + return new FieldSet(tokens, columnNames); + } + + protected FieldSetMapper fieldSetMapper() { + return new ShippingFieldSetMapper(); + } + +} diff --git a/samples/src/test/java/org/springframework/batch/sample/mapping/TradeFieldSetMapperTests.java b/samples/src/test/java/org/springframework/batch/sample/mapping/TradeFieldSetMapperTests.java new file mode 100644 index 000000000..6a7e25a2f --- /dev/null +++ b/samples/src/test/java/org/springframework/batch/sample/mapping/TradeFieldSetMapperTests.java @@ -0,0 +1,40 @@ +package org.springframework.batch.sample.mapping; + +import java.math.BigDecimal; + +import org.springframework.batch.io.file.FieldSet; +import org.springframework.batch.io.file.FieldSetMapper; +import org.springframework.batch.sample.domain.Trade; +import org.springframework.batch.sample.mapping.TradeFieldSetMapper; + +public class TradeFieldSetMapperTests extends AbstractFieldSetMapperTests{ + + private static final String CUSTOMER = "Mike Tomcat"; + private static final BigDecimal PRICE = new BigDecimal(1.3); + private static final long QUANTITY = 7; + private static final String ISIN = "fj893gnsalX"; + + protected Object expectedDomainObject() { + Trade trade = new Trade(); + trade.setIsin(ISIN); + trade.setQuantity(QUANTITY); + trade.setPrice(PRICE); + trade.setCustomer(CUSTOMER); + return trade; + } + + protected FieldSet fieldSet() { + String[] tokens = new String[4]; + tokens[TradeFieldSetMapper.ISIN_COLUMN] = ISIN; + tokens[TradeFieldSetMapper.QUANTITY_COLUMN] = String.valueOf(QUANTITY); + tokens[TradeFieldSetMapper.PRICE_COLUMN] = String.valueOf(PRICE); + tokens[TradeFieldSetMapper.CUSTOMER_COLUMN] = CUSTOMER; + + return new FieldSet(tokens); + } + + protected FieldSetMapper fieldSetMapper() { + return new TradeFieldSetMapper(); + } + +} diff --git a/samples/src/test/java/org/springframework/batch/sample/mapping/TradeRowMapperTests.java b/samples/src/test/java/org/springframework/batch/sample/mapping/TradeRowMapperTests.java new file mode 100644 index 000000000..8c2b5d65c --- /dev/null +++ b/samples/src/test/java/org/springframework/batch/sample/mapping/TradeRowMapperTests.java @@ -0,0 +1,46 @@ +package org.springframework.batch.sample.mapping; + +import java.math.BigDecimal; +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.easymock.MockControl; +import org.springframework.batch.sample.domain.Trade; +import org.springframework.batch.sample.mapping.TradeRowMapper; +import org.springframework.jdbc.core.RowMapper; + +public class TradeRowMapperTests extends AbstractRowMapperTests { + + private static final String ISIN = "jsgk342"; + private static final long QUANTITY = 0; + private static final BigDecimal PRICE = new BigDecimal(1.1); + private static final String CUSTOMER = "Martin Hrancok"; + + protected Object expectedDomainObject() { + Trade trade = new Trade(); + trade.setIsin(ISIN); + trade.setQuantity(QUANTITY); + trade.setPrice(PRICE); + trade.setCustomer(CUSTOMER); + return trade; + } + + protected RowMapper rowMapper() { + return new TradeRowMapper(); + } + + protected void setUpResultSetMock(ResultSet rs, MockControl rsControl) throws SQLException { + rs.getString(TradeRowMapper.ISIN_COLUMN); + rsControl.setReturnValue(ISIN); + + rs.getLong(TradeRowMapper.QUANTITY_COLUMN); + rsControl.setReturnValue(QUANTITY); + + rs.getBigDecimal(TradeRowMapper.PRICE_COLUMN); + rsControl.setReturnValue(PRICE); + + rs.getString(TradeRowMapper.CUSTOMER_COLUMN); + rsControl.setReturnValue(CUSTOMER); + } + +} diff --git a/samples/src/test/java/org/springframework/batch/sample/module/CollectionItemProviderTest.java b/samples/src/test/java/org/springframework/batch/sample/module/CollectionItemProviderTest.java new file mode 100644 index 000000000..ed1b09f3e --- /dev/null +++ b/samples/src/test/java/org/springframework/batch/sample/module/CollectionItemProviderTest.java @@ -0,0 +1,72 @@ +package org.springframework.batch.sample.module; + +import java.util.Collection; +import java.util.Iterator; + +import junit.framework.TestCase; + +import org.easymock.MockControl; +import org.springframework.batch.io.file.FieldSet; +import org.springframework.batch.io.file.FieldSetInputSource; +import org.springframework.batch.io.file.FieldSetMapper; + +public class CollectionItemProviderTest extends TestCase { + + private MockControl inputControl; + private FieldSetInputSource input; + private MockControl mapperControl; + private FieldSetMapper mapper; + private CollectionItemProvider provider; + + public void setUp() { + + //create mock for input + inputControl = MockControl.createControl(FieldSetInputSource.class); + input = (FieldSetInputSource) inputControl.getMock(); + + //create mock for mapper + mapperControl = MockControl.createControl(FieldSetMapper.class); + mapper = (FieldSetMapper) mapperControl.getMock(); + mapperControl.setDefaultMatcher(MockControl.ALWAYS_MATCHER); + mapper.mapLine(null); + mapperControl.setDefaultReturnValue("line"); + mapperControl.replay(); + + //create provider + provider = new CollectionItemProvider(); + provider.setInputSource(input); + provider.setFieldSetMapper(mapper); + } + + public void testNext() { + + //set-up mock input + input.readFieldSet(); + inputControl.setReturnValue(new FieldSet(new String[] {"BEGIN"})); + input.readFieldSet(); + inputControl.setReturnValue(new FieldSet(new String[] {"line"}),3); + input.readFieldSet(); + inputControl.setReturnValue(new FieldSet(new String[] {"END"})); + input.readFieldSet(); + inputControl.setReturnValue(null); + inputControl.replay(); + + //read object + Object result = provider.next(); + + //it should be collection od 3 strings "line" + assertTrue(result instanceof Collection); + Collection lines = (Collection)result; + assertEquals(3, lines.size()); + + for (Iterator i = lines.iterator(); i.hasNext();) { + assertEquals("line", i.next()); + } + + //read object again - it should return null + assertNull(provider.next()); + + //verify method calls + inputControl.verify(); + } +} diff --git a/samples/src/test/java/org/springframework/batch/sample/module/ExceptionRestartableTaskletTests.java b/samples/src/test/java/org/springframework/batch/sample/module/ExceptionRestartableTaskletTests.java new file mode 100644 index 000000000..884ccf78a --- /dev/null +++ b/samples/src/test/java/org/springframework/batch/sample/module/ExceptionRestartableTaskletTests.java @@ -0,0 +1,64 @@ +package org.springframework.batch.sample.module; + +import java.util.ArrayList; + +import junit.framework.TestCase; + +import org.easymock.MockControl; +import org.springframework.batch.io.exception.BatchCriticalException; +import org.springframework.batch.item.ItemProcessor; +import org.springframework.batch.item.provider.ListItemProvider; +import org.springframework.batch.repeat.context.RepeatContextSupport; +import org.springframework.batch.repeat.synch.RepeatSynchronizationManager; + +public class ExceptionRestartableTaskletTests extends TestCase { + + //expected call count before exception is thrown (exception should be thrown in next iteration) + private static final int ITER_COUNT = 5; + + protected void tearDown() throws Exception { + RepeatSynchronizationManager.clear(); + } + + public void testProcess() throws Exception { + + //create mock item processor wich will be called by module.process() method + MockControl processorControl = MockControl.createControl(ItemProcessor.class); + ItemProcessor itemProcessor = (ItemProcessor)processorControl.getMock(); + + //set expected call count and argument matcher + itemProcessor.process(null); + processorControl.setMatcher(MockControl.ALWAYS_MATCHER); + processorControl.setVoidCallable(ITER_COUNT); + processorControl.replay(); + + //create module and set item processor and iteration count + ExceptionRestartableTasklet module = new ExceptionRestartableTasklet(); + module.setItemProcessor(itemProcessor); + module.setThrowExceptionOnRecordNumber(ITER_COUNT + 1); + + module.setItemProvider(new ListItemProvider(new ArrayList() {{ + add("a"); + add("b"); + add("c"); + add("d"); + add("e"); + add("f"); + }})); + + RepeatSynchronizationManager.register(new RepeatContextSupport(null)); + + //call process method multiple times and verify whether exception is thrown when expected + for (int i = 0; i <= ITER_COUNT; i++) { + try { + module.execute(); + assertTrue(i < ITER_COUNT); + } catch (BatchCriticalException bce) { + assertEquals(ITER_COUNT,i); + } + } + + //verify method calls + processorControl.verify(); + } +} diff --git a/samples/src/test/java/org/springframework/batch/sample/module/OrderItemProviderTests.java b/samples/src/test/java/org/springframework/batch/sample/module/OrderItemProviderTests.java new file mode 100644 index 000000000..ff662eedd --- /dev/null +++ b/samples/src/test/java/org/springframework/batch/sample/module/OrderItemProviderTests.java @@ -0,0 +1,162 @@ +package org.springframework.batch.sample.module; + +import java.util.Iterator; + +import junit.framework.TestCase; + +import org.easymock.MockControl; +import org.springframework.batch.io.file.FieldSet; +import org.springframework.batch.io.file.FieldSetInputSource; +import org.springframework.batch.io.file.FieldSetMapper; +import org.springframework.batch.item.validator.Validator; +import org.springframework.batch.sample.domain.Address; +import org.springframework.batch.sample.domain.BillingInfo; +import org.springframework.batch.sample.domain.Customer; +import org.springframework.batch.sample.domain.LineItem; +import org.springframework.batch.sample.domain.Order; +import org.springframework.batch.sample.domain.ShippingInfo; + +public class OrderItemProviderTests extends TestCase { + + private OrderItemProvider provider; + private MockControl inputControl; + private FieldSetInputSource input; + private MockControl mapperControl; + private FieldSetMapper mapper; + private MockControl validatorControl; + private Validator validator; + + public void setUp() { + + inputControl = MockControl.createControl(FieldSetInputSource.class); + input = (FieldSetInputSource)inputControl.getMock(); + + provider = new OrderItemProvider(); + provider.setInputSource(input); + } + + /* + * OrderItemProvider is resposible for retrieving validated value object from input source. + * OrderItemProvider.next(): + * - reads lines from the input source - returned as fieldsets + * - pass fieldsets to the mapper - mapper will create value object + * - pass value object to validator + * - returns validated object + * + * In testNext method we are going to test these responsibilities. So we need create mock + * objects for input source, mapper and validator. + */ + public void testNext() { + + //create fieldsets and set return values for input source + FieldSet headerFS = new FieldSet(new String[] {Order.LINE_ID_HEADER}); + FieldSet customerFS = new FieldSet(new String[] {Customer.LINE_ID_NON_BUSINESS_CUST}); + FieldSet billingFS = new FieldSet(new String[] {Address.LINE_ID_BILLING_ADDR}); + FieldSet shippingFS = new FieldSet(new String[] {Address.LINE_ID_SHIPPING_ADDR}); + FieldSet billingInfoFS = new FieldSet(new String[] {BillingInfo.LINE_ID_BILLING_INFO}); + FieldSet shippingInfoFS = new FieldSet(new String[] {ShippingInfo.LINE_ID_SHIPPING_INFO}); + FieldSet itemFS = new FieldSet(new String[] {LineItem.LINE_ID_ITEM}); + FieldSet footerFS = new FieldSet(new String[] {Order.LINE_ID_FOOTER, "100","3","3"}, + new String[] {"ID","TOTAL_PRICE","TOTAL_LINE_ITEMS","TOTAL_ITEMS"}); + + input.readFieldSet(); + inputControl.setReturnValue(headerFS); + input.readFieldSet(); + inputControl.setReturnValue(customerFS); + input.readFieldSet(); + inputControl.setReturnValue(billingFS); + input.readFieldSet(); + inputControl.setReturnValue(shippingFS); + input.readFieldSet(); + inputControl.setReturnValue(billingInfoFS); + input.readFieldSet(); + inputControl.setReturnValue(shippingInfoFS); + input.readFieldSet(); + inputControl.setReturnValue(itemFS,3); + input.readFieldSet(); + inputControl.setReturnValue(footerFS); + input.readFieldSet(); + inputControl.setReturnValue(null); + inputControl.replay(); + + //create value objects + Order order = new Order(); + Customer customer = new Customer(); + Address billing = new Address(); + Address shipping = new Address(); + BillingInfo billingInfo = new BillingInfo(); + ShippingInfo shippingInfo = new ShippingInfo(); + LineItem item = new LineItem(); + + //create mock mapper + mapperControl = MockControl.createControl(FieldSetMapper.class); + mapper = (FieldSetMapper)mapperControl.getMock(); + //set how mapper should respond - set return values for mapper + mapper.mapLine(headerFS); + mapperControl.setReturnValue(order); + mapper.mapLine(customerFS); + mapperControl.setReturnValue(customer); + mapper.mapLine(billingFS); + mapperControl.setReturnValue(billing); + mapper.mapLine(shippingFS); + mapperControl.setReturnValue(shipping); + mapper.mapLine(billingInfoFS); + mapperControl.setReturnValue(billingInfo); + mapper.mapLine(shippingInfoFS); + mapperControl.setReturnValue(shippingInfo); + mapper.mapLine(itemFS); + mapperControl.setReturnValue(item,3); + mapperControl.replay(); + + //create mock validator + validatorControl = MockControl.createControl(Validator.class); + validator = (Validator)validatorControl.getMock(); + validator.validate(null); + validatorControl.setMatcher(MockControl.ALWAYS_MATCHER); + validatorControl.setVoidCallable(1); + validatorControl.replay(); + + //set-up provider: set mappers and validator + provider.setValidator(validator); + provider.setAddressMapper(mapper); + provider.setBillingMapper(mapper); + provider.setCustomerMapper(mapper); + provider.setHeaderMapper(mapper); + provider.setItemMapper(mapper); + provider.setShippingMapper(mapper); + + //call tested method + Object result = provider.next(); + + //verify result + assertNotNull(result); + //result should be Order + assertTrue(result instanceof Order); + + //verify whether order is constructed correctly + //Order object should contain same instances as returned by mapper + Order o = (Order) result; + assertEquals(o,order); + assertEquals(o.getCustomer(),customer); + //is it non-bussines customer + assertFalse(o.getCustomer().isBusinessCustomer()); + assertEquals(o.getBillingAddress(),billing); + assertEquals(o.getShippingAddress(),shipping); + assertEquals(o.getBilling(),billingInfo); + assertEquals(o.getShipping(), shippingInfo); + //there should be 3 line items + assertEquals(3, o.getLineItems().size()); + for (Iterator i = o.getLineItems().iterator(); i.hasNext();) { + assertEquals(i.next(),item); + } + + //try to retrieve next object - nothing should be returned + assertNull(provider.next()); + + //verify method calls on input source, mapper and validator + inputControl.verify(); + mapperControl.verify(); + validatorControl.verify(); + } + +} diff --git a/samples/src/test/java/org/springframework/batch/sample/module/SimpleTradeTaskletTests.java b/samples/src/test/java/org/springframework/batch/sample/module/SimpleTradeTaskletTests.java new file mode 100644 index 000000000..a4f62c817 --- /dev/null +++ b/samples/src/test/java/org/springframework/batch/sample/module/SimpleTradeTaskletTests.java @@ -0,0 +1,69 @@ +package org.springframework.batch.sample.module; + +import java.math.BigDecimal; + +import junit.framework.TestCase; + +import org.springframework.batch.io.file.FieldSet; +import org.springframework.batch.io.file.support.DefaultFlatFileInputSource; +import org.springframework.batch.sample.dao.TradeWriter; +import org.springframework.batch.sample.domain.Trade; + +public class SimpleTradeTaskletTests extends TestCase { + + private boolean inputCalled = false; + private boolean writerCalled = false; + + public void testReadAndProcess() { + + //create input + DefaultFlatFileInputSource input = new DefaultFlatFileInputSource() { + + private boolean done = false; + + public FieldSet readFieldSet() { + if (!done) { + FieldSet fs = new FieldSet(new String[] {"1234","5","100","testName"}, + new String[] {"ISIN", "quantity", "price", "customer"}); + inputCalled = true; + done = true; + return fs; + } else { + return null; + } + } + }; + + //create writer + TradeWriter writer = new TradeWriter() { + public void writeTrade(Trade trade) { + assertEquals("1234",trade.getIsin()); + assertEquals(5, trade.getQuantity()); + assertEquals(new BigDecimal(100), trade.getPrice()); + assertEquals("testName", trade.getCustomer()); + writerCalled = true; + } + public void write(Object output) {} + public void close() {} + public void open() {} + }; + + //create module + SimpleTradeTasklet module = new SimpleTradeTasklet(); + module.setInputSource(input); + module.setTradeDao(writer); + + //call tested methods + //read method should return true, because input returned fieldset + assertTrue(module.read()); + //call process method - see asserts in writer.writeTrade() + module.process(); + + //verify whether input and writer were called + assertTrue(inputCalled); + assertTrue(writerCalled); + + //read should return false, because input returned null + assertFalse(module.read()); + } +} diff --git a/samples/src/test/java/org/springframework/batch/sample/module/SkipSampleItemProviderTests.java b/samples/src/test/java/org/springframework/batch/sample/module/SkipSampleItemProviderTests.java new file mode 100644 index 000000000..c2a793521 --- /dev/null +++ b/samples/src/test/java/org/springframework/batch/sample/module/SkipSampleItemProviderTests.java @@ -0,0 +1,63 @@ +package org.springframework.batch.sample.module; + +import junit.framework.TestCase; + +import org.easymock.MockControl; +import org.springframework.batch.io.exception.TransactionInvalidException; +import org.springframework.batch.io.file.FieldSetInputSource; +import org.springframework.batch.io.file.FieldSetMapper; + +public class SkipSampleItemProviderTests extends TestCase { + + private MockControl inputControl; + private FieldSetInputSource input; + private MockControl mapperControl; + private FieldSetMapper mapper; + private SkipSampleItemProvider provider; + + private static final int ITER_COUNT = 7; + + public void setUp() { + + inputControl = MockControl.createControl(FieldSetInputSource.class); + input = (FieldSetInputSource)inputControl.getMock(); + + mapperControl = MockControl.createControl(FieldSetMapper.class); + mapper = (FieldSetMapper)mapperControl.getMock(); + + provider = new SkipSampleItemProvider(); + provider.setInputSource(input); + provider.setFieldSetMapper(mapper); + } + + public void testNext() { + + //set-up mock input + input.readFieldSet(); + inputControl.setReturnValue(null,ITER_COUNT); + inputControl.replay(); + + //set-up mock mapper + mapper.mapLine(null); + mapperControl.setReturnValue("line",ITER_COUNT); + mapperControl.replay(); + + //set exception iteration count + provider.setThrowExceptionOnRecordNumber(ITER_COUNT + 1); + + //call next() method multiple times and verify whether exception is thrown when expected + for (int i = 0; i <= ITER_COUNT; i++) { + try { + assertEquals("line", provider.next()); + assertTrue(i < ITER_COUNT); + } catch (TransactionInvalidException tie) { + assertEquals(ITER_COUNT,i); + } + } + + //verify method calls + inputControl.verify(); + mapperControl.verify(); + } + +} diff --git a/samples/src/test/java/org/springframework/batch/sample/module/process/CustomerCreditUpdateProcessorTests.java b/samples/src/test/java/org/springframework/batch/sample/module/process/CustomerCreditUpdateProcessorTests.java new file mode 100644 index 000000000..f2c474da8 --- /dev/null +++ b/samples/src/test/java/org/springframework/batch/sample/module/process/CustomerCreditUpdateProcessorTests.java @@ -0,0 +1,56 @@ +package org.springframework.batch.sample.module.process; + +import java.math.BigDecimal; + +import junit.framework.TestCase; + +import org.easymock.MockControl; +import org.springframework.batch.sample.dao.CustomerCreditWriter; +import org.springframework.batch.sample.domain.CustomerCredit; + +public class CustomerCreditUpdateProcessorTests extends TestCase { + + private MockControl writerControl; + private CustomerCreditWriter writer; + private CustomerCreditUpdateProcessor processor; + private static final double CREDIT_FILTER = 355.0; + + public void setUp() { + //create mock writer + writerControl = MockControl.createControl(CustomerCreditWriter.class); + writer = (CustomerCreditWriter) writerControl.getMock(); + //create processor, set writer and credit filter + processor = new CustomerCreditUpdateProcessor(); + processor.setWriter(writer); + processor.setCreditFilter(CREDIT_FILTER); + } + + public void testProcess() { + + //set-up mock writer - no writer's method should be called + writerControl.replay(); + + //create credit and set it to same value as credit filter + CustomerCredit credit = new CustomerCredit(); + credit.setCredit(new BigDecimal(CREDIT_FILTER)); + //call tested method + processor.process(credit); + //verify method calls - no method should be called + //because credit is not greater then credit filter + writerControl.verify(); + + //change credit to be greater than credit filter + credit.setCredit(new BigDecimal(CREDIT_FILTER + 1)); + //reset and set-up writer - write method is expected to be called + writerControl.reset(); + writer.write(credit); + writerControl.replay(); + + //call tested method + processor.process(credit); + + //verify method calls + writerControl.verify(); + } + +} diff --git a/samples/src/test/java/org/springframework/batch/sample/module/process/CustomerUpdateProcessorTests.java b/samples/src/test/java/org/springframework/batch/sample/module/process/CustomerUpdateProcessorTests.java new file mode 100644 index 000000000..59f79ffce --- /dev/null +++ b/samples/src/test/java/org/springframework/batch/sample/module/process/CustomerUpdateProcessorTests.java @@ -0,0 +1,35 @@ +package org.springframework.batch.sample.module.process; + +import java.math.BigDecimal; + +import junit.framework.TestCase; + +import org.springframework.batch.sample.dao.JdbcCustomerDebitWriter; +import org.springframework.batch.sample.domain.CustomerDebit; +import org.springframework.batch.sample.domain.Trade; + +public class CustomerUpdateProcessorTests extends TestCase { + + public void testProcess() { + + //create trade object + Trade trade = new Trade(); + trade.setCustomer("testCustomerName"); + trade.setPrice(new BigDecimal(123.0)); + + //create dao + JdbcCustomerDebitWriter dao = new JdbcCustomerDebitWriter() { + public void write(CustomerDebit customerDebit) { + assertEquals("testCustomerName", customerDebit.getName()); + assertEquals(new BigDecimal(123.0), customerDebit.getDebit()); + } + }; + + //create processor and set dao + CustomerUpdateProcessor processor = new CustomerUpdateProcessor(); + processor.setDao(dao); + + //call tested method - see asserts in dao.write() method + processor.process(trade); + } +} diff --git a/samples/src/test/java/org/springframework/batch/sample/module/process/DefaultFlatFileProcessorTests.java b/samples/src/test/java/org/springframework/batch/sample/module/process/DefaultFlatFileProcessorTests.java new file mode 100644 index 000000000..6a307ae91 --- /dev/null +++ b/samples/src/test/java/org/springframework/batch/sample/module/process/DefaultFlatFileProcessorTests.java @@ -0,0 +1,27 @@ +package org.springframework.batch.sample.module.process; + +import junit.framework.TestCase; + +import org.springframework.batch.io.file.support.FlatFileOutputSource; + +public class DefaultFlatFileProcessorTests extends TestCase { + + public void testProcess() throws Exception { + + final Object testLine = new Object(); + + //create output source + FlatFileOutputSource output = new FlatFileOutputSource() { + public void write(Object line) { + assertEquals(""+testLine, line); + } + }; + + //create processor and set output source + DefaultFlatFileProcessor processor = new DefaultFlatFileProcessor(); + processor.setFlatFileOutputSource(output); + + //call tested method - see assert in output.write() method + processor.process(testLine); + } +} diff --git a/samples/src/test/java/org/springframework/batch/sample/module/process/OrderProcessorTests.java b/samples/src/test/java/org/springframework/batch/sample/module/process/OrderProcessorTests.java new file mode 100644 index 000000000..4be708a14 --- /dev/null +++ b/samples/src/test/java/org/springframework/batch/sample/module/process/OrderProcessorTests.java @@ -0,0 +1,53 @@ +package org.springframework.batch.sample.module.process; + +import junit.framework.TestCase; + +import org.easymock.MockControl; +import org.springframework.batch.io.exception.BatchCriticalException; +import org.springframework.batch.sample.dao.OrderWriter; +import org.springframework.batch.sample.domain.Order; + +public class OrderProcessorTests extends TestCase { + + private MockControl writerControl; + private OrderWriter writer; + private OrderProcessor processor; + + public void setUp() { + + //create mock writer + writerControl = MockControl.createControl(OrderWriter.class); + writer = (OrderWriter)writerControl.getMock(); + + //create processor + processor = new OrderProcessor(); + processor.setWriter(writer); + } + + public void testProcess() { + + Order order = new Order(); + //set-up mock writer + writer.write(order); + writerControl.replay(); + + //call tested method + processor.process(order); + + //verify method calls + writerControl.verify(); + } + + public void testProcessWithException() { + + writerControl.replay(); + //call tested method + try { + processor.process(this); + fail("Batch critical exception was expected"); + } catch (BatchCriticalException bce) { + assertTrue(true); + } + writerControl.verify(); + } +} diff --git a/samples/src/test/java/org/springframework/batch/sample/module/process/TradeProcessorTests.java b/samples/src/test/java/org/springframework/batch/sample/module/process/TradeProcessorTests.java new file mode 100644 index 000000000..9b2d1dc2e --- /dev/null +++ b/samples/src/test/java/org/springframework/batch/sample/module/process/TradeProcessorTests.java @@ -0,0 +1,47 @@ +package org.springframework.batch.sample.module.process; + +import junit.framework.TestCase; + +import org.easymock.MockControl; +import org.springframework.batch.sample.dao.TradeWriter; +import org.springframework.batch.sample.domain.Trade; + +public class TradeProcessorTests extends TestCase { + + private MockControl writerControl; + private TradeWriter writer; + private TradeProcessor processor; + + public void setUp() { + + //create mock writer + writerControl = MockControl.createControl(TradeWriter.class); + writer = (TradeWriter)writerControl.getMock(); + + //create processor + processor = new TradeProcessor(); + processor.setWriter(writer); + } + + public void testProcess() { + + Trade trade = new Trade(); + //set-up mock writer + writer.writeTrade(trade); + writerControl.replay(); + + //call tested method + processor.process(trade); + + //verify method calls + writerControl.verify(); + } + + public void testProcessNonTradeObject() { + + writerControl.replay(); + //call tested method + processor.process(this); + writerControl.verify(); + } +} diff --git a/samples/src/test/java/org/springframework/batch/sample/validation/valang/custom/FutureDateFunctionTests.java b/samples/src/test/java/org/springframework/batch/sample/validation/valang/custom/FutureDateFunctionTests.java new file mode 100644 index 000000000..f94d6abaa --- /dev/null +++ b/samples/src/test/java/org/springframework/batch/sample/validation/valang/custom/FutureDateFunctionTests.java @@ -0,0 +1,61 @@ +package org.springframework.batch.sample.validation.valang.custom; + +import java.util.Date; + +import org.easymock.MockControl; +import org.springmodules.validation.valang.functions.Function; +import junit.framework.TestCase; + +public class FutureDateFunctionTests extends TestCase { + + private FutureDateFunction function; + private MockControl argumentControl; + private Function argument; + + public void setUp() { + argumentControl = MockControl.createControl(Function.class); + argument = (Function) argumentControl.getMock(); + + //create function + function = new FutureDateFunction(new Function[] {argument}, 0, 0); + } + + public void testFunctionWithNonDateValue() { + + //set-up mock argument - set return value to non Date value + argument.getResult(null); + argumentControl.setReturnValue(this); + argumentControl.replay(); + + //call tested method - exception is expected because non date value + try { + function.doGetResult(null); + fail("Exception was expected."); + } catch (Exception e) { + assertTrue(true); + } + } + + public void testFunctionWithFutureDate() throws Exception { + + //set-up mock argument - set return value to future Date + argument.getResult(null); + argumentControl.setReturnValue(new Date(Long.MAX_VALUE)); + argumentControl.replay(); + + //vefify result - should be true because of future date + assertTrue(((Boolean)function.doGetResult(null)).booleanValue()); + } + + public void testFunctionWithPastDate() throws Exception { + + //set-up mock argument - set return value to future Date + argument.getResult(null); + argumentControl.setReturnValue(new Date(0)); + argumentControl.replay(); + + //vefify result - should be false because of past date + assertFalse(((Boolean)function.doGetResult(null)).booleanValue()); + + } +} diff --git a/samples/src/test/java/org/springframework/batch/sample/validation/valang/custom/TotalOrderItemsFunctionTests.java b/samples/src/test/java/org/springframework/batch/sample/validation/valang/custom/TotalOrderItemsFunctionTests.java new file mode 100644 index 000000000..a3cacea9c --- /dev/null +++ b/samples/src/test/java/org/springframework/batch/sample/validation/valang/custom/TotalOrderItemsFunctionTests.java @@ -0,0 +1,82 @@ +package org.springframework.batch.sample.validation.valang.custom; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.batch.sample.domain.LineItem; + +import org.easymock.MockControl; +import org.springmodules.validation.valang.functions.Function; +import junit.framework.TestCase; + +public class TotalOrderItemsFunctionTests extends TestCase { + + private TotalOrderItemsFunction function; + private MockControl argument1Control; + private Function argument1; + private MockControl argument2Control; + private Function argument2; + + public void setUp() { + //create mock for first argument - set count to 3 + argument1Control = MockControl.createControl(Function.class); + argument1 = (Function) argument1Control.getMock(); + argument1.getResult(null); + argument1Control.setReturnValue(new Integer(3)); + argument1Control.replay(); + + argument2Control = MockControl.createControl(Function.class); + argument2 = (Function) argument2Control.getMock(); + + //create function + function = new TotalOrderItemsFunction(new Function[] {argument1, argument2}, 0, 0); + } + + public void testFunctionWithNonListValue() { + + argument2.getResult(null); + argument2Control.setReturnValue(this); + argument2Control.replay(); + + //call tested method - exception is expected because non list value + try { + function.doGetResult(null); + fail("Exception was expected."); + } catch (Exception e) { + assertTrue(true); + } + } + + public void testFunctionWithCorrectItemCount() throws Exception { + + //create list with correct item count + LineItem item = new LineItem(); + item.setQuantity(3); + List list = new ArrayList(); + list.add(item); + + argument2.getResult(null); + argument2Control.setReturnValue(list); + argument2Control.replay(); + + //vefify result + assertTrue(((Boolean)function.doGetResult(null)).booleanValue()); + } + + public void testFunctionWithIncorrectItemCount() throws Exception { + + //create list with incorrect item count + LineItem item = new LineItem(); + item.setQuantity(99); + List list = new ArrayList(); + list.add(item); + + argument2.getResult(null); + argument2Control.setReturnValue(list); + argument2Control.replay(); + + //vefify result + assertFalse(((Boolean)function.doGetResult(null)).booleanValue()); + } + +} diff --git a/samples/src/test/java/org/springframework/batch/sample/validation/valang/custom/ValidateDiscountsFunctionTests.java b/samples/src/test/java/org/springframework/batch/sample/validation/valang/custom/ValidateDiscountsFunctionTests.java new file mode 100644 index 000000000..6f2b2fee0 --- /dev/null +++ b/samples/src/test/java/org/springframework/batch/sample/validation/valang/custom/ValidateDiscountsFunctionTests.java @@ -0,0 +1,166 @@ +package org.springframework.batch.sample.validation.valang.custom; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +import org.springframework.batch.sample.domain.LineItem; + +import org.easymock.MockControl; +import org.springmodules.validation.valang.functions.Function; +import junit.framework.TestCase; + +public class ValidateDiscountsFunctionTests extends TestCase { + + private ValidateDiscountsFunction function; + private MockControl argumentControl; + private Function argument; + + public void setUp() { + argumentControl = MockControl.createControl(Function.class); + argument = (Function) argumentControl.getMock(); + + //create function + function = new ValidateDiscountsFunction(new Function[] {argument}, 0, 0); + } + + public void testDiscountPercentageMin() throws Exception { + + //create line item with correct discount percentage and zero discount amount + LineItem item = new LineItem(); + item.setDiscountPerc(new BigDecimal(1.0)); + item.setDiscountAmount(new BigDecimal(0.0)); + + //add it to line items list + List items = new ArrayList(); + items.add(item); + + //set return value for mock argument + argument.getResult(null); + argumentControl.setReturnValue(items,2); + argumentControl.replay(); + + //verify result - should be true - all discount percentages are correct + assertTrue(((Boolean)function.doGetResult(null)).booleanValue()); + + //now add line item with negative percentage + item = new LineItem(); + item.setDiscountPerc(new BigDecimal(-1.0)); + item.setDiscountAmount(new BigDecimal(0.0)); + items.add(item); + + //verify result - should be false - second item has invalid discount percentage + assertFalse(((Boolean)function.doGetResult(null)).booleanValue()); + } + + public void testDiscountPercentageMax() throws Exception { + + //create line item with correct discount percentage and zero discount amount + LineItem item = new LineItem(); + item.setDiscountPerc(new BigDecimal(99.0)); + item.setDiscountAmount(new BigDecimal(0.0)); + + //add it to line items list + List items = new ArrayList(); + items.add(item); + + //set return value for mock argument + argument.getResult(null); + argumentControl.setReturnValue(items,2); + argumentControl.replay(); + + //verify result - should be true - all discount percentages are correct + assertTrue(((Boolean)function.doGetResult(null)).booleanValue()); + + //now add line item with discount percentage above 100 + item = new LineItem(); + item.setDiscountPerc(new BigDecimal(101.0)); + item.setDiscountAmount(new BigDecimal(0.0)); + items.add(item); + + //verify result - should be false - second item has invalid discount percentage + assertFalse(((Boolean)function.doGetResult(null)).booleanValue()); + } + + public void testDiscountPriceMin() throws Exception { + + //create line item with correct discount amount and zero discount percentage + LineItem item = new LineItem(); + item.setDiscountPerc(new BigDecimal(0.0)); + item.setDiscountAmount(new BigDecimal(10.0)); + item.setPrice(new BigDecimal(100.0)); + + //add it to line items list + List items = new ArrayList(); + items.add(item); + + //set return value for mock argument + argument.getResult(null); + argumentControl.setReturnValue(items,2); + argumentControl.replay(); + + //verify result - should be true - all discount amounts are correct + assertTrue(((Boolean)function.doGetResult(null)).booleanValue()); + + //now add line item with negative discount amount + item = new LineItem(); + item.setDiscountPerc(new BigDecimal(0.0)); + item.setDiscountAmount(new BigDecimal(-1.0)); + item.setPrice(new BigDecimal(100.0)); + items.add(item); + + //verify result - should be false - second item has invalid discount amount + assertFalse(((Boolean)function.doGetResult(null)).booleanValue()); + } + + public void testDiscountPriceMax() throws Exception { + + //create line item with correct discount amount and zero discount percentage + LineItem item = new LineItem(); + item.setDiscountPerc(new BigDecimal(0.0)); + item.setDiscountAmount(new BigDecimal(99.0)); + item.setPrice(new BigDecimal(100.0)); + + //add it to line items list + List items = new ArrayList(); + items.add(item); + + //set return value for mock argument + argument.getResult(null); + argumentControl.setReturnValue(items,2); + argumentControl.replay(); + + //verify result - should be true - all discount amounts are correct + assertTrue(((Boolean)function.doGetResult(null)).booleanValue()); + + //now add line item with discount amount above item price + item = new LineItem(); + item.setDiscountPerc(new BigDecimal(0.0)); + item.setDiscountAmount(new BigDecimal(101.0)); + item.setPrice(new BigDecimal(100.0)); + items.add(item); + + //verify result - should be false - second item has invalid discount amount + assertFalse(((Boolean)function.doGetResult(null)).booleanValue()); + } + + public void testBothDiscountValuesNonZero() throws Exception { + + //create line item with non-zero discount amount and non-zero discount percentage + LineItem item = new LineItem(); + item.setDiscountPerc(new BigDecimal(10.0)); + item.setDiscountAmount(new BigDecimal(99.0)); + + //add it to line items list + List items = new ArrayList(); + items.add(item); + + //set return value for mock argument + argument.getResult(null); + argumentControl.setReturnValue(items); + argumentControl.replay(); + + //verify result - should be false - only one of the discount values is empty + assertFalse(((Boolean)function.doGetResult(null)).booleanValue()); + } +} diff --git a/samples/src/test/java/org/springframework/batch/sample/validation/valang/custom/ValidateHandlingPricesFunctionTests.java b/samples/src/test/java/org/springframework/batch/sample/validation/valang/custom/ValidateHandlingPricesFunctionTests.java new file mode 100644 index 000000000..31ccab808 --- /dev/null +++ b/samples/src/test/java/org/springframework/batch/sample/validation/valang/custom/ValidateHandlingPricesFunctionTests.java @@ -0,0 +1,81 @@ +package org.springframework.batch.sample.validation.valang.custom; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +import org.easymock.MockControl; + +import org.springframework.batch.sample.domain.LineItem; + +import org.springmodules.validation.valang.functions.Function; +import junit.framework.TestCase; + +public class ValidateHandlingPricesFunctionTests extends TestCase { + + private ValidateHandlingPricesFunction function; + private MockControl argumentControl; + private Function argument; + + public void setUp() { + argumentControl = MockControl.createControl(Function.class); + argument = (Function) argumentControl.getMock(); + + //create function + function = new ValidateHandlingPricesFunction(new Function[] {argument}, 0, 0); + } + + public void testHandlingPriceMin() throws Exception { + + //create line item with correct handling price + LineItem item = new LineItem(); + item.setHandlingPrice(new BigDecimal(1.0)); + + //add it to line items list + List items = new ArrayList(); + items.add(item); + + //set return value for mock argument + argument.getResult(null); + argumentControl.setReturnValue(items,2); + argumentControl.replay(); + + //verify result - should be true - all handling prices are correct + assertTrue(((Boolean)function.doGetResult(null)).booleanValue()); + + //now add line item with negative handling price + item = new LineItem(); + item.setHandlingPrice(new BigDecimal(-1.0)); + items.add(item); + + //verify result - should be false - second item has invalid handling price + assertFalse(((Boolean)function.doGetResult(null)).booleanValue()); + } + + public void testHandlingPriceMax() throws Exception { + + //create line item with correct handling price + LineItem item = new LineItem(); + item.setHandlingPrice(new BigDecimal(99999999.0)); + + //add it to line items list + List items = new ArrayList(); + items.add(item); + + //set return value for mock argument + argument.getResult(null); + argumentControl.setReturnValue(items,2); + argumentControl.replay(); + + //verify result - should be true - all handling prices are correct + assertTrue(((Boolean)function.doGetResult(null)).booleanValue()); + + //now add line item with handling price above allowed max + item = new LineItem(); + item.setHandlingPrice(new BigDecimal(100000000.0)); + items.add(item); + + //verify result - should be false - second item has invalid handling price + assertFalse(((Boolean)function.doGetResult(null)).booleanValue()); + } +} diff --git a/samples/src/test/java/org/springframework/batch/sample/validation/valang/custom/ValidateIdsFunctionTests.java b/samples/src/test/java/org/springframework/batch/sample/validation/valang/custom/ValidateIdsFunctionTests.java new file mode 100644 index 000000000..6819cf526 --- /dev/null +++ b/samples/src/test/java/org/springframework/batch/sample/validation/valang/custom/ValidateIdsFunctionTests.java @@ -0,0 +1,80 @@ +package org.springframework.batch.sample.validation.valang.custom; + +import java.util.ArrayList; +import java.util.List; + +import junit.framework.TestCase; +import org.easymock.MockControl; + +import org.springframework.batch.sample.domain.LineItem; + +import org.springmodules.validation.valang.functions.Function; + +public class ValidateIdsFunctionTests extends TestCase { + + private ValidateIdsFunction function; + private MockControl argumentControl; + private Function argument; + + public void setUp() { + argumentControl = MockControl.createControl(Function.class); + argument = (Function) argumentControl.getMock(); + + //create function + function = new ValidateIdsFunction(new Function[] {argument}, 0, 0); + } + + public void testIdMin() throws Exception { + + //create line item with correct item id + LineItem item = new LineItem(); + item.setItemId(1); + + //add it to line items list + List items = new ArrayList(); + items.add(item); + + //set return value for mock argument + argument.getResult(null); + argumentControl.setReturnValue(items,2); + argumentControl.replay(); + + //verify result - should be true - all ids are correct + assertTrue(((Boolean)function.doGetResult(null)).booleanValue()); + + //now add line item with negative id + item = new LineItem(); + item.setItemId(-1); + items.add(item); + + //verify result - should be false - second item has invalid id + assertFalse(((Boolean)function.doGetResult(null)).booleanValue()); + } + + public void testIdMax() throws Exception { + + //create line item with correct item id + LineItem item = new LineItem(); + item.setItemId(9999999999L); + + //add it to line items list + List items = new ArrayList(); + items.add(item); + + //set return value for mock argument + argument.getResult(null); + argumentControl.setReturnValue(items,2); + argumentControl.replay(); + + //verify result - should be true - all item ids are correct + assertTrue(((Boolean)function.doGetResult(null)).booleanValue()); + + //now add line item with item id above allowed max + item = new LineItem(); + item.setItemId(10000000000L); + items.add(item); + + //verify result - should be false - second item has invalid item id + assertFalse(((Boolean)function.doGetResult(null)).booleanValue()); + } +} diff --git a/samples/src/test/java/org/springframework/batch/sample/validation/valang/custom/ValidatePricesFunctionTests.java b/samples/src/test/java/org/springframework/batch/sample/validation/valang/custom/ValidatePricesFunctionTests.java new file mode 100644 index 000000000..e73832332 --- /dev/null +++ b/samples/src/test/java/org/springframework/batch/sample/validation/valang/custom/ValidatePricesFunctionTests.java @@ -0,0 +1,82 @@ +package org.springframework.batch.sample.validation.valang.custom; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +import org.easymock.MockControl; + +import org.springframework.batch.sample.domain.LineItem; + +import org.springmodules.validation.valang.functions.Function; +import junit.framework.TestCase; + +public class ValidatePricesFunctionTests extends TestCase { + + private ValidatePricesFunction function; + private MockControl argumentControl; + private Function argument; + + public void setUp() { + argumentControl = MockControl.createControl(Function.class); + argument = (Function) argumentControl.getMock(); + + //create function + function = new ValidatePricesFunction(new Function[] {argument}, 0, 0); + } + + public void testItemPriceMin() throws Exception { + + //create line item with correct item price + LineItem item = new LineItem(); + item.setPrice(new BigDecimal(1.0)); + + //add it to line items list + List items = new ArrayList(); + items.add(item); + + //set return value for mock argument + argument.getResult(null); + argumentControl.setReturnValue(items,2); + argumentControl.replay(); + + //verify result - should be true - all item prices are correct + assertTrue(((Boolean)function.doGetResult(null)).booleanValue()); + + //now add line item with negative item price + item = new LineItem(); + item.setPrice(new BigDecimal(-1.0)); + items.add(item); + + //verify result - should be false - second item has invalid item price + assertFalse(((Boolean)function.doGetResult(null)).booleanValue()); + } + + public void testItemPriceMax() throws Exception { + + //create line item with correct item price + LineItem item = new LineItem(); + item.setPrice(new BigDecimal(99999999.0)); + + //add it to line items list + List items = new ArrayList(); + items.add(item); + + //set return value for mock argument + argument.getResult(null); + argumentControl.setReturnValue(items,2); + argumentControl.replay(); + + //verify result - should be true - all item prices are correct + assertTrue(((Boolean)function.doGetResult(null)).booleanValue()); + + //now add line item with item price above allowed max + item = new LineItem(); + item.setPrice(new BigDecimal(100000000.0)); + items.add(item); + + //verify result - should be false - second item has invalid item price + assertFalse(((Boolean)function.doGetResult(null)).booleanValue()); + } + +} diff --git a/samples/src/test/java/org/springframework/batch/sample/validation/valang/custom/ValidateQuantitiesFunctionTests.java b/samples/src/test/java/org/springframework/batch/sample/validation/valang/custom/ValidateQuantitiesFunctionTests.java new file mode 100644 index 000000000..836d0f51a --- /dev/null +++ b/samples/src/test/java/org/springframework/batch/sample/validation/valang/custom/ValidateQuantitiesFunctionTests.java @@ -0,0 +1,80 @@ +package org.springframework.batch.sample.validation.valang.custom; + +import java.util.ArrayList; +import java.util.List; + +import org.easymock.MockControl; + +import org.springframework.batch.sample.domain.LineItem; + +import org.springmodules.validation.valang.functions.Function; +import junit.framework.TestCase; + +public class ValidateQuantitiesFunctionTests extends TestCase { + + private ValidateQuantitiesFunction function; + private MockControl argumentControl; + private Function argument; + + public void setUp() { + argumentControl = MockControl.createControl(Function.class); + argument = (Function) argumentControl.getMock(); + + //create function + function = new ValidateQuantitiesFunction(new Function[] {argument}, 0, 0); + } + + public void testQuantityMin() throws Exception { + + //create line item with correct item quantity + LineItem item = new LineItem(); + item.setQuantity(1); + + //add it to line items list + List items = new ArrayList(); + items.add(item); + + //set return value for mock argument + argument.getResult(null); + argumentControl.setReturnValue(items,2); + argumentControl.replay(); + + //verify result - should be true - all quantities are correct + assertTrue(((Boolean)function.doGetResult(null)).booleanValue()); + + //now add line item with negative quantity + item = new LineItem(); + item.setQuantity(-1); + items.add(item); + + //verify result - should be false - second item has invalid quantity + assertFalse(((Boolean)function.doGetResult(null)).booleanValue()); + } + + public void testQuantityMax() throws Exception { + + //create line item with correct item quantity + LineItem item = new LineItem(); + item.setQuantity(9999); + + //add it to line items list + List items = new ArrayList(); + items.add(item); + + //set return value for mock argument + argument.getResult(null); + argumentControl.setReturnValue(items,2); + argumentControl.replay(); + + //verify result - should be true - all item quantities are correct + assertTrue(((Boolean)function.doGetResult(null)).booleanValue()); + + //now add line item with item quantity above allowed max + item = new LineItem(); + item.setQuantity(10000); + items.add(item); + + //verify result - should be false - second item has invalid item quantity + assertFalse(((Boolean)function.doGetResult(null)).booleanValue()); + } +} diff --git a/samples/src/test/java/org/springframework/batch/sample/validation/valang/custom/ValidateShippingPricesFunctionTests.java b/samples/src/test/java/org/springframework/batch/sample/validation/valang/custom/ValidateShippingPricesFunctionTests.java new file mode 100644 index 000000000..51b1d1c41 --- /dev/null +++ b/samples/src/test/java/org/springframework/batch/sample/validation/valang/custom/ValidateShippingPricesFunctionTests.java @@ -0,0 +1,81 @@ +package org.springframework.batch.sample.validation.valang.custom; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +import org.easymock.MockControl; + +import org.springframework.batch.sample.domain.LineItem; + +import org.springmodules.validation.valang.functions.Function; +import junit.framework.TestCase; + +public class ValidateShippingPricesFunctionTests extends TestCase { + + private ValidateShippingPricesFunction function; + private MockControl argumentControl; + private Function argument; + + public void setUp() { + argumentControl = MockControl.createControl(Function.class); + argument = (Function) argumentControl.getMock(); + + //create function + function = new ValidateShippingPricesFunction(new Function[] {argument}, 0, 0); + } + + public void testShippingPriceMin() throws Exception { + + //create line item with correct shipping price + LineItem item = new LineItem(); + item.setShippingPrice(new BigDecimal(1.0)); + + //add it to line items list + List items = new ArrayList(); + items.add(item); + + //set return value for mock argument + argument.getResult(null); + argumentControl.setReturnValue(items,2); + argumentControl.replay(); + + //verify result - should be true - all shipping prices are correct + assertTrue(((Boolean)function.doGetResult(null)).booleanValue()); + + //now add line item with negative shipping price + item = new LineItem(); + item.setShippingPrice(new BigDecimal(-1.0)); + items.add(item); + + //verify result - should be false - second item has invalid shipping price + assertFalse(((Boolean)function.doGetResult(null)).booleanValue()); + } + + public void testShippingPriceMax() throws Exception { + + //create line item with correct shipping price + LineItem item = new LineItem(); + item.setShippingPrice(new BigDecimal(99999999.0)); + + //add it to line items list + List items = new ArrayList(); + items.add(item); + + //set return value for mock argument + argument.getResult(null); + argumentControl.setReturnValue(items,2); + argumentControl.replay(); + + //verify result - should be true - all shipping prices are correct + assertTrue(((Boolean)function.doGetResult(null)).booleanValue()); + + //now add line item with shipping price above allowed max + item = new LineItem(); + item.setShippingPrice(new BigDecimal(100000000.0)); + items.add(item); + + //verify result - should be false - second item has invalid shipping price + assertFalse(((Boolean)function.doGetResult(null)).booleanValue()); + } +} diff --git a/samples/src/test/java/org/springframework/batch/sample/validation/valang/custom/ValidateTotalPricesFunctionTests.java b/samples/src/test/java/org/springframework/batch/sample/validation/valang/custom/ValidateTotalPricesFunctionTests.java new file mode 100644 index 000000000..3ef27e2cf --- /dev/null +++ b/samples/src/test/java/org/springframework/batch/sample/validation/valang/custom/ValidateTotalPricesFunctionTests.java @@ -0,0 +1,133 @@ +package org.springframework.batch.sample.validation.valang.custom; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +import org.easymock.MockControl; + +import org.springframework.batch.sample.domain.LineItem; + +import org.springmodules.validation.valang.functions.Function; +import junit.framework.TestCase; + +public class ValidateTotalPricesFunctionTests extends TestCase { + + private ValidateTotalPricesFunction function; + private MockControl argumentControl; + private Function argument; + + public void setUp() { + argumentControl = MockControl.createControl(Function.class); + argument = (Function) argumentControl.getMock(); + + //create function + function = new ValidateTotalPricesFunction(new Function[] {argument}, 0, 0); + } + + public void testTotalPriceMin() throws Exception { + + //create line item with correct total price + LineItem item = new LineItem(); + item.setDiscountAmount(new BigDecimal(0.0)); + item.setDiscountPerc(new BigDecimal(0.0)); + item.setHandlingPrice(new BigDecimal(0.0)); + item.setShippingPrice(new BigDecimal(0.0)); + item.setPrice(new BigDecimal(1.0)); + item.setQuantity(1); + item.setTotalPrice(new BigDecimal(1.0)); + + //add it to line items list + List items = new ArrayList(); + items.add(item); + + //set return value for mock argument + argument.getResult(null); + argumentControl.setReturnValue(items,2); + argumentControl.replay(); + + //verify result - should be true - all total prices are correct + assertTrue(((Boolean)function.doGetResult(null)).booleanValue()); + + //now add line item with negative item price + item = new LineItem(); + item.setTotalPrice(new BigDecimal(-1.0)); + items.add(item); + + //verify result - should be false - second item has invalid total price + assertFalse(((Boolean)function.doGetResult(null)).booleanValue()); + } + + public void testTotalPriceMax() throws Exception { + + //create line item with correct total price + LineItem item = new LineItem(); + item.setDiscountAmount(new BigDecimal(0.0)); + item.setDiscountPerc(new BigDecimal(0.0)); + item.setHandlingPrice(new BigDecimal(0.0)); + item.setShippingPrice(new BigDecimal(0.0)); + item.setPrice(new BigDecimal(99999999.0)); + item.setQuantity(1); + item.setTotalPrice(new BigDecimal(99999999.0)); + + //add it to line items list + List items = new ArrayList(); + items.add(item); + + //set return value for mock argument + argument.getResult(null); + argumentControl.setReturnValue(items,2); + argumentControl.replay(); + + //verify result - should be true - all total prices are correct + assertTrue(((Boolean)function.doGetResult(null)).booleanValue()); + + //now add line item with total price above allowed max + item = new LineItem(); + item.setTotalPrice(new BigDecimal(100000000.0)); + items.add(item); + + //verify result - should be false - second item has invalid total price + assertFalse(((Boolean)function.doGetResult(null)).booleanValue()); + } + + public void testTotalPriceCalculation() throws Exception { + + //create line item + LineItem item = new LineItem(); + item.setDiscountAmount(new BigDecimal(5.0)); + item.setDiscountPerc(new BigDecimal(0.0)); + item.setHandlingPrice(new BigDecimal(1.0)); + item.setShippingPrice(new BigDecimal(2.0)); + item.setPrice(new BigDecimal(250.0)); + item.setQuantity(1); + item.setTotalPrice(new BigDecimal(248.0)); + + //add it to line items list + List items = new ArrayList(); + items.add(item); + + //set return value for mock argument + argument.getResult(null); + argumentControl.setReturnValue(items,2); + argumentControl.replay(); + + //verify result - should be true - all total prices are correct + assertTrue(((Boolean)function.doGetResult(null)).booleanValue()); + + //now add line item with incorrect total price + item = new LineItem(); + item.setDiscountAmount(new BigDecimal(5.0)); + item.setDiscountPerc(new BigDecimal(0.0)); + item.setHandlingPrice(new BigDecimal(1.0)); + item.setShippingPrice(new BigDecimal(2.0)); + item.setPrice(new BigDecimal(250.0)); + item.setQuantity(1); + item.setTotalPrice(new BigDecimal(253.0)); + + items.add(item); + + //verify result - should be false - second item has incorrect total price + assertFalse(((Boolean)function.doGetResult(null)).booleanValue()); + } +} diff --git a/samples/src/test/java/test/jdbc/datasource/InitializingDataSourceFactoryBean.java b/samples/src/test/java/test/jdbc/datasource/InitializingDataSourceFactoryBean.java new file mode 100644 index 000000000..ff6b0bc9a --- /dev/null +++ b/samples/src/test/java/test/jdbc/datasource/InitializingDataSourceFactoryBean.java @@ -0,0 +1,156 @@ +/* + * 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 test.jdbc.datasource; + +import java.io.IOException; +import java.util.Iterator; +import java.util.List; + +import javax.sql.DataSource; + +import org.apache.commons.io.IOUtils; +import org.springframework.beans.factory.BeanInitializationException; +import org.springframework.beans.factory.config.AbstractFactoryBean; +import org.springframework.core.io.Resource; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.TransactionCallback; +import org.springframework.transaction.support.TransactionTemplate; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +public class InitializingDataSourceFactoryBean extends AbstractFactoryBean { + + private Resource[] initScripts; + + private Resource destroyScript; + + DataSource dataSource; + + private static boolean initialized = false; + + /** + * @throws Throwable + * @see java.lang.Object#finalize() + */ + protected void finalize() throws Throwable { + super.finalize(); + initialized = false; + logger.debug("finalize called"); + } + + protected void destroyInstance(Object instance) throws Exception { + try { + doExecuteScript(destroyScript); + } + catch (Exception e) { + if (logger.isDebugEnabled()) { + logger.warn("Could not execute destroy script [" + destroyScript + "]", e); + } + else { + logger.warn("Could not execute destroy script [" + destroyScript + "]"); + } + } + } + + public void afterPropertiesSet() throws Exception { + Assert.notNull(dataSource); + super.afterPropertiesSet(); + } + + protected Object createInstance() throws Exception { + Assert.notNull(dataSource); + if (!initialized) { + try { + doExecuteScript(destroyScript); + } + catch (Exception e) { + logger.debug("Could not execute destroy script [" + destroyScript + "]", e); + } + if (initScripts != null) { + for (int i = 0; i < initScripts.length; i++) { + Resource initScript = initScripts[i]; + doExecuteScript(initScript); + } + } + initialized = true; + } + return dataSource; + } + + private void doExecuteScript(final Resource scriptResource) { + if (scriptResource == null || !scriptResource.exists()) + return; + TransactionTemplate transactionTemplate = new TransactionTemplate(new DataSourceTransactionManager(dataSource)); + transactionTemplate.execute(new TransactionCallback() { + + public Object doInTransaction(TransactionStatus status) { + JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); + String[] scripts; + try { + scripts = StringUtils.delimitedListToStringArray(stripComments(IOUtils.readLines(scriptResource + .getInputStream())), ";"); + } + catch (IOException e) { + throw new BeanInitializationException("Cannot load script from [" + scriptResource + "]", e); + } + for (int i = 0; i < scripts.length; i++) { + String script = scripts[i].trim(); + if (StringUtils.hasText(script)) { + jdbcTemplate.execute(scripts[i]); + } + } + return null; + } + + }); + + } + + private String stripComments(List list) { + StringBuffer buffer = new StringBuffer(); + for (Iterator iter = list.iterator(); iter.hasNext();) { + String line = (String) iter.next(); + if (!line.startsWith("//") && !line.startsWith("--")) { + buffer.append(line + "\n"); + } + } + return buffer.toString(); + } + + public Class getObjectType() { + return DataSource.class; + } + + public void setInitScript(Resource initScript) { + this.initScripts = new Resource[] { initScript }; + } + + public void setInitScripts(Resource[] initScripts) { + this.initScripts = initScripts; + } + + public void setDestroyScript(Resource destroyScript) { + this.destroyScript = destroyScript; + } + + public void setDataSource(DataSource dataSource) { + this.dataSource = dataSource; + } + +} diff --git a/spring-batch.iml b/spring-batch.iml new file mode 100644 index 000000000..9f553394e --- /dev/null +++ b/spring-batch.iml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + diff --git a/spring-batch.ipr b/spring-batch.ipr new file mode 100644 index 000000000..66a409c98 --- /dev/null +++ b/spring-batch.ipr @@ -0,0 +1,304 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-batch.iws b/spring-batch.iws new file mode 100644 index 000000000..541a811e1 --- /dev/null +++ b/spring-batch.iws @@ -0,0 +1,672 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-eclipse-code-conventions.xml b/spring-eclipse-code-conventions.xml new file mode 100644 index 000000000..c32c487e4 --- /dev/null +++ b/spring-eclipse-code-conventions.xml @@ -0,0 +1,251 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/.project b/src/.project new file mode 100644 index 000000000..380d6eec5 --- /dev/null +++ b/src/.project @@ -0,0 +1,11 @@ + + + batch-master + + + + + + + + diff --git a/src/site/apt/batch-principles-guidelines.apt b/src/site/apt/batch-principles-guidelines.apt new file mode 100644 index 000000000..eecbde577 --- /dev/null +++ b/src/site/apt/batch-principles-guidelines.apt @@ -0,0 +1,40 @@ + ------ + Spring Batch Overview + ------ + Scott Wintermute + ------ + May 2007 + +General Principles and Guidelines for Batch Architectures + + The following are a number of key principles, guidelines, and general considerations to take into consideration when building a batch solution. + + * A batch architecture typically affects on-line architecture and vice versa. Design with both architectures and environments in mind using common building blocks when possible. + + * Simplify as much as possible and avoid building complex logical structures in single batch applications. + + * Process data as close to where the data physically resides as possible or vice versa (i.e., keep your data where your processing occurs). + + * Minimize system resource use, especially I/O. Perform as many operations as possible in internal memory. + + * Review application I/O (analyze SQL statements) to ensure that unnecessary physical I/O is avoided. In particular, the following four common flaws need to be looked for: + + ** Reading data for every transaction when the data could be read once and kept cached or in the working storage; + + ** Rereading data for a transaction where the data was read earlier in the same transaction; + + ** Causing unnecessary table or index scans; + + ** Not specifying key values in the WHERE clause of an SQL statement. + + * Do not do things twice in a batch run. For instance, if you need data summarization for reporting purposes, increment stored totals if possible when data is being initially processed, so your reporting application does not have to reprocess the same data. + + * Allocate enough memory at the beginning of a batch application to avoid time-consuming reallocation during the process. + + * Always assume the worst with regard to data integrity. Insert adequate checks and record validation to maintain data integrity. + + * Implement checksums for internal validation where possible. For example, flat files should have a trailer record telling the total of records in the file and an aggregate of the key fields. + + * Plan and execute stress tests as early as possible in a production-like environment with realistic data volumes. + + * In large batch systems backups can be challenging, especially if the system is running concurrent with on-line on a 24-7 basis. Database backups are typically well taken care of in the on-line design, but file backups should be considered to be just as important. If the system depends on flat files, file backup procedures should not only be in place and documented, but regularly tested as well. diff --git a/src/site/apt/batch-processing-strategies.apt b/src/site/apt/batch-processing-strategies.apt new file mode 100644 index 000000000..233959d77 --- /dev/null +++ b/src/site/apt/batch-processing-strategies.apt @@ -0,0 +1,196 @@ + ------ + Batch Processing Strategy + ------ + Scott Wintermute, Wayne Lund + ------ + May 2007 + +Batch Processing Strategies + + To help design and implement batch systems, basic batch application building blocks and patterns should be provided to the designers and programmers in form of sample structure charts and code shells. When starting to design a batch job, the business logic should be decomposed into a series of steps which can be implemented using the following standard building blocks: + + * <> For each type of file supplied by or generated to an external system, a conversion application will need to be created to convert the transaction records supplied into a standard format required for processing. This type of batch application can partly or entirely consist of translation utility modules (see Basic Batch Services). + + * <> Validation applications ensure that all input/output records are correct and consistent. Validation is typically based on file headers and trailers, checksums and validation algorithms as well as record level cross-checks. + + * <> An application that reads a set of records from a database or input file, selects records based on predefined rules, and writes the records to an output file. + + * <> An application that reads records from a database or an input file, and makes changes to a database or an output file driven by the data found in each input record. + + * <> An application that performs processing on input transactions from an extract or a validation application. The processing will usually involve reading a database to obtain data required for processing, potentially updating the database and creating records for output processing. + + * <> Applications reading an input file, restructures data from this record according to a standard format, and produces an output file for printing or transmission to another program or system. + + Additionally a basic application shell should be provided for business logic that cannot be built using the previously mentioned building blocks. + + In addition to the main building blocks, each application may use one or more of standard utility steps, such as: + + * Sort - A Program that reads an input file and produces an output file where records have been re-sequenced according to a sort key field in the records. Sorts are usually performed by standard system utilities. + + * Split - A program that reads a single input file, and writes each record to one of several output files based on a field value. Splits can be tailored or performed by parameter-driven standard system utilities. + + * Merge - A program that reads records from multiple input files and produces one output file with combined data from the input files. Merges can be tailored or performed by parameter-driven standard system utilities. + + Batch applications can additionally be categorized by their input source: + + * Database-driven applications are driven by rows or values retrieved from the database. + + * File-driven applications are driven by records or values retrieved from a file. + + * Message-driven applications are driven by messages retrieved from a message queue. + + The foundation of any batch system is the processing strategy. Factors affecting the selection of the strategy include: estimated batch system volume, concurrency with on-line or with another batch systems, available batch windows (and with more enterprises wanting to be up and running 24x7, this leaves no obvious batch windows). + + Typical processing options for batch are: + + * Normal processing in a batch window during off-line + + * Concurrent batch / on-line processing + + * Parallel processing of many different batch runs or jobs at the same time + + * Partitioning (i.e. processing of many instances of the same job at the same time) + + * A combination of these + + The order in the list above reflects the implementation complexity, processing in a batch window being the easiest and partitioning the most complex to implement. + + Some or all of these options may be supported by a commercial scheduler. + + In the following section these processing options are discussed in more detail. It is important to notice that the commit and locking strategy adopted by batch processes will be dependent on the type of processing performed, and as a rule of thumb and the on-line locking strategy should also use the same principles. Therefore, the batch architecture cannot be simply an afterthought when designing an overall architecture. + + The locking strategy can use only normal database locks, or an additional custom locking service can be implemented in the architecture. The locking service would track database locking (for example by storing the necessary information in a dedicated db-table) and give or deny permissions to the application programs requesting a db operation. Retry logic could also be implemented by this architecture to avoid aborting a batch job in case of a lock situation. + + <<1. Normal processing in a batch window>> + For simple batch processes running in a separate batch window, where the data being updated is not required by on-line users or other batch processes, concurrency is not an issue and a single commit can be done at the end of the batch run. + + In most cases a more robust approach is more appropriate. A thing to keep in mind is that batch systems have a tendency to grow as time goes by, both in terms of complexity and the data volumes they will handle. If no locking strategy is in place and the system still relies on a single commit point, modifying the batch programs can be painful. Therefore, even with the simplest batch systems, consider the need for commit logic for restart-recovery options as well as the information concerning the more complex cases below. + + <<2. Concurrent batch / on-line processing>> + Batch applications processing data that can simultaneously be updated by on-line users, should not lock any data (either in the database or in files) which could be required by on-line users for more than a few seconds. Also updates should be committed to the database at the end of every few transaction. This minimizes the portion of data that is unavailable to other processes and the elapsed time the data is unavailable. + + Another option to minimize physical locking is to have a logical row-level locking implemented using either an Optimistic Locking Pattern or a Pessimistic Locking Pattern. + + * Optimistic locking assumes a low likelihood of record contention. It typically means inserting a timestamp column in each database table used concurrently by both batch and on-line processing. When an application fetches a row for processing, it also fetches the timestamp. As the application then tries to update the processed row, the update uses the original timestamp in the WHERE clause. If the timestamp matches, the data and the timestamp will be updated successfully. If the timestamp does not match, this indicates that another application has updated the same row between the fetch and the update attempt and therefore the update cannot be performed. + + * Pessimistic locking is any locking strategy that assumes there is a high likelihood of record contention and therefore either a physical or logical lock needs to be obtained at retrieval time. One type of pessimistic logical locking uses a dedicated lock-column in the database table. When an application retrieves the row for update, it sets a flag in the lock column. With the flag in place, other applications attempting to retrieve the same row will logically fail. When the application that set the flag updates the row, it also clears the flag, enabling the row to be retrieved by other applications. Please note, that the integrity of data must be maintained also between the initial fetch and the setting of the flag, for example by using db locks (e.g., SELECT FOR UPDATE). Note also that this method suffers from the same downside as physical locking except that it is somewhat easier to manage building a time-out mechanism that will get the lock released if the user goes to lunch while the record is locked. + + These patterns are not necessarily suitable for batch processing, but they might be used for concurrent batch and on-line processing (e.g. in cases where the database doesn't support row-level locking). As a general rule, optimistic locking is more suitable for on-line applications, while pessimistic locking is more suitable for batch applications. Whenever logical locking is used, the same scheme must be used for all applications accessing data entities protected by logical locks. + + Note that both of these solutions only address locking a single record. Often we may need to lock a logically related group of records. With physical locks, you have to manage these very carefully in order to avoid potential deadlocks. With logical locks, it is usually best to build a logical lock manager that understands the logical record groups you want to protect and can ensure that locks are coherent and non-deadlocking. This logical lock manager usually uses its own tables for lock management, contention reporting, time-out mechanism, etc. + + <<3. Parallel Processing>> + Parallel processing allows multiple batch runs / jobs to run in parallel to minimize the total elapsed batch processing time. This is not a problem as long as the jobs are not sharing the same files, db-tables or index spaces. If they do, this service should be implemented using partitioned data. Another option is to build an architecture module for maintaining interdependencies using a control table. A control table should contain a row for each shared resource and whether it is in use by an application or not. The batch architecture or the application in a parallel job would then retrieve information from that table to determine if it can get access to the resource it needs or not. + + If the data access is not a problem, parallel processing can be implemented through the use of additional threads to process in parallel. In the mainframe environment, parallel job classes have traditionally been used, in order to ensure adequate CPU time for all the processes. Regardless, the solution has to be robust enough to ensure time slices for all the running processes. + + Other key issues in parallel processing include load balancing and the availability of general system resources such as files, database buffer pools etc. Also note that the control table itself can easily become a critical resource. + + <<4. Partitioning>> + Using partitioning allows multiple versions of large batch applications to run concurrently. The purpose of this is to reduce the elapsed time required to process long batch jobs. Processes which can be successfully partitioned are those where the input file can be split and/or the main database tables partitioned to allow the application to run against different sets of data. + + In addition, processes which are partitioned must be designed to only process their assigned data set. A partitioning architecture has to be closely tied to the database design and the database partitioning strategy. Please note, that the database partitioning doesn't necessarily mean physical partitioning of the database, although in most cases this is advisable. The following picture illustrates the partitioning approach: + +[images/partitioned.png] + + + The architecture should be flexible enough to allow dynamic configuration of the number of partitions. Both automatic and user controlled configuration should be considered. Automatic configuration may be based on parameters such as the input file size and/or the number of input records. + + <<4.1 Partitioning Approaches>> + The following lists some of the possible partitioning approaches. Selecting a partitioning approach has to be done on a case-by-case basis. + + <1. Fixed and Even Break-Up of Record Set> + + This involves breaking the input record set into an even number of portions (e.g. 10, where each portion will have exactly 1/10th of the entire record set). Each portion is then processed by one instance of the batch/extract application. + + In order to use this approach, preprocessing will be required to split the recordset up. The result of this split will be a lower and upper bound placement number which can be used as input to the batch/extract application in order to restrict its processing to its portion alone. + + Preprocessing could be a large overhead as it has to calculate and determine the bounds of each portion of the record set. + + <2. Breakup by a Key Column> + + This involves breaking up the input record set by a key column such as a location code, and assigning data from each key to a batch instance. In order to achieve this, column values can either be + + <3. Assigned to a batch instance via a partitioning table (see below for details).> + + <4. Assigned to a batch instance by a portion of the value (e.g. values 0000-0999, 1000 - 1999, etc.)> + + Under option 1, addition of new values will mean a manual reconfiguration of the batch/extract to ensure that the new value is added to a particular instance. + + Under option 2, this will ensure that all values are covered via an instance of the batch job. However, the number of values processed by one instance is dependent on the distribution of column values (i.e. there may be a large number of locations in the 0000-0999 range, and few in the 1000-1999 range). Under this option, the data range should be designed with partitioning in mind. + + Under both options, the optimal even distribution of records to batch instances cannot be realized. There is no dynamic configuration of the number of batch instances used. + + <5. Breakup by Views> + + This approach is basically breakup by a key column, but on the database level. It involves breaking up the recordset into views. These views will be used by each instance of the batch application during its processing. The breakup will be done by grouping the data. + + With this option, each instance of a batch application will have to be configured to hit a particular view (instead of the master table). Also, with the addition of new data values, this new group of data will have to be included into a view. There is no dynamic configuration capability, as a change in the number of instances will result in a change to the views. + + <6. Addition of a Processing Indicator> + + This involves the addition of a new column to the input table, which acts as an indicator. As a preprocessing step, all indicators would be marked to non-processed. During the record fetch stage of the batch application, records are read on the condition that that record is marked non-processed, and once they are read (with lock), they are marked processing. When that record is completed, the indicator is updated to either complete or error. Many instances of a batch application can be started without a change, as the additional column ensures that a record is only processed once. + + With this option, I/O on the table increases dynamically. In the case of an updating batch application, this impact is reduced, as a write will have to occur anyway. + + <7. Extract Table to a Flat File> + + This involves the extraction of the table into a file. This file can then be split into multiple segments and used as input to the batch instances. + + With this option, the additional overhead of extracting the table into a file, and splitting it, may cancel out the effect of multi-partitioning. Dynamic configuration can be achieved via changing the file splitting script. + + <8. Use of a Hashing Column> + + This scheme involves the addition of a hash column (key/index) to the database tables used to retrieve the driver record. This hash column will have an indicator to determine which instance of the batch application will process this particular row. For example, if there are three batch instances to be started, then an indicator of 'A' will mark that row for processing by instance 1, an indicator of 'B' will mark that row for processing by instance 2, etc. + + The procedure used to retrieve the records would then have an additional WHERE clause to select all rows marked by a particular indicator. The inserts in this table would involve the addition of the marker field, which would be defaulted to one of the instances (e.g. 'A'). + + A simple batch application would be used to update the indicators such as to redistribute the load between the different instances. When a sufficiently large number of new rows have been added, this batch can be run (anytime, except in the batch window) to redistribute the new rows to other instances. + + Additional instances of the batch application only require the running of the batch application as above to redistribute the indicators to cater for a new number of instances. + + + <<4.2 Database and Application design Principles>> + + An architecture that supports multi-partitioned applications which run against partitioned database tables using the key column approach, should include a central partition repository for storing partition parameters. This provides flexibility and ensures maintainability. The repository will generally consist of a single table known as the partition table. + + Information stored in the partition table will be static and in general should be maintained by the DBA. The table should consist of one row of information for each partition of a multi-partitioned application. The table should have columns for: Program ID Code, Partition Number (Logical ID of the partition), Low Value of the db key column for this partition, High Value of the db key column for this partition. + + On program start-up the program id and partition number should be passed to the application from the architecture (Control Processing Tasklet). These variables are used to read the partition table, to determine what range of data the application is to process (if a key column approach is used). In addition the partition number must be used throughout the processing to: + + * Add to the output files/database updates in order for the merge process to work properly + + * Report normal processing to the batch log and any errors that occur during execution to the architecture error handler + + <<4.3 Minimizing Deadlocks>> + When applications run in parallel or partitioned, contention in database resources and deadlocks may occur. It is critical that the database design team eliminates potential contention situations as far as possible as part of the database design. + + Also ensure that the database index tables are designed with deadlock prevention and performance in mind. + + Deadlocks or hot spots often occur in administration or architecture tables such as log tables, control tables, and lock tables. The implications of these should be taken into account as well. A realistic stress test is crucial for identifying the possible bottlenecks in the architecture. + + To minimize the impact of conflicts on data, the architecture should provide services such as wait-and-retry intervals when attaching to a database or when encountering a deadlock. This means a built-in mechanism to react to certain database return codes and instead of issuing an immediate error handling, waiting a predetermined amount of time and retrying the database operation. + + <<4.4 Parameter Passing and Validation>> + + The partition architecture should be relatively transparent to application developers. The architecture should perform all tasks associated with running the application in a partitioned mode including: + + * Retrieve partition parameters before application start-up + + * Validate partition parameters before application start-up + + * Pass parameters to application at start-up + + The validation should include checks to ensure that: + + * the application has sufficient partitions to cover the whole data range + + * there are no gaps between partitions + + If the database is partitioned, some additional validation may be necessary to ensure that a single partition does not span database partitions. + + Also the architecture should take into consideration the consolidation of partitions. Key questions include: + + * Must all the partitions be finished before going into the next job step? + + * What happens if one of the partitions aborts? diff --git a/src/site/apt/blotter.apt b/src/site/apt/blotter.apt new file mode 100644 index 000000000..3e58e8133 --- /dev/null +++ b/src/site/apt/blotter.apt @@ -0,0 +1,104 @@ + ------ + Spring Batch-Retry Comments + ------ + Dave Syer + ------ + February 2007 + +Open Comments and Questions + +* Batches and Asynchronous JMS + + There are a large number of common concerns between + <<>> and <<>>. In + fact one could imagine <<>> being a + simple example of a batch, something like: + ++--- +RepeatTemplate template = new RepeatTemplate(); + +template.setTaskExecutor(new SimpleAsyncTaskExecutor()); + +template.setTerminationPolicy(new TerminateNeverPolicy()); + +template.execute(new JmsItemProviderCallback(jmsTemplate)); ++--- + + * Instead of setting the transactionManager in + <<>>, wrap the callback in a + transaction proxy. + + * Instead of setting sessionTransacted=true in the + <<>>, set it on the + <<>>. + + I think that would give me 80% of the functionality in a + <<>> without any changes to + <<>>. Maybe the other 20% would be useful additions + to <<>> anyway (like being able to stop and start). + Maybe there is a case for sharing some code, e.g. a base class. + Maybe the batch project should be an offshoot from the core.task + package. It certainly looks like the + <<>> could be a lot simpler (and + easier to test), if it delegates transactional properties to + something not a lot different from a <<>>. + +Resolved + +* Using <<>> in Batches + + * What can you do with a <<>> that you couldn't do with + a <<>>? Maybe <<>> should be a + <<>>, or use one to execute the batch? Probably the + latter would work best, on the basis of preferring composition to + inheritance generally. + +* Asynchronous Batching + + * Can you run a batch asynchronously? Would need a thread-safe + <<>> that can be shared amongst participating + threads, and used to determine termination conditions. + + * If <<>> used a <<>> to execute its + tasks, asynchronous batch might be as simple as using an + asynchronous <<>> internally - the same + <<>> would be able to operate in both modes, just + by changing the <<>>. + +* Using RetryContext to Stash State for the Policies + + E.g. in <<>>: + ++--- + protected void setupContext(RetryCallback callback, + RetryContext context) { + + if (callback instanceof AttributeAccessor) { + AttributeAccessor accessor = (AttributeAccessor) callback; + String[] names = accessor.attributeNames(); + for (int i = 0; i < names.length; i++) { + String name = names[i]; + context.setAttribute(name, accessor.getAttribute(name)); + } + } + } + ++--- + + But this is pants: it makes the callback stateful. We can't store + state in the callback to do with the current item becaue the + callback might (will?) be shared between attempts in a concurrent + system. + + So what to do? Nothing - the state for retry is nothing to do with + the callback, and it is natural to store it in the context. + +* Asynchronous Batching - Thread Safe Context + + * What would happen if several threads were sharing a context object + via a synchronization manager in a thread local (like + <<>>)? The context itself had + better be thread safe, otherwise the concurrent peers might assume + that they have the only copy of the context and try and modify it. + + diff --git a/src/site/apt/building.apt b/src/site/apt/building.apt new file mode 100644 index 000000000..b44067d07 --- /dev/null +++ b/src/site/apt/building.apt @@ -0,0 +1,311 @@ + ------ + Building Spring Batch + ------ + Dave Syer + ------ + April 2007 + +Building Spring Batch + + Spring Batch is organised as a reactor build in Maven (m2). To + build from the command line use + ++--- +$ mvn install ++--- + + or the goal of your choice (compile, test, etc.). This builds the + artifact (e.g. jar file) from the project in the current directory, + and deploys it to you local m2 repo at + <<<${user.home}/.m2/repository>>>. See below for instructions on how + to build the documentation and web site. + + By default the whole project (including subprojects) will be built + using Maven's "reactor" plugin. This can be expensive. To build + only one module, cd to that directory first. Or at the top level + use -N (for non-recursive) to exclude subprojects. + ++--- +$ mvn -N install ++--- + +* Skipping Tests + + The profile <> skips all the tests, so + ++--- +mvn -o install -P fast ++--- + + is the quickest way to update your local repo (assuming the tests + are OK). It is equivalent of setting <<<-Dmaven.test.skip=true>>>. + +* Eclipse IDE + + Each of the reactor modules at the top level also builds on its own + if you use the (excellent) Eclipse-plugin for m2 + (http://m2eclipse.codehaus.org/update/). Get version 0.0.10 or + better, because it supports dependencies on Eclipse projects that + are themselves Maven parents of the current project. + +* Dependencies + + If you get multiple versions of the same jar across projects, or a + jar is appearing in the classpath that you don't think is necessary, + look into the dependency structure and try and exclude it from + wherever it is being transitively included. To see the dependencies + for a project look in the site for the dependency report. + Alternatively (very useful for quickly locating a rogue jar) use + ++--- +$ mvn -P snapshots dependency:tree ++--- + + We use the "snapshots" profile here so that we get a snapshot of the + dependency plugin (older versions did not have the tree goal, but + newer versions are not stable enough to use in production). + +* Documentation + + With the exception of reference docs, please put content in the + project that it is most closely associated with. Here is a + {{{sitemap.html}site map}} to help you decide. + +** Quotidian Web Content + + Maven allows you to choose from a range of source format for + building web content. For Spring Batch we prefer the "almost plain + text" version. See files under <<>> in all the projects + for examples, and also refer to the + {{{http://maven.apache.org/guides/mini/guide-apt-format.html}Apt + Format Guide}} on the Maven website. + + N.B. you put .apt source files in a subdirectory called <<>>, + but they are moved to the top level when the site is built. Thus + <<>> becomes <<>>. + +*** Using emacs to edit .apt files + + Because the .apt format relies on indentation in plain text files, + the emacs auto-fill feature in text mode makes editing very + convenient. Put this in your .emacs + ++--- +(setq auto-mode-alist (cons '("\\.apt\\'" . text-mode) auto-mode-alist)) ++--- + + Then use <<>> to auto-fill the current paragraph. Emacs + adjusts the indentation of all the lines to match the first one (or + the first two if the second is different. + + If anyone knows how to do this with Eclipse or other editors, let us + know and we'll put a note here. + +** Reference Guide + + The <<>> project is reserved for reference guides in the + normal Spring docbook format. Each chapter of the reference guide + is in a separate xml file under <<>>. + The easiest way to work with the reference guide is to cd to the + <<>> module, and run Maven from there. + + Use the DTD with a validating XML editor (e.g. Eclipse) to explore + the docbook format. Also look at existing examples in Spring Batch + and in the Core Spring Framework source code. + + N.B. there is no need to explicitly create section numbers in the + XML - this is done for you by the build when everything is stitched + together into a book. + + N.B. you put docbook .xml source files in a subdirectory called + <<>>, but they are moved to the top level when the site is + built. Thus <<>> becomes + <<>>. + +** Adding a new chapter to the Reference Guide + + Here is a skeleton chapter including the DTD to get you started on a + new chapter. + ++--- + + + + Chapter Title +
+ Introduction + +
+
++--- + + Create a file with the template above, and put it in + <<>>. Use lower case, dash separated file names + (XML style), e.g. <<>>. + + Add the chapter to the master book in <<>> using + ++--- + ++--- + +* Adding graphics + + Put (e.g.) PNG image content in <<>>, and + then refer to the file using the <<>> directory prefix. + +** In .apt + + With no whitespace add the image name in square brackets (\[\]): + ++--- +[images/MyFigure.png] Caption content here is not rendered by default +in a browser (it's the ALT content)... ++--- + +** In docbook + + Use the \ element: + ++--- + + + + + + + + + + Figure 1: the figure caption... + + + ++--- + +* Program Listings in Docbook (Including XML) + + Use CDATA to save you from having to use the HTML escapes for all + the special characters. E.g. + ++--- + +]]> + ++--- + +* Dynamic Editing + + To see your changes to web site content as soon as you have typed + it, use + ++--- +mvn site:run ++--- + + and go to http://localhost:8080. + + In a project with unit tests, you can skip the tests and go straight + to the documentation using + ++--- +mvn -o site:run -P fast ++--- + + If you are offline, or want to speed things up a bit, the "-o" stops + Maven from trying to resolve dependencies on the internet. + + Use -N to build only the current project, not subprojects, So this + is pretty useful at the top level: + ++--- +mvn -N -o site:run -P fast ++--- + + In the <<>> project the docbook reference guide shows up at + http://localhost:8080/reference/*.html, where * is the name of an + xml file with a chapter in it. There is no link to these pages on + the site because the real docbook generated output is much nicer, + but this is still pretty useful for debugging and dynamic + editing. + + Note that the formatting is a bit limited compared to the whole + docbook stylesheet - Maven uses Doxia to squish all of docbook into + some simple wiki-like formatting rules. In particular it can't + generate the index page in the format we need it, so you may see + errors from <<>> if you visit that page. One of the + features is that the <<<\>>> syntax we use to build the + index and table of contents in the docbook-generated pages does not + work. Images are another problem. Use the generated content from + <<>> to view these artifacts. + +* Building and deploying the web site + + There is a bug in the m2 reactor (MNG-740) which means that we have + to install the parent pom to the local repo first. + + So do it this way: + ++--- +$ mvn install -P fast +$ mvn clean site site:deploy ++--- + + Add "-P deployment" to deploy to the real website (requires ssh + access to static.springframework.org). + + The default without -P is to deploy to <<>>, so we + don't get accidental updates to the site. To test the site contents + navigate with your browser to that directory. The site:stage goal + deos not work properly for this build: all the subprojects are not + integrated into the staging site, so use site:deploy instead. + + The static website content is not deleted during the deployment + process - merely replaced. If you need to clean everything up from + scratch you need to delete the contents on the server as well + (using ssh). + +Problems? + + Make sure your source code is up to date. Delete everything from + your local Spring Batch repo + <<<${user.home}/.m2/repository/org/springframework/batch>>>. If + necessary, delete a project or directory and update from SVN again. + + Try + ++--- +$ mvn install ++--- + + or + ++--- +$ mvn clean install ++--- + + or + ++--- +$ mvn clean install -P fast ++--- + + from the top level, and + ++--- +$ mvn -U ... ++--- + + from wherever you are (top level or sub-project). The latter will + update any older plugins you have in your local Maven repository. + Some people have had trouble building the web site without this. + + If you get <<>> e.g. building the site, use + MAVEN_OPTS to boost the heap size (on the command line if you have a + sensible shell): + ++--- +$ MAVEN_OPTS=-Xmx256m mvn site ++--- diff --git a/src/site/apt/cases/async.apt b/src/site/apt/cases/async.apt new file mode 100644 index 000000000..229036dc6 --- /dev/null +++ b/src/site/apt/cases/async.apt @@ -0,0 +1,159 @@ + ------ + Asynchronous Chunk Processing Use Case + ------ + Dave Syer + ------ + January 2007 + +Use Case: Asynchronous Chunk Processing + +* Goal + + Increased the efficiency of chunk processing by having it execute + asynchronously: each record in a separate thread. Maintain + transactional intergrity of the chunk. + +* Scope + + * All chunks might conceivably benefit from parallel processing, so + we don't want any unnecessary restrictions on the batch operation, + or its implementation. A should be possible for Client to write a + batch operation without reference to the fact that it might run in + an asynchronous chunk. + +* Preconditions + + * Input data exists with non-trivial size: chunks contain more than + one record. + + * Batch processing of a record is slow, or can be delayed, so that + the asynchronous processing can take longer than launching the + threads. + + * A chunk can be made to fail after at least one record is + processed. + +* Success + + * A chunk is processed and the results inspected to verify that all + records were processed. + + * Transactional behaviour is verified by rolling back a chunk and + verifying that no records were processed. + +* Description + + The vanilla case proceeds as for normal {{{chunks.html}chunk + processing}}, but: + + [[1]] Within a chunk, Container processes records in parallel. + + [[1]] At the end of a chunk, Container waits for the last record + to be processed (with a timeout if the wait is long). + +* Variations + +** Rollback on Failure + + If there is an exception in one of the record processing threads, + the whole chunk should roll back: + + [[1]] Client throws exception in record processing. + + [[1]] Container catahes exception and attempts to abort other + running processes. + + [[1]] Container waits for running processes to abort (or finish + normally, but preferably to abort). + + [[1]] Container propagates the exception and signals transaction to + rollback. + +** Timeout + + If there is a timeout during a chunk, it might happen before the + chunk has finished, or while waiting for the processes to complete + before exiting. + + [[1]] At end of chunk, Container is waiting for all processes to + finish. It times out, according to a parameter set by the + Operator. + + [[1]] Container does not start any new processes, and attempts to + abort running processes. + + [[1]] Container waits for running processes to abort (or finish + normally, but preferably to abort). + + [[1]] Container throws a time out exception and signals chunk + transaction to rollback. + +* Implementation + + * The implementation of this use case could be tricky in the general + case. In particular, the transactional nature is going to be hard + or impossible to maintain across multiple threads without the + individual processes being aware of the transaction, and (perhaps) + without global (XA) transaction support. + + A "normal" local transaction is thread bound - i.e. it only executes + in one thread. If the code inside the transaction creates new + threads, then they might not finish processing before the parent + exits and the transaction wants to finish. The transaction needs to + wait for the sub-processes before committing, or (more difficult) + rolling back. The rollback case basically forces us to a model of + one transaction per thread, and therefore to one transaction per + data item in a concurrent environment. + + Otherwise some transactional semantics might be respected in a + parallel process, but others certainly will not be because + synchronizations and resources are managed at the level of the + thread where the transaction started. If the transaction manager is + a local one (not XA) there is little hope even that the datasource + resource would be the same for all the parallel threads and the + parent method. + + If we use a global transaction manager to make the parallel + processes transactional, how will they know which transaction to + participate in? There could be many active chunks, and each would + have its own threads - how would each one be able to guide its child + processes to participate in the same transaction? + + * Beware a framework that extracts data from an <<>> + before executing the business logic (e.g. in a + <<>>). It is not enough to allow concurrent + processing but simply insist that the individual records are + processed transactionally because the <<>> will then + not be able to participate in the transaction - its next record has + already been passed to the consumer when the transaction starts, so + if there is a rollback then the record is lost. + + This is the origin of the signature: + ++--- +public interface ItemProvider { + Object next(); +} ++--- + + There is no peeking and no iteratror-style <<>>. If there + is a processing problem, transactional clients of the + <<>> throw an exception the provider's + <<>> has been called, but in the same thread (so that + transactional semantics are preserved and the data provider reverts + to its previous state). + + This means that in the callback interface also picks up an + <<>> return type + ++--- +public interface RepeatCallback { + Object doInIteration(BatchContext context); +} ++--- + + so we can return an object, which is null when the processing has + finished. + + In the end we decided against the <<>> retrun type and went + with a boolean flag to signal (false) for no more processing. diff --git a/src/site/apt/cases/chunks.apt b/src/site/apt/cases/chunks.apt new file mode 100644 index 000000000..204749b7d --- /dev/null +++ b/src/site/apt/cases/chunks.apt @@ -0,0 +1,208 @@ + ------ + Commit Periodically Use Case + ------ + Dave Syer + ------ + January 2007 + +Use Case: Commit Batch Process Periodically + +* Goal + + Read a file line-by-line and process into database inserts, for + example using the Jdbc API. Commit periodically, and if there is a + fault where the database transaction rolls back, then the file + reader is reset to the place it was after the last successful + commit. + + To develop a batch process to achieve the goal above should be as + simple a process as possible. The more that can be done with simple + POJOs and Spring configuration the better. + +* Scope + + To keep things simple for now, assume that: + + * All lines in the input file are in the same format and each line + generates a single database insert (or a fixed number). + + * The file is read synchronously by a single consumer. + +* Preconditions + + * A file exists in the right format, with a sufficiently large + number of lines to be realistic. + + * A mechanism exists to force a rollback at a non-trivial position + (not during the first commit), but produce a successful operation + on the second try. + + * A framework for retry exists, so that the case above can be + tested. + +* Success + + Integration test confirms that + + * All data are processed and records inserted successfully. + + * When a rollback occurs and the retry is successful, the complete + dataset is processed (same result as successful run). + + * Batch operations can be implemented without framework code (or + with minimal dependencies, e.g. through interfaces). Launching + the batch might require access to framework code. + +* Description + + The vanilla successful batch use case proceeds as follows: + + [[1]] Container starts a transaction. + + [[1]] Container makes resources available, e.g. opens file and + creates <<>> for it. + + [[1]] Client reads a line from the file, and converts it to a + database statement, then runs it. + + [[1]] Container increments counter. + + [[1]] Repeat previous two steps until a counter is equal to chunk + size. + + [[1]] Container commits database transaction. + + [[1]] Repeat chunk processing until input source is exhausted. + +* Variations + +** Non-fatal Chunk Failure + + If there is an unrecoverable database exception during execution of + client code: + + [[1]] Container rolls back current transaction. + + [[1]] Container resets input source to the point it was at before + failure. + + [[1]] Container retries chunk. + +** Fatal Chunk Failure + + If there is an error in the input data in the middle of a chunk + (could be manifested as database exception, e.g. uniqueness + exception, or nullable exception): + + [[1]] Container rolls back current transaction. + + [[1]] Container terminates batch and notifies client of precise + details, including the line number of error, and the last line + that was committed (last of the previous chunk). + + There is no need to reset the input source because the error is + fatal. + + To restart: + + [[1]] Operator truncates the input file so the completed chunks + are not repeated. + + [[1]] Operator fixes bad line (if there was one), and starts the + batch process wit hthe same parameters. + + Variations on this theme are also necessary, e.g. a tolerance for a + small number of bad records in the input data. + +* Implementation + + * The concept of a batch iterator seems relevant here (see also the + {{{simple.html}simple}} use case). The iterator could be more than + just a loop that might terminate early: here it could also manage + the file cursor on the input source. In this design there is a + <<>> interface that can take care of termination and + iteration (e.g. iterator-like method signatures). + + * Another design idea (more encapsulated and more in keeping with + existing Spring practice) is to make the data source transaction + aware, and for the client use it like a database resource, through a + template. In this case there is a <<>>. The + <<>> needs to be aware of the data source template, so + that it can terminate when the data is exhausted. + + In this version of events there are two kinds of resource in play. + The transaction itself, and the data sources that are aware of the + transaction. The comparison with <<>> + and <<>> is obvious. The client is often completely + unaware of the transaction manager, which is applied through an + interceptor, whereas the data source is used explicitly with its own + API through a template. The Client can concentrate on his domain, + and not be concerned with infrastructure or resource handling. + + * The analogy with <<>> is even stronger. If the input + data came from JMS instead of a file, we would hardly have to do + anything to implement very robust chunking. JMS is the obvious best + practice and already provides all the transactional semantics we + need for chunking - simply roll back a transaction and the records + processed return to the message system for delivery to the next + consumer. Bad records can be sent to a bad message queue for + independent processing. JMS might ssem like overkill for a lot of + batch processes, but it is tempting to say that if the robustness is + needed then the we should take that as a sign that installing and + configuring JMS is worth the extra effort. + + * Naturally we do not want to insist that the client code is aware + of the transaction that is surrounding it - this would be the normal + practice familiar from the Spring programming model. Should a + client need access to transaction-scoped resources, the usual way to + do that is to wrap the transactional resource (data source etc.) in + a proxy that uses a synchronization, or a more generic thread-bound + resource (using <<>>). The aim + is to retain this separation in a batch operation. The batch + framework itself might provide some of these synchronizations. + + * The {{{simple.html}Simple Batch Repeat}} is actually a pretty good + model for the chunk processing in this use case. This observation + leads to another: that a batch of chunks is a nested (or composed) + batch - the outer termination policy is dependent only on the data + source having further records to process, the inner one is a simple + iterator (with a check for empty data). A simplified programming + model for this is + ++--- +RepeatCallback chunkCallback = new RepeatCallback() { + + public boolean doInIteration(RepeatContext context) { + + int count = 0; + + do { + + Object result = callback.doWithRepeat(context); + + } while (result!=null && count++>>). Thw termination policy depends + only on a data source eventually returning null. + + * N.B. the chunkSize can be dynamic. E.g., if the chunk is long + during a nightime batch window, and short when the window is over, + in case the batch has to be terminated. + + * Chunking can also be implemented simply in an + <<>>. The handler just buffers records up to a + chunk size, and then executes them all in one step (which might be + transactional). This is easier to implement, and easier to + configure for the clients, but cannot easily be made both concurrent + and transactional. diff --git a/src/site/apt/cases/file-to-file.apt b/src/site/apt/cases/file-to-file.apt new file mode 100644 index 000000000..56ea4af8a --- /dev/null +++ b/src/site/apt/cases/file-to-file.apt @@ -0,0 +1,89 @@ + ------ + Copy File to File + ------ + Dave Syer + ------ + January 2007 + +Use Case: Copy File to File + +* Goal + + Read a file line-by-line and process into a file in a different + format (possibly different number of lines). Commit periodically + and in the event of an error both data sources (input and output) + rollback to the last known good point. + +* Scope + + To keep things simple for now, assume that: + + * All lines in the file are in the same format and the final + output is an aggregate. + + * The files are read and written synchronously by a single + consumer. + + * This use case requires two kinds of transactional file source. + One is read-only and the other is write-only. Only one consumer + can use the write-only source at a time. + +* Preconditions + + * An input file exists in the right format, with a sufficiently + large number of lines to be realistic. + +* Success + + Integration test confirms that + + * All data are processed and output produced successfully. + +* Description + + Very similar to the use case {{{chunks.html}Copy File to + Database}}, but involving transactional access to an output source + which is a file. Also we are introducing the idea of an aggregate + function for the output. + + The vanilla successful case proceeds as in the file to database + version, except that: + + [[1]] A successful chunk results in a line in an intermediate file + output source. + + [[1]] After all chunks are successfully processed the intermediate + file is itself processed in a single transaction to complete the + aggregate. The output is itself sent to an output channel + (e.g. database or file). + +* Variations + + * Chunk failure variations proceed as in the use case + {{{chunks.html}Copy File to Database}}. In the case of a + restart after fatal failure, the intermediate output file need does + not need to be reset or re-created. + +* Implementation + + * The write-only file source is new in this use case. It has a + similar flavour to the read-only version, but also has more serious + implications for implementation and usage. Since a file system is + not inherently transactional, when we create the write-only data + source we are assuming that consumers will play by the rules, + principally that there is only one consumer at a time. + + * With some external limitations the write-only file source can be + implemented so that within a single JVM it will behave like a + transactional database datasource. We can provide a + <<>> that hides the resource acquisition and + release, and interacts with an existing transaction to provide the + transactional behaviour that is required. + + * File-based transactional resources are a lot like messaging + clients. We can send a message (write a line) through a sender + client, and receive a message (read a line) through a consumer + client. In the case of a transaction rollback, all sent messages + are guaranteed not to reach consumers, and all received messages are + returned to the queue. Maybe ActiveMQ has a file transport already? + Mule definitely does, but it isn't transactional. diff --git a/src/site/apt/cases/index.apt b/src/site/apt/cases/index.apt new file mode 100644 index 000000000..f06293007 --- /dev/null +++ b/src/site/apt/cases/index.apt @@ -0,0 +1,101 @@ + ------ + Use Cases + ------ + Dave Syer + ------ + January 2007 + +Use Cases for Spring Batch + + These are more like scenarios or flows than real use cases in formal + UML terms, but they serve a useful purpose as both. We don't want + to be over formal, and probably code is being written and tested at + the same time as these use cases. But there are many stakeholders + in this project, and use cases are a useful resource to make sure + they are all agreed on scope and certain implementation details. + + * {{{simple.html}Simple Batch Repeat}} + + * {{{retry.html}Automatic Retry After Failure}} + + * {{{chunks.html}Commit Batch Process Periodically}}: chunk + processing. + + * {{{async.html}Asynchronous Chunk Processing}}: parallel + processing within a chunk. + + * {{{file-to-file.html}Copy File to File in a Batch}} + + * {{{parallel.html}Massively Parallel Batch Processing}} + + * {{{restart.html}Manual Restart After Failure}} + + * {{{steps.html}Sequential Processing of Dependent Steps}} + + * {{{partial.html}Partial Processing}}: skip records (e.g. on rollback). + + * Whole-Batch Transaction - transactional support for the whole + batch, not just chunks. Quite a common requirement, but not + always practical using normal transaction support. May require a + staging area, and a decision after it is full about whether to + copy it in one big batch (e.g. using native database tools) or + chunk it (e.g. if it is now in a form for which chunk failure is + easier to deal with). + + * {{{scheduled.html}Scheduled Processing}}: Batch Jobs controlled by scheduler (e.g. start, stop, suspend, kill) + + +* Actors + + The following actors are involved in the use cases (Container and + Client being the most common / important). + +** Client or Business Domain + + Code written by the batch developer. + + One aim us that the client is a POJO - the batch behaviour, boundary + conditions, transactions etc. can be dealt with by the Container in + such as way that the client does not need to know about them. The + client may have access to framework abstractions, like templated + data sources (<<>> etc.), but these should work the + same whether they are in a batch or not. + +** Container + + An application that converts user requests for batch jobs into + running processes. Container concerns are robustness, traceability, + manageability. + +** Framework + + The Framework is the infrastructure code that the Container depends + on, and possibly spi implementations where knowledge of the + non-business logic resides. + + The Framework provides two kinds of infrastruture (as per usual + Spring cornerstones and ): + + * For cross-cutting concerns there are interceptors that can be + wrapped around client code without it needing any knowledge of the + Framework at all. An existing parallel is with transaction + support - the client code can use <<>> + directly, but does not always need to. + + * Concrete abstractions that allow access to resources in a + uniform way without needing to know the details of how they are + provided (e.g. partitioned). Client code can use these + abstractions like it would a use a <<>>. + +** Operator + + The batch operator is not a developer. Tools are provided for the + Operator to be able to stop and start a batch, and to monitor the + progress and status of on ongoing or finished batch. + +** Business User + + The Operator has technical skills, e.g. a member of an application + support team, but may need help with business-related decisions. + For instance if input data are bad, he would not expect to be able + to fix them alone because they might be bad for a business reason. diff --git a/src/site/apt/cases/parallel.apt b/src/site/apt/cases/parallel.apt new file mode 100644 index 000000000..fd00e1e64 --- /dev/null +++ b/src/site/apt/cases/parallel.apt @@ -0,0 +1,239 @@ + ------ + Parallel Processing Use Case + ------ + Dave Syer + ------ + January 2007 + +Use Case: Massively Parallel Batch Processing + +* Goal + + Support efficient processing of really large batch jobs (100K - + 1000K records) through parallel processing, across multiple + processes or physical or virtual machines. The goals of other use + cases should not be compromised, e.g. we need to be able to start + and stop a batch job easily (for non developer), and trace the + progress and failure points of a batch. The client code should not + be aware of whether the processing is parallel or serial. + +* Scope + + * Any batch operation that reads data item-by-item from an input + source is capable of being scaled up by parallelizing. + + * The initial implementation might concentrate on multiple threads + in a single process. Ultimately we need to be able to support + multiple processes each one running in an application server (so + that jobs that require EJBs can be used). + +* Preconditions + + * A data source with multiple chunks (commitable units) - more chunks + than parallel processes. + + * A way for the container to launch parallel processes. + +* Success + + * A batch completes successfully, and the results are verified. + + * A batch fails in one of the nodes, and when restarted processes + the remaining records. + +* Description + + [[1]] Container splits input data into partitions. + + [[1]] Container sends input data (or references to them) to + processing nodes. + + [[1]] Processing nodes act independently, converting the input data + and sending it transactionally to output source (as per normal + single process batch). + + [[1]] Container collects status data from individual nodes for + reporting and auditing. + + [[1]] When all nodes are complete Container decides that batch is + complete finishes processing. + +* Variations + + Two failure cases can be distinguished, bad input data on a node and + an internal node failure have different implications for how to + proceed. In both cases, however + + [[1]] Container catches exception and classifies it. Rolls back + current transaction to preserve state of data (input and output). + + [[1]] Container saves state for restart from last known good + point, including a pointer to the next input record. + + Then if a processing node detects bad data in the input source, it + cannot be restarted or re-distributed because the data need to be + modified for a successful outcome. + + [[1]] Container alerts Operator of the location and nature of the + failure. + + [[1]] Operator waits for batch to finish - the overall status will + be a failure, but most of the data might be consumed. + + [[1]] Operator fixes problem and restarts batch. + + [[1]] Container does not re-process data that has already been + processed successfully. The parallel processing nodes are used as + before. + + [[1]] Batch completes normally. + + If a processing node fails unrecoverably (e.g. after retry timeout), + but with no indication that the input data were bad, then the data + can be re-used: Container returns unprocessed input data, and + redistributes it to other nodes. + +* Implementation + + * The hard thing about this use case is the partitioning of input + (and output) sources. This has to be done in such a way that the + individual operations are unaware that they are participating in a + batch farm. Partitioning has to be at least partially deterministic + because restarts have to be able to ignore data that have already + been processed successfully. + + Consider two examples: a file input source and a JDBC (SQL query) + based input source. Each provides its own challenges. + +** File Data Source Partitioning + + * If each node reads the whole file there could be a performance + issue. They would all need to have instructions about which lines + to process. + + * If each record of input data is a line, this isn't so bad. Each + node can have a range of line numbers to process. The only problem + is knowing how many lines there are, and how many nodes, so that the + job can be partitionaed efficiently. + + * But if each input record can span a variable number of lines (not + that unlikely in practice), then we can't use line numbers + + * Maybe the best solution is to have a single process parsing the + file and sending it to a message queue, either formally using a + messaging infrastructure or informally using some sort of + roll-your-own approach. The integration pattern could then be a + simple Eager Consumer, assuming that all records are processed + independently. The messaging semantics would simply have to ensure + that a consumer can roll back and return the input records to a + queue for another consumer to retry. + + For large batches a real messaging infrastructure (JMS etc.) with + guaranteed delivery would be a benefit, but might be seen as + overkill for a system that didn't otherwise require it. In this + case we could imagine the partitioning process being one of simply + dividing the input file up into smaller files, which are then + processed by individual nodes independently. The integration + pattern is then different - more like a Router. + + * What would parallel processing look like to the client? We can + make it completely transparent if we assume that the client only + ever implements <<>> and <<>>. The + client code is unaware of the partitioning of its data source: + ++--- +batchTemplate.iterate(new ItemProviderRepeatCallback(provider, processor)); ++--- + + * Parallelisation could also take place at the level of the + <<>> - we could proxy the data provider and wrap it in + a partitioning proxy: + ++--- + + + + + ... + + + + + + + + ... + + ++--- + +** SQL Data Source Partitioning + + * If each node is allowed to do its own query or queries to + determine the input data: + + * Each node has to be given a way to narrow the query so that they + don't all use the same data. There is no easy universal way to + achieve this, and in the general case we have to know in advance + when we are going to execute in a parallel or as a single process. + Maybe a range of primary keys would work as a special case that we + could support as a strategy. + + * Maybe we could assume that all nodes execute precisely the same + query, and then provide a way to add a cursor to the result set, + so it can be treated a bit more like a file. + + * We might be forced to use a distributed transaction to ensure + that all the nodes see the same data. This would be unfortunate, + but possibly necessary. It would be up to the client to configure + distributed transactions if that was required, otherwise the + result might be unpredictable if data can be added to an input + source while it is being read. + + * If only one query is done by the Container and the results shared + out amongst the nodes we face the issue of how to send the data + between nodes. Performance problems might ensue. Plus (more + seriously) the individual nodes would now need a different + implementation if they were acting in a parallel cluster to the + vanilla serial processing case - a single node would do the query + and work directly with the results, whereas in a parallel + environment it would be one step removed from the actual query. + This breaks our encapsulation design goal. + + * When considering the approach to partitioning the data source + we should follow closely the discussion above on partitioning a file + input source. If the client is to remain unaware of the batch + parameters, then an interceptor looks like the best approach. + + If each node prefers to do its own query then an interceptor would + have to catch the call to a JDBC template and modify the query + dynamically. This is quite a scary thing to be doing - it might end + up with us needing to parse the SQL and add where clauses. Maybe a + client should be forced to specify (in the case of a parallel batch) + how his query should be partitioned. For example: + ++--- + + + + SELECT * from T_INPUT + + + + SELECT * from T_INPUT where ID>=? and ID + + + ++--- + + It would be an error to run a batch in parallel if the partition + query had not been provided. + + * What happens if the data source changes between a batch and + restart? We can't legislate for that because it is outside the + realm of what can be controlled through a transaction. A restart + might produce different results than the original failed batch would + have done were it successful. diff --git a/src/site/apt/cases/partial.apt b/src/site/apt/cases/partial.apt new file mode 100644 index 000000000..fc5dd63fe --- /dev/null +++ b/src/site/apt/cases/partial.apt @@ -0,0 +1,154 @@ + ------ + Partial Processing Use Case + ------ + Dave Syer + ------ + January 2007 + +Partial Processing + +* Goal + + Support partial processing of a batch, without having to interrupt + or manually restart, but enabling corrective action to be taken + after the process has finished to complete the processing of failed + records. A batch that is going to fail completely can be be + identified as soon as possible, but one which is substantially + alright can run as far as possible to prevent costly duplication. + Records that are skipped are reported in such a way that they can be + easily identified by the Operator and / or Business User and a new + batch created to finish the original goal. By the same token, in + the case of an aborted batch where a minority of records are + processed successfully first time, it should be possible to identify + the successful records and exclude them from data presented on + restart. + +* Scope + + Any batch should be configurable to support partial processing. + +* Preconditions + + * A data source with a small number of bad records exists. + +* Success + + * A test data set with a small number of bad records is run through + the batch processer and completes normally. Operator confirms + that the good recirds are all processed and then fixes and + resubmits the bad records, and confirms that they are also + correctly processed with no duplicates. + +* Description + + The vanilla flow proceeds as follows: + + [[1]] Batch processing begins as per normal (see for example + {{{chunks.apt}chunk processing use case}}). + + [[1]] A record is processed. This step repeats until... + + [[1]] Container detects a bad record, e.g. by catching a + classified execption. + + [[1]] Container logs the exception in a way that identifies the + bad record easily and immediately to the Operator. + + [[1]] Container stores an identifier for the bad record (or the + whole record) in a location designated to the Operator for that + purpose. + + [[1]] Container determines that the batch can still succeed + despite the cumulative number or nature of bad records - the bad + record is skipped. Container goes back to normal processing, and + eventually completes the whole batch. + +* Variations + +** Abort Batch Early + + The batch cannot skip all records. After each failure the decision + about whether to coninue has to be made: + + [[1]] When a record is processed successfully, Container logs the + event in a form that can be used later to identify successful + records in case the batch is aborted. + + [[1]] Container determines that a sufficiently large fraction of + the records processed so far have failed. The faction relevant is + to be specified through configuration meta data (not specified by + business logic). + + [[1]] Container aborts the batch with a clear signal to the + Operator that it has aborted owing to an unacceptable number of + errors. + +* Implementation + + * When the decision to abort is taken, Container may have + successfully processed a small number of records and the + corresponding transactions might have committed. Those records that + were successfully processed on the first attempt are easy to + exclude from the restart, if transactional semantics are respected + by the item processing. + + * The decision to abort is based on exception classification. Each + time an item is processed, the framework needs to catch exceptions + and classify them as + + * fatal: signals an abort - rethrow. + + * transient: nominally fatal, but the operation is retryable. + + * non-fatal: signals a skip. + + The transient failure is really just a sub-type of fatal case. It + is treated differently by the {{{retry.html}retry framework}} but + not necessarily by the vanilla batch. + + * Actually we can't decide what action to take simply on the + evidence of the current exception. What we need to do is decide, + potentially based on the whole history of exceptions in a given + batch, whether the latest one should trigger an abort. E.g. a + simple and sensible policy would be to abort if the total number of + exceptions reaches a threshold, either absolute or relative to the + number of items processed. + + * So how does it look? In the template... + ++--- +public void iterate(RepeatCallback callback) { + + ... + + try { + result = callback.doInIteration(context); + } catch (Exception e) { + handleException(e); // Maybe re-throw, maybe not... + } + + ... + +} ++--- + + If the callback was transactional it has already rolled back. If + the whole <<>> was transactional we need to rethrow + + * If the processing is asynchronous, the template has to execute in + a separate thread (see {{{async.html}asynchronous example}}). In + this case the whole thread (i.e. the <<>>) has to be + transactional. Whoever is counting failed items needs to be + poooling information from multiple threads. + + * It may also be the role of the framework to translate exceptions + into a batch-specific hierarchy. This is not the same concern as + exception classification (as done for instance by the Spring Jdbc + and Jms templates). Exception classification might also be of + value, but the argument is not as clear cut as the existing core + templates, where there is an underlying Jave EE API checked + exception to convert. In the absence of a batch-specific exception + hierarchy definition, we could choose to leave exception translation + out of the batch framework. + + diff --git a/src/site/apt/cases/restart.apt b/src/site/apt/cases/restart.apt new file mode 100644 index 000000000..bd4296ec9 --- /dev/null +++ b/src/site/apt/cases/restart.apt @@ -0,0 +1,86 @@ + ------ + Restart Use Case + ------ + Dave Syer + ------ + January 2007 + +Use Case: Manual Restart After Failure + +* Goal + + Restart a failed or interrupted batch and have it pick up where it + left off (within limits of transaction boundaries) to save time and + resources. A key goal is that the management of the batch process + (locating a job and its input and results, starting, scheduling, + restarting) should be as easy as possible for a non-developer, like + an application support team with some business back up. + +* Scope + + Any batch should be able to restart gracefully, even if (depending + on chosen container or client implementation) it might have to go + right back to the beginning. + +* Preconditions + + * It is possible to identify exception conditions under which a + restart will be able to carry on processing a batch from where it + left off. + + * There exists a presistent storage mechanism for the initial + conditions. + +* Success + + * Force a batch to fail, and then fix the problem and restart. See + successful completion with no duplicate results. + +* Description + + [[1]] A batch operation encounters an exception which forces the + process to stop processing. + + [[1]] Container catches exception and classifies it. + + [[1]] Container logs event with enough information to identify the + location of the job and the nature of the problem. + + [[1]] Container saves initial condition from last commit point, to + enable restart to start from the last known good operation. + + [[1]] Operator fixes problem (e.g. makes missing resource available, + edits input file). + + [[1]] Operator restarts batch. + + [[1]] Container loads initial conditions and continues processing. + +* Variations + + * Some restarts might lend themsleves to being handled automatically + - see the use case {{{retry.html}Automatic Retry}}. + +* Implementation + + * The saving of initial conditions needs to be strategised. In some + cases saving a native serialization to a file will suffice. In + others a database might be used, or some custom serialization + (persist / rehydrate). + + * The initial condition is naturally under control of the + <<>>. The client need not know about the persistence + and rehydration. In fact explicit persistence and rehydration might + be overkill - just relying on the transaction semantics might be + adequate in a lot of cases. The <<>> would have to be + aware of the transactions, which we assume are normally demarcated + in the <<>>. Since the point at which persistence + is needed is tied to transaction commits, there may have to be some + transaction synchronization. + + * The persistence of initial conditions is a cross cutting concern. + It may lend itself (along with the application of an execution + handler generally) to being implemented as an aspect. Compare the + <<>>, where the most common usage is via an + interceptor, but occasionally the template is used directly by + client code. diff --git a/src/site/apt/cases/retry.apt b/src/site/apt/cases/retry.apt new file mode 100644 index 000000000..da5b87c21 --- /dev/null +++ b/src/site/apt/cases/retry.apt @@ -0,0 +1,279 @@ + ------ + Automatic Retry Use Case + ------ + Dave Syer + ------ + January 2007 + +Use Case: Automatic Retry + +* Goal + + Support automatic retry of an operation if it fails in certain + pre-determined ways. Client code is not aware of the details of + when and how many times to retry the operation, and various + strategies for those details are available. The decision about + whether to retry or abandon lies with the Framework, but is + parameterisable through some retry meta data. + + Retryable operations are usually transactional, but this can be + provided by a normal transaction template or interceptor + (transaction meta data are independent of the retry meta data). + +* Scope + + Any operation can be retried, but there are restrictions on nesting + transactions (normally an inner transaction needs to be + propagation=NESTED). + +* Preconditions + + An operation exists that can be forced to fail and is able to + succeed on a retry. + +* Success + + * Verify that an operation fails and then succeeds on a retry. + + * Verify that back off policy (time between retries) can be + strategised without changing client code. + + * Verify that the retry policy can be strategised, and can be used + to change the number of retry attempts depending on the type of + exception thrown in the retry block. + +* Description + + Successful retry proceeds as follows: + + [[1]] Framework executes an operation provided by Client. + + [[1]] The operation fails and Framework catches an exception, + classified as retryable. + + [[1]] Framework waits for a pre-defined back off period. The + period is not be fixed, but is strategised so that different + policies can be applied. The most common and useful policy is an + exponentially increasing back off delay, with a ceiling. + + [[1]] Framework repeats the operation. + + [[1]] Processing is successful. + + [[1]] Framework stores and / or logs statistics about the retry + for management purposes. Details? + +* Variations + + The following variations are supported. + +** Retry Failure + + A retry can fail for a number of reasons. E.g. if the number of + retries is too high, or there is a timeout, or an exception of + another sort that cannot be classified as retryable. + + [[1]] Last retry attempt fails and Framework determines that + another retry is not permitted by the current policy. + + [[1]] Framework records status for management purposes. + + [[1]] Framework throws a recognisable exception? + + [[1]] Control may return to client (if the exception was caught), + or the processing may end. + +** Transient and Non-transient Failures + + We may wish to classify exceptions into (at least) three types, and + vary the retry policy based on the classification: + + * Transient failures come from resources that are external and may + have independent lifecycles to the client process. Examples are + database deadlock, network connectivity. It is always worth + retrying on a transient failure, and normally we can keep retrying + (if not forever then for a very long time), in the belief that + eventually the resource will become available again. + + * Non-transient failures can be retried a few times. This is the + default. + + * Non-retryable failures like a configuration or input data error + should not be retried (they will always fail the same way). + +** Early Termination + + Normally client code is unaware of the Framework, but occasionally + emergency measures might be taken inside client code where all + further retry attempts are vetoed for the current block. + +** Stateful Retry + + A stateful (or external) retry is used to force a roll back of an + external message (or other data) resource, so that the message will + be re-delivered. The implementation has to be stateful so it can + remember the context for the failed message next time it is + delivered. The additional features of a stateful retry, as opposed + to a normal rollback, are that: + + * A message can be retried indefinitely or up to a set number of + times, after which an error processing route is taken. + + * A back-off delay is used at the of the retry + before any other transactional resources are enlisted. + +* Implementation + + * The vanilla case and most of the variations can be achieved with a + simple template approach: + ++--- +RetryTemplate retryTemplate = new RetryTemplate(); +retryTemplate.setRetryPolicy(new SimpleRetryPolicy(5)); +Object result = retryTemplate.execute(new RetryCallback() { + public Object doWithRetry(RetryContext context) throws Throwable { + // do some processing + return result; + } +}); ++--- + + * Schematically we can represent the implementation of the [retry} + template as follows: + ++--- +1 | TRY { +1.1 | do something; +2 | } FAIL { +2.1 | if (retry limit reached) { +2.2 | rethrow exception; + | } else { +2.3 | TRY(1) again; + | } + | } ++--- + + * The template has policies for back off and retry (whether or not + to retry the last exception). The example above shows the retry + policy being set to simply retry all exceptions up to a limit of 5 + times. + + * The <<>> has an API that allows clients to override + the retry policy. The context can also be accessed as a thread + local from a static convenience class, in the case that the callback + is implemented as a wrapper around a POJO. + + * External retry is the most difficult variation to implement, and + doesn't fit naturally into the template model above. Two things + depend on the retry count - back-off delay and the decision to + follow the recovery path - so it needs to be available at the + beginning of every processing block. + + We will discuss the implementation from a JMS-flavoured viewpoint, + where the current item being processed is a message. This can be + generalised to more generic data types, as long as the item can be + rejected transactionally to signal that we require it to be + re-delivered to this or another consumer. + + Consider this pattern, which is very typical: + ++--- +1 | SESSION { +2 | receive; +3 | RETRY { + | remote access; + | } + | } ++--- + + A <<>> is responsible for the RETRY(3) block. But + we can't put the same wrapper around the whole process: + ++--- +0 | RETRY { // Do not do this! +1 | SESSION { +2 | receive; +3 | RETRY { + | remote access; + | } + | } + | } ++--- + + because the receive(2) might not get the same message back on the + second and subsequent attempts (another consumer might get it, or it + might come out of order). So external retry has a different flow - + it might be a different implementation of the same interface, or a + different parameterisation of the normal retry template. + + We can break down the implementation of an external retry into steps + as follows: + ++--- +1 | SESSION { +2 | receive; +3 | TRY { +3.1 | if (already processed) { +3.2 | backoff; + | } +4 | RETRY { + | remote access; + | } +5 | } FAIL { +5.1 | if (retry limit reached) { +5.2 | recover; + | } else { +5.3 | rethrow exception; + | } + | } + | } ++--- + + Decisions (3.1) and (5.1) require knowledge of the history of + processing the current message. Note that the action on failure is + the opposite to the vanilla case {{{#retry}above}} - if the retry + limit is not reached then we rethrow the exception. + + If the retry limit is not reached then the rethrow(5.3) causes the + SESSION(1) to roll back, and the message will be re-delivered. + RETRY(4) is a normal retry with a template. + + The retry logic is easy to implement - the hard bit is that the + policies depend on the history of the message. This requires some + special retry and back off policies that are aware of the history: + + * When a message arrives, at the beginning of the TRY(3) above, we + need to update our knowledge of its history. + + * The backoff policy can decide whether to back off immediately + when it is initialized at step (3.1). + + * The retry decision at (5.1) has to be aware of the history as + well as some simple exception classification rules. + + * If the retry cannot proceed the retry policy can take steps to + recover (5.2), e.g. send the current message to an error queue. + The exception should not propagate in this case. + + * If we fail and rethrow (5.3), then we need to store the + knowledge of the message history somewhere where another consumer + can access it. + + There is a small conundrum about what value to return from the + TRY(3) block if it ultimately fails (5.2) - a normal retry never + completes unless it is successful, but an external retry can + complete if it is unsuccessful. The obvious choice is to return + null. It probably won't matter in a messaging application anyway + because the client of the retry block probably isn't expecting + anything. It may matter if the TRY(3) block is part of a batch + because the batch template uses null as a signal that the current + batch is complete. But on the other hand it might be a good + strategy to close the batch if processing a message fails. + + With JMS there is no indication in the <<>> how many times + it has been rejected - only a flag <<>> to show + that it has failed at least once. To count the number of retries, + we have to store a global map of messages (ids) to retry counts + (within a single VM - for more than one OS process each one has to + be independent). + diff --git a/src/site/apt/cases/scheduled.apt b/src/site/apt/cases/scheduled.apt new file mode 100644 index 000000000..3caeb0a59 --- /dev/null +++ b/src/site/apt/cases/scheduled.apt @@ -0,0 +1,34 @@ + ------ + Scheduler Managed Use Case + ------ + Wayne Lund + ------ + May 2007 + +Use Case: Scheduler Managed Processing + +* Goal + + Ensure that an Enterprise Scheduler can interact with the Batch Launcher to start, stop, + suspend and/or kill a batch job. + +* Scope + + * Batch jobs tends to run within carefully planned job stream schedules. At a minimum this requires + an integration between the Batch Launcher (in the abstract) and the scheduler's control mechanism to + start and stop batch jobs and then to understand the results of the batch job execution + (e.g. COMPLETED, ABENDED, etc.) so that subsequent actions may be taken. + +* Preconditions + + * A mechanism has been established for the scheduler to launch a batch job. This is often times + a simple unix or dos shell script. + + * A mapping of exit codes to the error code numbers that the scheduler is expecting on the exiting + of a batch job. + +* Success + + * Batch Jobs are launched and managed by scheduler + +* Description diff --git a/src/site/apt/cases/simple.apt b/src/site/apt/cases/simple.apt new file mode 100644 index 000000000..72488aa3b --- /dev/null +++ b/src/site/apt/cases/simple.apt @@ -0,0 +1,290 @@ + ------ + Simple Batch Repeat Use Case + ------ + Dave Syer + ------ + January 2007 + +Use Case: Simple Batch Repeat + +* Goal + + Repeat a simple operation such as processing a data item, or a + message, up to a fixed number of times, normally with a transaction + scoped to the whole batch. Transaction resources are shared between + the operations in the batch, leading to performance benefits. + +* Scope + + The operation to be repeated: + + * Can expect to use and manage its own I/O or datastore resources, + but not necessarily transactions; + + * May need to introspect the batch status (as a variation); + + * Executes synchronously or asynchronously (as a variation). + + * Is stateless - this is not a framework restriction in principle, + but simplifies the implementation for now. See in the + {{{#store}Implementation}} section below for some notes on + stateful synchronisation; + + * Should be implementable as a POJO if desired. + +* Preconditions + + Client code can locate and acquire all the resources it needs for + the batched operation, and can force transactions to rollback for + testing purposes. + +* Success + + * Verify that a successful batch executed a fixed number of times. + + * Verify that a batch completes early but successfully if an + underlying transaction times out. + + * Terminate a batch by failing one of the operations, and verify + that the preceding operations rolled back (subject to batch meta + data). + + * Execute a batch asynchronously and verify that the correct number + of operations is performed. + +* Description + + We are often interested in a specific scenario of this use case + where the batched operation is: + + * Read a message or data item from an endpoint like a JMS + Destination. + + * Do some business processing involving database reads and writes. + + The vanilla successful batch use case proceeds as follows: + + [[1]] Framework starts a batch, acquiring resources as needed and + creating a context for the execution. + + [[1]] Client provides a batch operation in the form of a source of + data items and a processor acting on the data item. + + [[1]] Framework executes batch operation. + + [[1]] Repeat the last step until the batch size is reached. + + [[1]] Framework commits the batch. All database changes are + committed and received messages removed from the endpoints. + +* Variations + +** Rollback + + If one of the operations rolls back it will throw an exception. + Normal transaction semantics determine what happens next. Usually + (in the scenario described above) there is an outer transaction for + the whole batch, which rolls back as well: all the messages remain + unsent, and all the data remain uncommitted. A retry will receive + exactly the same initial conditions. + +** Timeout + + The batch size is not fixed. The use case proceeds as above, but in + the middle of a batch operation execution: + + + [[1]] Framework determines that the batch has timed out operation + (e.g. while it was waiting for an incoming message). + + [[1]] Framework commits the batch with all operations so far + complete - possibly a smaller than normal size. + +** Asynchronous Processing + + Instead of the Framework waiting for each operation to complete it + could spin them off independently into separate threads or a work + queue. The batch still has to have a definite endpoint, so the + Framework waits for all the operations to finish or fail + before cmpleting the batch. + +** Introspection of Batch Context + + Client may wish to inspect the state of the ongoing batch operation, + and potentially force an early completion. + +* {Implementation} + + * The completion of the batch loop is handled by a policy delegate + that we can use to strategise the concept of a loop that might + complete early. This can cover both the timeout variation and the + vanilla use case flow. + + * What form should the batch template (<<>>) + interface take? We might start with something like this: + ++--- +batchTemplate.iterate(new RepeatCallback() { + + public boolean doInIteration() { + // do stuff + } + +}); ++--- + + * A nice tool for a batch operation in a callback is an iterator + through a data set or message endpoint (<<>>), coupled + with a handler for processing the item. This adds a potential + implementation of <<>> that knows about the + <<>> and adds a processor object. E.g. as an + anonymous inner class: + ++--- +final ItemProvider provider = new JmsItemProvider(); +final ItemProcessor processor = new ItemProcessor() { + public void process(Object data) { + // do something with the data (a record) + } +}; + +batchTemplate.execute(new RepeatCallback() { + + public boolean doInIteration() { + Object data = provider.next(); + if (data!=null) { + processor.process(data); + } + return data!=null; + } + +}); ++--- + + * Is a batch template with callback the best implementation? Could + we perhaps use or re-use <<>> somehow? Which is + better for the client: + ++--- +batchTemplate.iterate(new RepeatCallback() { + + public boolean doInIteration() { + // do stuff + } + +}); ++--- + + where the batch template might itself use a <<>> + internally, or + ++--- +batchTemplate.iterate(new Runnable() { + + public void run() { + // do stuff with data + }; + +}); ++--- + + where the batch template is a <<>>. Probably the + former because it is more encapsulated: it gives the framework more + freedom to implement the template in any way it needs to, e.g. to + accommodate more complicated use cases. + + * To {store} up SQL operations until the end of a batch, and take + advantage of JDBC driver efficiencies, the client needs to store + some state during the batch, and also register a transaction + synchronisation. For this kind of scenario we introduce an + interceptor framework in the template execution. The template calls + back to interceptors, which themselves can strategise clean up and + close-type behaviour: + ++--- +public class RepeatTemplate implements RepeatOperations { + + public void iterate(RepeatCallback callback) { + + // set up the batch + interceptors.open(); + + while (running) { + + // allow interceptor to pre-process and veto continuation + interceptor.before(); + + // continue only if batch is ongoing + if (running = callback.doInIteration()!=null) { + interceptor.after(); + } + + } + + // clean up or commit the whole batch + interceptor.close(); + + } +} ++--- + + The <<>> can be stateful, and can store up inserts + until the end of the batch. If the <<>> is + transactional then they will only happen if the transaction is + successful. + + This way the client can even decide to use a batch interceptor + that runs in its own transaction at the end of the batch. + + * There is no need for an overall batch timeout because the inner + operations are synchronous and have their own timeout metadata + though transaction definitions. The whole batch (outer transaction) + may still have a timeout attribute, and then there is a corner case + where the batch operations are all successful, but because they all + took a long time the whole batch rolls back because of the timeout. + + * The context of the ongoing batch is closely linked with the + completion policy. The completion policy is pluggable into the + batch template, and acts as a factory for context objects which can + then be inspected by Client in the callback. For example: + ++--- +public class RepeatTemplate implements RepeatOperations { + + public void iterate(RepeatCallback callback) { + + // set up the batch session + RepeatContext context = completionPolicy.start(); + + while (!completionPolicy.isComplete(context)) { + + // callback gets the context as an argument + callback.doInIteration(context); + + completionPolicy.update(context); + } + + } +} ++--- + + * The example above provides Client the opportunity to inspect the + context through the callback interface. If Client is a POJO, + Framework has to create a callback and wrap it, in which case there + needs to be a global accessor for the current context or session. + The template is then responsible for registering the current context + with a <<>>. E.g.client code can look + at the session and mark it as complete if desired + (c.f. <<>>): + ++--- +public Object doMyBatch() { + + // do some processing + + // something bad happened... + RepeatContext context = RepeatSynchronizationManager.getContext(); + context.setCompleteOnly(); + +} ++--- diff --git a/src/site/apt/cases/steps.apt b/src/site/apt/cases/steps.apt new file mode 100644 index 000000000..4d21c1667 --- /dev/null +++ b/src/site/apt/cases/steps.apt @@ -0,0 +1,176 @@ + ------ + Batch: Sequential Steps Use Case + ------ + Dave Syer + ------ + January 2007 + +Use Case: Sequential Processing of Dependent Steps + +* Goal + + Compose a batch operation from a sequence of dependent steps. + Define and implement the operation only once, and allow restart + after failure without having to change configuration, and without + having to repeat steps that were successful. + + A sub-goal is to allow the progress of a batch through the steps to + be traced accurately for reporting and auditing purposes. This + requires the steps to be uniquely identified. + +* Scope + + * Simple linear sequence of steps. Slightly more complicated + requirements can be handled by putting independent steps in a + sequence (no need for splits and joins). + +* Preconditions + + * A non-trivial sequence is defined: + + * more than one step: + + * the effects of each step can be measured. + + * The sequence can be interrupted or artificially terminated in the + second or subsequent step. + +* Success + + * A non-trivial sequence executes successfully. The progress and + success of each step can be verified by the tester. + + * The same sequence is forced to fail on second step in such a way + that the first step result is not suspected of being in error, + e.g. by interrupting it. When it is restarted the first step is not + repeated, and the sequence is successful. + + * The same sequence is forced to fail on second step in such a way + that the first step result is obviously in error, even though it + completed normally. When the batch is restarted the first step + repeated, and the sequence is successful. + +* Description + + The vanilla successful case proceeds as follows: + + [[1]] Container logs the start of a step, uniquely indentifying + the initial conditions. + + [[1]] Container stores internal state so that initial conditions + can be re-created in the event of a restart. + + [[1]] Step execution proceeds as per one of the other use cases + (e.g. {{{file-to-database.html}Copy File to Database}}), including + transactional behaviour. + + [[1]] Client instructs Container to store internal state needed by + further steps (e.g. cached reference data). + + [[1]] Container logs successful completion of step, and stores + + [[1]] Repeat for next and subsequent steps. Internal state is + passed from one state to the next. + +* Variations + +** Internal Failure of Step + + If a step fails internally, e.g. because of resource becoming + temporarily unavailable, the sequence can be restarted without + repeating the previous steps. + + [[1]] Operator fixes resource problem (e.g. starts web service). + + [[1]] Operator restarts batch with no configuration or input data + changes. + + [[1]] Container resumes batch from the last commit point of the + failed step. + + [[1]] Sequence completes normally. + + The process above could be carried out by the container entirely (no + need for operator intervention) if a retry policy is in effect. + +** Failure of Step Owing to Bad Initial State + + If a step fails because it receives bad data from an earlier step, + the Container cannot recover without intervention. + + [[1]] Operator attempts to restart without doing anything to fix + the problem. + + [[1]] Container detects bad initial state immediately and fails + fast. + + If the original problem can be located and fixed (e.g. input data + for earlier step is revised): + + [[1]] Operator restarts batch signalling to container which step + to begin with. + + [[1]] Container locates initial state for the first step to be + executed. + + [[1]] Container starts execution from the beginning of the desired + state. This time the input data are different, so the sequence + can complete normally. + +* Implementation + + * The need to save state for subsequent steps leads to the + introduction of a batch context concept. And the need for + initialising restarts leads to the context being serializable, + either natively or by some pluggable strategy (this is covered in + the {{{restart.html}Restart after Failure}} use case). + + Unfortunately, the need for {{{parallel.html}parallel processing}} + and automatic {{{restart.html}restart}} also makes it practically + impossible for steps to handle the context at the level of a single + thread of execution, where the client needs to implement business + logic. If a step is executing in parallel, then each node needs to + be able to restart independently, but the context needs to be a + single object that can be passed on to the next step (unless all the + steps are parallelised with the same multiplicity, which might not + be efficient in general). + + Thus batch context must be defined and managed by the template or + execution handler. + + * The requirement for steps might have implications for the + implementer of the batch operation (the client). Obviously a client + defines the sequence of steps according to the business requirement, + but ideally we would like him to be unaware of the reporting and + restart infrastructure. Maybe an array of callbacks works (the + callback interface is irrelevant, except that it accepts a context + object as an argument): + ++--- +batchTemplate.iterate(new RepeatCallback[] { + + new RepeatCallback() { + public boolean doInIteration(RepeatContext context) { + // do stuff for step one + }; + }, + + new RepeatCallback() { + public boolean doInIteration(RepeatContext context) { + // do stuff for step two - the context + // is the same... + }; + } + +}); ++--- + + Notice that there is no need for the context to be set explicitly + before executing the callback. The context is handled internally to + the batch template using an analogue of the + <<>>. + + * If we prefer that clients never need to know about batch + templates, then the code above needs to be automated. This would be + where an additional domain layer might come into play + (c.f. <<>>). diff --git a/src/site/apt/cases/template.apt b/src/site/apt/cases/template.apt new file mode 100644 index 000000000..ed359bed6 --- /dev/null +++ b/src/site/apt/cases/template.apt @@ -0,0 +1,22 @@ + ------ + Template Use Case + ------ + Dave Syer + ------ + January 2007 + +Use Case: Template + +* Goal + +* Scope + +* Preconditions + +* Success + +* Description + +* Variations + +* Implementation \ No newline at end of file diff --git a/src/site/apt/changelog.apt b/src/site/apt/changelog.apt new file mode 100644 index 000000000..1d789e3a6 --- /dev/null +++ b/src/site/apt/changelog.apt @@ -0,0 +1,7 @@ +Changelog: Spring Batch + + See the individual subprojects for their changelogs: + + * {{{spring-batch-infrastructure/changelog.html}Infrastructure}} + + * {{{spring-batch-container/changelog.html}Container}} diff --git a/src/site/apt/features.apt b/src/site/apt/features.apt new file mode 100644 index 000000000..05a0b8939 --- /dev/null +++ b/src/site/apt/features.apt @@ -0,0 +1,152 @@ + ------ + Spring Batch Features + ------ + Dave Syer + ------ + July 2007 + +Spring Batch Features and Roadmap + +* 1.0 Features + + The following features are supported by Spring Batch 1.0: + +** Optimisation and Infrastructure + + * RepeatOperations: an abstraction for grouping repeated + operations together and moving the iteration logic into the + framework. + + * RetryOperations: an abstraction for automatic retry. + + * InputSource abstraction and implementations for flat files, xml + streaming and simple database queries. + + * Flat files are supported with fixed length and delimited records + (input and ouput). + + * Xml is supported through Xstream mapping between objects and Xml + elements (input and ouput). + + * A database input source is provided that maps a row of a ResultSet + identified by a simple (single column) primary key. + + * OutputSource abstraction and implementations for flat files and + xml (the Sql case is just a regular Jdbc Dao). + + * InputSource and OutputSource implementations are generally + Restartable and Skippable. Skippable means that they can be asked by + clients to mark items as skipped, and not provide or process them + next time they arrive. + + * Complementary to InputSource and OutputSource is a higher-level + abstraction layer with ItemProvider and ItemProcessor. Some + specialised concrete retry and repeat strategies have dependencies + on ItemProvider and/or ItemProcessor. + +** Core Domain + + * JobConfiguration is the root of the core domain - it is what most + developers and operators will be happy to call a "job": a recipe for + how to construct and run a JobInstance. + + * A JobConfiguration is composed of a list of StepConfigurations + (sequential step model for job). + + * StepConfiguration is a wrapper for a "unit of work", otherwise + known as a Tasklet (formerly Module). + + * JobExecutor is the entry point for launching a JobConfiguration. + + * StepExecutor is the corresponding point for a StepConfiguration. + StepExecutor is the main strategy for different scaling, + distribution and processing approaches. The 1.0 release contains + implementations for in-process execution (single VM). See below. + +** Job Execution and Management + + * A simple JobExecutorFacade to launch jobs. Start a new one or + restart one that has previously failed. The facade can be used by a + command-line or JMX launcher to take simple input parameters and + convert them to the form required by the Core. + + * Persistence of job meta data for management and reporting + purposes: job and step identifiers, commit counts, task counts, + statistics (a human readable represenation of the state of the job - + can be augmented by developers). + + * ItemProviderProcessorTasklet - uses an ItemProvider to obtain the + next record to process, and hands it to an ItemProvider if it is not + null. + + Developers are encouraged to use the ItemProviderProcessorTasklet + rather than implementing their own, because this is the + implementation that in future versions of Spring Batch will be able + to adapt to different deployment architectures, and take advantage + of automatic scaling up through distributed processing. + + * A StepExecutor (SimpleStepExecutor) that can run a + StepConfiguration in the same process (VM). + + * Concurrent execution of chunks (a chunk is a batch of items + processed in the same transaction) through the Spring TaskExecutor + abstraction. + + * Additional StepExecutor implementation that is aware of whether + its task is Recoverable - take recovery action on error. + + * Automatic retry of a chunk and recovery for items that have + exhausted their retry count. + + * Translation of job execution result into an exit code for + schedulers running the job as an OS process. + +** Samples + + * A range of samples is available as a separate module. They all + use a common simple configuration and extend in various ways to show + the different features of the Execution module. + +** Partial Support or Potentially Unstable APIs + + A milestone is a milestone, so we are going to continue refining the + APIs until we get to a release candidate. Hopefully most of the + developer touch points are functionally pretty stable, even if the + names and packages might still change. Partial implementations or + areas currently known to be undergoing refactoring as of + 1.0-m2-SNAPSHOT are listed in JIRA + (http://opensource.atlassian.com/projects/spring/browse/BATCH) for + items marked as open for versions 1.0-m2 or 1.0. + + Documentation is extensive but still incomplete. The recent + refactoring to create the Execution module is not yet reflected, so + the "container" concept is still ubiquitous. + +* Roadmap (Beyond 1.0). + + * Remote or distributed execution of steps. The step has to be + partitioned and the partition information passed on to the remote + processes to avoid double counting. The remote execution might be + in an EJB or other RPC like a web service. + + * Asynchronous pipeline processing - steps execute concurrently and + optionally in separate processes. Feedback loop between consumers + and producers to prevent overflows. + + * Issue tracking - a job is not finished until all issues with its + executions are resolved. Spring Batch can provide hooks to + integrate with internal issue tracking systems so that the lifetime + of a job can be properly managed. + + * Auditing. Implement hooks to monitor not only what jobs execute + and the result of the execution (as per 1.0 possibly with some + richer options for detailed outcome reports), but also who has + executed the job, what changes they made to runtime parameters. + + * OSGi support. Deploy the Spring Batch framework as an OSGi + service. Deploy individual jobs or groups of jobs as additional + bundles that depend on the core. + +* No Plans Yet to Support + + * Triggering. diff --git a/src/site/apt/index.apt b/src/site/apt/index.apt new file mode 100644 index 000000000..e22508960 --- /dev/null +++ b/src/site/apt/index.apt @@ -0,0 +1,98 @@ + ------ + Spring Batch + ------ + Dave Syer, Scott Wintermute + ------ + March 2007, May 2007 + +Introduction + + Many applications within the enterprise domain require bulk processing to perform business operations in mission critical environments. These business operations include automated, complex processing of large volumes of information that is most efficiently processed without user interaction. These operations typically include time based events (e.g. month-end calculations, notices or correspondence), periodic application of complex business rules processed repetitively across very large data sets (e.g. insurance benefit determination or rate adjustments), or the integration of information that is received from internal and external systems that typically requires formatting, validation and processing in a transactional manner into the system of record. Batch processing is used to process billions of transactions every day for enterprises. + + Spring Batch is a lightweight, comprehensive batch framework designed to enable the development of robust batch applications vital for the daily operations of enterprise systems. Spring Batch builds upon the productivity, POJO-based development approach, and general ease of use capabilities people have come to know from the Spring Framework, while making it easy for developers to access and leverage more advance enterprise services when necessary. + + Spring Batch provides reusable functions that are essential in + processing large volumes of records, including logging/tracing, + transaction management, job processing statistics, job restart, + skip, and resource management. It also provides more advance + technical services and features that will enable extremely + high-volume and high performance batch jobs though optimization and + partitioning techniques. Simple as well as complex, high-volume + batch jobs can leverage the framework in a highly scalable manner to + process significant volumes of information. + + Spring Batch is part of the + {{{http://www.springframework.org/sub-projects}Spring Portfolio}}. + +* Spring Batch Architecture + + Spring Batch is designed with extensibility and a diverse group of + end users in mind. The figure below shows a sketch of the layered + architecture that supports the extensibility and ease of use for + end-user developers. + +[images/ContainerLayer.png] Spring Batch Architecture showing +Infrastructure and Container Layers. Potential container +implementations support different platforms and end-user goals from +the same blocks of business logic in the Application Layer. + + The initial release provides an Infrastructure layer in the form of + low level tools. There is also a simple Container application, + using the infrastructure in its implementation. The container + provides robust features for traceability and management of the + batch lifecycle. A key goal is that the management of the batch + process (locating a job and its input, starting, scheduling, + restarting, and finally processing to created results) should be as + easy as possible for developers. + + The Infrastructure provides the ability to batch operations + together, and to retry an piece of work if there is an exception. + Both requirements have a transactional flavour, and similar concepts + are relevant (propagation, synchronisation). They also both lend + themselves to the template programming model common in Spring, + c.f. <<>>, <<>>, + <<>>. + + The Simple Batch Execution Container is the first container available. It provides a robust set of integrated features including logging/tracing, transaction management, job processing statistics, job restart, skip, and resource management to enable the management of the full lifecycle of traditional batch processing. A number of sample jobs are packaged with this container and are described in detail to more clearly articulate usage and capabilities of the container. + +* Roadmap + + Once the framework is released it can be used immediately to + simplify batch optimisations and automatic retries. The framework + is oriented around application developers not needing to know any + details of the framework - there are a few application developer + interfaces that can be used for convenient construction of data + processing pipelines, but apart from that we support as close to a + POJO programming model as is practical. This is similar to the + approach taken in Spring Core in the area of DAO implementation. + + A Partitioned Batch Execution Container is also being developed that will provide alternate scaling solutions. This container will provide more advance technical services and features to enable extremely high-volume and high performance batch jobs though proven optimization and partitioning techniques. Proven scaling techniques will be provided as partitioned strategies allowing users to spread the load across a pool of clustered J2EE application servers. There are also discussions to leverage grid technologies as an alternate scaling solution. + + Matt Welsh's work shows that + {{{http://www.eecs.harvard.edu/~mdw/proj/seda/}SEDA}} has enormous + benefits over more rigid processing architectures, and messaging + containers give us a lot of resilience out of the box. So we also + want to provide a more SEDA flavoured container, or container + support, as well as supporting the more traditional ETL style + approach. There might be a tie in with Mule and/or other ESB tools + here, giving the benefit of a very scalable architecture, where the + choice of transport and distribution strategy can be made as late as + possible. The same application code could be used in principle for + a standalone tool processing a small amount of data, and a massive + enterprise-scale bulk-processing engine. + +* Background + + While open source software projects and associated communities have focused greater attention on web-based and SOA messaging-based architecture frameworks, there has been a notable lack of focus on reusable architecture frameworks to accommodate Java-based batch processing needs, despite continued needs to handle such processing within enterprise IT environments. The lack of a standard, reusable batch architecture has resulted in the proliferation of many one-off, in-house solutions developed within client enterprise IT functions. + + Interface21 and Accenture are collaborating to change this. Accenture's hands-on industry and technical experience in implementing batch architectures, Interface21's depth of technical experience, and Spring's proven programming model together mark a natural and powerful partnership to create high-quality, market relevant software aimed at filling an important gap in enterprise Java. Both companies are also currently working with a number of clients solving similar problems developing Spring-based batch architecture solutions. This has provided some useful additional detail and real-life constraints helping to ensure the solution can be applied to the real-world problems posed by clients. For these reasons and many more, Interface21 and Accenture have teamed to collaborate on the development of Spring Batch. + + Accenture is contributing previously proprietary batch processing architecture frameworks -- based upon decades worth of experience in building batch architectures with the last several generations of platforms (i.e., COBOL/Mainframe, C++/Unix, and now Java/anywhere) -- to the Spring Batch project along with committer resources to drive support, enhancements, and the future roadmap. + + The collaborative effort between Accenture and Interface21 aims to promote the standardization of software processing approaches, frameworks, and tools that can be consistently leveraged by enterprise users when creating batch applications. Companies and government agencies desiring to deliver standard, proven solutions to their enterprise IT environments will benefit from Spring Batch. + + +* Links: + + * A discussion {{{blotter.html}blotter}}. + diff --git a/src/site/apt/scratch.apt b/src/site/apt/scratch.apt new file mode 100644 index 000000000..6e512936b --- /dev/null +++ b/src/site/apt/scratch.apt @@ -0,0 +1,58 @@ + ------ + Spring Batch Scratch + ------ + Dave Syer + ------ + March 2007 + + ++--- + | restore; + | +1 | BATCH(repeat=until exhausted) { + | +2 | RETRY(outer) { + | +3 | TX(datasource=batch) { + | +4 | TX(datasource=business) { + | +5 | BATCH(repeat=5) { + | +6 | RETRY(inner) { +6.1 | input; +7 | } PROCESS { + | output; +8 | } RECOVER { + | recover; + | } + | + | } + | +4.1 | savepoint; + | + | } + | + | } + | + | } + | + | } ++--- + + * The order of the transaction nesting might be important, but only + if they are not XA, and only if there is a partial failure (inner + commits and the outer rolls back), and only if that happens on the + last attempt at RETRY(2). + + * Batch TX is outside business TX so the worse that can happen is + that we might restart from the same point twice (if the inner TX + commits and the outer rolls back). If they were the other way + round the batch savepoint(4.1) could commit and the business + processing (7) roll back - then we would miss the business + processing if the batch had to restart. + + * The savepoint(4.1) needs to be implemented so that the state it + saves is synchronized with the business TX(4). That way if TX(4) + rolls back the savepoint will always be the correct state to + restart if a partial failure is followed by a successful RETRY(2). diff --git a/src/site/apt/sitemap.apt b/src/site/apt/sitemap.apt new file mode 100644 index 000000000..6f46215b9 --- /dev/null +++ b/src/site/apt/sitemap.apt @@ -0,0 +1,101 @@ + ------ + Site Map + ------ + Dave Syer + ------ + April 2007 + +Spring Batch Site Map + +* Overview + + * Main Site - high-level information and links to sub-projects + (called "modules" in Maven speak): + + * Docs - reference documentation, user guides + + * Infrastructure - CI build and technical information + + * Integration Tests - reports on tests of infrastructure + + * Container - CI and technical information about container layer + +* Main Site + + * Splash page - welcome, mission statement, download links + + * Whitepaper (JavaOne presentation translated to HTML) + + * Occasional Articles (e.g. transactions) + + * Use Cases + + * Project Information (standdard Maven stuff) + + * Developers + + * Source Repository + + * License + + * etc. + +* Documentation + + * Splash page - welcome, links to rest of reference docs. + + * User Guides (docbook, Spring branded reference guides - HTML, HTML + Single Page and PDF). Two choices: one big guide with parts as + listed below, or multiple mini-guides. The former is probably + better. + + Maybe we could also break each of these down a bit more... + + * Infrastructure - How to use the core API + + * Simple Container + + * Partitioning Container + + * Other Containers? + + * Changelog + +* Infrastructure + + * Splash page explaining the role of infrastructure, and high level + API packaging. + + * Changelog + + * Project information (duplicated from Main Site - Maven "feature") + + * CI Reports + + * JUnit test report + + * Clover coverage + + * JDepend report + + * Javadocs + +* Integration Tests + + * Changelog + + * Project information (duplicated from Main Site - Maven "feature" - + TODO: find a way to switch them off in sub-projects) + + * CI Reports (same as for infrastructure). + +* Container + + Should the use case go here (showing which ones are implemented)? + + * Changelog + + * Project information + + * CI Reports (same as for infrastructure). + diff --git a/src/site/apt/transactions.apt b/src/site/apt/transactions.apt new file mode 100644 index 000000000..c47d26239 --- /dev/null +++ b/src/site/apt/transactions.apt @@ -0,0 +1,354 @@ + ------ + Spring Batch-Retry Transaction Propagation + ------ + Dave Syer + ------ + February 2007 + +Batch Processing and Transactions + +* {Simple Batching} with No Retry + + Consider the following simple example of a nested batch with no + retries. This is a very common scenario for batch processing, where + an input source is processed until exhausted, but we commit + periodically at the end of a "chunk" of processing. + ++--- +1 | REPEAT(until=exhausted) { + | +2 | TX { +3 | REPEAT(size=5) { +3.1 | input; +3.2 | output; + | } + | } + | + | } ++--- + + The input operation (3.1) could be a message-based receive + (e.g. JMS), or a file-based read, but to recover and continue + processing with a chance of completing the whole job, it must be + transactional. The same applies to the operation at (3.2) - it must + be either transactional or idempotent. + + If the chunk at REPEAT(3) fails because of a database exception at + (3.2), then TX(2) will roll back the whole chunk. + +* Simple Stateless Retry + + It is also useful to use a retry for an operation which is not + transactional, like a call to a web-service or other remote + resource. For example: + ++--- +0 | TX { +1 | input; +1.1 | output; +2 | RETRY { +2.1 | remote access; + | } + | } ++--- + + This is actually one of the most useful applications of a retry, + since a remote call is much more likely to fail and be retryable + than a database update. As long as the remote access (2.1) + eventually succeeds, the transaction TX(0) will commit. If the + remote access (2.1) eventually fails, then the transaction TX(0) is + guaranteed to roll back. + +* {Typical} Repeat-Retry Pattern + + The most typical batch processing pattern is to add a retry to the + inner block of the chunk in the {{{#Simple Batching}simple}} example. + Consider this: + ++--- +1 | REPEAT(until=exhausted, exception=not critical) { + | +2 | TX { +3 | REPEAT(size=5) { + | +4 | RETRY(stateful, exception=deadlock loser) { +4.1 | input; +5 | } PROCESS { +5.1 | output; +6 | } SKIP and RECOVER { + | notify; + | } + | + | } + | } + | + | } ++--- + + The inner RETRY(4) block is marked as "stateful" - see the + {{{#Typical}typical}} use case for a description of an stateful + retry. This means that if the the retry PROCESS(5) block fails, the + behaviour of the RETRY(4) is as follows. + + * Throw an exception, rolling back the transaction TX(2) at the + chunk level, and allowing the item to be re-presented to the input + queue. + + * When the item re-appears, it might be retried depending on the + retry policy in place, executing PROCESS(5) again. The second and + subsequent attempts might fail again and rethrow the exception. + + * Eventually the item re-appears for the final time: the retry + policy disallows another attempt, so PROCESS(5) is never + executed. In this case we follow a RECOVER(6) path, effectively + "skipping" the item that was received and is being processed. + + Notice that the notation used for the RETRY(4) in the plan above + shows explictly that the the input step (4.1) is part of the retry. + It also makes clear that there are two alternate paths for + processing: the normal case is denoted by PROCESS(5), and the + recovery path is a separate block, RECOVER(6). The two alternate + paths are completely distinct: only one is ever taken in normal + circumstances. + + In special cases (e.g. a special <<>> + type), the retry policy might be able to determine that the + RECOVER(6) path can be taken on the last attempt after PROCESS(5) + has just failed, instead of waiting for the item to be re-presented. + This is not the default behaviour because it requires detailed + knowledge of what has happened inside the PROCESS(5) block, which is + not usually available - e.g. if the output included write + access before the failure, then the exception should be rethrown to + ensure transactional integrity. + + The completion policy in the outer, REPEAT(1) is crucial to the + success of the above plan. If the output(5.1) fails it may throw an + exception (it usually does, as described), in which case the + transaction TX(2) fails and the exception could propagate up through + the outer batch REPEAT(1). We do not want the whole batch to stop + because the RETRY(4) might still be successful if we try again, so + we add the exception=not critical to the outer REPEAT(1). + + Note, however, that if the TX(2) fails and we try again, by + virtue of the outer completion policy, the item that is next + processed in the inner REPEAT(3) is not guaranteed to be the one + that just failed. It might well be, but it depends on the + implementation of the input(4.1). Thus the output(5.1) might fail + again, on a new item, or on the old one. The client of the batch + should not assume that each RETRY(4) attempt is going to process the + same items as the last on ethat failed. E.g. if the termination + policy for REPEAT(1) is to fail after 10 attempts, it will fail + after 10 consecutive attempts, but not necessarily at the same item. + This is consistent with the overall retry strategy: it is the inner + RETRY(4) that is aware of the history of each item, and can decide + whether or not to have another attempt at it. + +* Asynchronous Chunk Processing + + The inner batches or chunks in the {{{#Typical}typical}} example + above can be executed concurrently by configuring the outer batch to + use an <<>>. The outer batch waits for all the + chunks to complete before completing. + ++--- +1 | REPEAT(until=exhausted, concurrent, exception=not critical) { + | +2 | TX { +3 | REPEAT(size=5) { + | +4 | RETRY(stateful, exception=deadlock loser) { +4.1 | input; +5 | } PROCESS { + | output; +6 | } RECOVER { + | recover; + | } + | + | } + | } + | + | } ++--- + +* Asynchronous Item Processing + + The individual items in chunks in the {{{#Typical}typical}} + can also in principle be processed concurrently. In this case the + transaction boundary has to move to the level of the individual + item, so that each transaction is on a single thread: + ++--- +1 | REPEAT(until=exhausted, exception=not critical) { + | +2 | REPEAT(size=5, concurrent) { + | +3 | TX { +4 | RETRY(stateful, exception=deadlock loser) { +4.1 | input; +5 | } PROCESS { + | output; +6 | } RECOVER { + | recover; + | } + | } + | + | } + | + | } ++--- + + This plan sacrifices the optimisation benefit, that the simple plan + had, of having all the transactional resources chunked together. It + is only useful if the cost of the processing (5) is much higher than + the cost of transaction management (3). + +Interactions Between Batching and Transaction Propagation + + There is a tighter coupling between batch-retry and TX management + than we would ideally like. In particular an stateless retry cannot + be used to retry database operations with a transaction manager that + doesn't support NESTED propagation. + + For a simple example using retry without repeat, consider this: + ++--- +1 | TX { + | +1.1 | input; +2.2 | database access; +2 | RETRY { +3 | TX { +3.1 | database access; + | } + | } + | + | } ++--- + + Again, and for the same reason, the inner transaction TX(3) can + cause the outer transaction TX(1) to fail, even if the RETRY(2) is + eventually successful. + + Unfortunately the same effect percolates from the retry block up to + the surrounding repeat batch if there is one: + ++--- +1 | TX { + | +2 | REPEAT(size=5) { +2.1 | input; +2.2 | database access; +3 | RETRY { +4 | TX { +4.1 | database access; + | } + | } + | } + | + | } ++--- + + Now if TX(3) rolls back it can pollute the whole batch at TX(1) and + force it to roll back at the end. + + What about non-default propagation? + + * In the last example PROPAGATION_REQUIRES_NEW at TX(3) will + prevent the outer TX(1) from being polluted if both transactions + are eventually successful. But if TX(3) commits and TX(1) rolls + back, then TX(3) stays committed, so we violate the transaction + contract for TX(1). + + If TX(3) rolls back, TX(1) does not necessarily (but it probably + will in practice because the retry will throw a roll back + exception). + + * PROPAGATION_NESTED at TX(3) works as we require in the retry + case (and for a batch with skips): TX(3) can commit, but + subsequently be rolled back by the outer transaction TX(1). If + TX(3) rolls back, again TX(1) will roll back in practice. This + option is only available on some platforms, e.g. not Hibernate or + JTA, but it is the only one that works consistently. + + So NESTED is best if the retry block contains any database access. + +* Special Case: Transactions with Orthogonal Resources + + Default propagation is always OK for simple cases where there are no + nested database transactions. Consider this (where the SESSION and + TX are not global XA resources, so their resources are orthogonal): + ++--- +0 | SESSION { +1 | input; +2 | RETRY { +3 | TX { +3.1 | database access; + | } + | } + | } ++--- + + Here there is a transactional message SESSION(0), but it doesn't + participate in other transactions with + <<>>, so doesn't propagate when TX(3) + starts. There is no database access outside the RETRY(2) block. If + TX(3) fails and then eventually succeeds on a retry, SESSION(0) can + commit (it can do this independent of a TX block). This is similar + to the vanilla "best-efforts-one-phase-commit" scenario - the worst + that can happen is a duplicate message when the RETRY(2) succeeds + and the SESSION(0) cannot commit, e.g. because the message system is + unavailable. + +* Stateless Retry Cannot Recover + + The distinction between an stateless and an stateful retry in the + {{{#Typical}typical}} example above is important. It is actually + ultimately a transactional constraint that forces the distiction, + and this constraint also makes it obvious why the distinction + exists. + + We start with the observation that there is no way to skip an item + that failed and successfully commit the rest of the chunk unless we + wrap the item processing in a transaction. So we simplify the + {{{#Typical}typical}} batch execution plan to look like this: + ++--- +0 | REPEAT(until=exhausted) { + | +1 | TX { +2 | REPEAT(size=5) { + | +3 | RETRY(stateless) { +4 | TX { +4.1 | input; +4.2 | database access; + | } +5 | } RECOVER { +5.1 | skip; + | } + | + | } + | } + | + | } ++--- + + Here we have an stateless RETRY(3) with a RECOVER(5) path that kicks + in after the final attempt fails. The "stateless" label just means + that the block will be repeated without rethrowing any exception up + to some limit. This will only work if the transaction TX(4) has + propagation NESTED. + + If the TX(3) has default propagation properties and it rolls back, + it will pollute the outer TX(1). The inner transaction is assumed by + the transaction manager to have corrupted the transactional + resource, and so it cannot be used again. + + Support for NESTED propagation is sufficiently rare that we choose + not to support recovery with stateless retries in current versions of + Spring Batch. The same effect can always be achieved (at the + expense of repeating more processing) using the + {{{#Typical}typical}} pattern above. + + diff --git a/src/site/fml/faq.fml b/src/site/fml/faq.fml new file mode 100644 index 000000000..3d940e7f5 --- /dev/null +++ b/src/site/fml/faq.fml @@ -0,0 +1,196 @@ + + + + + + There are 3 main layers of the architecture (application, container, and infrastructure), what is the + vision for how the Container layer might be used in future? + + +

+ The "layers" described are nicely segregated in terms of dependency. Each layer only depends (at + compile time) on layers below it. +

+

+ Actually we have recognised that what we used to call the container layer actually is composed of + two distinct contexts, "Core" and "Execution". So the full catalogue of contexts is: +

    +
  • + Application + is the business logic. It is written by the application developer - the client of Spring + Batch - and only depends on the other Core interfaces for compilation and configuration. +
  • +
  • + Core + is the public API of Spring Batch, including the core batch domain of Job, Step, + configuration and Executor interfaces. +
  • +
  • + Execution + is the deployment, execution and management concerns. Different execution environments (e.g. + in a JEE container, out of container) are configured differently, but can execute the same + application business logic. +
  • +
  • + Infrastructure + is a set of low level tools, that are used to implement the execution and parts of the core + layers. +
  • +
+
+

+

+ The "execution" layer is fertile ground for collaboration and contributions from the community and + from projects in the field. The central interface is + ExecutionService + with methods for starting and stopping jobs. The vision for this is that there can be multiple + implementations of + ExecutionService + providing different architectural patterns, and delivering different levels of scalability and + robustness, without changing either the business logic or the job configuration. The initial 1.0 + release of Spring Batch will have a single implementation + SimpleExecutionService + (formerly known as + SimpleBatchContainer + .) +

+
+
+ + + What is the Spring Batch philosophy on the use of flexible strategies and default implementations? + + + There are a great many extension points in Spring Batch for the framework developer (as opposed to the + implementor of business logic). We expect clients to create their own more specific strategies that can + be plugged in to control things like commit intervals ( + CompletionPolicy + ), rules about how to deal with exceptions ( + ExceptionHandler + ), and many others. + + + + How does Spring Batch differ from Quartz? Is there a place for them both in a solution? + +

+ Spring Batch and Quartz have different goals. Spring Batch provides functionality for processing + large volumes of data and Quartz provides functionality for scheduling tasks. So Quartz could + complement Spring Batch, but are not excluding technologies. A common combination would be to use + Quartz as a trigger for a Spring Batch job using a Cron expression and the Spring Core convenience + SchedulerFactoryBean + . +

+
+
+ + How do I schedule a job with Spring Batch? + +

+ Use a scheduling tool. There are plenty of them out there. Examples: Quartz, Control-M, Autosys. + Quartz doesn't have all the features of Control-M or Autosys - it is supposed to be lightweight. If + you want something even more lightweight you can just use the OS (cron, at, etc.). +

+

+ Simple sequential dependencies can be implemented using the job-steps model of Spring Batch. We + think this is quite common. And in fact it makes it easier to correct a common mis-use of scehdulers + - having hundreds of jobs configured, many of which are not independent, but only depend on one + other. +

+
+
+ + How stable are the interfaces in Spring Batch? + +

+ We are still in the milestone release phase (1.0-m2 is in the pipeline). This means that we are + still adding functionality that we want to be part of a 1.0 release. We do not rule out changes to + package and interface names in this phase, but that said we think the basic domain concepts in + Spring Batch are sound enough to survive significant re-factoring. The bulk of the application + developer "touch points" have been stable for quite some time now, and we have several early adopter + projects already using snapshot releases. +

+

+ The process from here is to collect feedback from the community and use that to decide on what extra + features need to be added to get us to 1.0. When we are feature complete we will move to the + "release candidate" phase, and the first release in that phase will be 1.0-rc1. When significant + issues are resolved (if there are any) we will promote the release through the "rc" numbers, until + we have a clean 1.0 release. +

+
+
+ + + How will Spring Batch allow project to optimize for performance and scalability (through parallel + processing or other)? + + + We see this as one of the roles of the Execution layer. A specific implementation (or implementations) + of the + ExecutionService + can deal with the concern of breaking apart the business logic and sharing it efficiently between + parallel processes or processors. There are a number of technologies that could play a role here. The + essence is just a set of concurrent remote calls to distributed agents that can handle some business + processing. Since the business processing is already typically modularised - e.g. input an item, process + it - Spring Batch can strategise the distribution in a number of ways. One implementation that we have + had some experience with (and have a prototype for) is a set of remote EJBs handling the business + processing. We switch off Home caching in the container and then send a specific range of primary keys + for the inputs to each of a number of remote calls. THe same basic strategy would work with any of the + Spring Remoting protocols (plain RMI, HttpInvoker, JMS, Hessian etc.) with little more than a couple of + lines change in the execution layer configuration. + + + + What are the key concepts in the Spring Batch core domain? + +

+ In a nutshell: A JobConfiguration with a list of StepConfigurations is passed to an + ExecutionService. From this a Job is constructed consisting of a series of Steps, each of which is + executed by a StepExecutor. The StepExecutor contains all the strategies for deciding when to + complete, when to commit, when to abort and when to continue. +

+

+ Many Jobs in practice consist of a single Step. Step is very useful and best practice for breaking a + Job down into logical units, rather than having to execute separate Jobs (potentially in separate OS + processes) which have no obvious logical connection. +

+

+ Jobs can be executed once, or many times with different logical identifiers (JobRuntimeInformation). + It is also possible to restart a failed Job with the same or a modified input source, and identify + the resulting JobExecution as a separate entity. In this way the progress of a Job and itys history + of successful and failed executions can easily be tracked. The same argument applies to Steps, which + have their corresponding StepExecution entity. +

+
+
+ + How can messaging be used to scale batch architectures? + + There is a good deal of practical evidence from existing projects that a pipeline approach to batch + processing is highly beneficial, leading to resilience and high throughput. We are often faced with + mission-critical applications where audit trails are essential, and guaranteed processing is demanded, + but where there are extremely tight limits on performance under load, or where high throughput gives a + competitive advantage. Matt Welsh's work shows that a Staged Event Driven Architecture (SEDA) has + enormous benefits over more rigid processing architectures, and message-oriented middleware (JMS, AQ, + MQ, Tibco etc.) gives us a lot of resilience out of the box. There are particular benefits in a system + where there is feedback between downstream and upstream stages, so the number of consumers can be + adjusted to account for the amount of demand. So how does this fit into Spring Batch? Well it's a good + example of an + ExecutionService + or (more broadly) execution runtime if the deployment is grid- or cluster-based, or in any way involves + multiple OS processes. + + + + How can I contribute to Spring Batch? + + Use JIRA and the forum to get involved in discussions about the product and its design. There is a + process for contributions and eventually becoming a committer. The process is pretty standard for all + Apache-licensed projects. You make contributions through JIRA (so sign up now); you assign the copyright + of any contributions using a standard Apache-like CLA (see the Apache one for example - ours might be + slightly different); when the contributions reach a certain level, or you somehow convince us otherwise + that you are going to be committed long term, even if part time, then you can become a committer. + + +
+
diff --git a/src/site/resources/discussion/retry-with-mq.png b/src/site/resources/discussion/retry-with-mq.png new file mode 100644 index 000000000..82870a460 Binary files /dev/null and b/src/site/resources/discussion/retry-with-mq.png differ diff --git a/src/site/resources/images/ContainerLayer.png b/src/site/resources/images/ContainerLayer.png new file mode 100644 index 000000000..0ae74bfe3 Binary files /dev/null and b/src/site/resources/images/ContainerLayer.png differ diff --git a/src/site/resources/images/logos/i21-banner-1.jpg b/src/site/resources/images/logos/i21-banner-1.jpg new file mode 100644 index 000000000..c72b017b9 Binary files /dev/null and b/src/site/resources/images/logos/i21-banner-1.jpg differ diff --git a/src/site/resources/images/partitioned.png b/src/site/resources/images/partitioned.png new file mode 100644 index 000000000..2451b30e0 Binary files /dev/null and b/src/site/resources/images/partitioned.png differ diff --git a/src/site/site.xml b/src/site/site.xml new file mode 100644 index 000000000..10f953b16 --- /dev/null +++ b/src/site/site.xml @@ -0,0 +1,36 @@ + + + + ${project.name} + http://www.springframework.org/ + + + images/shim.gif + + + + + + org.springframework.maven.skins + maven-spring-skin + 1.0.2 + + + + + + + + + + + + + + + + + + + +