diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/AnnotationEndpointDiscoverer.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/AnnotationEndpointDiscoverer.java index d66638d9b8..4aa125ab97 100644 --- a/spring-boot/src/main/java/org/springframework/boot/endpoint/AnnotationEndpointDiscoverer.java +++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/AnnotationEndpointDiscoverer.java @@ -217,9 +217,15 @@ public abstract class AnnotationEndpointDiscoverer private T createOperationIfPossible(String endpointId, String beanName, Method method) { - T readOperation = createReadOperationIfPossible(endpointId, beanName, method); - return (readOperation != null ? readOperation - : createWriteOperationIfPossible(endpointId, beanName, method)); + T operation = createReadOperationIfPossible(endpointId, beanName, method); + if (operation != null) { + return operation; + } + operation = createWriteOperationIfPossible(endpointId, beanName, method); + if (operation != null) { + return operation; + } + return createDeleteOperationIfPossible(endpointId, beanName, method); } private T createReadOperationIfPossible(String endpointId, String beanName, @@ -234,6 +240,12 @@ public abstract class AnnotationEndpointDiscoverer WriteOperation.class, OperationType.WRITE); } + private T createDeleteOperationIfPossible(String endpointId, String beanName, + Method method) { + return createOperationIfPossible(endpointId, beanName, method, + DeleteOperation.class, OperationType.DELETE); + } + private T createOperationIfPossible(String endpointId, String beanName, Method method, Class operationAnnotation, OperationType operationType) { diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/DeleteOperation.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/DeleteOperation.java new file mode 100644 index 0000000000..5d07fa0502 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/DeleteOperation.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed 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.springframework.boot.endpoint; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Identifies a method on an {@link Endpoint} as being a delete operation. + * + * @author Stephane Nicoll + * @since 2.0.0 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface DeleteOperation { + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/OperationType.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/OperationType.java index 6d1ae90d7b..8f56af0de8 100644 --- a/spring-boot/src/main/java/org/springframework/boot/endpoint/OperationType.java +++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/OperationType.java @@ -32,6 +32,11 @@ public enum OperationType { /** * A write operation. */ - WRITE + WRITE, + + /** + * A delete operation. + */ + DELETE } diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/jmx/EndpointMBeanInfoAssembler.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/jmx/EndpointMBeanInfoAssembler.java index be2e919eb2..e7b7633328 100644 --- a/spring-boot/src/main/java/org/springframework/boot/endpoint/jmx/EndpointMBeanInfoAssembler.java +++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/jmx/EndpointMBeanInfoAssembler.java @@ -103,7 +103,7 @@ class EndpointMBeanInfoAssembler { if (type == OperationType.READ) { return MBeanOperationInfo.INFO; } - if (type == OperationType.WRITE) { + if (type == OperationType.WRITE || type == OperationType.DELETE) { return MBeanOperationInfo.ACTION; } return MBeanOperationInfo.UNKNOWN; diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/web/WebAnnotationEndpointDiscoverer.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/WebAnnotationEndpointDiscoverer.java index 15fd546eb6..0326470ecd 100644 --- a/spring-boot/src/main/java/org/springframework/boot/endpoint/web/WebAnnotationEndpointDiscoverer.java +++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/WebAnnotationEndpointDiscoverer.java @@ -205,6 +205,9 @@ public class WebAnnotationEndpointDiscoverer extends if (operationType == OperationType.WRITE) { return WebEndpointHttpMethod.POST; } + if (operationType == OperationType.DELETE) { + return WebEndpointHttpMethod.DELETE; + } return WebEndpointHttpMethod.GET; } diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/web/WebEndpointHttpMethod.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/WebEndpointHttpMethod.java index d43d41b872..a340682988 100644 --- a/spring-boot/src/main/java/org/springframework/boot/endpoint/web/WebEndpointHttpMethod.java +++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/WebEndpointHttpMethod.java @@ -32,6 +32,11 @@ public enum WebEndpointHttpMethod { /** * An HTTP POST request. */ - POST + POST, + + /** + * An HTTP DELETE request. + */ + DELETE } diff --git a/spring-boot/src/test/java/org/springframework/boot/endpoint/AnnotationEndpointDiscovererTests.java b/spring-boot/src/test/java/org/springframework/boot/endpoint/AnnotationEndpointDiscovererTests.java index e7713f6fc7..dfa04fadb7 100644 --- a/spring-boot/src/test/java/org/springframework/boot/endpoint/AnnotationEndpointDiscovererTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/endpoint/AnnotationEndpointDiscovererTests.java @@ -64,12 +64,14 @@ public class AnnotationEndpointDiscovererTests { assertThat(endpoints).containsOnlyKeys("test"); Map operations = mapOperations( endpoints.get("test")); - assertThat(operations).hasSize(3); + assertThat(operations).hasSize(4); assertThat(operations).containsKeys( ReflectionUtils.findMethod(TestEndpoint.class, "getAll"), ReflectionUtils.findMethod(TestEndpoint.class, "getOne", String.class), ReflectionUtils.findMethod(TestEndpoint.class, "update", String.class, + String.class), + ReflectionUtils.findMethod(TestEndpoint.class, "deleteOne", String.class)); }); } @@ -82,13 +84,15 @@ public class AnnotationEndpointDiscovererTests { assertThat(endpoints).containsOnlyKeys("test"); Map operations = mapOperations( endpoints.get("test")); - assertThat(operations).hasSize(4); + assertThat(operations).hasSize(5); assertThat(operations).containsKeys( ReflectionUtils.findMethod(TestEndpoint.class, "getAll"), ReflectionUtils.findMethod(TestEndpoint.class, "getOne", String.class), ReflectionUtils.findMethod(TestEndpoint.class, "update", String.class, String.class), + ReflectionUtils.findMethod(TestEndpoint.class, "deleteOne", + String.class), ReflectionUtils.findMethod(TestEndpointSubclass.class, "updateWithMoreArguments", String.class, String.class, String.class)); @@ -114,7 +118,7 @@ public class AnnotationEndpointDiscovererTests { assertThat(endpoints).containsOnlyKeys("test"); Map operations = mapOperations( endpoints.get("test")); - assertThat(operations).hasSize(3); + assertThat(operations).hasSize(4); operations.values() .forEach(operation -> assertThat(operation.getInvoker()) .isNotInstanceOf(CachingOperationInvoker.class)); @@ -133,7 +137,7 @@ public class AnnotationEndpointDiscovererTests { assertThat(endpoints).containsOnlyKeys("test"); Map operations = mapOperations( endpoints.get("test")); - assertThat(operations).hasSize(3); + assertThat(operations).hasSize(4); operations.values() .forEach(operation -> assertThat(operation.getInvoker()) .isNotInstanceOf(CachingOperationInvoker.class)); @@ -232,6 +236,11 @@ public class AnnotationEndpointDiscovererTests { } + @DeleteOperation + public void deleteOne(@Selector String id) { + + } + public void someOtherMethod() { } diff --git a/spring-boot/src/test/java/org/springframework/boot/endpoint/jmx/EndpointMBeanTests.java b/spring-boot/src/test/java/org/springframework/boot/endpoint/jmx/EndpointMBeanTests.java index d41d5906b6..842b8fda41 100644 --- a/spring-boot/src/test/java/org/springframework/boot/endpoint/jmx/EndpointMBeanTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/endpoint/jmx/EndpointMBeanTests.java @@ -43,6 +43,7 @@ import reactor.core.publisher.Mono; import org.springframework.boot.endpoint.CachingConfiguration; import org.springframework.boot.endpoint.ConversionServiceOperationParameterMapper; +import org.springframework.boot.endpoint.DeleteOperation; import org.springframework.boot.endpoint.Endpoint; import org.springframework.boot.endpoint.ReadOperation; import org.springframework.boot.endpoint.WriteOperation; @@ -109,6 +110,15 @@ public class EndpointMBeanTests { new Object[] { "one" }, new String[] { String.class.getName() }); assertThat(updatedOneResponse).isEqualTo("1"); + // deleteOne + Object deleteResponse = this.server.invoke(objectName, "deleteOne", + new Object[] { "one" }, new String[] { String.class.getName() }); + assertThat(oneResponse).isEqualTo("ONE"); + + // getOne validation after delete + updatedOneResponse = this.server.invoke(objectName, "getOne", + new Object[] { "one" }, new String[] { String.class.getName() }); + assertThat(updatedOneResponse).isNull(); } catch (Exception ex) { throw new AssertionError("Failed to invoke method on FooEndpoint", ex); @@ -123,7 +133,8 @@ public class EndpointMBeanTests { try { MBeanInfo mBeanInfo = this.server.getMBeanInfo(objectName); Map operations = mapOperations(mBeanInfo); - assertThat(operations).containsOnlyKeys("getAll", "getOne", "update"); + assertThat(operations).containsOnlyKeys("getAll", "getOne", "update", + "deleteOne"); assertOperation(operations.get("getAll"), String.class, MBeanOperationInfo.INFO, new Class[0]); assertOperation(operations.get("getOne"), String.class, @@ -131,6 +142,8 @@ public class EndpointMBeanTests { assertOperation(operations.get("update"), Void.TYPE, MBeanOperationInfo.ACTION, new Class[] { String.class, String.class }); + assertOperation(operations.get("deleteOne"), Void.TYPE, + MBeanOperationInfo.ACTION, new Class[] { String.class }); } catch (Exception ex) { throw new AssertionError("Failed to retrieve MBeanInfo of FooEndpoint", @@ -305,6 +318,11 @@ public class EndpointMBeanTests { this.all.put(name, new Foo(value)); } + @DeleteOperation + public void deleteOne(FooName name) { + this.all.remove(name); + } + } @Endpoint(id = "reactive") diff --git a/spring-boot/src/test/java/org/springframework/boot/endpoint/jmx/JmxAnnotationEndpointDiscovererTests.java b/spring-boot/src/test/java/org/springframework/boot/endpoint/jmx/JmxAnnotationEndpointDiscovererTests.java index 95ad8e97e7..a4d5145c81 100644 --- a/spring-boot/src/test/java/org/springframework/boot/endpoint/jmx/JmxAnnotationEndpointDiscovererTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/endpoint/jmx/JmxAnnotationEndpointDiscovererTests.java @@ -30,6 +30,7 @@ import org.junit.rules.ExpectedException; import org.springframework.boot.endpoint.CachingConfiguration; import org.springframework.boot.endpoint.CachingOperationInvoker; import org.springframework.boot.endpoint.ConversionServiceOperationParameterMapper; +import org.springframework.boot.endpoint.DeleteOperation; import org.springframework.boot.endpoint.Endpoint; import org.springframework.boot.endpoint.EndpointExposure; import org.springframework.boot.endpoint.EndpointInfo; @@ -71,7 +72,7 @@ public class JmxAnnotationEndpointDiscovererTests { Map operationByName = mapOperations( endpoints.get("test").getOperations()); assertThat(operationByName).containsOnlyKeys("getAll", "getSomething", - "update"); + "update", "deleteSomething"); JmxEndpointOperation getAll = operationByName.get("getAll"); assertThat(getAll.getDescription()) .isEqualTo("Invoke getAll for endpoint test"); @@ -92,6 +93,12 @@ public class JmxAnnotationEndpointDiscovererTests { assertThat(update.getParameters()).hasSize(2); hasDefaultParameter(update, 0, String.class); hasDefaultParameter(update, 1, String.class); + JmxEndpointOperation deleteSomething = operationByName.get("deleteSomething"); + assertThat(deleteSomething.getDescription()) + .isEqualTo("Invoke deleteSomething for endpoint test"); + assertThat(deleteSomething.getOutputType()).isEqualTo(Void.TYPE); + assertThat(deleteSomething.getParameters()).hasSize(1); + hasDefaultParameter(deleteSomething, 0, String.class); }); } @@ -136,7 +143,7 @@ public class JmxAnnotationEndpointDiscovererTests { Map operationByName = mapOperations( endpoints.get("test").getOperations()); assertThat(operationByName).containsOnlyKeys("getAll", "getSomething", - "update", "getAnother"); + "update", "deleteSomething", "getAnother"); JmxEndpointOperation getAnother = operationByName.get("getAnother"); assertThat(getAnother.getDescription()).isEqualTo("Get another thing"); assertThat(getAnother.getOutputType()).isEqualTo(Object.class); @@ -153,7 +160,7 @@ public class JmxAnnotationEndpointDiscovererTests { Map operationByName = mapOperations( endpoints.get("test").getOperations()); assertThat(operationByName).containsOnlyKeys("getAll", "getSomething", - "update"); + "update", "deleteSomething"); JmxEndpointOperation getAll = operationByName.get("getAll"); assertThat(getAll.getInvoker()).isInstanceOf(CachingOperationInvoker.class); assertThat(((CachingOperationInvoker) getAll.getInvoker()).getTimeToLive()) @@ -171,7 +178,7 @@ public class JmxAnnotationEndpointDiscovererTests { Map operationByName = mapOperations( endpoints.get("test").getOperations()); assertThat(operationByName).containsOnlyKeys("getAll", "getSomething", - "update", "getAnother"); + "update", "deleteSomething", "getAnother"); JmxEndpointOperation getAll = operationByName.get("getAll"); assertThat(getAll.getInvoker()) .isInstanceOf(CachingOperationInvoker.class); @@ -239,7 +246,8 @@ public class JmxAnnotationEndpointDiscovererTests { private void assertJmxTestEndpoint(EndpointInfo endpoint) { Map operationByName = mapOperations( endpoint.getOperations()); - assertThat(operationByName).containsOnlyKeys("getAll", "getSomething", "update"); + assertThat(operationByName).containsOnlyKeys("getAll", "getSomething", "update", + "deleteSomething"); JmxEndpointOperation getAll = operationByName.get("getAll"); assertThat(getAll.getDescription()).isEqualTo("Get all the things"); assertThat(getAll.getOutputType()).isEqualTo(Object.class); @@ -257,6 +265,13 @@ public class JmxAnnotationEndpointDiscovererTests { assertThat(update.getParameters()).hasSize(2); hasDocumentedParameter(update, 0, "foo", String.class, "Foo identifier"); hasDocumentedParameter(update, 1, "bar", String.class, "Bar value"); + JmxEndpointOperation deleteSomething = operationByName.get("deleteSomething"); + assertThat(deleteSomething.getDescription()) + .isEqualTo("Delete something based on a timeUnit"); + assertThat(deleteSomething.getOutputType()).isEqualTo(Void.TYPE); + assertThat(deleteSomething.getParameters()).hasSize(1); + hasDocumentedParameter(deleteSomething, 0, "unitMs", Long.class, + "Number of milliseconds"); } private void hasDefaultParameter(JmxEndpointOperation operation, int index, @@ -329,6 +344,11 @@ public class JmxAnnotationEndpointDiscovererTests { } + @DeleteOperation + public void deleteSomething(TimeUnit timeUnit) { + + } + } @Endpoint(id = "jmx", exposure = EndpointExposure.JMX) @@ -367,6 +387,14 @@ public class JmxAnnotationEndpointDiscovererTests { } + @DeleteOperation + @ManagedOperation(description = "Delete something based on a timeUnit") + @ManagedOperationParameters({ + @ManagedOperationParameter(name = "unitMs", description = "Number of milliseconds") }) + public void deleteSomething(Long timeUnit) { + + } + } @JmxEndpointExtension(endpoint = TestEndpoint.class) diff --git a/spring-boot/src/test/java/org/springframework/boot/endpoint/web/AbstractWebEndpointIntegrationTests.java b/spring-boot/src/test/java/org/springframework/boot/endpoint/web/AbstractWebEndpointIntegrationTests.java index 0c4fe80aee..d89e70b413 100644 --- a/spring-boot/src/test/java/org/springframework/boot/endpoint/web/AbstractWebEndpointIntegrationTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/endpoint/web/AbstractWebEndpointIntegrationTests.java @@ -27,6 +27,7 @@ import org.junit.Test; import org.springframework.boot.endpoint.CachingConfiguration; import org.springframework.boot.endpoint.ConversionServiceOperationParameterMapper; +import org.springframework.boot.endpoint.DeleteOperation; import org.springframework.boot.endpoint.Endpoint; import org.springframework.boot.endpoint.OperationParameterMapper; import org.springframework.boot.endpoint.ReadOperation; @@ -159,6 +160,23 @@ public abstract class AbstractWebEndpointIntegrationTests client.delete().uri("/test/one") + .accept(MediaType.APPLICATION_JSON).exchange().expectStatus() + .isOk().expectBody().jsonPath("part").isEqualTo("one")); + } + + @Test + public void deleteOperationWithVoidResponse() { + load(VoidDeleteResponseEndpointConfiguration.class, (context, client) -> { + client.delete().uri("/voiddelete").accept(MediaType.APPLICATION_JSON).exchange() + .expectStatus().isNoContent().expectBody().isEmpty(); + verify(context.getBean(EndpointDelegate.class)).delete(); + }); + } + @Test public void nullIsPassedToTheOperationWhenArgumentIsNotFoundInPostRequestBody() { load(TestEndpointConfiguration.class, (context, client) -> { @@ -188,6 +206,14 @@ public abstract class AbstractWebEndpointIntegrationTests client.delete().uri("/nulldelete") + .accept(MediaType.APPLICATION_JSON).exchange().expectStatus() + .isNoContent()); + } + @Test public void nullResponseFromWriteOperationResultsInNoContentResponseStatus() { load(NullWriteResponseEndpointConfiguration.class, @@ -308,6 +334,19 @@ public abstract class AbstractWebEndpointIntegrationTests deletePart(@Selector String part) { + return Collections.singletonMap("part", part); + } + } @Endpoint(id = "query") @@ -421,6 +476,22 @@ public abstract class AbstractWebEndpointIntegrationTests