Introduce read/write API for archived content
StorageService always had an archive() method to create
a history trail for entities, e.g. projects, but there was
no way to retrieve archived content, or backup/restore
the archive via REST API.
This patch add a couple of new methods to StorageService:
* readFromArchive() retrieves the history of an entity and
passes it to a consumer function (StorageConsumer). This
allows the JPA implementation to make use of database
cursors for minimal memory consumption.
* writeToArchive() allows to push new entries to the
history bypassing the archive() function. This will be
used in a subsequent patch to implement a restore API.
All known implementations of StorageService are adapted
in this patch, and some more archiving tests have been
added.
Change-Id: I1adbe767efb58c99f91ce12daaf88ed64dc62117
Signed-off-by: Michael Ochmann <michael.ochmann@sap.com>
diff --git a/org.eclipse.skalli.api/src/main/java/org/eclipse/skalli/services/persistence/StorageConsumer.java b/org.eclipse.skalli.api/src/main/java/org/eclipse/skalli/services/persistence/StorageConsumer.java
new file mode 100644
index 0000000..65d27b1
--- /dev/null
+++ b/org.eclipse.skalli.api/src/main/java/org/eclipse/skalli/services/persistence/StorageConsumer.java
@@ -0,0 +1,32 @@
+/*******************************************************************************
+ * Copyright (c) 2010-2016 SAP AG and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * SAP AG - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.skalli.services.persistence;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Represent a consumer of stored items.
+ */
+public interface StorageConsumer {
+
+ /**
+ * Consumes the provided content.
+ *
+ * @param category
+ * @param key
+ * @param lastModified
+ * @param blob
+ *
+ * @throws IOException
+ */
+ public void consume(String category, String key, long lastModified, InputStream blob) throws IOException;
+}
diff --git a/org.eclipse.skalli.api/src/main/java/org/eclipse/skalli/services/persistence/StorageService.java b/org.eclipse.skalli.api/src/main/java/org/eclipse/skalli/services/persistence/StorageService.java
index 1290d28..d2397fd 100644
--- a/org.eclipse.skalli.api/src/main/java/org/eclipse/skalli/services/persistence/StorageService.java
+++ b/org.eclipse.skalli.api/src/main/java/org/eclipse/skalli/services/persistence/StorageService.java
@@ -56,6 +56,26 @@
public void archive(String category, String key) throws IOException;
/**
+ * Writes the given content to the archive
+ *
+ * @param category
+ * @param key
+ * @param blob
+ * @throws IOException if an i/o error occured while eriting to the archive.
+ */
+ public void writeToArchive(String category, String key, long timestamp, InputStream blob) throws IOException;
+
+ /**
+ * Provides all archived content for the given category and key to the specified consumer.
+ *
+ * @param category category of archived items to provide.
+ * @param key the unique key of the content within its category.
+ * @param consumer consumer for archived items.
+ * @throws IOException if an i/o error occured while reading content from the store.
+ */
+ public void readFromArchive(String category, String key, StorageConsumer consumer) throws IOException;
+
+ /**
* Returns the keys of storage entries for the given category.
*
* @param category category or type of the content.
diff --git a/org.eclipse.skalli.core/src/main/java/org/eclipse/skalli/core/storage/FileStorageComponent.java b/org.eclipse.skalli.core/src/main/java/org/eclipse/skalli/core/storage/FileStorageComponent.java
index cf4f657..4870e5b 100644
--- a/org.eclipse.skalli.core/src/main/java/org/eclipse/skalli/core/storage/FileStorageComponent.java
+++ b/org.eclipse.skalli.core/src/main/java/org/eclipse/skalli/core/storage/FileStorageComponent.java
@@ -23,7 +23,10 @@
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
+import org.eclipse.skalli.core.storage.Historian.HistoryEntry;
+import org.eclipse.skalli.core.storage.Historian.HistoryIterator;
import org.eclipse.skalli.services.BundleProperties;
+import org.eclipse.skalli.services.persistence.StorageConsumer;
import org.eclipse.skalli.services.persistence.StorageService;
import org.osgi.service.component.ComponentConstants;
import org.osgi.service.component.ComponentContext;
@@ -68,18 +71,6 @@
(String) context.getProperties().get(ComponentConstants.COMPONENT_NAME)));
}
- private String getPath(String category, String key) {
- return category + "/" + key; //$NON-NLS-1$
- }
-
- private File getFile(String category, String key) {
- File path = new File(storageBase, category);
- if (!path.exists()) {
- path.mkdirs();
- }
- return new File(path, key + ".xml"); //$NON-NLS-1$
- }
-
@Override
public void write(String category, String key, InputStream blob) throws IOException {
File file = getFile(category, key);
@@ -100,34 +91,27 @@
}
@Override
- public InputStream read(String category, String key) throws IOException {
- File file = getFile(category, key);
+ public void writeToArchive(String category, String id, long timestamp, InputStream blob) throws IOException {
+ new Historian(new File(storageBase, category)).historize(id, timestamp, blob);
+ }
+
+ @Override
+ public void readFromArchive(String category, String key, StorageConsumer consumer) throws IOException {
+ HistoryIterator history = null;
try {
- return new FileInputStream(file);
- } catch (FileNotFoundException e) {
- return null;
+ history = new Historian(new File(storageBase, category)).getHistory(key);
+ while (history.hasNext()) {
+ HistoryEntry next = history.next();
+ consumer.consume(category, key, next.getTimestamp(), IOUtils.toInputStream(next.getContent()));
+ }
+ } finally {
+ history.close();
}
}
- private final File getDefaultStorageDirectory() {
- File storageDirectory = null;
- String workdir = BundleProperties.getProperty(BundleProperties.PROPERTY_WORKDIR);
- if (workdir != null) {
- File workingDirectory = new File(workdir);
- if (workingDirectory.exists() && workingDirectory.isDirectory()) {
- storageDirectory = new File(workingDirectory, STORAGE_BASE);
- } else {
- LOG.warn("Working directory '" + workingDirectory.getAbsolutePath()
- + "' not found - falling back to current directory");
- }
- }
- if (storageDirectory == null) {
- // fall back: use current directory as working directory
- storageDirectory = new File(STORAGE_BASE);
- }
-
- LOG.info("Using storage directory '" + storageDirectory.getAbsolutePath() + "'");
- return storageDirectory;
+ @Override
+ public InputStream read(String category, String key) throws IOException {
+ return toStream(getFile(category, key));
}
@Override
@@ -149,4 +133,45 @@
return list;
}
+
+ private static File getDefaultStorageDirectory() {
+ File storageDirectory = null;
+ String workdir = BundleProperties.getProperty(BundleProperties.PROPERTY_WORKDIR);
+ if (workdir != null) {
+ File workingDirectory = new File(workdir);
+ if (workingDirectory.exists() && workingDirectory.isDirectory()) {
+ storageDirectory = new File(workingDirectory, STORAGE_BASE);
+ } else {
+ LOG.warn("Working directory '" + workingDirectory.getAbsolutePath()
+ + "' not found - falling back to current directory");
+ }
+ }
+ if (storageDirectory == null) {
+ // fall back: use current directory as working directory
+ storageDirectory = new File(STORAGE_BASE);
+ }
+
+ LOG.info("Using storage directory '" + storageDirectory.getAbsolutePath() + "'");
+ return storageDirectory;
+ }
+
+ private File getFile(String category, String key) {
+ File path = new File(storageBase, category);
+ if (!path.exists()) {
+ path.mkdirs();
+ }
+ return new File(path, key + ".xml"); //$NON-NLS-1$
+ }
+
+ private static String getPath(String category, String key) {
+ return category + "/" + key; //$NON-NLS-1$
+ }
+
+ private static InputStream toStream(File file) {
+ try {
+ return file != null && file.exists() && file.isFile() && file.canRead() ? new FileInputStream(file) : null;
+ } catch (FileNotFoundException e) {
+ return null;
+ }
+ }
}
diff --git a/org.eclipse.skalli.core/src/main/java/org/eclipse/skalli/core/storage/Historian.java b/org.eclipse.skalli.core/src/main/java/org/eclipse/skalli/core/storage/Historian.java
index 5d1d816..10f2a71 100644
--- a/org.eclipse.skalli.core/src/main/java/org/eclipse/skalli/core/storage/Historian.java
+++ b/org.eclipse.skalli.core/src/main/java/org/eclipse/skalli/core/storage/Historian.java
@@ -58,6 +58,24 @@
}
}
+ void historize(String id, long timestamp, InputStream blob) throws IOException {
+ OutputStream out = null;
+ try {
+ byte[] buf = IOUtils.toByteArray(blob);
+ out = new BufferedOutputStream(new FileOutputStream(historyFile, historyFile.exists()));
+ String header = MessageFormat.format("{0}:{1}:{2}", //$NON-NLS-1$
+ getNextEntryName(id),
+ Long.toString(buf.length),
+ Long.toString(timestamp));
+ out.write(header.getBytes("UTF-8")); //$NON-NLS-1$
+ out.write(CRLF.getBytes("UTF-8")); //$NON-NLS-1$
+ out.write(buf);
+ out.write(CRLF.getBytes("UTF-8")); //$NON-NLS-1$
+ } finally {
+ IOUtils.closeQuietly(out);
+ }
+ }
+
String getNextEntryName(String fileName) throws IOException {
int count = 0;
InputStream in = null;
diff --git a/org.eclipse.skalli.jpa.test/src/main/test/org/eclipse/skalli/core/storage/jpa/JPAStorageComponentTest.java b/org.eclipse.skalli.jpa.test/src/main/test/org/eclipse/skalli/core/storage/jpa/JPAStorageComponentTest.java
index 9a76195..c958481 100644
--- a/org.eclipse.skalli.jpa.test/src/main/test/org/eclipse/skalli/core/storage/jpa/JPAStorageComponentTest.java
+++ b/org.eclipse.skalli.jpa.test/src/main/test/org/eclipse/skalli/core/storage/jpa/JPAStorageComponentTest.java
@@ -10,15 +10,11 @@
*******************************************************************************/
package org.eclipse.skalli.core.storage.jpa;
-import static org.junit.Assert.*;
-
-import java.io.ByteArrayInputStream;
-import java.util.List;
+import static org.junit.Assert.fail;
import org.eclipse.skalli.services.persistence.StorageService;
import org.eclipse.skalli.testutil.BundleManager;
import org.eclipse.skalli.testutil.StorageServiceTestBase;
-import org.junit.Test;
@SuppressWarnings("nls")
public class JPAStorageComponentTest extends StorageServiceTestBase {
@@ -32,46 +28,4 @@
}
return jpaStorageService;
}
-
- @Test
- public void testArchive() throws Exception {
- final String TEST_CATEGORY = "test_archive";
-
- StorageService service = getStorageService();
- if (!(service instanceof JPAStorageComponent)) {
- return;
- }
-
- JPAStorageComponent pdb = (JPAStorageComponent)service;
-
- // initially empty
- List<HistoryStorageItem> items = pdb.getHistory(TEST_CATEGORY, TEST_ID);
- assertTrue(items.isEmpty());
-
- // archive non existing element, should do nothing
- pdb.archive(TEST_CATEGORY, TEST_ID);
- items = pdb.getHistory(TEST_CATEGORY, TEST_ID);
- assertTrue(items.isEmpty());
-
- // create item
- ByteArrayInputStream is = new ByteArrayInputStream(TEST_CONTENT.getBytes("UTF-8"));
- pdb.write(TEST_CATEGORY, TEST_ID, is);
-
- // first archive step
- pdb.archive(TEST_CATEGORY, TEST_ID);
- items = pdb.getHistory(TEST_CATEGORY, TEST_ID);
- assertTrue(items.size() == 1);
- assertTrue(items.get(0).getDateCreated() != null);
- assertEquals(TEST_ID, items.get(0).getId());
-
- // second archive step
- pdb.archive(TEST_CATEGORY, TEST_ID);
- items = pdb.getHistory(TEST_CATEGORY, TEST_ID);
- assertTrue(items.size() == 2);
- assertTrue(items.get(0).getDateCreated() != null);
- assertEquals(TEST_ID, items.get(0).getId());
- assertTrue(items.get(1).getDateCreated() != null);
- assertEquals(TEST_ID, items.get(1).getId());
- }
-
}
\ No newline at end of file
diff --git a/org.eclipse.skalli.jpa/META-INF/MANIFEST.MF b/org.eclipse.skalli.jpa/META-INF/MANIFEST.MF
index 7370816..c9dae0a 100644
--- a/org.eclipse.skalli.jpa/META-INF/MANIFEST.MF
+++ b/org.eclipse.skalli.jpa/META-INF/MANIFEST.MF
@@ -9,6 +9,8 @@
org.apache.commons.io,
org.apache.commons.lang,
org.eclipse.persistence.annotations,
+ org.eclipse.persistence.config,
+ org.eclipse.persistence.queries,
org.eclipse.skalli.commons,
org.eclipse.skalli.services.feed,
org.eclipse.skalli.services.persistence,
diff --git a/org.eclipse.skalli.jpa/src/main/java/org/eclipse/skalli/core/storage/jpa/HistoryStorageItem.java b/org.eclipse.skalli.jpa/src/main/java/org/eclipse/skalli/core/storage/jpa/HistoryStorageItem.java
index 92abe21..377666e 100644
--- a/org.eclipse.skalli.jpa/src/main/java/org/eclipse/skalli/core/storage/jpa/HistoryStorageItem.java
+++ b/org.eclipse.skalli.jpa/src/main/java/org/eclipse/skalli/core/storage/jpa/HistoryStorageItem.java
@@ -46,49 +46,35 @@
@Column(length = 100000)
private String content;
- public HistoryStorageItem() {
- // just needed for JPA
- }
-
- /**
- * Creates a HistoryStorageItem using the current date.
- *
- */
- public HistoryStorageItem(StorageItem item) {
- this.category = item.getCategory();
- this.id = item.getId();
- this.content = item.getContent();
-
- this.dateCreated = new Date();
- }
-
- /**
- * Creates a HistoryStorageItem using the supplied date.
- *
- */
- public HistoryStorageItem(StorageItem item, Date dateCreated) {
- this(item);
-
- this.dateCreated = dateCreated;
- }
-
- public int getAutoId() {
- return autoId;
- }
-
public String getCategory() {
return category;
}
+ public void setCategory(String category) {
+ this.category = category;
+ }
+
public String getId() {
return id;
}
- public String getContent() {
- return content;
+ public void setId(String id) {
+ this.id = id;
}
public Date getDateCreated() {
return dateCreated;
}
+
+ public void setDateCreated(Date dateCreated) {
+ this.dateCreated = dateCreated;
+ }
+
+ public String getContent() {
+ return content;
+ }
+
+ public void setContent(String content) {
+ this.content = content;
+ }
}
diff --git a/org.eclipse.skalli.jpa/src/main/java/org/eclipse/skalli/core/storage/jpa/JPAStorageComponent.java b/org.eclipse.skalli.jpa/src/main/java/org/eclipse/skalli/core/storage/jpa/JPAStorageComponent.java
index 341c493..573d834 100644
--- a/org.eclipse.skalli.jpa/src/main/java/org/eclipse/skalli/core/storage/jpa/JPAStorageComponent.java
+++ b/org.eclipse.skalli.jpa/src/main/java/org/eclipse/skalli/core/storage/jpa/JPAStorageComponent.java
@@ -13,18 +13,22 @@
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
-import java.io.UnsupportedEncodingException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import javax.persistence.EntityManager;
+import javax.persistence.Query;
import javax.persistence.TypedQuery;
import org.apache.commons.io.IOUtils;
+import org.eclipse.persistence.config.HintValues;
+import org.eclipse.persistence.config.QueryHints;
+import org.eclipse.persistence.queries.CursoredStream;
import org.eclipse.skalli.services.persistence.EntityManagerService;
import org.eclipse.skalli.services.persistence.EntityManagerServiceBase;
+import org.eclipse.skalli.services.persistence.StorageConsumer;
import org.eclipse.skalli.services.persistence.StorageService;
import org.osgi.service.component.ComponentConstants;
import org.osgi.service.component.ComponentContext;
@@ -35,6 +39,9 @@
private static final Logger LOG = LoggerFactory.getLogger(JPAStorageComponent.class);
+ // page size for mass operations
+ private static final int PAGE_SIZE = 100;
+
@Override
protected void activate(ComponentContext context) {
super.activate(context);
@@ -52,7 +59,6 @@
@Override
public void write(String category, String id, InputStream blob) throws IOException {
EntityManager em = getEntityManager();
-
em.getTransaction().begin();
try {
StorageItem item = findStorageItem(category, id, em);
@@ -61,15 +67,13 @@
newItem.setId(id);
newItem.setCategory(category);
newItem.setDateModified(new Date());
- newItem.setContent(IOUtils.toString(blob, "UTF-8"));
+ newItem.setContent(IOUtils.toString(blob, "UTF-8")); //$NON-NLS-1$
em.persist(newItem);
} else { //update
item.setDateModified(new Date());
- item.setContent(IOUtils.toString(blob, "UTF-8"));
+ item.setContent(IOUtils.toString(blob, "UTF-8")); //$NON-NLS-1$
}
em.getTransaction().commit();
- } catch (Exception e) {
- throw new IOException("Failed to write data", e);
} finally {
em.close();
}
@@ -78,42 +82,79 @@
@Override
public InputStream read(String category, String id) throws IOException {
EntityManager em = getEntityManager();
-
ByteArrayInputStream returnStream = null;
try {
StorageItem item = findStorageItem(category, id, em);
if (item != null) {
- returnStream = new ByteArrayInputStream(item.getContent().getBytes("UTF-8"));
+ returnStream = new ByteArrayInputStream(item.getContent().getBytes("UTF-8")); //$NON-NLS-1$
}
- } catch (UnsupportedEncodingException e) {
- throw new IOException("Failed to read data", e);
} finally {
em.close();
}
-
return returnStream;
}
@Override
public void archive(String category, String id) throws IOException {
EntityManager em = getEntityManager();
-
try {
em.getTransaction().begin();
-
- // find original StorageItem
StorageItem item = findStorageItem(category, id, em);
if (item == null) {
- // nothing to archive
return;
}
-
- // write to HistoryStorage
- HistoryStorageItem histItem = new HistoryStorageItem(item);
+ HistoryStorageItem histItem = new HistoryStorageItem();
+ histItem.setCategory(category);
+ histItem.setId(id);
+ histItem.setContent(item.getContent());
+ histItem.setDateCreated(new Date());
em.persist(histItem);
em.getTransaction().commit();
- } catch (Exception e) {
- throw new IOException("Failed to archive data", e);
+ } finally {
+ em.close();
+ }
+ }
+
+ @Override
+ public void writeToArchive(String category, String id, long timestamp, InputStream blob) throws IOException {
+ EntityManager em = getEntityManager();
+ try {
+ em.getTransaction().begin();
+ HistoryStorageItem histItem = new HistoryStorageItem();
+ histItem.setCategory(category);
+ histItem.setId(id);
+ histItem.setContent(IOUtils.toString(blob, "UTF-8")); //$NON-NLS-1$
+ histItem.setDateCreated(new Date(timestamp));
+ em.persist(histItem);
+ em.getTransaction().commit();
+ } finally {
+ em.close();
+ }
+ }
+
+ @Override
+ public void readFromArchive(String category, String id, StorageConsumer consumer) throws IOException {
+ EntityManager em = getEntityManager();
+ try {
+ Query query = em.createNamedQuery("getItemsByCompositeKey", HistoryStorageItem.class); //$NON-NLS-1$
+ query.setHint(QueryHints.CURSOR, HintValues.TRUE);
+ query.setHint(QueryHints.CURSOR_INITIAL_SIZE, PAGE_SIZE);
+ query.setHint(QueryHints.CURSOR_PAGE_SIZE, PAGE_SIZE);
+ query.setParameter("category", category); //$NON-NLS-1$
+ query.setParameter("id", id); //$NON-NLS-1$
+ CursoredStream cursor = null;
+ try {
+ cursor = (CursoredStream) query.getSingleResult();
+ while (cursor.hasNext()) {
+ HistoryStorageItem next = (HistoryStorageItem)cursor.next();
+ consumer.consume(next.getCategory(), next.getId(), next.getDateCreated().getTime(),
+ asStream(next.getContent()));
+ }
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
} finally {
em.close();
}
@@ -122,47 +163,22 @@
@Override
public List<String> keys(String category) throws IOException {
EntityManager em = getEntityManager();
-
List<String> resultList = new ArrayList<String>();
try {
- TypedQuery<String> query = em.createNamedQuery("getIdsByCategory", String.class);
- query.setParameter("category", category);
+ TypedQuery<String> query = em.createNamedQuery("getIdsByCategory", String.class); //$NON-NLS-1$
+ query.setParameter("category", category); //$NON-NLS-1$
resultList = query.getResultList();
- } catch (Exception e) {
- throw new IOException("Failed to retrieve IDs", e);
} finally {
em.close();
}
-
return resultList;
}
- public List<HistoryStorageItem> getHistory(String category, String id) throws IOException {
- EntityManager em = getEntityManager();
-
- List<HistoryStorageItem> resultList;
- try {
- TypedQuery<HistoryStorageItem> query = em.createNamedQuery("getItemsByCompositeKey",
- HistoryStorageItem.class);
- query.setParameter("category", category);
- query.setParameter("id", id);
- resultList = query.getResultList();
- } catch (Exception e) {
- throw new IOException("Failed to retrieve historical data", e);
- } finally {
- em.close();
- }
-
- return resultList;
+ private static StorageItem findStorageItem(String category, String id, EntityManager em) {
+ return em.find(StorageItem.class, new StorageId(category, id));
}
- private StorageItem findStorageItem(String category, String id, EntityManager em) throws IOException {
- StorageItem item;
- try {
- item = em.find(StorageItem.class, new StorageId(category, id));
- } catch (Exception e) {
- throw new IOException("Failed to find item", e);
- }
- return item;
+ private static InputStream asStream(String content) throws IOException {
+ return new ByteArrayInputStream(content.getBytes("UTF-8")); //$NON-NLS-1$
}
}
diff --git a/org.eclipse.skalli.jpa/src/main/java/org/eclipse/skalli/core/storage/jpa/StorageItem.java b/org.eclipse.skalli.jpa/src/main/java/org/eclipse/skalli/core/storage/jpa/StorageItem.java
index 4ae7ad7..25d8a98 100644
--- a/org.eclipse.skalli.jpa/src/main/java/org/eclipse/skalli/core/storage/jpa/StorageItem.java
+++ b/org.eclipse.skalli.jpa/src/main/java/org/eclipse/skalli/core/storage/jpa/StorageItem.java
@@ -69,5 +69,4 @@
public void setDateModified(Date dateModified) {
this.dateModified = dateModified;
}
-
}
\ No newline at end of file
diff --git a/org.eclipse.skalli.testutil/src/main/java/org/eclipse/skalli/testutil/HashMapStorageService.java b/org.eclipse.skalli.testutil/src/main/java/org/eclipse/skalli/testutil/HashMapStorageService.java
index 6e09b3d..0c85d17 100644
--- a/org.eclipse.skalli.testutil/src/main/java/org/eclipse/skalli/testutil/HashMapStorageService.java
+++ b/org.eclipse.skalli.testutil/src/main/java/org/eclipse/skalli/testutil/HashMapStorageService.java
@@ -22,6 +22,7 @@
import java.util.Set;
import org.apache.commons.io.IOUtils;
+import org.eclipse.skalli.services.persistence.StorageConsumer;
import org.eclipse.skalli.services.persistence.StorageService;
/**
@@ -59,17 +60,27 @@
StorageKey key = keyOf(category, id);
ByteArrayStorageItem item = store.get(key);
if (item != null) {
- List<ByteArrayStorageItem> items = archive.get(key);
- if (items == null) {
- items = new ArrayList<ByteArrayStorageItem>();
- archive.put(key, items);
- }
- items.add(new ByteArrayStorageItem(key, IOUtils.toByteArray(item.getContent())));
+ writeToArchive(key, System.currentTimeMillis(), item.getContent());
}
return;
}
@Override
+ public void writeToArchive(String category, String id, long timestamp, InputStream blob) throws IOException {
+ writeToArchive(keyOf(category, id), timestamp, blob);
+ }
+
+ @Override
+ public void readFromArchive(String category, String id, StorageConsumer consumer) throws IOException {
+ List<ByteArrayStorageItem> items = archive.get(keyOf(category, id));
+ if (items != null) {
+ for (ByteArrayStorageItem next: items) {
+ consumer.consume(category, next.getId(), next.lastModified(), next.getContent());
+ }
+ }
+ }
+
+ @Override
public List<String> keys(String category) throws IOException {
List<String> result = new ArrayList<String>();
Set<StorageKey> allKeys = store.keySet();
@@ -85,4 +96,13 @@
public String toString() {
return "HashMapStorageService [blobStore=" + store + "]";
}
-}
\ No newline at end of file
+
+ private void writeToArchive(StorageKey key, long timestamp, InputStream blob) throws IOException {
+ List<ByteArrayStorageItem> items = archive.get(key);
+ if (items == null) {
+ items = new ArrayList<ByteArrayStorageItem>();
+ archive.put(key, items);
+ }
+ items.add(new ByteArrayStorageItem(key, timestamp, IOUtils.toByteArray(blob)));
+ }
+}
diff --git a/org.eclipse.skalli.testutil/src/main/java/org/eclipse/skalli/testutil/StorageServiceTestBase.java b/org.eclipse.skalli.testutil/src/main/java/org/eclipse/skalli/testutil/StorageServiceTestBase.java
index 3d42c05..fbdaee7 100644
--- a/org.eclipse.skalli.testutil/src/main/java/org/eclipse/skalli/testutil/StorageServiceTestBase.java
+++ b/org.eclipse.skalli.testutil/src/main/java/org/eclipse/skalli/testutil/StorageServiceTestBase.java
@@ -13,11 +13,14 @@
import static org.junit.Assert.*;
import java.io.ByteArrayInputStream;
+import java.io.IOException;
import java.io.InputStream;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.apache.commons.io.IOUtils;
+import org.eclipse.skalli.services.persistence.StorageConsumer;
import org.eclipse.skalli.services.persistence.StorageService;
import org.junit.Test;
@@ -43,29 +46,9 @@
@Test
public void testReadWrite() throws Exception {
final String TEST_CATEGORY = "test_readwrite";
-
writeContent(TEST_CATEGORY, TEST_ID, TEST_CONTENT);
-
String outputText = readContent(TEST_CATEGORY);
-
assertEquals(TEST_CONTENT, outputText);
-
- }
-
- private String readContent(final String TEST_CATEGORY) throws Exception {
- StorageService storageService = getStorageService();
- InputStream stream = storageService.read(TEST_CATEGORY, TEST_ID);
- String outputText = IOUtils.toString(stream, "UTF-8");
- stream.close();
- return outputText;
- }
-
- private void writeContent(final String TEST_CATEGORY, String id, String content) throws Exception {
- StorageService storageService = getStorageService();
- ByteArrayInputStream is = new ByteArrayInputStream(content.getBytes("UTF-8"));
- storageService.write(TEST_CATEGORY, TEST_ID, is);
- is.close();
- return;
}
@Test
@@ -127,4 +110,109 @@
stream.close();
}
+
+ @Test
+ public void testArchive() throws Exception {
+ final String TEST_CATEGORY = "test_archive";
+
+ StorageService storageService = getStorageService();
+ // initially empty
+ List<ByteArrayStorageItem> items = getHistory(storageService, TEST_CATEGORY, TEST_ID);
+ assertTrue(items.isEmpty());
+
+ // archive non existing element, should do nothing
+ storageService.archive(TEST_CATEGORY, TEST_ID);
+ items = getHistory(storageService, TEST_CATEGORY, TEST_ID);
+ assertTrue(items.isEmpty());
+
+ // create item
+ ByteArrayInputStream is = new ByteArrayInputStream(TEST_CONTENT.getBytes("UTF-8"));
+ storageService.write(TEST_CATEGORY, TEST_ID, is);
+
+ // first archive step
+ storageService.archive(TEST_CATEGORY, TEST_ID);
+ items = getHistory(storageService, TEST_CATEGORY, TEST_ID);
+ assertEquals(1, items.size());
+ assertEquals(TEST_CATEGORY, items.get(0).getCategory());
+ assertEquals(TEST_ID, items.get(0).getId());
+ assertEquals(TEST_CONTENT, IOUtils.toString(items.get(0).getContent()));
+ assertTrue(items.get(0).lastModified() != 0);
+
+ // second archive step
+ storageService.archive(TEST_CATEGORY, TEST_ID);
+ items = getHistory(storageService, TEST_CATEGORY, TEST_ID);
+ assertEquals(2, items.size());
+ assertTrue(items.get(0).lastModified() != 0);
+ assertEquals(TEST_ID, items.get(0).getId());
+ assertTrue(items.get(1).lastModified() != 0);
+ assertTrue(items.get(1).lastModified() >= items.get(0).lastModified());
+ assertEquals(TEST_ID, items.get(1).getId());
+ }
+
+ @Test
+ public void testReadWriteArchive() throws Exception {
+ final String TEST_CATEGORY = "test_large_archive";
+
+ StorageService storageService = getStorageService();
+
+ // create item
+ ByteArrayInputStream is = new ByteArrayInputStream(TEST_CONTENT.getBytes("UTF-8"));
+ storageService.write(TEST_CATEGORY, TEST_ID, is);
+
+ // create some archive entries
+ for (int i = 0; i < 10; ++i) {
+ storageService.archive(TEST_CATEGORY, TEST_ID);
+ }
+
+ // read these entries and check that the timestamps form a series
+ List<ByteArrayStorageItem> items = getHistory(storageService, TEST_CATEGORY, TEST_ID);
+ assertEquals(10, items.size());
+ for (int i = 0; i < 10; ++i) {
+ assertEquals(TEST_CATEGORY, items.get(i).getCategory());
+ assertEquals(TEST_ID, items.get(i).getId());
+ assertEquals(TEST_CONTENT, IOUtils.toString(items.get(i).getContent()));
+ if (i > 0) {
+ assertTrue(items.get(i).lastModified() >= items.get(i-1).lastModified());
+ }
+ }
+
+ // write an additional entry
+ long now = System.currentTimeMillis() + 4711;
+ storageService.writeToArchive(TEST_CATEGORY, TEST_ID, now, IOUtils.toInputStream(TEST_CONTENT_UPDATED));
+ items = getHistory(storageService, TEST_CATEGORY, TEST_ID);
+ assertEquals(11, items.size());
+
+ assertEquals(TEST_CATEGORY, items.get(10).getCategory());
+ assertEquals(TEST_ID, items.get(10).getId());
+ assertEquals(TEST_CONTENT_UPDATED, IOUtils.toString(items.get(10).getContent()));
+ assertEquals(now, items.get(10).lastModified());
+ }
+
+ private String readContent(final String TEST_CATEGORY) throws Exception {
+ StorageService storageService = getStorageService();
+ InputStream stream = storageService.read(TEST_CATEGORY, TEST_ID);
+ String outputText = IOUtils.toString(stream, "UTF-8");
+ stream.close();
+ return outputText;
+ }
+
+ private void writeContent(final String TEST_CATEGORY, String id, String content) throws Exception {
+ StorageService storageService = getStorageService();
+ ByteArrayInputStream is = new ByteArrayInputStream(content.getBytes("UTF-8"));
+ storageService.write(TEST_CATEGORY, TEST_ID, is);
+ is.close();
+ return;
+ }
+
+ private List<ByteArrayStorageItem> getHistory(StorageService storageService, String category, String id)
+ throws IOException {
+ final List<ByteArrayStorageItem> items = new ArrayList<ByteArrayStorageItem>();
+ storageService.readFromArchive(category, id, new StorageConsumer(){
+ @Override
+ public void consume(String category, String key, long lastModified, InputStream blob) throws IOException {
+ items.add(new ByteArrayStorageItem(category, key, lastModified, IOUtils.toByteArray(blob)));
+ }
+ });
+ return items;
+ }
}