You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@qpid.apache.org by or...@apache.org on 2018/12/18 18:23:53 UTC

[qpid-broker-j] 01/03: QPID-6948: [Broker-J] Introduce interfaces for handling REST management requests and add pluggable mechanism for supporting previous versions of REST API

This is an automated email from the ASF dual-hosted git repository.

orudyy pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/qpid-broker-j.git

commit d0a6a700c6b9b2f9ec1f1885bc4a73c796bd5f2b
Author: Alex Rudyy <or...@apache.org>
AuthorDate: Sat Dec 15 22:42:46 2018 +0000

    QPID-6948: [Broker-J] Introduce interfaces for handling REST management requests and add pluggable mechanism for supporting previous versions of REST API
---
 .../server/management/plugin/HttpManagement.java   |   93 +-
 .../management/plugin/ManagementController.java    |   95 ++
 .../plugin/ManagementControllerFactory.java        |   47 +
 .../management/plugin/ManagementException.java     |  282 ++++++
 .../management/plugin/ManagementRequest.java       |   53 +
 .../management/plugin/ManagementResponse.java      |   34 +
 .../qpid/server/management/plugin/RequestType.java |   29 +
 .../server/management/plugin/ResponseType.java     |   28 +
 .../controller/AbstractManagementController.java   |  204 ++++
 .../plugin/controller/ConverterHelper.java         |  154 +++
 .../latest/LatestManagementController.java         |  863 ++++++++++++++++
 .../latest/LatestManagementControllerFactory.java  |   56 ++
 .../plugin/servlet/rest/RequestInfo.java           |    7 +-
 .../plugin/servlet/rest/RestServlet.java           | 1062 ++++++--------------
 .../servlet/rest/RestUserPreferenceHandler.java    |   19 +-
 .../latest/LatestManagementControllerTest.java     |  809 +++++++++++++++
 .../plugin/servlet/rest/RequestInfoParserTest.java |   23 +-
 17 files changed, 3023 insertions(+), 835 deletions(-)

diff --git a/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/HttpManagement.java b/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/HttpManagement.java
index b591010..b76ab9d 100644
--- a/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/HttpManagement.java
+++ b/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/HttpManagement.java
@@ -25,6 +25,7 @@ import java.io.StringWriter;
 import java.io.Writer;
 import java.net.BindException;
 import java.net.InetSocketAddress;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
@@ -655,55 +656,59 @@ public class HttpManagement extends AbstractPluginAdapter<HttpManagement> implem
         return factory;
     }
 
