[NO ISSUE]: Fail creation of external collection early if prefix starts with /

Ext-ref: MB-62711
Change-Id: Iab7dfe9f650518e3064bd00da814042818cbe7c6
Reviewed-on: https://asterix-gerrit.ics.uci.edu/c/asterixdb/+/18469
Integration-Tests: Jenkins <jenkins@fulliautomatix.ics.uci.edu>
Tested-by: Jenkins <jenkins@fulliautomatix.ics.uci.edu>
Reviewed-by: Hussain Towaileb <hussainht@gmail.com>
Reviewed-by: Murtadha Hubail <mhubail@apache.org>
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/external-dataset/common/start-with-slash/external_dataset.000.ddl.sqlpp b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/external-dataset/common/start-with-slash/external_dataset.000.ddl.sqlpp
new file mode 100644
index 0000000..ff5ff1e
--- /dev/null
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/queries_sqlpp/external-dataset/common/start-with-slash/external_dataset.000.ddl.sqlpp
@@ -0,0 +1,34 @@
+/*
+ * 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.
+ */
+
+drop dataverse test if exists;
+create dataverse test;
+use test;
+
+drop type test if exists;
+create type test as open {
+};
+
+drop dataset test1 if exists;
+CREATE EXTERNAL DATASET test1(test) USING %adapter% (
+%template%,
+("container"="playground"),
+("definition"="/json-data/reviews/single-line/json"),
+("format"="json")
+);
\ No newline at end of file
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/testsuite_external_dataset_azure_blob_storage.xml b/asterixdb/asterix-app/src/test/resources/runtimets/testsuite_external_dataset_azure_blob_storage.xml
index af0ae36..ca0167b 100644
--- a/asterixdb/asterix-app/src/test/resources/runtimets/testsuite_external_dataset_azure_blob_storage.xml
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/testsuite_external_dataset_azure_blob_storage.xml
@@ -607,5 +607,12 @@
         <output-dir compare="Text">common/byte_order_mark/tsv</output-dir>
       </compilation-unit>
     </test-case>
+    <test-case FilePath="external-dataset">
+      <compilation-unit name="common/start-with-slash">
+        <placeholder name="adapter" value="AZUREBLOB" />
+        <output-dir compare="Text">common/start-with-slash</output-dir>
+        <expected-error>Prefix should not start with "/". Prefix: '/json-data/reviews/single-line/json'</expected-error>
+      </compilation-unit>
+    </test-case>
   </test-group>
 </test-suite>
diff --git a/asterixdb/asterix-app/src/test/resources/runtimets/testsuite_external_dataset_s3.xml b/asterixdb/asterix-app/src/test/resources/runtimets/testsuite_external_dataset_s3.xml
index 5df65ba..98a2fa7 100644
--- a/asterixdb/asterix-app/src/test/resources/runtimets/testsuite_external_dataset_s3.xml
+++ b/asterixdb/asterix-app/src/test/resources/runtimets/testsuite_external_dataset_s3.xml
@@ -920,6 +920,13 @@
         <output-dir compare="Text">common/include-exclude/include-12</output-dir>
       </compilation-unit>
     </test-case>
+    <test-case FilePath="external-dataset">
+      <compilation-unit name="common/start-with-slash">
+        <placeholder name="adapter" value="S3" />
+        <output-dir compare="Text">common/start-with-slash</output-dir>
+        <expected-error>Prefix should not start with "/". Prefix: '/json-data/reviews/single-line/json'</expected-error>
+      </compilation-unit>
+    </test-case>
   </test-group>
   <test-group name="bom">
     <test-case FilePath="external-dataset">
diff --git a/asterixdb/asterix-common/src/main/java/org/apache/asterix/common/exceptions/ErrorCode.java b/asterixdb/asterix-common/src/main/java/org/apache/asterix/common/exceptions/ErrorCode.java
index 2810399..3694bb2 100644
--- a/asterixdb/asterix-common/src/main/java/org/apache/asterix/common/exceptions/ErrorCode.java
+++ b/asterixdb/asterix-common/src/main/java/org/apache/asterix/common/exceptions/ErrorCode.java
@@ -302,6 +302,7 @@
     MINIMUM_VALUE_ALLOWED_FOR_PARAM(1197),
     DUPLICATE_FIELD_IN_PRIMARY_KEY(1198),
     INCOMPATIBLE_FIELDS_IN_PRIMARY_KEY(1199),
