Spring MVC VersionResourceResolver / ContentVersionStrategy not working correctly in JSP - spring

I have a Spring MVC (4.3.0) application and have registered a VersionResourceResolver with added ContentVersionStrategy with the ResourceHandlerRegistry. I have the ResourceUrlEncodingFilter enabled.
#Bean
public ResourceUrlEncodingFilter resourceUrlEncodingFilter() {
return new ResourceUrlEncodingFilter();
}
#Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
boolean devMode = this.env.acceptsProfiles("local");
//boolean useResourceCache = !devMode;
Integer cachePeriod = devMode ? 0 : (60 * 60 * 24 * 365); //If dev clear cache else 1 year
registry.addResourceHandler("/resources/**")
.addResourceLocations("/resources/")
.setCachePeriod(cachePeriod)
.resourceChain(false)
.addResolver(new VersionResourceResolver()
.addContentVersionStrategy("/**"))
.addTransformer(new AppCacheManifestTransformer());
}
When I access anything in /resources (JS, Images, CSS, etc) on a JSP page using the c:url or spring:url tags, the "versioned" URL does not show up (meaning: no hash code in the URL). Example:
<link href="<c:url value="/resources/css/views/login.css" />" rel="stylesheet">
Produces: /myapp/resources/css/views/login.css as the URL string when inspecting the page.
But, if I use a ResourceURLProvider in my Controller, I do see the hash code in the URL:
#Autowired
private ResourceUrlProvider mvcResourceUrlProvider;
#RequestMapping(value = { "/" }, method = RequestMethod.GET)
public String projectBaseRedirect() {
logger.debug("js = '" + this.mvcResourceUrlProvider.getForLookupPath("/resources/js/views/overview.js") + "'");
logger.debug("css = '" + this.mvcResourceUrlProvider.getForLookupPath("/resources/css/views/overview-page.css") + "'");
return "redirect:/admin/overview";
}
The log messages produce this:
2016-07-09 11:47:19 DEBUG AdminLoginController:35 - js = '/resources/js/views/overview-36d1ff98d627d92a72d579eca49dbd8a.js'
2016-07-09 11:47:19 DEBUG AdminLoginController:36 - css = '/resources/css/views/overview-page-d47f10e5bcf0fdd67bd8057479b523f0.css'
Why is this working in the controller but not on my JSP pages?
I am also using Spring Security (4.1.0)...

As it is missing in your example it is maybe missing in your project as well. You need a resource provider for the template engine you're using. In case of JSP, register a filter:
#Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
#Bean
public ResourceUrlEncodingFilter resourceUrlEncodingFilter() {
return new ResourceUrlEncodingFilter();
}
}
Or use a filter registration bean with that filter:
#Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
#Bean
public FilterRegistrationBean filterRegistrationBean() {
final FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(new ResourceUrlEncodingFilter());
filterRegistrationBean.addUrlPatterns("/*");
return filterRegistrationBean;
}
}

Rossen's comment on Michael's answer was exactly the missing glue I needed. After I put this code in the class that extends AbstractAnnotationConfigDispatcherServletInitializer, it worked perfectly with the other code from my original post!
#Override
public void onStartup(ServletContext servletContext) throws ServletException {
super.onStartup(servletContext);
FilterRegistration.Dynamic fr = servletContext.addFilter("resourceUrlEncodingFilter",
new ResourceUrlEncodingFilter());
fr.setInitParameter("encoding", "UTF-8");
fr.setInitParameter("forceEncoding", "true");
fr.addMappingForUrlPatterns(null, true, "/*");
}

Related

Spring boot register bean when application is started

