Merge branch 'gh-28130' into 2.5.x

Fixes gh-28130
pull/28204/head
Andy Wilkinson 3 years ago
commit 51c3758801

@ -47,4 +47,16 @@ public interface Producible<E extends Enum<E> & Producible<E>> {
*/ */
MimeType getProducedMimeType(); MimeType getProducedMimeType();
/**
* Return if this enum value should be used as the default value when an accept header
* of &#42;&#47;&#42; is provided, or if the accept header is missing. Only one value
* can be marked as default. If no value is marked, then the value with the highest
* {@link Enum#ordinal() ordinal} is used as the default.
* @return if this value
* @since 2.5.6
*/
default boolean isDefault() {
return false;
}
} }

@ -21,6 +21,7 @@ import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.function.Supplier; import java.util.function.Supplier;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import org.springframework.util.MimeType; import org.springframework.util.MimeType;
import org.springframework.util.MimeTypeUtils; import org.springframework.util.MimeTypeUtils;
@ -29,6 +30,7 @@ import org.springframework.util.MimeTypeUtils;
* An {@link OperationArgumentResolver} for {@link Producible producible enums}. * An {@link OperationArgumentResolver} for {@link Producible producible enums}.
* *
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Phillip Webb
* @since 2.5.0 * @since 2.5.0
*/ */
public class ProducibleOperationArgumentResolver implements OperationArgumentResolver { public class ProducibleOperationArgumentResolver implements OperationArgumentResolver {
@ -56,30 +58,35 @@ public class ProducibleOperationArgumentResolver implements OperationArgumentRes
private Enum<? extends Producible<?>> resolveProducible(Class<Enum<? extends Producible<?>>> type) { private Enum<? extends Producible<?>> resolveProducible(Class<Enum<? extends Producible<?>>> type) {
List<String> accepts = this.accepts.get(); List<String> accepts = this.accepts.get();
List<Enum<? extends Producible<?>>> values = Arrays.asList(type.getEnumConstants()); List<Enum<? extends Producible<?>>> values = getValues(type);
Collections.reverse(values);
if (CollectionUtils.isEmpty(accepts)) { if (CollectionUtils.isEmpty(accepts)) {
return values.get(0); return getDefaultValue(values);
} }
Enum<? extends Producible<?>> result = null; Enum<? extends Producible<?>> result = null;
for (String accept : accepts) { for (String accept : accepts) {
for (String mimeType : MimeTypeUtils.tokenize(accept)) { for (String mimeType : MimeTypeUtils.tokenize(accept)) {
result = mostRecent(result, forType(values, MimeTypeUtils.parseMimeType(mimeType))); result = mostRecent(result, forMimeType(values, mimeType));
} }
} }
return result; return result;
} }
private static Enum<? extends Producible<?>> mostRecent(Enum<? extends Producible<?>> existing, private Enum<? extends Producible<?>> mostRecent(Enum<? extends Producible<?>> existing,
Enum<? extends Producible<?>> candidate) { Enum<? extends Producible<?>> candidate) {
int existingOrdinal = (existing != null) ? existing.ordinal() : -1; int existingOrdinal = (existing != null) ? existing.ordinal() : -1;
int candidateOrdinal = (candidate != null) ? candidate.ordinal() : -1; int candidateOrdinal = (candidate != null) ? candidate.ordinal() : -1;
return (candidateOrdinal > existingOrdinal) ? candidate : existing; return (candidateOrdinal > existingOrdinal) ? candidate : existing;
} }
private static Enum<? extends Producible<?>> forType(List<Enum<? extends Producible<?>>> candidates, private Enum<? extends Producible<?>> forMimeType(List<Enum<? extends Producible<?>>> values, String mimeType) {
MimeType mimeType) { if ("*/*".equals(mimeType)) {
for (Enum<? extends Producible<?>> candidate : candidates) { return getDefaultValue(values);
}
return forMimeType(values, MimeTypeUtils.parseMimeType(mimeType));
}
private Enum<? extends Producible<?>> forMimeType(List<Enum<? extends Producible<?>>> values, MimeType mimeType) {
for (Enum<? extends Producible<?>> candidate : values) {
if (mimeType.isCompatibleWith(((Producible<?>) candidate).getProducedMimeType())) { if (mimeType.isCompatibleWith(((Producible<?>) candidate).getProducedMimeType())) {
return candidate; return candidate;
} }
@ -87,4 +94,20 @@ public class ProducibleOperationArgumentResolver implements OperationArgumentRes
return null; return null;
} }
private List<Enum<? extends Producible<?>>> getValues(Class<Enum<? extends Producible<?>>> type) {
List<Enum<? extends Producible<?>>> values = Arrays.asList(type.getEnumConstants());
Collections.reverse(values);
Assert.state(values.stream().filter(this::isDefault).count() <= 1,
"Multiple default values declared in " + type.getName());
return values;
}
private Enum<? extends Producible<?>> getDefaultValue(List<Enum<? extends Producible<?>>> values) {
return values.stream().filter(this::isDefault).findFirst().orElseGet(() -> values.get(0));
}
private boolean isDefault(Enum<? extends Producible<?>> value) {
return ((Producible<?>) value).isDefault();
}
} }

@ -36,25 +36,30 @@ import org.springframework.util.MimeTypeUtils;
public enum TextOutputFormat implements Producible<TextOutputFormat> { public enum TextOutputFormat implements Producible<TextOutputFormat> {
/** /**
* OpenMetrics text version 1.0.0. * Prometheus text version 0.0.4.
*/ */
CONTENT_TYPE_OPENMETRICS_100(TextFormat.CONTENT_TYPE_OPENMETRICS_100) { CONTENT_TYPE_004(TextFormat.CONTENT_TYPE_004) {
@Override @Override
void write(Writer writer, Enumeration<MetricFamilySamples> samples) throws IOException { void write(Writer writer, Enumeration<MetricFamilySamples> samples) throws IOException {
TextFormat.writeOpenMetrics100(writer, samples); TextFormat.write004(writer, samples);
}
@Override
public boolean isDefault() {
return true;
} }
}, },
/** /**
* Prometheus text version 0.0.4. * OpenMetrics text version 1.0.0.
*/ */
CONTENT_TYPE_004(TextFormat.CONTENT_TYPE_004) { CONTENT_TYPE_OPENMETRICS_100(TextFormat.CONTENT_TYPE_OPENMETRICS_100) {
@Override @Override
void write(Writer writer, Enumeration<MetricFamilySamples> samples) throws IOException { void write(Writer writer, Enumeration<MetricFamilySamples> samples) throws IOException {
TextFormat.write004(writer, samples); TextFormat.writeOpenMetrics100(writer, samples);
} }
}; };

@ -22,12 +22,16 @@ import java.util.function.Supplier;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.util.MimeType;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/** /**
* Test for {@link ProducibleOperationArgumentResolver}. * Test for {@link ProducibleOperationArgumentResolver}.
* *
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Phillip Webb
*/ */
class ProducibleOperationArgumentResolverTests { class ProducibleOperationArgumentResolverTests {
@ -40,11 +44,21 @@ class ProducibleOperationArgumentResolverTests {
assertThat(resolve(acceptHeader())).isEqualTo(ApiVersion.V3); assertThat(resolve(acceptHeader())).isEqualTo(ApiVersion.V3);
} }
@Test
void whenAcceptHeaderIsEmptyAndWithDefaultThenDefaultIsReturned() {
assertThat(resolve(acceptHeader(), WithDefault.class)).isEqualTo(WithDefault.TWO);
}
@Test @Test
void whenEverythingIsAcceptableThenHighestOrdinalIsReturned() { void whenEverythingIsAcceptableThenHighestOrdinalIsReturned() {
assertThat(resolve(acceptHeader("*/*"))).isEqualTo(ApiVersion.V3); assertThat(resolve(acceptHeader("*/*"))).isEqualTo(ApiVersion.V3);
} }
@Test
void whenEverythingIsAcceptableWithDefaultThenDefaultIsReturned() {
assertThat(resolve(acceptHeader("*/*"), WithDefault.class)).isEqualTo(WithDefault.TWO);
}
@Test @Test
void whenNothingIsAcceptableThenNullIsReturned() { void whenNothingIsAcceptableThenNullIsReturned() {
assertThat(resolve(acceptHeader("image/png"))).isEqualTo(null); assertThat(resolve(acceptHeader("image/png"))).isEqualTo(null);
@ -68,13 +82,72 @@ class ProducibleOperationArgumentResolverTests {
assertThat(resolve(acceptHeader(V2_JSON + "," + V3_JSON))).isEqualTo(ApiVersion.V3); assertThat(resolve(acceptHeader(V2_JSON + "," + V3_JSON))).isEqualTo(ApiVersion.V3);
} }
@Test
void withMultipleValuesOneOfWhichIsAllReturnsDefault() {
assertThat(resolve(acceptHeader("one/one", "*/*"), WithDefault.class)).isEqualTo(WithDefault.TWO);
}
@Test
void whenMultipleDefaultsThrowsException() {
assertThatIllegalStateException().isThrownBy(() -> resolve(acceptHeader("one/one"), WithMultipleDefaults.class))
.withMessageContaining("Multiple default values");
}
private Supplier<List<String>> acceptHeader(String... types) { private Supplier<List<String>> acceptHeader(String... types) {
List<String> value = Arrays.asList(types); List<String> value = Arrays.asList(types);
return () -> (value.isEmpty() ? null : value); return () -> (value.isEmpty() ? null : value);
} }
private ApiVersion resolve(Supplier<List<String>> accepts) { private ApiVersion resolve(Supplier<List<String>> accepts) {
return new ProducibleOperationArgumentResolver(accepts).resolve(ApiVersion.class); return resolve(accepts, ApiVersion.class);
}
private <T> T resolve(Supplier<List<String>> accepts, Class<T> type) {
return new ProducibleOperationArgumentResolver(accepts).resolve(type);
}
enum WithDefault implements Producible<WithDefault> {
ONE("one/one"),
TWO("two/two") {
@Override
public boolean isDefault() {
return true;
}
},
THREE("three/three");
private final MimeType mimeType;
WithDefault(String mimeType) {
this.mimeType = MimeType.valueOf(mimeType);
}
@Override
public MimeType getProducedMimeType() {
return this.mimeType;
}
}
enum WithMultipleDefaults implements Producible<WithMultipleDefaults> {
ONE, TWO, THREE;
@Override
public boolean isDefault() {
return true;
}
@Override
public MimeType getProducedMimeType() {
return MimeType.valueOf("image/jpeg");
}
} }
} }

@ -55,6 +55,14 @@ class PrometheusScrapeEndpointIntegrationTests {
.contains("counter1_total").contains("counter2_total").contains("counter3_total")); .contains("counter1_total").contains("counter2_total").contains("counter3_total"));
} }
@WebEndpointTest
void scrapePrefersToProduceOpenMetrics100(WebTestClient client) {
MediaType openMetrics = MediaType.parseMediaType(TextFormat.CONTENT_TYPE_OPENMETRICS_100);
MediaType textPlain = MediaType.parseMediaType(TextFormat.CONTENT_TYPE_004);
client.get().uri("/actuator/prometheus").accept(openMetrics, textPlain).exchange().expectStatus().isOk()
.expectHeader().contentType(openMetrics);
}
@WebEndpointTest @WebEndpointTest
void scrapeWithIncludedNames(WebTestClient client) { void scrapeWithIncludedNames(WebTestClient client) {
client.get().uri("/actuator/prometheus?includedNames=counter1_total,counter2_total").exchange().expectStatus() client.get().uri("/actuator/prometheus?includedNames=counter1_total,counter2_total").exchange().expectStatus()

Loading…
Cancel
Save