-    private void addRestServlet(ServletContextHandler root)
+    private void addRestServlet(final ServletContextHandler root)
     {
-        Set<Class<? extends ConfiguredObject>> categories = new HashSet<>(getModel().getSupportedCategories());
-        final RestServlet restServlet = new RestServlet();
+        final Map<String, ManagementControllerFactory> factories = ManagementControllerFactory.loadFactories();
         final ApiDocsServlet apiDocsServlet = new ApiDocsServlet();
-
-        for (Class<? extends ConfiguredObject> category : categories)
-        {
-            String name = category.getSimpleName().toLowerCase();
-
-            ServletHolder servletHolder = new ServletHolder(name, restServlet);
-            servletHolder.getRegistration().setMultipartConfig(
-                    new MultipartConfigElement("",
-                                               getContextValue(Long.class, MAX_HTTP_FILE_UPLOAD_SIZE_CONTEXT_NAME),
-                                               -1L,
-                                               getContextValue(Integer.class,
-                                                               MAX_HTTP_FILE_UPLOAD_SIZE_CONTEXT_NAME)));
-
-            List<String> paths = Arrays.asList("/api/latest/" + name,
-                                               "/api/v" + BrokerModel.MODEL_VERSION + "/" + name);
-
-            for (String path : paths)
+        final List<String> supportedVersions = new ArrayList<>();
+        String currentVersion = BrokerModel.MODEL_VERSION;
+        ManagementController managementController = null;
+        ManagementControllerFactory factory;
+        do
+        {
+            factory = factories.get(currentVersion);
+            if (factory != null)
             {
-                root.addServlet(servletHolder, path + "/*");
+                managementController = factory.createManagementController(this, managementController);
+                final RestServlet managementServlet = new RestServlet();
+                final Collection<String> categories = managementController.getCategories();
+                for (String category : categories)
+                {
+                    final String name = category.toLowerCase();
+                    final String path = managementController.getCategoryMapping(name);
+                    final ServletHolder servletHolder = new ServletHolder(path, managementServlet);
+                    servletHolder.setInitParameter("qpid.controller.version", managementController.getVersion());
+
+                    servletHolder.getRegistration().setMultipartConfig(
+                            new MultipartConfigElement("",
+                                                       getContextValue(Long.class,
+                                                                       MAX_HTTP_FILE_UPLOAD_SIZE_CONTEXT_NAME),
+                                                       -1L,
+                                                       getContextValue(Integer.class,
+                                                                       MAX_HTTP_FILE_UPLOAD_SIZE_CONTEXT_NAME)));
+
+                    root.addServlet(servletHolder, path + (path.endsWith("/") ? "*" : "/*"));
+
+                    if (BrokerModel.MODEL_VERSION.equals(managementController.getVersion()))
+                    {
+                        root.addServlet(servletHolder, "/api/latest/" + name + "/*");
+                        ServletHolder docServletHolder = new ServletHolder(name + "docs", apiDocsServlet);
+                        root.addServlet(docServletHolder, "/apidocs/latest/" + name + "/");
+                        root.addServlet(docServletHolder, "/apidocs/v" + BrokerModel.MODEL_VERSION + "/" + name + "/");
+                        root.addServlet(docServletHolder, "/apidocs/latest/" + name);
+                        root.addServlet(docServletHolder, "/apidocs/v" + BrokerModel.MODEL_VERSION + "/" + name);
+                    }
+                }
+                supportedVersions.add("v" + currentVersion);
+                currentVersion = factory.getPreviousVersion();
             }
-            ServletHolder docServletHolder = new ServletHolder(name + "docs", apiDocsServlet);
-            root.addServlet(docServletHolder, "/apidocs/latest/" + name + "/");
-            root.addServlet(docServletHolder, "/apidocs/v" + BrokerModel.MODEL_VERSION + "/" + name + "/");
-            root.addServlet(docServletHolder, "/apidocs/latest/" + name);
-            root.addServlet(docServletHolder, "/apidocs/v" + BrokerModel.MODEL_VERSION + "/" + name);
-
-
         }
-
-        final ServletHolder versionsServletHolder = new ServletHolder(new JsonValueServlet(getApiProperties()));
-        root.addServlet(versionsServletHolder,"/api");
-        root.addServlet(versionsServletHolder,"/api/");
-
-    }
-
-    private Map<String, Object> getApiProperties()
-    {
-        return Collections.singletonMap("supportedVersions", getSupportedRestApiVersions());
-    }
-
-    private List<String> getSupportedRestApiVersions()
-    {
-        // TODO - actually support multiple versions and add those versions to the list
-        return Collections.singletonList(getLatestSupportedVersion());
+        while (factory != null);
+        root.getServletContext().setAttribute("qpid.controller.chain", managementController);
+        final Map<String, List<String>> supported = Collections.singletonMap("supportedVersions", supportedVersions);
+        final ServletHolder versionsServletHolder = new ServletHolder(new JsonValueServlet(supported));
+        root.addServlet(versionsServletHolder, "/api");
+        root.addServlet(versionsServletHolder, "/api/");
     }
 
     private String getLatestSupportedVersion()
diff --git a/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/ManagementController.java b/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/ManagementController.java
new file mode 100644
index 0000000..5c923c2
--- /dev/null
+++ b/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/ManagementController.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.server.management.plugin;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.qpid.server.model.ConfiguredObject;
+
+public interface ManagementController
+{
+    String getVersion();
+
+    Collection<String> getCategories();
+
+    String getCategoryMapping(String category);
+
+    String getCategory(ConfiguredObject<?> managedObject);
+
+    Collection<String> getCategoryHierarchy(ConfiguredObject<?> root, String category);
+
+    ManagementController getNextVersionManagementController();
+
+    ManagementResponse handleGet(ManagementRequest request) throws ManagementException;
+
+    ManagementResponse handlePut(ManagementRequest request) throws ManagementException;
+
+    ManagementResponse handlePost(ManagementRequest request) throws ManagementException;
+
+    ManagementResponse handleDelete(ManagementRequest request) throws ManagementException;
+
+    Object get(ConfiguredObject<?> root,
+               String category,
+               List<String> path,
+               Map<String, List<String>> parameters) throws ManagementException;
+
+    Object createOrUpdate(ConfiguredObject<?> root,
+                          String category,
+                          List<String> path,
+                          Map<String, Object> attributes,
+                          boolean isPost) throws ManagementException;
+
+    int delete(ConfiguredObject<?> root,
+               String category,
+               List<String> path,
+               Map<String, List<String>> parameters) throws ManagementException;
+
+    ManagementResponse invoke(ConfiguredObject<?> root,
+                              String category,
+                              List<String> path,
+                              String operation,
+                              Map<String, Object> parameters,
+                              boolean isPost,
+                              final boolean isSecureOrAllowedOnInsecureChannel) throws ManagementException;
+
+    Object getPreferences(ConfiguredObject<?> root,
+                          String category,
+                          List<String> path,
+                          Map<String, List<String>> parameters) throws ManagementException;
+
+    void setPreferences(ConfiguredObject<?> root,
+                        String category,
+                        List<String> path,
+                        Object preferences,
+                        Map<String, List<String>> parameters,
+                        boolean isPost) throws ManagementException;
+
+    int deletePreferences(ConfiguredObject<?> root,
+                          String category,
+                          List<String> path,
+                          Map<String, List<String>> parameters) throws ManagementException;
+
+    Object formatConfiguredObject(Object content,
+                                  Map<String, List<String>> parameters,
+                                  boolean isSecureOrAllowedOnInsecureChannel);
+}
diff --git a/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/ManagementControllerFactory.java b/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/ManagementControllerFactory.java
new file mode 100644
index 0000000..8e02513
--- /dev/null
+++ b/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/ManagementControllerFactory.java
@@ -0,0 +1,47 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+package org.apache.qpid.server.management.plugin;
+
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
+
+import org.apache.qpid.server.plugin.Pluggable;
+import org.apache.qpid.server.plugin.QpidServiceLoader;
+
+public interface ManagementControllerFactory extends Pluggable
+{
+    String getVersion();
+
+    String getPreviousVersion();
+
+    ManagementController createManagementController(HttpManagementConfiguration<?> httpManagement,
+                                                    ManagementController nextVersionManagementController);
+
+    static Map<String, ManagementControllerFactory> loadFactories()
+    {
+        final Iterable<ManagementControllerFactory> factories =
+                new QpidServiceLoader().atLeastOneInstanceOf(ManagementControllerFactory.class);
+
+        return StreamSupport.stream(factories.spliterator(), false)
+                            .collect(Collectors.toMap(ManagementControllerFactory::getVersion, f -> f));
+    }
+}
diff --git a/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/ManagementException.java b/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/ManagementException.java
new file mode 100644
index 0000000..8e7b3c3
--- /dev/null
+++ b/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/ManagementException.java
@@ -0,0 +1,282 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+package org.apache.qpid.server.management.plugin;
+
+import java.security.AccessController;
+import java.security.Principal;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.stream.Collectors;
+
+import javax.security.auth.Subject;
+import javax.servlet.http.HttpServletResponse;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.qpid.server.configuration.IllegalConfigurationException;
+import org.apache.qpid.server.management.plugin.controller.ConverterHelper;
+import org.apache.qpid.server.management.plugin.servlet.rest.NotFoundException;
+import org.apache.qpid.server.model.AbstractConfiguredObject;
+import org.apache.qpid.server.model.IllegalStateTransitionException;
+import org.apache.qpid.server.model.IntegrityViolationException;
+import org.apache.qpid.server.model.OperationTimeoutException;
+import org.apache.qpid.server.util.ExternalServiceException;
+import org.apache.qpid.server.util.ExternalServiceTimeoutException;
+
+public class ManagementException extends RuntimeException
+{
+    private static final int SC_UNPROCESSABLE_ENTITY = 422;
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(ManagementException.class);
+
+    private final int _statusCode;
+    private final Map<String, String> _headers;
+
+    private ManagementException(final int statusCode,
+                                  final String message,
+                                  final Map<String, String> headers)
+    {
+        super(message);
+        _statusCode = statusCode;
+        _headers = headers;
+    }
+
+    private ManagementException(final int statusCode,
+                                  final String message,
+                                  final Throwable cause,
+                                  final Map<String, String> headers)
+    {
+        super(message, cause);
+        _statusCode = statusCode;
+        _headers = headers;
+    }
+
+    public int getStatusCode()
+    {
+        return _statusCode;
+    }
+
+    public Map<String, String> getHeaders()
+    {
+        return _headers;
+    }
+
+    public static ManagementException createNotFoundManagementException(final Exception e)
+    {
+        return new ManagementException(HttpServletResponse.SC_NOT_FOUND, e.getMessage(), e, null);
+    }
+
+    public static ManagementException createNotFoundManagementException(final String message)
+    {
+        return new ManagementException(HttpServletResponse.SC_NOT_FOUND, message, null);
+    }
+
+    public static ManagementException createGoneManagementException(final String message)
+    {
+        return new ManagementException(HttpServletResponse.SC_GONE, message, null);
+    }
+
+    public static ManagementException createUnprocessableManagementException(final String message)
+    {
+        return new ManagementException(SC_UNPROCESSABLE_ENTITY, message, null);
+    }
+
+    public static ManagementException createUnprocessableManagementException(final Exception e)
+    {
+        return new ManagementException(SC_UNPROCESSABLE_ENTITY, e.getMessage(), e, null);
+    }
+
+    private static ManagementException createConflictManagementException(final Exception e)
+    {
+        return new ManagementException(HttpServletResponse.SC_CONFLICT, e.getMessage(), e, null);
+    }
+
+
+    public static ManagementException createNotAllowedManagementException(final String message,
+                                                                          final Map<String, String> headers)
+    {
+        return new ManagementException(HttpServletResponse.SC_METHOD_NOT_ALLOWED, message, headers);
+    }
+
+    public static ManagementException createForbiddenManagementException(final String message)
+    {
+        return new ManagementException(HttpServletResponse.SC_FORBIDDEN, message, null);
+    }
+
+
+    public static ManagementException createForbiddenManagementException(final Exception e)
+    {
+        return new ManagementException(HttpServletResponse.SC_FORBIDDEN, e.getMessage(), e, null);
+    }
+
+
+    public static ManagementException createInternalServerErrorManagementException(final String message)
+    {
+        return new ManagementException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, message, null);
+    }
+
+    public static ManagementException createInternalServerErrorManagementException(final String message,
+                                                                                   final Exception e)
+    {
+        return new ManagementException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, message, e, null);
+    }
+
+
+    private static ManagementException createBadGatewayManagementException(final String message,
+                                                                           final RuntimeException e)
+    {
+        return new ManagementException(HttpServletResponse.SC_BAD_GATEWAY, message, e, null);
+    }
+
+    private static ManagementException createGatewayTimeoutManagementException(final RuntimeException e)
+    {
+        return new ManagementException(HttpServletResponse.SC_GATEWAY_TIMEOUT, e.getMessage(), e, null);
+    }
+
+
+    public static ManagementException createBadRequestManagementException(final String message)
+    {
+        return new ManagementException(HttpServletResponse.SC_BAD_REQUEST, message, null);
+    }
+
+    public static ManagementException createBadRequestManagementException(final String message, final Throwable e)
+    {
+        return new ManagementException(HttpServletResponse.SC_BAD_REQUEST, message, e, null);
+    }
+
+
+    public static ManagementException toManagementException(final RuntimeException e,
+                                                            final String categoryMapping,
+                                                            final List<String> path)
+    {
+        if (e instanceof SecurityException)
+        {
+            LOGGER.debug("{}, sending {}", e.getClass().getName(), HttpServletResponse.SC_FORBIDDEN, e);
+            return createForbiddenManagementException(e);
+        }
+        else if (e instanceof AbstractConfiguredObject.DuplicateIdException
+                 || e instanceof AbstractConfiguredObject.DuplicateNameException
+                 || e instanceof IntegrityViolationException
+                 || e instanceof IllegalStateTransitionException)
+        {
+            return createConflictManagementException(e);
+        }
+        else if (e instanceof NotFoundException)
+        {
+            if (LOGGER.isTraceEnabled())
+            {
+                LOGGER.trace(e.getClass().getSimpleName() + " processing request", e);
+            }
+            return createNotFoundManagementException(e);
+        }
+        else if (e instanceof IllegalConfigurationException || e instanceof IllegalArgumentException)
+        {
+            LOGGER.warn("{} processing request {} from user '{}': {}",
+                        e.getClass().getSimpleName(),
+                        getRequestURI(path, categoryMapping),
+                        getRequestPrincipals(),
+                        e.getMessage());
+            Throwable t = e;
+            int maxDepth = 10;
+            while ((t = t.getCause()) != null && maxDepth-- != 0)
+            {
+                LOGGER.warn("... caused by " + t.getClass().getSimpleName() + "  : " + t.getMessage());
+            }
+            if (LOGGER.isDebugEnabled())
+            {
+                LOGGER.debug(e.getClass().getSimpleName() + " processing request", e);
+            }
+            return createUnprocessableManagementException(e);
+        }
+        else if (e instanceof OperationTimeoutException)
+        {
+            if (LOGGER.isDebugEnabled())
+            {
+                LOGGER.debug("Timeout during processing of request {} from user '{}'",
+                             getRequestURI(path, categoryMapping),
+                             getRequestPrincipals(),
+                             e);
+            }
+            else
+            {
+                LOGGER.info("Timeout during processing of request {} from user '{}'",
+                            getRequestURI(path, categoryMapping), getRequestPrincipals());
+            }
+            return createBadGatewayManagementException("Timeout occurred", e);
+        }
+
+        else if (e instanceof ExternalServiceTimeoutException)
+        {
+            LOGGER.warn("External request timeout ", e);
+            return createGatewayTimeoutManagementException(e);
+        }
+        else if (e instanceof ExternalServiceException)
+        {
+            LOGGER.warn("External request failed ", e);
+            return createBadGatewayManagementException(e.getMessage(), e);
+        }
+        else if (e instanceof ManagementException)
+        {
+            return (ManagementException)e;
+        }
+        else
+        {
+            LOGGER.warn("Unexpected Exception", e);
+            return createInternalServerErrorManagementException("Unexpected Exception", e);
+        }
+    }
+
+    public static ManagementException handleError(final Error e)
+    {
+        if (e instanceof NoClassDefFoundError)
+        {
+            LOGGER.warn("Unexpected exception processing request ", e);
+            return createBadRequestManagementException("Not found: " + e.getMessage(), e);
+        }
+        else
+        {
+            throw e;
+        }
+    }
+
+    public static String getRequestURI(final List<String> path, final String categoryMapping)
+    {
+        return categoryMapping + (categoryMapping.endsWith("/") ? "" : "/")
+               + path.stream().map(ConverterHelper::encode).collect(Collectors.joining("/"));
+    }
+
+    private static String getRequestPrincipals()
+    {
+        final Subject subject = Subject.getSubject(AccessController.getContext());
+        if (subject == null)
+        {
+            return null;
+        }
+        final Set<Principal> principalSet = subject.getPrincipals();
+        return String.join("/",
+                           principalSet.stream()
+                                       .map(Principal::getName)
+                                       .collect(Collectors.toCollection(TreeSet::new)));
+    }
+}
diff --git a/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/ManagementRequest.java b/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/ManagementRequest.java
new file mode 100644
index 0000000..8b5c1ca
--- /dev/null
+++ b/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/ManagementRequest.java
@@ -0,0 +1,53 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+package org.apache.qpid.server.management.plugin;
+
+import java.util.List;
+import java.util.Map;
+
+import org.apache.qpid.server.model.ConfiguredObject;
+
+public interface ManagementRequest
+{
+    String getMethod();
+
+    Map<String, List<String>> getParameters();
+
+    String getParameter(String name);
+
+    Map<String, Object> getParametersAsFlatMap();
+
+    Map<String, String> getHeaders();
+
+    List<String> getPath();
+
+    String getCategory();
+
+    ConfiguredObject<?> getRoot();
+
+    boolean isSecure();
+
+    boolean isConfidentialOperationAllowedOnInsecureChannel();
+
+    <T> T getBody(Class<T> type);
+
+    String getRequestURL();
+}
diff --git a/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/ManagementResponse.java b/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/ManagementResponse.java
new file mode 100644
index 0000000..a000619
--- /dev/null
+++ b/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/ManagementResponse.java
@@ -0,0 +1,34 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+package org.apache.qpid.server.management.plugin;
+
+import java.util.Map;
+
+public interface ManagementResponse
+{
+    ResponseType getType();
+
+    Object getBody();
+
+    Map<String, String> getHeaders();
+
+    int getResponseCode();
+}
diff --git a/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/RequestType.java b/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/RequestType.java
new file mode 100644
index 0000000..29ca542
--- /dev/null
+++ b/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/RequestType.java
@@ -0,0 +1,29 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+package org.apache.qpid.server.management.plugin;
+
+public enum RequestType
+{
+    OPERATION,
+    USER_PREFERENCES,
+    VISIBLE_PREFERENCES,
+    MODEL_OBJECT
+}
diff --git a/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/ResponseType.java b/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/ResponseType.java
new file mode 100644
index 0000000..8396075
--- /dev/null
+++ b/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/ResponseType.java
@@ -0,0 +1,28 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+package org.apache.qpid.server.management.plugin;
+
+public enum ResponseType
+{
+    MODEL_OBJECT,
+    DATA,
+    EMPTY
+}
diff --git a/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/controller/AbstractManagementController.java b/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/controller/AbstractManagementController.java
new file mode 100644
index 0000000..3888869
--- /dev/null
+++ b/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/controller/AbstractManagementController.java
@@ -0,0 +1,204 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+package org.apache.qpid.server.management.plugin.controller;
+
+import static org.apache.qpid.server.management.plugin.controller.ConverterHelper.encode;
+import static org.apache.qpid.server.management.plugin.ManagementException.createBadRequestManagementException;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.qpid.server.management.plugin.ManagementController;
+import org.apache.qpid.server.management.plugin.ManagementException;
+import org.apache.qpid.server.management.plugin.RequestType;
+import org.apache.qpid.server.management.plugin.ManagementRequest;
+import org.apache.qpid.server.management.plugin.ManagementResponse;
+import org.apache.qpid.server.management.plugin.ResponseType;
+
+public abstract class AbstractManagementController implements ManagementController
+{
+    protected static final String USER_PREFERENCES = "userpreferences";
+    protected static final String VISIBLE_USER_PREFERENCES = "visiblepreferences";
+
+    public ManagementResponse handleGet(final ManagementRequest request) throws ManagementException
+    {
+        final RequestType type = getRequestType(request);
+        switch (type)
+        {
+            case OPERATION:
+            {
+                final Collection<String> hierarchy = getCategoryHierarchy(request.getRoot(), request.getCategory());
+                final List<String> operationPath = request.getPath().subList(0, hierarchy.size());
+                final String operationName = request.getPath().get(hierarchy.size());
+                return invoke(request.getRoot(),
+                                  request.getCategory(),
+                                  operationPath,
+                                  operationName,
+                                  request.getParametersAsFlatMap(),
+                                  false,
+                                  request.isSecure() || request.isConfidentialOperationAllowedOnInsecureChannel());
+            }
+            case MODEL_OBJECT:
+            {
+                final Object response = get(request.getRoot(),
+                                            request.getCategory(),
+                                            request.getPath(),
+                                            request.getParameters());
+
+                return new ControllerManagementResponse(ResponseType.MODEL_OBJECT, response);
+            }
+            case VISIBLE_PREFERENCES:
+            case USER_PREFERENCES:
+            {
+                final Object response = getPreferences(request.getRoot(),
+                                          request.getCategory(),
+                                          request.getPath(),
+                                          request.getParameters());
+                return new ControllerManagementResponse(ResponseType.DATA, response);
+            }
+            default:
+            {
+                throw createBadRequestManagementException(String.format("Unexpected request type '%s' for path '%s'",
+                                                                        type,
+                                                                        getCategoryMapping(request.getCategory())));
+            }
+        }
+
+
+    }
+
+    public ManagementResponse handlePut(final ManagementRequest request) throws ManagementException
+    {
+        return handlePostOrPut(request);
+    }
+
+    public ManagementResponse handlePost(final ManagementRequest request) throws ManagementException
+    {
+        return handlePostOrPut(request);
+    }
+
+    public ManagementResponse handleDelete(final ManagementRequest request) throws ManagementException
+    {
+        final RequestType type = getRequestType(request);
+        switch (type)
+        {
+            case MODEL_OBJECT:
+            {
+                delete(request.getRoot(),
+                       request.getCategory(),
+                       request.getPath(),
+                       request.getParameters());
+                break;
+            }
+            case VISIBLE_PREFERENCES:
+            case USER_PREFERENCES:
+            {
+                deletePreferences(request.getRoot(),
+                                  request.getCategory(),
+                                  request.getPath(),
+                                  request.getParameters());
+                break;
+            }
+            default:
+            {
+                throw createBadRequestManagementException(String.format("Unexpected request type '%s' for path '%s'",
+                                                                        type,
+                                                                        getCategoryMapping(request.getCategory())));
+            }
+        }
+        return new ControllerManagementResponse(ResponseType.EMPTY, null);
+    }
+
+    private ManagementResponse handlePostOrPut(final ManagementRequest request) throws ManagementException
+    {
+        final RequestType type = getRequestType(request);
+        final Collection<String> hierarchy = getCategoryHierarchy(request.getRoot(), request.getCategory());
+        switch (type)
+        {
+            case OPERATION:
+            {
+                final List<String> operationPath = request.getPath().subList(0, hierarchy.size());
+                final String operationName = request.getPath().get(hierarchy.size());
+                @SuppressWarnings("unchecked")
+                final Map<String, Object> arguments = request.getBody(LinkedHashMap.class);
+                return invoke(request.getRoot(),
+                                  request.getCategory(),
+                                  operationPath,
+                                  operationName,
+                                  arguments,
+                                  true,
+                                  request.isSecure()
+                                  || request.isConfidentialOperationAllowedOnInsecureChannel());
+            }
+            case MODEL_OBJECT:
+            {
+                @SuppressWarnings("unchecked")
+                final Map<String, Object> attributes = request.getBody(LinkedHashMap.class);
+                final Object response = createOrUpdate(request.getRoot(),
+                                          request.getCategory(),
+                                          request.getPath(),
+                                          attributes,
+                                          "POST".equalsIgnoreCase(request.getMethod()));
+                int responseCode = HttpServletResponse.SC_OK;
+                ResponseType responseType = ResponseType.EMPTY;
+                Map<String, String> headers = Collections.emptyMap();
+                if (response != null)
+                {
+                    responseCode = HttpServletResponse.SC_CREATED;
+                    final StringBuilder requestURL = new StringBuilder(request.getRequestURL());
+                    if (hierarchy.size() != request.getPath().size())
+                    {
+                        Object name = attributes.get("name");
+                        requestURL.append("/").append(encode(String.valueOf(name)));
+                    }
+                    headers = Collections.singletonMap("Location", requestURL.toString());
+                    responseType = ResponseType.MODEL_OBJECT;
+                }
+                return new ControllerManagementResponse(responseType, response, responseCode, headers);
+            }
+            case VISIBLE_PREFERENCES:
+            case USER_PREFERENCES:
+            {
+                setPreferences(request.getRoot(),
+                               request.getCategory(),
+                               request.getPath(),
+                               request.getBody(Object.class),
+                               request.getParameters(),
+                               "POST".equalsIgnoreCase(request.getMethod()));
+                return new ControllerManagementResponse(ResponseType.EMPTY, null);
+            }
+            default:
+            {
+                throw createBadRequestManagementException(String.format("Unexpected request type '%s' for path '%s'",
+                                                                        type,
+                                                                        getCategoryMapping(request.getCategory())));
+            }
+        }
+
+    }
+
+    protected abstract RequestType getRequestType(ManagementRequest managementRequest) throws ManagementException;
+}
diff --git a/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/controller/ConverterHelper.java b/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/controller/ConverterHelper.java
new file mode 100644
index 0000000..b4fc180
--- /dev/null
+++ b/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/controller/ConverterHelper.java
@@ -0,0 +1,154 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+package org.apache.qpid.server.management.plugin.controller;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+import org.apache.qpid.server.management.plugin.HttpManagementUtil;
+import org.apache.qpid.server.management.plugin.ManagementException;
+
+public class ConverterHelper
+{
+
+    private static final Pattern CONTEXT_VARIABLE_PATTERN = Pattern.compile("\\$\\{[\\w+.\\-:]+}");
+
+    private ConverterHelper()
+    {
+        super();
+    }
+
+    public static long toLong(final Object attributeValue)
+    {
+        long value;
+        if (attributeValue instanceof Number)
+        {
+            value = ((Number) attributeValue).longValue();
+        }
+        else if (attributeValue instanceof String)
+        {
+            try
+            {
+                value = Long.parseLong((String) attributeValue);
+            }
+            catch (Exception e)
+            {
+                value = 0;
+            }
+        }
+        else
+        {
+            value = 0;
+        }
+        return value;
+    }
+
+    public static boolean toBoolean(final Object attributeValue)
+    {
+        boolean value;
+        if (attributeValue instanceof Boolean)
+        {
+            value = (Boolean) attributeValue;
+        }
+        else if (attributeValue instanceof String)
+        {
+            return Boolean.parseBoolean((String)attributeValue);
+
+        }
+        else
+        {
+            value = false;
+        }
+        return value;
+    }
+
+    public static int toInt(final Object value)
+    {
+        int result;
+        if (value instanceof Number)
+        {
+            result = ((Number) value).intValue();
+        }
+        else if (value instanceof String)
+        {
+            try
+            {
+                result = Integer.parseInt(String.valueOf(value));
+            }
+            catch (RuntimeException e)
+            {
+                result = 0;
+            }
+        }
+        else
+        {
+            result = 0;
+        }
+        return result;
+    }
+
+    public static  boolean isContextVariable(final Object value)
+    {
+        return value != null && CONTEXT_VARIABLE_PATTERN.matcher(String.valueOf(value)).matches();
+    }
+
+    public static String encode (String value)
+    {
+        try
+        {
+            return URLEncoder.encode(value, StandardCharsets.UTF_8.displayName());
+        }
+        catch (UnsupportedEncodingException e)
+        {
+            throw ManagementException.createInternalServerErrorManagementException("UTF8 encoding is unsupported", e);
+        }
+    }
+
+    public static String getParameter(String name, Map<String, List<String>> parameters)
+    {
+        List<String> values = parameters.get(name);
+        return values == null || values.isEmpty() ? null : values.get(0);
+    }
+
+    public static int getIntParameterFromRequest(final Map<String, List<String>> parameters,
+                                                 final String parameterName,
+                                                 final int defaultValue)
+    {
+        int intValue = defaultValue;
+        final String stringValue = getParameter(parameterName, parameters);
+        if (stringValue != null)
+        {
+            try
+            {
+                intValue = Integer.parseInt(stringValue);
+            }
+            catch (NumberFormatException e)
+            {
+                // noop
+            }
+        }
+        return intValue;
+    }
+}
diff --git a/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/controller/latest/LatestManagementController.java b/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/controller/latest/LatestManagementController.java
new file mode 100644
index 0000000..18b457a
--- /dev/null
+++ b/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/controller/latest/LatestManagementController.java
@@ -0,0 +1,863 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+package org.apache.qpid.server.management.plugin.controller.latest;
+
+import static org.apache.qpid.server.management.plugin.HttpManagementConfiguration.DEFAULT_PREFERENCE_OPERATION_TIMEOUT;
+import static org.apache.qpid.server.management.plugin.HttpManagementConfiguration.PREFERENCE_OPERTAION_TIMEOUT_CONTEXT_NAME;
+import static org.apache.qpid.server.management.plugin.ManagementException.createBadRequestManagementException;
+import static org.apache.qpid.server.management.plugin.ManagementException.createForbiddenManagementException;
+import static org.apache.qpid.server.management.plugin.ManagementException.createNotAllowedManagementException;
+import static org.apache.qpid.server.management.plugin.ManagementException.createNotFoundManagementException;
+import static org.apache.qpid.server.management.plugin.controller.ConverterHelper.getParameter;
+import static org.apache.qpid.server.management.plugin.servlet.rest.AbstractServlet.CONTENT_DISPOSITION_ATTACHMENT_FILENAME_PARAM;
+import static org.apache.qpid.server.model.ConfiguredObjectTypeRegistry.returnsCollectionOfConfiguredObjects;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.qpid.server.management.plugin.HttpManagementConfiguration;
+import org.apache.qpid.server.management.plugin.ManagementController;
+import org.apache.qpid.server.management.plugin.ManagementException;
+import org.apache.qpid.server.management.plugin.ManagementRequest;
+import org.apache.qpid.server.management.plugin.ManagementResponse;
+import org.apache.qpid.server.management.plugin.RequestType;
+import org.apache.qpid.server.management.plugin.ResponseType;
+import org.apache.qpid.server.management.plugin.controller.AbstractManagementController;
+import org.apache.qpid.server.management.plugin.controller.ControllerManagementResponse;
+import org.apache.qpid.server.management.plugin.controller.ConverterHelper;
+import org.apache.qpid.server.management.plugin.servlet.rest.ConfiguredObjectToMapConverter;
+import org.apache.qpid.server.management.plugin.servlet.rest.NotFoundException;
+import org.apache.qpid.server.management.plugin.servlet.rest.RequestInfo;
+import org.apache.qpid.server.management.plugin.servlet.rest.RestUserPreferenceHandler;
+import org.apache.qpid.server.model.AbstractConfigurationChangeListener;
+import org.apache.qpid.server.model.BrokerModel;
+import org.apache.qpid.server.model.ConfiguredObject;
+import org.apache.qpid.server.model.ConfiguredObjectFinder;
+import org.apache.qpid.server.model.ConfiguredObjectOperation;
+import org.apache.qpid.server.model.Model;
+import org.apache.qpid.server.model.State;
+import org.apache.qpid.server.model.preferences.UserPreferences;
+
+public class LatestManagementController extends AbstractManagementController
+{
+    private static final Logger LOGGER = LoggerFactory.getLogger(LatestManagementController.class);
+
+    private static final String DEPTH_PARAM = "depth";
+    private static final String OVERSIZE_PARAM = "oversize";
+    private static final String ACTUALS_PARAM = "actuals";
+    private static final String SORT_PARAM = "sort";
+    private static final String EXTRACT_INITIAL_CONFIG_PARAM = "extractInitialConfig";
+    private static final String EXCLUDE_INHERITED_CONTEXT_PARAM = "excludeInheritedContext";
+    private static final String SINGLETON_MODEL_OBJECT_RESPONSE_AS_LIST = "singletonModelObjectResponseAsList";
+    private static final Set<String> RESERVED_PARAMS =
+            new HashSet<>(Arrays.asList(DEPTH_PARAM,
+                                        SORT_PARAM,
+                                        OVERSIZE_PARAM,
+                                        ACTUALS_PARAM,
+                                        EXTRACT_INITIAL_CONFIG_PARAM,
+                                        CONTENT_DISPOSITION_ATTACHMENT_FILENAME_PARAM,
+                                        EXCLUDE_INHERITED_CONTEXT_PARAM,
+                                        SINGLETON_MODEL_OBJECT_RESPONSE_AS_LIST));
+
+    private static final int DEFAULT_DEPTH = 0;
+    private static final int DEFAULT_OVERSIZE = 120;
+
+
+    private final ConcurrentMap<ConfiguredObject<?>, ConfiguredObjectFinder> _configuredObjectFinders =
+            new ConcurrentHashMap<>();
+    private final Set<String> _supportedCategories;
+
+    private final ConfiguredObjectToMapConverter _objectConverter = new ConfiguredObjectToMapConverter();
+    private final RestUserPreferenceHandler _userPreferenceHandler;
+
+
+    LatestManagementController(final HttpManagementConfiguration<?> httpManagement)
+    {
+        final Long preferenceOperationTimeout =
+                httpManagement.getContextValue(Long.class, PREFERENCE_OPERTAION_TIMEOUT_CONTEXT_NAME);
+        _userPreferenceHandler = new RestUserPreferenceHandler(preferenceOperationTimeout == null
+                                                                       ? DEFAULT_PREFERENCE_OPERATION_TIMEOUT
+                                                                       : preferenceOperationTimeout);
+        _supportedCategories = Collections.unmodifiableSet(BrokerModel.getInstance()
+                                                                      .getSupportedCategories()
+                                                                      .stream()
+                                                                      .map(Class::getSimpleName)
+                                                                      .collect(Collectors.toSet()));
+    }
+
+    @Override
+    public String getVersion()
+    {
+        return BrokerModel.MODEL_VERSION;
+    }
+
+    @Override
+    public Set<String> getCategories()
+    {
+        return _supportedCategories;
+    }
+
+    @Override
+    public String getCategoryMapping(final String category)
+    {
+        return String.format("/api/v%s/%s/", getVersion(), category.toLowerCase());
+    }
+
+    @Override
+    public String getCategory(final ConfiguredObject<?> managedObject)
+    {
+        return managedObject.getCategoryClass().getSimpleName();
+    }
+
+    @Override
+    public List<String> getCategoryHierarchy(final ConfiguredObject<?> root, final String category)
+    {
+        ConfiguredObjectFinder finder = getConfiguredObjectFinder(root);
+        Class<? extends ConfiguredObject>[] hierarchy = finder.getHierarchy(category.toLowerCase());
+        if (hierarchy == null)
+        {
+            return Collections.emptyList();
+        }
+        return Arrays.stream(hierarchy).map(Class::getSimpleName).collect(Collectors.toList());
+    }
+
+    @Override
+    public ManagementController getNextVersionManagementController()
+    {
+        return null;
+    }
+
+    @Override
+    protected RequestType getRequestType(final ManagementRequest request) throws ManagementException
+    {
+        final ConfiguredObject<?> root = request.getRoot();
+        if (root == null)
+        {
+            final String message =
+                    String.format("No HTTP Management alias mapping found for '%s'", request.getRequestURL());
+            LOGGER.info(message);
+            throw createNotFoundManagementException(message);
+        }
+        final ConfiguredObjectFinder finder = getConfiguredObjectFinder(root);
+        final String category = request.getCategory();
+        final Class<? extends ConfiguredObject> configuredClass = getRequestCategoryClass(category, root.getModel());
+        final Class<? extends ConfiguredObject>[] hierarchy = finder.getHierarchy(configuredClass);
+        return getManagementRequestType(request.getMethod(), category, request.getPath(), hierarchy);
+    }
+
+    @Override
+    public Object get(final ConfiguredObject<?> root,
+                      final String category,
+                      final List<String> path,
+                      final Map<String, List<String>> parameters) throws ManagementException
+    {
+        try
+        {
+            final Predicate<ConfiguredObject<?>> filterPredicate = buildFilterPredicates(parameters);
+            final boolean singleObjectRequest = isFullPath(root, path, category) && !hasFilter(parameters);
+            final Collection<ConfiguredObject<?>> allObjects = getTargetObjects(root, category, path, filterPredicate);
+            if (singleObjectRequest)
+            {
+                if (allObjects.isEmpty())
+                {
+                    throw createNotFoundManagementException("Not Found");
+                }
+                else if (allObjects.size() != 1)
+                {
+                    throw createBadRequestManagementException(String.format(
+                            "Unexpected number of objects found [%d] for singleton request URI '%s'",
+                            allObjects.size(), ManagementException.getRequestURI(path, getCategoryMapping(category))));
+                }
+                else
+                {
+                    return allObjects.iterator().next();
+                }
+            }
+            else
+            {
+                return allObjects;
+            }
+        }
+        catch (RuntimeException e)
+        {
+            throw ManagementException.toManagementException(e, getCategoryMapping(category), path);
+        }
+        catch (Error e)
+        {
+            throw ManagementException.handleError(e);
+        }
+     }
+
+    @Override
+    public ConfiguredObject<?> createOrUpdate(final ConfiguredObject<?> root,
+                                              final String category,
+                                              final List<String> path,
+                                              final Map<String, Object> providedObject,
+                                              final boolean isPost) throws ManagementException
+    {
+        try
+        {
+            final List<String> hierarchy = getCategoryHierarchy(root, category);
+            if (path.isEmpty() && hierarchy.size() == 0)
+            {
+                root.setAttributes(providedObject);
+                return null;
+            }
+            final Class<? extends ConfiguredObject> categoryClass = getRequestCategoryClass(category, root.getModel());
+            ConfiguredObject theParent = root;
+            if (hierarchy.size() > 1)
+            {
+                final ConfiguredObjectFinder finder = getConfiguredObjectFinder(root);
+                theParent = finder.findObjectParentsFromPath(path, finder.getHierarchy(categoryClass), categoryClass);
+            }
+
+            final boolean isFullObjectURL = path.size() == hierarchy.size();
+            if (isFullObjectURL)
+            {
+                final String name = path.get(path.size() - 1);
+                final ConfiguredObject<?> configuredObject = theParent.getChildByName(categoryClass, name);
+                if (configuredObject != null)
+                {
+                    configuredObject.setAttributes(providedObject);
+                    return null;
+                }
+                else if (isPost)
+                {
+                    throw createNotFoundManagementException(String.format("%s '%s' not found",
+                                                                          categoryClass.getSimpleName(),
+                                                                          name));
+                }
+                else
+                {
+                    providedObject.put(ConfiguredObject.NAME, name);
+                }
+            }
+
+            return theParent.createChild(categoryClass, providedObject);
+        }
+        catch (RuntimeException e)
+        {
+            throw ManagementException.toManagementException(e, getCategoryMapping(category), path);
+        }
+        catch (Error e)
+        {
+            throw ManagementException.handleError(e);
+        }
+    }
+
+    @Override
+    public int delete(final ConfiguredObject<?> root,
+                      final String category,
+                      final List<String> names,
+                      final Map<String, List<String>> parameters) throws ManagementException
+    {
+        int counter = 0;
+        try
+        {
+            final Predicate<ConfiguredObject<?>> filterPredicate = buildFilterPredicates(parameters);
+            final Collection<ConfiguredObject<?>> allObjects = getTargetObjects(root, category, names, filterPredicate);
+            if (allObjects.isEmpty())
+            {
+                throw createNotFoundManagementException("Not Found");
+            }
+
+            for (ConfiguredObject o : allObjects)
+            {
+                o.delete();
+                counter++;
+            }
+        }
+        catch (RuntimeException e)
+        {
+            throw ManagementException.toManagementException(e, getCategoryMapping(category), names);
+        }
+        catch (Error e)
+        {
+            throw ManagementException.handleError(e);
+        }
+        return counter;
+    }
+
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public ManagementResponse invoke(final ConfiguredObject<?> root,
+                                     final String category,
+                                     final List<String> names,
+                                     final String operationName,
+                                     final Map<String, Object> operationArguments,
+                                     final boolean isPost,
+                                     final boolean isSecureOrAllowedOnInsecureChannel) throws ManagementException
+    {
+        ResponseType responseType = ResponseType.DATA;
+        Object returnValue;
+        try
+        {
+            final ConfiguredObject<?> target = getTarget(root, category, names);
+            final Map<String, ConfiguredObjectOperation<?>> availableOperations =
+                    root.getModel().getTypeRegistry().getOperations(target.getClass());
+            final ConfiguredObjectOperation operation = availableOperations.get(operationName);
+            if (operation == null)
+            {
+                throw createNotFoundManagementException(String.format("No such operation '%s' in '%s'",
+                                                                      operationName,
+                                                                      category));
+            }
+
+            if (operation.isSecure(target, operationArguments) && !isSecureOrAllowedOnInsecureChannel)
+            {
+                throw createForbiddenManagementException(String.format(
+                        "Operation '%s' can only be performed over a secure (HTTPS) connection",
+                        operationName));
+            }
+
+            if (!isPost && !operation.isNonModifying())
+            {
+                throw createNotAllowedManagementException(String.format(
+                        "Operation '%s' modifies the object so you must use POST.",
+                        operationName), Collections.singletonMap("Allow", "POST"));
+            }
+
+            returnValue = operation.perform(target, operationArguments);
+
+            if (ConfiguredObject.class.isAssignableFrom(operation.getReturnType())
+                || returnsCollectionOfConfiguredObjects(operation))
+            {
+                responseType = ResponseType.MODEL_OBJECT;
+            }
+
+        }
+        catch (RuntimeException e)
+        {
+            throw ManagementException.toManagementException(e, getCategoryMapping(category), names);
+        }
+        catch (Error e)
+        {
+            throw ManagementException.handleError(e);
+        }
+        return new ControllerManagementResponse(responseType,returnValue);
+    }
+
+    @Override
+    public Object getPreferences(final ConfiguredObject<?> root,
+                                 final String category,
+                                 final List<String> path,
+                                 final Map<String, List<String>> parameters) throws ManagementException
+    {
+        Object responseObject;
+        try
+        {
+            final List<String> hierarchy = getCategoryHierarchy(root, category);
+            final Collection<ConfiguredObject<?>> allObjects = getTargetObjects(root, category, path, null);
+            if (allObjects.isEmpty() && isFullPath(root, path, category))
+            {
+                throw createNotFoundManagementException("Not Found");
+            }
+
+            final RequestInfo requestInfo = RequestInfo.createPreferencesRequestInfo(path.subList(0, hierarchy.size()),
+                                                                                     path.subList(hierarchy.size() + 1,
+                                                                                                  path.size()),
+                                                                                     parameters);
+
+            if (path.contains("*"))
+            {
+                List<Object> preferencesList = new ArrayList<>(allObjects.size());
+                responseObject = preferencesList;
+                for (ConfiguredObject<?> target : allObjects)
+                {
+                    try
+                    {
+                        final UserPreferences userPreferences = target.getUserPreferences();
+                        final Object preferences = _userPreferenceHandler.handleGET(userPreferences, requestInfo);
+                        if (preferences == null
+                            || (preferences instanceof Collection && ((Collection) preferences).isEmpty())
+                            || (preferences instanceof Map && ((Map) preferences).isEmpty()))
+                        {
+                            continue;
+                        }
+                        preferencesList.add(preferences);
+                    }
+                    catch (NotFoundException e)
+                    {
+                        // The case where the preference's type and name is provided, but this particular object does not
+                        // have a matching preference.
+                    }
+                }
+            }
+            else
+            {
+                final ConfiguredObject<?> target = allObjects.iterator().next();
+                final UserPreferences userPreferences = target.getUserPreferences();
+                responseObject = _userPreferenceHandler.handleGET(userPreferences, requestInfo);
+            }
+        }
+        catch (RuntimeException e)
+        {
+            throw ManagementException.toManagementException(e, getCategoryMapping(category), path);
+        }
+        catch (Error e)
+        {
+            throw ManagementException.handleError(e);
+        }
+        return responseObject;
+    }
+
+    @Override
+    public void setPreferences(final ConfiguredObject<?> root,
+                               final String category,
+                               final List<String> path,
+                               final Object providedObject,
+                               final Map<String, List<String>> parameters,
+                               final boolean isPost) throws ManagementException
+
+    {
+        try
+        {
+            final List<String> hierarchy = getCategoryHierarchy(root, category);
+            final RequestInfo requestInfo = RequestInfo.createPreferencesRequestInfo(path.subList(0, hierarchy.size()),
+                                                                                     path.subList(hierarchy.size() + 1,
+                                                                                                  path.size()),
+                                                                                     parameters);
+            final ConfiguredObject<?> target = getTarget(root, category, requestInfo.getModelParts());
+            if (isPost)
+            {
+                _userPreferenceHandler.handlePOST(target, requestInfo, providedObject);
+            }
+            else
+            {
+                _userPreferenceHandler.handlePUT(target, requestInfo, providedObject);
+            }
+        }
+        catch (RuntimeException e)
+        {
+            throw ManagementException.toManagementException(e, getCategoryMapping(category), path);
+        }
+        catch (Error e)
+        {
+            throw ManagementException.handleError(e);
+        }
+    }
+
+    @Override
+    public int deletePreferences(final ConfiguredObject<?> root,
+                                 final String category,
+                                 final List<String> names,
+                                 final Map<String, List<String>> parameters) throws ManagementException
+    {
+        int counter = 0;
+        try
+        {
+            final List<String> hierarchy = getCategoryHierarchy(root, category);
+            final RequestInfo requestInfo = RequestInfo.createPreferencesRequestInfo(names.subList(0, hierarchy.size()),
+                                                                                     names.subList(hierarchy.size() + 1,
+                                                                                                   names.size()),
+                                                                                     parameters);
+            final Collection<ConfiguredObject<?>> objects = getTargetObjects(root,
+                                                                             category,
+                                                                             requestInfo.getModelParts(),
+                                                                             buildFilterPredicates(parameters));
+            if (objects == null)
+            {
+                throw createNotFoundManagementException("Not Found");
+            }
+
+            //TODO: define format how to report the results for bulk delete, i.e. how to report individual errors and success statuses
+            if (objects.size() > 1)
+            {
+                throw createBadRequestManagementException("Deletion of user preferences using wildcards is unsupported");
+            }
+
+            for (ConfiguredObject o : objects)
+            {
+                _userPreferenceHandler.handleDELETE(o.getUserPreferences(), requestInfo);
+                counter++;
+            }
+        }
+        catch (RuntimeException e)
+        {
+            throw ManagementException.toManagementException(e, getCategoryMapping(category), names);
+        }
+        catch (Error e)
+        {
+            throw ManagementException.handleError(e);
+        }
+        return counter;
+    }
+
+    @Override
+    public Object formatConfiguredObject(final Object content,
+                                         final Map<String, List<String>> parameters,
+                                         final boolean isSecureOrAllowedOnInsecureChannel)
+    {
+
+        final int depth = ConverterHelper.getIntParameterFromRequest(parameters, DEPTH_PARAM, DEFAULT_DEPTH);
+        final int oversizeThreshold = ConverterHelper.getIntParameterFromRequest(parameters, OVERSIZE_PARAM, DEFAULT_OVERSIZE);
+        final boolean actuals = Boolean.parseBoolean(getParameter(ACTUALS_PARAM, parameters));
+        final String excludeInheritedContextParameter = getParameter(EXCLUDE_INHERITED_CONTEXT_PARAM, parameters);
+        final boolean excludeInheritedContext = excludeInheritedContextParameter == null
+                                                || Boolean.parseBoolean(excludeInheritedContextParameter);
+        final boolean responseAsList =
+                Boolean.parseBoolean(getParameter(SINGLETON_MODEL_OBJECT_RESPONSE_AS_LIST, parameters));
+
+        if (content instanceof ConfiguredObject)
+        {
+            Object object = convertObject(
+                    (ConfiguredObject) content,
+                    depth,
+                    actuals,
+                    oversizeThreshold,
+                    isSecureOrAllowedOnInsecureChannel,
+                    excludeInheritedContext);
+            return responseAsList ? Collections.singletonList(object) : object;
+        }
+        else if (content instanceof Collection)
+        {
+            Collection<Map<String,Object>> results = ((Collection<?>) content).stream()
+                                                                              .filter(o -> o instanceof ConfiguredObject)
+                                                                              .map(ConfiguredObject.class::cast)
+                                                                              .map(o -> convertObject(
+                                                                                      o,
+                                                                                      depth,
+                                                                                      actuals,
+                                                                                      oversizeThreshold,
+                                                                                      isSecureOrAllowedOnInsecureChannel,
+                                                                                      excludeInheritedContext)).collect(Collectors.toSet());
+            if (!results.isEmpty())
+            {
+                return results;
+            }
+        }
+        return content;
+    }
+
+    private Map<String,Object> convertObject(final ConfiguredObject<?> configuredObject, final int depth,
+                                 final boolean actuals,
+                                 final int oversizeThreshold,
+                                 final boolean isSecureOrConfidentialOperationAllowedOnInsecureChannel,
+                                 final boolean excludeInheritedContext)
+    {
+        return _objectConverter.convertObjectToMap(configuredObject, configuredObject.getCategoryClass(),
+                                                   new ConfiguredObjectToMapConverter.ConverterOptions(
+                                                           depth,
+                                                           actuals,
+                                                           oversizeThreshold,
+                                                           isSecureOrConfidentialOperationAllowedOnInsecureChannel,
+                                                           excludeInheritedContext));
+    }
+
+    private boolean isFullPath(final ConfiguredObject root, final List<String> parts, final String category)
+    {
+        List<String> hierarchy = getCategoryHierarchy(root, category);
+        return parts.size() == hierarchy.size() && !parts.contains("*");
+    }
+
+    private ConfiguredObjectFinder getConfiguredObjectFinder(final ConfiguredObject<?> root)
+    {
+        ConfiguredObjectFinder finder = _configuredObjectFinders.get(root);
+        if (finder == null)
+        {
+            finder = new ConfiguredObjectFinder(root);
+            final ConfiguredObjectFinder existingValue = _configuredObjectFinders.putIfAbsent(root, finder);
+            if (existingValue != null)
+            {
+                finder = existingValue;
+            }
+            else
+            {
+                final AbstractConfigurationChangeListener deletionListener =
+                        new AbstractConfigurationChangeListener()
+                        {
+                            @Override
+                            public void stateChanged(final ConfiguredObject<?> object,
+                                                     final State oldState,
+                                                     final State newState)
+                            {
+                                if (newState == State.DELETED)
+                                {
+                                    _configuredObjectFinders.remove(root);
+                                }
+                            }
+                        };
+                root.addChangeListener(deletionListener);
+                if (root.getState() == State.DELETED)
+                {
+                    _configuredObjectFinders.remove(root);
+                    root.removeChangeListener(deletionListener);
+                }
+            }
+        }
+        return finder;
+    }
+
+    private Collection<ConfiguredObject<?>> getTargetObjects(final ConfiguredObject<?> root,
+                                                             final String category,
+                                                             final List<String> path,
+                                                             final Predicate<ConfiguredObject<?>> filterPredicate)
+    {
+        final ConfiguredObjectFinder finder = getConfiguredObjectFinder(root);
+        final Class<? extends ConfiguredObject> configuredClass = getRequestCategoryClass(category, root.getModel());
+        Collection<ConfiguredObject<?>> targetObjects =
+                finder.findObjectsFromPath(path, finder.getHierarchy(configuredClass), true);
+
+        if (targetObjects == null)
+        {
+            targetObjects = Collections.emptySet();
+        }
+        else if (filterPredicate != null)
+        {
+            targetObjects = targetObjects.stream().filter(filterPredicate).collect(Collectors.toList());
+        }
+        return targetObjects;
+    }
+
+    private RequestType getManagementRequestType(final String method,
+                                                 final String categoryName,
+                                                 final List<String> parts,
+                                                 final Class<? extends ConfiguredObject>[] hierarchy)
+    {
+        String servletPath = getCategoryMapping(categoryName);
+        if ("POST".equals(method))
+        {
+            return getPostRequestType(parts, hierarchy, servletPath);
+        }
+        else if ("PUT".equals(method))
+        {
+            return getPutRequestType(parts, hierarchy, servletPath);
+        }
+        else if ("GET".equals(method))
+        {
+            return getGetRequestType(parts, hierarchy, servletPath);
+        }
+        else if ("DELETE".equals(method))
+        {
+            return getDeleteRequestType(parts, hierarchy, servletPath);
+        }
+        else
+        {
+            throw createBadRequestManagementException(String.format("Unexpected method type '%s' for path '%s/%s'",
+                                                                    method,
+                                                                    servletPath,
+                                                                    String.join("/", parts)));
+        }
+    }
+
+    private RequestType getDeleteRequestType(final List<String> parts,
+                                             final Class<? extends ConfiguredObject>[] hierarchy,
+                                             final String servletPath)
+    {
+        if (parts.size() <= hierarchy.length)
+        {
+            return RequestType.MODEL_OBJECT;
+        }
+        else
+        {
+            if (USER_PREFERENCES.equals(parts.get(hierarchy.length)))
+            {
+                return RequestType.USER_PREFERENCES;
+            }
+        }
+        final String expectedPath = buildExpectedPath(servletPath, Arrays.asList(hierarchy));
+        throw createBadRequestManagementException(String.format(
+                "Invalid DELETE path '%s/%s'. Expected: '%s' or '%s/userpreferences[/<preference type>[/<preference name>]]'",
+                servletPath,
+                String.join("/", parts),
+                expectedPath,
+                expectedPath));
+    }
+
+    private RequestType getGetRequestType(final List<String> parts,
+                                          final Class<? extends ConfiguredObject>[] hierarchy,
+                                          final String servletPath)
+    {
+        if (parts.size() <= hierarchy.length)
+        {
+            return RequestType.MODEL_OBJECT;
+        }
+        else
+        {
+            if (USER_PREFERENCES.equals(parts.get(hierarchy.length)))
+            {
+                return RequestType.USER_PREFERENCES;
+            }
+            else if (VISIBLE_USER_PREFERENCES.equals(parts.get(hierarchy.length)))
+            {
+                return RequestType.VISIBLE_PREFERENCES;
+            }
+            else if (parts.size() == hierarchy.length + 1)
+            {
+                return RequestType.OPERATION;
+            }
+        }
+        final String expectedPath = buildExpectedPath(servletPath, Arrays.asList(hierarchy));
+        throw new IllegalArgumentException(String.format("Invalid GET path '%s/%s'. Expected: '%s[/<operation name>]'",
+                                                         servletPath,
+                                                         String.join("/", parts),
+                                                         expectedPath));
+    }
+
+    private RequestType getPutRequestType(final List<String> parts,
+                                          final Class<? extends ConfiguredObject>[] hierarchy,
+                                          final String servletPath)
+    {
+        if (parts.size() == hierarchy.length || parts.size() == hierarchy.length - 1)
+        {
+            return RequestType.MODEL_OBJECT;
+        }
+        else if (parts.size() > hierarchy.length && USER_PREFERENCES.equals(parts.get(hierarchy.length)))
+        {
+            return RequestType.USER_PREFERENCES;
+        }
+        else
+        {
+            final String expectedPath = buildExpectedPath(servletPath, Arrays.asList(hierarchy));
+            throw createBadRequestManagementException(String.format("Invalid PUT path '%s/%s'. Expected: '%s'",
+                                                                    servletPath,
+                                                                    String.join("/", parts),
+                                                                    expectedPath));
+        }
+    }
+
+    private RequestType getPostRequestType(final List<String> parts,
+                                           final Class<? extends ConfiguredObject>[] hierarchy,
+                                           final String servletPath)
+    {
+        if (parts.size() == hierarchy.length || parts.size() == hierarchy.length - 1)
+        {
+            return RequestType.MODEL_OBJECT;
+        }
+        else if (parts.size() > hierarchy.length)
+        {
+            if (USER_PREFERENCES.equals(parts.get(hierarchy.length)))
+            {
+                return RequestType.USER_PREFERENCES;
+            }
+            else if (parts.size() == hierarchy.length + 1
+                     && !VISIBLE_USER_PREFERENCES.equals(parts.get(hierarchy.length)))
+            {
+                return RequestType.OPERATION;
+            }
+        }
+        final List<Class<? extends ConfiguredObject>> hierarchyList = Arrays.asList(hierarchy);
+        final String expectedFullPath = buildExpectedPath(servletPath, hierarchyList);
+        final String expectedParentPath = buildExpectedPath(servletPath, hierarchyList.subList(0, hierarchy.length - 1));
+
+        throw createBadRequestManagementException(String.format(
+                "Invalid POST path '%s/%s'. Expected: '%s/<operation name>'"
+                + " or '%s'"
+                + " or '%s/userpreferences[/<preference type>]'",
+                servletPath,
+                String.join("/", parts),
+                expectedFullPath,
+                expectedParentPath,
+                expectedFullPath));
+    }
+
+    private Class<? extends ConfiguredObject> getRequestCategoryClass(final String categoryName,
+                                                                      final Model model)
+    {
+        for (Class<? extends ConfiguredObject> category : model.getSupportedCategories())
+        {
+            if (category.getSimpleName().toLowerCase().equals(categoryName.toLowerCase()))
+            {
+                return category;
+            }
+        }
+        throw createNotFoundManagementException(String.format("Category is not found for '%s'", categoryName));
+    }
+
+    private String buildExpectedPath(final String servletPath, final List<Class<? extends ConfiguredObject>> hierarchy)
+    {
+        final StringBuilder expectedPath = new StringBuilder(servletPath);
+        for (Class<? extends ConfiguredObject> part : hierarchy)
+        {
+            expectedPath.append("/<");
+            expectedPath.append(part.getSimpleName().toLowerCase());
+            expectedPath.append(" name>");
+        }
+        return expectedPath.toString();
+    }
+
+    private Predicate<ConfiguredObject<?>> buildFilterPredicates(final Map<String, List<String>> parameters)
+    {
+        return parameters.entrySet().stream()
+                  .filter(entry -> !RESERVED_PARAMS.contains(entry.getKey()))
+                  .map(entry -> {
+                      final String paramName = entry.getKey();
+                      final List<String> allowedValues = entry.getValue();
+                      return (Predicate<ConfiguredObject<?>>) object -> {
+                          Object value = object.getAttribute(paramName);
+                          return allowedValues.contains(String.valueOf(value));
+                      };
+                  }).reduce(Predicate::and).orElse(t -> true);
+    }
+
+    private ConfiguredObject<?> getTarget(final ConfiguredObject<?> root,
+                                          final String category,
+                                          final List<String> names)
+    {
+        final Class<? extends ConfiguredObject> configuredClass = getRequestCategoryClass(category, root.getModel());
+        final ConfiguredObject<?> target;
+        final ConfiguredObjectFinder finder = getConfiguredObjectFinder(root);
+        final Class<? extends ConfiguredObject>[] hierarchy = finder.getHierarchy(configuredClass);
+        if (names.isEmpty() && hierarchy.length == 0)
+        {
+            target = root;
+        }
+        else
+        {
+            ConfiguredObject theParent = root;
+            if (hierarchy.length > 1)
+            {
+                theParent = finder.findObjectParentsFromPath(names, hierarchy, configuredClass);
+            }
+            final String name = names.get(names.size() - 1);
+            target = theParent.getChildByName(configuredClass, name);
+            if (target == null)
+            {
+
+                final String errorMessage = String.format("%s '%s' not found",
+                                                          configuredClass.getSimpleName(),
+                                                          String.join("/", names));
+                throw createNotFoundManagementException(errorMessage);
+            }
+        }
+        return target;
+    }
+
+    private boolean hasFilter(Map<String, List<String>> parameters)
+    {
+        return parameters.keySet().stream().anyMatch(parameter -> !RESERVED_PARAMS.contains(parameter));
+    }
+
+}
diff --git a/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/controller/latest/LatestManagementControllerFactory.java b/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/controller/latest/LatestManagementControllerFactory.java
new file mode 100644
index 0000000..82ba07e
--- /dev/null
+++ b/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/controller/latest/LatestManagementControllerFactory.java
@@ -0,0 +1,56 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+package org.apache.qpid.server.management.plugin.controller.latest;
+
+import org.apache.qpid.server.management.plugin.HttpManagementConfiguration;
+import org.apache.qpid.server.management.plugin.ManagementController;
+import org.apache.qpid.server.management.plugin.ManagementControllerFactory;
+import org.apache.qpid.server.model.BrokerModel;
+import org.apache.qpid.server.plugin.PluggableService;
+
+@PluggableService
+public class LatestManagementControllerFactory implements ManagementControllerFactory
+{
+    @Override
+    public String getType()
+    {
+        return "org.apache.qpid.server.management.plugin.model.latest";
+    }
+
+    @Override
+    public String getVersion()
+    {
+        return BrokerModel.MODEL_VERSION;
+    }
+
+    @Override
+    public String getPreviousVersion()
+    {
+        return "7.0";
+    }
+
+    @Override
+    public ManagementController createManagementController(final HttpManagementConfiguration<?> httpManagement,
+                                                           final ManagementController nextVersionManagementController)
+    {
+        return new LatestManagementController(httpManagement);
+    }
+}
diff --git a/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/servlet/rest/RequestInfo.java b/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/servlet/rest/RequestInfo.java
index 69de651..cc67cd7 100644
--- a/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/servlet/rest/RequestInfo.java
+++ b/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/servlet/rest/RequestInfo.java
@@ -26,6 +26,8 @@ import java.util.Map;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 
+import org.apache.qpid.server.management.plugin.RequestType;
+
 public class RequestInfo
 {
     private final RequestType _type;
@@ -124,8 +126,5 @@ public class RequestInfo
         return _hierarchySatisfied && !_hasWildcard;
     }
 
