Several CSV, API, HTTP API, and Web interface improvements.

- APIFramework: internal refactoring to consolidate output PrintWriter,
  OutputFormat, and all output flags into SessionConfig
- APIFramework: "HTML" is now a flag, rather than an OutputFormat
- HTTP API: Output format can be select via query parameter in
  addition to HTTP Accept header
- CSV: default output is now without header, to improve roundtripping
- CSV: header can be requested via Accept header or "header" query
  parameter
- Web interface: Added ability to select output format (JSON, CSV or ADM)

Change-Id: I91398bd30dbd6f3b1f69eb51fbf201010d0e5d93
Reviewed-on: http://fulliautomatix.ics.uci.edu:8443/242
Reviewed-by: Chris Hillery <ceej@lambda.nu>
Tested-by: Chris Hillery <ceej@lambda.nu>
diff --git a/asterix-app/src/main/java/edu/uci/ics/asterix/api/common/APIFramework.java b/asterix-app/src/main/java/edu/uci/ics/asterix/api/common/APIFramework.java
index 1ee9a3e..84a01da 100644
--- a/asterix-app/src/main/java/edu/uci/ics/asterix/api/common/APIFramework.java
+++ b/asterix-app/src/main/java/edu/uci/ics/asterix/api/common/APIFramework.java
@@ -153,43 +153,26 @@
 
     }
 
