diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties b/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties
index 32e02fdbcac4ebf477893315fae8e2f25b948f0b..57299ab1c67d761f3da6e20ec3909b8ed9095acf 100644
--- a/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties
+++ b/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties
@@ -78,13 +78,12 @@ Case.updateCaseName.exception.msg=Error while trying to update the case name.
 Case.updateExaminer.exception.msg=Error while trying to update the examiner.
 Case.updateCaseNum.exception.msg=Error while trying to update the case number.
 Case.exception.errGetRootObj=Error getting root objects.
-Case.createCaseDir.exception.existNotDir=Cannot create case dir, already exists and is not a directory\: {0}
-Case.createCaseDir.exception.existCantRW=Cannot create case dir, already exists and cannot read/write\: {0}
+Case.createCaseDir.exception.existNotDir=Cannot create case directory, it already exists and is not a directory\: {0}
+Case.createCaseDir.exception.existCantRW=Cannot create case directory, it already exists and cannot read/write\: {0}
 Case.createCaseDir.exception.cantCreate=Cannot create case directory or it already exists\: {0}
 Case.createCaseDir.exception.cantCreateCaseDir=Could not create case directory\: {0}
 Case.createCaseDir.exception.cantCreateModDir=Could not create modules output directory\: {0}
 Case.createCaseDir.exception.cantCreateReportsDir=Could not create reports output directory\: {0}
-Case.createCaseDir.exception.gen=Could not create case directory\: {0}
 Case.CollaborationSetup.FailNotify.ErrMsg=Failed to connect to any other nodes that may be collaborating on this case.
 Case.CollaborationSetup.FailNotify.Title=Connection Failure
 Case.GetCaseTypeGivenPath.Failure=Unable to get case type
@@ -190,17 +189,10 @@ ReviewModeCasePanel.StatusIconHeaderText=Status
 ReviewModeCasePanel.OutputFolderHeaderText=Output Folder
 ReviewModeCasePanel.LastAccessedTimeHeaderText=Last Accessed Time
 ReviewModeCasePanel.MetadataFileHeaderText=Metadata File
-OpenMultiUserCasePanel.jLabel1.text=Recent Cases
-OpenMultiUserCasePanel.openButton.text=Open
-OpenMultiUserCasePanel.cancelButton.text=Cancel
-MultiUserCasesPanel.bnOpen.text=&Open
 CueBannerPanel.newCaseLabel.text=New Case
 CueBannerPanel.openCaseButton.text=
 CueBannerPanel.openCaseLabel.text=Open Case
-MultiUserCasesPanel.bnOpenSingleUserCase.text=Open Single-User Case...
 CueBannerPanel.newCaseButton.text=
-MultiUserCasesPanel.searchLabel.text=Select any case and start typing to search by case name
-MultiUserCasesPanel.cancelButton.text=Cancel
 ImageFilePanel.sectorSizeLabel.text=Sector size:
 LocalDiskPanel.sectorSizeLabel.text=Sector Size:
 LocalFilesPanel.displayNameLabel.text=Logical File Set Display Name: Default
@@ -240,4 +232,8 @@ ImageFilePanel.sha1HashTextField.text=
 ImageFilePanel.md5HashTextField.text=
 ImageFilePanel.errorLabel.text=Error Label
 ImageFilePanel.hashValuesNoteLabel.text=NOTE: These values will not be validated when the data source is added.
-ImageFilePanel.hashValuesLabel.text=Hash Values (optional):
\ No newline at end of file
+ImageFilePanel.hashValuesLabel.text=Hash Values (optional):
+OpenMultiUserCasePanel.searchLabel.text=Select any case and start typing to search by case name
+OpenMultiUserCasePanel.cancelButton.text=Cancel
+OpenMultiUserCasePanel.openSelectedCaseButton.text=Open Selected Case
+OpenMultiUserCasePanel.openSingleUserCaseButton.text=Open Single-User Case...
diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/Bundle_ja.properties b/Core/src/org/sleuthkit/autopsy/casemodule/Bundle_ja.properties
index 208c5ed868b7aae14c00bc45b72ce6e717d515cd..b067b28c983b321a4774a7946f0a9983494479eb 100644
--- a/Core/src/org/sleuthkit/autopsy/casemodule/Bundle_ja.properties
+++ b/Core/src/org/sleuthkit/autopsy/casemodule/Bundle_ja.properties
@@ -65,7 +65,6 @@ Case.createCaseDir.exception.existNotDir=\u30b1\u30fc\u30b9\u30c7\u30a3\u30ec\u3
 Case.createCaseDir.exception.existCantRW=\u30b1\u30fc\u30b9\u30c7\u30a3\u30ec\u30af\u30c8\u30ea\u3092\u4f5c\u6210\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u65e2\u306b\u5b58\u5728\u3057\u3001\u8aad\u307f\u53d6\u308a\uff0f\u66f8\u304d\u8fbc\u307f\u304c\u3067\u304d\u307e\u305b\u3093\uff1a{0}
 Case.createCaseDir.exception.cantCreateCaseDir=\u30b1\u30fc\u30b9\u30c7\u30a3\u30ec\u30af\u30c8\u30ea\u3092\u4f5c\u6210\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\uff1a {0}
 Case.createCaseDir.exception.cantCreateModDir=\u30e2\u30b8\u30e5\u30fc\u30eb\u30a2\u30a6\u30c8\u30d7\u30c3\u30c8\u30c7\u30a3\u30ec\u30af\u30c8\u30ea\u3092\u4f5c\u6210\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\uff1a{0}
-Case.createCaseDir.exception.gen=\u30b1\u30fc\u30b9\u30c7\u30a3\u30ec\u30af\u30c8\u30ea\u3092\u4f5c\u6210\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\uff1a{0}
 CaseDeleteAction.closeConfMsg.text=\u3053\u306e\u30b1\u30fc\u30b9\u3092\u672c\u5f53\u306b\u9589\u3058\u3001\u524a\u9664\u3057\u307e\u3059\u304b\uff1f\n\
      \u30b1\u30fc\u30b9\u540d\uff1a {0}\n\
      \u30b1\u30fc\u30b9\u30c7\u30a3\u30ec\u30af\u30c8\u30ea\: {1}
@@ -132,7 +131,6 @@ AddImageWizardIngestConfigPanel.CANCEL_BUTTON.text=\u30ad\u30e3\u30f3\u30bb\u30e
 LocalFilesPanel.errorLabel.text=\u30a8\u30e9\u30fc\u30e9\u30d9\u30eb
 ImageFilePanel.errorLabel.text=\u30a8\u30e9\u30fc\u30e9\u30d9\u30eb
 NewCaseVisualPanel1.caseTypeLabel.text=\u30b1\u30fc\u30b9\u30bf\u30a4\u30d7\uff1a
-Case.databaseConnectionInfo.error.msg=\u30c7\u30fc\u30bf\u30d9\u30fc\u30b9\u30b5\u30fc\u30d0\u30fc\u306e\u63a5\u7d9a\u60c5\u5831\u3092\u5165\u624b\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002\u30c4\u30fc\u30eb\u3001\u30aa\u30d7\u30b7\u30e7\u30f3\u3001\u8907\u6570\u30e6\u30fc\u30b6\u30fc\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002
 Case.open.exception.multiUserCaseNotEnabled=\u8907\u6570\u30e6\u30fc\u30b6\u30fc\u306e\u30b1\u30fc\u30b9\u304c\u6709\u52b9\u5316\u3055\u308c\u3066\u3044\u306a\u3044\u3068\u3001\u8907\u6570\u30e6\u30fc\u30b6\u30fc\u306e\u30b1\u30fc\u30b9\u306f\u958b\u3051\u307e\u305b\u3093\u3002\u30c4\u30fc\u30eb\u3001\u30aa\u30d7\u30b7\u30e7\u30f3\u3001\u8907\u6570\u30e6\u30fc\u30b6\u30fc\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002
 Case.createCaseDir.exception.cantCreateReportsDir=\u30ec\u30dd\u30fc\u30c8\u30a2\u30a6\u30c8\u30d7\u30c3\u30c8\u30c7\u30a3\u30ec\u30af\u30c8\u30ea\u3092\u4f5c\u6210\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\uff1a{0}
 Case.CollaborationSetup.FailNotify.ErrMsg=\u3053\u306e\u30b1\u30fc\u30b9\u3067\u4f7f\u308f\u308c\u3066\u3044\u308b\u304b\u3082\u3057\u308c\u306a\u3044\u30ce\u30fc\u30c9\u306b\u63a5\u7d9a\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002
@@ -179,9 +177,6 @@ OptionalCasePropertiesPanel.examinerLabel.text=\u8abf\u67fb\u62c5\u5f53\u8005\uf
 OptionalCasePropertiesPanel.caseDisplayNameLabel.text=\u30b1\u30fc\u30b9\u756a\u53f7\uff1a
 CueBannerPanel.openRecentCaseLabel.text=\u6700\u8fd1\u958b\u3044\u305f\u30b1\u30fc\u30b9\u3092\u958b\u304f
 CueBannerPanel.openAutoIngestCaseLabel.text=\u65e2\u5b58\u30b1\u30fc\u30b9\u3092\u958b\u304f
-OpenMultiUserCasePanel.openButton.text=\u958b\u304f
-OpenMultiUserCasePanel.cancelButton.text=\u30ad\u30e3\u30f3\u30bb\u30eb
-OpenMultiUserCasePanel.jLabel1.text=\u6700\u8fd1\u958b\u3044\u305f\u30d5\u30a1\u30a4\u30eb
 CueBannerPanel.newCaseLabel.text=\u65b0\u898f\u30b1\u30fc\u30b9\u3092\u4f5c\u6210
 CueBannerPanel.openCaseLabel.text=\u65e2\u5b58\u30b1\u30fc\u30b9\u3092\u958b\u304f
 ImageFilePanel.sectorSizeLabel.text=\u30a4\u30f3\u30d7\u30c3\u30c8\u30bf\u30a4\u30e0\u30be\u30fc\u30f3\u3092\u9078\u629e\u3057\u3066\u4e0b\u3055\u3044\uff1a
@@ -200,4 +195,5 @@ LogicalEvidenceFilePanel.errorLabel.text=\u30a8\u30e9\u30fc\u30e9\u30d9\u30eb
 LogicalEvidenceFilePanel.logicalEvidenceFileChooser.dialogTitle=\u30ed\u30fc\u30ab\u30eb\u30d5\u30a1\u30a4\u30eb\u307e\u305f\u306f\u30d5\u30a9\u30eb\u30c0\u3092\u9078\u629e
 LogicalEvidenceFilePanel.logicalEvidenceFileChooser.approveButtonText=\u9078\u629e
 LocalDiskSelectionDialog.errorLabel.text=\u30a8\u30e9\u30fc\u30e9\u30d9\u30eb
-LocalDiskSelectionDialog.selectLocalDiskLabel.text=\u30ed\u30fc\u30ab\u30eb\u30c7\u30a3\u30b9\u30af\u3092\u9078\u629e\uff1a
\ No newline at end of file
+LocalDiskSelectionDialog.selectLocalDiskLabel.text=\u30ed\u30fc\u30ab\u30eb\u30c7\u30a3\u30b9\u30af\u3092\u9078\u629e\uff1a
+OpenMultiUserCasePanel.cancelButton.text=\u30ad\u30e3\u30f3\u30bb\u30eb
diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/Case.java b/Core/src/org/sleuthkit/autopsy/casemodule/Case.java
index 7d4dab493e290727691b9846a2b271225e8879e8..c50f83f357595773afd02ec8012bff0bd6d0b9ee 100644
--- a/Core/src/org/sleuthkit/autopsy/casemodule/Case.java
+++ b/Core/src/org/sleuthkit/autopsy/casemodule/Case.java
@@ -18,6 +18,7 @@
  */
 package org.sleuthkit.autopsy.casemodule;
 
+import org.sleuthkit.autopsy.coordinationservice.CaseNodeData;
 import java.awt.Frame;
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
@@ -32,6 +33,7 @@
 import java.sql.DriverManager;
 import java.sql.SQLException;
 import java.sql.Statement;
+import java.text.ParseException;
 import java.text.SimpleDateFormat;
 import java.util.Collection;
 import java.util.Date;
@@ -39,7 +41,6 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.MissingResourceException;
 import java.util.Set;
 import java.util.TimeZone;
 import java.util.UUID;
