Add restore for archived content

Change-Id: Ia4287a44eba2261f91fee87b901b319ac0c05874
Signed-off-by: Michael Ochmann <michael.ochmann@sap.com>
diff --git a/org.eclipse.skalli.core/src/main/java/org/eclipse/skalli/core/rest/admin/ProjectBackupResource.java b/org.eclipse.skalli.core/src/main/java/org/eclipse/skalli/core/rest/admin/ProjectBackupResource.java
index 3c62d82..4d680d3 100644
--- a/org.eclipse.skalli.core/src/main/java/org/eclipse/skalli/core/rest/admin/ProjectBackupResource.java
+++ b/org.eclipse.skalli.core/src/main/java/org/eclipse/skalli/core/rest/admin/ProjectBackupResource.java
@@ -25,7 +25,9 @@
 
 import org.apache.commons.io.IOUtils;
 import org.apache.commons.lang.StringUtils;
+import org.apache.commons.lang.math.NumberUtils;
 import org.eclipse.skalli.commons.CollectionUtils;
+import org.eclipse.skalli.commons.FormatUtils;
 import org.eclipse.skalli.commons.ThreadPool;
 import org.eclipse.skalli.core.storage.FileStorageComponent;
 import org.eclipse.skalli.services.BundleProperties;
@@ -136,7 +138,10 @@
             setStatus(Status.SUCCESS_NO_CONTENT);
             return null;
         }
+        boolean withHistory = accepted.contains("History"); //$NON-NLS-1$
 
+        int countItems = 0;
+        int countHistoryItems = 0;
         ZipInputStream zipStream = null;
         try {
             zipStream = new ZipInputStream(entity.getStream());
@@ -150,22 +155,42 @@
                             LOG.info(MessageFormat.format("Restore: {0} is not recognized as entity key", entryName));
                             continue;
                         }
+                        String category = parts[0];
+                        String name = parts[1];
+                        if (name.endsWith(".xml")) { //$NON-NLS-1$
+                            name = name.substring(0, name.length() - 4);
+                        };
+                        String[] nameParts = StringUtils.split(name, '_');
+                        String key = nameParts[0];
+                        long timestamp = NumberUtils.toLong(nameParts.length == 2 ? nameParts[1] : null, -1L);
+
                         // ensure that the category of the entry, i.e. the directory name,
                         // is in the set of accepted categories
