You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@qpid.apache.org by or...@apache.org on 2018/02/24 17:58:10 UTC

qpid-broker-j git commit: QPID-8103: [Broker-J] [WMC] [Query UI] Add ability to download results as CSV

Repository: qpid-broker-j
Updated Branches:
  refs/heads/master 51472013e -> 7dbb88471


QPID-8103: [Broker-J] [WMC] [Query UI] Add ability to download results as CSV


Project: http://git-wip-us.apache.org/repos/asf/qpid-broker-j/repo
Commit: http://git-wip-us.apache.org/repos/asf/qpid-broker-j/commit/7dbb8847
Tree: http://git-wip-us.apache.org/repos/asf/qpid-broker-j/tree/7dbb8847
Diff: http://git-wip-us.apache.org/repos/asf/qpid-broker-j/diff/7dbb8847

Branch: refs/heads/master
Commit: 7dbb88471a414d993e4ee3bd3e8fde2f30ca2f46
Parents: 5147201
Author: Alex Rudyy <or...@apache.org>
Authored: Sat Feb 24 02:23:26 2018 +0000
Committer: Alex Rudyy <or...@apache.org>
Committed: Sat Feb 24 17:57:45 2018 +0000

----------------------------------------------------------------------
 .../plugin/servlet/csv/CSVFormat.java           | 320 +++++++++++++++++++
 .../plugin/servlet/rest/AbstractServlet.java    |  23 ++
 .../plugin/servlet/rest/QueryServlet.java       |  58 +++-
 .../plugin/servlet/rest/RestServlet.java        |  22 --
 .../src/main/java/resources/css/common.css      |   4 +
 .../resources/js/qpid/management/Management.js  |  17 +-
 .../js/qpid/management/query/QueryWidget.js     |  23 +-
 .../main/java/resources/query/QueryWidget.html  |   7 +
 .../plugin/servlet/csv/CSVFormatTest.java       |  84 +++++
 9 files changed, 526 insertions(+), 32 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/qpid-broker-j/blob/7dbb8847/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/servlet/csv/CSVFormat.java