@@ -574,7 +575,7 @@ public static void createAsCurrentCase(CaseType caseType, String caseDir, CaseDe
      *                             exception.
      */
     @Messages({
-        "Case.exceptionMessage.failedToReadMetadata=Failed to read case metadata.",
+        "# {0} - exception message", "Case.exceptionMessage.failedToReadMetadata=Failed to read case metadata:\n{0}.",
         "Case.exceptionMessage.cannotOpenMultiUserCaseNoSettings=Multi-user settings are missing (see Tools, Options, Multi-user tab), cannot open a multi-user case."
     })
     public static void openAsCurrentCase(String caseMetadataFilePath) throws CaseActionException {
@@ -582,7 +583,7 @@ public static void openAsCurrentCase(String caseMetadataFilePath) throws CaseAct
         try {
             metadata = new CaseMetadata(Paths.get(caseMetadataFilePath));
         } catch (CaseMetadataException ex) {
-            throw new CaseActionException(Bundle.Case_exceptionMessage_failedToReadMetadata(), ex);
+            throw new CaseActionException(Bundle.Case_exceptionMessage_failedToReadMetadata(ex.getLocalizedMessage()), ex);
         }
         if (CaseType.MULTI_USER_CASE == metadata.getCaseType() && !UserPreferences.getIsMultiUserModeEnabled()) {
             throw new CaseActionException(Bundle.Case_exceptionMessage_cannotOpenMultiUserCaseNoSettings());
@@ -713,7 +714,8 @@ public static void deleteCurrentCase() throws CaseActionException {
         "Case.exceptionMessage.cannotDeleteCurrentCase=Cannot delete current case, it must be closed first.",
         "Case.progressMessage.checkingForOtherUser=Checking to see if another user has the case open...",
         "Case.exceptionMessage.cannotGetLockToDeleteCase=Cannot delete case because it is open for another user or there is a problem with the coordination service.",
-        "Case.exceptionMessage.failedToDeleteCoordinationServiceNodes=Failed to delete the coordination service nodes for the case.",})
+        "Case.exceptionMessage.failedToDeleteCoordinationServiceNodes=Failed to delete the coordination service nodes for the case."
+    })
     public static void deleteCase(CaseMetadata metadata) throws CaseActionException {
         StopWatch stopWatch = new StopWatch();
         stopWatch.start();
@@ -895,72 +897,71 @@ private static String displayNameToUniqueName(String caseDisplayName) {
     /**
      * Creates a case directory and its subdirectories.
      *
-     * @param caseDir  Path to the case directory (typically base + case name).
-     * @param caseType The type of case, single-user or multi-user.
+     * @param caseDirPath Path to the case directory (typically base + case
+     *                    name).
+     * @param caseType    The type of case, single-user or multi-user.
      *
      * @throws CaseActionException throw if could not create the case dir
      */
-    public static void createCaseDirectory(String caseDir, CaseType caseType) throws CaseActionException {
-
-        File caseDirF = new File(caseDir);
-
-        if (caseDirF.exists()) {
-            if (caseDirF.isFile()) {
-                throw new CaseActionException(
-                        NbBundle.getMessage(Case.class, "Case.createCaseDir.exception.existNotDir", caseDir));
-
-            } else if (!caseDirF.canRead() || !caseDirF.canWrite()) {
-                throw new CaseActionException(
-                        NbBundle.getMessage(Case.class, "Case.createCaseDir.exception.existCantRW", caseDir));
+    public static void createCaseDirectory(String caseDirPath, CaseType caseType) throws CaseActionException {
+        /*
+         * Check the case directory path and permissions. The case directory may
+         * already exist.
+         */
+        File caseDir = new File(caseDirPath);
+        if (caseDir.exists()) {
+            if (caseDir.isFile()) {
+                throw new CaseActionException(NbBundle.getMessage(Case.class, "Case.createCaseDir.exception.existNotDir", caseDirPath));
+            } else if (!caseDir.canRead() || !caseDir.canWrite()) {
+                throw new CaseActionException(NbBundle.getMessage(Case.class, "Case.createCaseDir.exception.existCantRW", caseDirPath));
             }
         }
 
-        try {
-            boolean result = (caseDirF).mkdirs(); // create root case Directory
-
-            if (result == false) {
-                throw new CaseActionException(
-                        NbBundle.getMessage(Case.class, "Case.createCaseDir.exception.cantCreate", caseDir));
-            }
-
-            // create the folders inside the case directory
-            String hostClause = "";
+        /*
+         * Create the case directory, if it does not already exist.
+         */
+        if (!caseDir.mkdirs()) {
+            throw new CaseActionException(NbBundle.getMessage(Case.class, "Case.createCaseDir.exception.cantCreate", caseDirPath));
+        }
 
-            if (caseType == CaseType.MULTI_USER_CASE) {
-                hostClause = File.separator + NetworkUtils.getLocalHostName();
-            }
-            result = result && (new File(caseDir + hostClause + File.separator + EXPORT_FOLDER)).mkdirs()
-                    && (new File(caseDir + hostClause + File.separator + LOG_FOLDER)).mkdirs()
-                    && (new File(caseDir + hostClause + File.separator + TEMP_FOLDER)).mkdirs()
-                    && (new File(caseDir + hostClause + File.separator + CACHE_FOLDER)).mkdirs();
-
-            if (result == false) {
-                throw new CaseActionException(
-                        NbBundle.getMessage(Case.class, "Case.createCaseDir.exception.cantCreateCaseDir", caseDir));
-            }
+        /*
+         * Create the subdirectories of the case directory, if they do not
+         * already exist. Note that multi-user cases get an extra layer of
+         * subdirectories, one subdirectory per application host machine.
+         */
+        String hostPathComponent = "";
+        if (caseType == CaseType.MULTI_USER_CASE) {
+            hostPathComponent = File.separator + NetworkUtils.getLocalHostName();
+        }
 
-            final String modulesOutDir = caseDir + hostClause + File.separator + MODULE_FOLDER;
-            result = new File(modulesOutDir).mkdir();
+        Path exportDir = Paths.get(caseDirPath, hostPathComponent, EXPORT_FOLDER);
+        if (!exportDir.toFile().mkdirs()) {
+            throw new CaseActionException(NbBundle.getMessage(Case.class, "Case.createCaseDir.exception.cantCreateCaseDir", exportDir));
+        }
 
-            if (result == false) {
-                throw new CaseActionException(
-                        NbBundle.getMessage(Case.class, "Case.createCaseDir.exception.cantCreateModDir",
-                                modulesOutDir));
-            }
+        Path logsDir = Paths.get(caseDirPath, hostPathComponent, LOG_FOLDER);
+        if (!logsDir.toFile().mkdirs()) {
+            throw new CaseActionException(NbBundle.getMessage(Case.class, "Case.createCaseDir.exception.cantCreateCaseDir", logsDir));
+        }
 
-            final String reportsOutDir = caseDir + hostClause + File.separator + REPORTS_FOLDER;
-            result = new File(reportsOutDir).mkdir();
+        Path tempDir = Paths.get(caseDirPath, hostPathComponent, TEMP_FOLDER);
+        if (!tempDir.toFile().mkdirs()) {
+            throw new CaseActionException(NbBundle.getMessage(Case.class, "Case.createCaseDir.exception.cantCreateCaseDir", tempDir));
+        }
 
-            if (result == false) {
-                throw new CaseActionException(
-                        NbBundle.getMessage(Case.class, "Case.createCaseDir.exception.cantCreateReportsDir",
-                                modulesOutDir));
+        Path cacheDir = Paths.get(caseDirPath, hostPathComponent, CACHE_FOLDER);
+        if (!cacheDir.toFile().mkdirs()) {
+            throw new CaseActionException(NbBundle.getMessage(Case.class, "Case.createCaseDir.exception.cantCreateCaseDir", cacheDir));
+        }
 
-            }
+        Path moduleOutputDir = Paths.get(caseDirPath, hostPathComponent, MODULE_FOLDER);
+        if (!moduleOutputDir.toFile().mkdirs()) {
+            throw new CaseActionException(NbBundle.getMessage(Case.class, "Case.createCaseDir.exception.cantCreateModDir", moduleOutputDir));
+        }
 
-        } catch (MissingResourceException | CaseActionException e) {
-            throw new CaseActionException(
-                    NbBundle.getMessage(Case.class, "Case.createCaseDir.exception.gen", caseDir), e);
+        Path reportsDir = Paths.get(caseDirPath, hostPathComponent, REPORTS_FOLDER);
+        if (!reportsDir.toFile().mkdirs()) {
+            throw new CaseActionException(NbBundle.getMessage(Case.class, "Case.createCaseDir.exception.cantCreateReportsDir", reportsDir));
         }
     }
 
@@ -1588,15 +1589,15 @@ public void notifyFailedAddingDataSource(UUID addingDataSourceEventId) {
     public void notifyDataSourceAdded(Content dataSource, UUID addingDataSourceEventId) {
         eventPublisher.publish(new DataSourceAddedEvent(dataSource, addingDataSourceEventId));
     }
-    
+
     /**
      * Notifies case event subscribers that a data source has been added to the
      * case database.
      *
      * This should not be called from the event dispatch thread (EDT)
      *
-     * @param dataSource              The data source.
-     * @param newName                 The new name for the data source
+     * @param dataSource The data source.
+     * @param newName    The new name for the data source
      */
     public void notifyDataSourceNameChanged(Content dataSource, String newName) {
         eventPublisher.publish(new DataSourceNameChangedEvent(dataSource, newName));
@@ -1759,7 +1760,7 @@ CaseMetadata getMetadata() {
     }
 
     /**
-     * Updates the case display name.
+     * Updates the case details.
      *
      * @param newDisplayName the new display name for the case
      *
@@ -1775,6 +1776,16 @@ void updateCaseDetails(CaseDetails caseDetails) throws CaseActionException {
         } catch (CaseMetadataException ex) {
             throw new CaseActionException(Bundle.Case_exceptionMessage_metadataUpdateError(), ex);
         }
+        if (getCaseType() == CaseType.MULTI_USER_CASE && !oldCaseDetails.getCaseDisplayName().equals(caseDetails.getCaseDisplayName())) {
+            try {
+                CoordinationService coordinationService = CoordinationService.getInstance();
+                CaseNodeData nodeData = new CaseNodeData(coordinationService.getNodeData(CategoryNode.CASES, metadata.getCaseDirectory()));
+                nodeData.setDisplayName(caseDetails.getCaseDisplayName());
+                coordinationService.setNodeData(CategoryNode.CASES, metadata.getCaseDirectory(), nodeData.toArray());
+            } catch (CoordinationServiceException | InterruptedException | IOException ex) {
+                throw new CaseActionException(Bundle.Case_exceptionMessage_couldNotUpdateCaseNodeData(ex.getLocalizedMessage()), ex);
+            }
+        }
         if (!oldCaseDetails.getCaseNumber().equals(caseDetails.getCaseNumber())) {
             eventPublisher.publish(new AutopsyEvent(Events.NUMBER.toString(), oldCaseDetails.getCaseNumber(), caseDetails.getCaseNumber()));
         }
@@ -1963,38 +1974,50 @@ private void open(boolean isNewCase) throws CaseActionException {
      * @param isNewCase         True for a new case, false otherwise.
      * @param progressIndicator A progress indicator.
      *
-     * @throws CaseActionException if there is a problem creating the case. The
+     * @throws CaseActionException If there is a problem creating the case. The
      *                             exception will have a user-friendly message
      *                             and may be a wrapper for a lower-level
      *                             exception.
      */
     private void open(boolean isNewCase, ProgressIndicator progressIndicator) throws CaseActionException {
         try {
-            if (Thread.currentThread().isInterrupted()) {
-                throw new CaseActionCancelledException(Bundle.Case_exceptionMessage_cancelledByUser());
+            checkForUserCancellation();
+            createCaseDirectoryIfDoesNotExist(progressIndicator);
+            checkForUserCancellation();
+            switchLoggingToCaseLogsDirectory(progressIndicator);
+            checkForUserCancellation();
+            if (isNewCase) {
+                saveCaseMetadataToFile(progressIndicator);
             }
-
+            checkForUserCancellation();
             if (isNewCase) {
-                createCaseData(progressIndicator);
+                createCaseNodeData(progressIndicator);
             } else {
-                openCaseData(progressIndicator);
+                updateCaseNodeData(progressIndicator);
             }
-
-            if (Thread.currentThread().isInterrupted()) {
-                throw new CaseActionCancelledException(Bundle.Case_exceptionMessage_cancelledByUser());
+            checkForUserCancellation();
+            if (!isNewCase) {
+                deleteTempfilesFromCaseDirectory(progressIndicator);
             }
-
-            openServices(progressIndicator);
-
-            if (Thread.currentThread().isInterrupted()) {
-                throw new CaseActionCancelledException(Bundle.Case_exceptionMessage_cancelledByUser());
+            checkForUserCancellation();
+            if (isNewCase) {
+                createCaseDatabase(progressIndicator);
+            } else {
+                openCaseDataBase(progressIndicator);
             }
+            checkForUserCancellation();
+            openCaseLevelServices(progressIndicator);
+            checkForUserCancellation();
+            openAppServiceCaseResources(progressIndicator);
+            checkForUserCancellation();
+            openCommunicationChannels(progressIndicator);
+
         } catch (CaseActionException ex) {
             /*
-             * Cancellation or failure. Clean up. The sleep is a little hack to
-             * clear the interrupted flag for this thread if this is a
-             * cancellation scenario, so that the clean up can run to completion
-             * in this thread.
+             * Cancellation or failure. Clean up by calling the close method.
+             * The sleep is a little hack to clear the interrupted flag for this
+             * thread if this is a cancellation scenario, so that the clean up
+             * can run to completion in the current thread.
              */
             try {
                 Thread.sleep(1);
@@ -2006,43 +2029,180 @@ private void open(boolean isNewCase, ProgressIndicator progressIndicator) throws
     }
 
     /**
-     * Creates the case directory, case database, and case metadata file.
+     * Checks current thread for an interrupt. Usage: checking for user
+     * cancellation of a case creation/opening operation, as reflected in the
+     * exception message.
      *
-     * @param progressIndicator A progress indicartor.
+     * @throws CaseActionCancelledException If the current thread is
+     *                                      interrupted, assumes interrupt was
+     *                                      due to a user action.
+     */
+    private static void checkForUserCancellation() throws CaseActionCancelledException {
+        if (Thread.currentThread().isInterrupted()) {
+            throw new CaseActionCancelledException(Bundle.Case_exceptionMessage_cancelledByUser());
+        }
+    }
+
+    /**
+     * Creates the case directory, if it does not already exist.
+     *
+     * TODO (JIRA-2180): Always create the case directory as part of the case
+     * creation process.
      *
-     * @throws CaseActionException If there is a problem creating the case
-     *                             database. The exception will have a
+     * @param progressIndicator A progress indicator.
+     *
+     * @throws CaseActionException If there is a problem completing the
+     *                             operation. The exception will have a
      *                             user-friendly message and may be a wrapper
      *                             for a lower-level exception.
      */
     @Messages({
-        "Case.progressMessage.creatingCaseDirectory=Creating case directory...",
-        "Case.progressMessage.creatingCaseDatabase=Creating case database...",
-        "# {0} - exception message", "Case.exceptionMessage.couldNotCreateCaseDatabase=Failed to create case database:\n{0}",
-        "Case.exceptionMessage.couldNotCreateMetadataFile=Failed to create case metadata file."
+        "Case.progressMessage.creatingCaseDirectory=Creating case directory..."
     })
-    private void createCaseData(ProgressIndicator progressIndicator) throws CaseActionException {
-        /*
-         * Create the case directory, if it does not already exist.
-         *
-         * TODO (JIRA-2180): Always create the case directory as part of the
-         * case creation process.
-         */
+    private void createCaseDirectoryIfDoesNotExist(ProgressIndicator progressIndicator) throws CaseActionException {
+        progressIndicator.progress(Bundle.Case_progressMessage_creatingCaseDirectory());
         if (new File(metadata.getCaseDirectory()).exists() == false) {
             progressIndicator.progress(Bundle.Case_progressMessage_creatingCaseDirectory());
             Case.createCaseDirectory(metadata.getCaseDirectory(), metadata.getCaseType());
         }
+    }
+
+    /**
+     * Switches from writing log messages to the application logs to the logs
+     * subdirectory of the case directory.
+     *
+     * @param progressIndicator A progress indicator.
+     */
+    @Messages({
+        "Case.progressMessage.switchingLogDirectory=Switching log directory..."
+    })
+    private void switchLoggingToCaseLogsDirectory(ProgressIndicator progressIndicator) {
+        progressIndicator.progress(Bundle.Case_progressMessage_switchingLogDirectory());
+        Logger.setLogDirectory(getLogDirectoryPath());
+    }
+
+    /**
+     * Saves teh case metadata to a file.SHould not be called until the case
+     * directory has been created.
+     *
+     * @param progressIndicator A progress indicator.
+     *
+     * @throws CaseActionException If there is a problem completing the
+     *                             operation. The exception will have a
+     *                             user-friendly message and may be a wrapper
+     *                             for a lower-level exception.
+     */
+    @Messages({
+        "Case.progressMessage.savingCaseMetadata=Saving case metadata to file...",
+        "# {0} - exception message", "Case.exceptionMessage.couldNotSaveCaseMetadata=Failed to save case metadata:\n{0}."
+    })
+    private void saveCaseMetadataToFile(ProgressIndicator progressIndicator) throws CaseActionException {
+        progressIndicator.progress(Bundle.Case_progressMessage_savingCaseMetadata());
+        try {
+            this.metadata.writeToFile();
+        } catch (CaseMetadataException ex) {
+            throw new CaseActionException(Bundle.Case_exceptionMessage_couldNotSaveCaseMetadata(ex.getLocalizedMessage()), ex);
+        }
+    }
+
+    /**
+     * Creates the node data for the case directory lock coordination service
+     * node.
+     *
+     * @param progressIndicator A progress indicator.
+     *
+     * @throws CaseActionException If there is a problem completing the
+     *                             operation. The exception will have a
+     *                             user-friendly message and may be a wrapper
+     *                             for a lower-level exception.
+     */
+    @Messages({
+        "Case.progressMessage.creatingCaseNodeData=Creating coordination service node data...",
+        "# {0} - exception message", "Case.exceptionMessage.couldNotCreateCaseNodeData=Failed to create coordination service node data:\n{0}."
+    })
+    private void createCaseNodeData(ProgressIndicator progressIndicator) throws CaseActionException {
+        if (getCaseType() == CaseType.MULTI_USER_CASE) {
+            progressIndicator.progress(Bundle.Case_progressMessage_creatingCaseNodeData());
+            try {
+                CoordinationService coordinationService = CoordinationService.getInstance();
+                CaseNodeData nodeData = new CaseNodeData(metadata);
+                coordinationService.setNodeData(CategoryNode.CASES, metadata.getCaseDirectory(), nodeData.toArray());
+            } catch (CoordinationServiceException | InterruptedException | ParseException | IOException ex) {
+                throw new CaseActionException(Bundle.Case_exceptionMessage_couldNotCreateCaseNodeData(ex.getLocalizedMessage()), ex);
+            }
+        }
+    }
+
+    /**
+     * Updates the node data for the case directory lock coordination service
+     * node.
+     *
+     * @throws CaseActionException If there is a problem completing the
+     *                             operation. The exception will have a
+     *                             user-friendly message and may be a wrapper
+     *                             for a lower-level exception.
+     */
+    @Messages({
+        "Case.progressMessage.updatingCaseNodeData=Updating coordination service node data...",
+        "# {0} - exception message", "Case.exceptionMessage.couldNotUpdateCaseNodeData=Failed to update coordination service node data:\n{0}."
+    })
+    private void updateCaseNodeData(ProgressIndicator progressIndicator) throws CaseActionException {
+        if (getCaseType() == CaseType.MULTI_USER_CASE) {
+            progressIndicator.progress(Bundle.Case_progressMessage_updatingCaseNodeData());
+            if (getCaseType() == CaseType.MULTI_USER_CASE) {
+                try {
+                    CoordinationService coordinationService = CoordinationService.getInstance();
+                    CaseNodeData nodeData = new CaseNodeData(coordinationService.getNodeData(CategoryNode.CASES, metadata.getCaseDirectory()));
+                    nodeData.setLastAccessDate(new Date());
+                    coordinationService.setNodeData(CategoryNode.CASES, metadata.getCaseDirectory(), nodeData.toArray());
+                } catch (CoordinationServiceException | InterruptedException | IOException ex) {
+                    throw new CaseActionException(Bundle.Case_exceptionMessage_couldNotUpdateCaseNodeData(ex.getLocalizedMessage()), ex);
+                }
+            }
+        }
+    }
 
+    /**
+     * Deletes any files in the temp subdirectory of the case directory.
+     *
+     * @param progressIndicator A progress indicator.
+     */
+    @Messages({
+        "Case.progressMessage.clearingTempDirectory=Clearing case temp directory..."
+    })
+    private void deleteTempfilesFromCaseDirectory(ProgressIndicator progressIndicator) {
         /*
-         * Create the case database.
+         * Clear the temp subdirectory of the case directory.
          */
+        progressIndicator.progress(Bundle.Case_progressMessage_clearingTempDirectory());
+        Case.clearTempSubDir(this.getTempDirectory());
+    }
+
+    /**
+     * Creates the node data for the case directory lock coordination service
+     * node, the case directory, the case database and the case metadata file.
+     *
+     * @param progressIndicator A progress indicator.
+     *
+     * @throws CaseActionException If there is a problem completing the
+     *                             operation. The exception will have a
+     *                             user-friendly message and may be a wrapper
+     *                             for a lower-level exception.
+     */
+    @Messages({
+        "Case.progressMessage.creatingCaseDatabase=Creating case database...",
+        "# {0} - exception message", "Case.exceptionMessage.couldNotGetDbServerConnectionInfo=Failed to get case database server conneciton info:\n{0}.",
+        "# {0} - exception message", "Case.exceptionMessage.couldNotCreateCaseDatabase=Failed to create case database:\n{0}.",
+        "# {0} - exception message", "Case.exceptionMessage.couldNotSaveDbNameToMetadataFile=Failed to save case database name to case metadata file:\n{0}."
+    })
+    private void createCaseDatabase(ProgressIndicator progressIndicator) throws CaseActionException {
         progressIndicator.progress(Bundle.Case_progressMessage_creatingCaseDatabase());
         try {
             if (CaseType.SINGLE_USER_CASE == metadata.getCaseType()) {
                 /*
                  * For single-user cases, the case database is a SQLite database
-                 * with a standard name, physically located in the root of the
-                 * case directory.
+                 * with a standard name, physically located in the case
+                 * directory.
                  */
                 caseDb = SleuthkitCase.newCase(Paths.get(metadata.getCaseDirectory(), SINGLE_USER_CASE_DB_NAME).toString());
                 metadata.setCaseDatabaseName(SINGLE_USER_CASE_DB_NAME);
@@ -2050,7 +2210,7 @@ private void createCaseData(ProgressIndicator progressIndicator) throws CaseActi
                 /*
                  * For multi-user cases, the case database is a PostgreSQL
                  * database with a name derived from the case display name,
-                 * physically located on a database server.
+                 * physically located on the PostgreSQL database server.
                  */
                 caseDb = SleuthkitCase.newCase(metadata.getCaseDisplayName(), UserPreferences.getDatabaseConnectionInfo(), metadata.getCaseDirectory());
                 metadata.setCaseDatabaseName(caseDb.getDatabaseName());
@@ -2058,143 +2218,81 @@ private void createCaseData(ProgressIndicator progressIndicator) throws CaseActi
         } catch (TskCoreException ex) {
             throw new CaseActionException(Bundle.Case_exceptionMessage_couldNotCreateCaseDatabase(ex.getLocalizedMessage()), ex);
         } catch (UserPreferencesException ex) {
-            throw new CaseActionException(NbBundle.getMessage(Case.class, "Case.databaseConnectionInfo.error.msg"), ex);
+            throw new CaseActionException(Bundle.Case_exceptionMessage_couldNotGetDbServerConnectionInfo(ex.getLocalizedMessage()), ex);
         } catch (CaseMetadataException ex) {
-            throw new CaseActionException(Bundle.Case_exceptionMessage_couldNotCreateMetadataFile(), ex);
+            throw new CaseActionException(Bundle.Case_exceptionMessage_couldNotSaveDbNameToMetadataFile(ex.getLocalizedMessage()), ex);
         }
     }
 
     /**
-     * Opens an existing case database.
+     * Updates the node data for an existing case directory lock coordination
+     * service node and opens an existing case database.
      *
      * @param progressIndicator A progress indicator.
      *
-     * @throws CaseActionException if there is a problem opening the case. The
-     *                             exception will have a user-friendly message
-     *                             and may be a wrapper for a lower-level
-     *                             exception.
+     * @throws CaseActionException If there is a problem completing the
+     *                             operation. The exception will have a
+     *                             user-friendly message and may be a wrapper
+     *                             for a lower-level exception.
      */
     @Messages({
         "Case.progressMessage.openingCaseDatabase=Opening case database...",
-        "Case.exceptionMessage.couldNotOpenCaseDatabase=Failed to open case database.",
-        "Case.unsupportedSchemaVersionMessage=Unsupported DB schema version - see log for details",
-        "Case.databaseConnectionInfo.error.msg=Error accessing database server connection info. See Tools, Options, Multi-User.",
-        "Case.open.exception.multiUserCaseNotEnabled=Cannot open a multi-user case if multi-user cases are not enabled. "
-        + "See Tools, Options, Multi-user."
+        "# {0} - exception message", "Case.exceptionMessage.couldNotOpenCaseDatabase=Failed to open case database:\n{0}.",
+        "# {0} - exception message", "Case.exceptionMessage.unsupportedSchemaVersionMessage=Unsupported case database schema version:\n{0}.",
+        "Case.open.exception.multiUserCaseNotEnabled=Cannot open a multi-user case if multi-user cases are not enabled. See Tools, Options, Multi-User."
     })
-    private void openCaseData(ProgressIndicator progressIndicator) throws CaseActionException {
+    private void openCaseDataBase(ProgressIndicator progressIndicator) throws CaseActionException {
+        progressIndicator.progress(Bundle.Case_progressMessage_openingCaseDatabase());
         try {
-            progressIndicator.progress(Bundle.Case_progressMessage_openingCaseDatabase());
             String databaseName = metadata.getCaseDatabaseName();
             if (CaseType.SINGLE_USER_CASE == metadata.getCaseType()) {
                 caseDb = SleuthkitCase.openCase(Paths.get(metadata.getCaseDirectory(), databaseName).toString());
             } else if (UserPreferences.getIsMultiUserModeEnabled()) {
-                try {
-                    caseDb = SleuthkitCase.openCase(databaseName, UserPreferences.getDatabaseConnectionInfo(), metadata.getCaseDirectory());
-                } catch (UserPreferencesException ex) {
-                    throw new CaseActionException(Case_databaseConnectionInfo_error_msg(), ex);
-                }
+                caseDb = SleuthkitCase.openCase(databaseName, UserPreferences.getDatabaseConnectionInfo(), metadata.getCaseDirectory());
             } else {
                 throw new CaseActionException(Case_open_exception_multiUserCaseNotEnabled());
             }
         } catch (TskUnsupportedSchemaVersionException ex) {
-            throw new CaseActionException(Bundle.Case_unsupportedSchemaVersionMessage(), ex);
+            throw new CaseActionException(Bundle.Case_exceptionMessage_unsupportedSchemaVersionMessage(ex.getLocalizedMessage()), ex);
+        } catch (UserPreferencesException ex) {
+            throw new CaseActionException(Bundle.Case_exceptionMessage_couldNotGetDbServerConnectionInfo(ex.getLocalizedMessage()), ex);
         } catch (TskCoreException ex) {
-            throw new CaseActionException(Bundle.Case_exceptionMessage_couldNotOpenCaseDatabase(), ex);
+            throw new CaseActionException(Bundle.Case_exceptionMessage_couldNotOpenCaseDatabase(ex.getLocalizedMessage()), ex);
         }
     }
 
     /**
-     * Completes the case opening tasks common to both new cases and existing
-     * cases.
+     * Opens the case-level services: the files manager, tags manager and
+     * blackboard.
      *
      * @param progressIndicator A progress indicator.
-     *
-     * @throws CaseActionException
      */
     @Messages({
-        "Case.progressMessage.switchingLogDirectory=Switching log directory...",
-        "Case.progressMessage.clearingTempDirectory=Clearing case temp directory...",
-        "Case.progressMessage.openingCaseLevelServices=Opening case-level services...",
-        "Case.progressMessage.openingApplicationServiceResources=Opening application service case resources...",
-        "Case.progressMessage.settingUpNetworkCommunications=Setting up network communications...",})
-    private void openServices(ProgressIndicator progressIndicator) throws CaseActionException {
-        /*
-         * Switch to writing to the application logs in the logs subdirectory of
-         * the case directory.
-         */
-        progressIndicator.progress(Bundle.Case_progressMessage_switchingLogDirectory());
-        Logger.setLogDirectory(getLogDirectoryPath());
-        if (Thread.currentThread().isInterrupted()) {
-            throw new CaseActionCancelledException(Bundle.Case_exceptionMessage_cancelledByUser());
-        }
-
-        /*
-         * Clear the temp subdirectory of the case directory.
-         */
-        progressIndicator.progress(Bundle.Case_progressMessage_clearingTempDirectory());
-        Case.clearTempSubDir(this.getTempDirectory());
-        if (Thread.currentThread().isInterrupted()) {
-            throw new CaseActionCancelledException(Bundle.Case_exceptionMessage_cancelledByUser());
-        }
-
-        /*
-         * Open the case-level services.
-         */
+        "Case.progressMessage.openingCaseLevelServices=Opening case-level services...",})
+    private void openCaseLevelServices(ProgressIndicator progressIndicator) {
         progressIndicator.progress(Bundle.Case_progressMessage_openingCaseLevelServices());
         this.caseServices = new Services(caseDb);
-        if (Thread.currentThread().isInterrupted()) {
-            throw new CaseActionCancelledException(Bundle.Case_exceptionMessage_cancelledByUser());
-        }
-
-        /*
-         * Allow any registered application services to open any resources
-         * specific to this case.
-         */
-        progressIndicator.progress(Bundle.Case_progressMessage_openingApplicationServiceResources());
-        openAppServiceCaseResources();
-        if (Thread.currentThread().isInterrupted()) {
-            throw new CaseActionCancelledException(Bundle.Case_exceptionMessage_cancelledByUser());
-        }
-
-        /*
-         * If this case is a multi-user case, set up for communication with
-         * other nodes.
-         */
-        if (CaseType.MULTI_USER_CASE == metadata.getCaseType()) {
-            progressIndicator.progress(Bundle.Case_progressMessage_settingUpNetworkCommunications());
-            try {
-                eventPublisher.openRemoteEventChannel(String.format(EVENT_CHANNEL_NAME, metadata.getCaseName()));
-                if (Thread.currentThread().isInterrupted()) {
-                    throw new CaseActionCancelledException(Bundle.Case_exceptionMessage_cancelledByUser());
-                }
-                collaborationMonitor = new CollaborationMonitor(metadata.getCaseName());
-            } catch (AutopsyEventException | CollaborationMonitor.CollaborationMonitorException ex) {
-                /*
-                 * The collaboration monitor and event channel are not
-                 * essential. Log an error and notify the user, but do not
-                 * throw.
-                 */
-                logger.log(Level.SEVERE, "Failed to setup network communications", ex); //NON-NLS
-                if (RuntimeProperties.runningWithGUI()) {
-                    SwingUtilities.invokeLater(() -> MessageNotifyUtil.Notify.error(
-                            NbBundle.getMessage(Case.class, "Case.CollaborationSetup.FailNotify.Title"),
-                            NbBundle.getMessage(Case.class, "Case.CollaborationSetup.FailNotify.ErrMsg")));
-                }
-            }
-        }
     }
 
     /**
      * Allows any registered application-level services to open resources
      * specific to this case.
+     *
+     * @param progressIndicator A progress indicator.
+     *
+     * @throws CaseActionException If there is a problem completing the
+     *                             operation. The exception will have a
+     *                             user-friendly message and may be a wrapper
+     *                             for a lower-level exception.
      */
     @NbBundle.Messages({
+        "Case.progressMessage.openingApplicationServiceResources=Opening application service case resources...",
         "# {0} - service name", "Case.serviceOpenCaseResourcesProgressIndicator.title={0} Opening Case Resources",
         "# {0} - service name", "Case.serviceOpenCaseResourcesProgressIndicator.cancellingMessage=Cancelling opening case resources by {0}...",
         "# {0} - service name", "Case.servicesException.notificationTitle={0} Error"
     })
-    private void openAppServiceCaseResources() throws CaseActionException {
+    private void openAppServiceCaseResources(ProgressIndicator progressIndicator) throws CaseActionException {
+        progressIndicator.progress(Bundle.Case_progressMessage_openingApplicationServiceResources());
         /*
          * Each service gets its own independently cancellable/interruptible
          * task, running in a named thread managed by an executor service, with
@@ -2210,20 +2308,20 @@ private void openAppServiceCaseResources() throws CaseActionException {
              * with a Cancel button.
              */
             CancelButtonListener cancelButtonListener = null;
-            ProgressIndicator progressIndicator;
+            ProgressIndicator appServiceProgressIndicator;
             if (RuntimeProperties.runningWithGUI()) {
                 cancelButtonListener = new CancelButtonListener(Bundle.Case_serviceOpenCaseResourcesProgressIndicator_cancellingMessage(service.getServiceName()));
-                progressIndicator = new ModalDialogProgressIndicator(
+                appServiceProgressIndicator = new ModalDialogProgressIndicator(
                         mainFrame,
                         Bundle.Case_serviceOpenCaseResourcesProgressIndicator_title(service.getServiceName()),
                         new String[]{Bundle.Case_progressIndicatorCancelButton_label()},
                         Bundle.Case_progressIndicatorCancelButton_label(),
                         cancelButtonListener);
             } else {
-                progressIndicator = new LoggingProgressIndicator();
+                appServiceProgressIndicator = new LoggingProgressIndicator();
             }
-            progressIndicator.start(Bundle.Case_progressMessage_preparing());
-            AutopsyService.CaseContext context = new AutopsyService.CaseContext(this, progressIndicator);
+            appServiceProgressIndicator.start(Bundle.Case_progressMessage_preparing());
+            AutopsyService.CaseContext context = new AutopsyService.CaseContext(this, appServiceProgressIndicator);
             String threadNameSuffix = service.getServiceName().replaceAll("[ ]", "-"); //NON-NLS
             threadNameSuffix = threadNameSuffix.toLowerCase();
             TaskThreadFactory threadFactory = new TaskThreadFactory(String.format(CASE_RESOURCES_THREAD_NAME, threadNameSuffix));
@@ -2279,17 +2377,50 @@ private void openAppServiceCaseResources() throws CaseActionException {
                  * task responded to a cancellation request.
                  */
                 ThreadUtils.shutDownTaskExecutor(executor);
-                progressIndicator.finish();
+                appServiceProgressIndicator.finish();
             }
+            checkForUserCancellation();
+        }
+    }
 
-            if (Thread.currentThread().isInterrupted()) {
-                throw new CaseActionCancelledException(Bundle.Case_exceptionMessage_cancelledByUser());
+    /**
+     * If this case is a multi-user case, sets up for communication with other
+     * application nodes.
+     *
+     * @param progressIndicator A progress indicator.
+     *
+     * @throws CaseActionException If there is a problem completing the
+     *                             operation. The exception will have a
+     *                             user-friendly message and may be a wrapper
+     *                             for a lower-level exception.
+     */
+    @Messages({
+        "Case.progressMessage.settingUpNetworkCommunications=Setting up network communications...",
+        "# {0} - exception message", "Case.exceptionMessage.couldNotOpenRemoteEventChannel=Failed to open remote events channel:\n{0}.",
+        "# {0} - exception message", "Case.exceptionMessage.couldNotCreatCollaborationMonitor=Failed to create collaboration monitor:\n{0}."
+    })
+    private void openCommunicationChannels(ProgressIndicator progressIndicator) throws CaseActionException {
+        if (CaseType.MULTI_USER_CASE == metadata.getCaseType()) {
+            progressIndicator.progress(Bundle.Case_progressMessage_settingUpNetworkCommunications());
+            try {
+                eventPublisher.openRemoteEventChannel(String.format(EVENT_CHANNEL_NAME, metadata.getCaseName()));
+                checkForUserCancellation();
+                collaborationMonitor = new CollaborationMonitor(metadata.getCaseName());
+            } catch (AutopsyEventException ex) {
+                throw new CaseActionException(Bundle.Case_exceptionMessage_couldNotOpenRemoteEventChannel(ex.getLocalizedMessage()), ex);
+            } catch (CollaborationMonitor.CollaborationMonitorException ex) {
+                throw new CaseActionException(Bundle.Case_exceptionMessage_couldNotCreatCollaborationMonitor(ex.getLocalizedMessage()), ex);
             }
         }
     }
 
     /**
      * Closes the case.
+     *
+     * @throws CaseActionException If there is a problem completing the
+     *                             operation. The exception will have a
+     *                             user-friendly message and may be a wrapper
+     *                             for a lower-level exception.
      */
     private void close() throws CaseActionException {
         /*
diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/CaseBrowser.java b/Core/src/org/sleuthkit/autopsy/casemodule/CaseBrowser.java
deleted file mode 100644
index 402b64d78f44bc93244aa02e26ed39ff5091f812..0000000000000000000000000000000000000000
--- a/Core/src/org/sleuthkit/autopsy/casemodule/CaseBrowser.java
+++ /dev/null
@@ -1,265 +0,0 @@
-/*
- * Autopsy Forensic Browser
- *
- * Copyright 2017-2018 Basis Technology Corp.
- * Contact: carrier <at> sleuthkit <dot> org
- *
- * 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.sleuthkit.autopsy.casemodule;
-
-import java.lang.reflect.InvocationTargetException;
-import javax.swing.ListSelectionModel;
-import javax.swing.event.ListSelectionListener;
-import javax.swing.table.TableColumnModel;
-import org.netbeans.swing.etable.ETableColumn;
-import org.netbeans.swing.etable.ETableColumnModel;
-import org.netbeans.swing.outline.DefaultOutlineModel;
-import org.netbeans.swing.outline.Outline;
-import org.openide.nodes.Node;
-import java.awt.EventQueue;
-import java.io.File;
-import java.io.IOException;
-import java.nio.file.LinkOption;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.logging.Level;
-import javax.swing.SwingWorker;
-import org.openide.explorer.ExplorerManager;
-import org.openide.util.NbBundle;
-import org.sleuthkit.autopsy.coordinationservice.CoordinationService;
-import org.sleuthkit.autopsy.coreutils.Logger;
-import org.sleuthkit.autopsy.datamodel.EmptyNode;
-
-/**
- * A Swing JPanel with a scroll pane child component. The scroll pane contain
- * the table of cases.
- *
- * Used to display a list of multi user cases and allow the user to open one of
- * them.
- */
-@SuppressWarnings("PMD.SingularField") // UI widgets cause lots of false positives
-class CaseBrowser extends javax.swing.JPanel implements ExplorerManager.Provider {
-
-    private static final long serialVersionUID = 1L;
-    private final Outline outline;
-    private ExplorerManager em;
-    private final org.openide.explorer.view.OutlineView outlineView;
-    private int originalPathColumnIndex = 0;
-    private static final Logger LOGGER = Logger.getLogger(CaseBrowser.class.getName());
-    private LoadCaseListWorker tableWorker;
-
-    @Override
-    public ExplorerManager getExplorerManager() {
-        return em;
-    }
-
-    /**
-     * Creates a new CaseBrowser
-     */
-    CaseBrowser() {
-        outlineView = new org.openide.explorer.view.OutlineView();
-        initComponents();
-
-        outline = outlineView.getOutline();
-        outlineView.setPropertyColumns(
-                Bundle.CaseNode_column_createdTime(), Bundle.CaseNode_column_createdTime(),
-                Bundle.CaseNode_column_metadataFilePath(), Bundle.CaseNode_column_metadataFilePath());
-        ((DefaultOutlineModel) outline.getOutlineModel()).setNodesColumnLabel(Bundle.CaseNode_column_name());
-        customize();
-
-    }
-
-    /**
-     * Configures the the table of cases and its columns.
-     */
-    private void customize() {
-        outline.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
-        TableColumnModel columnModel = outline.getColumnModel();
-        int dateColumnIndex = 0;
-        for (int index = 0; index < columnModel.getColumnCount(); index++) {
-            //get indexes for created date column and path column
-            if (columnModel.getColumn(index).getHeaderValue().toString().equals(Bundle.CaseNode_column_metadataFilePath())) {
-                originalPathColumnIndex = index;
-            } else if (columnModel.getColumn(index).getHeaderValue().toString().equals(Bundle.CaseNode_column_createdTime())) {
-                dateColumnIndex = index;
-            }
-        }
-        //Hide path column by default (user can unhide it)
-        ETableColumn column = (ETableColumn) columnModel.getColumn(originalPathColumnIndex);
-        ((ETableColumnModel) columnModel).setColumnHidden(column, true);
-        outline.setRootVisible(false);
-
-        //Sort on Created date column in descending order by default
-        outline.setColumnSorted(dateColumnIndex, false, 1);
-        if (null == em) {
-            em = new ExplorerManager();
-        }
-        caseTableScrollPane.setViewportView(outlineView);
-        this.setVisible(true);
-        outline.setRowSelectionAllowed(false);
-    }
-
-    /**
-     * Add a listener to changes in case selections in the table
-     *
-     * @param listener the ListSelectionListener to add
-     */
-    void addListSelectionListener(ListSelectionListener listener) {
-        outline.getSelectionModel().addListSelectionListener(listener);
-    }
-
-    /**
-     * Get the path to the .aut file for the selected case.
-     *
-     * @return the full path to the selected case's .aut file
-     */
-    String getCasePath() {
-        int[] selectedRows = outline.getSelectedRows();
-        if (selectedRows.length == 1) {
-            try {
-                return ((Node.Property) outline.getModel().getValueAt(outline.convertRowIndexToModel(selectedRows[0]), originalPathColumnIndex)).getValue().toString();
-            } catch (IllegalAccessException | InvocationTargetException ex) {
-                LOGGER.log(Level.SEVERE, "Unable to get case path from table.", ex);
-            }
-        }
-        return null;
-    }
-
-    /**
-     * Check if a row could be and is selected.
-     *
-     * @return true if a row is selected, false if no row is selected
-     */
-    boolean isRowSelected() {
-        return outline.getRowSelectionAllowed() && outline.getSelectedRows().length > 0;
-    }
-
-    @NbBundle.Messages({"CaseBrowser.caseListLoading.message=Please Wait..."})
-    /**
-     * Gets the list of cases known to the review mode cases manager and
-     * refreshes the cases table.
-     */
-    void refresh() {
-        if (tableWorker == null || tableWorker.isDone()) {
-            outline.setRowSelectionAllowed(false);
-            //create a new TableWorker to and execute it in a background thread if one is not currently working
-            //set the table to display text informing the user that the list is being retreived and disable case selection
-            EmptyNode emptyNode = new EmptyNode(Bundle.CaseBrowser_caseListLoading_message());
-            em.setRootContext(emptyNode);
-            tableWorker = new LoadCaseListWorker();
-            tableWorker.execute();
-        }
-
-    }
-
-    /**
-     * This method is called from within the constructor to initialize the form.
-     * WARNING: Do NOT modify this code. The content of this method is always
-     * regenerated by the Form Editor.
-     */
-    @SuppressWarnings("unchecked")
-    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
-    private void initComponents() {
-
-        caseTableScrollPane = new javax.swing.JScrollPane();
-
-        setMinimumSize(new java.awt.Dimension(0, 5));
-        setPreferredSize(new java.awt.Dimension(5, 5));
-        setLayout(new java.awt.BorderLayout());
-
-        caseTableScrollPane.setHorizontalScrollBarPolicy(javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
-        caseTableScrollPane.setMinimumSize(new java.awt.Dimension(0, 5));
-        caseTableScrollPane.setOpaque(false);
-        caseTableScrollPane.setPreferredSize(new java.awt.Dimension(5, 5));
-        add(caseTableScrollPane, java.awt.BorderLayout.CENTER);
-    }// </editor-fold>//GEN-END:initComponents
-    // Variables declaration - do not modify//GEN-BEGIN:variables
-    private javax.swing.JScrollPane caseTableScrollPane;
-    // End of variables declaration//GEN-END:variables
-
-    /**
-     * Swingworker to fetch the updated List of cases in a background thread
-     */
-    private class LoadCaseListWorker extends SwingWorker<Void, Void> {
-
-        private List<CaseMetadata> cases;
-
-        /**
-         * Gets a list of the cases in the top level case folder
-         *
-         * @return List of cases.
-         *
-         * @throws CoordinationServiceException
-         */
-        private List<CaseMetadata> getCases() throws CoordinationService.CoordinationServiceException {
-            List<CaseMetadata> caseList = new ArrayList<>();
-            List<String> nodeList = CoordinationService.getInstance().getNodeList(CoordinationService.CategoryNode.CASES);
-
-            for (String node : nodeList) {
-                Path casePath;
-                try {
-                    casePath = Paths.get(node).toRealPath(LinkOption.NOFOLLOW_LINKS);
-
-                    File caseFolder = casePath.toFile();
-                    if (caseFolder.exists()) {
-                        /*
-                         * Search for '*.aut' files.
-                         */
-                        File[] fileArray = caseFolder.listFiles();
-                        if (fileArray == null) {
-                            continue;
-                        }
-                        String autFilePath = null;
-                        for (File file : fileArray) {
-                            String name = file.getName().toLowerCase();
-                            if (autFilePath == null && name.endsWith(".aut")) {
-                                try {
-                                    caseList.add(new CaseMetadata(Paths.get(file.getAbsolutePath())));
-                                } catch (CaseMetadata.CaseMetadataException ex) {
-                                    LOGGER.log(Level.SEVERE, String.format("Error reading case metadata file '%s'.", autFilePath), ex);
-                                }
-                                break;
-                            }
-                        }
-                    }
-                } catch (IOException ignore) {
-                    //if a path could not be resolved to a real path do add it to the caseList
-                }
-            }
-            return caseList;
-        }
-
-        @Override
-        protected Void doInBackground() throws Exception {
-
-            try {
-                cases = getCases();
-            } catch (CoordinationService.CoordinationServiceException ex) {
-                LOGGER.log(Level.SEVERE, "Unexpected exception while refreshing the table.", ex); //NON-NLS
-            }
-            return null;
-        }
-
-        @Override
-        protected void done() {
-            EventQueue.invokeLater(() -> {
-                MultiUserNode caseListNode = new MultiUserNode(cases);
-                em.setRootContext(caseListNode);
-                outline.setRowSelectionAllowed(true);
-            });
-        }
-    }
-}
diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/CaseMetadata.java b/Core/src/org/sleuthkit/autopsy/casemodule/CaseMetadata.java
index 6e47363db541b5ce8e46606e99a086711b2103c3..202ed86f7222d0cf8c0573a57e9412a454a7565e 100644
--- a/Core/src/org/sleuthkit/autopsy/casemodule/CaseMetadata.java
+++ b/Core/src/org/sleuthkit/autopsy/casemodule/CaseMetadata.java
@@ -1,7 +1,7 @@
 /*
  * Autopsy Forensic Browser
  *
- * Copyright 2011-2017 Basis Technology Corp.
+ * Copyright 2011-2019 Basis Technology Corp.
  * Contact: carrier <at> sleuthkit <dot> org
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
@@ -28,6 +28,7 @@
 import java.text.DateFormat;
 import java.text.SimpleDateFormat;
 import java.util.Date;
+import java.util.Locale;
 import javax.xml.parsers.DocumentBuilder;
 import javax.xml.parsers.DocumentBuilderFactory;
 import javax.xml.parsers.ParserConfigurationException;
@@ -52,7 +53,8 @@
 public final class CaseMetadata {
 
     private static final String FILE_EXTENSION = ".aut";
-    private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss (z)");
+    private static final String DATE_FORMAT_STRING = "yyyy/MM/dd HH:mm:ss (z)";
+    private static final DateFormat DATE_FORMAT = new SimpleDateFormat(DATE_FORMAT_STRING, Locale.US);
 
     /*
      * Fields from schema version 1
@@ -92,6 +94,7 @@ public final class CaseMetadata {
     private final static String EXAMINER_ELEMENT_PHONE = "ExaminerPhone"; //NON-NLS  
     private final static String EXAMINER_ELEMENT_EMAIL = "ExaminerEmail"; //NON-NLS
     private final static String CASE_ELEMENT_NOTES = "CaseNotes"; //NON-NLS
+    
     /*
      * Unread fields, regenerated on save.
      */
@@ -119,6 +122,15 @@ public static String getFileExtension() {
         return FILE_EXTENSION;
     }
 
+    /**
+     * Gets the date format used for dates in case metadata.
+     *
+     * @return The date format.
+     */
+    public static DateFormat getDateFormat() {
+        return new SimpleDateFormat(DATE_FORMAT_STRING, Locale.US);
+    }
+
     /**
      * Constructs a CaseMetadata object for a new case. The metadata is not
      * persisted to the case metadata file until writeFile or a setX method is
@@ -295,7 +307,7 @@ public String getTextIndexName() {
      *
      * @return The date this case was created, as a string.
      */
-    String getCreatedDate() {
+    public String getCreatedDate() {
         return createdDate;
     }
 
@@ -352,7 +364,7 @@ void setCreatedByVersion(String buildVersion) throws CaseMetadataException {
      * @throws CaseMetadataException If there is an error writing to the case
      *                               metadata file.
      */
-    private void writeToFile() throws CaseMetadataException {
+    void writeToFile() throws CaseMetadataException {
         try {
             /*
              * Create the XML DOM.
diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/CaseOpenAction.java b/Core/src/org/sleuthkit/autopsy/casemodule/CaseOpenAction.java
index a8e18da6507b6bf7719c064e1a050e0f6019809a..47c002d36b96e6a09a8d22617996acdcd0013bfb 100644
--- a/Core/src/org/sleuthkit/autopsy/casemodule/CaseOpenAction.java
+++ b/Core/src/org/sleuthkit/autopsy/casemodule/CaseOpenAction.java
@@ -161,7 +161,7 @@ public void actionPerformed(ActionEvent e) {
             WindowManager.getDefault().getMainWindow().setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
 
             if (multiUserCaseWindow == null) {
-                multiUserCaseWindow = MultiUserCasesDialog.getInstance();
+                multiUserCaseWindow = OpenMultiUserCaseDialog.getInstance();
             }
             multiUserCaseWindow.setLocationRelativeTo(WindowManager.getDefault().getMainWindow());
             multiUserCaseWindow.setVisible(true);
diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/MultiUserCaseNode.java b/Core/src/org/sleuthkit/autopsy/casemodule/MultiUserCaseNode.java
new file mode 100755
index 0000000000000000000000000000000000000000..aa689b0c250c59cdffd49bbfbf63558371776369
--- /dev/null
+++ b/Core/src/org/sleuthkit/autopsy/casemodule/MultiUserCaseNode.java
@@ -0,0 +1,103 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2017-2019 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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.sleuthkit.autopsy.casemodule;
+
+import org.sleuthkit.autopsy.coordinationservice.CaseNodeData;
+import java.util.ArrayList;
+import java.util.List;
+import javax.swing.Action;
+import org.openide.nodes.AbstractNode;
+import org.openide.nodes.Children;
+import org.openide.nodes.Sheet;
+import org.openide.util.NbBundle;
+import org.sleuthkit.autopsy.datamodel.NodeProperty;
+
+/**
+ * A NetBeans Explorer View node that represents a multi-user case.
+ */
+final class MultiUserCaseNode extends AbstractNode {
+
+    private final CaseNodeData caseNodeData;
+
+    /**
+     * Constructs a NetBeans Explorer View node that represents a multi-user
+     * case.
+     *
+     * @param caseNodeData The coordination service node data for the case.
+     */
+    MultiUserCaseNode(CaseNodeData caseNodeData) {
+        super(Children.LEAF);
+        super.setName(caseNodeData.getDisplayName());
+        setName(caseNodeData.getDisplayName());
+        setDisplayName(caseNodeData.getDisplayName());
+        this.caseNodeData = caseNodeData;
+    }
+
+    @NbBundle.Messages({
+        "MultiUserCaseNode.column.name=Name",
+        "MultiUserCaseNode.column.createTime=Create Time",
+        "MultiUserCaseNode.column.path=Path"
+    })
+    @Override
+    protected Sheet createSheet() {
+        Sheet sheet = super.createSheet();
+        Sheet.Set sheetSet = sheet.get(Sheet.PROPERTIES);
+        if (sheetSet == null) {
+            sheetSet = Sheet.createPropertiesSet();
+            sheet.put(sheetSet);
+        }
+        sheetSet.put(new NodeProperty<>(Bundle.MultiUserCaseNode_column_name(),
+                Bundle.MultiUserCaseNode_column_name(),
+                Bundle.MultiUserCaseNode_column_name(),
+                caseNodeData.getDisplayName()));
+        sheetSet.put(new NodeProperty<>(Bundle.MultiUserCaseNode_column_createTime(),
+                Bundle.MultiUserCaseNode_column_createTime(),
+                Bundle.MultiUserCaseNode_column_createTime(),
+                caseNodeData.getCreateDate()));
+        sheetSet.put(new NodeProperty<>(Bundle.MultiUserCaseNode_column_path(),
+                Bundle.MultiUserCaseNode_column_path(),
+                Bundle.MultiUserCaseNode_column_path(),
+                caseNodeData.getDirectory().toString()));
+        return sheet;
+    }
+
+    @Override
+    public Action[] getActions(boolean context) {
+        List<Action> actions = new ArrayList<>();
+        actions.add(new OpenMultiUserCaseAction(this.caseNodeData));
+        actions.add(new OpenCaseAutoIngestLogAction(this.caseNodeData));
+        return actions.toArray(new Action[actions.size()]);
+    }
+
+    @Override
+    public Action getPreferredAction() {
+        return new OpenMultiUserCaseAction(this.caseNodeData);
+    }
+
+    /**
+     * Gets the coordintaion service case node data this Explorer View node
+     * represents.
+     *
+     * @return The case node data.
+     */
+    CaseNodeData getCaseNodeData() {
+        return this.caseNodeData;
+    }
+    
+}
diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/MultiUserCaseNodeDataCollector.java b/Core/src/org/sleuthkit/autopsy/casemodule/MultiUserCaseNodeDataCollector.java
new file mode 100755
index 0000000000000000000000000000000000000000..d90fec1a64baab4f1d272bb3aefb497c34fb8471
--- /dev/null
+++ b/Core/src/org/sleuthkit/autopsy/casemodule/MultiUserCaseNodeDataCollector.java
@@ -0,0 +1,164 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2019-2019 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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.sleuthkit.autopsy.casemodule;
+
+import org.sleuthkit.autopsy.coordinationservice.CaseNodeData;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.logging.Level;
+import org.sleuthkit.autopsy.coordinationservice.CoordinationService;
+import org.sleuthkit.autopsy.coreutils.Logger;
+
+/**
+ * Queries the coordination service to collect the multi-user case node data
+ * stored in the case directory lock ZooKeeper nodes.
+ */
+final class MultiUserCaseNodeDataCollector {
+
+    private static final Logger logger = Logger.getLogger(MultiUserCaseNodeDataCollector.class.getName());
+    private static final String CASE_AUTO_INGEST_LOG_NAME = "AUTO_INGEST_LOG.TXT"; //NON-NLS
+    private static final String RESOURCES_LOCK_SUFFIX = "_RESOURCES"; //NON-NLS
+
+    /**
+     * Queries the coordination service to collect the multi-user case node data
+     * stored in the case directory lock ZooKeeper nodes.
+     *
+     * @return A list of CaseNodedata objects that convert data for a case
+     *         directory lock coordination service node to and from byte arrays.
+     *
+     * @throws CoordinationServiceException If there is an error
+     */
+    public static List<CaseNodeData> getNodeData() throws CoordinationService.CoordinationServiceException {
+        final List<CaseNodeData> cases = new ArrayList<>();
+        final CoordinationService coordinationService = CoordinationService.getInstance();
+        final List<String> nodeList = coordinationService.getNodeList(CoordinationService.CategoryNode.CASES);
+        for (String nodeName : nodeList) {
+            /*
+             * Ignore auto ingest case name lock nodes.
+             */
+            final Path nodeNameAsPath = Paths.get(nodeName);
+            if (!(nodeNameAsPath.toString().contains("\\") || nodeNameAsPath.toString().contains("//"))) {
+                continue;
+            }
+
+            /*
+             * Ignore case auto ingest log lock nodes and resource lock nodes.
+             */
+            final String lastNodeNameComponent = nodeNameAsPath.getFileName().toString();
+            if (lastNodeNameComponent.equals(CASE_AUTO_INGEST_LOG_NAME)) {
+                continue;
+            }
+
+            /*
+             * Ignore case resources lock nodes.
+             */
+            if (lastNodeNameComponent.endsWith(RESOURCES_LOCK_SUFFIX)) {
+                continue;
+            }
+
+            /*
+             * Get the data from the case directory lock node. This data may not
+             * exist for "legacy" nodes. If it is missing, create it.
+             */
+            try {
+                CaseNodeData nodeData;
+                byte[] nodeBytes = coordinationService.getNodeData(CoordinationService.CategoryNode.CASES, nodeName);
+                if (nodeBytes != null && nodeBytes.length > 0) {
+                    nodeData = new CaseNodeData(nodeBytes);
+                    if (nodeData.getVersion() == 0) {
+                        /*
+                         * Version 0 case node data was only written if errors
+                         * occurred during an auto ingest job and consisted of
+                         * only the set errors flag.
+                         */
+                        nodeData = createNodeDataFromCaseMetadata(nodeName, true);
+                    }
+                } else {
+                    nodeData = createNodeDataFromCaseMetadata(nodeName, false);
+                }
+                cases.add(nodeData);
+
+            } catch (CoordinationService.CoordinationServiceException | InterruptedException | IOException | ParseException | CaseMetadata.CaseMetadataException ex) {
+                logger.log(Level.SEVERE, String.format("Error getting coordination service node data for %s", nodeName), ex);
+            }
+
+        }
+        return cases;
+    }
+
+    /**
+     * Creates and saves case directory lock coordination service node data from
+     * the metadata file for the case associated with the node.
+     *
+     * @param nodeName       The coordination service node name, i.e., the case
+     *                       directory path.
+     * @param errorsOccurred Whether or not errors occurred during an auto
+     *                       ingest job for the case.
+     *
+     * @return A CaseNodedata object.
+     *
+     * @throws IOException                  If there is an error writing the
+     *                                      node data to a byte array.
+     * @throws CaseMetadataException        If there is an error reading the
+     *                                      case metadata file.
+     * @throws ParseException               If there is an error parsing a date
+     *                                      from the case metadata file.
+     * @throws CoordinationServiceException If there is an error interacting
+     *                                      with the coordination service.
+     * @throws InterruptedException         If a coordination service operation
+     *                                      is interrupted.
+     */
+    private static CaseNodeData createNodeDataFromCaseMetadata(String nodeName, boolean errorsOccurred) throws IOException, CaseMetadata.CaseMetadataException, ParseException, CoordinationService.CoordinationServiceException, InterruptedException {
+        CaseNodeData nodeData = null;
+        Path caseDirectoryPath = Paths.get(nodeName).toRealPath(LinkOption.NOFOLLOW_LINKS);
+        File caseDirectory = caseDirectoryPath.toFile();
+        if (caseDirectory.exists()) {
+            File[] files = caseDirectory.listFiles();
+            for (File file : files) {
+                String name = file.getName().toLowerCase();
+                if (name.endsWith(CaseMetadata.getFileExtension())) {
+                    CaseMetadata metadata = new CaseMetadata(Paths.get(file.getAbsolutePath()));
+                    nodeData = new CaseNodeData(metadata);
+                    nodeData.setErrorsOccurred(errorsOccurred);
+                    break;
+                }
+            }
+        }
+        if (nodeData != null) {
+            CoordinationService coordinationService = CoordinationService.getInstance();
+            coordinationService.setNodeData(CoordinationService.CategoryNode.CASES, nodeName, nodeData.toArray());
+            return nodeData;
+        } else {
+            throw new IOException(String.format("Could not find case metadata file for %s", nodeName));
+        }
+    }
+
+    /**
+     * Private constructor to prevent instantiation of this utility class.
+     */
+    private MultiUserCaseNodeDataCollector() {
+    }
+
+}
diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/CaseBrowser.form b/Core/src/org/sleuthkit/autopsy/casemodule/MultiUserCasesBrowserPanel.form
similarity index 100%
rename from Core/src/org/sleuthkit/autopsy/casemodule/CaseBrowser.form
rename to Core/src/org/sleuthkit/autopsy/casemodule/MultiUserCasesBrowserPanel.form
diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/MultiUserCasesBrowserPanel.java b/Core/src/org/sleuthkit/autopsy/casemodule/MultiUserCasesBrowserPanel.java
new file mode 100644
index 0000000000000000000000000000000000000000..620c89a2d6a613b8f6f523b195fc7dcc125a788c
--- /dev/null
+++ b/Core/src/org/sleuthkit/autopsy/casemodule/MultiUserCasesBrowserPanel.java
@@ -0,0 +1,144 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2017-2019 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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.sleuthkit.autopsy.casemodule;
+
+import javax.swing.ListSelectionModel;
+import javax.swing.event.ListSelectionListener;
+import javax.swing.table.TableColumnModel;
+import org.netbeans.swing.etable.ETableColumn;
+import org.netbeans.swing.etable.ETableColumnModel;
+import org.netbeans.swing.outline.DefaultOutlineModel;
+import org.netbeans.swing.outline.Outline;
+import org.openide.explorer.ExplorerManager;
+import org.openide.util.NbBundle;
+import org.openide.explorer.view.OutlineView;
+
+/**
+ * A JPanel with a scroll pane child component that contains a NetBeans
+ * OutlineView that can be used to display a list of the multi-user cases known
+ * to the coordination service.
+ */
+@SuppressWarnings("PMD.SingularField") // Matisse-generated UI widgets cause lots of false positives
+final class MultiUserCasesBrowserPanel extends javax.swing.JPanel implements ExplorerManager.Provider {
+
+    private static final long serialVersionUID = 1L;
+    private final ExplorerManager explorerManager;
+    private final OutlineView outlineView;
+    private final Outline outline;
+
+    /**
+     * Constructs a JPanel with a scroll pane child component that contains a
+     * NetBeans OutlineView that can be used to display a list of the multi-user
+     * cases known to the coordination service.
+     */
+    MultiUserCasesBrowserPanel() {
+        explorerManager = new ExplorerManager();
+        outlineView = new org.openide.explorer.view.OutlineView();
+        initComponents();
+        outline = outlineView.getOutline();
+        configureOutlineView();
+        explorerManager.setRootContext(new MultiUserCasesRootNode());
+    }
+
+    /**
+     * Configures the child scroll pane component's child OutlineView component.
+     */
+    private void configureOutlineView() {
+        outlineView.setPropertyColumns(
+                Bundle.MultiUserCaseNode_column_createTime(), Bundle.MultiUserCaseNode_column_createTime(),
+                Bundle.MultiUserCaseNode_column_path(), Bundle.MultiUserCaseNode_column_path());
+        ((DefaultOutlineModel) outline.getOutlineModel()).setNodesColumnLabel(Bundle.MultiUserCaseNode_column_name());
+        outline.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+        TableColumnModel columnModel = outline.getColumnModel();
+        int pathColumnIndex = 0;
+        int dateColumnIndex = 0;
+        for (int index = 0; index < columnModel.getColumnCount(); index++) {
+            if (columnModel.getColumn(index).getHeaderValue().toString().equals(Bundle.MultiUserCaseNode_column_path())) {
+                pathColumnIndex = index;
+            } else if (columnModel.getColumn(index).getHeaderValue().toString().equals(Bundle.MultiUserCaseNode_column_createTime())) {
+                dateColumnIndex = index;
+            }
+        }
+
+        /*
+         * Hide path column by default (user can unhide it)
+         */
+        ETableColumn column = (ETableColumn) columnModel.getColumn(pathColumnIndex);
+        ((ETableColumnModel) columnModel).setColumnHidden(column, true);
+        outline.setRootVisible(false);
+
+        /*
+         * Sort on Created date column in descending order by default.
+         */
+        outline.setColumnSorted(dateColumnIndex, false, 1);
+
+        caseTableScrollPane.setViewportView(outlineView);
+        this.setVisible(true);
+    }
+
+    @Override
+    public ExplorerManager getExplorerManager() {
+        return explorerManager;
+    }
+
+    /**
+     * Adds a listener to changes in case selection in this browser.
+     *
+     * @param listener the ListSelectionListener to add
+     */
+    void addListSelectionListener(ListSelectionListener listener) {
+        outline.getSelectionModel().addListSelectionListener(listener);
+    }    
+    
+    /**
+     * Refreshes the list of multi-user cases in this browser.
+     */
+    @NbBundle.Messages({
+        "MultiUserCasesBrowserPanel.waitNode.message=Please Wait..."
+    })
+    void refreshCases() {
+        explorerManager.setRootContext(new MultiUserCasesRootNode());
+    }
+
+    /**
+     * This method is called from within the constructor to initialize the form.
+     * WARNING: Do NOT modify this code. The content of this method is always
+     * regenerated by the Form Editor.
+     */
+    @SuppressWarnings("unchecked")
+    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
+    private void initComponents() {
+
+        caseTableScrollPane = new javax.swing.JScrollPane();
+
+        setMinimumSize(new java.awt.Dimension(0, 5));
+        setPreferredSize(new java.awt.Dimension(5, 5));
+        setLayout(new java.awt.BorderLayout());
+
+        caseTableScrollPane.setHorizontalScrollBarPolicy(javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
+        caseTableScrollPane.setMinimumSize(new java.awt.Dimension(0, 5));
+        caseTableScrollPane.setOpaque(false);
+        caseTableScrollPane.setPreferredSize(new java.awt.Dimension(5, 5));
+        add(caseTableScrollPane, java.awt.BorderLayout.CENTER);
+    }// </editor-fold>//GEN-END:initComponents
+    // Variables declaration - do not modify//GEN-BEGIN:variables
+    private javax.swing.JScrollPane caseTableScrollPane;
+    // End of variables declaration//GEN-END:variables
+
+}
diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/MultiUserCasesDialog.java b/Core/src/org/sleuthkit/autopsy/casemodule/MultiUserCasesDialog.java
deleted file mode 100644
index af95b0c5a0c87d6d70024d820b820f12f1e66db2..0000000000000000000000000000000000000000
--- a/Core/src/org/sleuthkit/autopsy/casemodule/MultiUserCasesDialog.java
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * Autopsy Forensic Browser
- *
- * Copyright 2011-2018 Basis Technology Corp.
- * Contact: carrier <at> sleuthkit <dot> org
- *
- * 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.sleuthkit.autopsy.casemodule;
-
-import java.awt.Dialog;
-import java.awt.event.KeyEvent;
-import javax.swing.JComponent;
-import javax.swing.JDialog;
-import javax.swing.KeyStroke;
-import org.openide.windows.WindowManager;
-
-/**
- * This class extends a JDialog and maintains the MultiUserCasesPanel.
- */
-@SuppressWarnings("PMD.SingularField") // UI widgets cause lots of false positives
-final class MultiUserCasesDialog extends JDialog {
-    
-    private static final long serialVersionUID = 1L;
-    private static final String REVIEW_MODE_TITLE = "Open Multi-User Case";
-    private static MultiUserCasesPanel multiUserCasesPanel;
-    private static MultiUserCasesDialog instance;
-
-    /**
-     * Gets the instance of the MultiuserCasesDialog.
-     *
-     * @return The instance.
-     */
-    static public MultiUserCasesDialog getInstance() {
-        if(instance == null) {
-            instance = new MultiUserCasesDialog();
-            instance.init();
-        }
-        return instance;
-    }
-    
-    /**
-     * Constructs a MultiUserCasesDialog object.
-     */
-    private MultiUserCasesDialog() {
-        super(WindowManager.getDefault().getMainWindow(),
-                REVIEW_MODE_TITLE,
-                Dialog.ModalityType.APPLICATION_MODAL);
-    }
-    
-    /**
-     * Initializes the multi-user cases panel.
-     */
-    private void init() {
-        getRootPane().registerKeyboardAction(
-                e -> {
-                    setVisible(false);
-                },
-                KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_IN_FOCUSED_WINDOW);
-        
-        multiUserCasesPanel = new MultiUserCasesPanel(this);
-        add(multiUserCasesPanel);
-        pack();
-        setResizable(false);
-    }
-    
-    /**
-     * Set the dialog visibility. When setting it to visible, the contents will
-     * refresh.
-     * 
-     * @param value True or false. 
-     */
-    @Override
-    public void setVisible(boolean value) {
-        if(value) {
-            multiUserCasesPanel.refresh();
-        }
-        super.setVisible(value);
-    }
-}
diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/MultiUserCasesPanel.java b/Core/src/org/sleuthkit/autopsy/casemodule/MultiUserCasesPanel.java
deleted file mode 100644
index 07d863bc2be6ddd3f734d8b6d8493fc0556d9561..0000000000000000000000000000000000000000
--- a/Core/src/org/sleuthkit/autopsy/casemodule/MultiUserCasesPanel.java
+++ /dev/null
@@ -1,261 +0,0 @@
-/*
- * Autopsy Forensic Browser
- *
- * Copyright 2011-2018 Basis Technology Corp.
- * Contact: carrier <at> sleuthkit <dot> org
- *
- * 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.sleuthkit.autopsy.casemodule;
-
-import java.awt.Cursor;
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.logging.Level;
-import javax.swing.JDialog;
-import javax.swing.JPanel;
-import javax.swing.SortOrder;
-import javax.swing.SwingUtilities;
-import javax.swing.event.ListSelectionEvent;
-import javax.swing.table.DefaultTableModel;
-import javax.swing.table.TableRowSorter;
-import org.openide.util.Lookup;
-import org.sleuthkit.autopsy.coreutils.Logger;
-import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil;
-
-/**
- * A panel that allows a user to open cases created by auto ingest.
- */
-@SuppressWarnings("PMD.SingularField") // UI widgets cause lots of false positives
-final class MultiUserCasesPanel extends JPanel{
-
-    private static final Logger logger = Logger.getLogger(MultiUserCasesPanel.class.getName());
-    private static final long serialVersionUID = 1L;
-    private final JDialog parentDialog;
-    private final CaseBrowser caseBrowserPanel;
-
-    /**
-     * Constructs a panel that allows a user to open cases created by automated
-     * ingest.
-     */
-    MultiUserCasesPanel(JDialog parentDialog) {
-        this.parentDialog = parentDialog;
-        initComponents();
-
-        caseBrowserPanel = new CaseBrowser();
-        caseExplorerScrollPane.add(caseBrowserPanel);
-        caseExplorerScrollPane.setViewportView(caseBrowserPanel);
-        /*
-         * Listen for row selection changes and set button state for the current
-         * selection.
-         */
-        caseBrowserPanel.addListSelectionListener((ListSelectionEvent e) -> {
-            setButtons();
-        });
-
-    }
-
-    /**
-     * Gets the list of cases known to the review mode cases manager and
-     * refreshes the cases table.
-     */
-    void refresh() {
-        caseBrowserPanel.refresh();
-    }
-
-    /**
-     * Enables/disables the Open and Show Log buttons based on the case selected
-     * in the cases table.
-     */
-    void setButtons() {
-        bnOpen.setEnabled(caseBrowserPanel.isRowSelected());
-    }
-
-    /**
-     * Open a case.
-     *
-     * @param caseMetadataFilePath The path to the case metadata file.
-     */
-    private void openCase(String caseMetadataFilePath) {
-        if (caseMetadataFilePath != null) {
-            setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
-
-            StartupWindowProvider.getInstance().close();
-            if (parentDialog != null) {
-                parentDialog.setVisible(false);
-            }
-            new Thread(() -> {
-                try {
-                    Case.openAsCurrentCase(caseMetadataFilePath);
-                } catch (CaseActionException ex) {
-                    if (null != ex.getCause() && !(ex.getCause() instanceof CaseActionCancelledException)) {
-                        logger.log(Level.SEVERE, String.format("Error opening case with metadata file path %s", caseMetadataFilePath), ex); //NON-NLS
-                        MessageNotifyUtil.Message.error(ex.getCause().getLocalizedMessage());
-                    }
-                    SwingUtilities.invokeLater(() -> {
-                        //GUI changes done back on the EDT
-                        StartupWindowProvider.getInstance().open();
-                    });
-                } finally {
-                    SwingUtilities.invokeLater(() -> {
-                        //GUI changes done back on the EDT
-                        setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
-                    });
-                }
-            }).start();
-        }
-    }
-
-    /**
-     * RowSorter which makes columns whose type is Date to be sorted first in
-     * Descending order then in Ascending order
-     */
-    private static class RowSorter<M extends DefaultTableModel> extends TableRowSorter<M> {
-
-        RowSorter(M tModel) {
-            super(tModel);
-        }
-
-        @Override
-        public void toggleSortOrder(int column) {
-            if (!this.getModel().getColumnClass(column).equals(Date.class)) {
-                super.toggleSortOrder(column);  //if it isn't a date column perform the regular sorting
-            } else {
-                ArrayList<RowSorter.SortKey> sortKeys = new ArrayList<>(getSortKeys());
-                if (sortKeys.isEmpty() || sortKeys.get(0).getColumn() != column) {  //sort descending
-                    sortKeys.add(0, new RowSorter.SortKey(column, SortOrder.DESCENDING));
-                } else if (sortKeys.get(0).getSortOrder() == SortOrder.ASCENDING) {
-                    sortKeys.removeIf(key -> key.getColumn() == column);
-                    sortKeys.add(0, new RowSorter.SortKey(column, SortOrder.DESCENDING));
-                } else {
-                    sortKeys.removeIf(key -> key.getColumn() == column);
-                    sortKeys.add(0, new RowSorter.SortKey(column, SortOrder.ASCENDING));
-                }
-                setSortKeys(sortKeys);
-            }
-        }
-    }
-
-    /**
-     * This method is called from within the constructor to initialize the form.
-     * WARNING: Do NOT modify this code. The content of this method is always
-     * regenerated by the Form Editor.
-     */
-    @SuppressWarnings("unchecked")
-    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
-    private void initComponents() {
-
-        bnOpen = new javax.swing.JButton();
-        bnOpenSingleUserCase = new javax.swing.JButton();
-        cancelButton = new javax.swing.JButton();
-        searchLabel = new javax.swing.JLabel();
-        caseExplorerScrollPane = new javax.swing.JScrollPane();
-
-        setName("Completed Cases"); // NOI18N
-        setPreferredSize(new java.awt.Dimension(960, 485));
-
-        org.openide.awt.Mnemonics.setLocalizedText(bnOpen, org.openide.util.NbBundle.getMessage(MultiUserCasesPanel.class, "MultiUserCasesPanel.bnOpen.text")); // NOI18N
-        bnOpen.setEnabled(false);
-        bnOpen.setMaximumSize(new java.awt.Dimension(80, 23));
-        bnOpen.setMinimumSize(new java.awt.Dimension(80, 23));
-        bnOpen.setPreferredSize(new java.awt.Dimension(80, 23));
-        bnOpen.addActionListener(new java.awt.event.ActionListener() {
-            public void actionPerformed(java.awt.event.ActionEvent evt) {
-                bnOpenActionPerformed(evt);
-            }
-        });
-
-        org.openide.awt.Mnemonics.setLocalizedText(bnOpenSingleUserCase, org.openide.util.NbBundle.getMessage(MultiUserCasesPanel.class, "MultiUserCasesPanel.bnOpenSingleUserCase.text")); // NOI18N
-        bnOpenSingleUserCase.setMinimumSize(new java.awt.Dimension(156, 23));
-        bnOpenSingleUserCase.setPreferredSize(new java.awt.Dimension(156, 23));
-        bnOpenSingleUserCase.addActionListener(new java.awt.event.ActionListener() {
-            public void actionPerformed(java.awt.event.ActionEvent evt) {
-                bnOpenSingleUserCaseActionPerformed(evt);
-            }
-        });
-
-        org.openide.awt.Mnemonics.setLocalizedText(cancelButton, org.openide.util.NbBundle.getMessage(MultiUserCasesPanel.class, "MultiUserCasesPanel.cancelButton.text")); // NOI18N
-        cancelButton.setMaximumSize(new java.awt.Dimension(80, 23));
-        cancelButton.setMinimumSize(new java.awt.Dimension(80, 23));
-        cancelButton.setPreferredSize(new java.awt.Dimension(80, 23));
-        cancelButton.addActionListener(new java.awt.event.ActionListener() {
-            public void actionPerformed(java.awt.event.ActionEvent evt) {
-                cancelButtonActionPerformed(evt);
-            }
-        });
-
-        org.openide.awt.Mnemonics.setLocalizedText(searchLabel, org.openide.util.NbBundle.getMessage(MultiUserCasesPanel.class, "MultiUserCasesPanel.searchLabel.text")); // NOI18N
-
-        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
-        this.setLayout(layout);
-        layout.setHorizontalGroup(
-            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
-            .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup()
-                .addContainerGap()
-                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING)
-                    .addComponent(caseExplorerScrollPane)
-                    .addGroup(layout.createSequentialGroup()
-                        .addComponent(searchLabel)
-                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
-                        .addComponent(bnOpenSingleUserCase, javax.swing.GroupLayout.PREFERRED_SIZE, 192, javax.swing.GroupLayout.PREFERRED_SIZE)
-                        .addGap(190, 190, 190)
-                        .addComponent(bnOpen, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
-                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
-                        .addComponent(cancelButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)))
-                .addContainerGap())
-        );
-
-        layout.linkSize(javax.swing.SwingConstants.HORIZONTAL, new java.awt.Component[] {bnOpen, cancelButton});
-
-        layout.setVerticalGroup(
-            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
-            .addGroup(layout.createSequentialGroup()
-                .addGap(6, 6, 6)
-                .addComponent(caseExplorerScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, 450, javax.swing.GroupLayout.PREFERRED_SIZE)
-                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
-                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
-                    .addComponent(cancelButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
-                    .addComponent(bnOpen, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
-                    .addComponent(bnOpenSingleUserCase, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
-                    .addComponent(searchLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
-                .addContainerGap())
-        );
-    }// </editor-fold>//GEN-END:initComponents
-
-    /**
-     * Open button action
-     *
-     * @param evt -- The event that caused this to be called
-     */
-    private void bnOpenActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_bnOpenActionPerformed
-        openCase(caseBrowserPanel.getCasePath());
-    }//GEN-LAST:event_bnOpenActionPerformed
-
-    private void bnOpenSingleUserCaseActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_bnOpenSingleUserCaseActionPerformed
-        Lookup.getDefault().lookup(CaseOpenAction.class).openCaseSelectionWindow();
-    }//GEN-LAST:event_bnOpenSingleUserCaseActionPerformed
-
-    private void cancelButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_cancelButtonActionPerformed
-        if (parentDialog != null) {
-            parentDialog.setVisible(false);
-        }
-    }//GEN-LAST:event_cancelButtonActionPerformed
-
-    // Variables declaration - do not modify//GEN-BEGIN:variables
-    private javax.swing.JButton bnOpen;
-    private javax.swing.JButton bnOpenSingleUserCase;
-    private javax.swing.JButton cancelButton;
-    private javax.swing.JScrollPane caseExplorerScrollPane;
-    private javax.swing.JLabel searchLabel;
-    // End of variables declaration//GEN-END:variables
-}
diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/MultiUserCasesRootNode.java b/Core/src/org/sleuthkit/autopsy/casemodule/MultiUserCasesRootNode.java
new file mode 100644
index 0000000000000000000000000000000000000000..7a8ec7893886de3c452f3d4df6e89fa7594f74e8
--- /dev/null
+++ b/Core/src/org/sleuthkit/autopsy/casemodule/MultiUserCasesRootNode.java
@@ -0,0 +1,74 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2017-2019 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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.sleuthkit.autopsy.casemodule;
+
+import org.sleuthkit.autopsy.coordinationservice.CaseNodeData;
+import java.util.List;
+import java.util.logging.Level;
+import org.openide.nodes.AbstractNode;
+import org.openide.nodes.ChildFactory;
+import org.openide.nodes.Children;
+import org.openide.nodes.Node;
+import org.sleuthkit.autopsy.coordinationservice.CoordinationService;
+import org.sleuthkit.autopsy.coreutils.Logger;
+
+/**
+ * A root node for displaying MultiUserCaseNodes in a NetBeans Explorer View.
+ */
+final class MultiUserCasesRootNode extends AbstractNode {
+
+    private static final Logger logger = Logger.getLogger(MultiUserCasesRootNode.class.getName());
+
+    /**
+     * Constructs a root node for displaying MultiUserCaseNodes in a NetBeans
+     * Explorer View.
+     *
+     * @param case A list of coordination service node data objects representing
+     *             multi-user cases.
+     */
+    MultiUserCasesRootNode() {
+        super(Children.create(new MultiUserCasesRootNodeChildren(), true));
+    }
+
+    /**
+     * A child factory for creating child nodes for a MultiUserCasesRootNode.
+     * The child nodes are of type MultiUserCaseNode. The node keys are of type
+     * CaseNodeData.
+     */
+    private static class MultiUserCasesRootNodeChildren extends ChildFactory<CaseNodeData> {
+
+        @Override
+        protected boolean createKeys(List<CaseNodeData> keys) {
+            try {
+                List<CaseNodeData> caseNodeData = MultiUserCaseNodeDataCollector.getNodeData();
+                keys.addAll(caseNodeData);
+            } catch (CoordinationService.CoordinationServiceException ex) {
+                logger.log(Level.SEVERE, "Failed to get case node data from coodination service", ex);
+            }
+            return true;
+        }
+
+        @Override
+        protected Node createNodeForKey(CaseNodeData key) {
+            return new MultiUserCaseNode(key);
+        }
+
+    }
+
+}
diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/MultiUserNode.java b/Core/src/org/sleuthkit/autopsy/casemodule/MultiUserNode.java
deleted file mode 100644
index 1efbffe19011d4b7473919ac46b55f9435b50a1c..0000000000000000000000000000000000000000
--- a/Core/src/org/sleuthkit/autopsy/casemodule/MultiUserNode.java
+++ /dev/null
@@ -1,232 +0,0 @@
-/*
- * Autopsy Forensic Browser
- *
- * Copyright 2017-2018 Basis Technology Corp.
- * Contact: carrier <at> sleuthkit <dot> org
- *
- * 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.sleuthkit.autopsy.casemodule;
-
-import java.awt.Desktop;
-import java.awt.event.ActionEvent;
-import java.io.IOException;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.logging.Level;
-import javax.swing.AbstractAction;
-import javax.swing.Action;
-import javax.swing.JOptionPane;
-import javax.swing.SwingUtilities;
-import org.openide.nodes.AbstractNode;
-import org.openide.nodes.ChildFactory;
-import org.openide.nodes.Children;
-import org.openide.nodes.Node;
-import org.openide.nodes.Sheet;
-import org.openide.util.NbBundle.Messages;
-import org.sleuthkit.autopsy.coreutils.Logger;
-import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil;
-import org.sleuthkit.autopsy.datamodel.NodeProperty;
-
-/**
- * A root node containing child nodes of the multi user cases
- */
-final class MultiUserNode extends AbstractNode {
-
-    @Messages({"CaseNode.column.name=Name",
-        "CaseNode.column.createdTime=Created Time",
-        "CaseNode.column.metadataFilePath=Path"})
-    private static final Logger LOGGER = Logger.getLogger(MultiUserNode.class.getName());
-    private static final String LOG_FILE_NAME = "auto_ingest_log.txt";
-
-    /**
-     * Provides a root node with children which each represent a case.
-     *
-     * @param caseList the list of CaseMetadata objects representing the cases
-     */
-    MultiUserNode(List<CaseMetadata> caseList) {
-        super(Children.create(new MultiUserNodeChildren(caseList), true));
-    }
-
-    static class MultiUserNodeChildren extends ChildFactory<CaseMetadata> {
-
-        private final List<CaseMetadata> caseList;
-
-        MultiUserNodeChildren(List<CaseMetadata> caseList) {
-            this.caseList = caseList;
-        }
-
-        @Override
-        protected boolean createKeys(List<CaseMetadata> list) {
-            if (caseList != null && caseList.size() > 0) {
-                list.addAll(caseList);
-            }
-            return true;
-        }
-
-        @Override
-        protected Node createNodeForKey(CaseMetadata key) {
-            return new MultiUserCaseNode(key);
-        }
-
-    }
-
-    /**
-     * A node which represents a single multi user case.
-     */
-    static final class MultiUserCaseNode extends AbstractNode {
-
-        private final String caseName;
-        private final String caseCreatedDate;
-        private final String caseMetadataFilePath;
-        private final Path caseLogFilePath;
-
-        MultiUserCaseNode(CaseMetadata multiUserCase) {
-            super(Children.LEAF);
-            caseName = multiUserCase.getCaseDisplayName();
-            caseCreatedDate = multiUserCase.getCreatedDate();
-            super.setName(caseName);
-            setName(caseName);
-            setDisplayName(caseName);
-            caseMetadataFilePath = multiUserCase.getFilePath().toString();
-            caseLogFilePath = Paths.get(multiUserCase.getCaseDirectory(), LOG_FILE_NAME);
-        }
-        
-        /**
-         * Returns action to open the Case represented by this node
-         * @return an action which will open the current case
-         */
-        @Override 
-        public Action getPreferredAction() {
-            return new OpenMultiUserCaseAction(caseMetadataFilePath);
-        }
-
-        @Override
-        protected Sheet createSheet() {
-            Sheet sheet = super.createSheet();
-            Sheet.Set sheetSet = sheet.get(Sheet.PROPERTIES);
-            if (sheetSet == null) {
-                sheetSet = Sheet.createPropertiesSet();
-                sheet.put(sheetSet);
-            }
-            sheetSet.put(new NodeProperty<>(Bundle.CaseNode_column_name(), Bundle.CaseNode_column_name(), Bundle.CaseNode_column_name(),
-                    caseName));
-            sheetSet.put(new NodeProperty<>(Bundle.CaseNode_column_createdTime(), Bundle.CaseNode_column_createdTime(), Bundle.CaseNode_column_createdTime(),
-                    caseCreatedDate));
-            sheetSet.put(new NodeProperty<>(Bundle.CaseNode_column_metadataFilePath(), Bundle.CaseNode_column_metadataFilePath(), Bundle.CaseNode_column_metadataFilePath(),
-                    caseMetadataFilePath));
-            return sheet;
-        }
-
-        @Override
-        public Action[] getActions(boolean context) {
-            List<Action> actions = new ArrayList<>();
-            actions.add(new OpenMultiUserCaseAction(caseMetadataFilePath));  //open case context menu option
-            actions.add(new OpenCaseLogAction(caseLogFilePath));
-            return actions.toArray(new Action[actions.size()]);
-        }
-    }
-
-    @Messages({"MultiUserNode.OpenMultiUserCaseAction.text=Open Case"})
-    /**
-     * An action that opens the specified case and hides the multi user case
-     * panel.
-     */
-    private static final class OpenMultiUserCaseAction extends AbstractAction {
-
-        private static final long serialVersionUID = 1L;
-
-        private final String caseMetadataFilePath;
-
-        OpenMultiUserCaseAction(String path) {
-            super(Bundle.MultiUserNode_OpenMultiUserCaseAction_text());
-            caseMetadataFilePath = path;
-        }
-
-        @Override
-        public void actionPerformed(ActionEvent e) {
-            StartupWindowProvider.getInstance().close();
-            MultiUserCasesDialog.getInstance().setVisible(false);
-            new Thread(
-                    () -> {
-                        try {
-                            Case.openAsCurrentCase(caseMetadataFilePath);
-                        } catch (CaseActionException ex) {
-                            if (null != ex.getCause() && !(ex.getCause() instanceof CaseActionCancelledException)) {
-                                LOGGER.log(Level.SEVERE, String.format("Error opening case with metadata file path %s", caseMetadataFilePath), ex); //NON-NLS
-                                MessageNotifyUtil.Message.error(ex.getCause().getLocalizedMessage());
-                            }
-                            SwingUtilities.invokeLater(() -> {
-                                //GUI changes done back on the EDT
-                                StartupWindowProvider.getInstance().open();
-                                MultiUserCasesDialog.getInstance().setVisible(true);
-                            });
-                        }
-                    }
-            ).start();
-        }
-
-        @Override
-        public Object clone() throws CloneNotSupportedException {
-            return super.clone(); //To change body of generated methods, choose Tools | Templates.
-        }
-    }
-
-    @Messages({"MultiUserNode.OpenCaseLogAction.text=Open Log File"})
-    /**
-     * An action that opens the specified case and hides the multi user case
-     * panel.
-     */
-    private static final class OpenCaseLogAction extends AbstractAction {
-
-        private static final long serialVersionUID = 1L;
-
-        private final Path pathToLog;
-
-        OpenCaseLogAction(Path caseLogFilePath) {
-            super(Bundle.MultiUserNode_OpenCaseLogAction_text());
-            pathToLog = caseLogFilePath;
-            this.setEnabled(caseLogFilePath != null && caseLogFilePath.toFile().exists());
-        }
-
-        @Override
-        public void actionPerformed(ActionEvent e) {
-
-            if (pathToLog != null) {
-                try {
-                    if (pathToLog.toFile().exists()) {
-                        Desktop.getDesktop().edit(pathToLog.toFile());
-
-                    } else {
-                        JOptionPane.showMessageDialog(MultiUserCasesDialog.getInstance(), org.openide.util.NbBundle.getMessage(MultiUserNode.class, "DisplayLogDialog.cannotFindLog"),
-                                org.openide.util.NbBundle.getMessage(MultiUserNode.class, "DisplayLogDialog.unableToShowLogFile"), JOptionPane.ERROR_MESSAGE);
-                    }
-                } catch (IOException ex) {
-                    LOGGER.log(Level.SEVERE, String.format("Error attempting to open case auto ingest log file %s", pathToLog), ex);
-                    JOptionPane.showMessageDialog(MultiUserCasesDialog.getInstance(),
-                            org.openide.util.NbBundle.getMessage(MultiUserNode.class, "DisplayLogDialog.cannotOpenLog"),
-                            org.openide.util.NbBundle.getMessage(MultiUserNode.class, "DisplayLogDialog.unableToShowLogFile"),
-                            JOptionPane.PLAIN_MESSAGE);
-                }
-            }
-        }
-
-        @Override
-        public Object clone() throws CloneNotSupportedException {
-            return super.clone(); //To change body of generated methods, choose Tools | Templates.
-        }
-    }
-
-}
diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/OpenCaseAutoIngestLogAction.java b/Core/src/org/sleuthkit/autopsy/casemodule/OpenCaseAutoIngestLogAction.java
new file mode 100755
index 0000000000000000000000000000000000000000..165af8776c44e158c001234bdd9529f0b1eb0dd2
--- /dev/null
+++ b/Core/src/org/sleuthkit/autopsy/casemodule/OpenCaseAutoIngestLogAction.java
@@ -0,0 +1,83 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2017-2019 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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.sleuthkit.autopsy.casemodule;
+
+import java.awt.Desktop;
+import java.awt.event.ActionEvent;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.logging.Level;
+import javax.swing.AbstractAction;
+import org.openide.util.NbBundle;
+import org.sleuthkit.autopsy.coordinationservice.CaseNodeData;
+import org.sleuthkit.autopsy.coreutils.Logger;
+import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil;
+
+/**
+ * An action that opens a case auto ingest log given the coordination service
+ * node data for the case.
+ */
+final class OpenCaseAutoIngestLogAction extends AbstractAction {
+
+    private static final long serialVersionUID = 1L;
+    private static final Logger logger = Logger.getLogger(OpenCaseAutoIngestLogAction.class.getName());
+    private static final String CASE_AUTO_INGEST_LOG_FILE_NAME = "auto_ingest_log.txt";
+    private final Path caseAutoIngestLogFilePath;
+
+    /**
+     * Constructs an action that opens a case auto ingest log given the
+     * coordination service node data for the case.
+     *
+     * @param caseNodeData The coordination service node data for the case.
+     */
+    @NbBundle.Messages({
+        "OpenCaseAutoIngestLogAction.menuItemText=Open Auto Ingest Log File"
+    })
+    OpenCaseAutoIngestLogAction(CaseNodeData caseNodeData) {
+        super(Bundle.OpenCaseAutoIngestLogAction_menuItemText());
+        this.caseAutoIngestLogFilePath = Paths.get(caseNodeData.getDirectory().toString(), CASE_AUTO_INGEST_LOG_FILE_NAME);
+        this.setEnabled(caseAutoIngestLogFilePath.toFile().exists());
+    }
+
+    @NbBundle.Messages({
+        "OpenCaseAutoIngestLogAction.deletedLogErrorMsg=The case auto ingest log has been deleted.",
+        "OpenCaseAutoIngestLogAction.logOpenFailedErrorMsg=Failed to open case auto ingest log. See application log for details."
+    })
+    @Override
+    public void actionPerformed(ActionEvent event) {
+        try {
+            if (caseAutoIngestLogFilePath.toFile().exists()) {
+                Desktop.getDesktop().edit(caseAutoIngestLogFilePath.toFile());
+            } else {
+                MessageNotifyUtil.Message.error(Bundle.OpenCaseAutoIngestLogAction_deletedLogErrorMsg());
+            }
+        } catch (IOException ex) {
+            logger.log(Level.SEVERE, String.format("Error opening case auto ingest log file at %s", caseAutoIngestLogFilePath), ex); //NON-NLS
+            MessageNotifyUtil.Message.error(Bundle.OpenCaseAutoIngestLogAction_logOpenFailedErrorMsg());
+        }
+    }
+
+    @Override
+    public OpenCaseAutoIngestLogAction clone() throws CloneNotSupportedException {
+        super.clone();
+        throw new CloneNotSupportedException();
+    }
+
+}
diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/OpenMultiUserCaseAction.java b/Core/src/org/sleuthkit/autopsy/casemodule/OpenMultiUserCaseAction.java
new file mode 100755
index 0000000000000000000000000000000000000000..5814d6b12b43cc3a0299e2c44d1db9af72def43f
--- /dev/null
+++ b/Core/src/org/sleuthkit/autopsy/casemodule/OpenMultiUserCaseAction.java
@@ -0,0 +1,99 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2017-2019 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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.sleuthkit.autopsy.casemodule;
+
+import java.awt.event.ActionEvent;
+import java.io.File;
+import java.util.logging.Level;
+import javax.swing.AbstractAction;
+import javax.swing.SwingUtilities;
+import org.openide.util.NbBundle;
+import org.sleuthkit.autopsy.coordinationservice.CaseNodeData;
+import org.sleuthkit.autopsy.coreutils.Logger;
+import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil;
+
+/**
+ * An action that opens a multi-user case and hides the open multi-user case
+ * dialog given the coordination service node data for the case.
+ */
+final class OpenMultiUserCaseAction extends AbstractAction {
+
+    private static final long serialVersionUID = 1L;
+    private static final Logger logger = Logger.getLogger(OpenMultiUserCaseAction.class.getName());
+    private final CaseNodeData caseNodeData;
+
+    /**
+     * Constructs an action that opens a multi-user case and hides the open
+     * multi-user case dialog given the coordination service node data for the
+     * case.
+     */
+    @NbBundle.Messages({
+        "OpenMultiUserCaseAction.menuItemText=Open Case"
+    })
+    OpenMultiUserCaseAction(CaseNodeData caseNodeData) {
+        super(Bundle.OpenMultiUserCaseAction_menuItemText());
+        this.caseNodeData = caseNodeData;
+    }
+
+    @NbBundle.Messages({
+        "# {0} - caseErrorMessage", "OpenMultiUserCaseAction.caseOpeningErrorErrorMsg=Failed to open case: {0}"
+    })
+    @Override
+    public void actionPerformed(ActionEvent event) {
+        StartupWindowProvider.getInstance().close();
+        OpenMultiUserCaseDialog.getInstance().setVisible(false);
+        new Thread(() -> {
+            String caseMetadataFilePath = null;
+            File caseDirectory = caseNodeData.getDirectory().toFile();
+            File[] filesInDirectory = caseDirectory.listFiles();
+            if (filesInDirectory != null) {
+                for (File file : filesInDirectory) {
+                    if (file.getName().toLowerCase().endsWith(CaseMetadata.getFileExtension()) && file.isFile()) {
+                        caseMetadataFilePath = file.getPath();
+                    }
+                }
+            }
+            if (caseMetadataFilePath != null) {
+                try {
+                    Case.openAsCurrentCase(caseMetadataFilePath);
+                } catch (CaseActionException ex) {
+                    if (null != ex.getCause() && !(ex.getCause() instanceof CaseActionCancelledException)) {
+                        logger.log(Level.SEVERE, String.format("Error opening case with metadata file path %s", caseMetadataFilePath), ex); //NON-NLS
+                    }
+                    SwingUtilities.invokeLater(() -> {
+                        MessageNotifyUtil.Message.error(Bundle.OpenMultiUserCaseAction_caseOpeningErrorErrorMsg(ex.getLocalizedMessage()));
+                        StartupWindowProvider.getInstance().open();
+                        OpenMultiUserCaseDialog.getInstance().setVisible(true);
+                    });
+                }
+            } else {
+                SwingUtilities.invokeLater(() -> {
+                    MessageNotifyUtil.Message.error(Bundle.OpenMultiUserCaseAction_caseOpeningErrorErrorMsg("Could not locate case metadata file."));
+                });
+            }
+        }).start();
+    }
+
+    @Override
+    public OpenMultiUserCaseAction clone() throws CloneNotSupportedException {
+        super.clone();
+        throw new CloneNotSupportedException();
+    }
+
+}
diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/OpenMultiUserCaseDialog.java b/Core/src/org/sleuthkit/autopsy/casemodule/OpenMultiUserCaseDialog.java
new file mode 100644
index 0000000000000000000000000000000000000000..c81ceabd7c6bc7580f3075ab83af53865a42c0ea
--- /dev/null
+++ b/Core/src/org/sleuthkit/autopsy/casemodule/OpenMultiUserCaseDialog.java
@@ -0,0 +1,84 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2017-2019 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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.sleuthkit.autopsy.casemodule;
+
+import java.awt.Dialog;
+import javax.swing.JDialog;
+import org.openide.util.NbBundle;
+import org.openide.windows.WindowManager;
+
+/**
+ * A singleton JDialog that allows a user to open a multi-user case.
+ */
+final class OpenMultiUserCaseDialog extends JDialog {
+
+    private static final long serialVersionUID = 1L;
+    private static OpenMultiUserCaseDialog instance;
+    private static OpenMultiUserCasePanel multiUserCasesPanel;
+
+    /**
+     * Gets the singleton JDialog that allows a user to open a multi-user case.
+     *
+     * @return The singleton JDialog instance.
+     */
+    public synchronized static OpenMultiUserCaseDialog getInstance() {
+        if (instance == null) {
+            instance = new OpenMultiUserCaseDialog();
+            instance.init();
+        }
+        return instance;
+    }
+
+    /**
+     * Constructs a singleton JDialog that allows a user to open a multi-user
+     * case.
+     */
+    @NbBundle.Messages({
+        "OpenMultiUserCaseDialog.title=Open Multi-User Case"
+    })
+    private OpenMultiUserCaseDialog() {
+        super(WindowManager.getDefault().getMainWindow(), Bundle.OpenMultiUserCaseDialog_title(), Dialog.ModalityType.APPLICATION_MODAL);
+    }
+
+    /**
+     * Registers a keyboard action to hide the dialog when the escape key is
+     * pressed and adds a OpenMultiUserCasePanel child component.
+     */
+    private void init() {
+        multiUserCasesPanel = new OpenMultiUserCasePanel(this);
+        add(multiUserCasesPanel);
+        pack();
+        setResizable(false);
+    }
+
+    /**
+     * Sets the dialog visibility. When made visible, the dialog refreshes the
+     * display of its OpenMultiUserCasePanel child component.
+     *
+     * @param makeVisible True or false.
+     */
+    @Override
+    public void setVisible(boolean makeVisible) {
+        if (makeVisible) {
+            multiUserCasesPanel.refreshDisplay();
+        }
+        super.setVisible(makeVisible);
+    }
+
+}
diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/MultiUserCasesPanel.form b/Core/src/org/sleuthkit/autopsy/casemodule/OpenMultiUserCasePanel.form
similarity index 71%
rename from Core/src/org/sleuthkit/autopsy/casemodule/MultiUserCasesPanel.form
rename to Core/src/org/sleuthkit/autopsy/casemodule/OpenMultiUserCasePanel.form
index 71810b738739972097df89b9a9022cc8835e4f98..30996651a699d90feebfa5d5258c7e774b1cf0b4 100644
--- a/Core/src/org/sleuthkit/autopsy/casemodule/MultiUserCasesPanel.form
+++ b/Core/src/org/sleuthkit/autopsy/casemodule/OpenMultiUserCasePanel.form
@@ -28,12 +28,12 @@
                   <Component id="caseExplorerScrollPane" max="32767" attributes="0"/>
                   <Group type="102" attributes="0">
                       <Component id="searchLabel" min="-2" max="-2" attributes="0"/>
-                      <EmptySpace max="32767" attributes="0"/>
-                      <Component id="bnOpenSingleUserCase" min="-2" pref="192" max="-2" attributes="0"/>
-                      <EmptySpace min="-2" pref="190" max="-2" attributes="0"/>
-                      <Component id="bnOpen" linkSize="1" min="-2" max="-2" attributes="0"/>
+                      <EmptySpace min="-2" pref="32" max="-2" attributes="0"/>
+                      <Component id="openSingleUserCaseButton" linkSize="10" min="-2" pref="172" max="-2" attributes="0"/>
+                      <EmptySpace pref="96" max="32767" attributes="0"/>
+                      <Component id="openSelectedCaseButton" linkSize="10" min="-2" pref="160" max="-2" attributes="0"/>
                       <EmptySpace max="-2" attributes="0"/>
-                      <Component id="cancelButton" linkSize="1" min="-2" max="-2" attributes="0"/>
+                      <Component id="cancelButton" linkSize="10" min="-2" max="-2" attributes="0"/>
                   </Group>
               </Group>
               <EmptySpace max="-2" attributes="0"/>
@@ -47,10 +47,10 @@
               <Component id="caseExplorerScrollPane" min="-2" pref="450" max="-2" attributes="0"/>
               <EmptySpace max="-2" attributes="0"/>
               <Group type="103" groupAlignment="3" attributes="0">
-                  <Component id="cancelButton" alignment="3" min="-2" max="-2" attributes="0"/>
-                  <Component id="bnOpen" alignment="3" min="-2" max="-2" attributes="0"/>
-                  <Component id="bnOpenSingleUserCase" alignment="3" min="-2" max="-2" attributes="0"/>
+                  <Component id="cancelButton" linkSize="7" alignment="3" min="-2" max="-2" attributes="0"/>
+                  <Component id="openSingleUserCaseButton" linkSize="7" alignment="3" min="-2" max="-2" attributes="0"/>
                   <Component id="searchLabel" alignment="3" max="32767" attributes="0"/>
+                  <Component id="openSelectedCaseButton" linkSize="7" alignment="3" min="-2" max="-2" attributes="0"/>
               </Group>
               <EmptySpace max="-2" attributes="0"/>
           </Group>
@@ -58,30 +58,10 @@
     </DimensionLayout>
   </Layout>
   <SubComponents>
-    <Component class="javax.swing.JButton" name="bnOpen">
+    <Component class="javax.swing.JButton" name="openSingleUserCaseButton">
       <Properties>
         <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
-          <ResourceString bundle="org/sleuthkit/autopsy/casemodule/Bundle.properties" key="MultiUserCasesPanel.bnOpen.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
-        </Property>
-        <Property name="enabled" type="boolean" value="false"/>
-        <Property name="maximumSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor">
-          <Dimension value="[80, 23]"/>
-        </Property>
-        <Property name="minimumSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor">
-          <Dimension value="[80, 23]"/>
-        </Property>
-        <Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor">
-          <Dimension value="[80, 23]"/>
-        </Property>
-      </Properties>
-      <Events>
-        <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="bnOpenActionPerformed"/>
-      </Events>
-    </Component>
-    <Component class="javax.swing.JButton" name="bnOpenSingleUserCase">
-      <Properties>
-        <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
-          <ResourceString bundle="org/sleuthkit/autopsy/casemodule/Bundle.properties" key="MultiUserCasesPanel.bnOpenSingleUserCase.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+          <ResourceString bundle="org/sleuthkit/autopsy/casemodule/Bundle.properties" key="OpenMultiUserCasePanel.openSingleUserCaseButton.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
         </Property>
         <Property name="minimumSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor">
           <Dimension value="[156, 23]"/>
@@ -91,13 +71,13 @@
         </Property>
       </Properties>
       <Events>
-        <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="bnOpenSingleUserCaseActionPerformed"/>
+        <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="openSingleUserCaseButtonActionPerformed"/>
       </Events>
     </Component>
     <Component class="javax.swing.JButton" name="cancelButton">
       <Properties>
         <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
-          <ResourceString bundle="org/sleuthkit/autopsy/casemodule/Bundle.properties" key="MultiUserCasesPanel.cancelButton.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+          <ResourceString bundle="org/sleuthkit/autopsy/casemodule/Bundle.properties" key="OpenMultiUserCasePanel.cancelButton.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
         </Property>
         <Property name="maximumSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor">
           <Dimension value="[80, 23]"/>
@@ -116,7 +96,7 @@
     <Component class="javax.swing.JLabel" name="searchLabel">
       <Properties>
         <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
-          <ResourceString bundle="org/sleuthkit/autopsy/casemodule/Bundle.properties" key="MultiUserCasesPanel.searchLabel.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+          <ResourceString bundle="org/sleuthkit/autopsy/casemodule/Bundle.properties" key="OpenMultiUserCasePanel.searchLabel.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
         </Property>
       </Properties>
     </Component>
@@ -124,5 +104,15 @@
 
       <Layout class="org.netbeans.modules.form.compat2.layouts.support.JScrollPaneSupportLayout"/>
     </Container>
+    <Component class="javax.swing.JButton" name="openSelectedCaseButton">
+      <Properties>
+        <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+          <ResourceString bundle="org/sleuthkit/autopsy/casemodule/Bundle.properties" key="OpenMultiUserCasePanel.openSelectedCaseButton.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+        </Property>
+      </Properties>
+      <Events>
+        <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="openSelectedCaseButtonActionPerformed"/>
+      </Events>
+    </Component>
   </SubComponents>
 </Form>
diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/OpenMultiUserCasePanel.java b/Core/src/org/sleuthkit/autopsy/casemodule/OpenMultiUserCasePanel.java
new file mode 100644
index 0000000000000000000000000000000000000000..5159cb8163b0f44f2691f62886ff63692b80af20
--- /dev/null
+++ b/Core/src/org/sleuthkit/autopsy/casemodule/OpenMultiUserCasePanel.java
@@ -0,0 +1,194 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2017-2019 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * 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.sleuthkit.autopsy.casemodule;
+
+import javax.swing.JDialog;
+import javax.swing.JPanel;
+import javax.swing.event.ListSelectionEvent;
+import org.openide.explorer.ExplorerManager;
+import org.openide.nodes.Node;
+import org.openide.util.Lookup;
+import org.sleuthkit.autopsy.coordinationservice.CaseNodeData;
+
+/**
+ * A JPanel that allows a user to open a multi-user case.
+ */
+@SuppressWarnings("PMD.SingularField")  // Matisse-generated UI widgets cause lots of false positives
+final class OpenMultiUserCasePanel extends JPanel {
+
+    private static final long serialVersionUID = 1L;
+    private final JDialog parentDialog;
+    private final MultiUserCasesBrowserPanel caseBrowserPanel;
+
+    /**
+     * Constructs a JPanel that allows a user to open a multi-user case.
+     *
+     * @param parentDialog The parent dialog of the panel, may be null. If
+     *                     provided, the dialog is hidden when this poanel's
+     *                     cancel button is pressed.
+     */
+    OpenMultiUserCasePanel(JDialog parentDialog) {
+        this.parentDialog = parentDialog;
+        initComponents(); // Machine generated code 
+        caseBrowserPanel = new MultiUserCasesBrowserPanel();
+        caseExplorerScrollPane.add(caseBrowserPanel);
+        caseExplorerScrollPane.setViewportView(caseBrowserPanel);
+        openSelectedCaseButton.setEnabled(false);
+        caseBrowserPanel.addListSelectionListener((ListSelectionEvent event) -> {
+            openSelectedCaseButton.setEnabled(caseBrowserPanel.getExplorerManager().getSelectedNodes().length > 0);
+        });
+    }
+
+    /**
+     * Refreshes the child component that displays the multi-user cases known to
+     * the coordination service..
+     */
+    void refreshDisplay() {
+        caseBrowserPanel.refreshCases();
+    }
+
+    /**
+     * This method is called from within the constructor to initialize the form.
+     * WARNING: Do NOT modify this code. The content of this method is always
+     * regenerated by the Form Editor.
+     */
+    @SuppressWarnings("unchecked")
+    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
+    private void initComponents() {
+
+        openSingleUserCaseButton = new javax.swing.JButton();
+        cancelButton = new javax.swing.JButton();
+        searchLabel = new javax.swing.JLabel();
+        caseExplorerScrollPane = new javax.swing.JScrollPane();
+        openSelectedCaseButton = new javax.swing.JButton();
+
+        setName("Completed Cases"); // NOI18N
+        setPreferredSize(new java.awt.Dimension(960, 485));
+
+        org.openide.awt.Mnemonics.setLocalizedText(openSingleUserCaseButton, org.openide.util.NbBundle.getMessage(OpenMultiUserCasePanel.class, "OpenMultiUserCasePanel.openSingleUserCaseButton.text")); // NOI18N
+        openSingleUserCaseButton.setMinimumSize(new java.awt.Dimension(156, 23));
+        openSingleUserCaseButton.setPreferredSize(new java.awt.Dimension(156, 23));
+        openSingleUserCaseButton.addActionListener(new java.awt.event.ActionListener() {
+            public void actionPerformed(java.awt.event.ActionEvent evt) {
+                openSingleUserCaseButtonActionPerformed(evt);
+            }
+        });
+
+        org.openide.awt.Mnemonics.setLocalizedText(cancelButton, org.openide.util.NbBundle.getMessage(OpenMultiUserCasePanel.class, "OpenMultiUserCasePanel.cancelButton.text")); // NOI18N
+        cancelButton.setMaximumSize(new java.awt.Dimension(80, 23));
+        cancelButton.setMinimumSize(new java.awt.Dimension(80, 23));
+        cancelButton.setPreferredSize(new java.awt.Dimension(80, 23));
+        cancelButton.addActionListener(new java.awt.event.ActionListener() {
+            public void actionPerformed(java.awt.event.ActionEvent evt) {
+                cancelButtonActionPerformed(evt);
+            }
+        });
+
+        org.openide.awt.Mnemonics.setLocalizedText(searchLabel, org.openide.util.NbBundle.getMessage(OpenMultiUserCasePanel.class, "OpenMultiUserCasePanel.searchLabel.text")); // NOI18N
+
+        org.openide.awt.Mnemonics.setLocalizedText(openSelectedCaseButton, org.openide.util.NbBundle.getMessage(OpenMultiUserCasePanel.class, "OpenMultiUserCasePanel.openSelectedCaseButton.text")); // NOI18N
+        openSelectedCaseButton.addActionListener(new java.awt.event.ActionListener() {
+            public void actionPerformed(java.awt.event.ActionEvent evt) {
+                openSelectedCaseButtonActionPerformed(evt);
+            }
+        });
+
+        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
+        this.setLayout(layout);
+        layout.setHorizontalGroup(
+            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+            .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup()
+                .addContainerGap()
+                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING)
+                    .addComponent(caseExplorerScrollPane)
+                    .addGroup(layout.createSequentialGroup()
+                        .addComponent(searchLabel)
+                        .addGap(32, 32, 32)
+                        .addComponent(openSingleUserCaseButton, javax.swing.GroupLayout.PREFERRED_SIZE, 172, javax.swing.GroupLayout.PREFERRED_SIZE)
+                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, 96, Short.MAX_VALUE)
+                        .addComponent(openSelectedCaseButton, javax.swing.GroupLayout.PREFERRED_SIZE, 160, javax.swing.GroupLayout.PREFERRED_SIZE)
+                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
+                        .addComponent(cancelButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)))
+                .addContainerGap())
+        );
+
+        layout.linkSize(javax.swing.SwingConstants.HORIZONTAL, new java.awt.Component[] {cancelButton, openSelectedCaseButton, openSingleUserCaseButton});
+
+        layout.setVerticalGroup(
+            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+            .addGroup(layout.createSequentialGroup()
+                .addGap(6, 6, 6)
+                .addComponent(caseExplorerScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, 450, javax.swing.GroupLayout.PREFERRED_SIZE)
+                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
+                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
+                    .addComponent(cancelButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
+                    .addComponent(openSingleUserCaseButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
+                    .addComponent(searchLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
+                    .addComponent(openSelectedCaseButton))
+                .addContainerGap())
+        );
+
+        layout.linkSize(javax.swing.SwingConstants.VERTICAL, new java.awt.Component[] {cancelButton, openSelectedCaseButton, openSingleUserCaseButton});
+
+    }// </editor-fold>//GEN-END:initComponents
+
+    /**
+     * Opens the standard open single user case window.
+     *
+     * @param evt An ActionEvent, unused.
+     */
+    private void openSingleUserCaseButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_openSingleUserCaseButtonActionPerformed
+        Lookup.getDefault().lookup(CaseOpenAction.class).openCaseSelectionWindow();
+    }//GEN-LAST:event_openSingleUserCaseButtonActionPerformed
+
+    /**
+     * Closes the parent open multi-user case dialog.
+     *
+     * @param evt An ActionEvent, unused.
+     */
+    private void cancelButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_cancelButtonActionPerformed
+        if (parentDialog != null) {
+            parentDialog.setVisible(false);
+        }
+    }//GEN-LAST:event_cancelButtonActionPerformed
+
+    /**
+     * Opens the multi-user case selected in the child multi-user case browser
+     * panel.
+     *
+     * @param evt An ActionEvent, unused.
+     */
+    private void openSelectedCaseButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_openSelectedCaseButtonActionPerformed
+        ExplorerManager explorerManager = caseBrowserPanel.getExplorerManager();
+        Node[] selectedNodes = explorerManager.getSelectedNodes();
+        if (selectedNodes.length > 0 && selectedNodes[0] instanceof MultiUserCaseNode) {
+            MultiUserCaseNode caseNode = (MultiUserCaseNode) selectedNodes[0];
+            CaseNodeData nodeData = caseNode.getCaseNodeData();
+            new OpenMultiUserCaseAction(nodeData).actionPerformed(evt);
+        }
+    }//GEN-LAST:event_openSelectedCaseButtonActionPerformed
+
+    // Variables declaration - do not modify//GEN-BEGIN:variables
+    private javax.swing.JButton cancelButton;
+    private javax.swing.JScrollPane caseExplorerScrollPane;
+    private javax.swing.JButton openSelectedCaseButton;
+    private javax.swing.JButton openSingleUserCaseButton;
+    private javax.swing.JLabel searchLabel;
+    // End of variables declaration//GEN-END:variables
+}
diff --git a/Core/src/org/sleuthkit/autopsy/coordinationservice/CaseNodeData.java b/Core/src/org/sleuthkit/autopsy/coordinationservice/CaseNodeData.java
index 0b220e04b27e62ce40c3582c3c755f963857c526..2fd1b4cf04d7a2482683e6065c991a3464b0dea8 100644
--- a/Core/src/org/sleuthkit/autopsy/coordinationservice/CaseNodeData.java
+++ b/Core/src/org/sleuthkit/autopsy/coordinationservice/CaseNodeData.java
@@ -1,7 +1,7 @@
 /*
  * Autopsy Forensic Browser
  *
- * Copyright 2011-2017 Basis Technology Corp.
+ * Copyright 2017-2019 Basis Technology Corp.
  * Contact: carrier <at> sleuthkit <dot> org
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
@@ -18,69 +18,115 @@
  */
 package org.sleuthkit.autopsy.coordinationservice;
 
-import java.nio.BufferUnderflowException;
-import java.nio.ByteBuffer;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.text.ParseException;
+import java.util.Date;
+import org.sleuthkit.autopsy.casemodule.CaseMetadata;
 
 /**
- * An object that converts case data for a case directory coordination service
+ * An object that converts data for a case directory lock coordination service
  * node to and from byte arrays.
  */
 public final class CaseNodeData {
 
-    private static final int CURRENT_VERSION = 0;
-    
-    private int version;
+    private static final int CURRENT_VERSION = 1;
+
+    /*
+     * Version 0 fields.
+     */
+    private final int version;
     private boolean errorsOccurred;
 
+    /*
+     * Version 1 fields.
+     */
+    private Path directory;
+    private Date createDate;
+    private Date lastAccessDate;
+    private String name;
+    private String displayName;
+    private short deletedItemFlags;
+
     /**
-     * Gets the current version of the case directory coordination service node
-     * data.
+     * Gets the current version of the case directory lock coordination service
+     * node data.
      *
      * @return The version number.
      */
     public static int getCurrentVersion() {
         return CaseNodeData.CURRENT_VERSION;
     }
-    
+
+    /**
+     * Uses a CaseMetadata object to construct an object that converts data for
+     * a case directory lock coordination service node to and from byte arrays.
+     *
+     * @param metadata The case meta data.
+     *
+     * @throws java.text.ParseException If there is an error parsing dates from
+     *                                  string representations of dates in the
+     *                                  meta data.
+     */
+    public CaseNodeData(CaseMetadata metadata) throws ParseException {
+        this.version = CURRENT_VERSION;
+        this.errorsOccurred = false;
+        this.directory = Paths.get(metadata.getCaseDirectory());
+        this.createDate = CaseMetadata.getDateFormat().parse(metadata.getCreatedDate());
+        this.lastAccessDate = new Date();
+        this.name = metadata.getCaseName();
+        this.displayName = metadata.getCaseDisplayName();
+        this.deletedItemFlags = 0;
+    }
+
     /**
      * Uses coordination service node data to construct an object that converts
-     * case data for a case directory coordination service node to and from byte
+     * data for a case directory lock coordination service node to and from byte
      * arrays.
      *
      * @param nodeData The raw bytes received from the coordination service.
-     * 
-     * @throws InvalidDataException If the node data buffer is smaller than
-     *                              expected.
-     */
-    public CaseNodeData(byte[] nodeData) throws InvalidDataException {
-        if(nodeData == null || nodeData.length == 0) {
-            this.version = CURRENT_VERSION;
-            this.errorsOccurred = false;
+     *
+     * @throws IOException If there is an error reading the node data.
+     */
+    public CaseNodeData(byte[] nodeData) throws IOException {
+        if (nodeData == null || nodeData.length == 0) {
+            throw new IOException(null == nodeData ? "Null node data byte array" : "Zero-length node data byte array");
+        }
+        DataInputStream inputStream = new DataInputStream(new ByteArrayInputStream(nodeData));
+        this.version = inputStream.readInt();
+        if (this.version > 0) {
+            this.errorsOccurred = inputStream.readBoolean();
         } else {
-            /*
-             * Get fields from node data.
-             */
-            ByteBuffer buffer = ByteBuffer.wrap(nodeData);
-            try {
-                if (buffer.hasRemaining()) {
-                    this.version = buffer.getInt();
-
-                    /*
-                     * Flags bit format: 76543210
-                     * 0-6 --> reserved for future use
-                     * 7 --> errorsOccurred
-                     */
-                    byte flags = buffer.get();
-                    this.errorsOccurred = (flags < 0);
-                }
-            } catch (BufferUnderflowException ex) {
-                throw new InvalidDataException("Node data is incomplete", ex);
-            }
+            short legacyErrorsOccurred = inputStream.readByte();
+            this.errorsOccurred = (legacyErrorsOccurred < 0);
         }
+        if (this.version > 0) {
+            this.directory = Paths.get(inputStream.readUTF());
+            this.createDate = new Date(inputStream.readLong());
+            this.lastAccessDate = new Date(inputStream.readLong());
+            this.name = inputStream.readUTF();
+            this.displayName = inputStream.readUTF();
+            this.deletedItemFlags = inputStream.readShort();
+        }
+    }
+
+    /**
+     * Gets the node data version number of this node.
+     *
+     * @return The version number.
+     */
+    public int getVersion() {
+        return this.version;
     }
 
     /**
-     * Gets whether or not any errors occurred during the processing of the job.
+     * Gets whether or not any errors occurred during the processing of any auto
+     * ingest job for the case represented by this node data.
      *
      * @return True or false.
      */
@@ -89,7 +135,8 @@ public boolean getErrorsOccurred() {
     }
 
     /**
-     * Sets whether or not any errors occurred during the processing of job.
+     * Sets whether or not any errors occurred during the processing of any auto
+     * ingest job for the case represented by this node data.
      *
      * @param errorsOccurred True or false.
      */
@@ -98,32 +145,121 @@ public void setErrorsOccurred(boolean errorsOccurred) {
     }
 
     /**
-     * Gets the node data version number.
+     * Gets the path of the case directory of the case represented by this node
+     * data.
      *
-     * @return The version number.
+     * @return The case directory path.
      */
-    public int getVersion() {
-        return this.version;
+    public Path getDirectory() {
+        return this.directory;
+    }
+
+    /**
+     * Sets the path of the case directory of the case represented by this node
+     * data.
+     *
+     * @param caseDirectory The case directory path.
+     */
+    public void setDirectory(Path caseDirectory) {
+        this.directory = caseDirectory;
+    }
+
+    /**
+     * Gets the date the case represented by this node data was created.
+     *
+     * @return The create date.
+     */
+    public Date getCreateDate() {
+        return new Date(this.createDate.getTime());
+    }
+
+    /**
+     * Sets the date the case represented by this node data was created.
+     *
+     * @param createDate The create date.
+     */
+    public void setCreateDate(Date createDate) {
+        this.createDate = new Date(createDate.getTime());
+    }
+
+    /**
+     * Gets the date the case represented by this node data last accessed.
+     *
+     * @return The last access date.
+     */
+    public Date getLastAccessDate() {
+        return new Date(this.lastAccessDate.getTime());
+    }
+
+    /**
+     * Sets the date the case represented by this node data was last accessed.
+     *
+     * @param lastAccessDate The last access date.
+     */
+    public void setLastAccessDate(Date lastAccessDate) {
+        this.lastAccessDate = new Date(lastAccessDate.getTime());
+    }
+
+    /**
+     * Gets the unique and immutable (user cannot change it) name of the case
+     * represented by this node data.
+     *
+     * @return The case name.
+     */
+    public String getName() {
+        return this.name;
+    }
+
+    /**
+     * Sets the unique and immutable (user cannot change it) name of the case
+     * represented by this node data.
+     *
+     * @param name The case name.
+     */
+    public void setName(String name) {
+        this.name = name;
     }
-    
+
+    /**
+     * Gets the display name of the case represented by this node data.
+     *
+     * @return The case display name.
+     */
+    public String getDisplayName() {
+        return this.displayName;
+    }
+
+    /**
+     * Sets the display name of the case represented by this node data.
+     *
+     * @param displayName The case display name.
+     */
+    public void setDisplayName(String displayName) {
+        this.displayName = displayName;
+    }
+
     /**
      * Gets the node data as a byte array that can be sent to the coordination
      * service.
      *
      * @return The node data as a byte array.
+     *
+     * @throws IOException If there is an error writing the node data.
      */
-    public byte[] toArray() {
-        ByteBuffer buffer = ByteBuffer.allocate(5);
-        
-        buffer.putInt(this.version);
-        buffer.put((byte)(this.errorsOccurred ? 0x80 : 0));
-        
-        // Prepare the array
-        byte[] array = new byte[buffer.position()];
-        buffer.rewind();
-        buffer.get(array, 0, array.length);
-
-        return array;
+    public byte[] toArray() throws IOException {
+        ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
+        DataOutputStream outputStream = new DataOutputStream(byteStream);
+        outputStream.writeInt(this.version);
+        outputStream.writeBoolean(this.errorsOccurred);
+        outputStream.writeUTF(this.directory.toString());
+        outputStream.writeLong(this.createDate.getTime());
+        outputStream.writeLong(this.lastAccessDate.getTime());
+        outputStream.writeUTF(this.name);
+        outputStream.writeUTF(this.displayName);
+        outputStream.writeShort(this.deletedItemFlags);
+        outputStream.flush();
+        byteStream.flush();
+        return byteStream.toByteArray();
     }
 
     public final static class InvalidDataException extends Exception {
diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerThumbnail.java b/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerThumbnail.java
index ea4f4d3136d922a6532a5b2b03e056fcb5c48698..f03c7b01cff5278930085b9961a298409e7865bc 100644
--- a/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerThumbnail.java
+++ b/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerThumbnail.java
@@ -380,7 +380,7 @@ public boolean isSupported(Node selectedNode) {
     public void setNode(Node givenNode) {
         setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
         if (selectionListener == null) {
-            this.getExplorerManager().addPropertyChangeListener(new NodeSelectionListener()); // RJCTODO: remove listener on cleanup
+            this.getExplorerManager().addPropertyChangeListener(new NodeSelectionListener());
         }
         if (rootNodeChildren != null) {
             rootNodeChildren.cancelLoadingThumbnails();
diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestManager.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestManager.java
index 1a1a384745536f405689f3641f5e0026a7b40772..39b229f85643d8a468d06bb593d888fa179275c1 100644
--- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestManager.java
+++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestManager.java
@@ -1135,9 +1135,9 @@ void updateCoordinationServiceManifestNode(AutoIngestJob job) throws Coordinatio
      *
      * @throws CoordinationService.CoordinationServiceException
      * @throws InterruptedException
-     * @throws CaseNodeData.InvalidDataException
+     * @throws IOException
      */
-    private void setCaseNodeDataErrorsOccurred(Path caseDirectoryPath) throws CoordinationServiceException, InterruptedException, CaseNodeData.InvalidDataException {
+    private void setCaseNodeDataErrorsOccurred(Path caseDirectoryPath) throws CoordinationServiceException, InterruptedException, IOException {
         CaseNodeData caseNodeData = new CaseNodeData(coordinationService.getNodeData(CoordinationService.CategoryNode.CASES, caseDirectoryPath.toString()));
         caseNodeData.setErrorsOccurred(true);
         byte[] rawData = caseNodeData.toArray();
@@ -1517,8 +1517,8 @@ private void doRecoveryIfCrashed(Manifest manifest, AutoIngestJobNodeData jobNod
                         job.setErrorsOccurred(true);
                         try {
                             setCaseNodeDataErrorsOccurred(caseDirectoryPath);
-                        } catch (CaseNodeData.InvalidDataException ex) {
-                            sysLogger.log(Level.SEVERE, String.format("Error attempting to get case node data for %s", caseDirectoryPath), ex);
+                        } catch (IOException ex) {
+                            sysLogger.log(Level.SEVERE, String.format("Error attempting to set error flag in case node data for %s", caseDirectoryPath), ex);
                         }
                     } else {
                         job.setErrorsOccurred(false);
@@ -2012,7 +2012,7 @@ private void waitForInputDirScan() throws InterruptedException {
          *                                                    for an auto ingest
          *                                                    job.
          */
-        private void processJobs() throws CoordinationServiceException, SharedConfigurationException, ServicesMonitorException, DatabaseServerDownException, KeywordSearchServerDownException, CaseManagementException, AnalysisStartupException, FileExportException, AutoIngestJobLoggerException, InterruptedException, AutoIngestDataSourceProcessor.AutoIngestDataSourceProcessorException, AutoIngestJobNodeData.InvalidDataException, CaseNodeData.InvalidDataException, JobMetricsCollectionException {
+        private void processJobs() throws CoordinationServiceException, SharedConfigurationException, ServicesMonitorException, DatabaseServerDownException, KeywordSearchServerDownException, CaseManagementException, AnalysisStartupException, FileExportException, AutoIngestJobLoggerException, InterruptedException, AutoIngestDataSourceProcessor.AutoIngestDataSourceProcessorException, AutoIngestJobNodeData.InvalidDataException, IOException, JobMetricsCollectionException {
             sysLogger.log(Level.INFO, "Started processing pending jobs queue");
             Lock manifestLock = JobProcessingTask.this.dequeueAndLockNextJob();
             while (null != manifestLock) {
@@ -2213,7 +2213,7 @@ private Lock dequeueAndLockNextJob(boolean enforceMaxJobsPerCase) throws Coordin
          *                                                    for an auto ingest
          *                                                    job.
          */
-        private void processJob() throws CoordinationServiceException, SharedConfigurationException, ServicesMonitorException, DatabaseServerDownException, KeywordSearchServerDownException, CaseManagementException, AnalysisStartupException, FileExportException, AutoIngestJobLoggerException, InterruptedException, AutoIngestDataSourceProcessor.AutoIngestDataSourceProcessorException, CaseNodeData.InvalidDataException, JobMetricsCollectionException {
+        private void processJob() throws CoordinationServiceException, SharedConfigurationException, ServicesMonitorException, DatabaseServerDownException, KeywordSearchServerDownException, CaseManagementException, AnalysisStartupException, FileExportException, AutoIngestJobLoggerException, InterruptedException, AutoIngestDataSourceProcessor.AutoIngestDataSourceProcessorException, IOException, JobMetricsCollectionException {
             Path manifestPath = currentJob.getManifest().getFilePath();
             sysLogger.log(Level.INFO, "Started processing of {0}", manifestPath);
             currentJob.setProcessingStatus(AutoIngestJob.ProcessingStatus.PROCESSING);
@@ -2301,7 +2301,7 @@ private void processJob() throws CoordinationServiceException, SharedConfigurati
          *                                          to collect metrics for an
          *                                          auto ingest job.
          */
-        private void attemptJob() throws CoordinationServiceException, SharedConfigurationException, ServicesMonitorException, DatabaseServerDownException, KeywordSearchServerDownException, CaseManagementException, AnalysisStartupException, FileExportException, AutoIngestJobLoggerException, InterruptedException, AutoIngestDataSourceProcessor.AutoIngestDataSourceProcessorException, CaseNodeData.InvalidDataException, JobMetricsCollectionException {
+        private void attemptJob() throws CoordinationServiceException, SharedConfigurationException, ServicesMonitorException, DatabaseServerDownException, KeywordSearchServerDownException, CaseManagementException, AnalysisStartupException, FileExportException, AutoIngestJobLoggerException, InterruptedException, AutoIngestDataSourceProcessor.AutoIngestDataSourceProcessorException, IOException, JobMetricsCollectionException {
             updateConfiguration();
             if (currentJob.isCanceled() || jobProcessingTaskFuture.isCancelled()) {
                 return;
@@ -2481,7 +2481,7 @@ private Case openCase() throws CoordinationServiceException, CaseManagementExcep
          *                                       collect metrics for an auto
          *                                       ingest job.
          */
-        private void runIngestForJob(Case caseForJob) throws CoordinationServiceException, AnalysisStartupException, FileExportException, AutoIngestJobLoggerException, InterruptedException, AutoIngestDataSourceProcessor.AutoIngestDataSourceProcessorException, CaseNodeData.InvalidDataException, JobMetricsCollectionException {
+        private void runIngestForJob(Case caseForJob) throws CoordinationServiceException, AnalysisStartupException, FileExportException, AutoIngestJobLoggerException, InterruptedException, AutoIngestDataSourceProcessor.AutoIngestDataSourceProcessorException, IOException, JobMetricsCollectionException {
             try {
                 if (currentJob.isCanceled() || jobProcessingTaskFuture.isCancelled()) {
                     return;
@@ -2520,7 +2520,7 @@ private void runIngestForJob(Case caseForJob) throws CoordinationServiceExceptio
          *                                       collect metrics for an auto
          *                                       ingest job.
          */
-        private void ingestDataSource(Case caseForJob) throws AnalysisStartupException, FileExportException, AutoIngestJobLoggerException, InterruptedException, AutoIngestDataSourceProcessor.AutoIngestDataSourceProcessorException, CaseNodeData.InvalidDataException, CoordinationServiceException, JobMetricsCollectionException {
+        private void ingestDataSource(Case caseForJob) throws AnalysisStartupException, FileExportException, AutoIngestJobLoggerException, InterruptedException, AutoIngestDataSourceProcessor.AutoIngestDataSourceProcessorException, IOException, CoordinationServiceException, JobMetricsCollectionException {
             if (currentJob.isCanceled() || jobProcessingTaskFuture.isCancelled()) {
                 return;
             }
@@ -2577,7 +2577,7 @@ private void ingestDataSource(Case caseForJob) throws AnalysisStartupException,
          *                                      interrupted while blocked, i.e.,
          *                                      if auto ingest is shutting down.
          */
-        private AutoIngestDataSource identifyDataSource() throws AutoIngestJobLoggerException, InterruptedException, CaseNodeData.InvalidDataException, CoordinationServiceException {
+        private AutoIngestDataSource identifyDataSource() throws AutoIngestJobLoggerException, InterruptedException, IOException, CoordinationServiceException {
             Manifest manifest = currentJob.getManifest();
             Path manifestPath = manifest.getFilePath();
             sysLogger.log(Level.INFO, "Identifying data source for {0} ", manifestPath);
@@ -2611,7 +2611,7 @@ private AutoIngestDataSource identifyDataSource() throws AutoIngestJobLoggerExce
          *                                      while blocked, i.e., if auto
          *                                      ingest is shutting down.
          */
-        private void runDataSourceProcessor(Case caseForJob, AutoIngestDataSource dataSource) throws InterruptedException, AutoIngestJobLoggerException, AutoIngestDataSourceProcessor.AutoIngestDataSourceProcessorException, CaseNodeData.InvalidDataException, CoordinationServiceException {
+        private void runDataSourceProcessor(Case caseForJob, AutoIngestDataSource dataSource) throws InterruptedException, AutoIngestJobLoggerException, AutoIngestDataSourceProcessor.AutoIngestDataSourceProcessorException, IOException, CoordinationServiceException {
             Manifest manifest = currentJob.getManifest();
             Path manifestPath = manifest.getFilePath();
             sysLogger.log(Level.INFO, "Adding data source for {0} ", manifestPath);
@@ -2693,7 +2693,7 @@ private void runDataSourceProcessor(Case caseForJob, AutoIngestDataSource dataSo
          *                                      while blocked, i.e., if auto
          *                                      ingest is shutting down.
          */
-        private void logDataSourceProcessorResult(AutoIngestDataSource dataSource) throws AutoIngestJobLoggerException, InterruptedException, CaseNodeData.InvalidDataException, CoordinationServiceException {
+        private void logDataSourceProcessorResult(AutoIngestDataSource dataSource) throws AutoIngestJobLoggerException, InterruptedException, IOException, CoordinationServiceException {
             Manifest manifest = currentJob.getManifest();
             Path manifestPath = manifest.getFilePath();
             Path caseDirectoryPath = currentJob.getCaseDirectoryPath();
@@ -2755,7 +2755,7 @@ private void logDataSourceProcessorResult(AutoIngestDataSource dataSource) throw
          *                                      while blocked, i.e., if auto
          *                                      ingest is shutting down.
          */
-        private void analyze(AutoIngestDataSource dataSource) throws AnalysisStartupException, AutoIngestJobLoggerException, InterruptedException, CaseNodeData.InvalidDataException, CoordinationServiceException {
+        private void analyze(AutoIngestDataSource dataSource) throws AnalysisStartupException, AutoIngestJobLoggerException, InterruptedException, IOException, CoordinationServiceException {
             Manifest manifest = currentJob.getManifest();
             Path manifestPath = manifest.getFilePath();
             sysLogger.log(Level.INFO, "Starting ingest modules analysis for {0} ", manifestPath);
@@ -2893,7 +2893,7 @@ private void collectMetrics(SleuthkitCase caseDb, AutoIngestDataSource dataSourc
          *                                      while blocked, i.e., if auto
          *                                      ingest is shutting down.
          */
-        private void exportFiles(AutoIngestDataSource dataSource) throws FileExportException, AutoIngestJobLoggerException, InterruptedException, CaseNodeData.InvalidDataException, CoordinationServiceException {
+        private void exportFiles(AutoIngestDataSource dataSource) throws FileExportException, AutoIngestJobLoggerException, InterruptedException, IOException, CoordinationServiceException {
             Manifest manifest = currentJob.getManifest();
             Path manifestPath = manifest.getFilePath();
             sysLogger.log(Level.INFO, "Exporting files for {0}", manifestPath);