From commits-return-47644-archive-asf-public=cust-asf.ponee.io@qpid.apache.org Tue Dec 18 19:23:57 2018 Return-Path: X-Original-To: archive-asf-public@cust-asf.ponee.io Delivered-To: archive-asf-public@cust-asf.ponee.io Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by mx-eu-01.ponee.io (Postfix) with SMTP id 33855180675 for ; Tue, 18 Dec 2018 19:23:54 +0100 (CET) Received: (qmail 51422 invoked by uid 500); 18 Dec 2018 18:23:53 -0000 Mailing-List: contact commits-help@qpid.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: dev@qpid.apache.org Delivered-To: mailing list commits@qpid.apache.org Received: (qmail 51413 invoked by uid 99); 18 Dec 2018 18:23:53 -0000 Received: from ec2-52-202-80-70.compute-1.amazonaws.com (HELO gitbox.apache.org) (52.202.80.70) by apache.org (qpsmtpd/0.29) with ESMTP; Tue, 18 Dec 2018 18:23:53 +0000 Received: by gitbox.apache.org (ASF Mail Server at gitbox.apache.org, from userid 33) id 7054885289; Tue, 18 Dec 2018 18:23:52 +0000 (UTC) Date: Tue, 18 Dec 2018 18:23:53 +0000 To: "commits@qpid.apache.org" Subject: [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 MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit From: orudyy@apache.org In-Reply-To: <154515743233.26070.7474398988535470535@gitbox.apache.org> References: <154515743233.26070.7474398988535470535@gitbox.apache.org> X-Git-Host: gitbox.apache.org X-Git-Repo: qpid-broker-j X-Git-Refname: refs/heads/master X-Git-Reftype: branch X-Git-Rev: d0a6a700c6b9b2f9ec1f1885bc4a73c796bd5f2b X-Git-NotificationType: diff X-Git-Multimail-Version: 1.5.dev Auto-Submitted: auto-generated Message-Id: <20181218182352.7054885289@gitbox.apache.org> 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 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 implem return factory; } - private void addRestServlet(ServletContextHandler root) + private void addRestServlet(final ServletContextHandler root) { - Set> categories = new HashSet<>(getModel().getSupportedCategories()); - final RestServlet restServlet = new RestServlet(); + final Map factories = ManagementControllerFactory.loadFactories(); final ApiDocsServlet apiDocsServlet = new ApiDocsServlet(); - - for (Class 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 paths = Arrays.asList("/api/latest/" + name, - "/api/v" + BrokerModel.MODEL_VERSION + "/" + name); - - for (String path : paths) + final List 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 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 getApiProperties() - { - return Collections.singletonMap("supportedVersions", getSupportedRestApiVersions()); - } - - private List 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> 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 getCategories(); + + String getCategoryMapping(String category); + + String getCategory(ConfiguredObject managedObject); + + Collection 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 path, + Map> parameters) throws ManagementException; + + Object createOrUpdate(ConfiguredObject root, + String category, + List path, + Map attributes, + boolean isPost) throws ManagementException; + + int delete(ConfiguredObject root, + String category, + List path, + Map> parameters) throws ManagementException; + + ManagementResponse invoke(ConfiguredObject root, + String category, + List path, + String operation, + Map parameters, + boolean isPost, + final boolean isSecureOrAllowedOnInsecureChannel) throws ManagementException; + + Object getPreferences(ConfiguredObject root, + String category, + List path, + Map> parameters) throws ManagementException; + + void setPreferences(ConfiguredObject root, + String category, + List path, + Object preferences, + Map> parameters, + boolean isPost) throws ManagementException; + + int deletePreferences(ConfiguredObject root, + String category, + List path, + Map> parameters) throws ManagementException; + + Object formatConfiguredObject(Object content, + Map> 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 loadFactories() + { + final Iterable 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 _headers; + + private ManagementException(final int statusCode, + final String message, + final Map headers) + { + super(message); + _statusCode = statusCode; + _headers = headers; + } + + private ManagementException(final int statusCode, + final String message, + final Throwable cause, + final Map headers) + { + super(message, cause); + _statusCode = statusCode; + _headers = headers; + } + + public int getStatusCode() + { + return _statusCode; + } + + public Map 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 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 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 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 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> getParameters(); + + String getParameter(String name); + + Map getParametersAsFlatMap(); + + Map getHeaders(); + + List getPath(); + + String getCategory(); + + ConfiguredObject getRoot(); + + boolean isSecure(); + + boolean isConfidentialOperationAllowedOnInsecureChannel(); + + T getBody(Class 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 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 hierarchy = getCategoryHierarchy(request.getRoot(), request.getCategory()); + final List 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 hierarchy = getCategoryHierarchy(request.getRoot(), request.getCategory()); + switch (type) + { + case OPERATION: + { + final List operationPath = request.getPath().subList(0, hierarchy.size()); + final String operationName = request.getPath().get(hierarchy.size()); + @SuppressWarnings("unchecked") + final Map 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 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 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> parameters) + { + List values = parameters.get(name); + return values == null || values.isEmpty() ? null : values.get(0); + } + + public static int getIntParameterFromRequest(final Map> 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 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, ConfiguredObjectFinder> _configuredObjectFinders = + new ConcurrentHashMap<>(); + private final Set _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 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 getCategoryHierarchy(final ConfiguredObject root, final String category) + { + ConfiguredObjectFinder finder = getConfiguredObjectFinder(root); + Class[] 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 configuredClass = getRequestCategoryClass(category, root.getModel()); + final Class[] hierarchy = finder.getHierarchy(configuredClass); + return getManagementRequestType(request.getMethod(), category, request.getPath(), hierarchy); + } + + @Override + public Object get(final ConfiguredObject root, + final String category, + final List path, + final Map> parameters) throws ManagementException + { + try + { + final Predicate> filterPredicate = buildFilterPredicates(parameters); + final boolean singleObjectRequest = isFullPath(root, path, category) && !hasFilter(parameters); + final Collection> 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 path, + final Map providedObject, + final boolean isPost) throws ManagementException + { + try + { + final List hierarchy = getCategoryHierarchy(root, category); + if (path.isEmpty() && hierarchy.size() == 0) + { + root.setAttributes(providedObject); + return null; + } + final Class 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 names, + final Map> parameters) throws ManagementException + { + int counter = 0; + try + { + final Predicate> filterPredicate = buildFilterPredicates(parameters); + final Collection> 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 names, + final String operationName, + final Map 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> 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 path, + final Map> parameters) throws ManagementException + { + Object responseObject; + try + { + final List hierarchy = getCategoryHierarchy(root, category); + final Collection> 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 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 path, + final Object providedObject, + final Map> parameters, + final boolean isPost) throws ManagementException + + { + try + { + final List 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 names, + final Map> parameters) throws ManagementException + { + int counter = 0; + try + { + final List hierarchy = getCategoryHierarchy(root, category); + final RequestInfo requestInfo = RequestInfo.createPreferencesRequestInfo(names.subList(0, hierarchy.size()), + names.subList(hierarchy.size() + 1, + names.size()), + parameters); + final Collection> 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> 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> 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 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 parts, final String category) + { + List 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> getTargetObjects(final ConfiguredObject root, + final String category, + final List path, + final Predicate> filterPredicate) + { + final ConfiguredObjectFinder finder = getConfiguredObjectFinder(root); + final Class configuredClass = getRequestCategoryClass(category, root.getModel()); + Collection> 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 parts, + final Class[] 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 parts, + final Class[] 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[/[/]]'", + servletPath, + String.join("/", parts), + expectedPath, + expectedPath)); + } + + private RequestType getGetRequestType(final List parts, + final Class[] 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[/]'", + servletPath, + String.join("/", parts), + expectedPath)); + } + + private RequestType getPutRequestType(final List parts, + final Class[] 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 parts, + final Class[] 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> 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/'" + + " or '%s'" + + " or '%s/userpreferences[/]'", + servletPath, + String.join("/", parts), + expectedFullPath, + expectedParentPath, + expectedFullPath)); + } + + private Class getRequestCategoryClass(final String categoryName, + final Model model) + { + for (Class 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> hierarchy) + { + final StringBuilder expectedPath = new StringBuilder(servletPath); + for (Class part : hierarchy) + { + expectedPath.append("/<"); + expectedPath.append(part.getSimpleName().toLowerCase()); + expectedPath.append(" name>"); + } + return expectedPath.toString(); + } + + private Predicate> buildFilterPredicates(final Map> parameters) + { + return parameters.entrySet().stream() + .filter(entry -> !RESERVED_PARAMS.contains(entry.getKey())) + .map(entry -> { + final String paramName = entry.getKey(); + final List allowedValues = entry.getValue(); + return (Predicate>) 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 names) + { + final Class configuredClass = getRequestCategoryClass(category, root.getModel()); + final ConfiguredObject target; + final ConfiguredObjectFinder finder = getConfiguredObjectFinder(root); + final Class[] 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> 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 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> getTargetObjects(final Class configuredClass, - final ConfiguredObjectFinder finder, - RequestInfo requestInfo, - List>> filterPredicateList) - { - List names = requestInfo.getModelParts(); + final ServletConfig servletConfig = getServletConfig(); + final ServletContext servletContext = servletConfig.getServletContext(); - Collection> 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> iter = targetObjects.iterator(); - while (iter.hasNext()) + if (controller.getVersion().equals(modelVersion)) { - ConfiguredObject obj = iter.next(); - for (Predicate> predicate : filterPredicateList) - { - if (!predicate.apply(obj)) - { - iter.remove(); - break; - } - } + _managementController = controller; + break; } - + controller = controller.getNextVersionManagementController(); } - return targetObjects; - } + while (controller != null); - private List>> buildFilterPredicates(final HttpServletRequest request) - { - List>> predicates = new ArrayList<>(); - - for (final String paramName : Collections.list(request.getParameterNames())) + if (_managementController == null) { - if (!RESERVED_PARAMS.contains(paramName)) - { - final List allowedValues = Arrays.asList(request.getParameterValues(paramName)); - - predicates.add(new Predicate>() - { - @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 configuredClass = getConfiguredClass(request, managedObject); - if(configuredClass == null) - { - sendError(response, HttpServletResponse.SC_NOT_FOUND); - return; - } - final Class[] 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>> filterPredicateList = buildFilterPredicates(request); - Collection> 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 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[] hierarchy) - { - if (hierarchy.length > 0) - { - List pathInfoElements = requestInfo.getModelParts(); - return pathInfoElements.size() == hierarchy.length; + sendResponse(request, response, httpServletRequest, httpServletResponse, controller); } - - return false; - } - - private Class getConfiguredClass(HttpServletRequest request, ConfiguredObject managedObject) - { - final String[] servletPathElements = request.getServletPath().split("/"); - String categoryName = servletPathElements[servletPathElements.length-1]; - Model model = managedObject.getModel(); - for(Class 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 configuredClass = getConfiguredClass(request, managedObject); - final Class[] 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 names = requestInfo.getModelParts(); - boolean isFullObjectURL = names.size() == hierarchy.length; - Map providedObject = getRequestProvidedObject(request, requestInfo); - if (names.isEmpty() && hierarchy.length == 0) - { - managedObject.setAttributes(providedObject); - response.setStatus(HttpServletResponse.SC_OK); - return; - } - - ConfiguredObject theParent = managedObject; - Class 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 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> allObjects = getTargetObjects( - configuredClass, - finder, - requestInfo, - Collections.>>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) 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 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 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 headers = managementException.getHeaders(); + if (headers != null) { - return; + headers.forEach(response::setHeader); } - String operationName = requestInfo.getOperationName(); - final Map> availableOperations = - managedObject.getModel().getTypeRegistry().getOperations(target.getClass()); - ConfiguredObjectOperation operation = availableOperations.get(operationName); - Map 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 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> 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> 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 configuredClass, - final ConfiguredObjectFinder finder) throws IOException + private void setHeaders(final HttpServletResponse response) { - final ConfiguredObject target; - final List names = requestInfo.getModelParts(); - final Class[] 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> parseQueryString(String queryString) + { + if (Strings.isNullOrEmpty(queryString)) { - ConfiguredObject theParent = managedObject; - if (hierarchy.length > 1) + return Collections.emptyMap(); + } + Map> query = new LinkedHashMap<>(); + final String[] pairs = queryString.split("&"); + for (String pairString : pairs) + { + List 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 getOperationArgumentsAsMap(HttpServletRequest request) + private static class ServletManagementRequest implements ManagementRequest { - Map providedObject; - providedObject = new HashMap<>(); - for (Map.Entry entry : request.getParameterMap().entrySet()) + private final HttpPort _port; + private final HttpServletRequest _request; + private final Map> _query; + private final List _path; + private final ConfiguredObject _root; + private final String _category; + private final Map _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 headers = Collections.list(request.getHeaderNames()) + .stream() + .collect(Collectors.toMap(name -> name, request::getHeader)); + _headers = Collections.unmodifiableMap(headers); } - return providedObject; - } - private Map getRequestProvidedObject(HttpServletRequest request, final RequestInfo requestInfo) - throws IOException, ServletException - { - return getRequestProvidedObject(request, requestInfo, LinkedHashMap.class); - } + public ConfiguredObject getRoot() + { + return _root; + } - private T getRequestProvidedObject(HttpServletRequest request, - final RequestInfo requestInfo, - Class expectedClass) - throws IOException, ServletException - { - T providedObject; + public boolean isSecure() + { + return _request.isSecure(); + } - ArrayList 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 getPath() { - providedObject = (T) new LinkedHashMap<>(); - Map fileUploads = new HashMap<>(); - Collection 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) providedObject).putAll(fileUploads); + public String getMethod() + { + return _request.getMethod(); } - else + + public Map> 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 values = _query.get(name); + return values == null || values.isEmpty() ? null : values.get(0); } - else + + public Map 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 getBody(Class 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 configuredClass = getConfiguredClass(request, managedObject); - final Class[] hierarchy = finder.getHierarchy(configuredClass); - RequestInfoParser requestInfoParser = new RequestInfoParser(hierarchy); - - RequestInfo requestInfo = requestInfoParser.parse(request); - - Collection> 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 parse(Class 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 items = new LinkedHashMap<>(); + Map fileUploads = new HashMap<>(); + Collection 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 getParametersAsFlatMap() { - try + final Map providedObject = new HashMap<>(); + for (Map.Entry> entry : _query.entrySet()) { - intValue = Integer.parseInt(stringValue); - } - catch (NumberFormatException e) - { - LOGGER.warn("Could not parse " + stringValue + " as integer for parameter " + paramName); + final List 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> 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 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 expected = Collections.singletonList("Exchange"); + assertThat(_controller.getCategoryHierarchy(object, "Exchange"), is(equalTo(expected))); + } + + + @Test + public void getCategoryHierarchyForBrokerRootAndUnknownCategory() + { + final Broker object = BrokerTestHelper.createBrokerMock(); + final Collection 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 path = Arrays.asList(virtualHost.getParent().getName(), hostName); + + Map 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 preferenceValue = Collections.singletonMap("foo", "bar"); + final Subject testSubject = createTestSubject(); + final String prefernceName = "test"; + createPreferences(testSubject, virtualHost, preferencesType, prefernceName, preferenceValue); + + List path = Arrays.asList(virtualHost.getParent().getName(), hostName, "userpreferences"); + final Object preferences = Subject.doAs(testSubject, (PrivilegedAction) () -> + _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 preferenceValue = Collections.singletonMap("foo", "bar"); + final Subject testSubject = createTestSubject(); + final String preferenceName = "pref"; + final UUID id = createPreferences(testSubject, virtualHost, preferencesType, preferenceName, preferenceValue); + final List path = Arrays.asList(virtualHost.getParent().getName(), hostName, "userpreferences"); + final Map newValue = Collections.singletonMap("foo", "bar2"); + final Map data = new HashMap<>(); + data.put("id", id.toString()); + data.put("name", preferenceName); + data.put("value", newValue); + final Map> modifiedPreferences = Collections.singletonMap(preferencesType, + Collections.singletonList(data)); + Subject.doAs(testSubject, (PrivilegedAction) () -> { + _controller.setPreferences(virtualHost.getBroker(), + "virtualhost", + path, + modifiedPreferences, + Collections.emptyMap(), + true); + return null; + }); + final Object preferences = Subject.doAs(testSubject, (PrivilegedAction) () -> + _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 preferenceValue = Collections.singletonMap("foo", "bar"); + final Subject testSubject = createTestSubject(); + final String preferenceName = "pref"; + createPreferences(testSubject, virtualHost, preferencesType, preferenceName, preferenceValue); + + final List path = Arrays.asList(virtualHost.getParent().getName(), + hostName, + "userpreferences", + preferencesType, + preferenceName); + + Subject.doAs(testSubject, (PrivilegedAction) () -> { + _controller.deletePreferences(virtualHost.getBroker(), + "virtualhost", + path, + Collections.emptyMap()); + return null; + }); + + final List path2 = Arrays.asList(virtualHost.getParent().getName(), hostName, "userpreferences"); + + final Object preferences = Subject.doAs(testSubject, (PrivilegedAction) () -> + _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 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 preferenceList = Collections.singletonList(preference); + final Future result = Subject.doAs(testSubject, + (PrivilegedAction>) () -> 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 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