----------------------------------------------------------------------
diff --git a/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/servlet/csv/CSVFormat.java b/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/servlet/csv/CSVFormat.java
new file mode 100644
index 0000000..90a5f5f
--- /dev/null
+++ b/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/servlet/csv/CSVFormat.java
@@ -0,0 +1,320 @@
+/*
+ *
+ * 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.qpid.server.management.plugin.servlet.csv;
+
+
+import java.io.IOException;
+import java.util.Collection;
+
+/**
+ * Simplified version of CSVFormat from Apache Commons CSV
+ */
+public final class CSVFormat
+{
+    private static final char COMMA = ',';
+
+    private static final char COMMENT = '#';
+
+    private static final char CR = '\r';
+
+    private static final String CRLF = "\r\n";
+
+    private static final Character DOUBLE_QUOTE_CHAR = '"';
+
+    private static final String EMPTY = "";
+
+    private static final char LF = '\n';
+
+    private static final char SP = ' ';
+
+    private final char _delimiter;
+
+    private final Character _escapeCharacter; // null if escaping is disabled
+
+    private final Character _quoteCharacter; // null if quoting is disabled
+
+    private final String _recordSeparator; // for outputs
+
+    public CSVFormat()
+    {
+        this(COMMA, DOUBLE_QUOTE_CHAR, null, CRLF);
+    }
+
+    /**
+     * Creates a customized CSV format.
+     *
+     * @param delimiter       the char used for value separation, must not be a line break character
+     * @param quoteCharacter  the Character used as value encapsulation marker, may be {@code null} to disable
+     * @param escapeCharacter the Character used to escape special characters in values, may be {@code null} to disable
+     * @param recordSeparator the line separator to use for output
+     * @throws IllegalArgumentException if the _delimiter is a line break character
+     */
+    CSVFormat(final char delimiter,
+              final Character quoteCharacter,
+              final Character escapeCharacter,
+              final String recordSeparator)
+    {
+        if (delimiter == LF || delimiter == CR)
+        {
+            throw new IllegalArgumentException("The _delimiter cannot be a line break");
+        }
+
+        if (quoteCharacter != null && delimiter == quoteCharacter)
+        {
+            throw new IllegalArgumentException(
+                    "The quote character and the delimiter cannot be the same ('" + quoteCharacter + "')");
+        }
+
+        if (escapeCharacter != null && delimiter == escapeCharacter)
+        {
+            throw new IllegalArgumentException(
+                    "The escape character and the delimiter cannot be the same ('" + escapeCharacter + "')");
+        }
+
+        this._delimiter = delimiter;
+        this._quoteCharacter = quoteCharacter;
+        this._escapeCharacter = escapeCharacter;
+        this._recordSeparator = recordSeparator;
+    }
+
+    public <T extends Collection<?>> void printRecord(final Appendable out, final T record) throws IOException
+    {
+        boolean newRecord = true;
+        for (Object item : record)
+        {
+            print(out, item, newRecord);
+            newRecord = false;
+        }
+        println(out);
+    }
+
+    public <C extends Collection<? extends Collection<?>>> void printRecords(final Appendable out, final C records)
+            throws IOException
+    {
+        for (Collection<?> record : records)
+        {
+            printRecord(out, record);
+        }
+    }
+
+
+    public void println(final Appendable out) throws IOException
+    {
+        if (_recordSeparator != null)
+        {
+            out.append(_recordSeparator);
+        }
+    }
+
+    public void print(final Appendable out, final Object value, final boolean newRecord) throws IOException
+    {
+        CharSequence charSequence;
+        if (value == null)
+        {
+            charSequence = EMPTY;
+        }
+        else
+        {
+            charSequence = value instanceof CharSequence ? (CharSequence) value : value.toString();
+        }
+        this.print(out, value, charSequence, 0, charSequence.length(), newRecord);
+    }
+
+
+    public void printComments(final Appendable out,
+                              final String... comments) throws IOException
+    {
+        for (String comment: comments)
+        {
+            out.append(COMMENT).append(SP).append(comment);
+            println(out);
+        }
+    }
+
+    private void print(final Appendable out,
+                       final Object object,
+                       final CharSequence value,
+                       final int offset,
+                       final int len,
+                       final boolean newRecord) throws IOException
+    {
+        if (!newRecord)
+        {
+            out.append(_delimiter);
+        }
+        if (object == null)
+        {
+            out.append(value);
+        }
+        else if (_quoteCharacter != null)
+        {
+            printAndQuote(value, offset, len, out, newRecord);
+        }
+        else if (_escapeCharacter != null)
+        {
+            printAndEscape(out, value, offset, len);
+        }
+        else
+        {
+            out.append(value, offset, offset + len);
+        }
+    }
+
+    private void printAndEscape(final Appendable out,
+                                final CharSequence value,
+                                final int offset,
+                                final int len)
+            throws IOException
+    {
+        int start = offset;
+        int pos = offset;
+        final int end = offset + len;
+
+        final char escape = _escapeCharacter;
+
+        while (pos < end)
+        {
+            char c = value.charAt(pos);
+            if (c == CR || c == LF || c == _delimiter || c == escape)
+            {
+                // write out segment up until this char
+                if (pos > start)
+                {
+                    out.append(value, start, pos);
+                }
+                if (c == LF)
+                {
+                    c = 'n';
+                }
+                else if (c == CR)
+                {
+                    c = 'r';
+                }
+
+                out.append(escape);
+                out.append(c);
+
+                start = pos + 1; // start on the current char after this one
+            }
+
+            pos++;
+        }
+
+        // write last segment
+        if (pos > start)
+        {
+            out.append(value, start, pos);
+        }
+    }
+
+    private void printAndQuote(final CharSequence value, final int offset, final int len,
+                               final Appendable out, final boolean newRecord) throws IOException
+    {
+        boolean quote = false;
+        int start = offset;
+        int pos = offset;
+        final int end = offset + len;
+
+        final char quoteChar = _quoteCharacter;
+
+        if (len <= 0)
+        {
+            // always quote an empty token that is the first
+            // on the line, as it may be the only thing on the
+            // line. If it were not quoted in that case,
+            // an empty line has no tokens.
+            if (newRecord)
+            {
+                quote = true;
+            }
+        }
+        else
+        {
+            char c = value.charAt(pos);
+
+            if (c <= COMMENT)
+            {
+                // Some other chars at the start of a value caused the parser to fail, so for now
+                // encapsulate if we start in anything less than '#'. We are being conservative
+                // by including the default comment char too.
+                quote = true;
+            }
+            else
+            {
+                while (pos < end)
+                {
+                    c = value.charAt(pos);
+                    if (c == LF || c == CR || c == quoteChar || c == _delimiter)
+                    {
+                        quote = true;
+                        break;
+                    }
+                    pos++;
+                }
+
+                if (!quote)
+                {
+                    pos = end - 1;
+                    c = value.charAt(pos);
+                    // Some other chars at the end caused the parser to fail, so for now
+                    // encapsulate if we end in anything less than ' '
+                    if (c <= SP)
+                    {
+                        quote = true;
+                    }
+                }
+            }
+        }
+
+        if (!quote)
+        {
+            // no encapsulation needed - write out the original value
+            out.append(value, start, end);
+            return;
+        }
+
+        // we hit something that needed encapsulation
+        out.append(quoteChar);
+
+        // Pick up where we left off: pos should be positioned on the first character that caused
+        // the need for encapsulation.
+        while (pos < end)
+        {
+            final char c = value.charAt(pos);
+            if (c == quoteChar)
+            {
+                // write out the chunk up until this point
+
+                // add 1 to the length to write out the encapsulator also
+                out.append(value, start, pos + 1);
+                // put the next starting position on the encapsulator so we will
+                // write it out again with the next string (effectively doubling it)
+                start = pos;
+            }
+            pos++;
+        }
+
+        // write the last segment
+        out.append(value, start, pos);
+        out.append(quoteChar);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/qpid-broker-j/blob/7dbb8847/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/servlet/rest/AbstractServlet.java
----------------------------------------------------------------------
diff --git a/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/servlet/rest/AbstractServlet.java b/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/servlet/rest/AbstractServlet.java
index 4403200..76d87f1 100644
--- a/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/servlet/rest/AbstractServlet.java
+++ b/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/servlet/rest/AbstractServlet.java
@@ -22,6 +22,7 @@ package org.apache.qpid.server.management.plugin.servlet.rest;
 
 import static org.apache.qpid.server.management.plugin.HttpManagementUtil.CONTENT_ENCODING_HEADER;
 import static org.apache.qpid.server.management.plugin.HttpManagementUtil.GZIP_CONTENT_ENCODING;
+import static org.apache.qpid.server.management.plugin.HttpManagementUtil.ensureFilenameIsRfc2183;
 
 import java.io.IOException;
 import java.io.OutputStream;
@@ -66,6 +67,11 @@ import org.apache.qpid.server.util.ConnectionScopedRuntimeException;
 public abstract class AbstractServlet extends HttpServlet
 {
     public static final int SC_UNPROCESSABLE_ENTITY = 422;
+    /**
+     * Signifies that the agent wishes the servlet to set the Content-Disposition on the
+     * response with the value attachment.  This filename will be derived from the parameter value.
+     */
+    public static final String CONTENT_DISPOSITION_ATTACHMENT_FILENAME_PARAM = "contentDispositionAttachmentFilename";
     private static final Logger LOGGER = LoggerFactory.getLogger(AbstractServlet.class);
     public static final String CONTENT_DISPOSITION = "Content-Disposition";
 
@@ -158,6 +164,23 @@ public abstract class AbstractServlet extends HttpServlet
         }
     }
 
+    protected void setContentDispositionHeaderIfNecessary(final HttpServletResponse response,
+                                                        final String attachmentFilename)
+    {
+        if (attachmentFilename != null)
+        {
+            String filenameRfc2183 = ensureFilenameIsRfc2183(attachmentFilename);
+            if (filenameRfc2183.length() > 0)
+            {
+                response.setHeader(CONTENT_DISPOSITION, String.format("attachment; filename=\"%s\"", filenameRfc2183));
+            }
+            else
+            {
+                response.setHeader(CONTENT_DISPOSITION, "attachment");  // Agent will allow user to choose a name
+            }
+        }
+    }
+
     protected void doPut(HttpServletRequest req,
                          HttpServletResponse resp,
                          final ConfiguredObject<?> managedObject) throws ServletException, IOException

http://git-wip-us.apache.org/repos/asf/qpid-broker-j/blob/7dbb8847/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/servlet/rest/QueryServlet.java
----------------------------------------------------------------------
diff --git a/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/servlet/rest/QueryServlet.java b/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/servlet/rest/QueryServlet.java
index 2b72295..8ae06f8 100644
--- a/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/servlet/rest/QueryServlet.java
+++ b/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/servlet/rest/QueryServlet.java
@@ -21,6 +21,8 @@
 package org.apache.qpid.server.management.plugin.servlet.rest;
 
 import java.io.IOException;
+import java.io.PrintWriter;
+import java.nio.charset.StandardCharsets;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -33,15 +35,18 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import org.apache.qpid.server.filter.SelectorParsingException;
+import org.apache.qpid.server.management.plugin.servlet.csv.CSVFormat;
 import org.apache.qpid.server.management.plugin.servlet.query.ConfiguredObjectQuery;
 import org.apache.qpid.server.management.plugin.servlet.query.EvaluationException;
 import org.apache.qpid.server.model.ConfiguredObject;
+import org.apache.qpid.server.model.Container;
 import org.apache.qpid.server.model.Model;
 
 public abstract class QueryServlet<X extends ConfiguredObject<?>> extends AbstractServlet
 {
     private static final Logger LOGGER = LoggerFactory.getLogger(QueryServlet.class);
 
+    private static final CSVFormat CSV_FORMAT = new CSVFormat();
 
     @Override
     protected void doGet(HttpServletRequest request,
@@ -78,7 +83,6 @@ public abstract class QueryServlet<X extends ConfiguredObject<?>> extends Abstra
             if (category != null)
             {
                 List<ConfiguredObject<?>> objects = getAllObjects(parent, category, request);
-                Map<String, Object> resultsObject = new LinkedHashMap<>();
 
                 try
                 {
@@ -89,10 +93,26 @@ public abstract class QueryServlet<X extends ConfiguredObject<?>> extends Abstra
                                                                             request.getParameter("limit"),
                                                                             request.getParameter("offset"));
 
-                    resultsObject.put("headers", query.getHeaders());
-                    resultsObject.put("results", query.getResults());
-                    resultsObject.put("total", query.getTotalNumberOfRows());
-                    sendJsonResponse(resultsObject, request, response);
+
+                    String attachmentFilename = request.getParameter(CONTENT_DISPOSITION_ATTACHMENT_FILENAME_PARAM);
+                    if (attachmentFilename != null)
+                    {
+                        setContentDispositionHeaderIfNecessary(response, attachmentFilename);
+                    }
+
+                    if ("csv".equalsIgnoreCase(request.getParameter("format")))
+                    {
+                        sendCsvResponse(categoryName, parent, query, request, response);
+                    }
+                    else
+                    {
+                        Map<String, Object> resultsObject = new LinkedHashMap<>();
+                        resultsObject.put("headers", query.getHeaders());
+                        resultsObject.put("results", query.getResults());
+                        resultsObject.put("total", query.getTotalNumberOfRows());
+
+                        sendJsonResponse(resultsObject, request, response);
+                    }
                 }
                 catch (SelectorParsingException e)
                 {
@@ -125,6 +145,34 @@ public abstract class QueryServlet<X extends ConfiguredObject<?>> extends Abstra
 
     }
 
+    private void sendCsvResponse(final String categoryName,
+                                 final X parent,
+                                 final ConfiguredObjectQuery query,
+                                 final HttpServletRequest request,
+                                 final HttpServletResponse response)
+            throws IOException
+    {
+        response.setStatus(HttpServletResponse.SC_OK);
+        response.setContentType("text/csv;charset=utf-8;");
+        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
+        sendCachingHeadersOnResponse(response);
+        try (PrintWriter writer = response.getWriter())
+        {
+            CSV_FORMAT.printComments(writer,
+                                     String.format("parent : %s %s ",
+                                                   parent.getCategoryClass().getSimpleName(),
+                                                   (parent instanceof Container
+                                                           ? ""
+                                                           : parent.getName())),
+                                     String.format("category : %s", categoryName),
+                                     String.format("select : %s", request.getParameter("select")),
+                                     String.format("where : %s", request.getParameter("where")),
+                                     String.format("order by : %s", request.getParameter("orderBy")));
+            CSV_FORMAT.printRecord(writer, query.getHeaders());
+            CSV_FORMAT.printRecords(writer, query.getResults());
+        }
+    }
+
     abstract protected X getParent(final HttpServletRequest request, final ConfiguredObject<?> managedObject);
 
     abstract protected Class<? extends ConfiguredObject> getSupportedCategory(final String categoryName,

http://git-wip-us.apache.org/repos/asf/qpid-broker-j/blob/7dbb8847/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/servlet/rest/RestServlet.java
----------------------------------------------------------------------
diff --git a/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/servlet/rest/RestServlet.java b/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/servlet/rest/RestServlet.java
index 8361886..4e5e524 100644
--- a/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/servlet/rest/RestServlet.java
+++ b/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/servlet/rest/RestServlet.java
@@ -81,11 +81,6 @@ public class RestServlet extends AbstractServlet
     public static final String EXTRACT_INITIAL_CONFIG_PARAM = "extractInitialConfig";
     public static final String EXCLUDE_INHERITED_CONTEXT_PARAM = "excludeInheritedContext";
     private static final String SINGLETON_MODEL_OBJECT_RESPONSE_AS_LIST = "singletonModelObjectResponseAsList";
-    /**
-     * Signifies that the agent wishes the servlet to set the Content-Disposition on the
-     * response with the value attachment.  This filename will be derived from the parameter value.
-     */
-    public static final String CONTENT_DISPOSITION_ATTACHMENT_FILENAME_PARAM = "contentDispositionAttachmentFilename";
     public static final Set<String> RESERVED_PARAMS =
             new HashSet<>(Arrays.asList(DEPTH_PARAM,
                                         SORT_PARAM,
@@ -313,23 +308,6 @@ public class RestServlet extends AbstractServlet
         return false;
     }
 
-    private void setContentDispositionHeaderIfNecessary(final HttpServletResponse response,
-                                                        final String attachmentFilename)
-    {
-        if (attachmentFilename != null)
-        {
-            String filenameRfc2183 = ensureFilenameIsRfc2183(attachmentFilename);
-            if (filenameRfc2183.length() > 0)
-            {
-                response.setHeader(CONTENT_DISPOSITION, String.format("attachment; filename=\"%s\"", filenameRfc2183));
-            }
-            else
-            {
-                response.setHeader(CONTENT_DISPOSITION, "attachment");  // Agent will allow user to choose a name
-            }
-        }
-    }
-
     private Class<? extends ConfiguredObject> getConfiguredClass(HttpServletRequest request, ConfiguredObject<?> managedObject)
     {
         final String[] servletPathElements = request.getServletPath().split("/");

http://git-wip-us.apache.org/repos/asf/qpid-broker-j/blob/7dbb8847/broker-plugins/management-http/src/main/java/resources/css/common.css
----------------------------------------------------------------------
diff --git a/broker-plugins/management-http/src/main/java/resources/css/common.css b/broker-plugins/management-http/src/main/java/resources/css/common.css
index e4b2511..b2577c7 100644
--- a/broker-plugins/management-http/src/main/java/resources/css/common.css
+++ b/broker-plugins/management-http/src/main/java/resources/css/common.css
@@ -611,6 +611,10 @@ td.advancedSearchField, col.autoWidth {
     background-position: -180px -98px;
 }
 
+.exportIcon.ui-icon {
+    background-position: -112px -112px;
+}
+
 .claro .searchBox {
     padding-right: 16px;
     padding-left: 16px;

http://git-wip-us.apache.org/repos/asf/qpid-broker-j/blob/7dbb8847/broker-plugins/management-http/src/main/java/resources/js/qpid/management/Management.js
----------------------------------------------------------------------
diff --git a/broker-plugins/management-http/src/main/java/resources/js/qpid/management/Management.js b/broker-plugins/management-http/src/main/java/resources/js/qpid/management/Management.js
index 5570636..bf56bf4 100644
--- a/broker-plugins/management-http/src/main/java/resources/js/qpid/management/Management.js
+++ b/broker-plugins/management-http/src/main/java/resources/js/qpid/management/Management.js
@@ -612,17 +612,26 @@ define(["dojo/_base/lang",
         //      Promise returned by dojo.request.xhr with modified then method allowing to use default error handler if none is specified.
         Management.prototype.query = function (query)
         {
-            var url = "api/latest/" + (query.parent && query.parent.type === "virtualhost" ? "queryvhost/"
-                      + this.objectToPath({parent: query.parent}) : "querybroker") + (query.category ? "/"
-                      + query.category : "");
             var request = {
-                url: this.getFullUrl(url),
+                url: this.getQueryUrl(query),
                 query: {}
             };
             shallowCopy(query, request.query, ["parent", "category"]);
             return this.get(request);
         };
 
+        Management.prototype.getQueryUrl = function (query, parameters)
+        {
+            var url = "api/latest/" + (query.parent && query.parent.type === "virtualhost" ? "queryvhost/"
+                      + this.objectToPath({parent: query.parent}) : "querybroker") + (query.category ? "/"
+                      + query.category : "");
+            if (parameters)
+            {
+                url = url + "?" + ioQuery.objectToQuery(parameters);
+            }
+           return this.getFullUrl(url);
+        };
+
         Management.prototype.savePreference = function(parentObject, preference)
         {
            var url =  this.buildPreferenceUrl(parentObject, preference.type);

http://git-wip-us.apache.org/repos/asf/qpid-broker-j/blob/7dbb8847/broker-plugins/management-http/src/main/java/resources/js/qpid/management/query/QueryWidget.js
----------------------------------------------------------------------
diff --git a/broker-plugins/management-http/src/main/java/resources/js/qpid/management/query/QueryWidget.js b/broker-plugins/management-http/src/main/java/resources/js/qpid/management/query/QueryWidget.js
index 4f6dad6..799ec67 100644
--- a/broker-plugins/management-http/src/main/java/resources/js/qpid/management/query/QueryWidget.js
+++ b/broker-plugins/management-http/src/main/java/resources/js/qpid/management/query/QueryWidget.js
@@ -35,6 +35,7 @@ define(["dojo/_base/declare",
         "dgrid/extensions/ColumnHider",
         "qpid/management/query/QueryGrid",
         "qpid/common/MessageDialog",
+        "dojox/uuid/generateRandomUuid",
         "qpid/management/query/DropDownSelect",
         "qpid/management/query/WhereExpression",
         "dijit/_WidgetBase",
@@ -61,7 +62,8 @@ define(["dojo/_base/declare",
               ColumnReorder,
               ColumnHider,
               QueryGrid,
-              MessageDialog)
+              MessageDialog,
+              uuid)
     {
 
         var QueryCloneDialogForm = declare("qpid.management.query.QueryCloneDialogForm",
@@ -183,6 +185,8 @@ define(["dojo/_base/declare",
                 cloneButtonTooltip: null,
                 deleteButtonTooltip: null,
                 searchForm: null,
+                exportButton: null,
+                exportButtonTooltip: null,
 
                 /**
                  * constructor parameter
@@ -241,6 +245,7 @@ define(["dojo/_base/declare",
                     this.saveButton.on("click", lang.hitch(this, this._saveQuery));
                     this.cloneButton.on("click", lang.hitch(this, this._cloneQuery));
                     this.deleteButton.on("click", lang.hitch(this, this._deleteQuery));
+                    this.exportButton.on("click", lang.hitch(this, this._exportQueryResults));
 
                     this._ownQuery = !this.preference
                                      || !this.preference.owner
@@ -248,6 +253,7 @@ define(["dojo/_base/declare",
                     var newQuery = !this.preference || !this.preference.createdDate;
                     this.saveButton.set("disabled", !this._ownQuery);
                     this.deleteButton.set("disabled", !this._ownQuery || newQuery);
+                    this.exportButton.set("disabled", true);
 
                     if (!this._ownQuery)
                     {
@@ -537,6 +543,7 @@ define(["dojo/_base/declare",
                         zeroBased: false,
                         rowsPerPage: rowsPerPage,
                         _currentPage: currentPage,
+                        allowTextSelection: true,
                         transformer: function (data)
                         {
                             var dataResults = data.results;
@@ -585,6 +592,7 @@ define(["dojo/_base/declare",
                 _queryCompleted: function (e)
                 {
                     this._buildColumnsIfHeadersChanged(e.data);
+                    this.exportButton.set("disabled", !(e.data.total && e.data.total > 0));
                 },
                 _buildColumnsIfHeadersChanged: function (data)
                 {
@@ -977,6 +985,19 @@ define(["dojo/_base/declare",
                             this.emit("change", {preference: pref});
                         }
                     }
+                },
+                _exportQueryResults: function () {
+                    var query = this._getQuery();
+                    query.format = "csv";
+                    var id = uuid();
+                    query.contentDispositionAttachmentFilename ="query-results-" + id + ".csv";
+                    delete query.category;
+                    var url = this.management.getQueryUrl({category: this.categoryName, parent: this.parentObject}, query);
+                    var iframe = document.createElement('iframe');
+                    iframe.id = "query_downloader_" + id;
+                    iframe.style.display = "none";
+                    document.body.appendChild(iframe);
+                    iframe.src = url;
                 }
             });
 

http://git-wip-us.apache.org/repos/asf/qpid-broker-j/blob/7dbb8847/broker-plugins/management-http/src/main/java/resources/query/QueryWidget.html
----------------------------------------------------------------------
diff --git a/broker-plugins/management-http/src/main/java/resources/query/QueryWidget.html b/broker-plugins/management-http/src/main/java/resources/query/QueryWidget.html
index ff1b471..0a28eac 100644
--- a/broker-plugins/management-http/src/main/java/resources/query/QueryWidget.html
+++ b/broker-plugins/management-http/src/main/java/resources/query/QueryWidget.html
@@ -40,6 +40,13 @@
         <div data-dojo-attach-point="deleteButtonTooltip"
              data-dojo-type="dijit/Tooltip"
              data-dojo-props="connectId:'deleteButton_${id}',position:['below']">Delete query from preferences and close the tab</div>
+        <div id="exportButton_${id}"
+             data-dojo-type="dijit/form/Button"
+             data-dojo-attach-point="exportButton"
+             data-dojo-props="iconClass: 'exportIcon ui-icon'">Export</div>
+        <div data-dojo-attach-point="exportButtonTooltip"
+             data-dojo-type="dijit/Tooltip"
+             data-dojo-props="connectId:'exportButton_${id}',position:['below']">Export query results into CSV format</div>
         <div data-dojo-type="dijit/form/Button"
              data-dojo-attach-point="modeButton"
              data-dojo-props="iconClass: 'advancedViewIcon ui-icon', title:'Switch to \'Advanced View\' search using SQL-like expressions'"

http://git-wip-us.apache.org/repos/asf/qpid-broker-j/blob/7dbb8847/broker-plugins/management-http/src/test/java/org/apache/qpid/server/management/plugin/servlet/csv/CSVFormatTest.java
----------------------------------------------------------------------
diff --git a/broker-plugins/management-http/src/test/java/org/apache/qpid/server/management/plugin/servlet/csv/CSVFormatTest.java b/broker-plugins/management-http/src/test/java/org/apache/qpid/server/management/plugin/servlet/csv/CSVFormatTest.java
new file mode 100644
index 0000000..4adb0b7
--- /dev/null
+++ b/broker-plugins/management-http/src/test/java/org/apache/qpid/server/management/plugin/servlet/csv/CSVFormatTest.java
@@ -0,0 +1,84 @@
+/*
+ *
+ * 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.qpid.server.management.plugin.servlet.csv;
+
+import java.io.StringWriter;
+import java.util.Arrays;
+
+import org.apache.qpid.test.utils.QpidTestCase;
+
+public class CSVFormatTest extends QpidTestCase
+{
+
+    public void testPrintRecord() throws Exception
+    {
+        CSVFormat csvFormat = new CSVFormat();
+        final StringWriter out = new StringWriter();
+        csvFormat.printRecord(out, Arrays.asList("test", 1, true, "\"quoted\" test"));
+        assertEquals("Unexpected format",
+                     String.format("%s,%d,%b,%s%s", "test", 1, true, "\"\"\"quoted\"\" test\"", "\r\n"),
+                     out.toString());
+    }
+
+    public void testPrintRecords() throws Exception
+    {
+        CSVFormat csvFormat = new CSVFormat();
+        final StringWriter out = new StringWriter();
+        csvFormat.printRecords(out, Arrays.asList(Arrays.asList("test", 1, true, "\"quoted\" test"),
+                                                  Arrays.asList("delimeter,test", 1.0f, false,
+                                                                "quote\" in the middle")));
+        assertEquals("Unexpected format",
+                     String.format("%s,%d,%b,%s%s%s,%s,%b,%s%s", "test", 1, true, "\"\"\"quoted\"\" test\"", "\r\n",
+                                   "\"delimeter,test\"", "1.0", false, "\"quote\"\" in the middle\"", "\r\n"),
+                     out.toString());
+    }
+
+    public void testPrintln() throws Exception
+    {
+        CSVFormat csvFormat = new CSVFormat();
+        final StringWriter out = new StringWriter();
+        csvFormat.println(out);
+        assertEquals("Unexpected new line", "\r\n", out.toString());
+    }
+
+    public void testPrint() throws Exception
+    {
+        CSVFormat csvFormat = new CSVFormat();
+        final StringWriter out = new StringWriter();
+        csvFormat.print(out, "test", true);
+        csvFormat.print(out, 1, false);
+        csvFormat.print(out, true, false);
+        csvFormat.print(out, "\"quoted\" test", false);
+        assertEquals("Unexpected format ",
+                     String.format("%s,%d,%b,%s", "test", 1, true, "\"\"\"quoted\"\" test\""),
+                     out.toString());
+    }
+
+    public void testPrintComments() throws Exception
+    {
+        CSVFormat csvFormat = new CSVFormat();
+        final StringWriter out = new StringWriter();
+        csvFormat.printComments(out, "comment1", "comment2");
+        assertEquals("Unexpected format of comments",
+                     String.format("# %s%s# %s%s", "comment1", "\r\n", "comment2", "\r\n"),
+                     out.toString());
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@qpid.apache.org
For additional commands, e-mail: commits-help@qpid.apache.org