-    /**
-     * Used to select the output from the various servlets. Note: "HTML" is
-     * primarily intended for use by the built-in web interface. It produces
-     * ADM output with various HTML wrappers.
-     */
-    public enum OutputFormat {
-        ADM,
-        HTML,
-        JSON,
-        CSV
-    }
-
     public static Pair<Query, Integer> reWriteQuery(List<FunctionDecl> declaredFunctions,
-            AqlMetadataProvider metadataProvider, Query q, SessionConfig pc, PrintWriter out, OutputFormat pdf)
+            AqlMetadataProvider metadataProvider, Query q, SessionConfig conf)
             throws AsterixException {
 
-        if (!pc.isPrintPhysicalOpsOnly() && pc.isPrintExprParam()) {
-            out.println();
-            switch (pdf) {
-                case HTML: {
-                    out.println("<h4>Expression tree:</h4>");
-                    out.println("<pre>");
-                    break;
-                }
-                default: {
-                    out.println("----------Expression tree:");
-                    break;
-                }
+        if (conf.is(SessionConfig.FORMAT_ONLY_PHYSICAL_OPS) && conf.is(SessionConfig.OOB_EXPR_TREE)) {
+            conf.out().println();
+
+            if (conf.is(SessionConfig.FORMAT_HTML)) {
+                conf.out().println("<h4>Expression tree:</h4>");
+                conf.out().println("<pre>");
+            } else {
+                conf.out().println("----------Expression tree:");
             }
+
             if (q != null) {
-                q.accept(new AQLPrintVisitor(out), 0);
+                q.accept(new AQLPrintVisitor(conf.out()), 0);
             }
-            switch (pdf) {
-                case HTML: {
-                    out.println("</pre>");
-                    break;
-                }
+
+            if (conf.is(SessionConfig.FORMAT_HTML)) {
+                conf.out().println("</pre>");
             }
         }
         AqlRewriter rw = new AqlRewriter(declaredFunctions, q, metadataProvider);
@@ -200,35 +183,26 @@
 
     public static JobSpecification compileQuery(List<FunctionDecl> declaredFunctions,
             AqlMetadataProvider queryMetadataProvider, Query rwQ, int varCounter, String outputDatasetName,
-            SessionConfig pc, PrintWriter out, OutputFormat pdf, ICompiledDmlStatement statement)
+            SessionConfig conf, ICompiledDmlStatement statement)
             throws AsterixException, AlgebricksException, JSONException, RemoteException, ACIDException {
 
-        if (!pc.isPrintPhysicalOpsOnly() && pc.isPrintRewrittenExprParam()) {
-            out.println();
+        if (conf.is(SessionConfig.FORMAT_ONLY_PHYSICAL_OPS) && conf.is(SessionConfig.OOB_REWRITTEN_EXPR_TREE)) {
+            conf.out().println();
 
-            switch (pdf) {
-                case HTML: {
-                    out.println("<h4>Rewritten expression tree:</h4>");
-                    out.println("<pre>");
-                    break;
-                }
-                default: {
-                    out.println("----------Rewritten expression:");
-                    break;
-                }
+            if (conf.is(SessionConfig.FORMAT_HTML)) {
+                conf.out().println("<h4>Rewritten expression tree:</h4>");
+                conf.out().println("<pre>");
+            } else {
+                conf.out().println("----------Rewritten expression:");
             }
 
             if (rwQ != null) {
-                rwQ.accept(new AQLPrintVisitor(out), 0);
+                rwQ.accept(new AQLPrintVisitor(conf.out()), 0);
             }
 
-            switch (pdf) {
-                case HTML: {
-                    out.println("</pre>");
-                    break;
-                }
+            if (conf.is(SessionConfig.FORMAT_HTML)) {
+                conf.out().println("</pre>");
             }
-
         }
 
         edu.uci.ics.asterix.common.transactions.JobId asterixJobId = JobIdFactory.generateJobId();
@@ -246,31 +220,24 @@
         boolean isWriteTransaction = queryMetadataProvider.isWriteTransaction();
 
         LogicalOperatorPrettyPrintVisitor pvisitor = new LogicalOperatorPrettyPrintVisitor();
-        if (!pc.isPrintPhysicalOpsOnly() && pc.isPrintLogicalPlanParam()) {
+        if (conf.is(SessionConfig.FORMAT_ONLY_PHYSICAL_OPS) && conf.is(SessionConfig.OOB_LOGICAL_PLAN)) {
+            conf.out().println();
 
-            switch (pdf) {
-                case HTML: {
-                    out.println("<h4>Logical plan:</h4>");
-                    out.println("<pre>");
-                    break;
-                }
-                default: {
-                    out.println("----------Logical plan:");
-                    break;
-                }
+            if (conf.is(SessionConfig.FORMAT_HTML)) {
+                conf.out().println("<h4>Logical plan:</h4>");
+                conf.out().println("<pre>");
+            } else {
+                conf.out().println("----------Logical plan:");
             }
 
             if (rwQ != null || statement.getKind() == Kind.LOAD) {
                 StringBuilder buffer = new StringBuilder();
                 PlanPrettyPrinter.printPlan(plan, buffer, pvisitor, 0);
-                out.print(buffer);
+                conf.out().print(buffer);
             }
 
-            switch (pdf) {
-                case HTML: {
-                    out.println("</pre>");
-                    break;
-                }
+            if (conf.is(SessionConfig.FORMAT_HTML)) {
+                conf.out().println("</pre>");
             }
         }
 
@@ -305,45 +272,39 @@
         builder.setNullableTypeComputer(AqlNullableTypeComputer.INSTANCE);
 
         ICompiler compiler = compilerFactory.createCompiler(plan, queryMetadataProvider, t.getVarCounter());
-        if (pc.isOptimize()) {
+        if (conf.isOptimize()) {
             compiler.optimize();
             //plot optimized logical plan
             if (plot)
                 PlanPlotter.printOptimizedLogicalPlan(plan);
-            if (pc.isPrintOptimizedLogicalPlanParam()) {
-                if (pc.isPrintPhysicalOpsOnly()) {
+            if (conf.is(SessionConfig.OOB_OPTIMIZED_LOGICAL_PLAN)) {
+                if (conf.is(SessionConfig.FORMAT_ONLY_PHYSICAL_OPS)) {
                     // For Optimizer tests.
                     StringBuilder buffer = new StringBuilder();
                     PlanPrettyPrinter.printPhysicalOps(plan, buffer, 0);
-                    out.print(buffer);
+                    conf.out().print(buffer);
                 } else {
-                    switch (pdf) {
-                        case HTML: {
-                            out.println("<h4>Optimized logical plan:</h4>");
-                            out.println("<pre>");
-                            break;
-                        }
-                        default: {
-                            out.println("----------Optimized logical plan:");
-                            break;
-                        }
+                    if (conf.is(SessionConfig.FORMAT_HTML)) {
+                        conf.out().println("<h4>Optimized logical plan:</h4>");
+                        conf.out().println("<pre>");
+                    } else {
+                        conf.out().println("----------Optimized logical plan:");
                     }
+
                     if (rwQ != null || statement.getKind() == Kind.LOAD) {
                         StringBuilder buffer = new StringBuilder();
                         PlanPrettyPrinter.printPlan(plan, buffer, pvisitor, 0);
-                        out.print(buffer);
+                        conf.out().print(buffer);
                     }
-                    switch (pdf) {
-                        case HTML: {
-                            out.println("</pre>");
-                            break;
-                        }
+
+                    if (conf.is(SessionConfig.FORMAT_HTML)) {
+                        conf.out().println("</pre>");
                     }
                 }
             }
         }
 
-        if (!pc.isGenerateJobSpec()) {
+        if (!conf.isGenerateJobSpec()) {
             return null;
         }
 
@@ -359,16 +320,18 @@
         builder.setNullWriterFactory(format.getNullWriterFactory());
         builder.setPredicateEvaluatorFactoryProvider(format.getPredicateEvaluatorFactoryProvider());
 
-        switch (pdf) {
+        switch (conf.fmt()) {
             case JSON:
                 builder.setPrinterProvider(format.getJSONPrinterFactoryProvider());
                 break;
             case CSV:
                 builder.setPrinterProvider(format.getCSVPrinterFactoryProvider());
                 break;
-            default:
+            case ADM:
                 builder.setPrinterProvider(format.getPrinterFactoryProvider());
                 break;
+            default:
+                throw new RuntimeException("Unexpected OutputFormat!");
         }
 
         builder.setSerializerDeserializerProvider(format.getSerdeProvider());
@@ -379,34 +342,28 @@
                 isWriteTransaction);
         JobSpecification spec = compiler.createJob(AsterixAppContextInfo.getInstance(), jobEventListenerFactory);
 
-        if (pc.isPrintJob()) {
-            switch (pdf) {
-                case HTML: {
-                    out.println("<h4>Hyracks job:</h4>");
-                    out.println("<pre>");
-                    break;
-                }
-                default: {
-                    out.println("----------Hyracks job:");
-                    break;
-                }
+        if (conf.is(SessionConfig.OOB_HYRACKS_JOB)) {
+            if (conf.is(SessionConfig.FORMAT_HTML)) {
+                conf.out().println("<h4>Hyracks job:</h4>");
+                conf.out().println("<pre>");
+            } else {
+                conf.out().println("----------Hyracks job:");
             }
+
             if (rwQ != null) {
-                out.println(spec.toJSON().toString(1));
-                out.println(spec.getUserConstraints());
+                conf.out().println(spec.toJSON().toString(1));
+                conf.out().println(spec.getUserConstraints());
             }
-            switch (pdf) {
-                case HTML: {
-                    out.println("</pre>");
-                    break;
-                }
+
+            if (conf.is(SessionConfig.FORMAT_HTML)) {
+                conf.out().println("</pre>");
             }
         }
         return spec;
     }
 
-    public static void executeJobArray(IHyracksClientConnection hcc, JobSpecification[] specs, PrintWriter out,
-            OutputFormat pdf) throws Exception {
+    public static void executeJobArray(IHyracksClientConnection hcc, JobSpecification[] specs, PrintWriter out)
+        throws Exception {
         for (int i = 0; i < specs.length; i++) {
             specs[i].setMaxReattempts(0);
             JobId jobId = hcc.startJob(specs[i]);
@@ -419,7 +376,7 @@
 
     }
 
-    public static void executeJobArray(IHyracksClientConnection hcc, Job[] jobs, PrintWriter out, OutputFormat pdf)
+    public static void executeJobArray(IHyracksClientConnection hcc, Job[] jobs, PrintWriter out)
             throws Exception {
         for (int i = 0; i < jobs.length; i++) {
             jobs[i].getJobSpec().setMaxReattempts(0);
diff --git a/asterix-app/src/main/java/edu/uci/ics/asterix/api/common/SessionConfig.java b/asterix-app/src/main/java/edu/uci/ics/asterix/api/common/SessionConfig.java
index a2f27b4..a82558f 100644
--- a/asterix-app/src/main/java/edu/uci/ics/asterix/api/common/SessionConfig.java
+++ b/asterix-app/src/main/java/edu/uci/ics/asterix/api/common/SessionConfig.java
@@ -14,80 +14,188 @@
  */
 package edu.uci.ics.asterix.api.common;
 
+import java.io.PrintWriter;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * SessionConfig captures several different parameters for controlling
+ * the execution of an APIFramework call.
+ * <li> It specifies how the execution will proceed (for instance,
+ * whether to optimize, or whether to execute at all).
+ * <li> It allows you specify where the primary execution output will
+ * be sent.
+ * <li> It also allows you to request additional output for optional
+ * out-of-band data about the execution (query plan, etc).
+ * <li> It allows you to specify the output format for the primary
+ * execution output - JSON, CSV, etc.
+ * <li> It allows you to specify output format-specific parameters.
+ */
+
 public class SessionConfig {
-    private final boolean optimize;
-    private final boolean printExprParam;
-    private final boolean printRewrittenExprParam;
-    private final boolean printLogicalPlanParam;
-    private final boolean printOptimizedLogicalPlanParam;
-    private final boolean printPhysicalOpsOnly;
-    private final boolean executeQuery;
-    private final boolean generateJobSpec;
-    private final boolean printJob;
+    /**
+     * Used to specify the output format for the primary execution.
+     */
+    public enum OutputFormat {
+        ADM,
+        JSON,
+        CSV
+    };
 
     /**
-     * Note: the various "print" options below will cause additional output
-     * to be generated when invoking servlet functions. This output is NOT
-     * guaranteed to match the OutputFormat (JSON, ADM...) except when the
-     * OutputFormat is "HTML". This is primarily for use by the built-in
-     * web interface (APIServlet).
-     * @param optimize
-     * @param printExprParam
-     * @param printRewrittenExprParam
-     * @param printLogicalPlanParam
-     * @param printOptimizedLogicalPlanParam
-     * @param printPhysicalOpsOnly
-     * @param executeQuery
-     * @param generateJobSpec
-     * @param printJob
+     * Produce out-of-band output for Hyracks Job.
      */
-    public SessionConfig(boolean optimize, boolean printExprParam, boolean printRewrittenExprParam,
-            boolean printLogicalPlanParam, boolean printOptimizedLogicalPlanParam, boolean printPhysicalOpsOnly,
-            boolean executeQuery, boolean generateJobSpec, boolean printJob) {
+    public static final String OOB_HYRACKS_JOB = "oob-hyracks-job";
+
+    /**
+     * Produce out-of-band output for Expression Tree.
+     */
+    public static final String OOB_EXPR_TREE = "oob-expr-tree";
+
+    /**
+     * Produce out-of-band output for Rewritten Expression Tree.
+     */
+    public static final String OOB_REWRITTEN_EXPR_TREE = "oob-rewritten-expr-tree";
+
+    /**
+     * Produce out-of-band output for Logical Plan.
+     */
+    public static final String OOB_LOGICAL_PLAN = "oob-logical-plan";
+
+    /**
+     * Produce out-of-band output for Optimized Logical Plan.
+     */
+    public static final String OOB_OPTIMIZED_LOGICAL_PLAN = "oob-optimized-logical-plan";
+
+    /**
+     * Format flag: print only physical ops (for optimizer tests).
+     */
+    public static final String FORMAT_ONLY_PHYSICAL_OPS = "format-only-physical-ops";
+
+    /**
+     * Format flag: wrap out-of-band data in HTML.
+     */
+    public static final String FORMAT_HTML = "format-html";
+
+    /**
+     * Format flag: print CSV header line.
+     */
+    public static final String FORMAT_CSV_HEADER = "format-csv-header";
+
+    // Standard execution flags.
+    private final boolean executeQuery;
+    private final boolean generateJobSpec;
+    private final boolean optimize;
+
+    // Output path for primary execution.
+    private final PrintWriter out;
+
+    // Output format.
+    private final OutputFormat fmt;
+
+    // Flags.
+    private final Map<String,Boolean> flags;
+
+    /**
+     * Create a SessionConfig object with all default values:
+     *
+     * - All format flags set to "false".
+     * - All out-of-band outputs set to "null".
+     * - "Optimize" set to "true".
+     * - "Execute Query" set to "true".
+     * - "Generate Job Spec" set to "true".
+     * @param out PrintWriter for execution output.
+     * @param fmt Output format for execution output.
+     */
+    public SessionConfig(PrintWriter out, OutputFormat fmt) {
+        this(out, fmt, true, true, true);
+    }
+
+    /**
+     * Create a SessionConfig object with all optional values set to defaults:
+     *
+     * - All format flags set to "false".
+     * - All out-of-band outputs set to "false".
+     * @param out PrintWriter for execution output.
+     * @param fmt Output format for execution output.
+     * @param optimize Whether to optimize the execution.
+     * @param executeQuery Whether to execute the query or not.
+     * @param generateJobSpec Whether to generate the Hyracks job specification (if
+     *    false, job cannot be executed).
+     */
+    public SessionConfig(PrintWriter out, OutputFormat fmt, boolean optimize, boolean executeQuery, boolean generateJobSpec) {
+        this.out = out;
+        this.fmt = fmt;
         this.optimize = optimize;
-        this.printExprParam = printExprParam;
-        this.printRewrittenExprParam = printRewrittenExprParam;
-        this.printLogicalPlanParam = printLogicalPlanParam;
-        this.printOptimizedLogicalPlanParam = printOptimizedLogicalPlanParam;
-        this.printPhysicalOpsOnly = printPhysicalOpsOnly;
         this.executeQuery = executeQuery;
         this.generateJobSpec = generateJobSpec;
-        this.printJob = printJob;
+        this.flags = new HashMap<String,Boolean>();
     }
 
-    public boolean isPrintExprParam() {
-        return printExprParam;
+    /**
+     * Retrieve the PrintWriter to produce output to.
+     */
+    public PrintWriter out() {
+        return this.out;
     }
 
-    public boolean isPrintRewrittenExprParam() {
-        return printRewrittenExprParam;
+    /**
+     * Retrieve the OutputFormat for this execution.
+     */
+    public OutputFormat fmt() {
+        return this.fmt;
     }
 
-    public boolean isPrintLogicalPlanParam() {
-        return printLogicalPlanParam;
-    }
-
-    public boolean isPrintOptimizedLogicalPlanParam() {
-        return printOptimizedLogicalPlanParam;
-    }
-
-    public boolean isPrintJob() {
-        return printJob;
-    }
-
-    public boolean isPrintPhysicalOpsOnly() {
-        return printPhysicalOpsOnly;
-    }
-
+    /**
+     * Retrieve the value of the "execute query" flag.
+     */
     public boolean isExecuteQuery() {
         return executeQuery;
     }
 
+    /**
+     * Retrieve the value of the "optimize" flag.
+     */
     public boolean isOptimize() {
         return optimize;
     }
 
+    /**
+     * Retrieve the value of the "generate job spec" flag.
+     */
     public boolean isGenerateJobSpec() {
         return generateJobSpec;
     }
-}
\ No newline at end of file
+
+    /**
+     * Specify all out-of-band settings at once. For convenience of older code.
+     */
+    public void setOOBData(boolean expr_tree, boolean rewritten_expr_tree,
+                           boolean logical_plan, boolean optimized_logical_plan,
+                           boolean hyracks_job) {
+        this.set(OOB_EXPR_TREE, expr_tree);
+        this.set(OOB_REWRITTEN_EXPR_TREE, rewritten_expr_tree);
+        this.set(OOB_LOGICAL_PLAN, logical_plan);
+        this.set(OOB_OPTIMIZED_LOGICAL_PLAN, optimized_logical_plan);
+        this.set(OOB_HYRACKS_JOB, hyracks_job);
+    }
+
+    /**
+     * Specify a flag.
+     * @param flag One of the OOB_ or FORMAT_ constants from this class.
+     * @param value Value for the flag (all flags default to "false").
+     */
+    public void set(String flag, boolean value) {
+        flags.put(flag, Boolean.valueOf(value));
+    }
+
+    /**
+     * Retrieve the setting of a format-specific flag.
+     * @param flag One of the FORMAT_ constants from this class.
+     * @returns true or false (all flags default to "false").
+     */
+    public boolean is(String flag) {
+        Boolean value = flags.get(flag);
+        return value == null ? false : value.booleanValue();
+    }
+}
diff --git a/asterix-app/src/main/java/edu/uci/ics/asterix/api/http/servlet/APIServlet.java b/asterix-app/src/main/java/edu/uci/ics/asterix/api/http/servlet/APIServlet.java
index be88a1e..4984741 100644
--- a/asterix-app/src/main/java/edu/uci/ics/asterix/api/http/servlet/APIServlet.java
+++ b/asterix-app/src/main/java/edu/uci/ics/asterix/api/http/servlet/APIServlet.java
@@ -30,8 +30,8 @@
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
-import edu.uci.ics.asterix.api.common.APIFramework.OutputFormat;
 import edu.uci.ics.asterix.api.common.SessionConfig;
+import edu.uci.ics.asterix.api.common.SessionConfig.OutputFormat;
 import edu.uci.ics.asterix.aql.base.Statement;
 import edu.uci.ics.asterix.aql.parser.AQLParser;
 import edu.uci.ics.asterix.aql.parser.ParseException;
@@ -54,12 +54,23 @@
 
     @Override
     public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
-        OutputFormat format = OutputFormat.HTML;
-        if (request.getContentType().equals("application/json")) {
-            format = OutputFormat.JSON;
-        } else if (request.getContentType().equals("text/plain")) {
+        OutputFormat format;
+        boolean csv_and_header = false;
+        String output = request.getParameter("output-format");
+        if (output.equals("ADM")) {
             format = OutputFormat.ADM;
         }
+        else if (output.equals("CSV")) {
+            format = OutputFormat.CSV;
+        }
+        else if (output.equals("CSV-Header")) {
+            format = OutputFormat.CSV;
+            csv_and_header = true;
+        }
+        else {
+            // Default output format
+            format = OutputFormat.JSON;
+        }
 
         String query = request.getParameter("query");
         String printExprParam = request.getParameter("print-expr-tree");
@@ -87,11 +98,14 @@
             }
             AQLParser parser = new AQLParser(query);
             List<Statement> aqlStatements = parser.parse();
-            SessionConfig sessionConfig = new SessionConfig(true, isSet(printExprParam),
-                    isSet(printRewrittenExprParam), isSet(printLogicalPlanParam),
-                    isSet(printOptimizedLogicalPlanParam), false, isSet(executeQuery), true, isSet(printJob));
+            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.setOOBData(isSet(printExprParam), isSet(printRewrittenExprParam),
+                                     isSet(printLogicalPlanParam), isSet(printOptimizedLogicalPlanParam),
+                                     isSet(printJob));
             MetadataManager.INSTANCE.init();
-            AqlTranslator aqlTranslator = new AqlTranslator(aqlStatements, out, sessionConfig, format);
+            AqlTranslator aqlTranslator = new AqlTranslator(aqlStatements, sessionConfig);
             double duration = 0;
             long startTime = System.currentTimeMillis();
             aqlTranslator.compileAndExecute(hcc, hds, AqlTranslator.ResultDelivery.SYNC);
diff --git a/asterix-app/src/main/java/edu/uci/ics/asterix/api/http/servlet/QueryResultAPIServlet.java b/asterix-app/src/main/java/edu/uci/ics/asterix/api/http/servlet/QueryResultAPIServlet.java
index 5ccbfc8..3f104fe 100644
--- a/asterix-app/src/main/java/edu/uci/ics/asterix/api/http/servlet/QueryResultAPIServlet.java
+++ b/asterix-app/src/main/java/edu/uci/ics/asterix/api/http/servlet/QueryResultAPIServlet.java
@@ -26,6 +26,8 @@
 import org.json.JSONObject;
 
 import edu.uci.ics.asterix.api.common.APIFramework;
+import edu.uci.ics.asterix.api.common.SessionConfig;
+import edu.uci.ics.asterix.api.common.SessionConfig.OutputFormat;
 import edu.uci.ics.asterix.result.ResultReader;
 import edu.uci.ics.asterix.result.ResultUtils;
 import edu.uci.ics.hyracks.api.client.HyracksConnection;
@@ -78,30 +80,14 @@
             ResultReader resultReader = new ResultReader(hcc, hds);
             resultReader.open(jobId, rsId);
 
-            APIFramework.OutputFormat format;
-            // QQQ This code is duplicated from RESTAPIServlet, and is
-            // erroneous anyway. The output format is determined by
-            // the initial query and cannot be modified here, so we need
-            // to find a way to send the same OutputFormat value here as
-            // was originally determined there. Need to save this value on
+            // QQQ The output format is determined by the initial
+            // query and cannot be modified here, so calling back to
+            // initResponse() is really an error. We need to find a
+            // way to send the same OutputFormat value here as was
+            // originally determined there. Need to save this value on
             // some object that we can obtain here.
-            String accept = request.getHeader("Accept");
-            if ((accept == null) || (accept.contains("application/x-adm"))) {
-                format = APIFramework.OutputFormat.ADM;
-                response.setContentType("application/x-adm");
-            } else if (accept.contains("text/html")) {
-                format = APIFramework.OutputFormat.HTML;
-                response.setContentType("text/html");
-            } else if (accept.contains("text/csv")) {
-                format = APIFramework.OutputFormat.CSV;
-                response.setContentType("text/csv; header=present");
-            } else {
-                // JSON output is the default; most generally useful for a
-                // programmatic HTTP API
-                format = APIFramework.OutputFormat.JSON;
-                response.setContentType("application/json");
-            }
-            ResultUtils.displayResults(resultReader, out, format);
+            SessionConfig sessionConfig = RESTAPIServlet.initResponse(request, response);
+            ResultUtils.displayResults(resultReader, sessionConfig);
 
         } catch (Exception e) {
             out.println(e.getMessage());
diff --git a/asterix-app/src/main/java/edu/uci/ics/asterix/api/http/servlet/RESTAPIServlet.java b/asterix-app/src/main/java/edu/uci/ics/asterix/api/http/servlet/RESTAPIServlet.java
index e783741..4e8427d 100644
--- a/asterix-app/src/main/java/edu/uci/ics/asterix/api/http/servlet/RESTAPIServlet.java
+++ b/asterix-app/src/main/java/edu/uci/ics/asterix/api/http/servlet/RESTAPIServlet.java
@@ -31,8 +31,8 @@
 import org.json.JSONObject;
 
 import edu.uci.ics.asterix.api.common.APIFramework;
-import edu.uci.ics.asterix.api.common.APIFramework.OutputFormat;
 import edu.uci.ics.asterix.api.common.SessionConfig;
+import edu.uci.ics.asterix.api.common.SessionConfig.OutputFormat;
 import edu.uci.ics.asterix.aql.base.Statement;
 import edu.uci.ics.asterix.aql.base.Statement.Kind;
 import edu.uci.ics.asterix.aql.parser.AQLParser;
@@ -55,6 +55,67 @@
 
     private static final String HYRACKS_DATASET_ATTR = "edu.uci.ics.asterix.HYRACKS_DATASET";
 
+    /**
+     * Initialize the Content-Type of the response, and construct a
+     * SessionConfig with the appropriate output writer and output-format
+     * based on the Accept: header and other servlet parameters.
+     */
+    static SessionConfig initResponse(HttpServletRequest request, HttpServletResponse response)
+        throws IOException {
+        response.setCharacterEncoding("utf-8");
+
+        // JSON output is the default; most generally useful for a
+        // programmatic HTTP API
+        OutputFormat format = OutputFormat.JSON;
+
+        // First check the "output" servlet parameter.
+        String output = request.getParameter("output");
+        String accept = request.getHeader("Accept");
+        if (output != null) {
+            if (output.equals("CSV")) {
+                format = OutputFormat.CSV;
+            }
+            else if (output.equals("ADM")) {
+                format = OutputFormat.ADM;
+            }
+        }
+        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;
+                }
+            }
+        }
+
+        SessionConfig sessionConfig = new SessionConfig(response.getWriter(), format);
+
+        // Now that format is set, output the content-type
+        switch (format) {
+            case ADM:
+                response.setContentType("application/x-adm");
+                break;
+            case JSON:
+                response.setContentType("application/json");
+                break;
+            case CSV: {
+                // Check for header parameter or in Accept:.
+                if ("present".equals(request.getParameter("header")) ||
+                    (accept != null && 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;
+    }
+
     @Override
     protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException,
             IOException {
@@ -72,28 +133,7 @@
 
     public void handleRequest(HttpServletRequest request, HttpServletResponse response, String query)
             throws IOException {
-        response.setCharacterEncoding("utf-8");
-
-        PrintWriter out = response.getWriter();
-        APIFramework.OutputFormat format;
-        // QQQ For now switch solely based on Accept header. Later will add
-        // an "output" query parameter.
-        String accept = request.getHeader("Accept");
-        if ((accept == null) || (accept.contains("application/x-adm"))) {
-            format = OutputFormat.ADM;
-            response.setContentType("application/x-adm");
-        } else if (accept.contains("text/html")) {
-            format = OutputFormat.HTML;
-            response.setContentType("text/html");
-        } else if (accept.contains("text/csv")) {
-            format = OutputFormat.CSV;
-            response.setContentType("text/csv; header=present");
-        } else {
-            // JSON output is the default; most generally useful for a
-            // programmatic HTTP API
-            format = APIFramework.OutputFormat.JSON;
-            response.setContentType("application/json");
-        }
+        SessionConfig sessionConfig = initResponse(request, response);
         AqlTranslator.ResultDelivery resultDelivery = whichResultDelivery(request);
 
         ServletContext context = getServletContext();
@@ -113,21 +153,19 @@
             AQLParser parser = new AQLParser(query);
             List<Statement> aqlStatements = parser.parse();
             if (!containsForbiddenStatements(aqlStatements)) {
-                SessionConfig sessionConfig = new SessionConfig(true, false, false, false, false, false, true, true,
-                        false);
                 MetadataManager.INSTANCE.init();
-                AqlTranslator aqlTranslator = new AqlTranslator(aqlStatements, out, sessionConfig, format);
+                AqlTranslator aqlTranslator = new AqlTranslator(aqlStatements, sessionConfig);
                 aqlTranslator.compileAndExecute(hcc, hds, resultDelivery);
             }
         } catch (ParseException | TokenMgrError | edu.uci.ics.asterix.aqlplus.parser.TokenMgrError pe) {
             GlobalConfig.ASTERIX_LOGGER.log(Level.SEVERE, pe.getMessage(), pe);
             String errorMessage = ResultUtils.buildParseExceptionMessage(pe, query);
             JSONObject errorResp = ResultUtils.getErrorResponse(2, errorMessage, "", "");
-            out.write(errorResp.toString());
+            sessionConfig.out().write(errorResp.toString());
             response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
         } catch (Exception e) {
             GlobalConfig.ASTERIX_LOGGER.log(Level.SEVERE, e.getMessage(), e);
-            ResultUtils.apiErrorHandler(out, e);
+            ResultUtils.apiErrorHandler(sessionConfig.out(), e);
             response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
         }
     }
diff --git a/asterix-app/src/main/java/edu/uci/ics/asterix/api/java/AsterixJavaClient.java b/asterix-app/src/main/java/edu/uci/ics/asterix/api/java/AsterixJavaClient.java
index e0ba829..54804e7 100644
--- a/asterix-app/src/main/java/edu/uci/ics/asterix/api/java/AsterixJavaClient.java
+++ b/asterix-app/src/main/java/edu/uci/ics/asterix/api/java/AsterixJavaClient.java
@@ -19,9 +19,9 @@
 import java.util.List;
 
 import edu.uci.ics.asterix.api.common.APIFramework;
-import edu.uci.ics.asterix.api.common.APIFramework.OutputFormat;
 import edu.uci.ics.asterix.api.common.Job;
 import edu.uci.ics.asterix.api.common.SessionConfig;
+import edu.uci.ics.asterix.api.common.SessionConfig.OutputFormat;
 import edu.uci.ics.asterix.aql.base.Statement;
 import edu.uci.ics.asterix.aql.parser.AQLParser;
 import edu.uci.ics.asterix.aql.parser.ParseException;
@@ -76,21 +76,25 @@
         }
         MetadataManager.INSTANCE.init();
 
-        SessionConfig pc = new SessionConfig(optimize, false, printRewrittenExpressions, printLogicalPlan,
-                printOptimizedPlan, printPhysicalOpsOnly, true, generateBinaryRuntime, printJob);
+        SessionConfig conf = new SessionConfig(writer, OutputFormat.ADM, optimize, true, generateBinaryRuntime);
+        conf.setOOBData(false, printRewrittenExpressions, printLogicalPlan,
+                        printOptimizedPlan, printJob);
+        if (printPhysicalOpsOnly) {
+            conf.set(SessionConfig.FORMAT_ONLY_PHYSICAL_OPS, true);
+        }
 
-        AqlTranslator aqlTranslator = new AqlTranslator(aqlStatements, writer, pc, OutputFormat.ADM);
+        AqlTranslator aqlTranslator = new AqlTranslator(aqlStatements, conf);
         aqlTranslator.compileAndExecute(hcc, null, AqlTranslator.ResultDelivery.SYNC);
         writer.flush();
     }
 
     public void execute() throws Exception {
         if (dmlJobs != null) {
-            APIFramework.executeJobArray(hcc, dmlJobs, writer, OutputFormat.ADM);
+            APIFramework.executeJobArray(hcc, dmlJobs, writer);
         }
         if (queryJobSpec != null) {
-            APIFramework.executeJobArray(hcc, new JobSpecification[] { queryJobSpec }, writer, OutputFormat.ADM);
+            APIFramework.executeJobArray(hcc, new JobSpecification[] { queryJobSpec }, writer);
         }
     }
 
-}
\ No newline at end of file
+}
diff --git a/asterix-app/src/main/java/edu/uci/ics/asterix/aql/translator/AqlTranslator.java b/asterix-app/src/main/java/edu/uci/ics/asterix/aql/translator/AqlTranslator.java
index 52fc25e..0184a65 100644
--- a/asterix-app/src/main/java/edu/uci/ics/asterix/aql/translator/AqlTranslator.java
+++ b/asterix-app/src/main/java/edu/uci/ics/asterix/aql/translator/AqlTranslator.java
@@ -37,9 +37,9 @@
 import org.json.JSONObject;
 
 import edu.uci.ics.asterix.api.common.APIFramework;
-import edu.uci.ics.asterix.api.common.APIFramework.OutputFormat;
 import edu.uci.ics.asterix.api.common.Job;
 import edu.uci.ics.asterix.api.common.SessionConfig;
+import edu.uci.ics.asterix.api.common.SessionConfig.OutputFormat;
 import edu.uci.ics.asterix.aql.base.Statement;
 import edu.uci.ics.asterix.aql.expression.CompactStatement;
 import edu.uci.ics.asterix.aql.expression.ConnectFeedStatement;
@@ -188,18 +188,14 @@
 
     public static final boolean IS_DEBUG_MODE = false;//true
     private final List<Statement> aqlStatements;
-    private final PrintWriter out;
     private final SessionConfig sessionConfig;
-    private final OutputFormat pdf;
     private Dataverse activeDefaultDataverse;
     private final List<FunctionDecl> declaredFunctions;
 
-    public AqlTranslator(List<Statement> aqlStatements, PrintWriter out, SessionConfig pc, APIFramework.OutputFormat pdf)
+    public AqlTranslator(List<Statement> aqlStatements, SessionConfig conf)
             throws MetadataException, AsterixException {
         this.aqlStatements = aqlStatements;
-        this.out = out;
-        this.sessionConfig = pc;
-        this.pdf = pdf;
+        this.sessionConfig = conf;
         declaredFunctions = getDeclaredFunctions(aqlStatements);
     }
 
@@ -1740,8 +1736,7 @@
             CompiledLoadFromFileStatement cls = new CompiledLoadFromFileStatement(dataverseName, loadStmt
                     .getDatasetName().getValue(), loadStmt.getAdapter(), loadStmt.getProperties(),
                     loadStmt.dataIsAlreadySorted());
-            JobSpecification spec = APIFramework.compileQuery(null, metadataProvider, null, 0, null, sessionConfig,
-                    out, pdf, cls);
+            JobSpecification spec = APIFramework.compileQuery(null, metadataProvider, null, 0, null, sessionConfig, cls);
             MetadataManager.INSTANCE.commitTransaction(mdTxnCtx);
             bActiveTxn = false;
             if (spec != null) {
@@ -1837,12 +1832,12 @@
 
         // Query Rewriting (happens under the same ongoing metadata transaction)
         Pair<Query, Integer> reWrittenQuery = APIFramework.reWriteQuery(declaredFunctions, metadataProvider, query,
-                sessionConfig, out, pdf);
+                sessionConfig);
 
         // Query Compilation (happens under the same ongoing metadata
         // transaction)
         JobSpecification spec = APIFramework.compileQuery(declaredFunctions, metadataProvider, reWrittenQuery.first,
-                reWrittenQuery.second, stmt == null ? null : stmt.getDatasetName(), sessionConfig, out, pdf, stmt);
+                reWrittenQuery.second, stmt == null ? null : stmt.getDatasetName(), sessionConfig, stmt);
 
         return spec;
 
@@ -2187,8 +2182,8 @@
                         handle.put(jobId.getId());
                         handle.put(metadataProvider.getResultSetId().getId());
                         response.put("handle", handle);
-                        out.print(response);
-                        out.flush();
+                        sessionConfig.out().print(response);
+                        sessionConfig.out().flush();
                         hcc.waitForCompletion(jobId);
                         break;
                     case SYNC:
@@ -2198,10 +2193,11 @@
                         // In this case (the normal case), we don't use the
                         // "response" JSONObject - just stream the results
                         // to the "out" PrintWriter
-                        if (pdf == OutputFormat.CSV) {
-                            ResultUtils.displayCSVHeader(metadataProvider.findOutputRecordType(), out);
+                        if (sessionConfig.fmt() == OutputFormat.CSV &&
+                            sessionConfig.is(SessionConfig.FORMAT_CSV_HEADER)) {
+                            ResultUtils.displayCSVHeader(metadataProvider.findOutputRecordType(), sessionConfig);
                         }
-                        ResultUtils.displayResults(resultReader, out, pdf);
+                        ResultUtils.displayResults(resultReader, sessionConfig);
 
                         hcc.waitForCompletion(jobId);
                         break;
@@ -2211,8 +2207,8 @@
                         handle.put(metadataProvider.getResultSetId().getId());
                         response.put("handle", handle);
                         hcc.waitForCompletion(jobId);
-                        out.print(response);
-                        out.flush();
+                        sessionConfig.out().print(response);
+                        sessionConfig.out().flush();
                         break;
                     default:
                         break;
@@ -2715,7 +2711,7 @@
 
     private JobId runJob(IHyracksClientConnection hcc, JobSpecification spec, boolean waitForCompletion)
             throws Exception {
-        JobId[] jobIds = executeJobArray(hcc, new Job[] { new Job(spec) }, out, waitForCompletion);
+        JobId[] jobIds = executeJobArray(hcc, new Job[] { new Job(spec) }, sessionConfig.out(), waitForCompletion);
         return jobIds[0];
     }
 
diff --git a/asterix-app/src/main/java/edu/uci/ics/asterix/hyracks/bootstrap/FeedLifecycleListener.java b/asterix-app/src/main/java/edu/uci/ics/asterix/hyracks/bootstrap/FeedLifecycleListener.java
index 9c7ff81..4cdc91c 100644
--- a/asterix-app/src/main/java/edu/uci/ics/asterix/hyracks/bootstrap/FeedLifecycleListener.java
+++ b/asterix-app/src/main/java/edu/uci/ics/asterix/hyracks/bootstrap/FeedLifecycleListener.java
@@ -1104,7 +1104,7 @@
 
         public void reviveFeed(String dataverse, String feedName, String dataset, String feedPolicy) {
             PrintWriter writer = new PrintWriter(System.out, true);
-            SessionConfig pc = new SessionConfig(true, false, false, false, false, false, true, true, false);
+            SessionConfig conf = new SessionConfig(writer, SessionConfig.OutputFormat.ADM);
             try {
                 DataverseDecl dataverseDecl = new DataverseDecl(new Identifier(dataverse));
                 ConnectFeedStatement stmt = new ConnectFeedStatement(new Identifier(dataverse),
@@ -1113,7 +1113,7 @@
                 List<Statement> statements = new ArrayList<Statement>();
                 statements.add(dataverseDecl);
                 statements.add(stmt);
-                AqlTranslator translator = new AqlTranslator(statements, writer, pc, APIFramework.OutputFormat.ADM);
+                AqlTranslator translator = new AqlTranslator(statements, conf);
                 translator.compileAndExecute(AsterixAppContextInfo.getInstance().getHcc(), null,
                         AqlTranslator.ResultDelivery.SYNC);
                 if (LOGGER.isLoggable(Level.INFO)) {
@@ -1147,7 +1147,7 @@
         private void endFeed(FeedInfo feedInfo) {
             MetadataTransactionContext ctx = null;
             PrintWriter writer = new PrintWriter(System.out, true);
-            SessionConfig pc = new SessionConfig(true, false, false, false, false, false, true, true, false);
+            SessionConfig conf = new SessionConfig(writer, SessionConfig.OutputFormat.ADM);
             try {
                 ctx = MetadataManager.INSTANCE.beginTransaction();
                 DisconnectFeedStatement stmt = new DisconnectFeedStatement(new Identifier(
@@ -1159,7 +1159,7 @@
                         new Identifier(feedInfo.feedConnectionId.getDataverse()));
                 statements.add(dataverseDecl);
                 statements.add(stmt);
-                AqlTranslator translator = new AqlTranslator(statements, writer, pc, APIFramework.OutputFormat.ADM);
+                AqlTranslator translator = new AqlTranslator(statements, conf);
                 translator.compileAndExecute(AsterixAppContextInfo.getInstance().getHcc(), null,
                         AqlTranslator.ResultDelivery.SYNC);
                 if (LOGGER.isLoggable(Level.INFO)) {
diff --git a/asterix-app/src/main/java/edu/uci/ics/asterix/result/ResultUtils.java b/asterix-app/src/main/java/edu/uci/ics/asterix/result/ResultUtils.java
index f558eb6..326697f 100644
--- a/asterix-app/src/main/java/edu/uci/ics/asterix/result/ResultUtils.java
+++ b/asterix-app/src/main/java/edu/uci/ics/asterix/result/ResultUtils.java
@@ -32,7 +32,8 @@
 import org.json.JSONException;
 import org.json.JSONObject;
 
-import edu.uci.ics.asterix.api.common.APIFramework;
+import edu.uci.ics.asterix.api.common.SessionConfig;
+import edu.uci.ics.asterix.api.common.SessionConfig.OutputFormat;
 import edu.uci.ics.asterix.api.http.servlet.APIServlet;
 import edu.uci.ics.asterix.om.types.ARecordType;
 import edu.uci.ics.hyracks.algebricks.common.exceptions.AlgebricksException;
@@ -61,22 +62,29 @@
         return s;
     }
 
-    public static void displayCSVHeader(ARecordType recordType, PrintWriter out) {
+    public static void displayCSVHeader(ARecordType recordType, SessionConfig conf) {
+        // If HTML-ifying, we have to output this here before the header -
+        // pretty ugly
+        if (conf.is(SessionConfig.FORMAT_HTML)) {
+            conf.out().println("<h4>Results:</h4>");
+            conf.out().println("<pre>");
+        }
+
         String[] fieldNames = recordType.getFieldNames();
         boolean notfirst = false;
         for (String name : fieldNames) {
             if (notfirst) {
-                out.print(',');
+                conf.out().print(',');
             }
             notfirst = true;
-            out.print('"');
-            out.print(name.replace("\"", "\"\""));
-            out.print('"');
+            conf.out().print('"');
+            conf.out().print(name.replace("\"", "\"\""));
+            conf.out().print('"');
         }
-        out.print("\r\n");
+        conf.out().print("\r\n");
     }
 
-    public static void displayResults(ResultReader resultReader, PrintWriter out, APIFramework.OutputFormat pdf)
+    public static void displayResults(ResultReader resultReader, SessionConfig conf)
             throws HyracksDataException {
         IFrameTupleAccessor fta = resultReader.getFrameTupleAccessor();
 
@@ -90,11 +98,15 @@
         // Whether this is the first instance being output
         boolean notfirst = false;
 
-        switch (pdf) {
-            case HTML:
-                out.println("<h4>Results:</h4>");
-                out.println("<pre>");
-                // Fall through
+        // If we're outputting CSV with a header, the HTML header was already
+        // output by displayCSVHeader(), so skip it here
+        if (conf.is(SessionConfig.FORMAT_HTML) &&
+            ! (conf.fmt() == OutputFormat.CSV && conf.is(SessionConfig.FORMAT_CSV_HEADER))) {
+            conf.out().println("<h4>Results:</h4>");
+            conf.out().println("<pre>");
+        }
+
+        switch (conf.fmt()) {
             case CSV:
                 need_commas = false;
                 break;
@@ -103,7 +115,7 @@
                 // Conveniently, 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.
-                out.print("[ ");
+                conf.out().print("[ ");
                 break;
         }
 
@@ -119,19 +131,19 @@
                         bbis.setByteBuffer(buffer, start);
                         byte[] recordBytes = new byte[length];
                         int numread = bbis.read(recordBytes, 0, length);
-                        if (pdf == APIFramework.OutputFormat.CSV) {
+                        if (conf.fmt() == OutputFormat.CSV) {
                             if ( (numread > 0) && (recordBytes[numread-1] == '\n') ) {
                                 numread--;
                             }
                         }
                         result = new String(recordBytes, 0, numread, UTF_8);
                         if (need_commas && notfirst) {
-                            out.print(", ");
+                            conf.out().print(", ");
                         }
                         notfirst = true;
-                        out.print(result);
-                        if (pdf == APIFramework.OutputFormat.CSV) {
-                            out.print("\r\n");
+                        conf.out().print(result);
+                        if (conf.fmt() == OutputFormat.CSV) {
+                            conf.out().print("\r\n");
                         }
                     }
                     buffer.clear();
@@ -145,21 +157,21 @@
             } while (resultReader.read(buffer) > 0);
         }
 
-        out.flush();
+        conf.out().flush();
 
-        switch (pdf) {
-            case HTML:
-                out.println("</pre>");
-                break;
+        switch (conf.fmt()) {
             case JSON:
             case ADM:
-                out.println(" ]");
+                conf.out().println(" ]");
                 break;
             case CSV:
                 // Nothing to do
                 break;
         }
 
+        if (conf.is(SessionConfig.FORMAT_HTML)) {
+            conf.out().println("</pre>");
+        }
     }
 
     public static JSONObject getErrorResponse(int errorCode, String errorMessage, String errorSummary,
diff --git a/asterix-app/src/main/resources/webui/querytemplate.html b/asterix-app/src/main/resources/webui/querytemplate.html
index 0adb518..bcd177c 100644
--- a/asterix-app/src/main/resources/webui/querytemplate.html
+++ b/asterix-app/src/main/resources/webui/querytemplate.html
@@ -186,15 +186,23 @@
               <textarea rows="10" id="qry" name="query" class="query" value="%s" placeholder="Type your AQL query ..."></textarea>
             </div>
             
-          <div class="btn-group">
-            <button id="checkboxes-on" class="btn">
-                <i id="opts" class="icon-ok" style="display:none;"></i>Select Options</button>
-            <button id="clear-query-button" class="btn">Clear Query</button>
-            <!-- <button id="checkboxes-off" class="btn">Clear All Options</button> -->
-            <button type="submit" id="run-btn" class="btn btn-custom-darken">Run</button>
-          </div>
+            <div class="btn-group">
+              <button id="checkboxes-on" class="btn">
+                  <i id="opts" class="icon-ok" style="display:none;"></i>Select Options</button>
+              <button id="clear-query-button" class="btn">Clear Query</button>
+              <!-- <button id="checkboxes-off" class="btn">Clear All Options</button> -->
+              <button type="submit" id="run-btn" class="btn btn-custom-darken">Run</button>
+            </div>
 
             <div>
+              <label class="checkbox optlabel"> Output Format:<br/>
+                <select name="output-format" class="btn">
+                  <option selected value="JSON">JSON</option>
+                  <option value="ADM">ADM</option>
+                  <option value="CSV">CSV (no header)</option>
+                  <option value="CSV-Header">CSV (with header)</option>
+                </select>
+              </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>
diff --git a/asterix-app/src/test/resources/runtimets/results/csv/basic-types-header/basic-types.1.csv b/asterix-app/src/test/resources/runtimets/results/csv/basic-types-header/basic-types.1.csv
new file mode 100644
index 0000000..941639e
--- /dev/null
+++ b/asterix-app/src/test/resources/runtimets/results/csv/basic-types-header/basic-types.1.csv
@@ -0,0 +1,2 @@
+"id","name","money"
+12345,Chris,18.25
diff --git a/asterix-app/src/test/resources/runtimets/results/csv/basic-types/basic-types.1.csv b/asterix-app/src/test/resources/runtimets/results/csv/basic-types/basic-types.1.csv
index 941639e..c7fe1a0 100644
--- a/asterix-app/src/test/resources/runtimets/results/csv/basic-types/basic-types.1.csv
+++ b/asterix-app/src/test/resources/runtimets/results/csv/basic-types/basic-types.1.csv
@@ -1,2 +1 @@
-"id","name","money"
 12345,Chris,18.25
diff --git a/asterix-app/src/test/resources/runtimets/testsuite.xml b/asterix-app/src/test/resources/runtimets/testsuite.xml
index 214056f..a22eb35 100644
--- a/asterix-app/src/test/resources/runtimets/testsuite.xml
+++ b/asterix-app/src/test/resources/runtimets/testsuite.xml
@@ -6569,6 +6569,11 @@
                 <output-dir compare="CSV">basic-types</output-dir>
             </compilation-unit>
         </test-case>
+        <test-case FilePath="csv">
+            <compilation-unit name="basic-types">
+                <output-dir compare="CSV_Header">basic-types-header</output-dir>
+            </compilation-unit>
+        </test-case>
     </test-group>
     <test-group name="binary">
         <test-case FilePath="binary">
diff --git a/asterix-common/src/test/java/edu/uci/ics/asterix/test/aql/TestsUtils.java b/asterix-common/src/test/java/edu/uci/ics/asterix/test/aql/TestsUtils.java
index 99afe9a..70e2f6f 100644
--- a/asterix-common/src/test/java/edu/uci/ics/asterix/test/aql/TestsUtils.java
+++ b/asterix-common/src/test/java/edu/uci/ics/asterix/test/aql/TestsUtils.java
@@ -245,7 +245,7 @@
     }
 
     //Executes AQL in either async or async-defer mode.
-    public static InputStream executeAnyAQLAsync(String str, boolean defer) throws Exception {
+    public static InputStream executeAnyAQLAsync(String str, boolean defer, OutputFormat fmt) throws Exception {
         final String url = "http://localhost:19002/aql";
 
         // Create a method instance.
@@ -256,6 +256,7 @@
             method.setQueryString(new NameValuePair[] { new NameValuePair("mode", "asynchronous") });
         }
         method.setRequestEntity(new StringRequestEntity(str));
+        method.setRequestHeader("Accept", fmt.mimeType());
 
         // Provide custom retry handler is necessary
         method.getParams().setParameter(HttpMethodParams.RETRY_HANDLER, new DefaultHttpMethodRetryHandler(3, false));
@@ -265,16 +266,17 @@
         String theHandle = IOUtils.toString(resultStream, "UTF-8");
 
         //take the handle and parse it so results can be retrieved 
-        InputStream handleResult = getHandleResult(theHandle);
+        InputStream handleResult = getHandleResult(theHandle, fmt);
         return handleResult;
     }
 
-    private static InputStream getHandleResult(String handle) throws Exception {
+    private static InputStream getHandleResult(String handle, OutputFormat fmt) throws Exception {
         final String url = "http://localhost:19002/query/result";
 
         // Create a method instance.
         GetMethod method = new GetMethod(url);
         method.setQueryString(new NameValuePair[] { new NameValuePair("handle", handle) });
+        method.setRequestHeader("Accept", fmt.mimeType());
 
         // Provide custom retry handler is necessary
         method.getParams().setParameter(HttpMethodParams.RETRY_HANDLER, new DefaultHttpMethodRetryHandler(3, false));
@@ -417,9 +419,9 @@
                             if (ctx.getType().equalsIgnoreCase("query"))
                                 resultStream = executeQuery(statement, fmt);
                             else if (ctx.getType().equalsIgnoreCase("async"))
-                                resultStream = executeAnyAQLAsync(statement, false);
+                                resultStream = executeAnyAQLAsync(statement, false, fmt);
                             else if (ctx.getType().equalsIgnoreCase("asyncdefer"))
-                                resultStream = executeAnyAQLAsync(statement, true);
+                                resultStream = executeAnyAQLAsync(statement, true, fmt);
 
                             if (queryCount >= expectedResultFileCtxs.size()) {
                                 throw new IllegalStateException("no result file for " + testFile.toString());
diff --git a/asterix-doc/src/site/markdown/csv.md b/asterix-doc/src/site/markdown/csv.md
index 338aa55..6e830f0 100644
--- a/asterix-doc/src/site/markdown/csv.md
+++ b/asterix-doc/src/site/markdown/csv.md
@@ -73,13 +73,9 @@
      ("format"="delimited-text"),
      ("header"="true"));
 
-This is useful when the CSV file was produced from an earlier
-AsterixDB operation, as AsterixDB's CSV output always has a header
-line.
-
 CSV data may also be loaded from HDFS; see [Accessing External
 Data](aql/externaldata.html) for details.  However please note that
-CSV files on HDFS cannot have headers; attempting to specify
+CSV files on HDFS cannot have headers. Attempting to specify
 "header"="true" when reading from HDFS could result in non-header
 lines of data being skipped as well.
 
@@ -139,22 +135,35 @@
 #### Request the CSV Output Format
 
 When sending requests to the Asterix HTTP API, Asterix decides what
-format to use for rendering the results based on the `Accept` HTTP
-header. By default, Asterix will produce JSON output, and this can be
-requested explicitly by specifying the MIME type `application/json`.
-To select CSV output, set the `Accept` header on your request to the
-MIME type `text/csv`. The details of how to accomplish this will of
-course depend on what tools you are using to contact the HTTP API.
-Here is an example from a Unix shell prompt using the command-line
-utility "curl":
+format to use for rendering the results in one of two ways:
 
-    curl -G -H "Accept: text/csv" "http://localhost:19002/query" --data-urlencode '
-        query=use dataverse csv;
+* A HTTP query parameter named "output", which must be set to one of
+  the following values: `JSON`, `CSV`, or `ADM`.
+
+* Based on the [`Accept` HTTP header](http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1)
+
+By default, Asterix will produce JSON output.  To select CSV output,
+pass the parameter `output=CSV`, or set the `Accept` header on your
+request to the MIME type `text/csv`. The details of how to accomplish
+this will of course depend on what tools you are using to contact the
+HTTP API.  Here is an example from a Unix shell prompt using the
+command-line utility "curl" and specifying the "output query parameter:
+
+    curl -G "http://localhost:19002/query" \
+        --data-urlencode 'output=CSV' \
+        --data-urlencode 'query=use dataverse csv;
+              set output-record-type "csv_type";
+              for $n in dataset csv_set return $n;'
+
+Alternately, the same query using the `Accept` header:
+
+    curl -G -H "Accept: text/csv" "http://localhost:19002/query" \
+        --data-urlencode 'query=use dataverse csv;
               set output-record-type "csv_type";
               for $n in dataset csv_set return $n;'
 
 Similarly, a trivial Java program to execute the above sample query
-would be:
+and selecting CSV output via the `Accept` header would be:
 
     import java.net.HttpURLConnection;
     import java.net.URL;
@@ -184,13 +193,22 @@
 
 For either of the above examples, the output would be:
 
-    "id","money","name"
     1,18.5,"Peter Krabnitz"
     2,74.5,"Jesse Stevens"
 
 assuming you had already run the previous examples to create the
 dataverse and populate the dataset.
 
+#### Outputting CSV with a Header
+
+By default, AsterixDB will produce CSV results with no header line.
+If you want a header, you may explicitly request it in one of two ways:
+
+* By passing the HTTP query parameter "header" with the value "present"
+
+* By specifying the MIME type {{text/csv; header=present}} in your
+HTTP Accept: header.  This is consistent with RFC 4180.
+
 #### Issues with open datatypes and optional fields
 
 As mentioned earlier, CSV is a rigid format. It cannot express records
diff --git a/asterix-test-framework/src/main/java/edu/uci/ics/asterix/testframework/context/TestCaseContext.java b/asterix-test-framework/src/main/java/edu/uci/ics/asterix/testframework/context/TestCaseContext.java
index 7589909..6488c95 100644
--- a/asterix-test-framework/src/main/java/edu/uci/ics/asterix/testframework/context/TestCaseContext.java
+++ b/asterix-test-framework/src/main/java/edu/uci/ics/asterix/testframework/context/TestCaseContext.java
@@ -36,7 +36,8 @@
         NONE  ("", ""),
         ADM   ("adm", "application/x-adm"),
         JSON  ("json", "application/json"),
-        CSV   ("csv", "text/csv");
+        CSV   ("csv", "text/csv"),
+        CSV_HEADER ("csv-header", "text/csv; header=present");
 
         private final String extension;
         private final String mimetype;
@@ -62,6 +63,8 @@
                 return OutputFormat.JSON;
             case CSV:
                 return OutputFormat.CSV;
+            case CSV_HEADER:
+                return OutputFormat.CSV_HEADER;
             case INSPECT:
             case IGNORE:
                 return OutputFormat.NONE;
diff --git a/asterix-test-framework/src/main/resources/Catalog.xsd b/asterix-test-framework/src/main/resources/Catalog.xsd
index a33399c..e43acdc 100644
--- a/asterix-test-framework/src/main/resources/Catalog.xsd
+++ b/asterix-test-framework/src/main/resources/Catalog.xsd
@@ -188,6 +188,7 @@
          <xs:enumeration value="Ignore"/>

          <xs:enumeration value="JSON"/>

          <xs:enumeration value="CSV"/>

+         <xs:enumeration value="CSV_Header"/>

       </xs:restriction>

    </xs:simpleType>