CASSANDRASC-129: Remove tableId from list snapshot response (#120)

Patch by Francisco Guerrero; Reviewed by Yifan Cai for CASSANDRASC-129
diff --git a/client-common/src/main/java/org/apache/cassandra/sidecar/common/response/ListSnapshotFilesResponse.java b/client-common/src/main/java/org/apache/cassandra/sidecar/common/response/ListSnapshotFilesResponse.java
index b418a08..3700d93 100644
--- a/client-common/src/main/java/org/apache/cassandra/sidecar/common/response/ListSnapshotFilesResponse.java
+++ b/client-common/src/main/java/org/apache/cassandra/sidecar/common/response/ListSnapshotFilesResponse.java
@@ -64,7 +64,6 @@
         public final String snapshotName;
         public final String keySpaceName;
         public final String tableName;
-        public final String tableId;
         public final String fileName;
         private String componentDownloadUrl;
 
@@ -75,7 +74,6 @@
                         @JsonProperty("snapshotName") String snapshotName,
                         @JsonProperty("keySpaceName") String keySpaceName,
                         @JsonProperty("tableName") String tableName,
-                        @JsonProperty("tableId") String tableId,
                         @JsonProperty("fileName") String fileName)
         {
             this.size = size;
@@ -85,7 +83,6 @@
             this.snapshotName = snapshotName;
             this.keySpaceName = keySpaceName;
             this.tableName = tableName;
-            this.tableId = tableId;
             this.fileName = fileName;
         }
 
@@ -93,10 +90,6 @@
         {
             if (componentDownloadUrl == null)
             {
-                String tableName = this.tableId != null
-                                   ? this.tableName + "-" + this.tableId
-                                   : this.tableName;
-
                 componentDownloadUrl = ApiEndpointsV1.COMPONENTS_ROUTE
                                        .replaceAll(ApiEndpointsV1.KEYSPACE_PATH_PARAM, keySpaceName)
                                        .replaceAll(ApiEndpointsV1.TABLE_PATH_PARAM, tableName)
diff --git a/client/src/testFixtures/java/org/apache/cassandra/sidecar/client/SidecarClientTest.java b/client/src/testFixtures/java/org/apache/cassandra/sidecar/client/SidecarClientTest.java
index 84a5646..69459fe 100644
--- a/client/src/testFixtures/java/org/apache/cassandra/sidecar/client/SidecarClientTest.java
+++ b/client/src/testFixtures/java/org/apache/cassandra/sidecar/client/SidecarClientTest.java
@@ -498,6 +498,50 @@
                                                 + "?includeSecondaryIndexFiles=true");
     }
 
