Skip to content

Commit 1ec49ce

Browse files
wilkinsonaphilwebb
authored andcommitted
Allow operations to produce different output
Update the actuator @Enpoint` infrastructure code so that operations may inject enums that indicate the type of output to produce. A new `Producible` interface can be implemented by any enum that indicates the mime-type that an enum value produces. The new `OperationArgumentResolver` provides a general strategy for resolving operation arguments with `ProducibleOperationArgumentResolver` providing support for `Producible` enums. Existing injection support has been refactored to use the new resolver. See gh-25738
1 parent 663fd8c commit 1ec49ce

File tree

15 files changed

+510
-55
lines changed

15 files changed

+510
-55
lines changed

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

Lines changed: 122 additions & 8 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.
@@ -16,7 +16,12 @@
1616

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

19+
import java.security.Principal;
20+
import java.util.ArrayList;
21+
import java.util.Arrays;
22+
import java.util.List;
1923
import java.util.Map;
24+
import java.util.function.Supplier;
2025

2126
import org.springframework.boot.actuate.endpoint.http.ApiVersion;
2227
import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker;
@@ -31,11 +36,9 @@
3136
*/
3237
public class InvocationContext {
3338

34-
private final SecurityContext securityContext;
35-
3639
private final Map<String, Object> arguments;
3740

38-
private final ApiVersion apiVersion;
41+
private final List<OperationArgumentResolver> argumentResolvers;
3942

4043
/**
4144
* Creates a new context for an operation being invoked by the given
@@ -54,13 +57,34 @@ public InvocationContext(SecurityContext securityContext, Map<String, Object> ar
5457
* @param securityContext the current security context. Never {@code null}
5558
* @param arguments the arguments available to the operation. Never {@code null}
5659
* @since 2.2.0
60+
* @deprecated since 2.5.0 in favor of
61+
* {@link #InvocationContext(SecurityContext, Map, List)}
5762
*/
63+
@Deprecated
5864
public InvocationContext(ApiVersion apiVersion, SecurityContext securityContext, Map<String, Object> arguments) {
65+
this(securityContext, arguments, Arrays.asList(new FixedValueArgumentResolver<>(ApiVersion.class, apiVersion)));
66+
}
67+
68+
/**
69+
* Creates a new context for an operation being invoked by the given
70+
* {@code securityContext} with the given available {@code arguments}.
71+
* @param securityContext the current security context. Never {@code null}
72+
* @param arguments the arguments available to the operation. Never {@code null}
73+
* @param argumentResolvers resolvers for additional arguments should be available to
74+
* the operation.
75+
*/
76+
public InvocationContext(SecurityContext securityContext, Map<String, Object> arguments,
77+
List<OperationArgumentResolver> argumentResolvers) {
5978
Assert.notNull(securityContext, "SecurityContext must not be null");
6079
Assert.notNull(arguments, "Arguments must not be null");
61-
this.apiVersion = (apiVersion != null) ? apiVersion : ApiVersion.LATEST;
62-
this.securityContext = securityContext;
6380
this.arguments = arguments;
81+
this.argumentResolvers = new ArrayList<>();
82+
if (argumentResolvers != null) {
83+
this.argumentResolvers.addAll(argumentResolvers);
84+
}
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));
6488
}
6589

6690
/**
@@ -69,15 +93,17 @@ public InvocationContext(ApiVersion apiVersion, SecurityContext securityContext,
6993
* @since 2.2.0
7094
*/
7195
public ApiVersion getApiVersion() {
72-
return this.apiVersion;
96+
return resolveArgument(ApiVersion.class);
7397
}
7498

7599
/**
76100
* Return the security context to use for the invocation.
77101
* @return the security context
102+
* @deprecated since 2.5.0 in favor of {@link #resolveArgument(Class)}
78103
*/
104+
@Deprecated
79105
public SecurityContext getSecurityContext() {
80-
return this.securityContext;
106+
return resolveArgument(SecurityContext.class);
81107
}
82108

83109
/**
@@ -88,4 +114,92 @@ public Map<String, Object> getArguments() {
88114
return this.arguments;
89115
}
90116

117+
/**
118+
* Resolves an argument with the given {@code argumentType}.
119+
* @param <T> type of the argument
120+
* @param argumentType type of the argument
121+
* @return resolved argument of the required type or {@code null}
122+
* @since 2.5.0
123+
* @see #canResolve(Class)
124+
*/
125+
public <T> T resolveArgument(Class<T> argumentType) {
126+
for (OperationArgumentResolver argumentResolver : this.argumentResolvers) {
127+
if (argumentResolver.canResolve(argumentType)) {
128+
T result = argumentResolver.resolve(argumentType);
129+
if (result != null) {
130+
return result;
131+
}
132+
}
133+
}
134+
return null;
135+
}
136+
137+
/**
138+
* Returns whether or not the context is capable of resolving an argument of the given
139+
* {@code type}. Note that, even when {@code true} is returned,
140+
* {@link #resolveArgument argument resolution} will return {@code null} if no
141+
* argument of the required type is available.
142+
* @param type argument type
143+
* @return {@code true} if resolution of arguments of the given type is possible,
144+
* otherwise {@code false}.
145+
* @since 2.5.0
146+
* @see #resolveArgument(Class)
147+
*/
148+
public boolean canResolve(Class<?> type) {
149+
for (OperationArgumentResolver argumentResolver : this.argumentResolvers) {
150+
if (argumentResolver.canResolve(type)) {
151+
return true;
152+
}
153+
}
154+
return false;
155+
}
156+
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+
91205
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright 2012-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.actuate.endpoint;
18+
19+
/**
20+
* Resolver for an argument of an {@link Operation}.
21+
*
22+
* @author Andy Wilkinson
23+
* @since 2.5.0
24+
*/
25+
public interface OperationArgumentResolver {
26+
27+
/**
28+
* Resolves an argument of the given {@code type}.
29+
* @param <T> required type of the argument
30+
* @param type argument type
31+
* @return an argument of the required type, or {@code null}
32+
*/
33+
<T> T resolve(Class<T> type);
34+
35+
/**
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}
40+
*/
41+
boolean canResolve(Class<?> type);
42+
43+
}

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

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2020 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.
@@ -20,6 +20,7 @@
2020
import java.util.Map;
2121

2222
import org.springframework.util.CollectionUtils;
23+
import org.springframework.util.MimeType;
2324
import org.springframework.util.MimeTypeUtils;
2425

2526
/**
@@ -29,17 +30,17 @@
2930
* @author Phillip Webb
3031
* @since 2.2.0
3132
*/
32-
public enum ApiVersion {
33+
public enum ApiVersion implements Producible<ApiVersion> {
3334

3435
/**
3536
* Version 2 (supported by Spring Boot 2.0+).
3637
*/
37-
V2,
38+
V2(ActuatorMediaType.V2_JSON),
3839

3940
/**
4041
* Version 3 (supported by Spring Boot 2.2+).
4142
*/
42-
V3;
43+
V3(ActuatorMediaType.V3_JSON);
4344

4445
private static final String MEDIA_TYPE_PREFIX = "application/vnd.spring-boot.actuator.";
4546

@@ -87,4 +88,15 @@ private static ApiVersion mostRecent(ApiVersion existing, ApiVersion candidate)
8788
return (candidateOrdinal > existingOrdinal) ? candidate : existing;
8889
}
8990

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+
90102
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright 2012-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.actuate.endpoint.http;
18+
19+
import org.springframework.util.MimeType;
20+
21+
/**
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.
26+
*
27+
* @param <E> enum type that implements this interface
28+
* @author Andy Wilkinson
29+
* @since 2.5.0
30+
*/
31+
public interface Producible<E extends Enum<E> & Producible<E>> {
32+
33+
/**
34+
* Mime type that can be produced.
35+
* @return the producible mime type
36+
*/
37+
MimeType getMimeType();
38+
39+
}

0 commit comments

Comments
 (0)