-    enum RequestType
-    {
-        OPERATION, USER_PREFERENCES, VISIBLE_PREFERENCES, MODEL_OBJECT
-    }
+
 }
diff --git a/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/servlet/rest/RestServlet.java b/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/servlet/rest/RestServlet.java
index 4e5e524..9a53d11 100644
--- a/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/servlet/rest/RestServlet.java
+++ b/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/servlet/rest/RestServlet.java
@@ -19,82 +19,51 @@
 
 package org.apache.qpid.server.management.plugin.servlet.rest;
 
-import static org.apache.qpid.server.management.plugin.HttpManagementConfiguration.DEFAULT_PREFERENCE_OPERATION_TIMEOUT;
-import static org.apache.qpid.server.management.plugin.HttpManagementConfiguration.PREFERENCE_OPERTAION_TIMEOUT_CONTEXT_NAME;
-import static org.apache.qpid.server.management.plugin.HttpManagementUtil.ensureFilenameIsRfc2183;
-import static org.apache.qpid.server.model.ConfiguredObjectTypeRegistry.getCollectionMemberType;
-import static org.apache.qpid.server.model.ConfiguredObjectTypeRegistry.returnsCollectionOfConfiguredObjects;
-
 import java.io.IOException;
-import java.lang.reflect.ParameterizedType;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
+import java.util.stream.Collectors;
 
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletContext;
 import javax.servlet.ServletException;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import javax.servlet.http.Part;
 
