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 @@
+
+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.+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 + <<+ * 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.+ * %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: + *+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).+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.+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. +
+ +