ASTERIXDB-1148: Selectable array-wrapping of results

Introduce "wrapper-array" parameter to HTTP API which selects (for ADM and
JSON) whether to wrap the result sequence in a generated outer array. For
JSON this defaults to "true" as before. For ADM this defaults to false,
resulting in a large number of expected-results changes.

Also introduce ability to have AQL tests which provide HTTP parameters.

Change-Id: I3122f136ff9ca8a2c2268238c57bb5eddab7b27e
Reviewed-on: https://asterix-gerrit.ics.uci.edu/473
Tested-by: Jenkins <jenkins@fulliautomatix.ics.uci.edu>
Reviewed-by: Chris Hillery <ceej@lambda.nu>
diff --git a/asterix-app/src/main/java/org/apache/asterix/api/common/SessionConfig.java b/asterix-app/src/main/java/org/apache/asterix/api/common/SessionConfig.java
index 4e64802..f0a0cc2 100644
--- a/asterix-app/src/main/java/org/apache/asterix/api/common/SessionConfig.java
+++ b/asterix-app/src/main/java/org/apache/asterix/api/common/SessionConfig.java
@@ -87,6 +87,11 @@
      */
     public static final String FORMAT_CSV_HEADER = "format-csv-header";
 
+    /**
+     * Format flag: wrap results in outer array brackets (JSON or ADM).
+     */
+    public static final String FORMAT_WRAPPER_ARRAY = "format-wrapper-array";
+
     // Standard execution flags.
     private final boolean executeQuery;
     private final boolean generateJobSpec;
diff --git a/asterix-app/src/main/java/org/apache/asterix/api/http/servlet/APIServlet.java b/asterix-app/src/main/java/org/apache/asterix/api/http/servlet/APIServlet.java
index c22cb8e..2c0f897 100644
--- a/asterix-app/src/main/java/org/apache/asterix/api/http/servlet/APIServlet.java
+++ b/asterix-app/src/main/java/org/apache/asterix/api/http/servlet/APIServlet.java
@@ -77,6 +77,7 @@
         }
 
         String query = request.getParameter("query");
+        String wrapperArray = request.getParameter("wrapper-array");
         String printExprParam = request.getParameter("print-expr-tree");
         String printRewrittenExprParam = request.getParameter("print-rewritten-expr-tree");
         String printLogicalPlanParam = request.getParameter("print-logical-plan");
@@ -105,6 +106,7 @@
             SessionConfig sessionConfig = new SessionConfig(out, format, true, isSet(executeQuery), true);
             sessionConfig.set(SessionConfig.FORMAT_HTML, true);
             sessionConfig.set(SessionConfig.FORMAT_CSV_HEADER, csv_and_header);
+            sessionConfig.set(SessionConfig.FORMAT_WRAPPER_ARRAY, isSet(wrapperArray));
             sessionConfig.setOOBData(isSet(printExprParam), isSet(printRewrittenExprParam),
                     isSet(printLogicalPlanParam), isSet(printOptimizedLogicalPlanParam), isSet(printJob));
             MetadataManager.INSTANCE.init();
diff --git a/asterix-app/src/main/java/org/apache/asterix/api/http/servlet/RESTAPIServlet.java b/asterix-app/src/main/java/org/apache/asterix/api/http/servlet/RESTAPIServlet.java
index be17229..fdaee18 100644
--- a/asterix-app/src/main/java/org/apache/asterix/api/http/servlet/RESTAPIServlet.java
+++ b/asterix-app/src/main/java/org/apache/asterix/api/http/servlet/RESTAPIServlet.java
@@ -71,6 +71,9 @@
         // First check the "output" servlet parameter.
         String output = request.getParameter("output");
         String accept = request.getHeader("Accept");