-import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
-import com.google.common.base.Joiner;
-import com.google.common.base.Predicate;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.google.common.base.Strings;
 
-import org.apache.qpid.server.configuration.IllegalConfigurationException;
 import org.apache.qpid.server.management.plugin.HttpManagementUtil;
-import org.apache.qpid.server.model.AbstractConfiguredObject;
+import org.apache.qpid.server.management.plugin.ManagementController;
+import org.apache.qpid.server.management.plugin.ManagementException;
+import org.apache.qpid.server.management.plugin.ManagementRequest;
+import org.apache.qpid.server.management.plugin.ManagementResponse;
+import org.apache.qpid.server.management.plugin.ResponseType;
 import org.apache.qpid.server.model.ConfiguredObject;
-import org.apache.qpid.server.model.ConfiguredObjectFinder;
-import org.apache.qpid.server.model.ConfiguredObjectOperation;
+import org.apache.qpid.server.model.ConfiguredObjectJacksonModule;
 import org.apache.qpid.server.model.Content;
-import org.apache.qpid.server.model.IllegalStateTransitionException;
-import org.apache.qpid.server.model.IntegrityViolationException;
-import org.apache.qpid.server.model.Model;
-import org.apache.qpid.server.model.OperationTimeoutException;
-import org.apache.qpid.server.model.preferences.UserPreferences;
+import org.apache.qpid.server.model.port.HttpPort;
 import org.apache.qpid.server.util.DataUrlUtils;