+    PREFIX_SHOULD_NOT_START_WITH_SLASH(1200),
 
     // Feed errors
     DATAFLOW_ILLEGAL_STATE(3001),
diff --git a/asterixdb/asterix-common/src/main/resources/asx_errormsg/en.properties b/asterixdb/asterix-common/src/main/resources/asx_errormsg/en.properties
index dd557b9..15c8831 100644
--- a/asterixdb/asterix-common/src/main/resources/asx_errormsg/en.properties
+++ b/asterixdb/asterix-common/src/main/resources/asx_errormsg/en.properties
@@ -304,6 +304,7 @@
 1197 = Minimum value allowed for '%1$s' is %2$s. Found %3$s
 1198 = Duplicate field '%1$s' in primary key
 1199 = Fields '%1$s' and '%2$s' are incompatible for primary key
+1200 = Prefix should not start with "/". Prefix: '%1$s'
 
 # Feed Errors
 3001 = Illegal state.
diff --git a/asterixdb/asterix-external-data/src/main/java/org/apache/asterix/external/util/ExternalDataUtils.java b/asterixdb/asterix-external-data/src/main/java/org/apache/asterix/external/util/ExternalDataUtils.java
index e5b2c9e..104f301 100644
--- a/asterixdb/asterix-external-data/src/main/java/org/apache/asterix/external/util/ExternalDataUtils.java
+++ b/asterixdb/asterix-external-data/src/main/java/org/apache/asterix/external/util/ExternalDataUtils.java
@@ -764,11 +764,12 @@
      *
      * @param configuration configuration
      */
-    public static String getPrefix(Map<String, String> configuration) {
-        return getPrefix(configuration, true);
+    public static String getPrefix(Map<String, String> configuration) throws CompilationException {
+        return getPrefix(configuration, true, true);
     }
 
-    public static String getPrefix(Map<String, String> configuration, boolean appendSlash) {
+    public static String getPrefix(Map<String, String> configuration, boolean appendSlash, boolean failSlashAtStart)
+            throws CompilationException {
         String root = configuration.get(ExternalDataPrefix.PREFIX_ROOT_FIELD_NAME);
         String definition = configuration.get(ExternalDataConstants.DEFINITION_FIELD_NAME);
         String subPath = configuration.get(ExternalDataConstants.SUBPATH);
@@ -777,6 +778,11 @@
         boolean hasDefinition = definition != null && !definition.isEmpty();
         boolean hasSubPath = subPath != null && !subPath.isEmpty();
 
+        // prefix cannot start with a "/"
+        if (failSlashAtStart && hasDefinition && definition.startsWith("/")) {
+            throw new CompilationException(ErrorCode.PREFIX_SHOULD_NOT_START_WITH_SLASH, definition);
+        }
+
         // if computed fields are used, subpath will not take effect. we can tell if we're using a computed field or
         // not by checking if the root matches the definition or not, they never match if computed fields are used
         if (hasRoot && hasDefinition && !root.equals(definition)) {
diff --git a/asterixdb/asterix-external-data/src/main/java/org/apache/asterix/external/util/azure/blob_storage/AzureUtils.java b/asterixdb/asterix-external-data/src/main/java/org/apache/asterix/external/util/azure/blob_storage/AzureUtils.java
index 6330db2..4860ed1 100644
--- a/asterixdb/asterix-external-data/src/main/java/org/apache/asterix/external/util/azure/blob_storage/AzureUtils.java
+++ b/asterixdb/asterix-external-data/src/main/java/org/apache/asterix/external/util/azure/blob_storage/AzureUtils.java
@@ -476,7 +476,7 @@
             ListPathsOptions listOptions = new ListPathsOptions();
             boolean recursive = Boolean.parseBoolean(configuration.get(RECURSIVE_FIELD_NAME));
             listOptions.setRecursive(recursive);
-            listOptions.setPath(getPrefix(configuration, false));
+            listOptions.setPath(getPrefix(configuration, false, false));
             PagedIterable<PathItem> pathItems = fileSystemClient.listPaths(listOptions, null);
 
             // Collect the paths to files only