diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/jsr/item/CheckpointSupport.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/jsr/item/CheckpointSupport.java new file mode 100644 index 000000000..21128be15 --- /dev/null +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/jsr/item/CheckpointSupport.java @@ -0,0 +1,140 @@ +/* + * Copyright 2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.batch.jsr.item; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; + +import javax.batch.api.chunk.ItemReader; +import javax.batch.api.chunk.ItemWriter; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.batch.item.ExecutionContext; +import org.springframework.batch.item.ItemStreamException; +import org.springframework.batch.item.ItemStreamSupport; +import org.springframework.util.Assert; + +/** + * Provides support for JSR-352 checkpointing. Checkpoint objects are copied prior + * to being added to the {@link ExecutionContext} for persistence by the framework. + * If the checkpoint object cannot be copied and further changes occur to the same + * instance, side effects may occur. In cases like this, it is recommended that a + * copy of the object being acted upon in the reader/writer is returned via the + * {@link ItemReader#checkpointInfo()} or {@link ItemWriter#checkpointInfo()} calls. + * + * @author Michael Minella + * @since 3.0 + */ +public abstract class CheckpointSupport extends ItemStreamSupport{ + + private final Log logger = LogFactory.getLog(this.getClass()); + + private final String checkpointKey; + + /** + * @param checkpointKey key to store the checkpoint object with in the {@link ExecutionContext} + */ + public CheckpointSupport(String checkpointKey) { + Assert.hasText(checkpointKey); + this.checkpointKey = checkpointKey; + } + + /* (non-Javadoc) + * @see org.springframework.batch.item.ItemStreamSupport#open(org.springframework.batch.item.ExecutionContext) + */ + @Override + public void open(ExecutionContext executionContext) + throws ItemStreamException { + try { + doOpen((Serializable) executionContext.get(getExecutionContextKey(checkpointKey))); + } catch (Exception e) { + throw new ItemStreamException(e); + } + } + + /** + * Used to open a batch artifact with previously saved checkpoint information. + * + * @param checkpoint previously saved checkpoint object + * @throws Exception + */ + protected abstract void doOpen(Serializable checkpoint) throws Exception; + + /* (non-Javadoc) + * @see org.springframework.batch.item.ItemStreamSupport#update(org.springframework.batch.item.ExecutionContext) + */ + @Override + public void update(ExecutionContext executionContext) + throws ItemStreamException { + try { + executionContext.put(getExecutionContextKey(checkpointKey), deepCopy(doCheckpoint())); + } catch (Exception e) { + throw new ItemStreamException(e); + } + } + + /** + * Used to provide a {@link Serializable} representing the current state of the + * batch artifact. + * + * @return the current state of the batch artifact + * @throws Exception + */ + protected abstract Serializable doCheckpoint() throws Exception; + + /* (non-Javadoc) + * @see org.springframework.batch.item.ItemStreamSupport#close() + */ + @Override + public void close() throws ItemStreamException { + try { + doClose(); + } catch (Exception e) { + throw new ItemStreamException(e); + } + } + + /** + * Used to close the underlying batch artifact + * + * @throws Exception + */ + protected abstract void doClose() throws Exception; + + private Object deepCopy(Serializable orig) { + Object obj = orig; + + try { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ObjectOutputStream out = new ObjectOutputStream(bos); + out.writeObject(orig); + out.flush(); + out.close(); + + ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray())); + obj = in.readObject(); + } catch (Exception e) { + logger.warn("Unable to copy checkpoint object. Updating the instance passed may cause side effects"); + } + + return obj; + } + +} diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/jsr/item/ItemReaderAdapter.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/jsr/item/ItemReaderAdapter.java index 5bef0cf8b..134f82931 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/jsr/item/ItemReaderAdapter.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/jsr/item/ItemReaderAdapter.java @@ -1,64 +1,84 @@ +/* + * Copyright 2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.springframework.batch.jsr.item; import java.io.Serializable; import javax.batch.api.chunk.ItemReader; -import org.springframework.batch.item.ExecutionContext; -import org.springframework.batch.item.ItemStreamException; -import org.springframework.batch.item.ItemStreamSupport; import org.springframework.batch.item.NonTransientResourceException; import org.springframework.batch.item.ParseException; import org.springframework.batch.item.UnexpectedInputException; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +/** + * Adapter that wraps an {@link ItemReader} for use by Spring Batch. All calls are delegated as appropriate + * to the corresponding method on the delegate. + * + * @author Michael Minella + * @since 3.0 + */ @SuppressWarnings("rawtypes") -public class ItemReaderAdapter extends ItemStreamSupport implements org.springframework.batch.item.ItemReader { +public class ItemReaderAdapter extends CheckpointSupport implements org.springframework.batch.item.ItemReader { private static final String CHECKPOINT_KEY = "reader.checkpoint"; private ItemReader delegate; + /** + * @param reader the {@link ItemReader} implementation to delegate to + */ public ItemReaderAdapter(ItemReader reader) { + super(CHECKPOINT_KEY); Assert.notNull(reader, "An ItemReader implementation is required"); this.delegate = reader; setExecutionContextName(ClassUtils.getShortName(delegate.getClass())); } - @Override - public void open(ExecutionContext executionContext) - throws ItemStreamException { - try { - delegate.open((Serializable) executionContext.get(getExecutionContextKey(CHECKPOINT_KEY))); - } catch (Exception e) { - throw new ItemStreamException(e); - } - } - - @Override - public void update(ExecutionContext executionContext) - throws ItemStreamException { - try { - Serializable checkpoint = delegate.checkpointInfo(); - executionContext.put(getExecutionContextKey(CHECKPOINT_KEY), checkpoint); - } catch (Exception e) { - throw new ItemStreamException(e); - } - } - - @Override - public void close() throws ItemStreamException { - try { - delegate.close(); - } catch (Exception e) { - throw new ItemStreamException(e); - } - } - + /* (non-Javadoc) + * @see org.springframework.batch.item.ItemReader#read() + */ @Override public Object read() throws Exception, UnexpectedInputException, ParseException, NonTransientResourceException { return delegate.readItem(); } + + /* (non-Javadoc) + * @see org.springframework.batch.jsr.item.CheckpointSupport#doClose() + */ + @Override + protected void doClose() throws Exception{ + delegate.close(); + } + + /* (non-Javadoc) + * @see org.springframework.batch.jsr.item.CheckpointSupport#doCheckpoint() + */ + @Override + protected Serializable doCheckpoint() throws Exception { + return delegate.checkpointInfo(); + } + + /* (non-Javadoc) + * @see org.springframework.batch.jsr.item.CheckpointSupport#doOpen(java.io.Serializable) + */ + @Override + protected void doOpen(Serializable checkpoint) throws Exception { + delegate.open(checkpoint); + } } diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/jsr/item/ItemWriterAdapter.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/jsr/item/ItemWriterAdapter.java index fc54b7f21..1e976e8d6 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/jsr/item/ItemWriterAdapter.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/jsr/item/ItemWriterAdapter.java @@ -1,3 +1,18 @@ +/* + * Copyright 2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.springframework.batch.jsr.item; import java.io.Serializable; @@ -5,58 +20,64 @@ import java.util.List; import javax.batch.api.chunk.ItemWriter; -import org.springframework.batch.item.ExecutionContext; -import org.springframework.batch.item.ItemStreamException; -import org.springframework.batch.item.ItemStreamSupport; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +/** + * Adapter that wraps an {@link ItemWriter} for use by Spring Batch. All calls are delegated as appropriate + * to the corresponding method on the delegate. + * + * @author Michael Minella + * @since 3.0 + */ @SuppressWarnings("rawtypes") -public class ItemWriterAdapter extends ItemStreamSupport implements org.springframework.batch.item.ItemWriter { +public class ItemWriterAdapter extends CheckpointSupport implements org.springframework.batch.item.ItemWriter { private static final String CHECKPOINT_KEY = "writer.checkpoint"; private ItemWriter delegate; + /** + * @param writer a {@link ItemWriter} to delegate calls to + */ public ItemWriterAdapter(ItemWriter writer) { + super(CHECKPOINT_KEY); Assert.notNull(writer, "An ItemWriter implementation is required"); this.delegate = writer; super.setExecutionContextName(ClassUtils.getShortName(delegate.getClass())); } - @Override - public void open(ExecutionContext executionContext) - throws ItemStreamException { - try { - delegate.open((Serializable) executionContext.get(getExecutionContextKey(CHECKPOINT_KEY))); - } catch (Exception e) { - throw new ItemStreamException(e); - } - } - - @Override - public void update(ExecutionContext executionContext) - throws ItemStreamException { - try { - Serializable checkpoint = delegate.checkpointInfo(); - executionContext.put(getExecutionContextKey(CHECKPOINT_KEY), checkpoint); - } catch (Exception e) { - throw new ItemStreamException(e); - } - } - - @Override - public void close() throws ItemStreamException { - try { - delegate.close(); - } catch (Exception e) { - throw new ItemStreamException(e); - } - } - + /* (non-Javadoc) + * @see org.springframework.batch.item.ItemWriter#write(java.util.List) + */ @Override @SuppressWarnings("unchecked") public void write(List items) throws Exception { delegate.writeItems(items); } + + /* (non-Javadoc) + * @see org.springframework.batch.jsr.item.CheckpointSupport#doOpen(java.io.Serializable) + */ + @Override + protected void doOpen(Serializable checkpoint) throws Exception { + delegate.open(checkpoint); + } + + /* (non-Javadoc) + * @see org.springframework.batch.jsr.item.CheckpointSupport#doCheckpoint() + */ + @Override + protected Serializable doCheckpoint() throws Exception { + Serializable checkpointInfo = delegate.checkpointInfo(); + return checkpointInfo; + } + + /* (non-Javadoc) + * @see org.springframework.batch.jsr.item.CheckpointSupport#doClose() + */ + @Override + protected void doClose() throws Exception{ + delegate.close(); + } } diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/jsr/item/ItemReaderAdapterTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/jsr/item/ItemReaderAdapterTests.java index 9f9e4e95c..cf934c059 100644 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/jsr/item/ItemReaderAdapterTests.java +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/jsr/item/ItemReaderAdapterTests.java @@ -5,6 +5,10 @@ import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + import javax.batch.api.chunk.ItemReader; import org.junit.Before; @@ -88,4 +92,79 @@ public class ItemReaderAdapterTests { assertEquals("item", adapter.read()); } + + @Test + @SuppressWarnings("serial") + public void testCheckpointChange() throws Exception { + ItemReaderAdapter adapter = new ItemReaderAdapter(new ItemReader() { + + private CheckpointContainer container = null; + private List items = new ArrayList() {{ + add("foo"); + add("bar"); + add("baz"); + }}; + + @Override + public Object readItem() throws Exception { + int index = container.getCount(); + + if(index < items.size()) { + container.setCount(index + 1); + return items.get(index); + } else { + return null; + } + } + + @Override + public void open(Serializable checkpoint) throws Exception { + container = new CheckpointContainer(); + } + + @Override + public void close() throws Exception { + } + + @Override + public Serializable checkpointInfo() throws Exception { + return container; + } + }); + + ExecutionContext context = new ExecutionContext(); + + adapter.open(context); + adapter.read(); + adapter.read(); + adapter.update(context); + adapter.read(); + adapter.close(); + + CheckpointContainer container = (CheckpointContainer) context.get("ItemReaderAdapterTests.1.reader.checkpoint"); + assertEquals(2, container.getCount()); + + } + + public static class CheckpointContainer implements Serializable{ + private static final long serialVersionUID = 1L; + private int count; + + public CheckpointContainer() { + count = 0; + } + + public int getCount() { + return count; + } + + public void setCount(int count) { + this.count = count; + } + + @Override + public String toString() { + return "CheckpointContinaer has a count of " + count; + } + } } diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/jsr/item/ItemWriterAdapterTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/jsr/item/ItemWriterAdapterTests.java index 248281703..8f53fb74a 100644 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/jsr/item/ItemWriterAdapterTests.java +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/jsr/item/ItemWriterAdapterTests.java @@ -1,9 +1,11 @@ package org.springframework.batch.jsr.item; +import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.io.Serializable; import java.util.ArrayList; import java.util.List; @@ -96,4 +98,70 @@ public class ItemWriterAdapterTests { verify(delegate).writeItems(items); } + + @Test + public void testCheckpointChange() throws Exception { + ItemWriterAdapter adapter = new ItemWriterAdapter(new ItemWriter() { + + private CheckpointContainer container = null; + + @Override + public void open(Serializable checkpoint) throws Exception { + container = new CheckpointContainer(); + } + + @Override + public void close() throws Exception { + } + + @Override + public void writeItems(List items) throws Exception { + container.setCount(container.getCount() + items.size()); + } + + @Override + public Serializable checkpointInfo() throws Exception { + return container; + } + }); + + ExecutionContext context = new ExecutionContext(); + + List items = new ArrayList(); + items.add("foo"); + items.add("bar"); + items.add("baz"); + adapter.open(context); + adapter.write(items); + adapter.update(context); + adapter.write(items); + adapter.close(); + + CheckpointContainer container = (CheckpointContainer) context.get("ItemWriterAdapterTests.1.writer.checkpoint"); + assertEquals(3, container.getCount()); + + } + + public static class CheckpointContainer implements Serializable{ + private static final long serialVersionUID = 1L; + + private int count; + + public CheckpointContainer() { + count = 0; + } + + public int getCount() { + return count; + } + + public void setCount(int count) { + this.count = count; + } + + @Override + public String toString() { + return "CheckpointContinaer has a count of " + count; + } + } }