summaryrefslogtreecommitdiff
path: root/qpid/tools/src/java/qpid-qmf2-rest
diff options
context:
space:
mode:
Diffstat (limited to 'qpid/tools/src/java/qpid-qmf2-rest')
-rw-r--r--qpid/tools/src/java/qpid-qmf2-rest/pom.xml42
-rw-r--r--qpid/tools/src/java/qpid-qmf2-rest/src/main/java/org/apache/qpid/restapi/ConnectionProxy.java287
-rw-r--r--qpid/tools/src/java/qpid-qmf2-rest/src/main/java/org/apache/qpid/restapi/ConnectionStore.java138
-rw-r--r--qpid/tools/src/java/qpid-qmf2-rest/src/main/java/org/apache/qpid/restapi/FileServer.java369
-rw-r--r--qpid/tools/src/java/qpid-qmf2-rest/src/main/java/org/apache/qpid/restapi/HttpTransaction.java165
-rw-r--r--qpid/tools/src/java/qpid-qmf2-rest/src/main/java/org/apache/qpid/restapi/JSON.java265
-rw-r--r--qpid/tools/src/java/qpid-qmf2-rest/src/main/java/org/apache/qpid/restapi/JSONMapParser.java402
-rw-r--r--qpid/tools/src/java/qpid-qmf2-rest/src/main/java/org/apache/qpid/restapi/QpidRestAPI.java206
-rw-r--r--qpid/tools/src/java/qpid-qmf2-rest/src/main/java/org/apache/qpid/restapi/QpidServer.java654
-rw-r--r--qpid/tools/src/java/qpid-qmf2-rest/src/main/java/org/apache/qpid/restapi/Server.java95
-rw-r--r--qpid/tools/src/java/qpid-qmf2-rest/src/main/java/org/apache/qpid/restapi/httpserver/Authenticator.java151
-rw-r--r--qpid/tools/src/java/qpid-qmf2-rest/src/main/java/org/apache/qpid/restapi/httpserver/Delegator.java89
-rw-r--r--qpid/tools/src/java/qpid-qmf2-rest/src/main/java/org/apache/qpid/restapi/httpserver/HttpExchangeTransaction.java311
-rw-r--r--qpid/tools/src/java/qpid-qmf2-rest/src/main/java/org/apache/qpid/restapi/servlet/TODO25
14 files changed, 3199 insertions, 0 deletions
diff --git a/qpid/tools/src/java/qpid-qmf2-rest/pom.xml b/qpid/tools/src/java/qpid-qmf2-rest/pom.xml
new file mode 100644
index 0000000000..b0735250d9
--- /dev/null
+++ b/qpid/tools/src/java/qpid-qmf2-rest/pom.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ 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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.apache.qpid</groupId>
+ <artifactId>qpid-qmf2-parent</artifactId>
+ <version>0.32-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>qpid-qmf2-rest</artifactId>
+ <name>Qpid QMF2 REST</name>
+ <description>QMF2 REST API</description>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.qpid</groupId>
+ <artifactId>qpid-qmf2</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ </dependencies>
+
+ <build>
+ </build>
+
+</project>
diff --git a/qpid/tools/src/java/qpid-qmf2-rest/src/main/java/org/apache/qpid/restapi/ConnectionProxy.java b/qpid/tools/src/java/qpid-qmf2-rest/src/main/java/org/apache/qpid/restapi/ConnectionProxy.java
new file mode 100644
index 0000000000..435372a0d6
--- /dev/null
+++ b/qpid/tools/src/java/qpid-qmf2-rest/src/main/java/org/apache/qpid/restapi/ConnectionProxy.java
@@ -0,0 +1,287 @@
+/*
+ *
+ * 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.restapi;
+
+// Misc Imports
+import java.util.TimerTask;
+
+// JMS Imports
+import javax.jms.Connection;
+import javax.jms.ExceptionListener;
+import javax.jms.JMSException;
+
+// Simple Logging Facade 4 Java
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+// QMF2 Imports
+import org.apache.qpid.qmf2.common.BlockingNotifier;
+import org.apache.qpid.qmf2.common.QmfException;
+import org.apache.qpid.qmf2.console.Console;
+import org.apache.qpid.qmf2.util.ConnectionHelper;
+
+/**
+ * Contains a Connection object under a "leasehold agreement" whereby the Connection (and associated Sessions and QMF
+ * Consoles) will expire after a period of time.
+ * <p>
+ * The idea here is to allow a user to create multiple Connection instances (for example to monitor multiple brokers)
+ * but by using the lease metaphor we can expire instances that haven't been used for some predetermined period.
+ * Using the leashold agreement means that we don't have to rely on users explicitly deleting Connections that they
+ * are no longer interested in, because obviously we can't rely on that :-)
+ *
+ * @author Fraser Adams
+ */
+public final class ConnectionProxy extends TimerTask implements ExceptionListener
+{
+ private static final Logger _log = LoggerFactory.getLogger(ConnectionProxy.class);
+
+ private static final int MAX_WORKITEM_QUEUE_SIZE = 20; // Maximum number of items allowed on WorkItem queue.
+
+ // Connections expire after 20 minutes of no use.
+ private static final int TIMEOUT_THRESHOLD = (20*60000)/ConnectionStore.PING_PERIOD;
+
+ // Connections expire after 1 minute if they have never been dereferenced.
+ private static final int UNUSED_THRESHOLD = 60000/ConnectionStore.PING_PERIOD;
+
+ private Connection _connection;
+ private Console _console;
+ private boolean _connected;
+ private int _expireCount;
+ private final ConnectionStore _store;
+ private final String _name;
+ private final String _url;
+ private final String _connectionOptions;
+ private final boolean _disableEvents;
+
+ /**
+ * Actually create the Qpid Connection and QMF2 Console specified in the Constructor.
+ */
+ private synchronized void createConnection()
+ {
+ //System.out.println("ConnectionProxy createConnection() name: " + _name + ", thread: " + Thread.currentThread().getId() + ", creating connection to " + _url + ", options " + _connectionOptions);
+ try
+ {
+ _connection = ConnectionHelper.createConnection(_url, _connectionOptions);
+ if (_connection != null)
+ {
+ _connection.setExceptionListener(this);
+
+ // N.B. creating a Console with a notifier causes the internal WorkQueue to get populated, so care must
+ // be taken to manage its size. In a normal Console application the application would only declare this
+ // if there was an intention to retrieve work items, but in a fairly general REST API we can't guarantee
+ // that clients will. ConsoleLease acts to make the WorkQueue "circular" by deleting items from the
+ // front of the WorkQueue if it exceeds a particular size.
+ if (_disableEvents)
+ {
+ _console = new Console(_name, null, null, null);
+ _console.disableEvents();
+ }
+ else
+ {
+ BlockingNotifier notifier = new BlockingNotifier();
+ _console = new Console(_name, null, notifier, null);
+ }
+ _console.addConnection(_connection);
+ _connected = true;
+ _expireCount = UNUSED_THRESHOLD;
+ notifyAll();
+ }
+ }
+ catch (Exception ex)
+ {
+ _log.info("Exception {} caught in ConnectionProxy constructor.", ex.getMessage());
+ _connected = false;
+ }
+ }
+
+ /**
+ * This method blocks until the Connection has been created.
+ */
+ public synchronized void waitForConnection()
+ {
+ while (!_connected)
+ {
+ try
+ {
+ wait();
+ }
+ catch (InterruptedException ie)
+ {
+ continue;
+ }
+ }
+ }
+
+ /**
+ * This method blocks until the Connection has been created or timeout expires (or wait has been interrupted).
+ * @param timeout the maximum time in milliseconds to wait for notification of the connection's availability.
+ */
+ public synchronized void waitForConnection(long timeout)
+ {
+ try
+ {
+ wait(timeout);
+ }
+ catch (InterruptedException ie)
+ { // Ignore
+ }
+ }
+
+ /**
+ * Construct a Proxy to the specified Qpid Connection with the supplied name to be stored in the specified store.
+ * @param store The ConnectionStore that we want to store this ConnectionProxy in.
+ * @param name A unique name for the Connection that we want to create.
+ * @param url A Connection URL using one of the forms supported by {@link org.apache.qpid.qmf2.util.ConnectionHelper}.
+ * @param connectionOptions A set of connection options in the form supported by {@link org.apache.qpid.qmf2.util.ConnectionHelper}.
+ * @param disableEvents if true create a QMF Console Connection that can only perform synchronous
+ * operations like getObjects() and cannot do asynchronous things like Agent discovery or receive Events.
+ */
+ public ConnectionProxy(final ConnectionStore store, final String name,
+ final String url, final String connectionOptions, final boolean disableEvents)
+ {
+ _connected = false;
+ _store = store;
+ _name = name;
+ _url = url;
+ _connectionOptions = connectionOptions;
+ _disableEvents = disableEvents;
+ }
+
+ /**
+ * The exception listener for the underlying Qpid Connection. This is used to trigger the ConnectionProxy internal
+ * reconnect logic. N.B. ConnectionProxy uses its own reconnection logic for two reasons: firstly the Qpid auto
+ * retry mechanism has some undesireable and unreliable behaviours prior to Qpid version 0.16 and secondly the
+ * Qpid auto retry mechanism is transparent whereas we actually <b>want</b> to detect connection failures in the REST
+ * API so that we can report failures back to the client.
+ * @param jmse The JMSException that has caused onException to be triggered.
+ */
+ public void onException(JMSException jmse)
+ {
+ _log.info("ConnectionProxy onException {}", jmse.getMessage());
+ _connected = false;
+ }
+
+ /**
+ * This method is called periodically by {@link org.apache.qpid.restapi.ConnectionStore} to carry out a number
+ * of housekeeping tasks. It checks if the Qpid Connection is still connected and if not it attempts to reconnect
+ * it also checks whether the Connection "lease" has run out and if it has it tidies up the Connection. Finally
+ * it restricts the size of the QMF2 WorkItem queue as the REST API has no control over whether a client is or
+ * is not interested in being notified of QMF2 Events.
+ */
+ public void run()
+ {
+ if (_connected)
+ {
+ //System.out.println("ConnectionProxy name: " + _name + ", thread: " + Thread.currentThread().getId() + ", WorkItem count = " + _console.getWorkitemCount());
+
+ while (_console.getWorkitemCount() > MAX_WORKITEM_QUEUE_SIZE)
+ {
+ _console.getNextWorkitem();
+ }
+
+ _expireCount--;
+ //System.out.println("ConnectionProxy name: " + _name + ", thread: " + Thread.currentThread().getId() + ", expireCount = " + _expireCount);
+ if (_expireCount == 0)
+ {
+ _store.delete(_name);
+ }
+ }
+ else
+ {
+ createConnection();
+ }
+ }
+
+ /**
+ * Stops scheduled housekeeping, destroys any attached QMF2 Console instances then closes the Qpid Connection.
+ */
+ public synchronized void close()
+ {
+ //System.out.println("ConnectionProxy close() name: " + _name + ", thread: " + Thread.currentThread().getId() + ", expireCount = " + _expireCount);
+
+ cancel();
+
+ try
+ {
+ _console.destroy();
+ _connection.close();
+ }
+ catch (Exception e)
+ { // Log and Ignore
+ _log.info("ConnectionProxy close() caught Exception {}", e.getMessage());
+ }
+ }
+
+ /**
+ * Retrieves the QMF2 Console that we've associated with this Connection.
+ * @return The QMF2 Console that we've associated with this Connection.
+ */
+ public Console getConsole()
+ {
+ _expireCount = TIMEOUT_THRESHOLD;
+ return _console;
+ }
+
+ /**
+ * Returns whether or not the Connection is currently connected to the broker. This is used by the REST API to
+ * tell any clients about the Connection state.
+ * @return true if currently connected or false if not.
+ */
+ public boolean isConnected()
+ {
+ _expireCount = TIMEOUT_THRESHOLD;
+ return _connected;
+ }
+
+ /**
+ * Returns the Connection URL String used to create the Connection.
+ * @return The Connection URL String used to create the Connection.
+ */
+ public String getUrl()
+ {
+ _expireCount = TIMEOUT_THRESHOLD;
+ return _url;
+ }
+
+ /**
+ * Returns the Connection options String used to create the Connection.
+ * @return The Connection options String used to create the Connection.
+ */
+ public String getConnectionOptions()
+ {
+ _expireCount = TIMEOUT_THRESHOLD;
+ return _connectionOptions;
+ }
+
+ /**
+ * Returns a String representation of a ConnectionProxy.
+ * @return The String representation of this ConnectionProxy Object.
+ */
+ @Override
+ public String toString()
+ {
+ // The reason we use JSON.toMap on the string is because it is fairly tolerant and doesn't need pure JSON
+ // if we then call JSON.fromMap we get a pure JSON String.
+ return "{" + "\"url\":\"" + _url + "\",\"connectionOptions\":" +
+ JSON.fromMap(JSON.toMap(_connectionOptions)) + "}";
+ }
+}
+
diff --git a/qpid/tools/src/java/qpid-qmf2-rest/src/main/java/org/apache/qpid/restapi/ConnectionStore.java b/qpid/tools/src/java/qpid-qmf2-rest/src/main/java/org/apache/qpid/restapi/ConnectionStore.java
new file mode 100644
index 0000000000..79b85c9c42
--- /dev/null
+++ b/qpid/tools/src/java/qpid-qmf2-rest/src/main/java/org/apache/qpid/restapi/ConnectionStore.java
@@ -0,0 +1,138 @@
+/*
+ *
+ * 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.restapi;
+
+// Misc Imports
+import java.util.Map;
+import java.util.Timer;
+import java.util.concurrent.ConcurrentHashMap;
+
+// QMF2 Imports
+import org.apache.qpid.qmf2.common.QmfException;
+import org.apache.qpid.qmf2.console.Console;
+
+/**
+ * A ConnectionStore is a container for Qpid Connection Objects, or rather it's a container for ConnectionProxy
+ * Objects which wrap Qpid Connections and provide some additional housekeeping behaviour necessary for a distributed
+ * system. The ConnectionStore schedules regular housekeeping tasks to be executed on the ConnectionProxy Objects
+ * using a java.util.Timer, which scales fairly well.
+ *
+ * @author Fraser Adams
+ */
+public class ConnectionStore
+{
+ /**
+ * This represents the time between "pings" to the stored ConnectionProxy Objects. The pings run scheduled tasks
+ * such as attempting reconnection if the broker has disconnected and checking lease timeouts.
+ */
+ public static final int PING_PERIOD = 5000;
+
+ /**
+ * This Map is used to associate connection names with their ConnectionProxies. Note that the names are prefixed
+ * internally with the authenticated user name to prevent users accidentally (or maliciously) sharing connections.
+ */
+ private Map<String, ConnectionProxy> _connections = new ConcurrentHashMap<String, ConnectionProxy>();
+
+ /**
+ * Create a Timer used to schedule regular checks on ConnectionProxy Objects to see that they are still in use.
+ * In essence ConnectionProxy Objects behave in a similar way to RMI Leases in that if they are not used
+ * (dereferenced) within a particular period it is assumed that the client has lost interest and they are reaped.
+ */
+ private Timer _timer = new Timer(true);
+
+ /**
+ * Creates a new ConnectionProxy Object with the given name, which in turn creates a Qpid Connection using the
+ * supplied Connection URL and options. In addition it schedules some regular housekeeping on the ConnectionProxy
+ * to enable it to manage Connection failures and perform what amounts to distributed garbage collection.
+ * When a ConnectionProxy with a given name has been created it is cached and subsequent calls to this method
+ * will return the cached instance. If an new instance is required one must first call the delete method.
+ * @param name A unique name for the Connection that we want to create.
+ * @param url A Connection URL using one of the forms supported by {@link org.apache.qpid.qmf2.util.ConnectionHelper}.
+ * @param opts A set of connection options in the form supported by {@link org.apache.qpid.qmf2.util.ConnectionHelper}.
+ * @param disableEvents if true create a QMF Console Connection that can only perform synchronous
+ * operations like getObjects() and cannot do asynchronous things like Agent discovery or receive Events.
+ * @return the ConnectionProxy Object created or cached by this method.
+ */
+ public synchronized ConnectionProxy create(final String name, final String url, final String opts,
+ final boolean disableEvents)
+ {
+ ConnectionProxy connection = _connections.get(name);
+ if (connection == null)
+ {
+ connection = new ConnectionProxy(this, name, url, opts, disableEvents);
+ _connections.put(name, connection);
+ _timer.schedule(connection, 0, PING_PERIOD);
+ }
+ return connection;
+ }
+
+ /**
+ * Closes the named Connection, stops its scheduled housekeeping and removes from the store.
+ * @param name the name of the Connection that we want to delete.
+ */
+ public synchronized void delete(final String name)
+ {
+ ConnectionProxy connection = _connections.get(name);
+ if (connection != null)
+ {
+ connection.close();
+ _connections.remove(name);
+ }
+ }
+
+ /**
+ * Retrieves the named Connection from the store.
+ * @param name the name of the Connection that we want to retrieve.
+ * @return the ConnectionProxy instance with the given name.
+ */
+ public ConnectionProxy get(final String name)
+ {
+ return _connections.get(name);
+ }
+
+ /**
+ * Return a Map of ConnectionProxies associated with a given user. Note that this method is fairly inefficient
+ * as the main _connections Map contains all ConnectionProxies and we normally do a lookup by prefixing the
+ * key with the user name in order to demultiplex. It is done this way as we need to look up ConnectionProxy
+ * for each API call, whereas looking up all Connections for a user is likely to be rarely called.
+ * @param user the security principal associated with a particular user.
+ * @return a Map of ConnectionProxy objects associated with the given user.
+ */
+ public Map<String, ConnectionProxy> getAll(final String user)
+ {
+ Map<String, ConnectionProxy> map = new ConcurrentHashMap<String, ConnectionProxy>();
+ String prefix = user + ".";
+ int prefixLength = prefix.length();
+ for (Map.Entry<String, ConnectionProxy> entry : _connections.entrySet())
+ {
+ String key = entry.getKey();
+ if (key.startsWith(prefix))
+ {
+ key = key.substring(prefixLength);
+ ConnectionProxy value = entry.getValue();
+ map.put(key, value);
+ }
+ }
+ return map;
+ }
+}
+
+
diff --git a/qpid/tools/src/java/qpid-qmf2-rest/src/main/java/org/apache/qpid/restapi/FileServer.java b/qpid/tools/src/java/qpid-qmf2-rest/src/main/java/org/apache/qpid/restapi/FileServer.java
new file mode 100644
index 0000000000..2cbb1140b1
--- /dev/null
+++ b/qpid/tools/src/java/qpid-qmf2-rest/src/main/java/org/apache/qpid/restapi/FileServer.java
@@ -0,0 +1,369 @@
+/*
+ *
+ * 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.restapi;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.StringTokenizer;
+
+import static java.net.HttpURLConnection.HTTP_OK;
+import static java.net.HttpURLConnection.HTTP_BAD_METHOD;
+import static java.net.HttpURLConnection.HTTP_PARTIAL;
+import static java.net.HttpURLConnection.HTTP_MOVED_PERM;
+import static java.net.HttpURLConnection.HTTP_FORBIDDEN;
+import static java.net.HttpURLConnection.HTTP_NOT_FOUND;
+import static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR;
+
+/**
+ * FileServer is a fairly simple HTTP File Server that can be used to serve files from the root directory specified
+ * during construction.
+ * <p>
+ * Although this is a relatively simple File Server it is still able to serve large files as it uses streaming, in
+ * addition it uses the HTTP Range/Content-Range/Content-Length Headers to allow resuming of partial downloads
+ * from clients that support it.
+ *
+ * @author Fraser Adams
+ */
+public class FileServer implements Server
+{
+ /**
+ * HashMap mapping file extension to MIME type for common types.
+ * TODO make this set of mappings configurable via properties or similar mechanism.
+ */
+ private static Map<String, String> _mimeTypes = new HashMap<String, String>();
+ static
+ {
+ _mimeTypes.put("htm", "text/html");
+ _mimeTypes.put("html", "text/html");
+ _mimeTypes.put("txt", "text/plain");
+ _mimeTypes.put("asc", "text/plain");
+ _mimeTypes.put("xml", "text/xml");
+ _mimeTypes.put("css", "text/css");
+ _mimeTypes.put("htc", "text/x-component");
+ _mimeTypes.put("gif", "image/gif");
+ _mimeTypes.put("jpg", "image/jpeg");
+ _mimeTypes.put("jpeg", "image/jpeg");
+ _mimeTypes.put("png", "image/png");
+ _mimeTypes.put("ico", "image/x-icon");
+ _mimeTypes.put("mp3", "audio/mpeg");
+ _mimeTypes.put("m3u", "audio/mpeg-url");
+ _mimeTypes.put("js", "application/x-javascript");
+ _mimeTypes.put("pdf", "application/pdf");
+ _mimeTypes.put("doc", "application/msword");
+ _mimeTypes.put("ppt", "application/mspowerpoint");
+ _mimeTypes.put("xls", "application/excel");
+ _mimeTypes.put("ogg", "application/x-ogg");
+ _mimeTypes.put("zip", "application/octet-stream");
+ _mimeTypes.put("exe", "application/octet-stream");
+ _mimeTypes.put("class", "application/octet-stream");
+ }
+
+ private final File _home;
+ private final boolean _allowDirectoryListing;
+
+ /**
+ * URL-encodes everything between "/"-characters. Encodes spaces as '%20' instead of '+'.
+ *
+ * @param uri the uri to be encoded.
+ * @return the encoded uri as a String.
+ */
+ private String encodeUri(final String uri)
+ {
+ StringBuilder encodedUri = new StringBuilder();
+ StringTokenizer st = new StringTokenizer(uri, "/ ", true);
+ while (st.hasMoreTokens())
+ {
+ String tok = st.nextToken();
+ if (tok.equals("/"))
+ {
+ encodedUri.append("/");
+ }
+ else if (tok.equals(" "))
+ {
+ encodedUri.append("%20");
+ }
+ else
+ {
+ try
+ {
+ encodedUri.append(URLEncoder.encode(tok, "UTF-8"));
+ }
+ catch (UnsupportedEncodingException uee)
+ {
+ }
+ }
+ }
+ return encodedUri.toString();
+ }
+
+ /**
+ * Renders a number in a more "human readable" format providing a bytes/KB/MB/GB format depending on the size.
+ *
+ * @param number the number to be rendered.
+ * @return a String representation of the number in a more human readable form.
+ */
+ private String renderNumber(float number)
+ {
+ if (number < 1000)
+ {
+ return String.format("%.0f", number) + " bytes";
+ }
+ else if (number < 1000000)
+ {
+ number /= 1000.0f;
+ return String.format("%.1f", number) + " KB";
+ }
+ else if (number < 1000000000)
+ {
+ number /= 1000000.0f;
+ return String.format("%.1f", number) + " MB";
+ }
+ else
+ {
+ number /= 1000000000.0f;
+ return String.format("%.1f", number) + " GB";
+ }
+ }
+
+ /**
+ * Construct an instance of FileServer.
+ *
+ * @param home the path name of the root directory that we wish this FieServer to serve via HTTP.
+ * @param allowDirectoryListing a flag that if set will serve a directory listing to the client and thus enable
+ * browsing to sub-directories of the home directory. N.B. protection has been put in place to mitigate
+ * against the possibility of serving directories that may be parents of the root directory.
+ */
+ public FileServer(final String home, final boolean allowDirectoryListing)
+ {
+ _home = new File(home);
+ _allowDirectoryListing = allowDirectoryListing;
+ }
+
+ /**
+ * Called by the Web Server to allow a Server to handle a GET request.
+ *
+ * @param tx the HttpTransaction containing the request from the client and used to send the response.
+ */
+ public void doGet(final HttpTransaction tx) throws IOException
+ {
+ String user = tx.getPrincipal() != null ? tx.getPrincipal() : "none";
+ String path = tx.getRequestURI();
+
+ //System.out.println();
+ //System.out.println("FileServer doGet " + path + ", user: " + user);
+ //System.out.println("thread = " + Thread.currentThread().getId());
+ //tx.logRequest();
+
+ // If the _home filesystem that we use as a root to serve files from is not a directory then
+ // we sent an error response and return.
+ if (!_home.isDirectory())
+ {
+ tx.sendResponse(HTTP_INTERNAL_ERROR, "text/plain",
+ "500 Internal Server Error: given document root is not a directory.");
+ return;
+ }
+
+ //String path = tx.getRequestURI();
+
+ // Prohibit getting out of _home directory
+ if (path.startsWith("..") || path.endsWith("..") || path.indexOf("../") >= 0)
+ {
+ tx.sendResponse(HTTP_FORBIDDEN, "text/plain", "403 Forbidden: Won't serve ../ for security reasons.");
+ return;
+ }
+
+ File file = new File(_home, path);
+ if (!file.exists())
+ {
+ tx.sendResponse(HTTP_NOT_FOUND, "text/plain", "404 Not Found: File " + path + " not found.");
+ return;
+ }
+
+ // List the directory, if necessary
+ if (file.isDirectory())
+ {
+ File directory = file;
+ // Browsers get confused without '/' after the directory, send a redirect.
+ if (!path.endsWith("/"))
+ {
+ path += "/";
+ tx.setHeader("Location", path);
+ tx.sendResponse(HTTP_MOVED_PERM, "text/html",
+ "<html><body>Redirected: <a href=\"" + path + "\">" + path + "</a></body></html>");
+ return;
+ }
+
+ // First try index.html and index.htm
+ if (new File(directory, "index.html").exists())
+ {
+ file = new File(_home, path + "/index.html");
+ }
+ else if (new File(directory, "index.htm").exists())
+ {
+ file = new File(_home, path + "/index.htm");
+ }
+ else if (_allowDirectoryListing)
+ { // No index file, list the directory
+ StringBuilder response = new StringBuilder("<html><body><h1>Directory " + path + "</h1><br/>");
+
+ if (path.length() > 1)
+ {
+ String u = path.substring(0, path.length() - 1);
+ int slash = u.lastIndexOf('/');
+ if (slash >= 0 && slash < u.length())
+ {
+ response.append("<b><a href=\"" + path.substring(0, slash + 1) + "\">..</a></b><br/>");
+ }
+ }
+
+ String[] files = directory.list();
+ for (String name : files)
+ {
+ File current = new File(directory, name);
+ boolean isDir = current.isDirectory();
+ boolean isFile = current.isFile();
+ if (isDir)
+ {
+ response.append("<b>");
+ name += "/";
+ }
+
+ response.append("<a href=\"" + encodeUri(path + name) + "\">" + name + "</a>");
+
+ if (isFile)
+ { // If it's a file show the file size
+ response.append(" &nbsp;<font size=2>(" + renderNumber(current.length()) + ")</font>");
+ }
+ response.append("<br/>");
+ if (isDir)
+ {
+ response.append("</b>");
+ }
+ }
+ tx.sendResponse(HTTP_OK, "text/html", response.toString());
+ return;
+ }
+ else
+ {
+ tx.sendResponse(HTTP_FORBIDDEN, "text/plain", "403 Forbidden: No directory listing.");
+ return;
+ }
+ }
+
+ try
+ {
+ // Get MIME type from file name extension, if possible
+ String fileName = file.getCanonicalPath();
+ String mime = null;
+ int dot = fileName.lastIndexOf('.');
+ if (dot >= 0)
+ {
+ String fileExtension = fileName.substring(dot + 1).toLowerCase();
+ mime = _mimeTypes.get(fileExtension);
+ }
+
+ if (mime == null)
+ {
+ mime = "application/octet-stream";
+ }
+
+ // Use Range header allow download resuming.
+ long startFrom = 0;
+ long length = file.length();
+
+ String range = tx.getHeader("Range");
+ if (range != null)
+ {
+ if (range.startsWith("bytes="))
+ {
+ range = range.substring("bytes=".length());
+
+ int minus = range.indexOf('-');
+ if (minus > 0)
+ {
+ range = range.substring(0, minus);
+ }
+
+ try
+ {
+ startFrom = Long.parseLong(range);
+ }
+ catch (NumberFormatException nfe)
+ {
+ }
+ }
+ }
+
+ FileInputStream is = new FileInputStream(file);
+ is.skip(startFrom);
+
+ int status = (startFrom == 0) ? HTTP_OK : HTTP_PARTIAL;
+ tx.setHeader("Content-Length", "" + (length - startFrom));
+ tx.setHeader("Content-Range", "" + startFrom + "-" + (length - 1) + "/" + length);
+
+ tx.sendResponse(status, mime, is);
+ }
+ catch (IOException ioe)
+ {
+ tx.sendResponse(HTTP_FORBIDDEN, "text/plain", "403 Forbidden: Reading file failed.");
+ }
+ }
+
+ /**
+ * Called by the Web Server to allow a Server to handle a POST request.
+ *
+ * @param tx the HttpTransaction containing the request from the client and used to send the response.
+ */
+ public void doPost(final HttpTransaction tx) throws IOException
+ {
+ tx.sendResponse(HTTP_BAD_METHOD, "text/plain", "405 Bad Method.");
+ }
+
+ /**
+ * Called by the Web Server to allow a Server to handle a PUT request.
+ *
+ * @param tx the HttpTransaction containing the request from the client and used to send the response.
+ */
+ public void doPut(final HttpTransaction tx) throws IOException
+ {
+ tx.sendResponse(HTTP_BAD_METHOD, "text/plain", "405 Bad Method.");
+ }
+
+ /**
+ * Called by the Web Server to allow a Server to handle a DELETE request.
+ *
+ * @param tx the HttpTransaction containing the request from the client and used to send the response.
+ */
+ public void doDelete(final HttpTransaction tx) throws IOException
+ {
+ tx.sendResponse(HTTP_BAD_METHOD, "text/plain", "405 Bad Method.");
+ }
+}
+
diff --git a/qpid/tools/src/java/qpid-qmf2-rest/src/main/java/org/apache/qpid/restapi/HttpTransaction.java b/qpid/tools/src/java/qpid-qmf2-rest/src/main/java/org/apache/qpid/restapi/HttpTransaction.java
new file mode 100644
index 0000000000..323ba29861
--- /dev/null
+++ b/qpid/tools/src/java/qpid-qmf2-rest/src/main/java/org/apache/qpid/restapi/HttpTransaction.java
@@ -0,0 +1,165 @@
+/*
+ *
+ * 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.restapi;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * An HttpTransaction encapsulates an HTTP request received and a response to be generated in one HTTP request/response
+ * "transaction". It provides methods for examining the request from the client, and for building and sending the
+ * response from the server.
+ * <p>
+ * Note that the HttpTransaction interface isn't intended to be completely general, rather it is intended to abstract
+ * the services needed in org.apache.qpid.restapi in such a way as to be neutral as to whether the Web Server is
+ * based on com.sun.net.httpserver.HttpServer or javax.servlet.http.HttpServlet.
+ * <p>
+ * The Server and HttpTransaction interfaces are intended to provide abstractions to enable the "business logic" to
+ * be isolated from the actual Web Server implementation choice, so for example a concrete HttpTransaction implementation
+ * could be created by wrapping a com.sun.net.httpserver.HttpExchange, but equally another implementation could wrap
+ * javax.servlet.http.HttpServletRequest and javax.servlet.http.HttpServletResponse so for example an HttpServlet
+ * could delegate to a Server instance passing the HttpTransaction it constructed from the HttpServletRequest and
+ * HttpServletResponse.
+ *
+ * @author Fraser Adams
+ */
+public interface HttpTransaction
+{
+ /**
+ * Log the HTTP request information (primarily for debugging purposes)
+ */
+ public void logRequest();
+
+ /**
+ * Return the content passed in the request from the client as a Stream.
+ * @return the content passed in the request from the client as a Stream.
+ */
+ public InputStream getRequestStream() throws IOException;
+
+ /**
+ * Return the content passed in the request from the client as a String.
+ * @return the content passed in the request from the client as a String.
+ */
+ public String getRequestString() throws IOException;
+
+ /**
+ * Return the content passed in the request from the client as a byte[].
+ * @return the content passed in the request from the client as a byte[].
+ */
+ public byte[] getRequest() throws IOException;
+
+ /**
+ * Send the content passed as a String as an HTTP response back to the client.
+ * @param status the HTTP status code e.g. 200 for OK.
+ * @param mimeType the mimeType of the response content e.g. text/plain, text/xml, image/jpeg etc.
+ * @param content the content of the response passed as a String.
+ */
+ public void sendResponse(final int status, final String mimeType, final String content) throws IOException;
+
+ /**
+ * Send the content passed as a byte[] as an HTTP response back to the client.
+ * @param status the HTTP status code e.g. 200 for OK.
+ * @param mimeType the mimeType of the response content e.g. text/plain, text/xml, image/jpeg etc.
+ * @param content the content of the response passed as a byte[].
+ */
+ public void sendResponse(final int status, final String mimeType, final byte[] content) throws IOException;
+
+ /**
+ * Send the content passed as an InputStream as an HTTP response back to the client.
+ * @param status the HTTP status code e.g. 200 for OK.
+ * @param mimeType the mimeType of the response content e.g. text/plain, text/xml, image/jpeg etc.
+ * @param is the content of the response passed as an InputStream.
+ */
+ public void sendResponse(final int status, final String mimeType, final InputStream is) throws IOException;
+
+ /**
+ * Returns the Internet Protocol (IP) address of the client or last proxy that sent the request.
+ * @return the Internet Protocol (IP) address of the client or last proxy that sent the request.
+ */
+ public String getRemoteAddr();
+
+ /**
+ * Returns the fully qualified name of the client or the last proxy that sent the request.
+ * @return the fully qualified name of the client or the last proxy that sent the request.
+ */
+ public String getRemoteHost();
+
+ /**
+ * Returns the Internet Protocol (IP) source port of the client or last proxy that sent the request.
+ * @return the Internet Protocol (IP) source port of the client or last proxy that sent the request.
+ */
+ public int getRemotePort();
+
+ /**
+ * Returns a String containing the name of the current authenticated user. If the user has not been authenticated,
+ * the method returns null.
+ * @return a String containing the name of the user making this request; null if the user has not been authenticated.
+ */
+ public String getPrincipal();
+
+ /**
+ * Returns the name of the HTTP method with which this request was made, for example, GET, POST, or PUT.
+ * @return a String specifying the name of the method with which this request was made.
+ */
+ public String getMethod();
+
+ /**
+ * Returns the part of this request's URL from the protocol name up to the query string in the first line of
+ * the HTTP request.
+ * @return a String containing the part of the URL from the protocol name up to the query string.
+ */
+ public String getRequestURI();
+
+ /**
+ * Sets a response header with the given name and value. If the header had already been set, the new value
+ * overwrites the previous one.
+ * @param name a String specifying the header name.
+ * @param value a String specifying the header value. If it contains octet string, it should be encoded according
+ * to RFC 2047.
+ */
+ public void setHeader(final String name, final String value);
+
+ /**
+ * Returns the value of the specified request header as a String. If the request did not include a header of the
+ * specified name, this method returns null. If there are multiple headers with the same name, this method returns
+ * the first head in the request. The header name is case insensitive. You can use this method with any request
+ * header.
+ * @param name a String specifying the header name.
+ * @return a String containing the value of the requested header, or null if the request does not have a header of
+ * that name.
+ */
+ public String getHeader(final String name);
+
+ /**
+ * Returns the String value of the specified cookie.
+ * @param name a String specifying the cookie name.
+ */
+ public String getCookie(final String name);
+
+ /**
+ * Adds the specified cookie to the response. This method can be called multiple times to set more than one cookie.
+ * @param name a String specifying the cookie name.
+ * @param value a String specifying the cookie value.
+ */
+ public void addCookie(final String name, final String value);
+}
+
+
diff --git a/qpid/tools/src/java/qpid-qmf2-rest/src/main/java/org/apache/qpid/restapi/JSON.java b/qpid/tools/src/java/qpid-qmf2-rest/src/main/java/org/apache/qpid/restapi/JSON.java
new file mode 100644
index 0000000000..dfc1e0c85d
--- /dev/null
+++ b/qpid/tools/src/java/qpid-qmf2-rest/src/main/java/org/apache/qpid/restapi/JSON.java
@@ -0,0 +1,265 @@
+/*
+ *
+ * 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.restapi;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+// QMF2 imports
+import org.apache.qpid.qmf2.common.Handle;
+import org.apache.qpid.qmf2.common.ObjectId;
+import org.apache.qpid.qmf2.common.QmfData;
+import org.apache.qpid.qmf2.common.QmfDescribed;
+import org.apache.qpid.qmf2.common.SchemaClassId;
+import org.apache.qpid.qmf2.common.WorkItem;
+import org.apache.qpid.qmf2.console.QmfConsoleData;
+
+/**
+ * This class provides a number of convenience methods to serialise and deserialise JSON strings to/from Java
+ * Collections or QmfData objects.
+ *
+ * The JSONMapParser class used here is largely a direct copy of org.apache.qpid.messaging.util.AddressParser
+ * as it provides a handy mechanism to parse a JSON String into a Map which is the only JSON requirement that
+ * we really need for QMF. Originally this code simply did "import org.apache.qpid.messaging.util.AddressParser;"
+ * but there's a restriction/bug on the core AddressParser whereby it serialises integers into Java Integer
+ * which means that long integer values aren't correctly stored. It's this restriction that gives Java Address
+ * Strings a defacto 2GB queue size. I should really provide a patch for the *real* AddressParser but it's better
+ * to add features covering "shorthand" forms for large values (e.g. k/K, m/M, g/G for kilo, mega, giga etc.)
+ * to both the Java and C++ AddressParser to ensure maximum consistency.
+ *
+ * Because the JSON requirements for the REST API are relatively modest a fairly simple serialisation/deserialisation
+ * mechanism is included here and in the modified AddressParser classes rather than incorporating a full-blown
+ * Java JSON parser. This helps avoid a bot of bloat and keeps dependencies limited to the core qpid classes.
+ *
+ * @author Fraser Adams
+ */
+public final class JSON
+{
+ /**
+ * Serialise an Object to JSON. Note this isn't a full JSON serialisation of java.lang.Object, rather it only
+ * includes types that are relevant to QmfData Objects and the types that may be contained therein.
+ * @param item the Object that we wish to serialise to JSON.
+ * @return the JSON String encoding.
+ */
+ public final static String fromObject(final Object item)
+ {
+ if (item == null)
+ {
+ return "";
+ }
+ else
+ {
+ if (item instanceof Map)
+ { // Check if the value part is an ObjectId and serialise appropriately
+ Map map = (Map)item;
+ if (map.containsKey("_object_name"))
+ { // Serialise "ref" properties as String versions of ObjectId to match encoding used in fromQmfData()
+ return "\"" + new ObjectId(map) + "\"";
+ }
+ else
+ {
+ return fromMap(map);
+ }
+ }
+ else if (item instanceof List)
+ {
+ return fromList((List)item);
+ }
+ else if (item instanceof QmfData)
+ {
+ return fromQmfData((QmfData)item);
+ }
+ else if (item instanceof WorkItem)
+ {
+ return fromWorkItem((WorkItem)item);
+ }
+ else if (item instanceof String)
+ {
+ return "\"" + item + "\"";
+ }
+ else if (item instanceof byte[])
+ {
+ return "\"" + new String((byte[])item) + "\"";
+ }
+ else if (item instanceof UUID)
+ {
+ return "\"" + item.toString() + "\"";
+ }
+ else
+ {
+ return item.toString();
+ }
+ }
+ }
+
+ /**
+ * Encode the Map contents so we can use the same code for fromMap and fromQmfData as the latter also needs
+ * to encode _object_id and _schema_id. This returns a String that needs to be topped and tailed with braces.
+ * @param m the Map that we wish to serialise to JSON.
+ * @return the String encoding of the contents.
+ */
+ @SuppressWarnings("unchecked")
+ private final static String encodeMapContents(final Map m)
+ {
+ Map<String, Object> map = (Map<String, Object>)m;
+ StringBuilder buffer = new StringBuilder(512);
+ int size = map.size();
+ int count = 1;
+ for (Map.Entry<String, Object> entry : map.entrySet())
+ {
+ String key = (String)entry.getKey();
+ buffer.append("\"" + key + "\":");
+
+ Object value = entry.getValue();
+ buffer.append(fromObject(value));
+ if (count++ < size)
+ {
+ buffer.append(",");
+ }
+ }
+ return buffer.toString();
+ }
+
+ /**
+ * Serialise a Map to JSON.
+ * @param m the Map that we wish to serialise to JSON.
+ * @return the JSON String encoding.
+ */
+ @SuppressWarnings("unchecked")
+ public final static String fromMap(final Map m)
+ {
+ return "{" + encodeMapContents(m) + "}";
+ }
+
+ /**
+ * Serialise a List to JSON.
+ * @param list the List that we wish to serialise to JSON.
+ * @return the JSON String encoding.
+ */
+ public final static String fromList(final List list)
+ {
+ StringBuilder buffer = new StringBuilder(512);
+ buffer.append("[");
+ int size = list.size();
+ int count = 1;
+ for (Object item : list)
+ {
+ buffer.append(fromObject(item));
+ if (count++ < size)
+ {
+ buffer.append(",");
+ }
+ }
+ buffer.append("]");
+ return buffer.toString();
+ }
+
+ /**
+ * Serialise a QmfData Object to JSON. If the Object is a QmfConsoleData we serialise the ObjectId as a String
+ * which is the same encoding used for the various "ref" properies in fromObject().
+ * @param data the QmfData that we wish to serialise to JSON.
+ * @return the JSON String encoding.
+ */
+ public final static String fromQmfData(final QmfData data)
+ {
+ String consoleDataInfo = "";
+
+ if (data instanceof QmfConsoleData)
+ {
+ QmfConsoleData consoleData = (QmfConsoleData)data;
+ SchemaClassId sid = consoleData.getSchemaClassId();
+ long[] ts = consoleData.getTimestamps();
+
+ String objectId = "\"_object_id\":\"" + consoleData.getObjectId().toString() + "\",";
+ String schemaId = "\"_schema_id\":{" +
+ "\"_package_name\":\"" + sid.getPackageName() +
+ "\",\"_class_name\":\"" + sid.getClassName() +
+ "\",\"_type\":\"" + sid.getType() +
+ "\",\"_hash\":\"" + sid.getHashString() +
+ "\"},";
+
+ String timestamps = "\"_update_ts\":" + ts[0] + "," +
+ "\"_create_ts\":" + ts[1] + "," +
+ "\"_delete_ts\":" + ts[2] + ",";
+
+ consoleDataInfo = objectId + schemaId + timestamps;
+ }
+
+ return "{" + consoleDataInfo + encodeMapContents(data.mapEncode()) + "}";
+ }
+
+ /**
+ * Serialise a WorkItem Object to JSON.
+ * @param data the WorkItem that we wish to serialise to JSON.
+ * @return the JSON String encoding.
+ */
+ public final static String fromWorkItem(final WorkItem data)
+ {
+ // TODO There are a couple of WorkItem types that won't serialise correctly - SubscriptionIndicationWorkItem
+ // and MethodCallWorkItem. Their params require a custom serialiser - though they probably won't be used
+ // from a REST API so they've been parked for now.
+ String type = "\"_type\":\"" + data.getType() + "\",";
+ Handle handle = data.getHandle();
+ String handleString = handle == null ? "" : "\"_handle\":\"" + handle.getCorrelationId() + "\",";
+ String params = "\"_params\":" + fromObject(data.getParams());
+
+ return "{" + type + handleString + params + "}";
+ }
+
+ /**
+ * Create a Map from a JSON String.
+ * The JSONMapParser class used here is largely a direct copy of org.apache.qpid.messaging.util.AddressParser
+ * as it provides a handy mechanism to parse a JSON String into a Map which is the only JSON requirement that
+ * we really need for QMF. Originally this code simply did "import org.apache.qpid.messaging.util.AddressParser;"
+ * but there's a restriction/bug on the core AddressParser whereby it serialises integers into Java Integer
+ * which means that long integer values aren't correctly stored. It's this restriction that gives Java Address
+ * Strings a defacto 2GB queue size. I should really provide a patch for the *real* AddressParser but it's better
+ * to add features covering "shorthand" forms for large values (e.g. k/K, m/M, g/G for kilo, mega, giga etc.)
+ * to both the Java and C++ AddressParser to ensure maximum consistency.
+ * @param json the JSON String that we wish to decode into a Map.
+ * @return the Map encoding of the JSON String.
+ */
+ public final static Map toMap(final String json)
+ {
+ if (json == null || json.equals(""))
+ {
+ return Collections.EMPTY_MAP;
+ }
+ else
+ {
+ return new JSONMapParser(json).map();
+ }
+ }
+
+ /**
+ * Create a QmfData from a JSON String.
+ * @param json the JSON String that we wish to decode into a QmfData.
+ * @return the QmfData encoding of the JSON String.
+ */
+ public final static QmfData toQmfData(final String json)
+ {
+ return new QmfData(toMap(json));
+ }
+}
+
+
diff --git a/qpid/tools/src/java/qpid-qmf2-rest/src/main/java/org/apache/qpid/restapi/JSONMapParser.java b/qpid/tools/src/java/qpid-qmf2-rest/src/main/java/org/apache/qpid/restapi/JSONMapParser.java
new file mode 100644
index 0000000000..9a6354e099
--- /dev/null
+++ b/qpid/tools/src/java/qpid-qmf2-rest/src/main/java/org/apache/qpid/restapi/JSONMapParser.java
@@ -0,0 +1,402 @@
+/*
+ *
+ * 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.restapi;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.qpid.messaging.Address;
+import org.apache.qpid.messaging.util.Lexer;
+import org.apache.qpid.messaging.util.Lexicon;
+import org.apache.qpid.messaging.util.ParseError;
+import org.apache.qpid.messaging.util.Token;
+
+/**
+ * AddressParser
+ *
+ * This JSONMapParser class used is mostly just a direct copy of org.apache.qpid.messaging.util.AddressParser
+ * as it provides a handy mechanism to parse a JSON String into a Map which is the only JSON requirement that
+ * we really need for QMF.
+ *
+ * Unfortunately there's a restriction/bug on the core AddressParser whereby it serialises integers into Java Integer
+ * which means that long integer values aren't correctly stored. It's this restriction that gives Java Address
+ * Strings a defacto 2GB queue size. I should really provide a patch for the *real* AddressParser but it's better
+ * to add features covering "shorthand" forms for large values (e.g. k/K, m/M, g/G for kilo, mega, giga etc.)
+ * to both the Java and C++ AddressParser to ensure maximum consistency.
+ *
+ * This AddressParser clone largely uses the classes from org.apache.qpid.messaging.util like the real AddressParser
+ * but unfortunately the Parser class was package scope rather than public, so I've done some "copy and paste reuse"
+ * to add the Parser methods into this version of AddressParser. I've also removed the bits that actually create
+ * an Address as all we need to do is to parse into a java.util.Map.
+ */
+
+public class JSONMapParser
+{
+
+ private static Lexicon lxi = new Lexicon();
+
+ private static Token.Type LBRACE = lxi.define("LBRACE", "\\{");
+ private static Token.Type RBRACE = lxi.define("RBRACE", "\\}");
+ private static Token.Type LBRACK = lxi.define("LBRACK", "\\[");
+ private static Token.Type RBRACK = lxi.define("RBRACK", "\\]");
+ private static Token.Type COLON = lxi.define("COLON", ":");
+ private static Token.Type SEMI = lxi.define("SEMI", ";");
+ private static Token.Type SLASH = lxi.define("SLASH", "/");
+ private static Token.Type COMMA = lxi.define("COMMA", ",");
+ private static Token.Type NUMBER = lxi.define("NUMBER", "[+-]?[0-9]*\\.?[0-9]+");
+ // Make test for true and false case insensitive. N.B. org.apache.qpid.messaging.util.AddressParser test is
+ // case sensitive - not sure if that's a bug/oversight in the AddressParser??
+ private static Token.Type TRUE = lxi.define("TRUE", "(?i)True");
+ private static Token.Type FALSE = lxi.define("FALSE", "(?i)False");
+ private static Token.Type ID = lxi.define("ID", "[a-zA-Z_](?:[a-zA-Z0-9_-]*[a-zA-Z0-9_])?");
+ private static Token.Type STRING = lxi.define("STRING", "\"(?:[^\\\"]|\\.)*\"|'(?:[^\\']|\\.)*'");
+ private static Token.Type ESC = lxi.define("ESC", "\\\\[^ux]|\\\\x[0-9a-fA-F][0-9a-fA-F]|\\\\u[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]");
+ private static Token.Type SYM = lxi.define("SYM", "[.#*%@$^!+-]");
+ private static Token.Type WSPACE = lxi.define("WSPACE", "[\\s]+");
+ private static Token.Type EOF = lxi.eof("EOF");
+
+ private static Lexer LEXER = lxi.compile();
+
+/********** Copied from org.apache.qpid.messaging.util.Parser as Parser was package scope **********/
+
+ private List<Token> tokens;
+ private int idx = 0;
+
+ Token next()
+ {
+ return tokens.get(idx);
+ }
+
+ boolean matches(Token.Type ... types)
+ {
+ for (Token.Type t : types)
+ {
+ if (next().getType() == t)
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ Token eat(Token.Type ... types)
+ {
+ if (types.length > 0 && !matches(types))
+ {
+ throw new ParseError(next(), types);
+ }
+ else
+ {
+ Token t = next();
+ idx += 1;
+ return t;
+ }
+ }
+
+/***************************************************************************************************/
+
+ public static List<Token> lex(String input)
+ {
+ return LEXER.lex(input);
+ }
+
+ static List<Token> wlex(String input)
+ {
+ List<Token> tokens = new ArrayList<Token>();
+ for (Token t : lex(input))
+ {
+ if (t.getType() != WSPACE)
+ {
+ tokens.add(t);
+ }
+ }
+ return tokens;
+ }
+
+ static String unquote(String st, Token tok)
+ {
+ StringBuilder result = new StringBuilder();
+ for (int i = 1; i < st.length() - 1; i++)
+ {
+ char ch = st.charAt(i);
+ if (ch == '\\')
+ {
+ char code = st.charAt(i+1);
+ switch (code)
+ {
+ case '\n':
+ break;
+ case '\\':
+ result.append('\\');
+ break;
+ case '\'':
+ result.append('\'');
+ break;
+ case '"':
+ result.append('"');
+ break;
+ case 'a':
+ result.append((char) 0x07);
+ break;
+ case 'b':
+ result.append((char) 0x08);
+ break;
+ case 'f':
+ result.append('\f');
+ break;
+ case 'n':
+ result.append('\n');
+ break;
+ case 'r':
+ result.append('\r');
+ break;
+ case 't':
+ result.append('\t');
+ break;
+ case 'u':
+ result.append(decode(st.substring(i+2, i+6)));
+ i += 4;
+ break;
+ case 'v':
+ result.append((char) 0x0b);
+ break;
+ case 'o':
+ result.append(decode(st.substring(i+2, i+4), 8));
+ i += 2;
+ break;
+ case 'x':
+ result.append(decode(st.substring(i+2, i+4)));
+ i += 2;
+ break;
+ default:
+ throw new ParseError(tok);
+ }
+ i += 1;
+ }
+ else
+ {
+ result.append(ch);
+ }
+ }
+
+ return result.toString();
+ }
+
+ static char[] decode(String hex)
+ {
+ return decode(hex, 16);
+ }
+
+ static char[] decode(String code, int radix)
+ {
+ return Character.toChars(Integer.parseInt(code, radix));
+ }
+
+ /**
+ * This method is the the main place where this class differs from org.apache.qpid.messaging.util.AddressParser.
+ * If the token type is a STRING it checks for a number (with optional floating point) ending in K, M or G
+ * and if it is of this type it creates a Long out of the float value multiplied by 1000, 1000000 or 1000000000.
+ * If the token type is a NUMBER it tries to parse into an Integer like AddressParser, but if that fails it
+ * tries to parse onto a Long which allows much larger integer values to be used.
+ */
+ static Object tok2obj(Token tok)
+ {
+ Token.Type type = tok.getType();
+ String value = tok.getValue();
+ if (type == STRING)
+ {
+ value = unquote(value, tok);
+
+ // Initial regex to check for a number (with optional floating point) ending in K, M or G
+ if (value.matches("([0-9]*\\.[0-9]+|[0-9]+)\\s*[kKmMgG]"))
+ {
+ // If it's a numeric string perform the relevant multiplication and return as a Long.
+ int length = value.length();
+ if (length > 1)
+ {
+ String end = value.substring(length - 1, length).toUpperCase();
+ String start = value.substring(0, length - 1).trim();
+
+ if (end.equals("K"))
+ {
+ return Long.valueOf((long)(Float.parseFloat(start) * 1000.0));
+ }
+ else if (end.equals("M"))
+ {
+ return Long.valueOf((long)(Float.parseFloat(start) * 1000000.0));
+ }
+ else if (end.equals("G"))
+ {
+ return Long.valueOf((long)(Float.parseFloat(start) * 1000000000.0));
+ }
+ }
+
+ return value;
+ }
+ else
+ {
+ return value;
+ }
+ }
+ else if (type == NUMBER)
+ {
+ // This block extends the original AddressParser handling of NUMBER. It first attempts to parse the String
+ // into an Integer in order to be backwards compatible with AddressParser however if this causes a
+ // NumberFormatException it then attempts to parse into a Long.
+ if (value.indexOf('.') >= 0)
+ {
+ return Double.valueOf(value);
+ }
+ else
+ {
+ try
+ {
+ return Integer.decode(value);
+ }
+ catch (NumberFormatException nfe)
+ {
+ return Long.decode(value);
+ }
+ }
+ }
+ else if (type == TRUE)
+ {
+ return true;
+ }
+ else if (type == FALSE)
+ {
+ return false;
+ }
+ else
+ {
+ return value;
+ }
+ }
+
+ public JSONMapParser(String input)
+ {
+ this.tokens = wlex(input); // Copied from org.apache.qpid.messaging.util.Parser
+ this.idx = 0; // Copied from org.apache.qpid.messaging.util.Parser
+ }
+
+ public Map<Object,Object> map()
+ {
+ eat(LBRACE);
+
+ Map<Object,Object> result = new HashMap<Object,Object>();
+ while (true)
+ {
+ if (matches(NUMBER, STRING, ID, LBRACE, LBRACK))
+ {
+ keyval(result);
+ if (matches(COMMA))
+ {
+ eat(COMMA);
+ }
+ else if (matches(RBRACE))
+ {
+ break;
+ }
+ else
+ {
+ throw new ParseError(next(), COMMA, RBRACE);
+ }
+ }
+ else if (matches(RBRACE))
+ {
+ break;
+ }
+ else
+ {
+ throw new ParseError(next(), NUMBER, STRING, ID, LBRACE, LBRACK,
+ RBRACE);
+ }
+ }
+
+ eat(RBRACE);
+ return result;
+ }
+
+ void keyval(Map<Object,Object> map)
+ {
+ Object key = value();
+ eat(COLON);
+ Object val = value();
+ map.put(key, val);
+ }
+
+ Object value()
+ {
+ if (matches(NUMBER, STRING, ID, TRUE, FALSE))
+ {
+ return tok2obj(eat());
+ }
+ else if (matches(LBRACE))
+ {
+ return map();
+ }
+ else if (matches(LBRACK))
+ {
+ return list();
+ }
+ else
+ {
+ throw new ParseError(next(), NUMBER, STRING, ID, LBRACE, LBRACK);
+ }
+ }
+
+ List<Object> list()
+ {
+ eat(LBRACK);
+
+ List<Object> result = new ArrayList<Object>();
+
+ while (true)
+ {
+ if (matches(RBRACK))
+ {
+ break;
+ }
+ else
+ {
+ result.add(value());
+ if (matches(COMMA))
+ {
+ eat(COMMA);
+ }
+ else if (matches(RBRACK))
+ {
+ break;
+ }
+ else
+ {
+ throw new ParseError(next(), COMMA, RBRACK);
+ }
+ }
+ }
+
+ eat(RBRACK);
+ return result;
+ }
+
+}
diff --git a/qpid/tools/src/java/qpid-qmf2-rest/src/main/java/org/apache/qpid/restapi/QpidRestAPI.java b/qpid/tools/src/java/qpid-qmf2-rest/src/main/java/org/apache/qpid/restapi/QpidRestAPI.java
new file mode 100644
index 0000000000..b7ff7d9cfe
--- /dev/null
+++ b/qpid/tools/src/java/qpid-qmf2-rest/src/main/java/org/apache/qpid/restapi/QpidRestAPI.java
@@ -0,0 +1,206 @@
+/*
+ *
+ * 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.restapi;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.util.concurrent.Executors;
+import java.util.List;
+
+import com.sun.net.httpserver.HttpServer;
+
+import org.apache.qpid.qmf2.util.GetOpt;
+
+import org.apache.qpid.restapi.httpserver.Authenticator;
+import org.apache.qpid.restapi.httpserver.Delegator;
+
+/**
+ * Note QpidRestAPI makes use of the Java 1.6 "Easter Egg" HttpServer and associated classes to create a simple
+ * HTTP REST interface to Qpid and QMF2 functionality.
+ * <p>
+ * Because HttpServer is in the com.sun.net.httpserver.HttpServer namespace it is technically not part of core Java
+ * and so may not be supported in all JREs, however it seems to be present in the OpenJDK Runtime Environment that is
+ * installed on many Linux variants. It is documented here <a href="http://docs.oracle.com/javase/6/docs/jre/api/net/httpserver/spec/overview-summary.html">com.sun.net.httpserver</a>
+ * <p>
+ * The reason for choosing com.sun.net.httpserver.HttpServer is simply in order to provide a very lightweight and
+ * dependency free approach to creating an HTTP Proxy. Clearly other approaches such as full-blown Servlet containers
+ * and the like might provide a better mechanism for some environments, but HttpServer really is very simple to use.
+ * <p>
+ * If there is a desire to use Servlets rather than HttpServer it should be fairly straightforward as two interfaces
+ * have been provided (Server and HttpTransaction) that abstract the key behaviour, so for example a concrete HttpTransaction
+ * implementation could be created by wrapping a com.sun.net.httpserver.HttpExchange, but equally another implementation
+ * could wrap javax.servlet.http.HttpServletRequest and javax.servlet.http.HttpServletResponse, so for example an
+ * HttpServlet could delegate to a Server instance passing the Conversation it constructed from the HttpServletRequest
+ * and HttpServletResponse in a similar way that our Delegator implementation of HttpHandler delegates to the Servers.
+ *
+ * <pre>
+ * Usage: QpidRestAPI [options]
+ *
+ * Options:
+ * -h, --help
+ * show this help message and exit
+ * -a &lt;address&gt;, --broker-addr=&lt;address&gt;
+ * broker-addr is in the form: [username/password@]
+ * hostname | ip-address [:&lt;port&gt;] ex: localhost,
+ * 10.1.1.7:10000, broker-host:10000,
+ * guest/guest@localhost
+ * Default is the host QpidRestAPI runs on &amp; port 5672.
+ * -i &lt;address&gt;, --addr=&lt;address&gt;
+ * the hostname of the QpidRestAPI default is the wildcard address
+ * (Bind to a specific address on a multihomed host)
+ * -p &lt;port&gt;, --port=&lt;port&gt;
+ * the port the QpidRestAPI is bound to default is 8080
+ * -b &lt;backlog&gt;, --backlog=&lt;backlog&gt;
+ * the socket backlog default is 10
+ * -w &lt;directory&gt;, --webroot=&lt;directory&gt;
+ * the directory of the QpidRestAPI Web Site default is qpid-web
+ * </pre>
+ * @author Fraser Adams
+ */
+public class QpidRestAPI
+{
+ private static final String _usage =
+ "Usage: QpidRestAPI [options]\n";
+
+ private static final String _description =
+ "Creates an HTTP REST interface to enable us to send messages to Qpid brokers and use QMF2 via HTTP.\n";
+
+ private static final String _options =
+ "Options:\n" +
+ " -h, --help show this help message and exit.\n" +
+ " -a <address>, --broker-addr=<address>\n" +
+ " broker-addr is in the form: [username/password@]\n" +
+ " hostname | ip-address [:&lt;port&gt;] e.g.\n" +
+ " localhost, 10.1.1.7:10000, broker-host:10000,\n" +
+ " guest/guest@localhost, guest/guest@broker-host:10000\n" +
+ " default is the host QpidRestAPI runs on & port 5672.\n" +
+ " -i <address>, --addr=<address>\n" +
+ " the hostname of the QpidRestAPI.\n" +
+ " default is the wildcard address.\n" +
+ " (Bind to a specific address on a multihomed host).\n" +
+ " -p <port>, --port=<port>\n" +
+ " the port the QpidRestAPI is bound to.\n" +
+ " default is 8080.\n" +
+ " -b <backlog>, --backlog=<backlog>\n" +
+ " the socket backlog.\n" +
+ " default is 10\n" +
+ " -w <directory>, --webroot=<directory>\n" +
+ " the directory of the QpidRestAPI Web Site.\n" +
+ " default is qpid-web.\n";
+
+
+ /**
+ * Construct and start an instance of QpidRestAPI. This class used a Delegator class to delegate to underlying
+ * Server instances that actually implement the business logic of the REST API.
+ *
+ * @param addr the the address the QpidRestAPI is bound to (null = default).
+ * @param port the port the QpidRestAPI is bound to.
+ * @param broker the address of the Qpid broker to connect to (null = default).
+ * @param backlog the socket backlog.
+ * @param webroot the directory of the QpidRestAPI Web Site.
+ */
+ public QpidRestAPI(final String addr, final int port, String broker, final int backlog, final String webroot)
+ throws IOException
+ {
+ final InetSocketAddress inetaddr = (addr == null) ? new InetSocketAddress(port) :
+ new InetSocketAddress(addr, port);
+ final HttpServer server = HttpServer.create(inetaddr, backlog);
+
+ broker = (broker == null) ? inetaddr.getAddress().getHostAddress() + ":5672" : broker;
+
+ Delegator fileserver = new Delegator(new FileServer(webroot + "/web", true));
+ Delegator qpidserver = new Delegator(new QpidServer(broker));
+
+ Authenticator authenticator = new Authenticator(this.getClass().getCanonicalName(), webroot + "/authentication");
+
+ server.setExecutor(Executors.newCachedThreadPool());
+ server.createContext("/", fileserver);
+ server.createContext("/ui", fileserver).setAuthenticator(authenticator);
+ server.createContext("/qpid/connection", qpidserver).setAuthenticator(authenticator);
+ server.start();
+ }
+
+ /**
+ * Runs QpidRestAPI.
+ * @param args the command line arguments.
+ */
+ public static void main(String[] args) throws IOException
+ {
+ String logLevel = System.getProperty("amqj.logging.level");
+ logLevel = (logLevel == null) ? "FATAL" : logLevel; // Set default log level to FATAL rather than DEBUG.
+ System.setProperty("amqj.logging.level", logLevel);
+
+ String[] longOpts = {"help", "host=", "port=", "backlog=", "webroot="};
+ try
+ {
+ String addr = null;
+ int port = 8080;
+ String broker = null;
+ int backlog = 10;
+ String webroot = "qpid-web";
+
+ GetOpt getopt = new GetOpt(args, "ha:i:p:b:w:", longOpts);
+ List<String[]> optList = getopt.getOptList();
+ String[] cargs = {};
+ cargs = getopt.getEncArgs().toArray(cargs);
+
+ for (String[] opt : optList)
+ {
+ if (opt[0].equals("-h") || opt[0].equals("--help"))
+ {
+ System.out.println(_usage);
+ System.out.println(_description);
+ System.out.println(_options);
+ System.exit(1);
+ }
+ else if (opt[0].equals("-a") || opt[0].equals("--broker-addr"))
+ {
+ broker = opt[1];
+ }
+ else if (opt[0].equals("-i") || opt[0].equals("--addr"))
+ {
+ addr = opt[1];
+ }
+ else if (opt[0].equals("-p") || opt[0].equals("--port"))
+ {
+ port = Integer.parseInt(opt[1]);
+ }
+ else if (opt[0].equals("-b") || opt[0].equals("--backlog"))
+ {
+ backlog = Integer.parseInt(opt[1]);
+ }
+ else if (opt[0].equals("-w") || opt[0].equals("--webroot"))
+ {
+ webroot = opt[1];
+ }
+ }
+
+ QpidRestAPI restAPI = new QpidRestAPI(addr, port, broker, backlog, webroot);
+ }
+ catch (IllegalArgumentException e)
+ {
+ System.out.println(_usage);
+ System.out.println(e.getMessage());
+ System.exit(1);
+ }
+ }
+}
+
diff --git a/qpid/tools/src/java/qpid-qmf2-rest/src/main/java/org/apache/qpid/restapi/QpidServer.java b/qpid/tools/src/java/qpid-qmf2-rest/src/main/java/org/apache/qpid/restapi/QpidServer.java
new file mode 100644
index 0000000000..f1bdcf85bc
--- /dev/null
+++ b/qpid/tools/src/java/qpid-qmf2-rest/src/main/java/org/apache/qpid/restapi/QpidServer.java
@@ -0,0 +1,654 @@
+/*
+ *
+ * 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.restapi;
+
+// Simple Logging Facade 4 Java
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+// Misc Imports
+import java.io.IOException;
+import java.net.InetAddress;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+// QMF2 Imports
+import org.apache.qpid.qmf2.common.ObjectId;
+import org.apache.qpid.qmf2.common.QmfData;
+import org.apache.qpid.qmf2.common.QmfException;
+import org.apache.qpid.qmf2.console.Agent;
+import org.apache.qpid.qmf2.console.Console;
+import org.apache.qpid.qmf2.console.MethodResult;
+import org.apache.qpid.qmf2.console.QmfConsoleData;
+
+import static java.net.HttpURLConnection.HTTP_OK;
+import static java.net.HttpURLConnection.HTTP_BAD_METHOD;
+import static java.net.HttpURLConnection.HTTP_CREATED;
+import static java.net.HttpURLConnection.HTTP_NOT_FOUND;
+import static java.net.HttpURLConnection.HTTP_NOT_IMPLEMENTED;
+import static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR;
+
+
+/**
+ * This class implements the REST API proper, it is an implementation of the Server interface which allows us to
+ * abstract the "business logic" from the Web Server implementation technology (HttpServer/Servlet etc.).
+ * <p>
+ * The REST API is as follows:
+ * <pre>
+ * PUT: &lt;host&gt;:&lt;port&gt;/qpid/connection/&lt;name&gt;
+ * HTTP body: {"url":&lt;url&gt;,"connectionOptions":&lt;connectionOptions&gt;[,"disableEvents":true;]}
+ * &lt;url&gt;: A string containing an AMQP connection URL as used in the qpid::messaging API.
+ * &lt;connectionOptions&gt;: A JSON string containing connectionOptions in the form specified in the
+ * qpid::messaging API.
+ *
+ * This method creates a Qpid Connection Object with the name &lt;name&gt; using the specified url and options.
+ *
+ * The optional disableEvents property is used to start up a QMF Connection which can only
+ * do synchronous calls such as getObjects() and can't receive Agent updates or QMF2 Events.
+ *
+ * DELETE: &lt;host&gt;:&lt;port&gt;/qpid/connection/&lt;name&gt;
+ *
+ * This method deletes the Qpid Connection Object with the name &lt;name&gt;.
+ *
+ * POST: &lt;host&gt;:&lt;port&gt;/qpid/connection/&lt;name&gt;/object/&lt;ObjectId&gt;
+ * HTTP body: {"_method_name":&lt;method&gt;,"_arguments":&lt;inArgs&gt;}
+ * &lt;method&gt;: A string containing the QMF2 method name e.g. "getLogLevel", "setLogLevel", "create", "delete".
+ * &lt;inArgs&gt;: A JSON string containing the method arguments e.g. {"level":"debug+:Broker"} for setLogLevel.
+ * HTTP response: A JSON string containing the response e.g. {"level":"notice+"} for getLogLevel (may be empty).
+ *
+ * This method invokes the QMF2 method &lt;method&gt; with arguments &lt;inArgs&gt; on the object &lt;ObjectId&gt;
+ *
+ * GET: &lt;host&gt;:&lt;port&gt;/qpid/connection;
+ *
+ * This method retrieves (as a JSON string) the complete set of connections currently enabled on the Server.
+ * The returned JSON string represents a Map keyed by the connection name/handle. The value part is itself a
+ * Map containing the Connection URL and Connection Options used to create the Qpid Connection. e.g.
+ * {"8c5116d6-46a1-489b-93d8-fde525e0d76d":{"url":"0.0.0.0:5672","connectionOptions":{}}}
+ *
+ * GET: &lt;host&gt;:&lt;port&gt;/qpid/connection/&lt;name&gt;
+ *
+ * This method retrieves (as a JSON string) a Map containing the Connection URL and Connection Options used to
+ * create the Qpid Connection with the specified &lt;name&gt;. e.g.
+ * {"url":"0.0.0.0:5672","connectionOptions":{}}
+ *
+ * GET: &lt;host&gt;:&lt;port&gt;/qpid/connection/&lt;name&gt;/console/objects/&lt;className&gt;
+ *
+ * This method retrieves (as a JSON string) the list of QmfConsoleData objects with the specified &lt;className&gt;
+ * using the QMF2 Console associated with the Qpid Connection Object with the name &lt;name&gt;.
+ * This is the REST equivalent of Console.getObjects(className) which searches across all packages and all Agents
+ * for the specified className.
+ *
+ * GET: &lt;host&gt;:&lt;port&gt;/qpid/connection/&lt;name&gt;/console/objects/&lt;packageName&gt;/&lt;className&gt;
+ * !!not yet implemented!!
+ * This method retrieves (as a JSON string) the list of QmfConsoleData objects with the specified
+ * &lt;packageName&gt; and &lt;className&gt; using the QMF2 Console associated with the Qpid Connection Object
+ * with the name &lt;name&gt;.
+ * This is the REST equivalent of Console.getObjects(packageName, className) which searches across all Agents
+ * for the specified className in the package packageName.
+ *
+ * GET: &lt;host&gt;:&lt;port&gt;/qpid/connection/&lt;name&gt;/object/&lt;ObjectId&gt;
+ * This method retrieves (as a JSON string) the QmfConsoleData object with the specified &lt;ObjectId&gt;
+ * using the QMF2 Console associated with the Qpid Connection Object with the name &lt;name&gt;.
+ * This is the REST implementation of getObjects(oid) it is also the equivalent of the QmfConsoleData refresh()
+ * method where an object can update its state.
+ * Note that there's a slight variance on the QMF2 API here as that returns an array/list of QmfConsoledData
+ * objects for all queries, however as the call with ObjectId will only return one or zero objects this
+ * implementation returns the single QmfConsoleData object found or a 404 Not Found response.
+ *
+ * N.B. that the ManagementAgent on the broker appears not to set the timestamp properties in the response to this
+ * call, which means that they get set to current time in the QmfConsoleData, this is OK for _update_ts but not
+ * for _create_ts and _delete_ts. Users of this call should be aware of that in their own code.
+ *
+ * GET: &lt;host&gt;:&lt;port&gt;/qpid/connection/&lt;name&gt;/console/objects
+ * GET: &lt;host&gt;:&lt;port&gt;/qpid/connection/&lt;name&gt;/console/classes (synonyms)
+ *
+ * This method retrieves (as a JSON string) the list of SchemaClassId for all available Schema for all Agents.
+ * This is the REST equivalent of Console.getClasses() which searches across all Agents.
+ *
+ * GET: &lt;host&gt;:&lt;port&gt;/qpid/connection/&lt;name&gt;/console/classes/&lt;agentName&gt;
+ *
+ * This method retrieves (as a JSON string) the list of SchemaClassId for all available Schema on the
+ * Agent named &lt;agentName&gt;.
+ * This is the REST equivalent of Console.getClasses(agent) which searches across a specified Agent.
+ *
+ * GET: &lt;host&gt;:&lt;port&gt;/qpid/connection/&lt;name&gt;/console/packages
+ *
+ * This method retrieves (as a JSON string) the list of all known Packages for all Agents.
+ * This is the REST equivalent of Console.getPackages() which searches across all Agents.
+ *
+ * GET: &lt;host&gt;:&lt;port&gt;/qpid/connection/&lt;name&gt;/console/packages/&lt;agentName&gt;
+ *
+ * This method retrieves (as a JSON string) the list of all known Packages on the Agent named &lt;agentName&gt;.
+ * This is the REST equivalent of Console.getPackages(agent) which searches across a specified Agent.
+ *
+ * GET: &lt;host&gt;:&lt;port&gt;/qpid/connection/&lt;name&gt;/console/agents
+ * GET: &lt;host&gt;:&lt;port&gt;/qpid/connection/&lt;name&gt;/console/agent (synonyms)
+ *
+ * This method retrieves (as a JSON string) the list of all known Agents.
+ * This is the REST equivalent of Console.getAgents().
+ *
+ * GET: &lt;host&gt;:&lt;port&gt;/qpid/connection/&lt;name&gt;/console/agent/&lt;agentName&gt;
+ *
+ * This method retrieves (as a JSON string) the Agent named &lt;agentName&gt;.
+ * This is the REST equivalent of Console.getAgent(agentName).
+ *
+ * GET: &lt;host&gt;:&lt;port&gt;/qpid/connection/&lt;name&gt;/console/address
+ *
+ * This method retrieves (as a JSON string) the AMQP address this Console is listening to.
+ * This is the REST equivalent of Console.getAddress().
+ *
+ * GET: &lt;host&gt;:&lt;port&gt;/qpid/connection/&lt;name&gt;/console/workItemCount
+ *
+ * This method retrieves (as a plain text string) the count of pending WorkItems that can be retrieved
+ * from this Console.
+ * This is the REST equivalent of Console.getWorkItemCount().
+ *
+ * GET: &lt;host&gt;:&lt;port&gt;/qpid/connection/&lt;name&gt;/console/nextWorkItem
+ *
+ * This method retrieves (as a JSON string) the next pending work item from this Console (N.B. this method
+ * blocks until a WorkItem is available so should only be called asynchronously e.g. via AJAX).
+ * This is the REST equivalent of Console.getNextWorkitem().
+ * </pre>
+ * @author Fraser Adams
+ */
+public final class QpidServer implements Server
+{
+ private static final Logger _log = LoggerFactory.getLogger(QpidServer.class);
+
+ private ConnectionStore _connections = new ConnectionStore();
+ private String _defaultBroker = null;
+
+ public QpidServer(final String broker)
+ {
+ _defaultBroker = broker;
+ }
+
+ /**
+ * Handle a "/qpid/connection/<connectionName>/console/objects" or
+ * "/qpid/connection/<connectionName>/console/objects/" request,
+ * in other words a request for information about an object resource specified by the remaining path.
+ * Only the GET method is valid for this resource and it is in effect the REST mapping for Console.getObjects().
+ */
+ private void sendGetObjectsResponse(final HttpTransaction tx, final Console console, final String path) throws IOException
+ {
+ String[] params = path.split("/");
+ if (params.length == 1)
+ { // With one parameter we call getObjects(className)
+ tx.sendResponse(HTTP_OK, "application/json", JSON.fromObject(console.getObjects(params[0])));
+ }
+ else if (params.length == 2)
+ { // With two parameters we call getObjects(packageName, className)
+ //System.out.println("params = " + params[0] + ", " + params[1]);
+ tx.sendResponse(HTTP_OK, "application/json", JSON.fromObject(console.getObjects(params[0], params[1])));
+ }
+ else if (params.length == 3)
+ { // TODO With three parameters we call getObjects(packageName, className, agent)
+ //System.out.println("params = " + params[0] + ", " + params[1] + ", " + params[2]);
+ tx.sendResponse(HTTP_NOT_FOUND, "text/plain", "404 Too many parameters for objects GET request.");
+ } else {
+ tx.sendResponse(HTTP_NOT_FOUND, "text/plain", "404 Too many parameters for objects GET request.");
+ }
+ }
+
+ /**
+ * Called by the Web Server to allow a Server to handle a GET request.
+ * The HTTP GET URL structure for the REST API is specified above in the overall class documentation.
+ *
+ * @param tx the HttpTransaction containing the request from the client and used to send the response.
+ */
+ public void doGet(final HttpTransaction tx) throws IOException
+ {
+ String path = tx.getRequestURI();
+
+ //System.out.println();
+ //System.out.println("QpidServer doGet " + path);
+ //tx.logRequest();
+
+ if (path.startsWith("/qpid/connection/"))
+ {
+ path = path.substring(17);
+
+ String user = tx.getPrincipal(); // Using the principal lets different users use the default connection.
+ if (path.length() == 0)
+ { // handle "/qpid/connection/" request with unspecified connection (returns list of available connections).
+ tx.sendResponse(HTTP_OK, "application/json", JSON.fromObject(_connections.getAll(user)));
+ }
+ else
+ { // if path.length() > 0 we're dealing with a specified Connection so extract the name and look it up.
+ String connectionName = path;
+ int i = path.indexOf("/");
+ if (i > 0) // Can use > rather than >= as we've already tested for "/qpid/connection/" above.
+ {
+ connectionName = path.substring(0, i);
+ path = path.substring(i + 1);
+ }
+ else
+ {
+ path = "";
+ }
+
+ connectionName = user + "." + connectionName;
+
+ // TODO what if we don't want a default connection.
+ // If necessary we create a new "default" Connection associated with the user. The default connection
+ // attempts to connect to a broker specified in the QpidRestAPI config (or default 0.0.0.0:5672).
+ if (connectionName.equals(user + ".default"))
+ {
+ ConnectionProxy defaultConnection = _connections.get(connectionName);
+ if (defaultConnection == null)
+ {
+ defaultConnection = _connections.create(connectionName, _defaultBroker, "", false);
+
+ // Wait a maximum of 1000ms for the underlying Qpid Connection to become available. If we
+ // don't do this the first call using the default will return 404 Not Found.
+ defaultConnection.waitForConnection(1000);
+ }
+ }
+
+ // Find the Connection with the name extracted from the URI.
+ ConnectionProxy connection = _connections.get(connectionName);
+
+ if (connection == null)
+ {
+ tx.sendResponse(HTTP_NOT_FOUND, "text/plain", "404 Not Found.");
+ }
+ else if (!connection.isConnected())
+ {
+ tx.sendResponse(HTTP_INTERNAL_ERROR, "text/plain", "500 Broker Disconnected.");
+ }
+ else
+ {
+ if (path.length() == 0)
+ { // handle request for information about a specified Console
+ tx.sendResponse(HTTP_OK, "application/json", connection.toString());
+ }
+ else
+ { // In this block we are dealing with resources associated with a specified connectionName.
+ // path describes the resources specifically related to "/qpid/connection/<connectionName>"
+ Console console = connection.getConsole();
+
+ if (path.startsWith("console/objects/"))
+ { // Get information about specified objects.
+ path = path.substring(16);
+ sendGetObjectsResponse(tx, console, path);
+ }
+ else if (path.startsWith("console/objects") && path.length() == 15)
+ { // If objects is unspecified treat as a synonym for classes.
+ tx.sendResponse(HTTP_OK, "application/json", JSON.fromObject(console.getClasses()));
+ }
+ else if (path.startsWith("console/address/"))
+ { // Get the Console AMQP Address
+ tx.sendResponse(HTTP_OK, "application/json", JSON.fromObject(console.getAddress()));
+ }
+ else if (path.startsWith("console/address") && path.length() == 15)
+ { // Get the Console AMQP Address
+ tx.sendResponse(HTTP_OK, "application/json", JSON.fromObject(console.getAddress()));
+ }
+ else if (path.startsWith("console/workItemCount/"))
+ { // Returns the count of pending WorkItems that can be retrieved.
+ tx.sendResponse(HTTP_OK, "text/plain", "" + console.getWorkitemCount());
+ }
+ else if (path.startsWith("console/workItemCount") && path.length() == 21)
+ { // Returns the count of pending WorkItems that can be retrieved.
+ tx.sendResponse(HTTP_OK, "text/plain", "" + console.getWorkitemCount());
+ }
+ else if (path.startsWith("console/nextWorkItem/"))
+ { // Obtains the next pending work item, or null if none available.
+ tx.sendResponse(HTTP_OK, "application/json", JSON.fromObject(console.getNextWorkitem()));
+ }
+ else if (path.startsWith("console/nextWorkItem") && path.length() == 20)
+ { // Obtains the next pending work item, or null if none available.
+ tx.sendResponse(HTTP_OK, "application/json", JSON.fromObject(console.getNextWorkitem()));
+ }
+ else if (path.startsWith("console/agents") && path.length() == 14)
+ { // Get information about all available Agents.
+ tx.sendResponse(HTTP_OK, "application/json", JSON.fromObject(console.getAgents()));
+ }
+ else if (path.startsWith("console/agent/"))
+ { // Get information about a specified Agent.
+ Agent agent = console.getAgent(path.substring(14));
+ if (agent == null)
+ {
+ tx.sendResponse(HTTP_NOT_FOUND, "text/plain", "404 Not Found.");
+ }
+ else
+ {
+ tx.sendResponse(HTTP_OK, "application/json", JSON.fromObject(agent));
+ }
+ }
+ else if (path.startsWith("console/agent") && path.length() == 13)
+ { // If agent is unspecified treat as a synonym for agents.
+ tx.sendResponse(HTTP_OK, "application/json", JSON.fromObject(console.getAgents()));
+ }
+ else if (path.startsWith("console/classes/"))
+ { // Get information about the classes for a specified Agent
+ path = path.substring(16); // Get Agent name
+
+ // TODO handle getClasses() for specified Agent
+ tx.sendResponse(HTTP_NOT_IMPLEMENTED, "text/plain", "501 getClasses() for specified Agent not yet implemented.");
+ }
+ else if (path.startsWith("console/classes") && path.length() == 15)
+ { // Get information about all the classes for all Agents
+ tx.sendResponse(HTTP_OK, "application/json", JSON.fromObject(console.getClasses()));
+ }
+ else if (path.startsWith("console/packages/"))
+ { // Get information about the packages for a specified Agent
+ path = path.substring(17); // Get Agent name
+
+ // TODO handle getPackages() for specified Agent.
+ tx.sendResponse(HTTP_NOT_IMPLEMENTED, "text/plain", "501 getPackages() for specified Agent not yet implemented.");
+ }
+ else if (path.startsWith("object/"))
+ {
+ /**
+ * This is the REST implementation of getObjects(oid) it is also the equivalent of
+ * the QmfConsoleData refresh() method where an object can update its state.
+ * N.B. that the ManagementAgent on the broker appears not to set the timestamp properties
+ * in the response to this call, which means that they get set to current time in the
+ * QmfConsoleData, this is OK for _update_ts but not for _create_ts and _delete_ts
+ * users of this call should be aware of that in their own code.
+ */
+ path = path.substring(7);
+
+ // The ObjectId has been passed in the URI, create a real ObjectId
+ ObjectId oid = new ObjectId(path);
+
+ List<QmfConsoleData> objects = console.getObjects(oid);
+ if (objects.size() == 0)
+ {
+ tx.sendResponse(HTTP_NOT_FOUND, "text/plain", "404 Not Found.");
+ }
+ else
+ {
+ // Not that in a departure from the QMF2 API this returns the QmfConsoleData object
+ // rather than a list of size one. Perhaps the APIs should be completely consistent
+ // but this response seems more convenient.
+ tx.sendResponse(HTTP_OK, "application/json", JSON.fromObject(objects.get(0)));
+ }
+ }
+ else if (path.startsWith("console/packages") && path.length() == 16)
+ { // Get information about all the packages for all Agents
+ tx.sendResponse(HTTP_OK, "application/json", JSON.fromObject(console.getPackages()));
+ }
+ else
+ {
+ tx.sendResponse(HTTP_NOT_FOUND, "text/plain", "404 Not Found.");
+ }
+ }
+ }
+ }
+ }
+ else if (path.startsWith("/qpid/connection"))
+ { // handle "/qpid/connection" request with unspecified connection (returns list of available connections).
+ String user = tx.getPrincipal(); // Using the principal lets different users use the default connection.
+ tx.sendResponse(HTTP_OK, "application/json", JSON.fromObject(_connections.getAll(user)));
+ }
+ else
+ {
+ tx.sendResponse(HTTP_NOT_FOUND, "text/plain", "404 Not Found.");
+ }
+ }
+
+ /**
+ * Called by the Web Server to allow a Server to handle a POST request.
+ * <pre>
+ * POST: &lt;host&gt;:&lt;port&gt;/qpid/connection/&lt;name&gt;/object/&lt;ObjectId&gt;
+ * HTTP body: {"_method_name":&lt;method&gt;,"_arguments":&lt;inArgs&gt;}
+ * &lt;method&gt;: A string containing the QMF2 method name e.g. "getLogLevel", "setLogLevel", "create", "delete".
+ * &lt;inArgs&gt;: A JSON string containing the method arguments e.g. {"level":"debug+:Broker"} for setLogLevel.
+ * HTTP response: A JSON string containing the response e.g. {"level":"notice+"} for getLogLevel (may be empty).
+ *
+ * This method invokes the QMF2 method &lt;method&gt; with arguments &lt;inArgs&gt; on the object &lt;ObjectId&gt;
+ * </pre>
+ * @param tx the HttpTransaction containing the request from the client and used to send the response.
+ */
+ @SuppressWarnings("unchecked")
+ public void doPost(final HttpTransaction tx) throws IOException
+ {
+ String path = tx.getRequestURI();
+
+ //System.out.println();
+ //System.out.println("QpidServer doPost " + path);
+ //System.out.println("thread = " + Thread.currentThread().getId());
+
+ if (path.startsWith("/qpid/connection/"))
+ {
+ path = path.substring(17);
+ String user = tx.getPrincipal();
+
+ String connectionName = path;
+ int i = path.indexOf("/");
+ if (i > 0) // Can use > rather than >= as we've already tested for "/qpid/connection/" above.
+ {
+ connectionName = user + "." + path.substring(0, i);
+ path = path.substring(i + 1);
+
+ // Find the Connection with the name extracted from the URI.
+ ConnectionProxy connection = _connections.get(connectionName);
+
+ if (connection == null)
+ {
+ _log.info("QpidServer.doPost path: {} Connection not found.", tx.getRequestURI());
+ tx.sendResponse(HTTP_NOT_FOUND, "text/plain", "404 Not Found.");
+ }
+ else if (!connection.isConnected())
+ {
+ tx.sendResponse(HTTP_INTERNAL_ERROR, "text/plain", "500 Broker Disconnected.");
+ }
+ else
+ { // If we get this far we should have found a Qpid Connection so retrieve the QMF2 Console Object.
+ Console console = connection.getConsole();
+
+ if (path.startsWith("object/"))
+ {
+ path = path.substring(7);
+
+ // The ObjectId has been passed in the URI create an ObjectId and retrieve the Agent Name.
+ ObjectId oid = new ObjectId(path);
+ String agentName = oid.getAgentName();
+
+ // The qpidd ManagementAgent doesn't populate AgentName, if it's empty assume it's the broker.
+ agentName = agentName.equals("") ? "broker" : agentName;
+
+ Agent agent = console.getAgent(agentName); // Find the Agent we got the QmfData from.
+ if (agent == null)
+ {
+ _log.info("QpidServer.doPost path: {} Agent: {} not found.", tx.getRequestURI(), agentName);
+ tx.sendResponse(HTTP_NOT_FOUND, "text/plain", "404 Not Found.");
+ }
+ else
+ { // If we get this far we can create the Object and invoke the method.
+ // We can create a QmfConsoleData with nothing but an ObjectId and the agent.
+ QmfConsoleData object = new QmfConsoleData(Collections.EMPTY_MAP, agent);
+ object.setObjectId(oid);
+
+ String request = tx.getRequestString();
+ _log.info("QpidServer.doPost path: {} body: {}", tx.getRequestURI(), request);
+
+ //System.out.println(request);
+ String method = "";
+ try
+ {
+ Map<String, Object> reqMap = JSON.toMap(request);
+
+ method = (String)reqMap.get("_method_name");
+ Object arguments = reqMap.get("_arguments");
+
+ Map args = (arguments instanceof Map) ? (Map)arguments : null;
+ //System.out.println("method: " + method + ", args: " + args);
+
+ // Parse the args if present into a QmfData (needed by invokeMethod).
+ QmfData inArgs = (args == null) ? new QmfData() : new QmfData(args);
+
+ // Invoke the specified method on the QmfConsoleData we've created.
+ MethodResult results = null;
+
+ _log.info("invokeMethod: {}", request);
+ results = object.invokeMethod(method, inArgs);
+ tx.sendResponse(HTTP_OK, "application/json", JSON.fromObject(results));
+ }
+ catch (QmfException qmfe)
+ {
+ _log.info("QpidServer.doPost() caught Exception {}", qmfe.getMessage());
+ tx.sendResponse(HTTP_INTERNAL_ERROR, "text/plain", "invokeMethod(" +
+ method + ") -> " + qmfe.getMessage());
+ }
+ catch (Exception e)
+ {
+ _log.info("QpidServer.doPost() caught Exception {}", e.getMessage());
+ tx.sendResponse(HTTP_INTERNAL_ERROR, "text/plain", "500 " + e.getMessage());
+ }
+ }
+ }
+ else
+ {
+ tx.sendResponse(HTTP_NOT_FOUND, "text/plain", "404 Not Found.");
+ }
+ }
+ }
+ else
+ {
+ tx.sendResponse(HTTP_NOT_FOUND, "text/plain", "404 Not Found.");
+ }
+ }
+ else
+ {
+ tx.sendResponse(HTTP_NOT_FOUND, "text/plain", "404 Not Found.");
+ }
+ }
+
+ /**
+ * <pre>
+ * PUT: &lt;host&gt;:&lt;port&gt;/qpid/connection/&lt;name&gt;
+ * HTTP body: {"url":&lt;url&gt;,"connectionOptions":&lt;connectionOptions&gt;[,"disableEvents":true;]}
+ * &lt;url&gt;: A string containing an AMQP connection URL as used in the qpid::messaging API.
+ * &lt;connectionOptions&gt;: A JSON string containing connectionOptions in the form specified in the
+ * qpid::messaging API.
+ * </pre>
+ * Called by the Web Server to allow a Server to handle a PUT request.
+ * This method creates a Qpid Connection Object with the name &lt;name&gt; using the specified url and options.
+ * <p>
+ * The optional disableEvents property is used to start up a QMF Connection which can only
+ * do synchronous calls such as getObjects() and can't receive Agent updates or QMF2 Events.
+ * <p>
+ * N.B. It is possible for the Qpid broker to be unavailable when this method is called so it is actually a
+ * ConnectionProxy that is created. This method waits for up to 1000ms for the underlying Qpid Connection to
+ * become available and then returns. Clients should be aware that this method successfully returning only
+ * implies that the ConnectionProxy is in place and the underlying Qpid Connection may not be available.
+ * If the broker is down the ConnectionProxy will periodically attempt to reconnect, but whilst it is down
+ * the REST API will return a 500 Broker Disconnected response to any PUT or POST call.
+ *
+ * @param tx the HttpTransaction containing the request from the client and used to send the response.
+ */
+ @SuppressWarnings("unchecked")
+ public void doPut(final HttpTransaction tx) throws IOException
+ {
+ String path = tx.getRequestURI();
+ //tx.logRequest();
+
+ if (path.startsWith("/qpid/connection/"))
+ {
+ path = path.substring(17);
+ String user = tx.getPrincipal();
+ String request = tx.getRequestString();
+ _log.info("QpidServer.doPut path: {} body: {}", tx.getRequestURI(), request);
+
+ String name = user + "." + path;
+
+ try
+ {
+ // The PUT request is a JSON string containing a url String property and a connectionOptions
+ // property which is itself a JSON String.
+ Map<String, String> reqMap = JSON.toMap(request);
+
+ String url = reqMap.get("url");
+ url = url.equals("") ? _defaultBroker : url;
+
+ // Extract the connectionOptions property and check its type is a MAP
+ Object options = reqMap.get("connectionOptions");
+ Map optionsMap = (options instanceof Map) ? (Map)options : null;
+
+ // Turn the connectionOptions Map back into a JSON String. Note that we can't just get
+ // connectionOptions as a String from reqMap as it is sent as JSON and the JSON.toMap()
+ // call on the POST request will fully parse it.
+ String connectionOptions = JSON.fromObject(optionsMap);
+
+ boolean disableEvents = false;
+ String disableEventsString = reqMap.get("disableEvents");
+ if (disableEventsString != null)
+ {
+ disableEvents = disableEventsString.equalsIgnoreCase("true");
+ }
+
+ ConnectionProxy proxy = _connections.create(name, url, connectionOptions, disableEvents);
+
+ // Wait a maximum of 1000ms for the underlying Qpid Connection to become available then return.
+ proxy.waitForConnection(1000);
+ tx.sendResponse(HTTP_CREATED, "text/plain", "201 Created.");
+ }
+ catch (Exception e)
+ {
+ _log.info("QpidServer.doPut() caught Exception {}", e.getMessage());
+ tx.sendResponse(HTTP_INTERNAL_ERROR, "text/plain", "500 " + e.getMessage());
+ }
+ }
+ else
+ {
+ tx.sendResponse(HTTP_NOT_FOUND, "text/plain", "404 Not Found.");
+ }
+ }
+
+ /**
+ * Called by the Web Server to allow a Server to handle a DELETE request.
+ *
+ * DELETE: &lt;host&gt;:&lt;port&gt;/qpid/connection/&lt;name&gt;
+ *
+ * This method deletes the Qpid Connection Object with the name &lt;name&gt;.
+ *
+ * @param tx the HttpTransaction containing the request from the client and used to send the response.
+ */
+ public void doDelete(final HttpTransaction tx) throws IOException
+ {
+ String path = tx.getRequestURI();
+ //tx.logRequest();
+
+ if (path.startsWith("/qpid/connection/"))
+ {
+ path = path.substring(17);
+ String user = tx.getPrincipal();
+ String name = user + "." + path;
+
+ //System.out.println("Deleting " + name);
+ _log.info("QpidServer.doDelete path: {}", tx.getRequestURI());
+
+ _connections.delete(name);
+ tx.sendResponse(HTTP_OK, "text/plain", "200 Deleted.");
+ }
+ else
+ {
+ tx.sendResponse(HTTP_NOT_FOUND, "text/plain", "404 Not Found.");
+ }
+ }
+}
+
+
diff --git a/qpid/tools/src/java/qpid-qmf2-rest/src/main/java/org/apache/qpid/restapi/Server.java b/qpid/tools/src/java/qpid-qmf2-rest/src/main/java/org/apache/qpid/restapi/Server.java
new file mode 100644
index 0000000000..e7f5d61219
--- /dev/null
+++ b/qpid/tools/src/java/qpid-qmf2-rest/src/main/java/org/apache/qpid/restapi/Server.java
@@ -0,0 +1,95 @@
+/*
+ *
+ * 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.restapi;
+
+import java.io.IOException;
+
+/**
+ * A Server represents a handler that runs within a Web server, which is invoked to process HTTP exchanges.
+ * Servers receive and respond to requests from Web clients that are encapsulated into HttpTransactions.
+ * <p>
+ * The Server and HttpTransaction interfaces are intended to provide abstractions to enable the "business logic" to
+ * be isolated from the actual Web Server implementation choice, so for example a concrete HttpTransaction implementation
+ * could be created by wrapping a com.sun.net.httpserver.HttpExchange, but equally another implementation could wrap
+ * javax.servlet.http.HttpServletRequest and javax.servlet.http.HttpServletResponse so for example an HttpServlet
+ * could delegate to a Server instance passing the HttpTransaction it constructed from the HttpServletRequest and
+ * HttpServletResponse.
+ *
+ * @author Fraser Adams
+ */
+public interface Server
+{
+ /**
+ * Called by the Web Server to allow a Server to handle a GET request.
+ * <p>
+ * The GET method should be safe, that is, without any side effects for which users are held responsible. For
+ * example, most form queries have no side effects. If a client request is intended to change stored data, the
+ * request should use some other HTTP method.
+ * <p>
+ * The GET method should also be idempotent, meaning that it can be safely repeated. Sometimes making a method safe
+ * also makes it idempotent. For example, repeating queries is both safe and idempotent, but buying a product online
+ * or modifying data is neither safe nor idempotent.
+ *
+ * @param tx the HttpTransaction containing the request from the client and used to send the response.
+ */
+ public void doGet(HttpTransaction tx) throws IOException;
+
+ /**
+ * Called by the Web Server to allow a Server to handle a POST request.
+ * <p>
+ * The HTTP POST method allows the client to send data of unlimited length to the Web server a single time and is
+ * useful when posting information such as credit card numbers.
+ * <p>
+ * This method does not need to be either safe or idempotent. Operations requested through POST can have side
+ * effects for which the user can be held accountable, for example, updating stored data or buying items online.
+ *
+ * @param tx the HttpTransaction containing the request from the client and used to send the response.
+ */
+ public void doPost(HttpTransaction tx) throws IOException;
+
+ /**
+ * Called by the Web Server to allow a Server to handle a PUT request.
+ * <p>
+ * The PUT operation allows a client to place a file on the server and is similar to sending a file by FTP.
+ * <p>
+ * This method does not need to be either safe or idempotent. Operations that doPut performs can have side effects
+ * for which the user can be held accountable. When using this method, it may be useful to save a copy of the
+ * affected URL in temporary storage.
+ *
+ * @param tx the HttpTransaction containing the request from the client and used to send the response.
+ */
+ public void doPut(HttpTransaction tx) throws IOException;
+
+ /**
+ * Called by the Web Server to allow a Server to handle a DELETE request.
+ * <p>
+ * The DELETE operation allows a client to remove a document or Web page from the server.
+ * <p>
+ * This method does not need to be either safe or idempotent. Operations requested through DELETE can have side
+ * effects for which users can be held accountable. When using this method, it may be useful to save a copy of the
+ * affected URL in temporary storage.
+ *
+ * @param tx the HttpTransaction containing the request from the client and used to send the response.
+ */
+ public void doDelete(HttpTransaction tx) throws IOException;
+}
+
+
diff --git a/qpid/tools/src/java/qpid-qmf2-rest/src/main/java/org/apache/qpid/restapi/httpserver/Authenticator.java b/qpid/tools/src/java/qpid-qmf2-rest/src/main/java/org/apache/qpid/restapi/httpserver/Authenticator.java
new file mode 100644
index 0000000000..1083cb48df
--- /dev/null
+++ b/qpid/tools/src/java/qpid-qmf2-rest/src/main/java/org/apache/qpid/restapi/httpserver/Authenticator.java
@@ -0,0 +1,151 @@
+/*
+ *
+ * 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.restapi.httpserver;
+
+// Simple Logging Facade 4 Java
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.Properties;
+import java.util.Timer;
+import java.util.TimerTask;
+
+import com.sun.net.httpserver.BasicAuthenticator;
+
+/**
+ * This class implements a simple com.sun.net.httpserver.BasicAuthenticator. Clearly it's not very secure being a
+ * BasicAuthenticator that takes a plain (well Base64 encoded) username/password.
+ * TODO Clearly something more secure needs to be implemented.....
+ *
+ * This class creates a Timer used to schedule regular checks on the account.properties file and if it has changed
+ * it reloads the cache used for looking up the credentials. The poll period is every 10 seconds which should be OK
+ * for checking account updates. The class only updates the cache if the account.properties file has actually changed.
+ * Polling isn't ideal, but for this application it's probably no big deal. With Java 7 there is a Watch Service API
+ * https://blogs.oracle.com/thejavatutorials/entry/watching_a_directory_for_changes that allows asynchronous
+ * notification of changes, but it's an unnecessary dependency on Java 7 for this application.
+ *
+ * @author Fraser Adams
+ */
+public class Authenticator extends BasicAuthenticator
+{
+ private static final int CHECK_PERIOD = 10000; // Check every 10 seconds if the account properties have been changed.
+ private static final String ACCOUNT_FILENAME = "account.properties";
+ private static final Logger _log = LoggerFactory.getLogger(Authenticator.class);
+
+ private File _file;
+ private Properties _accountCache;
+ private long _accountFileLastModified = 0; // Used to check for updates to the account properties.
+
+ /**
+ * Create a Timer used to schedule regular checks on the account.properties file.
+ */
+ private Timer _timer = new Timer(true);
+
+ /**
+ * This private inner class is a fairly trivial TimerTask whose run() method simply calls checkAccountFile()
+ * in the main Authenticator class to check for account changes.
+ */
+ private final class CacheUpdater extends TimerTask
+ {
+ public void run()
+ {
+ checkAccountFile();
+ }
+ }
+
+ /**
+ * Construct the Authenticator. This fires up the CacheUpdater TimerTask to periodically check for changes.
+ * @param realm the authentication realm to use.
+ * @param path the path of the directory holding the account properties file.
+ */
+ public Authenticator(final String realm, final String path)
+ {
+ super(realm);
+
+ String accountPathname = path + "/" + ACCOUNT_FILENAME;
+ _file = new File(accountPathname);
+
+ if (_file.exists())
+ {
+ CacheUpdater updater = new CacheUpdater();
+ _timer.schedule(updater, 0, CHECK_PERIOD);
+ }
+ else
+ {
+ System.out.println("Authentication file " + accountPathname + " is missing.\nCannot continue - exiting.");
+ System.exit(1);
+ }
+ }
+
+ /**
+ * Checks if the account properties file has been updated, if it has it calls loadCache() to reload the cache.
+ */
+ public void checkAccountFile()
+ {
+ long mtime = _file.lastModified();
+ if (mtime != _accountFileLastModified)
+ {
+ _accountFileLastModified = mtime;
+ loadCache();
+ }
+ }
+
+ /**
+ * Load the account properties file into the account cache.
+ */
+ private void loadCache()
+ {
+ try
+ {
+ Properties properties = new Properties();
+ properties.load(new FileInputStream(_file));
+
+ // Set the cache to be the newly loaded one.
+ _accountCache = properties;
+ }
+ catch (IOException ex)
+ {
+ _log.info("loadCache failed with {}.", ex.getMessage());
+ }
+ }
+
+ @Override
+ public boolean checkCredentials(final String username, final String password)
+ {
+ //System.out.println("username = " + username);
+ //System.out.println("password = " + password);
+
+ // The original version of this forgot to check check for a null username Property. Thanks to
+ // Bruno Matos for picking that up and supplying the fix below.
+ if (_accountCache.getProperty(username) != null &&
+ _accountCache.getProperty(username).equals(password))
+ {
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+ }
+}
diff --git a/qpid/tools/src/java/qpid-qmf2-rest/src/main/java/org/apache/qpid/restapi/httpserver/Delegator.java b/qpid/tools/src/java/qpid-qmf2-rest/src/main/java/org/apache/qpid/restapi/httpserver/Delegator.java
new file mode 100644
index 0000000000..9dfd1fea0c
--- /dev/null
+++ b/qpid/tools/src/java/qpid-qmf2-rest/src/main/java/org/apache/qpid/restapi/httpserver/Delegator.java
@@ -0,0 +1,89 @@
+/*
+ *
+ * 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.restapi.httpserver;
+
+import java.io.IOException;
+import static java.net.HttpURLConnection.HTTP_BAD_METHOD;
+
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpHandler;
+
+import org.apache.qpid.restapi.HttpTransaction;
+import org.apache.qpid.restapi.Server;
+
+/**
+ * Delegator is an implementation of com.sun.net.httpserver.HttpHandler that is used to delegate to Server objects
+ * and thus provide an abstraction between the Web Server implementation (e.g. HttpServer or Servlets) and the
+ * implementation neutral Server and HttpTransaction interfaces.
+ * <p>
+ * In order to replace the HttpServer implementation with a Servlet based implementation all that should be necessary
+ * is a concrete implementation of HttpTransaction that wraps HttpServletRequest and HttpServletResponse and subclasses
+ * of HttpServlet that delegate to the appropriate Server instances from the appropriate context in a similar way
+ * to the Delegator class here.
+ *
+ * @author Fraser Adams
+ */
+public class Delegator implements HttpHandler
+{
+ private final Server _server;
+
+ /**
+ * Construct a Delegator instance that delegates to the specified Server instance.
+ * @param server the Server instance that this Delegator delegates to.
+ */
+ public Delegator(Server server)
+ {
+ _server = server;
+ }
+
+ /**
+ * Implements the HttpHandler handle interface and delegates to the Server instance.
+ * @param exchange the HttpExchange exchange object passed by the HttpServer. This will be used to construct
+ * an HttpTransaction instance that will be passed to the Server.
+ */
+ public void handle(final HttpExchange exchange) throws IOException
+ {
+ HttpTransaction tx = new HttpExchangeTransaction(exchange);
+ String method = tx.getMethod();
+ if (method.equals("GET"))
+ {
+ _server.doGet(tx);
+ }
+ else if (method.equals("POST"))
+ {
+ _server.doPost(tx);
+ }
+ else if (method.equals("PUT"))
+ {
+ _server.doPut(tx);
+ }
+ else if (method.equals("DELETE"))
+ {
+ _server.doDelete(tx);
+ }
+ else
+ {
+ tx.sendResponse(HTTP_BAD_METHOD, "text/plain", "405 Bad Method.");
+ }
+ }
+}
+
+
diff --git a/qpid/tools/src/java/qpid-qmf2-rest/src/main/java/org/apache/qpid/restapi/httpserver/HttpExchangeTransaction.java b/qpid/tools/src/java/qpid-qmf2-rest/src/main/java/org/apache/qpid/restapi/httpserver/HttpExchangeTransaction.java
new file mode 100644
index 0000000000..78932ec399
--- /dev/null
+++ b/qpid/tools/src/java/qpid-qmf2-rest/src/main/java/org/apache/qpid/restapi/httpserver/HttpExchangeTransaction.java
@@ -0,0 +1,311 @@
+/*
+ *
+ * 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.restapi.httpserver;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.List;
+import java.util.Map;
+
+import com.sun.net.httpserver.Headers;
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpPrincipal;
+
+import org.apache.qpid.restapi.HttpTransaction;
+
+/**
+ * This class provides an implementation of the HttpTransaction interface that wraps com.sun.net.httpserver.HttpExchange
+ * in order to provide an implementation neutral facade to the Server classes.
+ *
+ * @author Fraser Adams
+ */
+public final class HttpExchangeTransaction implements HttpTransaction
+{
+ final HttpExchange _exchange;
+
+ /**
+ * Construct an HttpExchangeTransaction from an HttpExchange object.
+ */
+ public HttpExchangeTransaction(final HttpExchange exchange)
+ {
+ _exchange = exchange;
+ }
+
+ /**
+ * Log the HTTP request information (primarily for debugging purposes)
+ */
+ public void logRequest()
+ {
+ System.out.println(_exchange.getRequestMethod() + " " + _exchange.getRequestURI());
+ for (Map.Entry<String, List<String>> header : _exchange.getRequestHeaders().entrySet())
+ {
+ System.out.println(header);
+ }
+ System.out.println("From: " + getRemoteHost() + ":" + getRemotePort());
+ }
+
+ /**
+ * Return the content passed in the request from the client as a Stream.
+ * @return the content passed in the request from the client as a Stream.
+ */
+ public InputStream getRequestStream() throws IOException
+ {
+ return _exchange.getRequestBody();
+ }
+
+ /**
+ * Return the content passed in the request from the client as a String.
+ * @return the content passed in the request from the client as a String.
+ */
+ public String getRequestString() throws IOException
+ {
+ return new String(getRequest());
+ }
+
+
+ /**
+ * Return the content passed in the request from the client as a byte[].
+ * @return the content passed in the request from the client as a byte[].
+ */
+ public byte[] getRequest() throws IOException
+ {
+ InputStream is = _exchange.getRequestBody();
+
+ // Convert InputStream to byte[].
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ byte[] buffer = new byte[1024];
+ int len;
+ while ((len = is.read(buffer, 0, 1024)) != -1)
+ {
+ bos.write(buffer, 0, len);
+ }
+ return bos.toByteArray();
+ }
+
+ /**
+ * Send the content passed as a String as an HTTP response back to the client.
+ * @param status the HTTP status code e.g. 200 for OK.
+ * @param mimeType the mimeType of the response content e.g. text/plain, text/xml, image/jpeg etc.
+ * @param content the content of the response passed as a String.
+ */
+ public void sendResponse(final int status, final String mimeType, final String content) throws IOException
+ {
+ if (content == null)
+ { // If response length has the value -1 then no response body is being sent.
+ _exchange.getResponseHeaders().set("Content-Type", mimeType);
+ _exchange.sendResponseHeaders(status, -1);
+ _exchange.close();
+ }
+ else
+ {
+ sendResponse(status, mimeType, content.getBytes());
+ }
+ }
+
+ /**
+ * Send the content passed as a byte[] as an HTTP response back to the client.
+ * @param status the HTTP status code e.g. 200 for OK.
+ * @param mimeType the mimeType of the response content e.g. text/plain, text/xml, image/jpeg etc.
+ * @param content the content of the response passed as a byte[].
+ */
+ public void sendResponse(final int status, final String mimeType, final byte[] content) throws IOException
+ {
+ _exchange.getResponseHeaders().set("Content-Type", mimeType);
+ if (content == null)
+ { // If response length has the value -1 then no response body is being sent.
+ _exchange.sendResponseHeaders(status, -1);
+ _exchange.close();
+ }
+ else
+ {
+ _exchange.sendResponseHeaders(status, content.length);
+ OutputStream os = _exchange.getResponseBody();
+ os.write(content);
+ os.flush();
+ os.close();
+ _exchange.close();
+ }
+ }
+
+ /**
+ * Send the content passed as an InputStream as an HTTP response back to the client.
+ * @param status the HTTP status code e.g. 200 for OK.
+ * @param mimeType the mimeType of the response content e.g. text/plain, text/xml, image/jpeg etc.
+ * @param is the content of the response passed as an InputStream.
+ */
+ public void sendResponse(final int status, final String mimeType, final InputStream is) throws IOException
+ {
+ _exchange.getResponseHeaders().set("Content-Type", mimeType);
+ if (is == null)
+ { // If response length has the value -1 then no response body is being sent.
+ _exchange.sendResponseHeaders(status, -1);
+ _exchange.close();
+ }
+ else
+ {
+ _exchange.sendResponseHeaders(status, 0); // For a stream we set to zero to force chunked transfer encoding.
+ OutputStream os = _exchange.getResponseBody();
+
+ byte[] buffer = new byte[8192];
+ while (true)
+ {
+ int read = is.read(buffer, 0, buffer.length);
+ if (read == -1) // Loop until EOF is reached
+ {
+ break;
+ }
+ os.write(buffer, 0, read);
+ }
+
+ os.flush();
+ os.close();
+ _exchange.close();
+ }
+ }
+
+ /**
+ * Returns the Internet Protocol (IP) address of the client or last proxy that sent the request.
+ * @return the Internet Protocol (IP) address of the client or last proxy that sent the request.
+ */
+ public String getRemoteAddr()
+ {
+ return _exchange.getRemoteAddress().getAddress().getHostAddress();
+ }
+
+ /**
+ * Returns the fully qualified name of the client or the last proxy that sent the request.
+ * @return the fully qualified name of the client or the last proxy that sent the request.
+ */
+ public String getRemoteHost()
+ {
+ return _exchange.getRemoteAddress().getHostName();
+ }
+
+ /**
+ * Returns the Internet Protocol (IP) source port of the client or last proxy that sent the request.
+ * @return the Internet Protocol (IP) source port of the client or last proxy that sent the request.
+ */
+ public int getRemotePort()
+ {
+ return _exchange.getRemoteAddress().getPort();
+ }
+
+ /**
+ * Returns a String containing the name of the current authenticated user. If the user has not been authenticated,
+ * the method returns null.
+ * @return a String containing the name of the user making this request; null if the user has not been authenticated.
+ */
+ public String getPrincipal()
+ {
+ HttpPrincipal principal = _exchange.getPrincipal();
+ return principal == null ? null : principal.getUsername();
+ }
+
+ /**
+ * Returns the name of the HTTP method with which this request was made, for example, GET, POST, or PUT.
+ * @return a String specifying the name of the method with which this request was made.
+ */
+ public String getMethod()
+ {
+ return _exchange.getRequestMethod();
+ }
+
+ /**
+ * Returns the part of this request's URL from the protocol name up to the query string in the first line of
+ * the HTTP request.
+ * @return a String containing the part of the URL from the protocol name up to the query string.
+ */
+ public String getRequestURI()
+ {
+ return _exchange.getRequestURI().getPath();
+ }
+
+ /**
+ * Sets a response header with the given name and value. If the header had already been set, the new value
+ * overwrites the previous one.
+ * @param name a String specifying the header name.
+ * @param value a String specifying the header value. If it contains octet string, it should be encoded according
+ * to RFC 2047.
+ */
+ public void setHeader(final String name, final String value)
+ {
+ _exchange.getResponseHeaders().set(name, value);
+ }
+
+ /**
+ * Returns the value of the specified request header as a String. If the request did not include a header of the
+ * specified name, this method returns null. If there are multiple headers with the same name, this method returns
+ * the first head in the request. The header name is case insensitive. You can use this method with any request
+ * header.
+ * @param name a String specifying the header name.
+ * @return a String containing the value of the requested header, or null if the request does not have a header of
+ * that name.
+ */
+ public String getHeader(final String name)
+ {
+ return _exchange.getRequestHeaders().getFirst(name);
+ }
+
+ /**
+ * Returns the String value of the specified cookie.
+ * @param name a String specifying the cookie name.
+ */
+ public String getCookie(final String name)
+ {
+ Headers headers = _exchange.getRequestHeaders();
+ if (!headers.containsKey("Cookie"))
+ {
+ return null;
+ }
+
+ List<String> values = headers.get("cookie");
+ for (String value : values)
+ {
+ String[] cookies = value.split(";");
+ for (String cookie : cookies)
+ {
+ String[] cdata = cookie.split("=");
+ if (cdata[0].trim().equals(name))
+ {
+ //return URLDecode(cdata[1]);
+ return cdata[1];
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Adds the specified cookie to the response. This method can be called multiple times to set more than one cookie.
+ * @param name a String specifying the cookie name.
+ * @param value a String specifying the cookie value.
+ */
+ public void addCookie(final String name, final String value)
+ {
+ //String data = name + "=" + URLEncode(value) + "; path=/";
+ String data = name + "=" + value + "; path=/";
+ _exchange.getResponseHeaders().add("Set-Cookie", data);
+ }
+}
+
+
diff --git a/qpid/tools/src/java/qpid-qmf2-rest/src/main/java/org/apache/qpid/restapi/servlet/TODO b/qpid/tools/src/java/qpid-qmf2-rest/src/main/java/org/apache/qpid/restapi/servlet/TODO
new file mode 100644
index 0000000000..9da5ee871a
--- /dev/null
+++ b/qpid/tools/src/java/qpid-qmf2-rest/src/main/java/org/apache/qpid/restapi/servlet/TODO
@@ -0,0 +1,25 @@
+/*
+ *
+ * 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.
+ *
+ */
+
+The org.apache.qpid.restapi.HttpTransaction and org.apache.qpid.restapi.Server interfaces are intended to
+provide abstractions to the underlying HTTP Server technology and thus should make it fairly easy to do
+a Servlet based implementation without having to rewrite the core REST API code. A Servlet implementation
+is still on the "TODO" list though.