Before a Spring boot application starts, I need to make a request for some credentials. I'm storing them in an object.
Is there a way to register this object as a bean before all other beans so that I can inject them in a configuration class?
I've tried like below but it's throwing exceptions:
SpringBootApplication(exclude = {MongoAutoConfiguration.class,
SecurityAutoConfiguration.class, DataSourceAutoConfiguration.class})
public class BeanOnInitApplication {
public static void main(String[] args) {
System.out.println("Starting the app");
MongoCredentials creds = makeARequestToExternalServiceAndGetCredentials();
GenericApplicationContext appContext = (GenericApplicationContext) SpringApplication.run(BeanOnInitApplication.class, args);
appContext.registerBean("mongoCredentials", MongoCredentials.class, () -> creds, bdc -> bdc.setLazyInit(false));
}
}
And config class:
#Configuration
public class AppConfig {
#Autowired
MongoCredentials mongoCredentials;
#Bean(name = "mongoTemplate")
public MongoTemplate mt() {
String url = "mongodb://" + mongoCredentials.getUsername() + ":" + mongoCredentials.getPassword() + "#localhost:27017/admin";
MongoDatabaseFactory mdf = new SimpleMongoClientDatabaseFactory(url);
return new MongoTemplate(mdf);
}
}
If this is not a solution what are the alternatives? The scope is to register a critical bean before anything else.
Just make it an #Bean.
#Bean
public MongoCredentials mongoCredentials() {
return makeARequestToExternalServiceAndGetCredentials();
}
Don't handle exceptions in the makeARequestToExternalServiceAndGetCredentials and just let them bubble up. If the request then fails the application will simply fail to start. If you want to retry wrap the call with a Spring Retry RetryTemplate and you have everything you want.
Another option is, which would allow for using auto configuration for Mongo as you appear to be using Spring Boot. Is to create an EnvironmentPostProcessor which loads the properties and adds them to the environment.
public class MongoCredentialsPostProcessor implements EnvironmentPostProcessor {
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
MongoCredentials credentials = makeARequestToExternalServiceAndGetCredentials();
String url = "mongodb://" + mongoCredentials.getUsername() + ":" + mongoCredentials.getPassword() + "#localhost:27017/admin";
Map<String, Object> props = Map.of("spring.data.mongodb.uri", url);
MapPropertySource mps = new MapPropertySource("mongo-credentials", props):
}
}
Finally you could also modify your current code to do the same as the EnvironmentPostProcessor.
SpringBootApplication(exclude = {
SecurityAutoConfiguration.class, DataSourceAutoConfiguration.class})
public class BeanOnInitApplication {
public static void main(String[] args) {
System.out.println("Starting the app");
MongoCredentials creds = makeARequestToExternalServiceAndGetCredentials();
String url = "mongodb://" + mongoCredentials.getUsername() + ":" + mongoCredentials.getPassword() + "#localhost:27017/admin";
SpringApplicationBuilder sab = new SpringApplicationBuilder(BeanOnInitApplication.class);
sab.properties(Map.of("spring.data.mongodb.uri", url)).run(args);
}
}
With both the latter and the EnvironmentPostProcessor you should be able to use the mongo autoconfiguration (which will provide the MongoTemplate for you.

best way to dynamically populate .js properties from spring boot runtime application.properties?

tldr
How to best "put" runtime application.properties values from a spring boot app in a javascript file
[Admittedly this is probably a stupid question for the experienced spring dev...so please share how to solve this problem "the spring way"]
Context
We have tiny app which a front-end developer put together and "threw over the wall"..It defines a properties file settings.js:
var SERVERROOT = 'http://solr:8983/solr/operations/select/';
referenced by html :
<script type='text/javascript' src="js/settings.js"></script>
I would like to define the solr path in application.yml at runtime as follows:
app:
solr:
path: 'http://solr:8983/solr/operations/select/'
Question
What is the best way to "populate" this settings.js value from application.properties (or equivalent, i.e. command line arguments, e.g.:
-Dapp.solr.path=...
)
Possible
I thought about putting 'settings.js' as a thymeleaf template (settings.js.html) and having a spring controller populate the model from application.properties.
I didn't know if a more "native spring" method existed.
Thanks!
You could use a ResourceTransformer:
#Component
public class InjectSolrPathResourceTransformer implements ResourceTransformer {
private final MySetting settings; // inject via constructor
#Override
public Resource transform(HttpServletRequest request, Resource resource, ResourceTransformerChain transformerChain) throws IOException {
if (request.getServletPath().equals("/js/settings.js")) {
byte[] bytes = FileCopyUtils.copyToByteArray(resource.getInputStream());
String content = new String(bytes, StandardCharsets.UTF_8);
content = content.replace("var SERVERROOT = SERVERROOT_VALUE",
"var SERVERROOT = '" + settings.getSolrPath() + "'");
return new TransformedResource(resource, content.getBytes(StandardCharsets.UTF_8));
} else {
return resource;
}
}
}
This assumes that you change settings.js to be:
var SERVERROOT = SERVERROOT_VALUE
To use the ResourceTransformer, register it in your WebMvcConfigurer:
#Configuration
public class WebConfig implements WebMvcConfigurer {
// inject this via constructor
private final InjectSolrPathResourceTransformer resourceTransformer;
#Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/js/**")
.addResourceLocations("classpath:/public/js/")
.addTransformer(resourceTransformer);
}
}
The answer of #Wim above is correct but with new spring boot edition use the below code to register your transformer;
#Configuration
public class WebConfig implements WebMvcConfigurer {
// inject this via constructor
private final InjectSolrPathResourceTransformer resourceTransformer;
#Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/js/**")
.addResourceLocations("classpath:/public/js/")
.resourceChain(false)
.addTransformer(resourceTransformer);
}
}

Why is my browser downloading file instead of rendering SpringBoot & Sitemesh output?

I'm trying to use SpringBoot with Freemarker and Sitemesh.
When I go to a URL at the moment the request is handled by the application, data loaded and HTML output generated, but for some reason the browser has decided it wants to download the file (which contains the correct content) rather than rendering it as a page.
This was working a while back, trouble is I'm not sure which change I've made has broken it!
Sitemesh filter:
#WebFilter
public class SitemeshFilter extends ConfigurableSiteMeshFilter {
private static final Logger LOG = Logger.getLogger(SitemeshFilter.class);
#Override
protected void applyCustomConfiguration(SiteMeshFilterBuilder builder) {
LOG.debug("SiteMeshFilter creation");
builder.addDecoratorPath("/*", "/templates/main.ftl")
.addExcludedPath("/h2console/*");
}
}
Application:
#ServletComponentScan
#SpringBootApplication
#EnableAutoConfiguration(exclude = {ErrorMvcAutoConfiguration.class})
public class ClubManagementApplication {
private static Logger LOG = Logger.getLogger(ClubManagementApplication.class);
public static void main(String[] args) {
SpringApplication.run(ClubManagementApplication.class, args);
}
}
Snippet of controller:
#Controller
public class ClubController {
#Autowired
ClubService clubService;
#RequestMapping(value = {"Club/{id}","club/{id}"})
public ModelAndView viewClub(#PathVariable("id") int clubId) {
ModelAndView mv = new ModelAndView("club");
....
return mv;
}
}
EDIT:
From the HttpServletRequest object in controller...
accept : text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,/;q=0.8
In the response headers:
Content-Type : application/octet-stream;charset=UTF-8
I guess the content type is the problem....just gotta find why it's being set like that.
In case someone else stumbles on this question, I changed my template file from an ftl to a html extension and suddenly it has woken up.
#Override
protected void applyCustomConfiguration(SiteMeshFilterBuilder builder) {
LOG.debug("SiteMeshFilter creation");
//builder.addDecoratorPath("/*", "/templates/main.ftl");
builder.addDecoratorPath("/*", "/templates/main.html");
}

Multiple servlet mappings in Spring Boot

Is there any way to set via property 'context-path' many mappings for a same Spring Boot MVC application? My goal is to avoid creating many 'Dispatcherservlet' for the uri mapping.
For example:
servlet.context-path =/, /context1, context2
You can create #Bean annotated method which returns ServletRegistrationBean , and add multiple mappings there. This is more preferable way, as Spring Boot encourage Java configuration rather than config files:
#Bean
public ServletRegistrationBean myServletRegistration()
{
String urlMapping1 = "/mySuperApp/service1/*";
String urlMapping2 = "/mySuperApp/service2/*";
ServletRegistrationBean registration = new ServletRegistrationBean(new MyBeautifulServlet(), urlMapping1, urlMapping2);
//registration.set... other properties may be here
return registration;
}
On application startup you'll be able to see in logs:
INFO | localhost | org.springframework.boot.web.servlet.ServletRegistrationBean | Mapping servlet: 'MyBeautifulServlet' to [/mySuperApp/service1/*, /mySuperApp/service2/*]
You only need a single Dispatcherservlet with a root context path set to what you want (could be / or mySuperApp).
By declaring multiple #RequestMaping, you will be able to serve different URI with the same DispatcherServlet.
Here is an example. Setting the DispatcherServlet to /mySuperApp with #RequestMapping("/service1") and #RequestMapping("/service2") would exposed the following endpoints :
/mySuperApp/service1
/mySuperApp/service2
Having multiple context for a single servlet is not part of the Servlet specification. A single servlet cannot serve from multiple context.
What you can do is map multiple values to your requesting mappings.
#RequestMapping({"/context1/service1}", {"/context2/service1}")
I don't see any other way around it.
You can use 'server.contextPath' property placeholder to set context path for the entire spring boot application. (e.g. server.contextPath=/live/path1)
Also, you can set class level context path that will be applied to all the methods e.g.:
#RestController
#RequestMapping(value = "/testResource", produces = MediaType.APPLICATION_JSON_VALUE)
public class TestResource{
#RequestMapping(method = RequestMethod.POST, value="/test", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<TestDto> save(#RequestBody TestDto testDto) {
...
With this structure, you can use /live/path1/testResource/test to execute save method.
None of the answers to this sort of question seem to mention that you'd normally solve this problem by configuring a reverse proxy in front of the application (eg nginx/apache httpd) to rewrite the request.
However if you must do it in the application then this method works (with Spring Boot 2.6.2 at least) : https://www.broadleafcommerce.com/blog/configuring-a-dynamic-context-path-in-spring-boot.
It describes creating a filter, putting it early in the filter chain and basically re-writing the URL (like a reverse proxy might) so that requests all go to the same place (ie the actual servlet.context-path).
I've found an alternative to using a filter described in https://www.broadleafcommerce.com/blog/configuring-a-dynamic-context-path-in-spring-boot that requires less code.
This uses RewriteValve (https://tomcat.apache.org/tomcat-9.0-doc/rewrite.html) to rewrite urls outside of the context path e.g. if the real context path is "context1" then it will map /context2/* to /context1/*
#Component
public class LegacyUrlWebServerFactoryCustomizer implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
private static final List<String> LEGACY_PATHS = List.of("context2", "context3");
#Override
public void customize(TomcatServletWebServerFactory factory) {
RewriteValve rewrite = new RewriteValve() {
#Override
protected void initInternal() throws LifecycleException {
super.initInternal();
try {
String config = LEGACY_PATHS.stream() //
.map(p -> String.format("RewriteRule ^/%s(/.*)$ %s$1", p, factory.getContextPath())) //
.collect(Collectors.joining("\n"));
setConfiguration(config);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
};
factory.addEngineValves(rewrite);
}
}
If you need to use HTTP redirects instead then there is a little bit more required (to avoid a NullPointerException in sendRedirect):
#Component
public class LegacyUrlWebServerFactoryCustomizer implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
private static final List<String> LEGACY_PATHS = List.of("context2", "context3");
#Override
public void customize(TomcatServletWebServerFactory factory) {
RewriteValve rewrite = new RewriteValve() {
#Override
protected void initInternal() throws LifecycleException {
super.initInternal();
try {
String config = LEGACY_PATHS.stream() //
.map(p -> String.format("RewriteRule ^/%s(/.*)$ %s$1 R=permanent", p, factory.getContextPath())) //
.collect(Collectors.joining("\n"));
setConfiguration(config);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
#Override
public void invoke(Request request, Response response) throws IOException, ServletException {
if (request.getContext() == null) {
String[] s = request.getRequestURI().split("/");
if (s.length > 1 && LEGACY_PATHS.contains(s[1])) {
request.getMappingData().context = new FailedContext();
}
}
super.invoke(request, response);
}
};
factory.addEngineValves(rewrite);
}
}
I use this approach:
import javax.servlet.ServletContext;
import javax.servlet.ServletRegistration;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
#Configuration
public class WebAppInitializer implements WebApplicationInitializer {
#Override
public void onStartup(ServletContext servletContext) {
AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext();
rootContext.register(AppConfig.class);
rootContext.setServletContext(servletContext);
ServletRegistration.Dynamic dispatcher = servletContext.addServlet("dispatcher", new DispatcherServlet(rootContext));
dispatcher.setLoadOnStartup(1);
dispatcher.addMapping("/mapping1/*");
dispatcher.addMapping("/mapping2/*");
servletContext.addListener(new ContextLoaderListener(rootContext));
}
}

Spring MVC TrailingSlash matching

I am using Spring MVC 4.1, and this is the core config:
#EnableWebMvc
#Configuration
#ComponentScan(basePackages = {""})
public class MvcConfig extends WebMvcConfigurerAdapter {
#Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.setUseSuffixPatternMatch(true);
configurer.setUseTrailingSlashMatch(false);
}
....
}
#Controller
#RequestMapping("/api/deps")
public class DepartmentCtrl {
#RequestMapping(value = "", method = RequestMethod.GET)
public Result index() {
return ...;
}
}
While when I open this the url:
/context/api/deps
I will get the result as expected, however once I will get a 404 once I visit the link:
/context/api/deps/
As shown, I have config the PathMatchConfigurer by setUseTrailingSlashMatch(false) , but it seems that it does not work.
Is there anything wrong in my configuration?
use this:
setUseTrailingSlashMatch(true)
According to javadoc, setUseTrailingSlashMatch determines:
Whether to match to URLs irrespective of the presence of a trailing
slash. If enabled a method mapped to "/users" also matches to
"/users/".
Honestly, there is no need for this piece of configuration, since its enabled by default and you by passing false to it, disabled it.
What happens when you pass "/" in the value attribute of RequestMapping Annotation?

Resources