-import org.apache.qpid.server.util.ExternalServiceException;
-import org.apache.qpid.server.util.ExternalServiceTimeoutException;
-import org.apache.qpid.server.util.urlstreamhandler.data.Handler;
 
 public class RestServlet extends AbstractServlet
 {
     private static final long serialVersionUID = 1L;
+    private static final String APPLICATION_JSON = "application/json";
 
-    private static final Logger LOGGER = LoggerFactory.getLogger(RestServlet.class);
-
-    public static final String DEPTH_PARAM = "depth";
-    public static final String OVERSIZE_PARAM = "oversize";
-    public static final String ACTUALS_PARAM = "actuals";
-    public static final String SORT_PARAM = "sort";
-    public static final String EXTRACT_INITIAL_CONFIG_PARAM = "extractInitialConfig";
-    public static final String EXCLUDE_INHERITED_CONTEXT_PARAM = "excludeInheritedContext";
-    private static final String SINGLETON_MODEL_OBJECT_RESPONSE_AS_LIST = "singletonModelObjectResponseAsList";
-    public static final Set<String> RESERVED_PARAMS =
-            new HashSet<>(Arrays.asList(DEPTH_PARAM,
-                                        SORT_PARAM,
-                                        OVERSIZE_PARAM,
-                                        ACTUALS_PARAM,
-                                        EXTRACT_INITIAL_CONFIG_PARAM,
-                                        CONTENT_DISPOSITION_ATTACHMENT_FILENAME_PARAM,
-                                        EXCLUDE_INHERITED_CONTEXT_PARAM,
-                                        SINGLETON_MODEL_OBJECT_RESPONSE_AS_LIST));
-    public static final int DEFAULT_DEPTH = 0;
-    public static final int DEFAULT_OVERSIZE = 120;
-
-    private transient final ConfiguredObjectToMapConverter _objectConverter = new ConfiguredObjectToMapConverter();
-    private transient RestUserPreferenceHandler _userPreferenceHandler;
+    private transient ManagementController _managementController;
 
     @SuppressWarnings("unused")
     public RestServlet()
@@ -107,505 +76,178 @@ public class RestServlet extends AbstractServlet
     {
         super.init();
 
-        Handler.register();
-        Long preferenceOperationTimeout = getManagementConfiguration().getContextValue(Long.class, PREFERENCE_OPERTAION_TIMEOUT_CONTEXT_NAME);
-        _userPreferenceHandler = new RestUserPreferenceHandler(preferenceOperationTimeout == null
-                                                                       ? DEFAULT_PREFERENCE_OPERATION_TIMEOUT
-                                                                       : preferenceOperationTimeout);
-    }
-
-
-    private Collection<ConfiguredObject<?>> getTargetObjects(final Class<? extends ConfiguredObject> configuredClass,
-                                                             final ConfiguredObjectFinder finder,
-                                                             RequestInfo requestInfo,
-                                                             List<Predicate<ConfiguredObject<?>>> filterPredicateList)
-    {
-        List<String> names = requestInfo.getModelParts();
+        final ServletConfig servletConfig = getServletConfig();
+        final ServletContext servletContext = servletConfig.getServletContext();
 
-        Collection<ConfiguredObject<?>> targetObjects = finder.findObjectsFromPath(names, finder.getHierarchy(configuredClass), true);
+        final String modelVersion = servletConfig.getInitParameter("qpid.controller.version");
+        if (modelVersion == null)
+        {
+            throw new ServletException("Controller version is not specified");
+        }
 
-        if (!(targetObjects == null || filterPredicateList.isEmpty()))
+        @SuppressWarnings("uncjecked")
+        ManagementController controller = (ManagementController) servletContext.getAttribute("qpid.controller.chain");
+        do
         {
-            Iterator<ConfiguredObject<?>> iter = targetObjects.iterator();
-            while (iter.hasNext())
+            if (controller.getVersion().equals(modelVersion))
             {
-                ConfiguredObject obj = iter.next();
-                for (Predicate<ConfiguredObject<?>> predicate : filterPredicateList)
-                {
-                    if (!predicate.apply(obj))
-                    {
-                        iter.remove();
-                        break;
-                    }
-                }
+                _managementController = controller;
+                break;
             }
-
+            controller = controller.getNextVersionManagementController();
         }
-        return targetObjects;
-    }
+        while (controller != null);
 
-    private List<Predicate<ConfiguredObject<?>>> buildFilterPredicates(final HttpServletRequest request)
-    {
-        List<Predicate<ConfiguredObject<?>>> predicates = new ArrayList<>();
-
-        for (final String paramName : Collections.list(request.getParameterNames()))
+        if (_managementController == null)
         {
-            if (!RESERVED_PARAMS.contains(paramName))
-            {
-                final List<String> allowedValues = Arrays.asList(request.getParameterValues(paramName));
-
-                predicates.add(new Predicate<ConfiguredObject<?>>()
-                {
-                    @Override
-                    public boolean apply(final ConfiguredObject<?> obj)
-                    {
-                        Object value = obj.getAttribute(paramName);
-                        return allowedValues.contains(String.valueOf(value));
-                    }
-                });
-            }
+            throw new ServletException("Controller is not found");
         }
-        return Collections.unmodifiableList(predicates);
     }
 
     @Override
-    protected void doGet(HttpServletRequest request,
-                         HttpServletResponse response,
+    protected void doGet(final HttpServletRequest httpServletRequest,
+                         final HttpServletResponse httpServletResponse,
                          final ConfiguredObject<?> managedObject)
-            throws ServletException, IOException
+            throws IOException
     {
-        ConfiguredObjectFinder finder = getConfiguredObjectFinder(managedObject);
-        Class<? extends ConfiguredObject> configuredClass = getConfiguredClass(request, managedObject);
-        if(configuredClass == null)
-        {
-            sendError(response, HttpServletResponse.SC_NOT_FOUND);
-            return;
-        }
-        final Class<? extends ConfiguredObject>[] hierarchy = finder.getHierarchy(configuredClass);
-        if(hierarchy == null)
-        {
-            sendError(response, HttpServletResponse.SC_NOT_FOUND);
-            return;
-        }
-
-        RequestInfoParser requestInfoParser = new RequestInfoParser(hierarchy);
-
-        RequestInfo requestInfo = requestInfoParser.parse(request);
-        switch (requestInfo.getType())
+        try
         {
-            case OPERATION:
-            {
-                doOperation(requestInfo, managedObject, configuredClass, finder, request, response);
-                break;
-            }
-            case MODEL_OBJECT:
-            {
-                // TODO - sort special params, everything else should act as a filter
-                String attachmentFilename = request.getParameter(CONTENT_DISPOSITION_ATTACHMENT_FILENAME_PARAM);
-
-                if (attachmentFilename != null)
-                {
-                    setContentDispositionHeaderIfNecessary(response, attachmentFilename);
-                }
+            final ManagementRequest request = new ServletManagementRequest(managedObject, httpServletRequest);
+            final ManagementController controller = getManagementController();
+            final ManagementResponse response = controller.handleGet(request);
 
-                List<Predicate<ConfiguredObject<?>>> filterPredicateList = buildFilterPredicates(request);
-                Collection<ConfiguredObject<?>> allObjects =
-                        getTargetObjects(configuredClass, finder, requestInfo, filterPredicateList);
-
-                boolean singleObjectRequest = requestInfo.isSingletonRequest() && filterPredicateList.isEmpty();
-
-                if (allObjects == null || (allObjects.isEmpty() && singleObjectRequest))
-                {
-                    sendJsonErrorResponse(request, response, HttpServletResponse.SC_NOT_FOUND, "Not Found");
-                    return;
-                }
-
-                int depth;
-                boolean actuals;
-                int oversizeThreshold;
-                boolean excludeInheritedContext;
-
-                depth = getIntParameterFromRequest(request, DEPTH_PARAM, DEFAULT_DEPTH);
-                oversizeThreshold = getIntParameterFromRequest(request, OVERSIZE_PARAM, DEFAULT_OVERSIZE);
-                actuals = getBooleanParameterFromRequest(request, ACTUALS_PARAM);
-                String excludeInheritedContextParameter = request.getParameter(EXCLUDE_INHERITED_CONTEXT_PARAM);
-
-                excludeInheritedContext = excludeInheritedContextParameter == null || Boolean.parseBoolean(
-                        excludeInheritedContextParameter);
-
-                boolean responseAsList = Boolean.parseBoolean(request.getParameter(SINGLETON_MODEL_OBJECT_RESPONSE_AS_LIST));
-                final Object responseObject;
-                if (!responseAsList && singleObjectRequest)
-                {
-                    if (allObjects.size() != 1)
-                    {
-                        throw new IllegalStateException(String.format(
-                                "Unexpected number of objects found [%d] for singleton request URI '%s'",
-                                allObjects.size(), request.getRequestURI()));
-                    }
-                    ConfiguredObject<?> singletonObject = allObjects.iterator().next();
-                    responseObject = _objectConverter.convertObjectToMap(singletonObject, configuredClass,
-                                                                         new ConfiguredObjectToMapConverter
-                                                                                 .ConverterOptions(
-                                                                                 depth,
-                                                                                 actuals,
-                                                                                 oversizeThreshold,
-                                                                                 request.isSecure(),
-                                                                                 excludeInheritedContext));
-                }
-                else
-                {
-                    final List<Object> outputList = new ArrayList<>();
-                    for (ConfiguredObject configuredObject : allObjects)
-                    {
-
-                        outputList.add(_objectConverter.convertObjectToMap(configuredObject, configuredClass,
-                                                                           new ConfiguredObjectToMapConverter.ConverterOptions(
-                                                                                   depth,
-                                                                                   actuals,
-                                                                                   oversizeThreshold,
-                                                                                   request.isSecure(),
-                                                                                   excludeInheritedContext)));
-                    }
-
-                    responseObject = outputList;
-                }
-
-                boolean sendCachingHeaders = attachmentFilename == null;
-                sendJsonResponse(responseObject,
-                                 request,
-                                 response,
-                                 HttpServletResponse.SC_OK,
-                                 sendCachingHeaders);
-                break;
-            }
-            case VISIBLE_PREFERENCES:
-            case USER_PREFERENCES:
-            {
-                doGetUserPreferences(managedObject, configuredClass, finder, requestInfo, request, response);
-                break;
-            }
-
-            default:
-            {
-                throw new IllegalStateException(String.format("Unexpected request type '%s' for path '%s'",
-                                                              requestInfo.getType(),
-                                                              request.getPathInfo()));
-            }
-        }
-    }
-
-
-    private boolean isSingleObjectRequest(final RequestInfo requestInfo,
-                                          final Class<? extends ConfiguredObject>[] hierarchy)
-    {
-        if (hierarchy.length > 0)
-        {
-            List<String> pathInfoElements = requestInfo.getModelParts();
-            return pathInfoElements.size() == hierarchy.length;
+            sendResponse(request, response, httpServletRequest, httpServletResponse, controller);
         }
-
-        return false;
-    }
-
-    private Class<? extends ConfiguredObject> getConfiguredClass(HttpServletRequest request, ConfiguredObject<?> managedObject)
-    {
-        final String[] servletPathElements = request.getServletPath().split("/");
-        String categoryName = servletPathElements[servletPathElements.length-1];
-        Model model = managedObject.getModel();
-        for(Class<? extends ConfiguredObject> category : model.getSupportedCategories())
+        catch (ManagementException e)
         {
-            if(category.getSimpleName().toLowerCase().equals(categoryName))
-            {
-                return category;
-            }
+            sendResponse(e, httpServletRequest, httpServletResponse);
         }
-        return null;
-    }
-
-    @Override
-    protected void doPut(HttpServletRequest request,
-                         HttpServletResponse response,
-                         final ConfiguredObject<?> managedObject)
-            throws ServletException, IOException
-    {
-        performCreateOrUpdate(request, response, managedObject);
     }
 
     @Override
-    protected void service(HttpServletRequest request, HttpServletResponse response)
-            throws ServletException, IOException
+    protected void doPost(final HttpServletRequest httpServletRequest,
+                          final HttpServletResponse httpServletResponse,
+                          final ConfiguredObject<?> managedObject) throws IOException
     {
         try
         {
-            super.service(request, response);
+            final ManagementRequest request = new ServletManagementRequest(managedObject, httpServletRequest);
+            final ManagementController controller = getManagementController();
+            final ManagementResponse response = controller.handlePost(request);
+
+            sendResponse(request, response, httpServletRequest, httpServletResponse, controller);
         }
-        catch (Exception | NoClassDefFoundError e)
+        catch (ManagementException e)
         {
-            setResponseStatus(request, response, e);
+            sendResponse(e, httpServletRequest, httpServletResponse);
         }
     }
 
-    private void performCreateOrUpdate(HttpServletRequest request,
-                                       HttpServletResponse response,
-                                       final ConfiguredObject<?> managedObject)
-            throws IOException, ServletException
+    @Override
+    protected void doPut(final HttpServletRequest httpServletRequest,
+                         final HttpServletResponse httpServletResponse,
+                         final ConfiguredObject<?> managedObject) throws IOException
     {
-
-        ConfiguredObjectFinder finder = getConfiguredObjectFinder(managedObject);
-        final Class<? extends ConfiguredObject> configuredClass = getConfiguredClass(request, managedObject);
-        final Class<? extends ConfiguredObject>[] hierarchy = finder.getHierarchy(configuredClass);
-        RequestInfoParser requestInfoParser = new RequestInfoParser(hierarchy);
-
-        response.setContentType("application/json");
-
-        RequestInfo requestInfo = requestInfoParser.parse(request);
-        switch (requestInfo.getType())
+        try
         {
-            case MODEL_OBJECT:
-            {
-                List<String> names = requestInfo.getModelParts();
-                boolean isFullObjectURL = names.size() == hierarchy.length;
-                Map<String, Object> providedObject = getRequestProvidedObject(request, requestInfo);
-                if (names.isEmpty() && hierarchy.length == 0)
-                {
-                    managedObject.setAttributes(providedObject);
-                    response.setStatus(HttpServletResponse.SC_OK);
-                    return;
-                }
-
-                ConfiguredObject theParent = managedObject;
-                Class<? extends ConfiguredObject> objClass = configuredClass;
-                if (hierarchy.length > 1)
-                {
+            final ManagementRequest request = new ServletManagementRequest(managedObject, httpServletRequest);
+            final ManagementController controller = getManagementController();
+            final ManagementResponse response = controller.handlePut(request);
 
-                    theParent = finder.findObjectParentsFromPath(names, hierarchy, configuredClass);
-                }
-
-                if (isFullObjectURL)
-                {
-                    String name = names.get(names.size() - 1);
-                    ConfiguredObject<?> configuredObject = theParent.getChildByName(objClass, name);
-
-                    if (configuredObject != null)
-                    {
-                        configuredObject.setAttributes(providedObject);
-                        response.setStatus(HttpServletResponse.SC_OK);
-                        return;
-                    }
-                    else if ("POST".equalsIgnoreCase(request.getMethod()))
-                    {
-                        sendJsonErrorResponse(request, response,
-                                              HttpServletResponse.SC_NOT_FOUND,
-                                              String.format("%s '%s' not found", configuredClass.getSimpleName(), name));
-                        return;
-                    }
-                    else
-                    {
-                        providedObject.put(ConfiguredObject.NAME, name);
-                    }
-                }
-
-                ConfiguredObject<?> configuredObject = theParent.createChild(objClass, providedObject);
-                StringBuffer requestURL = request.getRequestURL();
-                if (!isFullObjectURL)
-                {
-                    requestURL.append("/").append(configuredObject.getName());
-                }
-                response.setHeader("Location", requestURL.toString());
-                response.setStatus(HttpServletResponse.SC_CREATED);
-                break;
-            }
-            case OPERATION:
-            {
-                doOperation(requestInfo, managedObject, configuredClass, finder, request, response);
-                break;
-            }
-            case USER_PREFERENCES:
-            {
-                doPostOrPutUserPreference(requestInfo, managedObject, configuredClass, finder, request, response);
-                break;
-            }
-            default:
-            {
-                throw new IllegalStateException(String.format("Unexpected request type '%s' for path '%s'",
-                                                              requestInfo.getType(),
-                                                              request.getPathInfo()));
-            }
+            sendResponse(request, response, httpServletRequest, httpServletResponse, controller);
+        }
+        catch (ManagementException e)
+        {
+            sendResponse(e, httpServletRequest, httpServletResponse);
         }
     }
 
-    private void doGetUserPreferences(final ConfiguredObject<?> managedObject,
-                                      final Class<? extends ConfiguredObject> configuredClass,
-                                      final ConfiguredObjectFinder finder, final RequestInfo requestInfo,
-                                      final HttpServletRequest request,
-                                      final HttpServletResponse response) throws IOException, ServletException
+    @Override
+    protected void doDelete(final HttpServletRequest httpServletRequest,
+                            final HttpServletResponse httpServletResponse,
+                            final ConfiguredObject<?> managedObject) throws IOException
     {
-        Collection<ConfiguredObject<?>> allObjects = getTargetObjects(
-                configuredClass,
-                                                                      finder,
-                                                                      requestInfo,
-                                                                      Collections.<Predicate<ConfiguredObject<?>>>emptyList());
-
-        if (allObjects == null || (allObjects.isEmpty() && isSingleObjectRequest(requestInfo, finder.getHierarchy(configuredClass))))
+        try
         {
-            sendJsonErrorResponse(request, response, HttpServletResponse.SC_NOT_FOUND, "Not Found");
-            return;
-        }
+            final ManagementRequest request = new ServletManagementRequest(managedObject, httpServletRequest);
+            final ManagementController controller = getManagementController();
+            final ManagementResponse response = controller.handleDelete(request);
 
-        final Object responseObject;
-        if (requestInfo.hasWildcard())
-        {
-            responseObject = new ArrayList<>(allObjects.size());
-            for (ConfiguredObject<?> target : allObjects)
-            {
-                final UserPreferences userPreferences = target.getUserPreferences();
-                try
-                {
-                    final Object preferences = _userPreferenceHandler.handleGET(userPreferences, requestInfo);
-                    if (preferences == null || (preferences instanceof Collection
-                                                && ((Collection) preferences).isEmpty()) || (preferences instanceof Map
-                                                                                             && ((Map) preferences).isEmpty()))
-                    {
-                        continue;
-                    }
-                    ((List<Object>) responseObject).add(preferences);
-                }
-                catch (NotFoundException e)
-                {
-                    // The case where the preference's type and name is provided, but this particular object does not
-                    // have a matching preference.
-                }
-            }
+            sendResponse(request, response, httpServletRequest, httpServletResponse, controller);
         }
-        else
+        catch (ManagementException e)
         {
-            ConfiguredObject<?> target = allObjects.iterator().next();
-            final UserPreferences userPreferences = target.getUserPreferences();
-
-            responseObject = _userPreferenceHandler.handleGET(userPreferences, requestInfo);
+            sendResponse(e, httpServletRequest, httpServletResponse);
         }
-        sendJsonResponse(responseObject, request, response);
     }
 
-    private void doPostOrPutUserPreference(final RequestInfo requestInfo,
-                                           final ConfiguredObject<?> managedObject,
-                                           final Class<? extends ConfiguredObject> configuredClass,
-                                           final ConfiguredObjectFinder finder,
-                                           final HttpServletRequest request,
-                                           final HttpServletResponse response) throws IOException, ServletException
+    private ManagementController getManagementController()
     {
-        ConfiguredObject<?> target = getTarget(requestInfo, managedObject, configuredClass, finder);
+        return _managementController;
+    }
 
-        final Object providedObject = getRequestProvidedObject(request, requestInfo, Object.class);
-        if ("POST".equals(request.getMethod()))
-        {
-            _userPreferenceHandler.handlePOST(target, requestInfo, providedObject);
-        }
-        else if ("PUT".equals(request.getMethod()))
-        {
-            _userPreferenceHandler.handlePUT(target, requestInfo, providedObject);
-        }
-        else
-        {
-            sendJsonErrorResponse(request,
-                                  response,
-                                  HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
-                                  "unexpected http method");
-        }
+    private void sendResponse(final ManagementException managementException,
+                              final HttpServletRequest request,
+                              final HttpServletResponse response) throws IOException
+    {
+        setHeaders(response);
+        setExceptionHeaders(managementException, response);
+        response.setStatus(managementException.getStatusCode());
+        writeJsonResponse(Collections.singletonMap("errorMessage", managementException.getMessage()),
+                          request,
+                          response);
     }
 
-    private void doOperation(final RequestInfo requestInfo,
-                             final ConfiguredObject<?> managedObject,
-                             final Class<? extends ConfiguredObject> configuredClass,
-                             final ConfiguredObjectFinder finder,
-                             final HttpServletRequest request,
-                             final HttpServletResponse response) throws IOException, ServletException
+    private void setExceptionHeaders(final ManagementException managementException, final HttpServletResponse response)
     {
-        ConfiguredObject<?> target = getTarget(requestInfo, managedObject, configuredClass, finder);
-        if (target == null)
+        Map<String, String> headers = managementException.getHeaders();
+        if (headers != null)
         {
-            return;
+            headers.forEach(response::setHeader);
         }
-        String operationName = requestInfo.getOperationName();
-        final Map<String, ConfiguredObjectOperation<?>> availableOperations =
-                managedObject.getModel().getTypeRegistry().getOperations(target.getClass());
-        ConfiguredObjectOperation operation = availableOperations.get(operationName);
-        Map<String, Object> operationArguments;
-
+    }
 
-        String requestMethod = request.getMethod();
-        if (operation == null)
+    private String toContentDispositionHeader(final String attachmentFilename)
+    {
+        String filenameRfc2183 = HttpManagementUtil.ensureFilenameIsRfc2183(attachmentFilename);
+        if (filenameRfc2183.length() > 0)
         {
-            sendJsonErrorResponse(request,
-                                  response,
-                                  HttpServletResponse.SC_NOT_FOUND,
-                                  "No such operation: " + operationName);
-            return;
+            return String.format("attachment; filename=\"%s\"", filenameRfc2183);
         }
         else
         {
-            switch (requestMethod)
-            {
-                case "GET":
-                    if (operation.isNonModifying())
-                    {
-                        operationArguments = getOperationArgumentsAsMap(request);
-                        operationArguments.keySet().removeAll(Arrays.asList(CONTENT_DISPOSITION_ATTACHMENT_FILENAME_PARAM));
-                    }
-                    else
-                    {
-                        response.addHeader("Allow", "POST");
-                        sendJsonErrorResponse(request,
-                                              response,
-                                              HttpServletResponse.SC_METHOD_NOT_ALLOWED,
-                                              "Operation "
-                                              + operationName
-                                              + " modifies the object so you must use POST.");
-                        return;
-                    }
-
-                    break;
-                case "POST":
-                    operationArguments = getRequestProvidedObject(request, requestInfo);
-                    break;
-                default:
-                    response.addHeader("Allow", (operation.isNonModifying() ? "POST, GET" : "POST"));
-                    sendJsonErrorResponse(request,
-                                          response,
-                                          HttpServletResponse.SC_METHOD_NOT_ALLOWED,
-                                          "Operation "
-                                          + operationName
-                                          + " does not support the "
-                                          + requestMethod
-                                          + " requestMethod.");
-                    return;
-            }
+            // Agent will allow user to choose a name
+            return "attachment";
         }
+    }
 
-
-        if(operation.isSecure(target, operationArguments) && !(request.isSecure() || HttpManagementUtil.getPort(request).isAllowConfidentialOperationsOnInsecureChannels()))
+    private void sendResponse(final ManagementRequest managementRequest,
+                              final ManagementResponse managementResponse,
+                              final HttpServletRequest request,
+                              final HttpServletResponse response,
+                              final ManagementController controller) throws IOException
+    {
+        setHeaders(response);
+        Map<String, String> headers = managementResponse.getHeaders();
+        if (!headers.isEmpty())
         {
-            sendJsonErrorResponse(request,
-                                  response,
-                                  HttpServletResponse.SC_FORBIDDEN,
-                                  "Operation '" + operationName + "' can only be performed over a secure (HTTPS) connection");
-            return;
+            headers.forEach(response::setHeader);
         }
 
-        Object returnVal = operation.perform(target, operationArguments);
-        String attachmentFilename = request.getParameter(CONTENT_DISPOSITION_ATTACHMENT_FILENAME_PARAM);
-        if (attachmentFilename != null)
+        Map<String, List<String>> parameters = managementRequest.getParameters();
+        if (parameters.containsKey(CONTENT_DISPOSITION_ATTACHMENT_FILENAME_PARAM))
         {
-            setContentDispositionHeaderIfNecessary(response, attachmentFilename);
+            String attachmentFilename = managementRequest.getParameter(CONTENT_DISPOSITION_ATTACHMENT_FILENAME_PARAM);
+            response.setHeader(CONTENT_DISPOSITION, toContentDispositionHeader(attachmentFilename));
         }
+        response.setStatus(managementResponse.getResponseCode());
 
-        if(returnVal instanceof Content)
+        Object body = managementResponse.getBody();
+        if (body instanceof Content)
         {
-            Content content = (Content)returnVal;
+            Content content = (Content) body;
             try
             {
-
                 writeTypedContent(content, request, response);
             }
             finally
@@ -613,350 +255,242 @@ public class RestServlet extends AbstractServlet
                 content.release();
             }
         }
-        else
+        else if (body != null)
         {
-            final ConfiguredObjectToMapConverter.ConverterOptions converterOptions =
-                    new ConfiguredObjectToMapConverter.ConverterOptions(DEFAULT_DEPTH,
-                                                                        false,
-                                                                        DEFAULT_OVERSIZE,
-                                                                        request.isSecure(),
-                                                                        true);
-            if (ConfiguredObject.class.isAssignableFrom(operation.getReturnType()))
+            response.setContentType(APPLICATION_JSON);
+            Object data;
+            if (managementResponse.getType() == ResponseType.MODEL_OBJECT)
             {
-                returnVal = _objectConverter.convertObjectToMap((ConfiguredObject<?>) returnVal,
-                                                                operation.getReturnType(),
-                                                                converterOptions);
+                data = controller.formatConfiguredObject(
+                        managementResponse.getBody(),
+                        parameters,
+                        managementRequest.isSecure()
+                        || managementRequest.isConfidentialOperationAllowedOnInsecureChannel());
             }
-            else if (returnsCollectionOfConfiguredObjects(operation))
+            else
             {
-                List<Map<String, Object>> output = new ArrayList<>();
-                for (Object configuredObject : (Collection)returnVal)
-                {
-                    output.add(_objectConverter.convertObjectToMap((ConfiguredObject<?>) configuredObject,
-                                                                   getCollectionMemberType((ParameterizedType) operation.getGenericReturnType()),
-                                                                   converterOptions));
-                }
-                returnVal = output;
+                data = managementResponse.getBody();
             }
-            sendJsonResponse(returnVal, request, response);
+            writeJsonResponse(data, request, response);
         }
     }
 
-    private ConfiguredObject<?> getTarget(final RequestInfo requestInfo,
-                                          final ConfiguredObject<?> managedObject,
-                                          final Class<? extends ConfiguredObject> configuredClass,
-                                          final ConfiguredObjectFinder finder) throws IOException
+    private void setHeaders(final HttpServletResponse response)
     {
-        final ConfiguredObject<?> target;
-        final List<String> names = requestInfo.getModelParts();
-        final Class<? extends ConfiguredObject>[] hierarchy = finder.getHierarchy(configuredClass);
-        if (names.isEmpty() && hierarchy.length == 0)
+        response.setHeader("Cache-Control", "no-cache");
+        response.setHeader("Pragma", "no-cache");
+        response.setDateHeader("Expires", 0);
+        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
+    }
+
+    private void writeJsonResponse(final Object formattedResponse,
+                                   final HttpServletRequest request,
+                                   final HttpServletResponse response) throws IOException
+    {
+        try (OutputStream stream = HttpManagementUtil.getOutputStream(request,
+                                                                      response,
+                                                                      getManagementConfiguration()))
         {
-            target = managedObject;
+            ObjectMapper mapper = ConfiguredObjectJacksonModule.newObjectMapper(false);
+            mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
+            mapper.writeValue(stream, formattedResponse);
         }
-        else
+    }
+
+    private static Map<String, List<String>> parseQueryString(String queryString)
+    {
+        if (Strings.isNullOrEmpty(queryString))
         {
-            ConfiguredObject theParent = managedObject;
-            if (hierarchy.length > 1)
+            return Collections.emptyMap();
+        }
+        Map<String, List<String>> query = new LinkedHashMap<>();
+        final String[] pairs = queryString.split("&");
+        for (String pairString : pairs)
+        {
+            List<String> pair = new ArrayList<>(Arrays.asList(pairString.split("=")));
+            if (pair.size() == 1)
             {
-
-                ConfiguredObject parent =
-                        finder.findObjectParentsFromPath(names, hierarchy, configuredClass);
-                theParent = parent;
+                pair.add(null);
             }
-            String name = names.get(names.size() - 1);
-            target = theParent.getChildByName(configuredClass, name);
-            if (target == null)
+            else if (pair.size() != 2)
             {
+                throw new IllegalArgumentException(String.format("could not parse query string '%s'", queryString));
+            }
 
-                final String errorMessage = String.format("%s '%s' not found",
-                                                          configuredClass.getSimpleName(),
-                                                          Joiner.on("/").join(names));
-                throw new NotFoundException(errorMessage);
+            String key;
+            String value;
+            try
+            {
+                key = URLDecoder.decode(pair.get(0), "UTF-8");
+                value = pair.get(1) == null ? null : URLDecoder.decode(pair.get(1), "UTF-8");
+            }
+            catch (UnsupportedEncodingException e)
+            {
+                throw new RuntimeException(e);
+            }
+            if (!query.containsKey(key))
+            {
+                query.put(key, new ArrayList<>());
             }
+            query.get(key).add(value);
         }
-        return target;
+        return query;
     }
 
-    private Map<String, Object> getOperationArgumentsAsMap(HttpServletRequest request)
+    private static class ServletManagementRequest implements ManagementRequest
     {
-        Map<String, Object> providedObject;
-        providedObject = new HashMap<>();
-        for (Map.Entry<String, String[]> entry : request.getParameterMap().entrySet())
+        private final HttpPort<?> _port;
+        private final HttpServletRequest _request;
+        private final Map<String, List<String>> _query;
+        private final List<String> _path;
+        private final ConfiguredObject<?> _root;
+        private final String _category;
+        private final Map<String, String> _headers;
+
+        ServletManagementRequest(final ConfiguredObject<?> root,
+                                 final HttpServletRequest request)
         {
-            String[] value = entry.getValue();
-            if (value != null)
-            {
-                if(value.length > 1)
-                {
-                    providedObject.put(entry.getKey(), Arrays.asList(value));
-                }
-                else
-                {
-                    providedObject.put(entry.getKey(), value[0]);
-                }
-            }
+            _root = root;
+            _request = request;
+            _port = HttpManagementUtil.getPort(request);
+            _query = Collections.unmodifiableMap(parseQueryString(request.getQueryString()));
+            String pathInfo = _request.getPathInfo() == null ? "" : _request.getPathInfo();
+            String servletPath = request.getServletPath();
+            _path = Collections.unmodifiableList(HttpManagementUtil.getPathInfoElements(servletPath, pathInfo));
+            final String[] servletPathElements = servletPath.split("/");
+            _category = servletPathElements[servletPathElements.length - 1];
+            final Map<String, String> headers = Collections.list(request.getHeaderNames())
+                                                           .stream()
+                                                           .collect(Collectors.toMap(name -> name, request::getHeader));
+            _headers = Collections.unmodifiableMap(headers);
         }
-        return providedObject;
-    }
 
-    private Map<String, Object> getRequestProvidedObject(HttpServletRequest request, final RequestInfo requestInfo)
-            throws IOException, ServletException
-    {
-        return getRequestProvidedObject(request, requestInfo, LinkedHashMap.class);
-    }
+        public ConfiguredObject<?> getRoot()
+        {
+            return _root;
+        }
 
-    private <T> T getRequestProvidedObject(HttpServletRequest request,
-                                           final RequestInfo requestInfo,
-                                           Class<T> expectedClass)
-            throws IOException, ServletException
-    {
-        T providedObject;
+        public boolean isSecure()
+        {
+            return _request.isSecure();
+        }
 
-        ArrayList<String> headers = Collections.list(request.getHeaderNames());
-        ObjectMapper mapper = new ObjectMapper();
+        public boolean isConfidentialOperationAllowedOnInsecureChannel()
+        {
+            return _port.isAllowConfidentialOperationsOnInsecureChannels();
+        }
 
-        if (headers.contains("Content-Type") && request.getHeader("Content-Type").startsWith("multipart/form-data"))
+        public List<String> getPath()
         {
-            providedObject = (T) new LinkedHashMap<>();
-            Map<String, String> fileUploads = new HashMap<>();
-            Collection<Part> parts = request.getParts();
-            for (Part part : parts)
-            {
-                if ("data".equals(part.getName()) && "application/json".equals(part.getContentType()))
-                {
-                    try
-                    {
-                        providedObject = (T) mapper.readValue(part.getInputStream(), LinkedHashMap.class);
-                    }
-                    catch (JsonProcessingException e)
-                    {
-                        throw new IllegalArgumentException("Cannot parse the operation body as json",e);
-                    }
+            return _path;
+        }
 
-                }
-                else
-                {
-                    byte[] data = new byte[(int) part.getSize()];
-                    part.getInputStream().read(data);
-                    String inlineURL = DataUrlUtils.getDataUrlForBytes(data);
-                    fileUploads.put(part.getName(), inlineURL);
-                }
-            }
-            ((Map<String, Object>) providedObject).putAll(fileUploads);
+        public String getMethod()
+        {
+            return _request.getMethod();
         }
-        else
+
+        public Map<String, List<String>> getParameters()
         {
-            try
-            {
-                providedObject = mapper.readValue(request.getInputStream(), expectedClass);
-            }
-            catch (JsonProcessingException e)
-            {
-                throw new IllegalArgumentException("Cannot parse the operation body as json",e);
-            }
+            return Collections.unmodifiableMap(_query);
         }
-        return providedObject;
-    }
 
-    private void setResponseStatus(HttpServletRequest request, HttpServletResponse response, Throwable e)
-            throws IOException
-    {
-        if (e instanceof SecurityException)
+        @Override
+        public String getParameter(final String name)
         {
-            LOGGER.debug("{}, sending {}", e.getClass().getName(), HttpServletResponse.SC_FORBIDDEN, e);
-            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
+            final List<String> values = _query.get(name);
+            return values == null || values.isEmpty() ? null : values.get(0);
         }
-        else
+
+        public Map<String, String> getHeaders()
         {
-            int responseCode = HttpServletResponse.SC_BAD_REQUEST;
-            String message = e.getMessage();
-            if (e instanceof AbstractConfiguredObject.DuplicateIdException
-                || e instanceof AbstractConfiguredObject.DuplicateNameException
-                || e instanceof IntegrityViolationException
-                || e instanceof IllegalStateTransitionException)
-            {
-                responseCode = HttpServletResponse.SC_CONFLICT;
-            }
-            else if (e instanceof NotFoundException)
-            {
-                if (LOGGER.isTraceEnabled())
-                {
-                    LOGGER.trace(e.getClass().getSimpleName() + " processing request", e);
-                }
-                responseCode = HttpServletResponse.SC_NOT_FOUND;
-            }
-            else if (e instanceof IllegalConfigurationException || e instanceof IllegalArgumentException)
-            {
-                LOGGER.warn("{} processing request {} from user '{}': {}",
-                            e.getClass().getSimpleName(),
-                            HttpManagementUtil.getRequestURL(request),
-                            HttpManagementUtil.getRequestPrincipals(request),
-                            message);
-                Throwable t = e;
-                int maxDepth = 10;
-                while ((t = t.getCause()) != null && maxDepth-- != 0)
-                {
-                    LOGGER.warn("... caused by " + t.getClass().getSimpleName() + "  : " + t.getMessage());
-                }
-                if (LOGGER.isDebugEnabled())
-                {
-                    LOGGER.debug(e.getClass().getSimpleName() + " processing request", e);
-                }
-                responseCode = SC_UNPROCESSABLE_ENTITY;
-            }
-            else if (e instanceof OperationTimeoutException)
-            {
-                message = "Timeout occurred";
-                if (LOGGER.isDebugEnabled())
-                {
-                    LOGGER.debug("Timeout during processing of request {} from user '{}'",
-                                 HttpManagementUtil.getRequestURL(request),
-                                 HttpManagementUtil.getRequestPrincipals(request),
-                                 e);
-                }
-                else
-                {
-                    LOGGER.info("Timeout during processing of request {} from user '{}'",
-                            HttpManagementUtil.getRequestURL(request),
-                            HttpManagementUtil.getRequestPrincipals(request));
-                }
+            return _headers;
+        }
 
-                responseCode = HttpServletResponse.SC_BAD_GATEWAY;
-            }
-            else if (e instanceof NoClassDefFoundError)
-            {
-                message = "Not found: " + message;
-                LOGGER.warn("Unexpected exception processing request ", e);
-            }
-            else if (e instanceof ExternalServiceTimeoutException)
-            {
-                responseCode = HttpServletResponse.SC_GATEWAY_TIMEOUT;
-                LOGGER.warn("External request timeout ", e);
-            }
-            else if (e instanceof ExternalServiceException)
+        @SuppressWarnings("unchecked")
+        public <T> T getBody(Class<T> type)
+        {
+            try
             {
-                responseCode = HttpServletResponse.SC_BAD_GATEWAY;
-                LOGGER.warn("External request failed ", e);
+                return parse(type);
             }
-            else
+            catch (IOException | ServletException e)
             {
-                // This should not happen
-                if (e instanceof RuntimeException)
-                {
-                    throw (RuntimeException) e;
-                }
-                else if (e instanceof Error)
-                {
-                    throw (Error) e;
-                }
-                else
-                {
-                    throw new RuntimeException("Unexpected Exception", e);
-                }
+                throw ManagementException.createBadRequestManagementException("Cannot parse body", e);
             }
-
-            sendJsonErrorResponse(request, response, responseCode, message);
         }
-    }
 
-    @Override
-    protected void doDelete(HttpServletRequest request,
-                            HttpServletResponse response,
-                            final ConfiguredObject<?> managedObject) throws ServletException, IOException
-    {
-        ConfiguredObjectFinder finder = getConfiguredObjectFinder(managedObject);
-        Class<? extends ConfiguredObject> configuredClass = getConfiguredClass(request, managedObject);
-        final Class<? extends ConfiguredObject>[] hierarchy = finder.getHierarchy(configuredClass);
-        RequestInfoParser requestInfoParser = new RequestInfoParser(hierarchy);
-
-        RequestInfo requestInfo = requestInfoParser.parse(request);
-
-        Collection<ConfiguredObject<?>> allObjects = getTargetObjects(configuredClass, finder, requestInfo, buildFilterPredicates(request));
-        if (allObjects == null)
+        @Override
+        public String getRequestURL()
         {
-            throw new NotFoundException("Not Found");
+            return _request.getRequestURL().toString();
         }
 
-        switch (requestInfo.getType())
+        @SuppressWarnings("unchecked")
+        private <T> T parse(Class<T> type) throws IOException, ServletException
         {
-            case MODEL_OBJECT:
-            {
-                for (ConfiguredObject o : allObjects)
-                {
-                    o.delete();
-                }
+            T providedObject;
+            final ObjectMapper mapper = new ObjectMapper();
 
-                sendCachingHeadersOnResponse(response);
-                response.setStatus(HttpServletResponse.SC_OK);
-                break;
-            }
-            case USER_PREFERENCES:
+            if (_headers.containsKey("Content-Type") && _request.getHeader("Content-Type")
+                                                                .startsWith("multipart/form-data"))
             {
-                //TODO: define format how to report the results for bulk delete, i.e. how to report individual errors and success statuses
-                if (allObjects.size() > 1)
-                {
-                    sendJsonErrorResponse(request,
-                                          response,
-                                          HttpServletResponse.SC_BAD_REQUEST,
-                                          "Deletion of user preferences using wildcards is unsupported");
-                    return;
-                }
-                for (ConfiguredObject o : allObjects)
+                Map<String, Object> items = new LinkedHashMap<>();
+                Map<String, String> fileUploads = new HashMap<>();
+                Collection<Part> parts = _request.getParts();
+                for (Part part : parts)
                 {
-                    _userPreferenceHandler.handleDELETE(o.getUserPreferences(), requestInfo);
+                    if ("data".equals(part.getName()) && "application/json".equals(part.getContentType()))
+                    {
+                        items = mapper.readValue(part.getInputStream(), LinkedHashMap.class);
+                    }
+                    else
+                    {
+                        byte[] data = new byte[(int) part.getSize()];
+                        try (InputStream inputStream = part.getInputStream())
+                        {
+                            inputStream.read(data);
+                        }
+                        fileUploads.put(part.getName(), DataUrlUtils.getDataUrlForBytes(data));
+                    }
                 }
-                break;
-            }
+                items.putAll(fileUploads);
 
-            default:
+                providedObject = (T) items;
+            }
+            else
             {
-                sendJsonErrorResponse(request, response, HttpServletResponse.SC_BAD_REQUEST, "Unsupported delete call");
+                providedObject = mapper.readValue(_request.getInputStream(), type);
             }
+            return providedObject;
         }
-    }
 