-                        String category = parts[0];
-                        String key = parts[1];
                         if (accepted.contains(category)) {
-                            if (key.endsWith(".xml")) { //$NON-NLS-1$
-                                key = key.substring(0, key.length() - 4);
-                            }
-                            try {
-                                storageService.write(category, key, zipStream);
-                            } catch (IOException e) {
-                                LOG.error(MessageFormat.format(
-                                        "Failed to store entity with key {0} and category {1} ({2})",
-                                        key, category, ERROR_ID_FAILED_TO_STORE), e);
-                                return createErrorRepresentation(Status.SERVER_ERROR_INTERNAL, ERROR_ID_FAILED_TO_STORE,
-                                        "Failed to store the attached backup");
+                            if (timestamp < 0) {
+                                try {
+                                    storageService.write(category, key, zipStream);
+                                    ++countItems;
+                                } catch (IOException e) {
+                                    LOG.error(MessageFormat.format(
+                                            "Failed to store entity with key {0} and category {1} ({2})",
+                                            key, category, ERROR_ID_FAILED_TO_STORE), e);
+                                    return createErrorRepresentation(Status.SERVER_ERROR_INTERNAL, ERROR_ID_FAILED_TO_STORE,
+                                            "Failed to store the attached backup");
+                                }
+                            } else if (withHistory) {
+                                try {
+                                    storageService.writeToArchive(category, key, timestamp, zipStream);
+                                    ++countHistoryItems;
+                                } catch (IOException e) {
+                                    LOG.error(MessageFormat.format(
+                                            "Failed to store history entry for timestamp {0} with key {1} and category {2} ({3})",
+                                            FormatUtils.formatUTCWithMillis(timestamp), key, category,
+                                            ERROR_ID_FAILED_TO_STORE), e);
+                                    return createErrorRepresentation(Status.SERVER_ERROR_INTERNAL,
+                                            ERROR_ID_FAILED_TO_STORE,
+                                            "Failed to store the attached backup");
+                                }
                             }
                         } else {
                             LOG.info(MessageFormat.format("Restore: Excluded {0} (category ''{1}'' not accepted)",
@@ -182,6 +207,9 @@
         } finally {
             IOUtils.closeQuietly(zipStream);
         }
+        if (LOG.isInfoEnabled()) {
+            LOG.info(MessageFormat.format("Restored {0} items and {1} history items", countItems, countHistoryItems));
+        }
 
         // ensure that the persistence service attached to the storage
         // refreshes all caches and reloads all entities --- do that
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 377666e..35921c6 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
@@ -18,6 +18,7 @@
 import javax.persistence.GenerationType;
 import javax.persistence.Id;
 import javax.persistence.Lob;
+import javax.persistence.NamedQueries;
 import javax.persistence.NamedQuery;
 import javax.persistence.Table;
 import javax.persistence.TableGenerator;
@@ -27,7 +28,10 @@
 import org.eclipse.persistence.annotations.Index;
 
 @Table(name = "HistoryStorage")
-@NamedQuery(name = "getItemsByCompositeKey", query = "SELECT r FROM HistoryStorageItem r WHERE r.category = :category AND r.id = :id")
+@NamedQueries({
+    @NamedQuery(name = "getItemsByCompositeKey", query = "SELECT r FROM HistoryStorageItem r WHERE r.category = :category AND r.id = :id"),
+    @NamedQuery(name = "getItemByTimestamp", query = "SELECT r FROM HistoryStorageItem r WHERE r.category = :category AND r.id = :id AND r.dateCreated = :dateCreated")
+})
 @Entity
 public class HistoryStorageItem {
     @Id
@@ -43,7 +47,6 @@
     @Temporal(TemporalType.TIMESTAMP)
     private Date dateCreated;
     @Lob
-    @Column(length = 100000)
     private String content;
 
     public String getCategory() {
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 2c727e6..b5bbd04 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
@@ -147,12 +147,17 @@
         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);
+            HistoryStorageItem item = findHistoryItem(category, id, timestamp, em);
+            if (item == null) {
+                HistoryStorageItem newItem = new HistoryStorageItem();
+                newItem.setCategory(category);
+                newItem.setId(id);
+                newItem.setContent(IOUtils.toString(blob, "UTF-8")); //$NON-NLS-1$
+                newItem.setDateCreated(new Date(timestamp));
+                em.persist(newItem);
+            } else {
+                item.setContent(IOUtils.toString(blob, "UTF-8")); //$NON-NLS-1$
+            }
             em.getTransaction().commit();
         } finally {
             em.close();
@@ -205,6 +210,18 @@
         return em.find(StorageItem.class, new StorageId(category, id));
     }
 
+    private static HistoryStorageItem findHistoryItem(String category, String id, long timestamp, EntityManager em) {
+        TypedQuery<HistoryStorageItem> query = em.createNamedQuery("getItemByTimestamp", HistoryStorageItem.class); //$NON-NLS-1$
+        query.setParameter("category", category); //$NON-NLS-1$
+        query.setParameter("id", id); //$NON-NLS-1$
+        query.setParameter("dateCreated", new Date(timestamp)); //$NON-NLS-1$
+        List<HistoryStorageItem> resultList = query.getResultList();
+        if (resultList.size() > 1) {
+            throw new IllegalStateException();
+        }
+        return resultList.isEmpty() ? null : resultList.get(0);
+    }
+
     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 97eecb9..5634ca5 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
@@ -10,7 +10,6 @@
 
 import java.util.Date;
 
-import javax.persistence.Column;
 import javax.persistence.Entity;
 import javax.persistence.Id;
 import javax.persistence.IdClass;
@@ -34,7 +33,6 @@
     @Id
     private String id;
     @Lob
-    @Column(length = 100000)
     private String content;
     @Temporal(TemporalType.TIMESTAMP)
     private Date dateModified;