Allow Producible enums to indicate a default value
Add an `isDefault()` method to `Producible` which can be used to indicate which of the enum values should be used when the accept header is `*/*` or `null`. Prior to this commit, the last enum value was always used as the default. See gh-28130
This commit is contained in:
parent
a1c88faf46
commit
d8141e6a8d
@ -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 */* 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();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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");
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user