-    @Override
-    protected void doPost(HttpServletRequest request,
-                          HttpServletResponse response,
-                          final ConfiguredObject<?> managedObject) throws ServletException, IOException
-    {
-        performCreateOrUpdate(request, response, managedObject);
-    }
-
-
-    private int getIntParameterFromRequest(final HttpServletRequest request,
-                                           final String paramName,
-                                           final int defaultValue)
-    {
-        int intValue = defaultValue;
-        final String stringValue = request.getParameter(paramName);
-        if(stringValue!=null)
+        @Override
+        public Map<String, Object> getParametersAsFlatMap()
         {
-            try
+            final Map<String, Object> providedObject = new HashMap<>();
+            for (Map.Entry<String, List<String>> entry : _query.entrySet())
             {
-                intValue = Integer.parseInt(stringValue);
-            }
-            catch (NumberFormatException e)
-            {
-                LOGGER.warn("Could not parse " + stringValue + " as integer for parameter " + paramName);
+                final List<String> value = entry.getValue();
+                if (value != null)
+                {
+                    if (value.size() == 1)
+                    {
+                        providedObject.put(entry.getKey(), value.get(0));
+                    }
+                    else
+                    {
+                        providedObject.put(entry.getKey(), value);
+                    }
+                }
             }
+            return providedObject;
         }
-        return intValue;
-    }
 
-    private boolean getBooleanParameterFromRequest(HttpServletRequest request, final String paramName)
-    {
-        return getBooleanParameterFromRequest(request, paramName, false);
-    }
-
-    private boolean getBooleanParameterFromRequest(HttpServletRequest request, final String paramName, final boolean defaultValue)
-    {
-        String value = request.getParameter(paramName);
-        if (value == null)
+        @Override
+        public String getCategory()
         {
-            return defaultValue;
+            return _category;
         }
-        return Boolean.parseBoolean(value);
     }
 }
diff --git a/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/servlet/rest/RestUserPreferenceHandler.java b/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/servlet/rest/RestUserPreferenceHandler.java
index a0d4113..50c506c 100644
--- a/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/servlet/rest/RestUserPreferenceHandler.java
+++ b/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/servlet/rest/RestUserPreferenceHandler.java
@@ -19,12 +19,8 @@
 
 package org.apache.qpid.server.management.plugin.servlet.rest;
 
-import java.security.AccessController;
-import java.security.Principal;
 import java.util.ArrayList;
-import java.util.Collection;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
@@ -33,16 +29,15 @@ import java.util.Set;
 import java.util.UUID;
 import java.util.concurrent.TimeUnit;
 
-import javax.security.auth.Subject;
-
 import com.google.common.base.Joiner;
 import com.google.common.util.concurrent.ListenableFuture;
 
-import org.apache.qpid.server.util.FutureHelper;
+import org.apache.qpid.server.management.plugin.RequestType;
 import org.apache.qpid.server.model.ConfiguredObject;
 import org.apache.qpid.server.model.preferences.Preference;
 import org.apache.qpid.server.model.preferences.PreferenceFactory;
 import org.apache.qpid.server.model.preferences.UserPreferences;
+import org.apache.qpid.server.util.FutureHelper;
 
 public class RestUserPreferenceHandler
 {
@@ -88,7 +83,7 @@ public class RestUserPreferenceHandler
         awaitFuture(userPreferences.delete(type, name, id));
     }
 
-    void handlePUT(ConfiguredObject<?> target, RequestInfo requestInfo, Object providedObject)
+    public void handlePUT(ConfiguredObject<?> target, RequestInfo requestInfo, Object providedObject)
     {
         UserPreferences userPreferences = target.getUserPreferences();
         if (userPreferences == null)
@@ -146,7 +141,7 @@ public class RestUserPreferenceHandler
         }
     }
 
-    void handlePOST(ConfiguredObject<?> target, RequestInfo requestInfo, Object providedObject)
+    public void handlePOST(ConfiguredObject<?> target, RequestInfo requestInfo, Object providedObject)
     {
         UserPreferences userPreferences = target.getUserPreferences();
         if (userPreferences == null)
@@ -185,7 +180,7 @@ public class RestUserPreferenceHandler
         awaitFuture(userPreferences.updateOrAppend(preferences));
     }
 