+        if (accept == null) {
+            accept = "";
+        }
         if (output != null) {
             if (output.equals("CSV")) {
                 format = OutputFormat.CSV;
@@ -79,24 +82,43 @@
             }
         } else {
             // Second check the Accept: HTTP header.
-            if (accept != null) {
-                if (accept.contains("application/x-adm")) {
-                    format = OutputFormat.ADM;
-                } else if (accept.contains("text/csv")) {
-                    format = OutputFormat.CSV;
-                }
+            if (accept.contains("application/x-adm")) {
+                format = OutputFormat.ADM;
+            } else if (accept.contains("text/csv")) {
+                format = OutputFormat.CSV;
             }
         }
 
         // If it's JSON, check for the "lossless" flag
-        if (format == OutputFormat.CLEAN_JSON
-                && ("true".equals(request.getParameter("lossless")) || (accept != null && accept
-                        .contains("lossless=true")))) {
+        if (format == OutputFormat.CLEAN_JSON &&
+                ("true".equals(request.getParameter("lossless")) || accept.contains("lossless=true")) ) {
             format = OutputFormat.LOSSLESS_JSON;
         }
 
         SessionConfig sessionConfig = new SessionConfig(response.getWriter(), format);
 
+        // If it's JSON or ADM, check for the "wrapper-array" flag. Default is
+        // "true" for JSON and "false" for ADM. (Not applicable for CSV.)
+        boolean wrapper_array;
+        switch (format) {
+            case CLEAN_JSON:
+            case LOSSLESS_JSON:
+                wrapper_array = true;
+                break;
+            default:
+                wrapper_array = false;
+                break;
+        }
+        String wrapper_param = request.getParameter("wrapper-array");
+        if (wrapper_param != null) {
+            wrapper_array = Boolean.valueOf(wrapper_param);
+        } else if (accept.contains("wrap-array=true")) {
+            wrapper_array = true;
+        } else if (accept.contains("wrap-array=false")) {
+            wrapper_array = false;
+        }
+        sessionConfig.set(SessionConfig.FORMAT_WRAPPER_ARRAY, wrapper_array);
+
         // Now that format is set, output the content-type
         switch (format) {
             case ADM:
@@ -109,15 +131,15 @@
                 break;
             case CSV: {
                 // Check for header parameter or in Accept:.
-                if ("present".equals(request.getParameter("header"))
-                        || (accept != null && accept.contains("header=present"))) {
+                if ("present".equals(request.getParameter("header")) ||
+                    accept.contains("header=present")) {
                     response.setContentType("text/csv; header=present");
                     sessionConfig.set(SessionConfig.FORMAT_CSV_HEADER, true);
                 } else {
                     response.setContentType("text/csv; header=absent");
                 }
             }
-        };
+        }
 
         return sessionConfig;
     }
diff --git a/asterix-app/src/main/java/org/apache/asterix/result/ResultUtils.java b/asterix-app/src/main/java/org/apache/asterix/result/ResultUtils.java
index 44c878d..391f61b 100644
--- a/asterix-app/src/main/java/org/apache/asterix/result/ResultUtils.java
+++ b/asterix-app/src/main/java/org/apache/asterix/result/ResultUtils.java
@@ -100,8 +100,8 @@
         int bytesRead = resultReader.read(frame);
         ByteBufferInputStream bbis = new ByteBufferInputStream();
 
-        // Whether we need to separate top-level ADM instances with commas
-        boolean need_commas = true;
+        // Whether we are wrapping the output sequence in an array
+        boolean wrap_array = false;
         // Whether this is the first instance being output
         boolean notfirst = false;
 
@@ -114,16 +114,16 @@
         }
 
         switch (conf.fmt()) {
-            case CSV:
-                need_commas = false;
-                break;
             case LOSSLESS_JSON:
             case CLEAN_JSON:
             case ADM:
-                // Conveniently, LOSSLESS_JSON and ADM have the same syntax for an
-                // "ordered list", and our representation of the result of a
-                // statement is an ordered list of instances.
-                conf.out().print("[ ");
+                if (conf.is(SessionConfig.FORMAT_WRAPPER_ARRAY)) {
+                    // Conveniently, LOSSLESS_JSON and ADM have the same syntax for an
+                    // "ordered list", and our representation of the result of a
+                    // statement is an ordered list of instances.
+                    conf.out().print("[ ");
+                    wrap_array = true;
+                }
                 break;
         }
 
@@ -145,7 +145,7 @@
                             }
                         }
                         result = new String(recordBytes, 0, numread, UTF_8);
-                        if (need_commas && notfirst) {
+                        if (wrap_array && notfirst) {
                             conf.out().print(", ");
                         }
                         notfirst = true;
@@ -167,15 +167,8 @@
 
         conf.out().flush();
 
-        switch (conf.fmt()) {
-            case LOSSLESS_JSON:
-            case CLEAN_JSON:
-            case ADM:
-                conf.out().println(" ]");
-                break;
-            case CSV:
-                // Nothing to do
-                break;
+        if (wrap_array) {
+            conf.out().println(" ]");
         }
 
         if (conf.is(SessionConfig.FORMAT_HTML)) {
diff --git a/asterix-app/src/main/resources/webui/querytemplate.html b/asterix-app/src/main/resources/webui/querytemplate.html
index 6ddd664..6ad4818 100644
--- a/asterix-app/src/main/resources/webui/querytemplate.html
+++ b/asterix-app/src/main/resources/webui/querytemplate.html
@@ -220,7 +220,7 @@
             </div>
 
             <div>
-              <label class="checkbox optlabel"> Output Format:<br/>
+              <label id="output-format" class="optlabel"> Output Format:<br/>
                 <select name="output-format" class="btn">
                   <option selected value="ADM">ADM</option>
                   <option value="CSV">CSV (no header)</option>
@@ -229,12 +229,13 @@
                   <option value="LOSSLESS_JSON">JSON (lossless)</option>
                 </select>
               </label>
+              <label class="optlabel"><input type="checkbox" name="wrapper-array" value="true" /> Wrap results in outer array</label>
               <label class="checkbox optlabel"><input type="checkbox" name="print-expr-tree" value="true" /> Print parsed expressions</label>
               <label class="checkbox optlabel"><input type="checkbox" name="print-rewritten-expr-tree" value="true" /> Print rewritten expressions</label>
               <label class="checkbox optlabel"><input type="checkbox" name="print-logical-plan" value="true" /> Print logical plan</label>
               <label class="checkbox optlabel"><input type="checkbox" name="print-optimized-logical-plan" value="true" /> Print optimized logical plan</label>
               <label class="checkbox optlabel"><input type="checkbox" name="print-job" value="true" /> Print Hyracks job</label>
-              <label class="checkbox optlabel"><input type="checkbox" name="execute-query" value="true" checked/> Execute query</label>
+              <label class="optlabel"><input type="checkbox" name="execute-query" value="true" checked/> Execute query</label>
             </div>
           </form>
        </div>