+    /**
+     * CASSANDRASC-94 introduced a new field ({@code tableId}) to the payload when listing snapshots. We
+     * need to make sure the client is able to handle the payload with the additional field (and ignore it).
+     *
+     * @throws Exception when the test fails
+     */
+    @Test
+    public void testListSnapshotFilesPayloadWithTableId() throws Exception
+    {
+        String responseAsString = "{\"snapshotFilesInfo\":[{\"size\":15,\"host\":\"localhost1\",\"port\":2020," +
+                                  "\"dataDirIndex\":1,\"snapshotName\":\"2023.04.11\",\"keySpaceName\":\"cycling\"," +
+                                  "\"tableName\":\"cyclist_name\",\"tableId\":\"1234\",\"fileName\":" +
+                                  "\"nb-203-big-TOC.txt\"}]}";
+        MockResponse response = new MockResponse().setResponseCode(OK.code()).setBody(responseAsString);
+        SidecarInstanceImpl sidecarInstance = instances.get(2);
+        MockWebServer mockWebServer = servers.get(2);
+        mockWebServer.enqueue(response);
+
+        ListSnapshotFilesResponse result = client.listSnapshotFiles(sidecarInstance,
+                                                                    "cycling",
+                                                                    "cyclist_name",
+                                                                    "2023.04.11")
+                                                 .get(30, TimeUnit.SECONDS);
+        assertThat(result).isNotNull();
+        assertThat(result.snapshotFilesInfo()).hasSize(1);
+        ListSnapshotFilesResponse.FileInfo fileInfo = result.snapshotFilesInfo().get(0);
+        assertThat(fileInfo.size).isEqualTo(15);
+        assertThat(fileInfo.host).isEqualTo("localhost1");
+        assertThat(fileInfo.port).isEqualTo(2020);
+        assertThat(fileInfo.dataDirIndex).isEqualTo(1);
+        assertThat(fileInfo.snapshotName).isEqualTo("2023.04.11");
+        assertThat(fileInfo.keySpaceName).isEqualTo("cycling");
+        assertThat(fileInfo.tableName).isEqualTo("cyclist_name");
+        assertThat(fileInfo.fileName).isEqualTo("nb-203-big-TOC.txt");
+
+        assertThat(mockWebServer.getRequestCount()).isEqualTo(1);
+        RecordedRequest request = mockWebServer.takeRequest();
+        assertThat(request.getPath()).isEqualTo(ApiEndpointsV1.SNAPSHOTS_ROUTE
+                                                .replaceAll(KEYSPACE_PATH_PARAM, "cycling")
+                                                .replaceAll(ApiEndpointsV1.TABLE_PATH_PARAM, "cyclist_name")
+                                                .replaceAll(ApiEndpointsV1.SNAPSHOT_PATH_PARAM, "2023.04.11")
+                                                + "?includeSecondaryIndexFiles=true");
+    }
+
     @Test
     public void testListSnapshotFilesWithoutSecondaryIndexFiles() throws Exception
     {
@@ -914,8 +958,7 @@
                                                                                                      server.getPort(), 0,
                                                                                                      "2023.04.12",
                                                                                                      "cycling",
-                                                                                                     "cyclist_name",
-                                                                                                     "1234",
+                                                                                                     "cyclist_name-1234",
                                                                                                      "nb-1-big-TOC.txt");
                 client.streamSSTableComponent(sidecarInstance, fileInfo, null, mockStreamConsumer);
                 expectedPath = fileInfo.componentDownloadUrl();
@@ -1011,8 +1054,7 @@
                                                                                                      server.getPort(), 0,
                                                                                                      "2023.04.12",
                                                                                                      "cycling",
-                                                                                                     "cyclist_name",
-                                                                                                     "1234",
+                                                                                                     "cyclist_name-1234",
                                                                                                      "nb-1-big-TOC.txt");
 
                 client.streamSSTableComponent(sidecarInstance, fileInfo, HttpRange.of(10, 20), mockStreamConsumer);
@@ -1114,8 +1156,7 @@
                                                                                                      server.getPort(), 0,
                                                                                                      "2023.04.12",
                                                                                                      "cycling",
-                                                                                                     "cyclist_name",
-                                                                                                     "1234",
+                                                                                                     "cyclist_name-1234",
                                                                                                      "nb-1-big-TOC.txt");
                 client.streamSSTableComponent(sidecarInstance, fileInfo, null, mockStreamConsumer);
                 expectedPath = fileInfo.componentDownloadUrl();
@@ -1199,8 +1240,7 @@
                                                                                                      server.getPort(), 0,
                                                                                                      "2023.04.12",
                                                                                                      "cycling",
-                                                                                                     "cyclist_name",
-                                                                                                     "1234",
+                                                                                                     "cyclist_name-1234",
                                                                                                      "nb-1-big-TOC.txt");
                 client.streamSSTableComponent(sidecarInstance, fileInfo, HttpRange.of(10, 20), mockStreamConsumer);
                 expectedPath = fileInfo.componentDownloadUrl();
diff --git a/src/main/dist/conf/sidecar.yaml b/src/main/dist/conf/sidecar.yaml
index 31310c7..ecd836c 100644
--- a/src/main/dist/conf/sidecar.yaml
+++ b/src/main/dist/conf/sidecar.yaml
@@ -26,9 +26,8 @@
     username: cassandra
     password: cassandra
     data_dirs:
-      - /ccm/test/node1/data0
-      - /ccm/test/node1/data1
-    staging_dir: /ccm/test/node1/sstable-staging
+      - ~/.ccm/test/node1/data0
+    staging_dir: ~/.ccm/test/node1/sstable-staging
     jmx_host: 127.0.0.1
     jmx_port: 7100
     jmx_ssl_enabled: false
@@ -40,9 +39,8 @@
     username: cassandra
     password: cassandra
     data_dirs:
-      - /ccm/test/node2/data0
-      - /ccm/test/node2/data1
-    staging_dir: /ccm/test/node2/sstable-staging
+      - ~/.ccm/test/node2/data0
+    staging_dir: ~/.ccm/test/node2/sstable-staging
     jmx_host: 127.0.0.1
     jmx_port: 7200
     jmx_ssl_enabled: false
@@ -54,9 +52,8 @@
     username: cassandra
     password: cassandra
     data_dirs:
-      - /ccm/test/node3/data0
-      - /ccm/test/node3/data1
-    staging_dir: /ccm/test/node3/sstable-staging
+      - ~/.ccm/test/node3/data0
+    staging_dir: ~/.ccm/test/node3/sstable-staging
     jmx_host: 127.0.0.1
     jmx_port: 7300
     jmx_ssl_enabled: false
diff --git a/src/main/java/org/apache/cassandra/sidecar/cluster/instance/InstanceMetadataImpl.java b/src/main/java/org/apache/cassandra/sidecar/cluster/instance/InstanceMetadataImpl.java
index 1f90933..21cac7d 100644
--- a/src/main/java/org/apache/cassandra/sidecar/cluster/instance/InstanceMetadataImpl.java
+++ b/src/main/java/org/apache/cassandra/sidecar/cluster/instance/InstanceMetadataImpl.java
@@ -22,12 +22,14 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
+import java.util.stream.Collectors;
 
 import com.codahale.metrics.MetricRegistry;
 import org.apache.cassandra.sidecar.cluster.CassandraAdapterDelegate;
 import org.apache.cassandra.sidecar.common.DataObjectBuilder;
 import org.apache.cassandra.sidecar.metrics.instance.InstanceMetrics;
 import org.apache.cassandra.sidecar.metrics.instance.InstanceMetricsImpl;
+import org.apache.cassandra.sidecar.utils.FileUtils;
 import org.jetbrains.annotations.Nullable;
 
 /**
@@ -49,8 +51,10 @@
         id = builder.id;
         host = builder.host;
         port = builder.port;
-        dataDirs = Collections.unmodifiableList(builder.dataDirs);
-        stagingDir = builder.stagingDir;
+        dataDirs = builder.dataDirs.stream()
+                                   .map(FileUtils::maybeResolveHomeDirectory)
+                                   .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
+        stagingDir = FileUtils.maybeResolveHomeDirectory(builder.stagingDir);
         delegate = builder.delegate;
         metrics = builder.metrics;
     }
diff --git a/src/main/java/org/apache/cassandra/sidecar/routes/snapshots/ListSnapshotHandler.java b/src/main/java/org/apache/cassandra/sidecar/routes/snapshots/ListSnapshotHandler.java
index 0f87817..84db628 100644
--- a/src/main/java/org/apache/cassandra/sidecar/routes/snapshots/ListSnapshotHandler.java
+++ b/src/main/java/org/apache/cassandra/sidecar/routes/snapshots/ListSnapshotHandler.java
@@ -218,14 +218,16 @@
         int sidecarPort = configuration.port();
         ListSnapshotFilesResponse response = new ListSnapshotFilesResponse();
         snapshotFileStream.forEach(file -> {
+            String tableNameAndId = file.tableId != null
+                                    ? request.tableName() + "-" + file.tableId
+                                    : request.tableName();
             response.addSnapshotFile(new ListSnapshotFilesResponse.FileInfo(file.size,
                                                                             host,
                                                                             sidecarPort,
                                                                             file.dataDirectoryIndex,
                                                                             request.snapshotName(),
                                                                             request.keyspace(),
-                                                                            request.tableName(),
-                                                                            file.tableId,
+                                                                            tableNameAndId,
                                                                             file.name));
         });
         return Future.succeededFuture(response);
diff --git a/src/main/java/org/apache/cassandra/sidecar/utils/FileUtils.java b/src/main/java/org/apache/cassandra/sidecar/utils/FileUtils.java
new file mode 100644
index 0000000..51172e8
--- /dev/null
+++ b/src/main/java/org/apache/cassandra/sidecar/utils/FileUtils.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.apache.cassandra.sidecar.utils;
+
+/**
+ * Encompasses utilities for files
+ */
+public class FileUtils
+{
+    /**
+     * Resolves the home directory from the input {@code directory} string when the string begins with {@code ~}.
+     *
+     * @param directory the directory path
+     * @return the resolved directory path, replacing the user's home directory when the string begins with {@code ~}
+     */
+    public static String maybeResolveHomeDirectory(String directory)
+    {
+        if (directory == null || !directory.startsWith("~"))
+            return directory;
+        return System.getProperty("user.home") + directory.substring(1);
+    }
+}
diff --git a/src/test/java/org/apache/cassandra/sidecar/routes/snapshots/ListSnapshotHandlerTest.java b/src/test/java/org/apache/cassandra/sidecar/routes/snapshots/ListSnapshotHandlerTest.java
index 7c64d36..f98574f 100644
--- a/src/test/java/org/apache/cassandra/sidecar/routes/snapshots/ListSnapshotHandlerTest.java
+++ b/src/test/java/org/apache/cassandra/sidecar/routes/snapshots/ListSnapshotHandlerTest.java
@@ -121,8 +121,7 @@
                                                0,
                                                "snapshot1",
                                                "keyspace1",
-                                               "table1",
-                                               "1234",
+                                               "table1-1234",
                                                "1.db");
         ListSnapshotFilesResponse.FileInfo fileInfoNotExpected =
         new ListSnapshotFilesResponse.FileInfo(11,
@@ -131,8 +130,7 @@
                                                0,
                                                "snapshot1",
                                                "keyspace1",
-                                               "table1",
-                                               "1234",
+                                               "table1-1234",
                                                "2.db");
 
         client.get(server.actualPort(), "localhost", testRoute)
@@ -159,8 +157,7 @@
                                                0,
                                                "snapshot1",
                                                "keyspace1",
-                                               "table1",
-                                               "1234",
+                                               "table1-1234",
                                                "1.db"),
         new ListSnapshotFilesResponse.FileInfo(0,
                                                "localhost",
@@ -168,8 +165,7 @@
                                                0,
                                                "snapshot1",
                                                "keyspace1",
-                                               "table1",
-                                               "1234",
+                                               "table1-1234",
                                                ".index/secondary.db")
         );
         ListSnapshotFilesResponse.FileInfo fileInfoNotExpected =
@@ -179,8 +175,7 @@
                                                0,
                                                "snapshot1",
                                                "keyspace1",
-                                               "table1",
-                                               "1234",
+                                               "table1-1234",
                                                "2.db");
 
         client.get(server.actualPort(), "localhost", testRoute)
diff --git a/src/test/java/org/apache/cassandra/sidecar/utils/FileUtilsTest.java b/src/test/java/org/apache/cassandra/sidecar/utils/FileUtilsTest.java
new file mode 100644
index 0000000..eba7be1
--- /dev/null
+++ b/src/test/java/org/apache/cassandra/sidecar/utils/FileUtilsTest.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.apache.cassandra.sidecar.utils;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Unit tests for {@link FileUtils}
+ */
+class FileUtilsTest
+{
+    @Test
+    void testMaybeResolveHomeDirectory()
+    {
+        assertThat(FileUtils.maybeResolveHomeDirectory(null)).isNull();
+        assertThat(FileUtils.maybeResolveHomeDirectory("")).isEqualTo("");
+        assertThat(FileUtils.maybeResolveHomeDirectory("~")).isEqualTo(System.getProperty("user.home"));
+        assertThat(FileUtils.maybeResolveHomeDirectory("~/")).isEqualTo(System.getProperty("user.home") + "/");
+        assertThat(FileUtils.maybeResolveHomeDirectory("~/.ccm")).isEqualTo(System.getProperty("user.home") + "/.ccm");
+        assertThat(FileUtils.maybeResolveHomeDirectory("/dev/null")).isEqualTo("/dev/null");
+    }
+}