-    Object handleGET(UserPreferences userPreferences, RequestInfo requestInfo)
+    public Object handleGET(UserPreferences userPreferences, RequestInfo requestInfo)
     {
         if (userPreferences == null)
         {
@@ -197,11 +192,11 @@ public class RestUserPreferenceHandler
         UUID id = getIdFromQueryParameters(queryParameters);
 
         final ListenableFuture<Set<Preference>> allPreferencesFuture;
-        if (requestInfo.getType() == RequestInfo.RequestType.USER_PREFERENCES)
+        if (requestInfo.getType() == RequestType.USER_PREFERENCES)
         {
             allPreferencesFuture = userPreferences.getPreferences();
         }
-        else if (requestInfo.getType() == RequestInfo.RequestType.VISIBLE_PREFERENCES)
+        else if (requestInfo.getType() == RequestType.VISIBLE_PREFERENCES)
         {
             allPreferencesFuture = userPreferences.getVisiblePreferences();
         }
diff --git a/broker-plugins/management-http/src/test/java/org/apache/qpid/server/management/plugin/controller/latest/LatestManagementControllerTest.java b/broker-plugins/management-http/src/test/java/org/apache/qpid/server/management/plugin/controller/latest/LatestManagementControllerTest.java
new file mode 100644
index 0000000..39a22fa
--- /dev/null
+++ b/broker-plugins/management-http/src/test/java/org/apache/qpid/server/management/plugin/controller/latest/LatestManagementControllerTest.java
@@ -0,0 +1,809 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+package org.apache.qpid.server.management.plugin.controller.latest;
+
+import static org.apache.qpid.server.management.plugin.HttpManagementConfiguration.PREFERENCE_OPERTAION_TIMEOUT_CONTEXT_NAME;
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.security.PrivilegedAction;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import javax.security.auth.Subject;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import org.apache.qpid.server.management.plugin.HttpManagementConfiguration;
+import org.apache.qpid.server.management.plugin.ManagementException;
+import org.apache.qpid.server.management.plugin.ManagementRequest;
+import org.apache.qpid.server.management.plugin.ManagementResponse;
+import org.apache.qpid.server.management.plugin.RequestType;
+import org.apache.qpid.server.model.AuthenticationProvider;
+import org.apache.qpid.server.model.Broker;
+import org.apache.qpid.server.model.BrokerModel;
+import org.apache.qpid.server.model.BrokerTestHelper;
+import org.apache.qpid.server.model.ConfiguredObject;
+import org.apache.qpid.server.model.Queue;
+import org.apache.qpid.server.model.VirtualHost;
+import org.apache.qpid.server.model.VirtualHostNode;
+import org.apache.qpid.server.model.preferences.GenericPreferenceValueFactory;
+import org.apache.qpid.server.model.preferences.Preference;
+import org.apache.qpid.server.model.preferences.PreferenceImpl;
+import org.apache.qpid.server.security.auth.AuthenticatedPrincipal;
+import org.apache.qpid.server.security.auth.UsernamePrincipal;
+import org.apache.qpid.server.virtualhost.QueueManagingVirtualHost;
+import org.apache.qpid.test.utils.UnitTestBase;
+
+public class LatestManagementControllerTest extends UnitTestBase
+{
+    private LatestManagementController _controller;
+
+    @Before
+    public void setUp()
+    {
+        final HttpManagementConfiguration<?> httpManagement = mock(HttpManagementConfiguration.class);
+        when(httpManagement.getContextValue(Long.class, PREFERENCE_OPERTAION_TIMEOUT_CONTEXT_NAME)).thenReturn(1000L);
+        _controller = new LatestManagementController(httpManagement);
+    }
+
+    @Test
+    public void getVersion()
+    {
+        assertThat(_controller.getVersion(), is(equalTo(BrokerModel.MODEL_VERSION)));
+    }
+
+    @Test
+    public void getCategories()
+    {
+        assertThat(_controller.getCategories(), is(equalTo(BrokerModel.getInstance()
+                                                                      .getSupportedCategories()
+                                                                      .stream()
+                                                                      .map(Class::getSimpleName)
+                                                                      .collect(Collectors.toSet()))));
+    }
+
+    @Test
+    public void getCategoryMapping()
+    {
+        assertThat(_controller.getCategoryMapping("foo"),
+                   is(equalTo(String.format("/api/v%s/%s/", BrokerModel.MODEL_VERSION, "foo"))));
+    }
+
+    @Test
+    public void getCategory()
+    {
+        final ConfiguredObject<?> object = mock(ConfiguredObject.class);
+        doReturn(Broker.class).when(object).getCategoryClass();
+        assertThat(_controller.getCategory(object), is(equalTo(Broker.class.getSimpleName())));
+    }
+
+    @Test
+    public void getCategoryHierarchyForBrokerRootAndQueueCategory()
+    {
+        final Broker<?> object = BrokerTestHelper.createBrokerMock();
+        final Collection<String> expected = Arrays.asList("VirtualHostNode", "VirtualHost", "Queue");
+        assertThat(_controller.getCategoryHierarchy(object, "Queue"), is(equalTo(expected)));
+    }
+
+    @Test
+    public void getCategoryHierarchyForVirtualHostRootAndExchangeCategory() throws Exception
+    {
+        final QueueManagingVirtualHost<?> object = BrokerTestHelper.createVirtualHost("test", this);
+        final Collection<String> expected = Collections.singletonList("Exchange");
+        assertThat(_controller.getCategoryHierarchy(object, "Exchange"), is(equalTo(expected)));
+    }
+
+
+    @Test
+    public void getCategoryHierarchyForBrokerRootAndUnknownCategory()
+    {
+        final Broker<?> object = BrokerTestHelper.createBrokerMock();
+        final Collection<String> expected = Collections.emptyList();
+        assertThat(_controller.getCategoryHierarchy(object, "Binding"), is(equalTo(expected)));
+    }
+
+    @Test
+    public void getNextVersionManagementController()
+    {
+        assertThat(_controller.getNextVersionManagementController(), is(nullValue()));
+    }
+
+    @Test
+    public void getRequestTypeForGetAndModelObjectWithNotFullPath() throws Exception
+    {
+        final String hostName = "test";
+        final QueueManagingVirtualHost<?> virtualHost = createVirtualHostWithQueue(hostName, "foo", "bar");
+
+        final ManagementRequest request = mock(ManagementRequest.class);
+        when(request.getCategory()).thenReturn("queue");
+        doReturn(virtualHost.getBroker()).when(request).getRoot();
+        when(request.getPath()).thenReturn(Arrays.asList("*", hostName));
+        when(request.getParameters()).thenReturn(Collections.emptyMap());
+        when(request.getMethod()).thenReturn("GET");
+
+        final RequestType type = _controller.getRequestType(request);
+
+        assertThat(type, is(equalTo(RequestType.MODEL_OBJECT)));
+    }
+
+    @Test
+    public void getRequestTypeForGetAndModelObjectWithFullPath() throws Exception
+    {
+        final String hostName = "test";
+        final QueueManagingVirtualHost<?> virtualHost = createVirtualHostWithQueue(hostName, "foo", "bar");
+
+        final ManagementRequest request = mock(ManagementRequest.class);
+        when(request.getCategory()).thenReturn("queue");
+        doReturn(virtualHost.getBroker()).when(request).getRoot();
+        final List<String> path = Arrays.asList(virtualHost.getParent().getName(), hostName, "bar");
+        when(request.getPath()).thenReturn(path);
+        when(request.getParameters()).thenReturn(Collections.emptyMap());
+        when(request.getMethod()).thenReturn("GET");
+
+        final RequestType type = _controller.getRequestType(request);
+
+        assertThat(type, is(equalTo(RequestType.MODEL_OBJECT)));
+    }
+
+    @Test
+    public void getRequestTypeForGetAndUserPreferences() throws Exception
+    {
+        final String hostName = "test";
+        final QueueManagingVirtualHost<?> virtualHost = createVirtualHostWithQueue(hostName, "foo", "bar");
+
+        final ManagementRequest request = mock(ManagementRequest.class);
+        when(request.getCategory()).thenReturn("queue");
+        doReturn(virtualHost.getBroker()).when(request).getRoot();
+        List<String> path = Arrays.asList(virtualHost.getParent().getName(),
+                                          hostName,
+                                          "bar",
+                                          "userpreferences");
+        when(request.getPath()).thenReturn(path);
+        when(request.getParameters()).thenReturn(Collections.emptyMap());
+        when(request.getMethod()).thenReturn("GET");
+
+        final RequestType type = _controller.getRequestType(request);
+
+        assertThat(type, is(equalTo(RequestType.USER_PREFERENCES)));
+    }
+
+    @Test
+    public void getRequestTypeForGetAndVisiblePreferences() throws Exception
+    {
+        final String hostName = "test";
+        final QueueManagingVirtualHost<?> virtualHost = createVirtualHostWithQueue(hostName, "foo", "bar");
+
+        final ManagementRequest request = mock(ManagementRequest.class);
+        when(request.getCategory()).thenReturn("queue");
+        doReturn(virtualHost.getBroker()).when(request).getRoot();
+        List<String> path = Arrays.asList(virtualHost.getParent().getName(), hostName, "bar", "visiblepreferences");
+        when(request.getPath()).thenReturn(path);
+        when(request.getParameters()).thenReturn(Collections.emptyMap());
+        when(request.getMethod()).thenReturn("GET");
+
+        final RequestType type = _controller.getRequestType(request);
+
+        assertThat(type, is(equalTo(RequestType.VISIBLE_PREFERENCES)));
+    }
+
+    @Test
+    public void getForBrokerRootAndQueueSingletonPath() throws Exception
+    {
+        final String hostName = "test";
+        final QueueManagingVirtualHost<?> virtualHost = createVirtualHostWithQueue(hostName, "foo", "bar");
+        final String nodeName = virtualHost.getParent().getName();
+        final List<String> path = Arrays.asList(nodeName, hostName, "foo");
+
+        final Object object = _controller.get(virtualHost.getBroker(), "queue", path, Collections.emptyMap());
+        assertThat(object, is(notNullValue()));
+        assertThat(object, is(instanceOf(Queue.class)));
+
+        final Queue data = (Queue) object;
+        assertThat(data.getName(), is(equalTo("foo")));
+    }
+
+    @Test
+    public void getForBrokerRootAndQueuePathNoQueueName() throws Exception
+    {
+        final String hostName = "test";
+        final QueueManagingVirtualHost<?> virtualHost = createVirtualHostWithQueue(hostName, "foo", "bar");
+        final String nodeName = virtualHost.getParent().getName();
+        final List<String> path = Arrays.asList(nodeName, hostName);
+
+        final Object object = _controller.get(virtualHost.getBroker(), "queue", path, Collections.emptyMap());
+        assertThat(object, is(notNullValue()));
+        assertThat(object, is(instanceOf(Collection.class)));
+
+        final Collection<?> data = (Collection<?>) object;
+        final Iterator iterator = data.iterator();
+        final Object o = iterator.next();
+        final Object o2 = iterator.next();
+        assertThat(o, is(notNullValue()));
+        assertThat(o, is(instanceOf(Queue.class)));
+        assertThat(((Queue) o).getName(), is(equalTo("foo")));
+
+        assertThat(o2, is(notNullValue()));
+        assertThat(o2, is(instanceOf(Queue.class)));
+        assertThat(((Queue) o2).getName(), is(equalTo("bar")));
+    }
+
+    @Test
+    public void getForBrokerRootAndQueuePathWithWildCards() throws Exception
+    {
+        final String hostName = "test";
+        final QueueManagingVirtualHost<?> virtualHost = createVirtualHostWithQueue(hostName, "foo", "bar");
+        final List<String> path = Arrays.asList("*", hostName);
+
+        final Object object = _controller.get(virtualHost.getBroker(), "queue", path, Collections.emptyMap());
+        assertThat(object, is(notNullValue()));
+        assertThat(object, is(instanceOf(Collection.class)));
+
+        final Collection<?> data = (Collection<?>) object;
+        assertThat(data.size(), is(equalTo(2)));
+        final Iterator iterator = data.iterator();
+        final Object o = iterator.next();
+        final Object o2 = iterator.next();
+        assertThat(o, is(notNullValue()));
+        assertThat(o, is(instanceOf(Queue.class)));
+        assertThat(((Queue) o).getName(), is(equalTo("foo")));
+
+        assertThat(o2, is(notNullValue()));
+        assertThat(o2, is(instanceOf(Queue.class)));
+        assertThat(((Queue) o2).getName(), is(equalTo("bar")));
+    }
+
+    @Test
+    public void getForBrokerRootAndQueuePathWithFilter() throws Exception
+    {
+        final String hostName = "test";
+        final QueueManagingVirtualHost<?> virtualHost = createVirtualHostWithQueue(hostName, "foo", "bar", "bar2");
+        final List<String> path = Arrays.asList("*", hostName);
+
+        final Object object = _controller.get(virtualHost.getBroker(),
+                                              "queue",
+                                              path,
+                                              Collections.singletonMap(Queue.NAME, Arrays.asList("foo", "bar")));
+        assertThat(object, is(notNullValue()));
+        assertThat(object, is(instanceOf(Collection.class)));
+
+        final Collection<?> data = (Collection<?>) object;
+        assertThat(data.size(), is(equalTo(2)));
+        final Iterator iterator = data.iterator();
+        final Object o = iterator.next();
+        final Object o2 = iterator.next();
+        assertThat(o, is(notNullValue()));
+        assertThat(o, is(instanceOf(Queue.class)));
+        assertThat(((Queue) o).getName(), is(equalTo("foo")));
+
+        assertThat(o2, is(notNullValue()));
+        assertThat(o2, is(instanceOf(Queue.class)));
+        assertThat(((Queue) o2).getName(), is(equalTo("bar")));
+    }
+
+    @Test
+    public void createOrUpdateUsingPutAndFullPath() throws Exception
+    {
+        final String hostName = "test";
+        final QueueManagingVirtualHost<?> virtualHost = createVirtualHostWithQueue(hostName);
+        final List<String> path = Arrays.asList(virtualHost.getParent().getName(), hostName, "bar");
+
+        final Object object = _controller.createOrUpdate(virtualHost.getBroker(),
+                                                         "queue",
+                                                         path,
+                                                         new HashMap<>(Collections.singletonMap(Queue.NAME, "bar")),
+                                                         false);
+
+        assertThat(object, is(notNullValue()));
+        assertThat(object, is(instanceOf(Queue.class)));
+        assertThat(((Queue) object).getName(), is(equalTo("bar")));
+    }
+
+    @Test
+    public void createOrUpdateUsingPostAndFullPathForNonExisting() throws Exception
+    {
+        final String hostName = "test";
+        final QueueManagingVirtualHost<?> virtualHost = createVirtualHostWithQueue(hostName);
+        final List<String> path = Arrays.asList(virtualHost.getParent().getName(), hostName, "bar");
+
+        try
+        {
+            _controller.createOrUpdate(virtualHost.getBroker(),
+                                       "queue",
+                                       path,
+                                       new HashMap<>(Collections.singletonMap(Queue.NAME, "bar")),
+                                       true);
+            fail("Post update should fail for non existing");
+        }
+        catch (ManagementException e)
+        {
+            assertThat(e.getStatusCode(), is(equalTo(404)));
+        }
+    }
+
+    @Test
+    public void createOrUpdateUsingPostAndFullPathForExisting() throws Exception
+    {
+        final String hostName = "test";
+        final QueueManagingVirtualHost<?> virtualHost = createVirtualHostWithQueue(hostName, "bar");
+        final List<String> path = Arrays.asList(virtualHost.getParent().getName(), hostName, "bar");
+
+        final Object object = _controller.createOrUpdate(virtualHost.getBroker(),
+                                                         "queue",
+                                                         path,
+                                                         new HashMap<>(Collections.singletonMap(Queue.NAME, "bar")),
+                                                         true);
+
+        assertThat(object, is(nullValue()));
+    }
+
+    @Test
+    public void createOrUpdateUsingPostAndParentPath() throws Exception
+    {
+        final String hostName = "test";
+        final QueueManagingVirtualHost<?> virtualHost = createVirtualHostWithQueue(hostName);
+        final List<String> path = Arrays.asList(virtualHost.getParent().getName(), hostName);
+
+        final Object object = _controller.createOrUpdate(virtualHost.getBroker(),
+                                                         "queue",
+                                                         path,
+                                                         new HashMap<>(Collections.singletonMap(Queue.NAME, "bar")),
+                                                         true);
+
+        assertThat(object, is(notNullValue()));
+        assertThat(object, is(instanceOf(Queue.class)));
+        assertThat(((Queue) object).getName(), is(equalTo("bar")));
+    }
+
+    @Test
+    public void deleteUsingFullPath() throws Exception
+    {
+        final String hostName = "test";
+        final QueueManagingVirtualHost<?> virtualHost = createVirtualHostWithQueue(hostName, "foo", "bar");
+
+        List<String> path = Arrays.asList(virtualHost.getParent().getName(), hostName, "bar");
+
+        int count = _controller.delete(virtualHost.getBroker(), "queue", path, Collections.emptyMap());
+
+        assertThat(count, is(equalTo(1)));
+    }
+
+    @Test
+    public void deleteUsingFilter() throws Exception
+    {
+        final String hostName = "test";
+        final QueueManagingVirtualHost<?> virtualHost = createVirtualHostWithQueue(hostName, "foo", "bar");
+
+        List<String> path = Arrays.asList(virtualHost.getParent().getName(), hostName);
+
+        int count = _controller.delete(virtualHost.getBroker(),
+                                       "queue",
+                                       path,
+                                       Collections.singletonMap(Queue.NAME, Arrays.asList("foo", "bar", "bar2")));
+
+        assertThat(count, is(equalTo(2)));
+    }
+
+    @Test
+    public void deleteUsingWildcard() throws Exception
+    {
+        final String hostName = "test";
+        final QueueManagingVirtualHost<?> virtualHost = createVirtualHostWithQueue(hostName, "foo", "bar");
+
+        List<String> path = Arrays.asList(virtualHost.getParent().getName(), hostName, "*");
+
+        int count = _controller.delete(virtualHost.getBroker(), "queue", path, Collections.emptyMap());
+
+        assertThat(count, is(equalTo(2)));
+    }
+
+    @Test
+    public void invoke() throws Exception
+    {
+        final String hostName = "test";
+        final QueueManagingVirtualHost<?> virtualHost = createVirtualHostWithQueue(hostName, "foo", "bar");
+
+        List<String> path = Arrays.asList(virtualHost.getParent().getName(), hostName);
+
+        Map<String, Object> message = new HashMap<>();
+        message.put("address", "foo");
+        message.put("persistent", "false");
+        message.put("content", "Test Content");
+        message.put("mimeType", "text/plain");
+        ManagementResponse response = _controller.invoke(virtualHost.getBroker(),
+                                                         "virtualhost",
+                                                         path,
+                                                         "publishMessage",
+                                                         Collections.singletonMap("message", message),
+                                                         true,
+                                                         true);
+
+        assertThat(response, is(notNullValue()));
+        assertThat(response.getResponseCode(), is(equalTo(200)));
+        Object body = response.getBody();
+        assertThat(body, is(instanceOf(Number.class)));
+        assertThat(((Number) body).intValue(), is(equalTo(1)));
+    }
+
+    @Test
+    public void getPreferences() throws Exception
+    {
+        final String hostName = "default";
+        final QueueManagingVirtualHost<?> virtualHost = createVirtualHostWithQueue(hostName);
+        final String preferencesType = "X-type-preference";
+        final Map<String, Object> preferenceValue = Collections.singletonMap("foo", "bar");
+        final Subject testSubject = createTestSubject();
+        final String prefernceName = "test";
+        createPreferences(testSubject, virtualHost, preferencesType, prefernceName, preferenceValue);
+
+        List<String> path = Arrays.asList(virtualHost.getParent().getName(), hostName, "userpreferences");
+        final Object preferences = Subject.doAs(testSubject, (PrivilegedAction<Object>) () ->
+                _controller.getPreferences(virtualHost.getBroker(), "virtualhost", path, Collections.emptyMap()));
+
+        assertPreference(preferencesType, prefernceName, preferenceValue, preferences);
+    }
+
+
+    @Test
+    public void setPreferences() throws Exception
+    {
+        final String hostName = "default";
+        final QueueManagingVirtualHost<?> virtualHost = createVirtualHostWithQueue(hostName);
+        final String preferencesType = "X-type";
+        final Map<String, Object> preferenceValue = Collections.singletonMap("foo", "bar");
+        final Subject testSubject = createTestSubject();
+        final String preferenceName = "pref";
+        final UUID id = createPreferences(testSubject, virtualHost, preferencesType, preferenceName, preferenceValue);
+        final List<String> path = Arrays.asList(virtualHost.getParent().getName(), hostName, "userpreferences");
+        final Map<String, Object> newValue = Collections.singletonMap("foo", "bar2");
+        final Map<String, Object> data = new HashMap<>();
+        data.put("id", id.toString());
+        data.put("name", preferenceName);
+        data.put("value", newValue);
+        final Map<String, List<Object>> modifiedPreferences = Collections.singletonMap(preferencesType,
+                                                                                       Collections.singletonList(data));
+        Subject.doAs(testSubject, (PrivilegedAction<Void>) () -> {
+            _controller.setPreferences(virtualHost.getBroker(),
+                                       "virtualhost",
+                                       path,
+                                       modifiedPreferences,
+                                       Collections.emptyMap(),
+                                       true);
+            return null;
+        });
+        final Object preferences = Subject.doAs(testSubject, (PrivilegedAction<Object>) () ->
+                _controller.getPreferences(virtualHost.getBroker(), "virtualhost", path, Collections.emptyMap()));
+
+        assertPreference(preferencesType, preferenceName, newValue, preferences);
+    }
+
+    @Test
+    public void deletePreferences() throws Exception
+    {
+        final String hostName = "test";
+        final QueueManagingVirtualHost<?> virtualHost = createVirtualHostWithQueue(hostName);
+        final String preferencesType = "X-type";
+        final Map<String, Object> preferenceValue = Collections.singletonMap("foo", "bar");
+        final Subject testSubject = createTestSubject();
+        final String preferenceName = "pref";
+        createPreferences(testSubject, virtualHost, preferencesType, preferenceName, preferenceValue);
+
+        final List<String> path = Arrays.asList(virtualHost.getParent().getName(),
+                                                hostName,
+                                                "userpreferences",
+                                                preferencesType,
+                                                preferenceName);
+
+        Subject.doAs(testSubject, (PrivilegedAction<Void>) () -> {
+            _controller.deletePreferences(virtualHost.getBroker(),
+                                          "virtualhost",
+                                          path,
+                                          Collections.emptyMap());
+            return null;
+        });
+
+        final List<String> path2 = Arrays.asList(virtualHost.getParent().getName(), hostName, "userpreferences");
+
+        final Object preferences = Subject.doAs(testSubject, (PrivilegedAction<Object>) () ->
+                _controller.getPreferences(virtualHost.getBroker(), "virtualhost", path2, Collections.emptyMap()));
+        assertThat(preferences, is(notNullValue()));
+        assertThat(preferences, is(instanceOf(Map.class)));
+
+        final Map<?, ?> map = (Map<?, ?>) preferences;
+        assertThat(map.size(), is(equalTo(0)));
+    }
+
+    @Test
+    public void formatConfiguredObjectForSingletonResponse() throws Exception
+    {
+        final String hostName = "test";
+        final QueueManagingVirtualHost<?> virtualHost = createVirtualHostWithQueue(hostName, "foo", "bar");
+
+        final Object formatted = _controller.formatConfiguredObject(virtualHost,
+                                                                    Collections.singletonMap("depth",
+                                                                                             Collections.singletonList(
+                                                                                                     "1")),
+                                                                    true);
+        assertThat(formatted, is(notNullValue()));
+        assertThat(formatted, is(instanceOf(Map.class)));
+
+        final Map<?, ?> data = (Map<?, ?>) formatted;
+        assertThat(data.get(VirtualHost.NAME), is(equalTo(hostName)));
+        final Object queues = data.get("queues");
+        assertThat(queues, is(notNullValue()));
+        assertThat(queues, is(instanceOf(Collection.class)));
+
+        final Collection<?> queueCollection = (Collection<?>) queues;
+
+        assertThat(queueCollection.size(), is(equalTo(2)));
+        final Iterator<?> iterator = queueCollection.iterator();
+        final Object queue1 = iterator.next();
+        final Object queue2 = iterator.next();
+
+        assertThat(queue1, is(instanceOf(Map.class)));
+        assertThat(queue2, is(instanceOf(Map.class)));
+
+        final Map<?, ?> queueMap1 = (Map<?, ?>) queue1;
+        final Map<?, ?> queueMap2 = (Map<?, ?>) queue2;
+
+        assertThat(queueMap1.get(Queue.NAME), is(equalTo("bar")));
+        assertThat(queueMap2.get(Queue.NAME), is(equalTo("foo")));
+    }
+
+    @Test
+    public void formatConfiguredObjectForCollectionResponse() throws Exception
+    {
+        final String hostName = "test";
+        final QueueManagingVirtualHost<?> virtualHost = createVirtualHostWithQueue(hostName, "foo", "bar");
+
+        final Object formatted = _controller.formatConfiguredObject(Collections.singletonList(virtualHost),
+                                                                    Collections.singletonMap("depth",
+                                                                                             Collections.singletonList(
+                                                                                                     "1")),
+                                                                    true);
+        assertThat(formatted, is(notNullValue()));
+        assertThat(formatted, is(instanceOf(Collection.class)));
+
+        final Collection<?> formattedCollection = (Collection<?>) formatted;
+        assertThat(formattedCollection.size(), is(equalTo(1)));
+
+        Object item = formattedCollection.iterator().next();
+        assertThat(item, is(instanceOf(Map.class)));
+
+        final Map<?, ?> data = (Map<?, ?>) item;
+        assertThat(data.get(VirtualHost.NAME), is(equalTo(hostName)));
+        final Object queues = data.get("queues");
+        assertThat(queues, is(notNullValue()));
+        assertThat(queues, is(instanceOf(Collection.class)));
+
+        final Collection<?> queueCollection = (Collection<?>) queues;
+
+        assertThat(queueCollection.size(), is(equalTo(2)));
+        final Iterator<?> iterator = queueCollection.iterator();
+        final Object queue1 = iterator.next();
+        final Object queue2 = iterator.next();
+
+        assertThat(queue1, is(instanceOf(Map.class)));
+        assertThat(queue2, is(instanceOf(Map.class)));
+
+        final Map<?, ?> queueMap1 = (Map<?, ?>) queue1;
+        final Map<?, ?> queueMap2 = (Map<?, ?>) queue2;
+
+        assertThat(queueMap1.get(Queue.NAME), is(equalTo("bar")));
+        assertThat(queueMap2.get(Queue.NAME), is(equalTo("foo")));
+    }
+
+    @Test
+    public void handleGetForBrokerRootAndQueueSingletonPath() throws Exception
+    {
+        final String hostName = "test";
+        final QueueManagingVirtualHost<?> virtualHost = createVirtualHostWithQueue(hostName, "foo", "bar");
+
+        final String nodeName = virtualHost.getParent().getName();
+        final ManagementRequest request = mock(ManagementRequest.class);
+        when(request.getCategory()).thenReturn("queue");
+        doReturn(virtualHost.getBroker()).when(request).getRoot();
+        when(request.getPath()).thenReturn(Arrays.asList(nodeName, hostName, "foo"));
+        when(request.getMethod()).thenReturn("GET");
+
+        final ManagementResponse response = _controller.handleGet(request);
+        assertThat(response, is(notNullValue()));
+        assertThat(response.getResponseCode(), is(equalTo(200)));
+        assertThat(response.getBody(), is(notNullValue()));
+        assertThat(response.getBody(), is(instanceOf(Queue.class)));
+
+        final Queue data = (Queue) response.getBody();
+        assertThat(data.getName(), is(equalTo("foo")));
+    }
+
+
+    @Test
+    public void handleGetForBrokerRootAndQueuePathWithoutQueueName() throws Exception
+    {
+        final String hostName = "test";
+        final QueueManagingVirtualHost<?> virtualHost = createVirtualHostWithQueue(hostName, "foo", "bar");
+
+        final String nodeName = virtualHost.getParent().getName();
+        final ManagementRequest request = mock(ManagementRequest.class);
+        when(request.getCategory()).thenReturn("queue");
+        doReturn(virtualHost.getBroker()).when(request).getRoot();
+        when(request.getPath()).thenReturn(Arrays.asList(nodeName, hostName));
+        when(request.getParameters()).thenReturn(Collections.emptyMap());
+        when(request.getMethod()).thenReturn("GET");
+
+        final ManagementResponse response = _controller.handleGet(request);
+        assertThat(response, is(notNullValue()));
+        assertThat(response.getResponseCode(), is(equalTo(200)));
+        assertThat(response.getBody(), is(notNullValue()));
+        assertThat(response.getBody(), is(instanceOf(Collection.class)));
+
+        final Collection data = (Collection) response.getBody();
+        assertThat(data.size(), is(equalTo(2)));
+
+        final Iterator iterator = data.iterator();
+        final Object object = iterator.next();
+        final Object object2 = iterator.next();
+        assertThat(object, is(notNullValue()));
+        assertThat(object, is(instanceOf(Queue.class)));
+        assertThat(((Queue) object).getName(), is(equalTo("foo")));
+
+        assertThat(object2, is(notNullValue()));
+        assertThat(object2, is(instanceOf(Queue.class)));
+        assertThat(((Queue) object2).getName(), is(equalTo("bar")));
+    }
+
+    @Test
+    public void handleGetForBrokerRootAndQueuePathWithFilter() throws Exception
+    {
+        final String hostName = "test";
+        final QueueManagingVirtualHost<?> virtualHost = createVirtualHostWithQueue(hostName, "foo", "bar");
+
+        final String nodeName = virtualHost.getParent().getName();
+        final ManagementRequest request = mock(ManagementRequest.class);
+        when(request.getCategory()).thenReturn("queue");
+        doReturn(virtualHost.getBroker()).when(request).getRoot();
+        when(request.getPath()).thenReturn(Arrays.asList(nodeName, hostName));
+        when(request.getParameters()).thenReturn(Collections.singletonMap("name", Collections.singletonList("bar")));
+        when(request.getMethod()).thenReturn("GET");
+
+        ManagementResponse response = _controller.handleGet(request);
+        assertThat(response, is(notNullValue()));
+        assertThat(response.getResponseCode(), is(equalTo(200)));
+        assertThat(response.getBody(), is(notNullValue()));
+        assertThat(response.getBody(), is(instanceOf(Collection.class)));
+
+        Collection data = (Collection) response.getBody();
+        assertThat(data.size(), is(equalTo(1)));
+
+        Object object = data.iterator().next();
+        assertThat(object, is(notNullValue()));
+        assertThat(object, is(instanceOf(Queue.class)));
+        assertThat(((Queue) object).getName(), is(equalTo("bar")));
+    }
+
+    private QueueManagingVirtualHost<?> createVirtualHostWithQueue(final String hostName, String... queueName)
+            throws Exception
+    {
+        final QueueManagingVirtualHost<?> virtualHost = BrokerTestHelper.createVirtualHost(hostName, this);
+        final Broker root = virtualHost.getBroker();
+        final ConfiguredObject<?> virtualHostNode = virtualHost.getParent();
+        when(root.getChildren(VirtualHostNode.class)).thenReturn(Collections.singletonList(virtualHostNode));
+        when(virtualHostNode.getChildren(VirtualHost.class)).thenReturn(Collections.singletonList(virtualHost));
+        when(virtualHostNode.getChildByName(VirtualHost.class, hostName)).thenReturn(virtualHost);
+        Stream.of(queueName)
+              .forEach(n -> virtualHost.createChild(Queue.class, Collections.singletonMap(Queue.NAME, n)));
+        return virtualHost;
+    }
+
+
+    private UUID createPreferences(final Subject testSubject,
+                                   final QueueManagingVirtualHost<?> virtualHost,
+                                   final String preferenceType,
+                                   final String preferenceName,
+                                   final Map<String, Object> preferenceValue)
+            throws Exception
+    {
+        UUID uuid = UUID.randomUUID();
+        final Preference preference = new PreferenceImpl(virtualHost,
+                                                         uuid,
+                                                         preferenceName,
+                                                         preferenceType,
+                                                         "Some preference",
+                                                         null,
+                                                         new Date(),
+                                                         new Date(),
+                                                         null,
+                                                         new GenericPreferenceValueFactory().createInstance(
+                                                                 preferenceValue));
+        final List<Preference> preferenceList = Collections.singletonList(preference);
+        final Future<Void> result = Subject.doAs(testSubject,
+                                                 (PrivilegedAction<Future<Void>>) () -> virtualHost.getUserPreferences()
+                                                                                                   .updateOrAppend(
+                                                                                                           preferenceList));
+
+        result.get(2000L, TimeUnit.MILLISECONDS);
+        return uuid;
+    }
+
+    private Subject createTestSubject()
+    {
+        final AuthenticationProvider<?> provider = mock(AuthenticationProvider.class);
+        when(provider.getType()).thenReturn("type");
+        when(provider.getName()).thenReturn("name");
+
+        return new Subject(false,
+                           Collections.singleton(new AuthenticatedPrincipal(new UsernamePrincipal("user", provider))),
+                           Collections.emptySet(),
+                           Collections.emptySet());
+    }
+
+    private void assertPreference(final String expectedType,
+                                  final String expectedName,
+                                  final Map<String, Object> expectedValue,
+                                  final Object preferences)
+    {
+        assertThat(preferences, is(notNullValue()));
+        assertThat(preferences, is(instanceOf(Map.class)));
+
+        final Map<?, ?> data = (Map<?, ?>) preferences;
+
+        final Object pt = data.get(expectedType);
+        assertThat(pt, is(notNullValue()));
+        assertThat(pt, is(instanceOf(Collection.class)));
+
+        final Collection<?> items = (Collection<?>) pt;
+        assertThat(items.size(), is(equalTo(1)));
+
+        final Object item = items.iterator().next();
+        assertThat(item, is(notNullValue()));
+        assertThat(item, is(instanceOf(Map.class)));
+
+        final Map<?, ?> map = (Map<?, ?>) item;
+
+        final Object value = map.get("value");
+        assertThat(value, is(notNullValue()));
+        assertThat(value, is(equalTo(expectedValue)));
+
+        final Object name = map.get("name");
+        assertThat(name, is(notNullValue()));
+        assertThat(name, is(equalTo(expectedName)));
+    }
+}
\ No newline at end of file
diff --git a/broker-plugins/management-http/src/test/java/org/apache/qpid/server/management/plugin/servlet/rest/RequestInfoParserTest.java b/broker-plugins/management-http/src/test/java/org/apache/qpid/server/management/plugin/servlet/rest/RequestInfoParserTest.java
index e18ff34..5ffafbb 100644
--- a/broker-plugins/management-http/src/test/java/org/apache/qpid/server/management/plugin/servlet/rest/RequestInfoParserTest.java
+++ b/broker-plugins/management-http/src/test/java/org/apache/qpid/server/management/plugin/servlet/rest/RequestInfoParserTest.java
@@ -36,6 +36,7 @@ import javax.servlet.http.HttpServletRequest;
 
 import org.junit.Test;
 
+import org.apache.qpid.server.management.plugin.RequestType;
 import org.apache.qpid.server.model.Queue;
 import org.apache.qpid.server.model.VirtualHost;
 import org.apache.qpid.server.model.VirtualHostNode;
@@ -54,7 +55,7 @@ public class RequestInfoParserTest extends UnitTestBase
 
         RequestInfo info = parser.parse(_request);
 
-        assertEquals("Unexpected request type", RequestInfo.RequestType.MODEL_OBJECT, info.getType());
+        assertEquals("Unexpected request type", RequestType.MODEL_OBJECT, info.getType());
         assertEquals("Unexpected model parts", Collections.emptyList(), info.getModelParts());
     }
 
@@ -71,7 +72,7 @@ public class RequestInfoParserTest extends UnitTestBase
 
         RequestInfo info = parser.parse(_request);
 
-        assertEquals("Unexpected request type", RequestInfo.RequestType.MODEL_OBJECT, info.getType());
+        assertEquals("Unexpected request type", RequestType.MODEL_OBJECT, info.getType());
         assertEquals("Unexpected model parts", Arrays.asList(vhnName, vhName), info.getModelParts());
         assertTrue("Expected exact object request", info.isSingletonRequest());
     }
