Refine 'Allow operations to produce different output'

Refine the new `Producible` support so that it can also be used with
`@ReadOperation`, `@WriteOperation` and `@DeleteOperation` annotations.

This update allows the same enum to be used both as an argument and as
an indicator of the media-types that an operation may produce.

Closes gh-25738
This commit is contained in:
Phillip Webb
2021-03-18 19:12:13 -07:00
parent 1ec49cee8b
commit c81a0223cc
17 changed files with 212 additions and 101 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2021 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.
@@ -22,6 +22,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.endpoint.OperationType;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.util.MimeType;
import org.springframework.util.ReflectionUtils;
import static org.assertj.core.api.Assertions.assertThat;
@@ -48,12 +49,35 @@ class DiscoveredOperationMethodTests {
AnnotationAttributes annotationAttributes = new AnnotationAttributes();
String[] produces = new String[] { "application/json" };
annotationAttributes.put("produces", produces);
annotationAttributes.put("producesFrom", Producible.class);
DiscoveredOperationMethod discovered = new DiscoveredOperationMethod(method, OperationType.READ,
annotationAttributes);
assertThat(discovered.getProducesMediaTypes()).containsExactly("application/json");
}
@Test
void getProducesMediaTypesWhenProducesFromShouldReturnMediaTypes() {
Method method = ReflectionUtils.findMethod(getClass(), "example");
AnnotationAttributes annotationAttributes = new AnnotationAttributes();
annotationAttributes.put("produces", new String[0]);
annotationAttributes.put("producesFrom", ExampleProducible.class);
DiscoveredOperationMethod discovered = new DiscoveredOperationMethod(method, OperationType.READ,
annotationAttributes);
assertThat(discovered.getProducesMediaTypes()).containsExactly("one/*", "two/*", "three/*");
}
void example() {
}
enum ExampleProducible implements Producible<ExampleProducible> {
ONE, TWO, THREE;
@Override
public MimeType getProducedMimeType() {
return new MimeType(toString().toLowerCase());
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2021 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.
@@ -34,6 +34,7 @@ import org.springframework.boot.actuate.endpoint.invoke.OperationInvokerAdvisor;
import org.springframework.boot.actuate.endpoint.invoke.OperationParameters;
import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper;
import org.springframework.boot.actuate.endpoint.invoke.reflect.OperationMethod;
import org.springframework.util.MimeType;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
@@ -121,6 +122,14 @@ class DiscoveredOperationsFactoryTests {
assertThat(advisor.getParameters()).isEmpty();
}
@Test
void createOperationShouldApplyProducesFrom() {
TestOperation operation = getFirst(
this.factory.createOperations(EndpointId.of("test"), new ExampleWithProducesFrom()));
DiscoveredOperationMethod method = (DiscoveredOperationMethod) operation.getOperationMethod();
assertThat(method.getProducesMediaTypes()).containsExactly("one/*", "two/*", "three/*");
}
private <T> T getFirst(Iterable<T> iterable) {
return iterable.iterator().next();
}
@@ -175,6 +184,15 @@ class DiscoveredOperationsFactoryTests {
}
static class ExampleWithProducesFrom {
@ReadOperation(producesFrom = ExampleProducible.class)
String read() {
return "read";
}
}
static class TestDiscoveredOperationsFactory extends DiscoveredOperationsFactory<TestOperation> {
TestDiscoveredOperationsFactory(ParameterValueMapper parameterValueMapper,
@@ -229,4 +247,15 @@ class DiscoveredOperationsFactoryTests {
}
enum ExampleProducible implements Producible<ExampleProducible> {
ONE, TWO, THREE;
@Override
public MimeType getProducedMimeType() {
return new MimeType(toString().toLowerCase());
}
}
}

View File

@@ -39,24 +39,28 @@ class ApiVersionTests {
}
@Test
@Deprecated
void fromHttpHeadersWhenEmptyReturnsLatest() {
ApiVersion version = ApiVersion.fromHttpHeaders(Collections.emptyMap());
assertThat(version).isEqualTo(ApiVersion.V3);
}
@Test
@Deprecated
void fromHttpHeadersWhenHasSingleV2HeaderReturnsV2() {
ApiVersion version = ApiVersion.fromHttpHeaders(acceptHeader(ActuatorMediaType.V2_JSON));
assertThat(version).isEqualTo(ApiVersion.V2);
}
@Test
@Deprecated
void fromHttpHeadersWhenHasSingleV3HeaderReturnsV3() {
ApiVersion version = ApiVersion.fromHttpHeaders(acceptHeader(ActuatorMediaType.V3_JSON));
assertThat(version).isEqualTo(ApiVersion.V3);
}
@Test
@Deprecated
void fromHttpHeadersWhenHasV2AndV3HeaderReturnsV3() {
ApiVersion version = ApiVersion
.fromHttpHeaders(acceptHeader(ActuatorMediaType.V2_JSON, ActuatorMediaType.V3_JSON));
@@ -64,6 +68,7 @@ class ApiVersionTests {
}
@Test
@Deprecated
void fromHttpHeadersWhenHasV2AndV3AsOneHeaderReturnsV3() {
ApiVersion version = ApiVersion
.fromHttpHeaders(acceptHeader(ActuatorMediaType.V2_JSON + "," + ActuatorMediaType.V3_JSON));
@@ -71,18 +76,21 @@ class ApiVersionTests {
}
@Test
@Deprecated
void fromHttpHeadersWhenHasSingleHeaderWithoutJsonReturnsHeader() {
ApiVersion version = ApiVersion.fromHttpHeaders(acceptHeader("application/vnd.spring-boot.actuator.v2"));
assertThat(version).isEqualTo(ApiVersion.V2);
}
@Test
@Deprecated
void fromHttpHeadersWhenHasUnknownVersionReturnsLatest() {
ApiVersion version = ApiVersion.fromHttpHeaders(acceptHeader("application/vnd.spring-boot.actuator.v200"));
assertThat(version).isEqualTo(ApiVersion.V3);
}
@Test
@Deprecated
void fromHttpHeadersWhenAcceptsEverythingReturnsLatest() {
ApiVersion version = ApiVersion.fromHttpHeaders(acceptHeader("*/*"));
assertThat(version).isEqualTo(ApiVersion.V3);

View File

@@ -18,7 +18,6 @@ package org.springframework.boot.actuate.endpoint.invoker.cache;
import java.security.Principal;
import java.time.Duration;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
@@ -203,9 +202,9 @@ class CachingOperationInvokerTests {
Object expectedV2 = new Object();
Object expectedV3 = new Object();
InvocationContext contextV2 = new InvocationContext(mock(SecurityContext.class), Collections.emptyMap(),
Arrays.asList(new ApiVersionArgumentResolver(ApiVersion.V2)));
new ApiVersionArgumentResolver(ApiVersion.V2));
InvocationContext contextV3 = new InvocationContext(mock(SecurityContext.class), Collections.emptyMap(),
Arrays.asList(new ApiVersionArgumentResolver(ApiVersion.V3)));
new ApiVersionArgumentResolver(ApiVersion.V3));
given(target.invoke(contextV2)).willReturn(expectedV2);
given(target.invoke(contextV3)).willReturn(expectedV3);
CachingOperationInvoker invoker = new CachingOperationInvoker(target, CACHE_TTL);