Skip to content

Commit c81a022

Browse files
committed
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
1 parent 1ec49ce commit c81a022

17 files changed

+212
-101
lines changed

spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/InvocationContext.java

Lines changed: 7 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
import java.util.Arrays;
2222
import java.util.List;
2323
import java.util.Map;
24-
import java.util.function.Supplier;
2524

2625
import org.springframework.boot.actuate.endpoint.http.ApiVersion;
2726
import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker;
@@ -58,11 +57,11 @@ public InvocationContext(SecurityContext securityContext, Map<String, Object> ar
5857
* @param arguments the arguments available to the operation. Never {@code null}
5958
* @since 2.2.0
6059
* @deprecated since 2.5.0 in favor of
61-
* {@link #InvocationContext(SecurityContext, Map, List)}
60+
* {@link #InvocationContext(SecurityContext, Map, OperationArgumentResolver[])}
6261
*/
6362
@Deprecated
6463
public InvocationContext(ApiVersion apiVersion, SecurityContext securityContext, Map<String, Object> arguments) {
65-
this(securityContext, arguments, Arrays.asList(new FixedValueArgumentResolver<>(ApiVersion.class, apiVersion)));
64+
this(securityContext, arguments, OperationArgumentResolver.of(ApiVersion.class, () -> apiVersion));
6665
}
6766

6867
/**
@@ -74,17 +73,17 @@ public InvocationContext(ApiVersion apiVersion, SecurityContext securityContext,
7473
* the operation.
7574
*/
7675
public InvocationContext(SecurityContext securityContext, Map<String, Object> arguments,
77-
List<OperationArgumentResolver> argumentResolvers) {
76+
OperationArgumentResolver... argumentResolvers) {
7877
Assert.notNull(securityContext, "SecurityContext must not be null");
7978
Assert.notNull(arguments, "Arguments must not be null");
8079
this.arguments = arguments;
8180
this.argumentResolvers = new ArrayList<>();
8281
if (argumentResolvers != null) {
83-
this.argumentResolvers.addAll(argumentResolvers);
82+
this.argumentResolvers.addAll(Arrays.asList(argumentResolvers));
8483
}
85-
this.argumentResolvers.add(new FixedValueArgumentResolver<>(SecurityContext.class, securityContext));
86-
this.argumentResolvers.add(new SuppliedValueArgumentResolver<>(Principal.class, securityContext::getPrincipal));
87-
this.argumentResolvers.add(new FixedValueArgumentResolver<>(ApiVersion.class, ApiVersion.LATEST));
84+
this.argumentResolvers.add(OperationArgumentResolver.of(SecurityContext.class, () -> securityContext));
85+
this.argumentResolvers.add(OperationArgumentResolver.of(Principal.class, securityContext::getPrincipal));
86+
this.argumentResolvers.add(OperationArgumentResolver.of(ApiVersion.class, () -> ApiVersion.LATEST));
8887
}
8988

9089
/**
@@ -154,52 +153,4 @@ public boolean canResolve(Class<?> type) {
154153
return false;
155154
}
156155

157-
private static final class FixedValueArgumentResolver<T> implements OperationArgumentResolver {
158-
159-
private final Class<T> argumentType;
160-
161-
private final T value;
162-
163-
private FixedValueArgumentResolver(Class<T> argumentType, T value) {
164-
this.argumentType = argumentType;
165-
this.value = value;
166-
}
167-
168-
@SuppressWarnings("unchecked")
169-
@Override
170-
public <U> U resolve(Class<U> type) {
171-
return (U) this.value;
172-
}
173-
174-
@Override
175-
public boolean canResolve(Class<?> type) {
176-
return this.argumentType.equals(type);
177-
}
178-
179-
}
180-
181-
private static final class SuppliedValueArgumentResolver<T> implements OperationArgumentResolver {
182-
183-
private final Class<T> argumentType;
184-
185-
private final Supplier<T> value;
186-
187-
private SuppliedValueArgumentResolver(Class<T> argumentType, Supplier<T> value) {
188-
this.argumentType = argumentType;
189-
this.value = value;
190-
}
191-
192-
@SuppressWarnings("unchecked")
193-
@Override
194-
public <U> U resolve(Class<U> type) {
195-
return (U) this.value.get();
196-
}
197-
198-
@Override
199-
public boolean canResolve(Class<?> type) {
200-
return this.argumentType.equals(type);
201-
}
202-
203-
}
204-
205156
}

spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/OperationArgumentResolver.java

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616

1717
package org.springframework.boot.actuate.endpoint;
1818

19+
import java.util.function.Supplier;
20+
21+
import org.springframework.util.Assert;
22+
1923
/**
2024
* Resolver for an argument of an {@link Operation}.
2125
*
@@ -24,6 +28,14 @@
2428
*/
2529
public interface OperationArgumentResolver {
2630

31+
/**
32+
* Return whether an argument of the given {@code type} can be resolved.
33+
* @param type argument type
34+
* @return {@code true} if an argument of the required type can be resolved, otherwise
35+
* {@code false}
36+
*/
37+
boolean canResolve(Class<?> type);
38+
2739
/**
2840
* Resolves an argument of the given {@code type}.
2941
* @param <T> required type of the argument
@@ -33,11 +45,30 @@ public interface OperationArgumentResolver {
3345
<T> T resolve(Class<T> type);
3446

3547
/**
36-
* Return whether an argument of the given {@code type} can be resolved.
37-
* @param type argument type
38-
* @return {@code true} if an argument of the required type can be resolved, otherwise
39-
* {@code false}
48+
* Factory method that creates an {@link OperationArgumentResolver} for a specific
49+
* type using a {@link Supplier}.
50+
* @param <T> the resolvable type
51+
* @param type the resolvable type
52+
* @param supplier the value supplier
53+
* @return an {@link OperationArgumentResolver} instance
4054
*/
41-
boolean canResolve(Class<?> type);
55+
static <T> OperationArgumentResolver of(Class<T> type, Supplier<? extends T> supplier) {
56+
Assert.notNull(type, "Type must not be null");
57+
Assert.notNull(supplier, "Supplier must not be null");
58+
return new OperationArgumentResolver() {
59+
60+
@Override
61+
public boolean canResolve(Class<?> actualType) {
62+
return actualType.equals(type);
63+
}
64+
65+
@Override
66+
@SuppressWarnings("unchecked")
67+
public <R> R resolve(Class<R> argumentType) {
68+
return (R) supplier.get();
69+
}
70+
71+
};
72+
}
4273

4374
}

spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/DeleteOperation.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2019 the original author or authors.
2+
* Copyright 2012-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -40,4 +40,11 @@
4040
*/
4141
String[] produces() default {};
4242

43+
/**
44+
* The media types of the result of the operation.
45+
* @return the media types
46+
*/
47+
@SuppressWarnings("rawtypes")
48+
Class<? extends Producible> producesFrom() default Producible.class;
49+
4350
}

spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/DiscoveredOperationMethod.java

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2019 the original author or authors.
2+
* Copyright 2012-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,6 +17,7 @@
1717
package org.springframework.boot.actuate.endpoint.annotation;
1818

1919
import java.lang.reflect.Method;
20+
import java.util.ArrayList;
2021
import java.util.Arrays;
2122
import java.util.Collections;
2223
import java.util.List;
@@ -40,8 +41,32 @@ public DiscoveredOperationMethod(Method method, OperationType operationType,
4041
AnnotationAttributes annotationAttributes) {
4142
super(method, operationType);
4243
Assert.notNull(annotationAttributes, "AnnotationAttributes must not be null");
43-
String[] produces = annotationAttributes.getStringArray("produces");
44-
this.producesMediaTypes = Collections.unmodifiableList(Arrays.asList(produces));
44+
List<String> producesMediaTypes = new ArrayList<>();
45+
producesMediaTypes.addAll(Arrays.asList(annotationAttributes.getStringArray("produces")));
46+
producesMediaTypes.addAll(getProducesFromProducable(annotationAttributes));
47+
this.producesMediaTypes = Collections.unmodifiableList(producesMediaTypes);
48+
}
49+
50+
private <E extends Enum<E> & Producible<E>> List<String> getProducesFromProducable(
51+
AnnotationAttributes annotationAttributes) {
52+
Class<?> type = getProducesFrom(annotationAttributes);
53+
if (type == Producible.class) {
54+
return Collections.emptyList();
55+
}
56+
List<String> produces = new ArrayList<>();
57+
for (Object value : type.getEnumConstants()) {
58+
produces.add(((Producible<?>) value).getProducedMimeType().toString());
59+
}
60+
return produces;
61+
}
62+
63+
private Class<?> getProducesFrom(AnnotationAttributes annotationAttributes) {
64+
try {
65+
return annotationAttributes.getClass("producesFrom");
66+
}
67+
catch (IllegalArgumentException ex) {
68+
return Producible.class;
69+
}
4570
}
4671

4772
public List<String> getProducesMediaTypes() {
Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,23 @@
1414
* limitations under the License.
1515
*/
1616

17-
package org.springframework.boot.actuate.endpoint.http;
17+
package org.springframework.boot.actuate.endpoint.annotation;
1818

1919
import org.springframework.util.MimeType;
2020

2121
/**
22-
* Interface to be implemented by an {@link Enum} that can be injected into an operation
23-
* on a web endpoint. The value of the {@code Producible} enum is resolved using the
24-
* {@code Accept} header of the request. When multiple values are equally acceptable, the
25-
* value with the highest {@link Enum#ordinal() ordinal} is used.
22+
* Interface that can be implemented by any {@link Enum} that represents a finite set of
23+
* producible mime-types.
24+
* <p>
25+
* Can be used with {@link ReadOperation @ReadOperation},
26+
* {@link WriteOperation @ReadOperation} and {@link DeleteOperation @ReadOperation}
27+
* annotations to quickly define a list of {@code produces} values.
28+
* <p>
29+
* {@link Producible} types can also be injected into operations when the underlying
30+
* technology supports content negotiation. For example, with web based endpoints, the
31+
* value of the {@code Producible} enum is resolved using the {@code Accept} header of the
32+
* request. When multiple values are equally acceptable, the value with the highest
33+
* {@link Enum#ordinal() ordinal} is used.
2634
*
2735
* @param <E> enum type that implements this interface
2836
* @author Andy Wilkinson
@@ -34,6 +42,6 @@ public interface Producible<E extends Enum<E> & Producible<E>> {
3442
* Mime type that can be produced.
3543
* @return the producible mime type
3644
*/
37-
MimeType getMimeType();
45+
MimeType getProducedMimeType();
3846

3947
}

spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/ReadOperation.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2019 the original author or authors.
2+
* Copyright 2012-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -39,4 +39,11 @@
3939
*/
4040
String[] produces() default {};
4141

42+
/**
43+
* The media types of the result of the operation.
44+
* @return the media types
45+
*/
46+
@SuppressWarnings("rawtypes")
47+
Class<? extends Producible> producesFrom() default Producible.class;
48+
4249
}

spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/annotation/WriteOperation.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2019 the original author or authors.
2+
* Copyright 2012-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -39,4 +39,11 @@
3939
*/
4040
String[] produces() default {};
4141

42+
/**
43+
* The media types of the result of the operation.
44+
* @return the media types
45+
*/
46+
@SuppressWarnings("rawtypes")
47+
Class<? extends Producible> producesFrom() default Producible.class;
48+
4249
}

spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/http/ApiVersion.java

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.util.List;
2020
import java.util.Map;
2121

22+
import org.springframework.boot.actuate.endpoint.annotation.Producible;
2223
import org.springframework.util.CollectionUtils;
2324
import org.springframework.util.MimeType;
2425
import org.springframework.util.MimeTypeUtils;
@@ -49,12 +50,26 @@ public enum ApiVersion implements Producible<ApiVersion> {
4950
*/
5051
public static final ApiVersion LATEST = ApiVersion.V3;
5152

53+
private final MimeType mimeType;
54+
55+
ApiVersion(String mimeType) {
56+
this.mimeType = MimeTypeUtils.parseMimeType(mimeType);
57+
}
58+
59+
@Override
60+
public MimeType getProducedMimeType() {
61+
return this.mimeType;
62+
}
63+
5264
/**
5365
* Return the {@link ApiVersion} to use based on the HTTP request headers. The version
5466
* will be deduced based on the {@code Accept} header.
5567
* @param headers the HTTP headers
5668
* @return the API version to use
69+
* @deprecated since 2.5.0 in favor of direct injection with resolution via the
70+
* {@link ProducibleOperationArgumentResolver}.
5771
*/
72+
@Deprecated
5873
public static ApiVersion fromHttpHeaders(Map<String, List<String>> headers) {
5974
ApiVersion version = null;
6075
List<String> accepts = headers.get("Accept");
@@ -88,15 +103,4 @@ private static ApiVersion mostRecent(ApiVersion existing, ApiVersion candidate)
88103
return (candidateOrdinal > existingOrdinal) ? candidate : existing;
89104
}
90105

91-
private final MimeType mimeType;
92-
93-
ApiVersion(String mimeType) {
94-
this.mimeType = MimeTypeUtils.parseMimeType(mimeType);
95-
}
96-
97-
@Override
98-
public MimeType getMimeType() {
99-
return this.mimeType;
100-
}
101-
102106
}

0 commit comments

Comments
 (0)