spring-hateoas
spring-hateoas copied to clipboard
`Enum.toString()` isn't used in links generated by `WebMvcLinkBuilder` since 1.4
I'm using WebMvcLinkBuilder
to generate links to controller methods with specific predefined parameters represented by a list of enums. These enums have custom toString()
implementations to provide required labels for given API calls. However, it's not used since 1.4 resulting in incorrect links.
Simplified example:
enum class Words(private val text: String) {
HELLO("Hello,"),
WORLD("world!");
override fun toString(): String {
return text
}
}
val invocation = WebMvcLinkBuilder.methodOn(TestController::class.java)
.get(listOf(Words.HELLO, Words.WORLD))
val actualLink = WebMvcLinkBuilder.linkTo(invocation).withSelfRel().href
Currently, since 1.4, they are represented using their name()
:
/?words=HELLO&words=WORLD
However, before 1.4, enums were converted using their toString()
implementation, generated link was:
/?words=Hello,&words=world!
Sample project with unit test: demo-hateoas.zip
While I can see that this is a change that alters the previous output, it's actually needed to make the default handling symmetric. Using the enum's toString()
method would prevent the resulting value from being resolvable back into the enum as Spring's StringToEnumConverterFactory
uses Enum.valueOf(enumType, source.trim())
, i.e. uses Words.valueOf(…)
that would not find HELLO
for a Hello
received as a request parameter.
If you say those values worked for you before 1.4, how did you tweak the parameter resolution?
There is demo app and you can check it changing hateoas version. I think there should be "knob" to choose strategy for the enums. If you ask about handling those strings in controller it's done by adding custom converter afair.
I did that. What I am trying to say is that a generated URL of /?words=Hello,&words=world!
will not properly resolve into the MVC controller you have declared in the example. I.e. you cannot issue an HTTP request using that URI as the enums will not be parsed correctly unless you also register some converter to adapt the parsing the same way you tweak it for the printing. I.e. using the enum's ….toString()
method by default breaks symmetry and renders projects broken that do solely declared the method for other purposes. Thus, if you would like to switch back to ….toString()
, register two converters: one for the printing and one for parsing. Or a Formatter
that does both things.
The original code missed this part of our custom Converter which handled converting String
to Enum
in following way. Unfortunately with WebMvcConfigurer
it doesn't register converters for spring-hateoas. How can we register custom converters for spring-hateoas?
enum class Words(private val text: String) {
HELLO("Hello,"),
WORLD("world!");
companion object {
val CACHE: Map<String, Words> = values().associateBy { it.text }
}
override fun toString(): String {
return text
}
}
class StringToWordsConverter: Converter<String, Words> {
override fun convert(source: String): Words? = Words.CACHE[source]
}
class WordsToStringConverter: Converter<Words, String> {
override fun convert(source: Words): String = source.toString()
}
open class ExampleResponse(val enum: List<Words>)
@Configuration
class WebRequestEnumConverter: WebMvcConfigurer {
override fun addFormatters(registry: FormatterRegistry) {
registry.addConverter(StringToWordsConverter())
registry.addConverter(WordsToStringConverter())
}
}
In the end we've figured out how to setup unit test with custom converters. That requires "hadouken" setup code 🤣
like this:
RequestContextHolder.setRequestAttributes(
ServletRequestAttributes(
MockHttpServletRequest(
MockServletContext().apply {
this.setAttribute(
WebApplicationContext::class.java.name + ".ROOT",
StaticWebApplicationContext().apply {
this.registerBean(
"mvcConversionService",
ConversionService::class.java,
{ ->
val format = WebMvcProperties.Format()
val registry = WebConversionService(
DateTimeFormatters()
.dateFormat(format.date).timeFormat(format.time).dateTimeFormat(format.dateTime)
)
registry.addConverter(StringToWordsConverter())
registry.addConverter(WordsToStringConverter())
registry
}
)
}
)
}
)
)
)
Full working example of test code below: demo.zip
That's caused by org/springframework/hateoas/server/mvc/WebMvcLinkBuilderFactory.java:197
as there is no Servlet context in unit tests so it uses FALLBACK_CONVERSION_SERVICE
. That broke our unit tests before. Is there an easier way to setup such a test? If not, maybe it should be documented somewhere?