Skip to content

Commit

Permalink
Allow Producible enums to indicate a default value
Browse files Browse the repository at this point in the history
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
  • Loading branch information
philwebb authored and wilkinsona committed Oct 5, 2021
1 parent a1c88fa commit d8141e6
Show file tree
Hide file tree
Showing 3 changed files with 117 additions and 9 deletions.
Expand Up @@ -47,4 +47,16 @@ public interface Producible<E extends Enum<E> & Producible<E>> {
*/
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;
}

}
Expand Up @@ -21,6 +21,7 @@
import java.util.List;
import java.util.function.Supplier;

import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.MimeType;
import org.springframework.util.MimeTypeUtils;
Expand All @@ -29,6 +30,7 @@
* An {@link OperationArgumentResolver} for {@link Producible producible enums}.
*
* @author Andy Wilkinson
* @author Phillip Webb
* @since 2.5.0
*/
public class ProducibleOperationArgumentResolver implements OperationArgumentResolver {
Expand Down Expand Up @@ -56,35 +58,56 @@ public <T> T resolve(Class<T> type) {

private Enum<? extends Producible<?>> resolveProducible(Class<Enum<? extends Producible<?>>> type) {
List<String> accepts = this.accepts.get();
List<Enum<? extends Producible<?>>> values = Arrays.asList(type.getEnumConstants());
Collections.reverse(values);
List<Enum<? extends Producible<?>>> values = getValues(type);
if (CollectionUtils.isEmpty(accepts)) {
return values.get(0);
return getDefaultValue(values);
}
Enum<? extends Producible<?>> result = null;
for (String accept : accepts) {
for (String mimeType : MimeTypeUtils.tokenize(accept)) {
result = mostRecent(result, forType(values, MimeTypeUtils.parseMimeType(mimeType)));
result = mostRecent(result, forMimeType(values, mimeType));
}
}
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) {
int existingOrdinal = (existing != null) ? existing.ordinal() : -1;
int candidateOrdinal = (candidate != null) ? candidate.ordinal() : -1;
return (candidateOrdinal > existingOrdinal) ? candidate : existing;
}

private static Enum<? extends Producible<?>> forType(List<Enum<? extends Producible<?>>> candidates,
MimeType mimeType) {
for (Enum<? extends Producible<?>> candidate : candidates) {
private Enum<? extends Producible<?>> forMimeType(List<Enum<? extends Producible<?>>> values, String mimeType) {
if ("*/*".equals(mimeType)) {
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())) {
return candidate;
}
}
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();
}

}
Expand Up @@ -22,12 +22,16 @@

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.assertThatIllegalStateException;

/**
* Test for {@link ProducibleOperationArgumentResolver}.
*
* @author Andy Wilkinson
* @author Phillip Webb
*/
class ProducibleOperationArgumentResolverTests {

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

@Test
void whenAcceptHeaderIsEmptyAndWithDefaultThenDefaultIsReturned() {
assertThat(resolve(acceptHeader(), WithDefault.class)).isEqualTo(WithDefault.TWO);
}

@Test
void whenEverythingIsAcceptableThenHighestOrdinalIsReturned() {
assertThat(resolve(acceptHeader("*/*"))).isEqualTo(ApiVersion.V3);
}

@Test
void whenEverythingIsAcceptableWithDefaultThenDefaultIsReturned() {
assertThat(resolve(acceptHeader("*/*"), WithDefault.class)).isEqualTo(WithDefault.TWO);
}

@Test
void whenNothingIsAcceptableThenNullIsReturned() {
assertThat(resolve(acceptHeader("image/png"))).isEqualTo(null);
Expand All @@ -68,13 +82,72 @@ void whenMultipleValuesAreAcceptableAsSingleHeaderThenHighestOrdinalIsReturned()
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) {
List<String> value = Arrays.asList(types);
return () -> (value.isEmpty() ? null : value);
}

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");
}

}

}

0 comments on commit d8141e6

Please sign in to comment.