@@ -85,7 +86,7 @@ public class RequestInfoParserTest extends UnitTestBase
 
         RequestInfo info = parser.parse(_request);
 
-        assertEquals("Unexpected request type", RequestInfo.RequestType.MODEL_OBJECT, info.getType());
+        assertEquals("Unexpected request type", RequestType.MODEL_OBJECT, info.getType());
         assertEquals("Unexpected model parts", Arrays.asList(vhnName), info.getModelParts());
         assertFalse("Expected exact object request", info.isSingletonRequest());
     }
@@ -126,7 +127,7 @@ public class RequestInfoParserTest extends UnitTestBase
 
         RequestInfo info = parser.parse(_request);
 
-        assertEquals("Unexpected request type", RequestInfo.RequestType.MODEL_OBJECT, info.getType());
+        assertEquals("Unexpected request type", RequestType.MODEL_OBJECT, info.getType());
         assertEquals("Unexpected model parts", Arrays.asList(vhnName), info.getModelParts());
     }
 
@@ -144,7 +145,7 @@ public class RequestInfoParserTest extends UnitTestBase
 
         RequestInfo info = parser.parse(_request);
 
-        assertEquals("Unexpected request type", RequestInfo.RequestType.MODEL_OBJECT, info.getType());
+        assertEquals("Unexpected request type", RequestType.MODEL_OBJECT, info.getType());
         assertEquals("Unexpected model parts", Arrays.asList(vhnName, vhName), info.getModelParts());
     }
 
@@ -203,7 +204,7 @@ public class RequestInfoParserTest extends UnitTestBase
 
         RequestInfo info = parser.parse(_request);
 
-        assertEquals("Unexpected request type", RequestInfo.RequestType.MODEL_OBJECT, info.getType());
+        assertEquals("Unexpected request type", RequestType.MODEL_OBJECT, info.getType());
         assertEquals("Unexpected model parts", Arrays.asList(vhnName, vhName), info.getModelParts());
     }
 
@@ -218,7 +219,7 @@ public class RequestInfoParserTest extends UnitTestBase
 
         RequestInfo info = parser.parse(_request);
 
-        assertEquals("Unexpected request type", RequestInfo.RequestType.MODEL_OBJECT, info.getType());
+        assertEquals("Unexpected request type", RequestType.MODEL_OBJECT, info.getType());
         assertEquals("Unexpected model parts", Arrays.asList(vhnName, vhName), info.getModelParts());
     }
 
@@ -271,7 +272,7 @@ public class RequestInfoParserTest extends UnitTestBase
 
         RequestInfo info = parser.parse(_request);
 
-        assertEquals("Unexpected request type", RequestInfo.RequestType.MODEL_OBJECT, info.getType());
+        assertEquals("Unexpected request type", RequestType.MODEL_OBJECT, info.getType());
         assertEquals("Unexpected model parts", Arrays.asList(vhnName, vhName), info.getModelParts());
     }
 
@@ -287,7 +288,7 @@ public class RequestInfoParserTest extends UnitTestBase
 
         RequestInfo info = parser.parse(_request);
 
-        assertEquals("Unexpected request type", RequestInfo.RequestType.MODEL_OBJECT, info.getType());
+        assertEquals("Unexpected request type", RequestType.MODEL_OBJECT, info.getType());
         assertEquals("Unexpected model parts", Arrays.asList(vhnName, vhName), info.getModelParts());
     }
 
@@ -320,7 +321,7 @@ public class RequestInfoParserTest extends UnitTestBase
 
         RequestInfo info = parser.parse(_request);
 
-        assertEquals("Unexpected request type", RequestInfo.RequestType.MODEL_OBJECT, info.getType());
+        assertEquals("Unexpected request type", RequestType.MODEL_OBJECT, info.getType());
         assertEquals("Unexpected model parts", Arrays.asList(vhnName), info.getModelParts());
     }
 
@@ -359,7 +360,7 @@ public class RequestInfoParserTest extends UnitTestBase
 
         RequestInfo info = parser.parse(_request);
 
-        assertEquals("Unexpected request type", RequestInfo.RequestType.OPERATION, info.getType());
+        assertEquals("Unexpected request type", RequestType.OPERATION, info.getType());
         assertEquals("Unexpected model parts", Arrays.asList(vhnName), info.getModelParts());
         assertEquals("Unexpected operation name", operationName, info.getOperationName());
         assertTrue("Expected exact object request", info.isSingletonRequest());


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