Add support for delete operations
This commit adds a `@DeleteOperation` annotation that can be used to indicate that an endpoint's operation is meant to delete a resource. Such operation is mapped to a DELETE http method. Closes gh-10023
This commit is contained in:
parent
26b93e9454
commit
4c7088981f
|
@ -217,9 +217,15 @@ public abstract class AnnotationEndpointDiscoverer<T extends Operation, K>
|
|||
|
||||
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<T extends Operation, K>
|
|||
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<? extends Annotation> operationAnnotation,
|
||||
OperationType operationType) {
|
||||
|
|
|
@ -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 {
|
||||
|
||||
}
|
|
@ -32,6 +32,11 @@ public enum OperationType {
|
|||
/**
|
||||
* A write operation.
|
||||
*/
|
||||
WRITE
|
||||
WRITE,
|
||||
|
||||
/**
|
||||
* A delete operation.
|
||||
*/
|
||||
DELETE
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -32,6 +32,11 @@ public enum WebEndpointHttpMethod {
|
|||
/**
|
||||
* An HTTP POST request.
|
||||
*/
|
||||
POST
|
||||
POST,
|
||||
|
||||
/**
|
||||
* An HTTP DELETE request.
|
||||
*/
|
||||
DELETE
|
||||
|
||||
}
|
||||
|
|
|
@ -64,12 +64,14 @@ public class AnnotationEndpointDiscovererTests {
|
|||
assertThat(endpoints).containsOnlyKeys("test");
|
||||
Map<Method, TestEndpointOperation> 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<Method, TestEndpointOperation> 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<Method, TestEndpointOperation> 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<Method, TestEndpointOperation> 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() {
|
||||
|
||||
}
|
||||
|
|
|
@ -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<String, MBeanOperationInfo> 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")
|
||||
|
|
|
@ -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<String, JmxEndpointOperation> 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<String, JmxEndpointOperation> 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<String, JmxEndpointOperation> 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<String, JmxEndpointOperation> 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<JmxEndpointOperation> endpoint) {
|
||||
Map<String, JmxEndpointOperation> 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)
|
||||
|
|
|
@ -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<T extends Configurable
|
|||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deleteOperation() {
|
||||
load(TestEndpointConfiguration.class,
|
||||
(client) -> 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<T extends Configurable
|
|||
.isNotFound());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nullResponseFromDeleteOperationResultsInNoContentResponseStatus() {
|
||||
load(NullDeleteResponseEndpointConfiguration.class,
|
||||
(context, client) -> 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<T extends Configurable
|
|||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@Import(BaseConfiguration.class)
|
||||
static class VoidDeleteResponseEndpointConfiguration {
|
||||
|
||||
@Bean
|
||||
public VoidDeleteResponseEndpoint voidDeleteResponseEndpoint(
|
||||
EndpointDelegate delegate) {
|
||||
return new VoidDeleteResponseEndpoint(delegate);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Configuration
|
||||
@Import(BaseConfiguration.class)
|
||||
static class NullWriteResponseEndpointConfiguration {
|
||||
|
@ -331,6 +370,17 @@ public abstract class AbstractWebEndpointIntegrationTests<T extends Configurable
|
|||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@Import(BaseConfiguration.class)
|
||||
static class NullDeleteResponseEndpointConfiguration {
|
||||
|
||||
@Bean
|
||||
public NullDeleteResponseEndpoint nullDeleteResponseEndpoint() {
|
||||
return new NullDeleteResponseEndpoint();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@Import(BaseConfiguration.class)
|
||||
static class ResourceEndpointConfiguration {
|
||||
|
@ -377,6 +427,11 @@ public abstract class AbstractWebEndpointIntegrationTests<T extends Configurable
|
|||
this.endpointDelegate.write(foo, bar);
|
||||
}
|
||||
|
||||
@DeleteOperation
|
||||
public Map<String, Object> deletePart(@Selector String part) {
|
||||
return Collections.singletonMap("part", part);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Endpoint(id = "query")
|
||||
|
@ -421,6 +476,22 @@ public abstract class AbstractWebEndpointIntegrationTests<T extends Configurable
|
|||
|
||||
}
|
||||
|
||||
@Endpoint(id = "voiddelete")
|
||||
static class VoidDeleteResponseEndpoint {
|
||||
|
||||
private final EndpointDelegate delegate;
|
||||
|
||||
VoidDeleteResponseEndpoint(EndpointDelegate delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@DeleteOperation
|
||||
public void delete() {
|
||||
this.delegate.delete();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Endpoint(id = "nullwrite")
|
||||
static class NullWriteResponseEndpoint {
|
||||
|
||||
|
@ -448,6 +519,16 @@ public abstract class AbstractWebEndpointIntegrationTests<T extends Configurable
|
|||
|
||||
}
|
||||
|
||||
@Endpoint(id = "nulldelete")
|
||||
static class NullDeleteResponseEndpoint {
|
||||
|
||||
@DeleteOperation
|
||||
public String deleteReturningNull() {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Endpoint(id = "resource")
|
||||
static class ResourceEndpoint {
|
||||
|
||||
|
@ -476,6 +557,8 @@ public abstract class AbstractWebEndpointIntegrationTests<T extends Configurable
|
|||
|
||||
void write(String foo, String bar);
|
||||
|
||||
void delete();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue