Spring Boot SPA URL Rewrite - java

I'm trying to build an SPA backend (static content server, api) with some additional features/controls that require a flexible URL rewrite/routing/handling. These requirements are proving difficult to achieve together, despite trying the approach in some similar answers I've read through on here.
What I need to do:
Serve static assets (js,images,css,html,etc) from URL path: /assets/
Store these static assets in a filesystem directory and map to the above path
For any static asset request not found return a 404
Expose REST APIs from a set of named URL paths: /api/ and /anotherapi/ etc...
For all other requests outside of these URL paths, serve /index.htm to bootstrap the SPA
So far, I have the following...
For the REST APIs:
#RestController
#RequestMapping(value="/api/**")
public class StateAPIController {
#RequestMapping(value = {"/api/method1"}, method = RequestMethod.POST)
#ResponseBody
public String method1() {
return "method1...";
}
#RequestMapping(value = {"/api/method2"}, method = RequestMethod.POST)
#ResponseBody
public String method2() {
return "method2...";
}
}
(This works fine)
For rendering static files from a specific filesystem location and mapping "/" to "/index.htm":
#Configuration
#EnableWebMvc
public class AssetServerConfig implements WebMvcConfigurer {
#Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry
.addResourceHandler("/")
.addResourceLocations("file:/some/path/index.htm");
registry
.addResourceHandler("/assets/**")
.addResourceLocations("file:/some/path/assets/");
}
#Bean
public ViewResolver viewResolver() {
UrlBasedViewResolver viewResolver = new UrlBasedViewResolver();
viewResolver.setViewClass(InternalResourceView.class);
return viewResolver;
}
}
(This works, but not sure if the best way to solve this)
To redirect/forward any other requests (outside of those reserved paths) to "/" (and therefore "/index.htm"):
#ControllerAdvice
#RequestMapping(value="/**")
public class AssetServerController {
#RequestMapping(value = {"/**/{path:[^\\.]*}", "/{path:^(?!/assets/).*}", "/{path:^(?!/api/).*}"}, method = RequestMethod.GET)
public String index() {
return "forward:/";
}
}
(This only partially works... and the main issue I need help with)
So, here, I need to exclude a list of paths (/assets/ & /api/), but this is proving difficult to get right with the regex/AntPathMatcher filter in the RequestMapping, and has both false matches (showing index.htm when it shouldn't) and misses (showing 404s when it should show index.htm).
Due to the above, I also cannot correctly serve 404s when a resource is missing under one of the reserved paths (e.g. assets).
a) what is the best way to approach this? Have I got this completely wrong? Is there a better way?
b) how do I make the regex work, as it doesn't seem to follow normal regex rules, and examples I've seen so far don't achieve my goal...

Answered here: Spring RequestMapping Regex to exclude string
Based on answer here: Spring #RequestMapping "Not Contains" Regex
Pattern that worked for excluding /assets/:
value = {"/{path:(?!.*assets).+}/**"}

Related

Is there a way to server static files in Java Webflux?

Hi everyone i am searching now the full day and i do not found a solution.
I could server static file in a mvc spring application without problems but with webflux i do not found a way how i can serve them.
I put in ressource a folder with the name static and in there its a simple html file.
My configuration looks like:
#Configuration
#EnableWebFlux
#CrossOrigin(origins = "*", allowedHeaders = "*")
public class WebConfig implements WebFluxConfigurer {
#Bean
public RouterFunction<ServerResponse> route() {
return RouterFunctions.resources("/", new ClassPathResource("static/"));
}
When i start the application and go to localhost i just received a 404 response.
I also try it with adding:
spring.webflux.static-path-pattern = /**
spring.web.resources.static-locations = classpath:/static/
to the application.properties but i still received the 404 not found.
Even when i added Thymeleaf to my dependencies i still get 404.
Hopefully someone knows what to do.
What i think you are missing is basically to tell on what type (GET) of request you want to serve data.
Here is an old pice of code i found that i have used when i served a react application from a public folder in the resource folder.
When doing a GET against /* we fetch the index.html. If the index is containing javascript that does returning requests they are caught in the second router, serving whatever is in the public folder.
#Configuration
public class HtmlRoutes {
#Bean
public RouterFunction<ServerResponse> htmlRouter(#Value("classpath:/public/index.html") Resource html) {
return route(GET("/*"), request -> ok()
.contentType(MediaType.TEXT_HTML)
.bodyValue(html)
);
}
#Bean
public RouterFunction<ServerResponse> imgRouter() {
return RouterFunctions
.resources("/**", new ClassPathResource("public/"));
}
}

Spring Boot Override Wildcard RequestMapping

I am building a URL Shorter app (like Bitly). It is an SPA using Spring Boot & ReactJS. All web content is served off of index.html. All other routes are presumed to be shortLink redirect requests which should trigger a clickShortUrl() function to fetch the corresponding originalLink and redirect the user to that web address.
Therefore, I want the following routes to redirect to index.html:
#GetMapping(value = {"/", "/home", "/dashboard"})
public String redirect() {
return "forward:/index.html";
}
and all other/unknown routes to trigger a wildcard function:
#RequestMapping(value = "/{shortUrl}", method = RequestMethod.GET)
public Object clickShortUrl(#PathVariable("shortUrl") String shortUrl, #RequestBody ClickDTO request) {
// internalLogicHere
};
Individually, the mappings and functions are working. But combined, the /{shortUrl} wildcard route always takes precedence. I've googled around looking for ways to override this behavior. It seems to be possible a few ways, but all of my attempts have failed.
I read several posts like this suggesting to extend WebMvcConfigurerAdapter and override addViewControllers(ViewControllerRegistry registry) to define view controllers for specific routes. I don't really understand this. Is this the right path? If so, can someone help me understand what ViewControllerRegistry is all about and set me on the right path?
Thank you!
Answered my own question. Ended up using RegEx in the wildcard route to exclude the static paths used on the front end.
/** Redirect all '/' and '/dashboard/ requests to index.html. */
#GetMapping(value = {"path:/", "path:/dashboard"})
public String redirect() {
return "forward:/index.html";
}
and the fallback route:
/**
* Treat all routes as /{shortUrl} clicks except: '/', '/index.html, '/dashboard''
*/
#RequestMapping(value = "{_:^(?!index\\.html|dashboard).*$}")
public Object clickShortUrl(#PathVariable("shortUrl") String shortUrl, #RequestBody ClickDTO request) {
// internalLogicHere;
}

Is a specific #RequestMapping always called in favour of a variable #RequestMapping in Spring

Let's assume following controller:
#RestController
public class MyController {
#RequestMapping(method = GET, path = "/info")
public InfoModel getInfo(){
...
}
#RequestMapping(method = GET, path = "/{resourceId}")
public ResourceModel getResource(#PathVariable("resourceId") String resourceId){
...
}
}
The question is: Which method will be invoked when curling GET /info.
In all my tests getInfo was called which seems to be clear.
But I'm not 100% sure if this is just a lucky race condition or if it is specified that a static path has a higher precedence than a variable path.
Even after some research I couldn't find the specification for this case, only some pretty old (and probably outdated) blog posts.
I'm using SpringBoot 2.0.2.
It’s not a lucky race condition. The pattern with no path variables will always take precedence.
Please refer to the Spring MVC documentation that explains everything in detail Request Mapping under Pattern Comparison.
If you have the path specified at class level (#RequestMapping("/home") as per below
#RestController
#RequestMapping("/home")
public class MyController {
#RequestMapping(method = GET, path = "/info")
public InfoModel getInfo(){
...
}
}
then you would have to curl GET /home/info. All urls path are defined/decided by you.

Preserving model state with Post/Redirect/Get pattern and URL rewriting

First of all, I know that there are similar questions regarding "Preserving model state with Post/Redirect/Get pattern", but none of these have my very specific problem:
background:
My Code is working in an enterprise CMS software which does a lot of things. One of them is URL rewriting: Whenever I generate Links to my controller, dependending on the environment, the links are shortened - That's a SEO thing and can't be discussed.
I.e. if my Controller URL is /webapp/servlet/myController/doSomething, the generated URL will be /myController/doSomething. There's some LinkProcessing functionality that we have to use.
An apache rewrite rule will then expand this short url to /webapp/servlet/myController/doSomething when the apache uses mod_rewrite to call the corresponsing code on the tomcat:
RewriteCond %{REQUEST_URI} ^/myController/(.*)
RewriteRule ^/myController/(.*) /webapp/servlet/myController/$1 [PT,L]
Problem:
I'm trying to implement the Post/Redirect/Get pattern using Spring 3.1.2. I'm generating a form and POST it to the Controller, which validated and makes a redirect to either the success or error page using GET (Post/Redirect/Get pattern).
(highly simplified) Code:
#RequestMapping()
public class MyController {
#RequestMapping(value = "/doDispatch", method = RequestMethod.POST)
public RedirectView handleDispatch(RedirectAttributes redirectAttributes,
#Validated #ModelAttribute FormBean formBean,
BindingResult binding) {
if (binding.hasErrors()) {
redirectAttributes.addFlashAttribute(formBean);
redirectAttributes.addFlashAttribute(BindingResult.MODEL_KEY_PREFIX+"formBean", binding);
return new RedirectView(generateLink("/error"));
} else {
redirectAttributes.addFlashAttribute(formBean);
redirectAttributes.addFlashAttribute(BindingResult.MODEL_KEY_PREFIX+"formBean", binding);
return new RedirectView(generateLink("/success"));
}
}
#RequestMapping(value = "/success")
public ModelAndView handleSuccess(#ModelAttribute FormBean formBean) {
// do stuff (save things in the DB)
// ...
final ModelAndView modelAndView = createModelAndView(formBean);
modelAndView.addObject("success", true);
return modelAndView;
}
#RequestMapping(value = "/error")
public ModelAndView handleError(#Validated #ModelAttribute FormBean formBean,
BindingResult binding) {
final ModelAndView modelAndView = createModelAndView(formBean);
modelAndView.addObject("binding", binding);
modelAndView.addObject("formBean", formBean);
return modelAndView;
}
}
The problem ist that this generateLink() method will either generate links starting with /webapp/servlet or not - depending on the environment/success. And that's how this whole Enterprice CMS thing works. (that's the part which cannot be discussed)
Spring Flash-Attributes on the other hand work hand in hand with the URLs that are returned and store the URL as part of the FlashMap:
Quote from http://docs.spring.io/spring/docs/current/spring-framework-reference/html/mvc.html#mvc-flash-attributes :
To reduce the possibility of such issues, RedirectView automatically "stamps" FlashMap instances with the path and query parameters of the target redirect URL. In turn the default FlashMapManager matches that information to incoming requests when looking up the "input" FlashMap.
Since the next request (let's say I've had an error and returned "/myController/error") will be expanded to /webapp/servlet/myController/error, the FlashMap will not apply to this request, since the URLs do not match.
The code that is responsible is this here (AbstractFlashMapManager.java:157 ff):
protected boolean isFlashMapForRequest(FlashMap flashMap, HttpServletRequest request) {
if (flashMap.getTargetRequestPath() != null) {
String requestUri = this.urlPathHelper.getOriginatingRequestUri(request);
if (!requestUri.equals(flashMap.getTargetRequestPath())
&& !requestUri.equals(flashMap.getTargetRequestPath() + "/")) {
return false;
}
}
// ...
}
Question:
Do you know a way how I can still generate short URLs on the one hand, but pass the FlashAttributes to the following GET request?
Best regards and thanks for your help in advance,
Alexander
The best solution I've found so far is using an Interceptor that updates the targetRequestPath by prefixing /<webappPath>/<servletPath> to those FlashMaps from RequestContextUtils.getOutputFlashMap(request) that are missing this information. BTW: this can only be done in the afterCompletion method, because otherwise the targetRequestPath won't be set at all, when using a RedirectView.
Any other, better solutions?

How to find the base URL of the current webapp in Spring?

I'd like to find the absolute URL of the webapp in Spring, from the Controller. I'm aware of JSTL c:url, but I need this info from inside the Controller.
#Controller
public class AuthorizeController {
#Autowired
private Authorizer auth;
#RequestMapping("/auth")
public String sendToAuthorization() {
String baseUrl = "http://localhost:8080/tasks/";
return "redirect:" + auth.getAuthorizationUrl(baseUrl);
}
}
As you can see the baseUrl is hardcoded, and I could provide it to the Authorizer class via Spring configuration, but I am sure that it's possible to get this information from Spring within the Controller. I tried to google "spring mvc url" and could not find a way to solve this problem.
I think that getting absolute url is only possible while processing the request as your server may have many IP addresses and domain names.
#RequestMapping("/auth")
public String sendToAuthorization(HttpServletRequest request) {
String baseUrl = String.format("%s://%s:%d/tasks/",request.getScheme(), request.getServerName(), request.getServerPort());
return "redirect:" + auth.getAuthorizationUrl(baseUrl);
}
As for the servlet, it may also have several mappings in web.xml.
similar question
P.S. Anyway, url parsing in runtime does not look like a good idea to me.
Very late to this answer, but a variant to Boris's answer, if you don't want to push servlet objects into method signatures, is to use RequestContextHolder from a utility class/method. This would also give the ability to abstract fallback logic (e.g., pulling from a property file). Cheesy example:
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if(null != requestAttributes && requestAttributes instanceof ServletRequestAttributes) {
HttpServletRequest request = ((ServletRequestAttributes)requestAttributes).getRequest();
// build URL from request
}
else {
// fallback logic if request won't work...
}
This presumes you have org.springframework.web.context.request.RequestContextListener registered as a listener in web.xml

Categories

Resources