New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add capabilities for prefixing WebMvcLinkBuilder? #1221
Comments
Can you paste an example controller into this ticket that you'd like to handle, so there's no confusion between a meandering SO question and a desired feature? Thanks. |
Hi, wondering if you found a workaround, I have the same issue and stumbled on this post... |
I wrote this test case and verified this is a problem. @ExtendWith(SpringExtension.class)
@WebAppConfiguration
@ContextConfiguration
class WebMvcConfigurerUnitTest {
@Autowired WebApplicationContext context;
MockMvc mockMvc;
@BeforeEach
void setUp() {
this.mockMvc = webAppContextSetup(this.context).build();
}
@Test
void customWebMvcConfigurerShouldWork() throws Exception {
this.mockMvc.perform(get("/api").accept(MediaTypes.HAL_JSON)) //
.andDo(print()) //
.andExpect(status().isOk()) //
.andExpect(jsonPath("$._links.self.href", is("http://localhost/api")));
}
@RestController
static class TestController {
@GetMapping
RepresentationModel<?> root() {
return new RepresentationModel<>(linkTo(methodOn(TestController.class).root()).withSelfRel());
}
}
@Configuration
@EnableWebMvc
@EnableHypermediaSupport(type = HypermediaType.HAL)
static class TestConfig {
@Bean
TestController controller() {
return new TestController();
}
@Bean
WebMvcConfigurer configureCustomPathPrefix() {
return new WebMvcConfigurer() {
@Override
public void configurePathMatch(PathMatchConfigurer configurer1) {
configurer1.addPathPrefix("/api", HandlerTypePredicate.forAnyHandlerType());
}
};
}
}
} |
The issue appears to be in here => spring-hateoas/src/main/java/org/springframework/hateoas/server/mvc/UriComponentsBuilderFactory.java Lines 47 to 58 in 0d25b2e
This is where Spring HATEOAS has access to the current servlet context. In the debugger I can see the Simply put, we don't access it and apply it to the newly built URI. |
I've traveled quite a ways down this rabbit hole, and I'm not sure the answer is feasible. I tried to switch from "servlet mapping" to "current request" details deep inside link building, because my simple test case above happily turned green when you do that. This required that I reimplement "building a link outside a web call". I made much progress down that path, but ran into what may be a fatal issue, one of our integration test cases. If you are in the middle of a single-item method Essentially, you either need the base URI ( Or you need the ability to look at the whole request ( So far, it appears that Spring MVC does not have a special designation for "prefixing" things. It basically uses what you provide (annotations, Configuring path prefixes and then later inspecting them to insert into the middle of a URI doesn't appear to be a primary use case built into Spring MVC. |
Simply put, we may need to provide a configuration API to set a prefix (like This is very similar to what Spring Data REST currently does. Perhaps it would be possible to pull that feature from SDR into Spring HATEOAS, and then update SDR to use it from Spring HATEOAS. |
It's bad to fix here because WebMvcLinkBuilder works on a static way, while the path prefixes are available in the context, so in a non-static context. I have a workaround for WebMvc. Create a import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.springframework.hateoas.server.mvc.WebMvcLinkBuilder;
import java.net.URI;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
@RequiredArgsConstructor
public class UriMappingResolver {
private final Supplier<Map<String, Predicate<Class<?>>>> pathPrefixes;
protected Optional<String> getBasePath(Class<?> controllerClass) {
return this.pathPrefixes
.get()
.entrySet()
.stream()
.filter(e -> e.getValue().test(controllerClass))
.findFirst()
.map(Map.Entry::getKey);
}
@SneakyThrows
private static URI derive(URI original, String basePath) {
return new URI(
original.getScheme(),
original.getUserInfo(),
original.getHost(),
original.getPort(),
basePath + original.getPath(),
original.getQuery(),
original.getFragment()
);
}
public <ControllerClass> URI resolve(Class<ControllerClass> c, Function<ControllerClass, ?> methodOn) {
final var uri = WebMvcLinkBuilder.linkTo(
methodOn.apply(WebMvcLinkBuilder.methodOn(c))
).toUri();
return this.getBasePath(c)
.map(basePath -> derive(uri, basePath))
.orElse(uri);
}
} To register it correctly to the context, use this configuration: import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@Configuration
public class UriMappingResolverConfiguration {
@Bean
UriMappingResolver uriMappingResolver(@Lazy List<RequestMappingHandlerMapping> m) {
return new UriMappingResolver(
() -> m.stream()
.map(RequestMappingHandlerMapping::getPathPrefixes)
.map(Map::entrySet)
.flatMap(Set::stream)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))
);
}
@Bean
WebMvcConfigurer configureUriMappingResolverToWebMvc(final UriMappingResolver resolver) {
return new WebMvcConfigurer() {
@Override
public void addArgumentResolvers(
@SuppressWarnings("NullableProblems")
List<HandlerMethodArgumentResolver> resolvers
) {
resolvers.add(new HandlerMethodArgumentResolver() {
@Override
public boolean supportsParameter(
@SuppressWarnings("NullableProblems")
MethodParameter parameter
) {
return parameter
.getParameterType()
.equals(UriMappingResolver.class);
}
@Override
public Object resolveArgument(
@SuppressWarnings("NullableProblems")
MethodParameter parameter,
ModelAndViewContainer mavContainer,
@SuppressWarnings("NullableProblems")
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory
) throws Exception {
return resolver;
}
});
}
};
}
} If done so, we can use it as a method parameter withing the controller's method: ResponseEntity<AuthorDto> create(
@Valid
@RequestBody
AuthorDto author,
UriMappingResolver uriMappingResolver
) {
author.setId(null); // just to be sure
this.save(author);
return ResponseEntity
.created(
uriMappingResolver.resolve(
this.getClass(),
c -> c.findById(author.getId())
)
)
.body(author);
} |
Please see https://stackoverflow.com/q/60680748/330457.
I'm trying to make my controllers prefixed with, say,
/api/v1
.And it seems
WebMvcLinkBuilder#linkTo(Class<?>)
method is not aware ofPathMatchConfigurer#addPathPrefix(String, Predicate<Class<?>>)
.Can anybody please add some capability of adding prefixes to
WebMvcLinkBuilder
?Or do I have already an intrinsic way to do this?
The text was updated successfully, but these errors were encountered: