JSPWIKI-304: un/serialize Workflows + Decision Queue from/to disk
diff --git a/jspwiki-main/src/main/java/org/apache/wiki/workflow/DefaultWorkflowManager.java b/jspwiki-main/src/main/java/org/apache/wiki/workflow/DefaultWorkflowManager.java
index fb6e648..b1444f4 100644
--- a/jspwiki-main/src/main/java/org/apache/wiki/workflow/DefaultWorkflowManager.java
+++ b/jspwiki-main/src/main/java/org/apache/wiki/workflow/DefaultWorkflowManager.java
@@ -18,6 +18,7 @@
  */
 package org.apache.wiki.workflow;
 
+import org.apache.commons.lang3.time.StopWatch;
 import org.apache.log4j.Logger;
 import org.apache.wiki.api.core.Engine;
 import org.apache.wiki.api.core.Session;
@@ -28,6 +29,14 @@
 import org.apache.wiki.event.WikiEventEmitter;
 import org.apache.wiki.event.WorkflowEvent;
 
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
 import java.security.Principal;
 import java.util.ArrayList;
 import java.util.List;
@@ -48,11 +57,15 @@
 public class DefaultWorkflowManager implements WorkflowManager {
 
     private static final Logger LOG = Logger.getLogger( DefaultWorkflowManager.class );
+    static final String SERIALIZATION_FILE = "wkflmgr.ser";
 
-    private final DecisionQueue m_queue = new DecisionQueue();
-    private final Set< Workflow > m_workflows;
-    private final Map< String, Principal > m_approvers;
-    private final List< Workflow > m_completed;
+    /** We use this also a generic serialization id */
+    private static final long serialVersionUID = 6L;
+
+    DecisionQueue m_queue = new DecisionQueue();
+    Set< Workflow > m_workflows;
+    final Map< String, Principal > m_approvers;
+    List< Workflow > m_completed;
     private Engine m_engine = null;
 
     /**
@@ -111,6 +124,59 @@
                 }
             }
         }
+
+        unserializeFromDisk( new File( m_engine.getWorkDir(), SERIALIZATION_FILE ) );
+    }
+
+    /**
+     *  Reads the serialized data from the disk back to memory.
+     *
+     * @return the date when the data was last written on disk or {@code 0} if there has been problems reading from disk.
+     */
+    @SuppressWarnings( "unchecked" )
+    synchronized long unserializeFromDisk( final File f ) {
+        long saved = 0L;
+        final StopWatch sw = new StopWatch();
+        sw.start();
+        try( final ObjectInputStream in = new ObjectInputStream( new BufferedInputStream( new FileInputStream( f ) ) ) ) {
+            final long ver = in.readLong();
+            if( ver != serialVersionUID ) {
+                LOG.warn( "File format has changed; Unable to recover workflows and decision queue from disk." );
+            } else {
+                saved        = in.readLong();
+                m_workflows  = ( Set< Workflow > )in.readObject();
+                m_queue      = ( DecisionQueue )in.readObject();
+                m_completed  = ( List< Workflow > )in.readObject();
+                LOG.debug( "Read serialized data successfully in " + sw );
+            }
+        } catch( final IOException | ClassNotFoundException e ) {
+            LOG.error( "unable to recover from disk workflows and decision queue: " + e.getMessage(), e );
+        }
+        sw.stop();
+
+        return saved;
+    }
+
+    /**
+     *  Serializes workflows and decisionqueue to disk.  The format is private, don't touch it.
+     */
+    synchronized void serializeToDisk( final File f ) {
+        try( final ObjectOutputStream out = new ObjectOutputStream( new BufferedOutputStream( new FileOutputStream( f ) ) ) ) {
+            final StopWatch sw = new StopWatch();
+            sw.start();
+
+            out.writeLong( serialVersionUID );
+            out.writeLong( System.currentTimeMillis() ); // Timestamp
+            out.writeObject( m_workflows );
+            out.writeObject( m_queue );
+            out.writeObject( m_completed );
+
+            sw.stop();
+
+            LOG.debug( "serialization done - took " + sw );
+        } catch( final IOException ioe ) {
+            LOG.error( "Unable to serialize!", ioe );
+        }
     }
 
     /**
@@ -220,6 +286,7 @@
                 default: break;
                 }
             }
+            serializeToDisk( new File( m_engine.getWorkDir(), SERIALIZATION_FILE ) );
         }
     }
 
diff --git a/jspwiki-main/src/test/java/org/apache/wiki/workflow/WorkflowManagerTest.java b/jspwiki-main/src/test/java/org/apache/wiki/workflow/WorkflowManagerTest.java
index 5ec58c7..6735029 100644
--- a/jspwiki-main/src/test/java/org/apache/wiki/workflow/WorkflowManagerTest.java
+++ b/jspwiki-main/src/test/java/org/apache/wiki/workflow/WorkflowManagerTest.java
@@ -23,20 +23,22 @@
 import org.apache.wiki.api.exceptions.WikiException;
 import org.apache.wiki.auth.GroupPrincipal;
 import org.apache.wiki.auth.WikiPrincipal;
+import org.apache.wiki.event.WorkflowEvent;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
+import java.io.File;
+
 
 public class WorkflowManagerTest {
 
-    protected Workflow w;
-    protected WorkflowManager wm;
     protected WikiEngine m_engine = TestEngine.build();
+    protected WorkflowManager wm = m_engine.getManager( WorkflowManager.class );
+    protected Workflow w;
 
     @BeforeEach
     public void setUp() throws Exception {
-        wm = m_engine.getManager( WorkflowManager.class );
         // Create a workflow with 3 steps, with a Decision in the middle
         w = new Workflow( "workflow.key", new WikiPrincipal( "Owner1" ) );
         final Step startTask = new TaskTest.NormalTask( w );
@@ -97,4 +99,30 @@
         Assertions.assertThrows( WikiException.class, () -> wm.getApprover( "workflow.saveWikiPage" ) );
     }
 
+    @Test
+    public void testSerializeUnserialize() throws WikiException {
+        final DefaultWorkflowManager dwm = new DefaultWorkflowManager();
+        dwm.initialize( m_engine, TestEngine.getTestProperties() );
+
+        dwm.unserializeFromDisk( new File( "./target/test-classes", DefaultWorkflowManager.SERIALIZATION_FILE ) );
+        Assertions.assertEquals( 1, dwm.m_workflows.size() );
+        Assertions.assertEquals( 1, dwm.m_queue.decisions().length );
+        Assertions.assertEquals( 0, dwm.m_completed.size() );
+
+        final Workflow workflow = dwm.m_workflows.iterator().next();
+        final Decision d = ( Decision )workflow.getCurrentStep();
+        d.decide( Outcome.DECISION_APPROVE );
+        dwm.actionPerformed( new WorkflowEvent( workflow, WorkflowEvent.COMPLETED ) );
+        dwm.actionPerformed( new WorkflowEvent( d, WorkflowEvent.DQ_REMOVAL ) );
+        Assertions.assertEquals( 0, dwm.getWorkflows().size() );
+        Assertions.assertEquals( 0, dwm.m_queue.decisions().length );
+        Assertions.assertEquals( 1, dwm.getCompletedWorkflows().size() );
+        dwm.serializeToDisk( new File( "./target/test-classes", DefaultWorkflowManager.SERIALIZATION_FILE ) );
+
+        dwm.unserializeFromDisk( new File( "./target/test-classes", DefaultWorkflowManager.SERIALIZATION_FILE ) );
+        Assertions.assertEquals( 0, dwm.m_workflows.size() );
+        Assertions.assertEquals( 0, dwm.m_queue.decisions().length );
+        Assertions.assertEquals( 1, dwm.m_completed.size() );
+    }
+
 }