Skip to content

Commit 8aa9d0c

Browse files
committed
Merge branch '2.5.x'
Closes gh-28198
2 parents d34a5f1 + 51c3758 commit 8aa9d0c

File tree

5 files changed

+136
-15
lines changed

5 files changed

+136
-15
lines changed

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,16 @@ public interface Producible<E extends Enum<E> & Producible<E>> {
4747
*/
4848
MimeType getProducedMimeType();
4949

50+
/**
51+
* Return if this enum value should be used as the default value when an accept header
52+
* of &#42;&#47;&#42; is provided, or if the accept header is missing. Only one value
53+
* can be marked as default. If no value is marked, then the value with the highest
54+
* {@link Enum#ordinal() ordinal} is used as the default.
55+
* @return if this value
56+
* @since 2.5.6
57+
*/
58+
default boolean isDefault() {
59+
return false;
60+
}
61+
5062
}

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

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

24+
import org.springframework.util.Assert;
2425
import org.springframework.util.CollectionUtils;
2526
import org.springframework.util.MimeType;
2627
import org.springframework.util.MimeTypeUtils;
@@ -29,6 +30,7 @@
2930
* An {@link OperationArgumentResolver} for {@link Producible producible enums}.
3031
*
3132
* @author Andy Wilkinson
33+
* @author Phillip Webb
3234
* @since 2.5.0
3335
*/
3436
public class ProducibleOperationArgumentResolver implements OperationArgumentResolver {
@@ -56,35 +58,56 @@ public <T> T resolve(Class<T> type) {
5658

5759
private Enum<? extends Producible<?>> resolveProducible(Class<Enum<? extends Producible<?>>> type) {
5860
List<String> accepts = this.accepts.get();
59-
List<Enum<? extends Producible<?>>> values = Arrays.asList(type.getEnumConstants());
60-
Collections.reverse(values);
61+
List<Enum<? extends Producible<?>>> values = getValues(type);
6162
if (CollectionUtils.isEmpty(accepts)) {
62-
return values.get(0);
63+
return getDefaultValue(values);
6364
}
6465
Enum<? extends Producible<?>> result = null;
6566
for (String accept : accepts) {
6667
for (String mimeType : MimeTypeUtils.tokenize(accept)) {
67-
result = mostRecent(result, forType(values, MimeTypeUtils.parseMimeType(mimeType)));
68+
result = mostRecent(result, forMimeType(values, mimeType));
6869
}
6970
}
7071
return result;
7172
}
7273

73-
private static Enum<? extends Producible<?>> mostRecent(Enum<? extends Producible<?>> existing,
74+
private Enum<? extends Producible<?>> mostRecent(Enum<? extends Producible<?>> existing,
7475
Enum<? extends Producible<?>> candidate) {
7576
int existingOrdinal = (existing != null) ? existing.ordinal() : -1;
7677
int candidateOrdinal = (candidate != null) ? candidate.ordinal() : -1;
7778
return (candidateOrdinal > existingOrdinal) ? candidate : existing;
7879
}
7980

80-
private static Enum<? extends Producible<?>> forType(List<Enum<? extends Producible<?>>> candidates,
81-
MimeType mimeType) {
82-
for (Enum<? extends Producible<?>> candidate : candidates) {
81+
private Enum<? extends Producible<?>> forMimeType(List<Enum<? extends Producible<?>>> values, String mimeType) {
82+
if ("*/*".equals(mimeType)) {
83+
return getDefaultValue(values);
84+
}
85+
return forMimeType(values, MimeTypeUtils.parseMimeType(mimeType));
86+
}
87+
88+
private Enum<? extends Producible<?>> forMimeType(List<Enum<? extends Producible<?>>> values, MimeType mimeType) {
89+
for (Enum<? extends Producible<?>> candidate : values) {
8390
if (mimeType.isCompatibleWith(((Producible<?>) candidate).getProducedMimeType())) {
8491
return candidate;
8592
}
8693
}
8794
return null;
8895
}
8996

97+
private List<Enum<? extends Producible<?>>> getValues(Class<Enum<? extends Producible<?>>> type) {
98+
List<Enum<? extends Producible<?>>> values = Arrays.asList(type.getEnumConstants());
99+
Collections.reverse(values);
100+
Assert.state(values.stream().filter(this::isDefault).count() <= 1,
101+
"Multiple default values declared in " + type.getName());
102+
return values;
103+
}
104+
105+
private Enum<? extends Producible<?>> getDefaultValue(List<Enum<? extends Producible<?>>> values) {
106+
return values.stream().filter(this::isDefault).findFirst().orElseGet(() -> values.get(0));
107+
}
108+
109+
private boolean isDefault(Enum<? extends Producible<?>> value) {
110+
return ((Producible<?>) value).isDefault();
111+
}
112+
90113
}

spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/prometheus/TextOutputFormat.java

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,25 +36,30 @@
3636
public enum TextOutputFormat implements Producible<TextOutputFormat> {
3737

3838
/**
39-
* OpenMetrics text version 1.0.0.
39+
* Prometheus text version 0.0.4.
4040
*/
41-
CONTENT_TYPE_OPENMETRICS_100(TextFormat.CONTENT_TYPE_OPENMETRICS_100) {
41+
CONTENT_TYPE_004(TextFormat.CONTENT_TYPE_004) {
4242

4343
@Override
4444
void write(Writer writer, Enumeration<MetricFamilySamples> samples) throws IOException {
45-
TextFormat.writeOpenMetrics100(writer, samples);
45+
TextFormat.write004(writer, samples);
46+
}
47+
48+
@Override
49+
public boolean isDefault() {
50+
return true;
4651
}
4752

4853
},
4954

5055
/**
51-
* Prometheus text version 0.0.4.
56+
* OpenMetrics text version 1.0.0.
5257
*/
53-
CONTENT_TYPE_004(TextFormat.CONTENT_TYPE_004) {
58+
CONTENT_TYPE_OPENMETRICS_100(TextFormat.CONTENT_TYPE_OPENMETRICS_100) {
5459

5560
@Override
5661
void write(Writer writer, Enumeration<MetricFamilySamples> samples) throws IOException {
57-
TextFormat.write004(writer, samples);
62+
TextFormat.writeOpenMetrics100(writer, samples);
5863
}
5964

6065
};

spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/ProducibleOperationArgumentResolverTests.java

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,16 @@
2222

2323
import org.junit.jupiter.api.Test;
2424

25+
import org.springframework.util.MimeType;
26+
2527
import static org.assertj.core.api.Assertions.assertThat;
28+
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
2629

2730
/**
2831
* Test for {@link ProducibleOperationArgumentResolver}.
2932
*
3033
* @author Andy Wilkinson
34+
* @author Phillip Webb
3135
*/
3236
class ProducibleOperationArgumentResolverTests {
3337

@@ -40,11 +44,21 @@ void whenAcceptHeaderIsEmptyThenHighestOrdinalIsReturned() {
4044
assertThat(resolve(acceptHeader())).isEqualTo(ApiVersion.V3);
4145
}
4246

47+
@Test
48+
void whenAcceptHeaderIsEmptyAndWithDefaultThenDefaultIsReturned() {
49+
assertThat(resolve(acceptHeader(), WithDefault.class)).isEqualTo(WithDefault.TWO);
50+
}
51+
4352
@Test
4453
void whenEverythingIsAcceptableThenHighestOrdinalIsReturned() {
4554
assertThat(resolve(acceptHeader("*/*"))).isEqualTo(ApiVersion.V3);
4655
}
4756

57+
@Test
58+
void whenEverythingIsAcceptableWithDefaultThenDefaultIsReturned() {
59+
assertThat(resolve(acceptHeader("*/*"), WithDefault.class)).isEqualTo(WithDefault.TWO);
60+
}
61+
4862
@Test
4963
void whenNothingIsAcceptableThenNullIsReturned() {
5064
assertThat(resolve(acceptHeader("image/png"))).isEqualTo(null);
@@ -68,13 +82,72 @@ void whenMultipleValuesAreAcceptableAsSingleHeaderThenHighestOrdinalIsReturned()
6882
assertThat(resolve(acceptHeader(V2_JSON + "," + V3_JSON))).isEqualTo(ApiVersion.V3);
6983
}
7084

85+
@Test
86+
void withMultipleValuesOneOfWhichIsAllReturnsDefault() {
87+
assertThat(resolve(acceptHeader("one/one", "*/*"), WithDefault.class)).isEqualTo(WithDefault.TWO);
88+
}
89+
90+
@Test
91+
void whenMultipleDefaultsThrowsException() {
92+
assertThatIllegalStateException().isThrownBy(() -> resolve(acceptHeader("one/one"), WithMultipleDefaults.class))
93+
.withMessageContaining("Multiple default values");
94+
}
95+
7196
private Supplier<List<String>> acceptHeader(String... types) {
7297
List<String> value = Arrays.asList(types);
7398
return () -> (value.isEmpty() ? null : value);
7499
}
75100

76101
private ApiVersion resolve(Supplier<List<String>> accepts) {
77-
return new ProducibleOperationArgumentResolver(accepts).resolve(ApiVersion.class);
102+
return resolve(accepts, ApiVersion.class);
103+
}
104+
105+
private <T> T resolve(Supplier<List<String>> accepts, Class<T> type) {
106+
return new ProducibleOperationArgumentResolver(accepts).resolve(type);
107+
}
108+
109+
enum WithDefault implements Producible<WithDefault> {
110+
111+
ONE("one/one"),
112+
113+
TWO("two/two") {
114+
115+
@Override
116+
public boolean isDefault() {
117+
return true;
118+
}
119+
120+
},
121+
122+
THREE("three/three");
123+
124+
private final MimeType mimeType;
125+
126+
WithDefault(String mimeType) {
127+
this.mimeType = MimeType.valueOf(mimeType);
128+
}
129+
130+
@Override
131+
public MimeType getProducedMimeType() {
132+
return this.mimeType;
133+
}
134+
135+
}
136+
137+
enum WithMultipleDefaults implements Producible<WithMultipleDefaults> {
138+
139+
ONE, TWO, THREE;
140+
141+
@Override
142+
public boolean isDefault() {
143+
return true;
144+
}
145+
146+
@Override
147+
public MimeType getProducedMimeType() {
148+
return MimeType.valueOf("image/jpeg");
149+
}
150+
78151
}
79152

80153
}

spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/export/prometheus/PrometheusScrapeEndpointIntegrationTests.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,14 @@ void scrapeCanProduceOpenMetrics100(WebTestClient client) {
5555
.contains("counter1_total").contains("counter2_total").contains("counter3_total"));
5656
}
5757

58+
@WebEndpointTest
59+
void scrapePrefersToProduceOpenMetrics100(WebTestClient client) {
60+
MediaType openMetrics = MediaType.parseMediaType(TextFormat.CONTENT_TYPE_OPENMETRICS_100);
61+
MediaType textPlain = MediaType.parseMediaType(TextFormat.CONTENT_TYPE_004);
62+
client.get().uri("/actuator/prometheus").accept(openMetrics, textPlain).exchange().expectStatus().isOk()
63+
.expectHeader().contentType(openMetrics);
64+
}
65+
5866
@WebEndpointTest
5967
void scrapeWithIncludedNames(WebTestClient client) {
6068
client.get().uri("/actuator/prometheus?includedNames=counter1_total,counter2_total").exchange().expectStatus()

0 commit comments

Comments
 (0)