Spring-Boot with JSP Tag Libs in embedded Tomcat
Asked Answered
I

6

7

I am currently migrating a Spring MVC Webapp (xml-config to java-config, tomcat to embedded tomcat via spring-boot).

The webapp uses freemarker as templating engine and JSP Taglibs. Now when I call a freemarker page I get the following error:

freemarker.ext.jsp.TaglibFactory$TaglibGettingException: 
No TLD was found for the "http://www.springframework.org/tags/form" JSP taglib URI. (TLD-s are searched according the JSP 2.2 specification. In development- and embedded-servlet-container setups you may also need the "MetaInfTldSources" and "ClasspathTlds" freemarker.ext.servlet.FreemarkerServlet init-params or the similar system properites.)

The freemarker-header.ftl begins with following snippet:

<#assign form=JspTaglibs["http://www.springframework.org/tags/form"]>
<#assign core=JspTaglibs["http://java.sun.com/jstl/core"]>
<#assign spring=JspTaglibs["http://www.springframework.org/tags"]>
<#assign osc=JspTaglibs["/WEB-INF/osc.tld"]>

I did not find any usable search results for MetaInfTldSources and ClasspathTlds. Any one solved this problem before?

KR Habib

Indiaindiaman answered 17/11, 2015 at 11:37 Comment(0)
S
1

Spring Boot doesn't support the use of JSP taglibs with Freemarker out of the box. There's an open enhancement request that you might be interested in. It contains a link to a possible workaround where you configure FreemarkerConfigurer's tag lib factory with some additional TLDs to be loaded from the classpath:

freeMarkerConfigurer.getTaglibFactory().setClasspathTlds(…);
Stonecutter answered 17/11, 2015 at 13:42 Comment(1)
Hi, I tried that, but it does not work... Current workaround is putting the tlds from the JARs to webapp/META-INF folder. But that only works when starting the application with spring-boot:run command. Running the Application via standard main Application class in IntelliJ leads to the same result, that the app does not find the tld files... :-(Indiaindiaman
S
2

This really should be built-in.

First, disable the built in FreeMarkerAutoConfiguration on your Application:

@SpringBootApplication
@EnableAutoConfiguration(exclude = {FreeMarkerAutoConfiguration.class})
public class Application extends WebMvcConfigurerAdapter {
    ...
]

Then add this custom configuration:

(adapted from https://github.com/isopov/fan/blob/master/fan-web/src/main/java/com/sopovs/moradanen/fan/WebApplicationConfiguration.java; Added an ObjectWrapper to the TaglibFactory and removed the addResourceHandlers() override)

import freemarker.cache.ClassTemplateLoader;
import freemarker.ext.jsp.TaglibFactory;
import freemarker.template.TemplateException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
import org.springframework.web.servlet.i18n.SessionLocaleResolver;
import org.springframework.web.servlet.view.freemarker.FreeMarkerConfig;
import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer;
import org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver;

import javax.servlet.ServletContext;
import java.io.IOException;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Locale;
import java.util.Properties;

@Configuration
public class CustomFreemarkerConfiguration extends WebMvcConfigurerAdapter {


    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
        localeChangeInterceptor.setParamName("lang");
        registry.addInterceptor(localeChangeInterceptor);
    }

    @Bean
    public ReloadableResourceBundleMessageSource messageSource() {
        ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
        messageSource.setBasename("classpath:messages");
        messageSource.setFallbackToSystemLocale(false);
        messageSource.setDefaultEncoding("UTF-8");
        return messageSource;
    }

    @Bean
    public SessionLocaleResolver localeResolver() {
        SessionLocaleResolver localeResolver = new SessionLocaleResolver();
        localeResolver.setDefaultLocale(Locale.ENGLISH);
        return localeResolver;
    }

    @Bean
    @Autowired
    public freemarker.template.Configuration freeMarkerConfig(ServletContext servletContext) throws IOException,
            TemplateException {
        FreeMarkerConfigurer freemarkerConfig = configFreeMarkerConfigurer(servletContext);
        return freemarkerConfig.getConfiguration();
    }

    @Bean
    @Autowired
    public TaglibFactory taglibFactory(ServletContext servletContext) throws IOException, TemplateException {
        FreeMarkerConfigurer freemarkerConfig = configFreeMarkerConfigurer(servletContext);
        TaglibFactory taglibFactory = freemarkerConfig.getTaglibFactory();
        taglibFactory.setObjectWrapper(freemarker.template.Configuration.getDefaultObjectWrapper(freemarker.template.Configuration.getVersion()));
        return taglibFactory;
    }

    @Autowired
    @Bean
    public FreeMarkerConfig springFreeMarkerConfig(ServletContext servletContext) throws IOException, TemplateException {
        return new MyFreeMarkerConfig(freeMarkerConfig(servletContext), taglibFactory(servletContext));
    }

    private static FreeMarkerConfigurer configFreeMarkerConfigurer(ServletContext servletContext) throws IOException,
            TemplateException {
        FreeMarkerConfigurer freemarkerConfig = new FreeMarkerConfigurer();
        freemarkerConfig
                .setPreTemplateLoaders(new ClassTemplateLoader(CustomFreemarkerConfiguration.class, "/templates/"));
        ServletContext servletContextProxy = (ServletContext) Proxy.newProxyInstance(
                ServletContextResourceHandler.class.getClassLoader(),
                new Class<?>[] { ServletContext.class },
                new ServletContextResourceHandler(servletContext));
        freemarkerConfig.setServletContext(servletContextProxy);
        Properties settings = new Properties();
        settings.put("default_encoding", "UTF-8");
        freemarkerConfig.setFreemarkerSettings(settings);
        freemarkerConfig.afterPropertiesSet();
        return freemarkerConfig;
    }

    @Bean
    public FreeMarkerViewResolver viewResolver() {
        FreeMarkerViewResolver viewResolver = new FreeMarkerViewResolver();
        viewResolver.setCache(false);
        viewResolver.setSuffix(".ftl");
        viewResolver.setContentType("text/html;charset=UTF-8");
        return viewResolver;
    }


    private static class ServletContextResourceHandler implements InvocationHandler
    {

        private final ServletContext target;

        private ServletContextResourceHandler(ServletContext target) {
            this.target = target;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            if ("getResourceAsStream".equals(method.getName())) {
                Object result = method.invoke(target, args);
                if (result == null) {
                    result = CustomFreemarkerConfiguration.class.getResourceAsStream((String) args[0]);
                }
                return result;
            } else if ("getResource".equals(method.getName())) {
                Object result = method.invoke(target, args);
                if (result == null) {
                    result = CustomFreemarkerConfiguration.class.getResource((String) args[0]);
                }
                return result;
            }

            return method.invoke(target, args);
        }
    }

    private static class MyFreeMarkerConfig implements FreeMarkerConfig {

        private final freemarker.template.Configuration configuration;
        private final TaglibFactory taglibFactory;

        private MyFreeMarkerConfig(freemarker.template.Configuration configuration, TaglibFactory taglibFactory) {
            this.configuration = configuration;
            this.taglibFactory = taglibFactory;
        }

        @Override
        public freemarker.template.Configuration getConfiguration() {
            return configuration;
        }

        @Override
        public TaglibFactory getTaglibFactory() {
            return taglibFactory;
        }
    }
}

Add the following to your pom.xml:

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
    </dependency>
    <dependency>
        <groupId>javax.servlet.jsp</groupId>
        <artifactId>jsp-api</artifactId>
        <version>2.0</version>
    </dependency>

Then you can load in your template:

<#assign s=JspTaglibs["/META-INF/spring.tld"] />

<a href="${s.mvcUrl("IC#index").build()}">Home</a>
Swenson answered 24/1, 2016 at 10:23 Comment(1)
Great answer, totally solves the problem. I personally was trying to add spring-security support to my freemarker files and after adding this CustomFreemarkerConfiguration all I had to do to make this work was <#assign security=JspTaglibs["/security.tld"] />.Frosting
H
2

It is actually an easy task if you know how to do it. All you need is already embedded into FreeMarker, for instance it is TaglibFactory.ClasspathMetaInfTldSource class. I spend several hours to investigate that problem, so I want to share a solution.

I implemented it as BeanPostProcessor because now there is no way to set TaglibFactory before FreeMarkerConfigurer bean is initialized.

import freemarker.ext.jsp.TaglibFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer;

import java.util.Arrays;
import java.util.regex.Pattern;

/**
 * A {@link BeanPostProcessor} that enhances {@link FreeMarkerConfigurer} bean, adding
 * {@link freemarker.ext.jsp.TaglibFactory.ClasspathMetaInfTldSource} to {@code metaInfTldSources}
 * of {@link TaglibFactory}, containing in corresponding {@link FreeMarkerConfigurer} bean.
 *
 * <p>
 * This allows JSP Taglibs ({@code *.tld} files) to be found in classpath ({@code /META-INF/*.tld}) in opposition
 * to default FreeMarker behaviour, where it searches them only in ServletContext, which doesn't work
 * when we run in embedded servlet container like {@code tomcat-embed}.
 *
 * @author Ruslan Stelmachenko
 * @since 20.02.2019
 */
@Component
public class JspTagLibsFreeMarkerConfigurerBeanPostProcessor implements BeanPostProcessor {

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if (bean instanceof FreeMarkerConfigurer) {
            FreeMarkerConfigurer freeMarkerConfigurer = (FreeMarkerConfigurer) bean;
            TaglibFactory taglibFactory = freeMarkerConfigurer.getTaglibFactory();

            TaglibFactory.ClasspathMetaInfTldSource classpathMetaInfTldSource =
                    new TaglibFactory.ClasspathMetaInfTldSource(Pattern.compile(".*"));

            taglibFactory.setMetaInfTldSources(Arrays.asList(classpathMetaInfTldSource));
//            taglibFactory.setClasspathTlds(Arrays.asList("/META-INF/tld/common.tld"));
        }
        return bean;
    }
}

The only restriction is that *.tld files must have <uri> xml tag inside. All standard spring/spring-security TLDs have it. And also these files must be inside META-INF folder of classpath, like META-INF/mytaglib.tld. All standard spring/spring-security TLDs are also follow this convention.

Commented line is just for example of how you can add "custom" paths of *.tld files if for some reason you can't place them into standard location (maybe some external jar, which doesn't follow the convention). It can be extended to some sort of classpath scanning, searching for all *.tld files and adding them into classpathTlds. But usually it is just doesn't required if your TLDs follow JSP conventions to be placed inside META-INF directory.

I have tested this in my FreeMarker template and it works:

<#assign common = JspTaglibs["http://my-custom-tag-library/tags"]>
<#assign security = JspTaglibs["http://www.springframework.org/security/tags"]>
<#assign form = JspTaglibs["http://www.springframework.org/tags/form"]>
<#assign spring = JspTaglibs["http://www.springframework.org/tags"]>

For custom tag ("http://my-custom-tag-library/tags") to work, it must be *.tld file in src/main/resources/META-INF/some.tld and it must contain the <uri> xml tag, like <uri>http://my-custom-tag-library/tags</uri>. It will be found by FreeMarker then.

I hope it helps someone to save several hours to find "right" solution for this problem.

Tested with spring-boot v2.0.5.RELEASE

Headrail answered 20/2, 2019 at 2:10 Comment(1)
This should definitely be the accepted solution, as it doesn't require any changes in the FreeMarker templates. Thank you so much @Ruslan!Vanettavang
S
1

Spring Boot doesn't support the use of JSP taglibs with Freemarker out of the box. There's an open enhancement request that you might be interested in. It contains a link to a possible workaround where you configure FreemarkerConfigurer's tag lib factory with some additional TLDs to be loaded from the classpath:

freeMarkerConfigurer.getTaglibFactory().setClasspathTlds(…);
Stonecutter answered 17/11, 2015 at 13:42 Comment(1)
Hi, I tried that, but it does not work... Current workaround is putting the tlds from the JARs to webapp/META-INF folder. But that only works when starting the application with spring-boot:run command. Running the Application via standard main Application class in IntelliJ leads to the same result, that the app does not find the tld files... :-(Indiaindiaman
C
0

While rendering template, freemarker call TaglibFactory, which search for TLD in fours ways:

  1. addTldLocationsFromClasspathTlds
  2. addTldLocationsFromWebXml
  3. addTldLocationsFromWebInfTlds
  4. addTldLocationsFromMetaInfTlds

All of these methods are in TablibFactory class at freemarker jar. That last one, scan every jar in WEB-INF/lib searching for /META-INF/**/*.tld. You can see this logging if debug mode for freemarker is enabled.

Take a look how your project is deployed. In my case, using eclipse, wtp, tomcat and maven, the maven dependencies was configured in Eclipse/Deployment assembly as maven dependencies, of course :), hence these libs are not in WEB-INF/lib and so, was not found by addTldLocationsFromMetaInfTlds.

A way to solve is forcing deployment to copy all maven dependencies to WEB-INF/lib. I did it opening server configuration, at eclipse view 'servers', under server options uncheck all checkboxes but 'Module auto reload by default'.

Commentary answered 7/5, 2016 at 18:35 Comment(0)
M
0

None of those solutions worked for me, but after analysing the workarounds in the original ticket I found a solution that does work:

1 - Add the following in the pom.xml

    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
        <version>3.8.1</version>
    </dependency>
    <dependency>
        <groupId>javax.servlet.jsp</groupId>
        <artifactId>jsp-api</artifactId>
        <version>2.0</version>
    </dependency>

2 - Create the following classes

2.1 ClassPathTldsLoader

import org.apache.commons.lang3.ArrayUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer;

import javax.annotation.PostConstruct;
import java.util.Arrays;
import java.util.List;

public class ClassPathTldsLoader  {

    private static final String SECURITY_TLD = "/META-INF/security.tld";

    final private List<String> classPathTlds;

    public ClassPathTldsLoader(String... classPathTlds) {
        super();
        if(ArrayUtils.isEmpty(classPathTlds)){
            this.classPathTlds = Arrays.asList(SECURITY_TLD);
        }else{
            this.classPathTlds = Arrays.asList(classPathTlds);
        }
    }

    @Autowired
    private FreeMarkerConfigurer freeMarkerConfigurer;

    @PostConstruct
    public void loadClassPathTlds() {
        freeMarkerConfigurer.getTaglibFactory().setClasspathTlds(classPathTlds);
    }


}

2.2 FreemarkerTaglibsConfig

import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FreemarkerTaglibsConfig {


    @Bean
    @ConditionalOnMissingBean(ClassPathTldsLoader.class)
    public ClassPathTldsLoader classPathTldsLoader(){
        return new ClassPathTldsLoader();
    }

}

3 - Now you can load in ftl files, for example, security libs

<#assign spring = JspTaglibs["http://www.springframework.org/security/tags"]>

I hope this is useful for somebody else.

Marinate answered 12/5, 2019 at 1:54 Comment(0)
U
0

It's a little late for the party, but I had this problem upgrading an old project from Spring 3 to Spring 6, by now we have no time to replace the technology used to render the views, and the last version of FreeMarkerConfigurer does not provide the method getTaglibFactory:

So my solution for JSP security tags was rewrite them using FreeMarker macros:

<#ftl output_format="HTML" strip_whitespace=true> 
<#-- 
 * security.ftl 
 * 
 * This file consists of a collection of FreeMarker macros aimed at easing 
 * some of the common requirements of web applications - in particular 
 * handling of security. 
--> 
 
<#-- 
 * isAnonymous 
 * 
 * Verifies if there is no a logged user.
--> 
<#macro isAnonymous> 
  <#assign anonymous = true> 
  <#if SPRING_SECURITY_CONTEXT??> 
    <#assign anonymous = false> 
  </#if> 
  <#if anonymous> 
    <#nested> 
  </#if> 
</#macro> 
 
 
<#-- 
 * isAuthenticated 
 * 
 * Checks if there is a logged user and he/she is authenticated. 
--> 
<#macro isAuthenticated> 
  <#assign authenticated = false> 
  <#if SPRING_SECURITY_CONTEXT??> 
    <#assign authentication = SPRING_SECURITY_CONTEXT.authentication
              isUserAuthenticated = authentication.isAuthenticated()> 
    <#if isUserAuthenticated> 
      <#assign authenticated = true> 
    </#if> 
  </#if> 
  <#if authenticated> 
    <#nested> 
  </#if> 
</#macro>


<#-- 
 * hasRole 
 * 
 * Verifies if there is a logged user and he/she has the given role/authority.
 * 
 * Example: 
 * 
 *   <@security.hasRole role="ROLE_ADMIN"> 
 *     <br><span>User has the role: ROLE_ADMIN</span> 
 *   </@security.hasRole> 
 * 
 * @param role 
 *    The role and/or authority to verify 
--> 
<#macro hasRole role> 
  <#assign authorized = false> 
  <#if SPRING_SECURITY_CONTEXT?? && role??> 
    <#list SPRING_SECURITY_CONTEXT.authentication.authorities as authority> 
      <#if authority == role> 
        <#assign authorized = true> 
      </#if> 
    </#list> 
  </#if> 
  <#if authorized> 
    <#nested> 
  </#if> 
</#macro>
 
 
<#-- 
 * ifAnyGranted 
 * 
 * Checks if there is a logged user and he/she has one of the given roles/authorities. 
 * 
 * Example: 
 * 
 *   <@security.ifAnyGranted roles="ROLE_ADMIN,ROLE_SUPERADMIN"> 
 *     <br><span>User has one of the roles: ROLE_ADMIN, ROLE_SUPERADMIN</span> 
 *   </@security.ifAnyGranted> 
 * 
 * @param roles 
 *    Roles and/or authorities separated by commas to verify 
--> 
<#macro ifAnyGranted roles> 
  <#assign authorized = false> 
  <#if SPRING_SECURITY_CONTEXT?? && roles??> 
    <#list SPRING_SECURITY_CONTEXT.authentication.authorities as authority> 
      <#list roles?split(",") as role> 
        <#if authority == role> 
          <#assign authorized = true> 
        </#if> 
      </#list> 
    </#list> 
  </#if> 
  <#if authorized> 
    <#nested> 
  </#if> 
</#macro>


<#--
 * ifNotGranted
 *
 * Checks if there is a logged user and he/she does not have any of the given roles/authorities.
 *
 * Example:
 *
 *   <@security.ifNotGranted roles="ROLE_ADMIN,ROLE_SUPERADMIN">
 *      <br><span>User does not have any of the roles: ROLE_ADMIN, ROLE_SUPERADMIN</span>
 *   </@security.ifNotGranted>
 *
 * @param roles
 *    Roles and/or authorities separated by commas to verify
 -->
<#macro ifNotGranted roles>
    <#assign authorized = false>
    <#if SPRING_SECURITY_CONTEXT?? && roles??>
        <#assign authorized = true>
        <#list SPRING_SECURITY_CONTEXT.authentication.authorities as authority>
            <#list roles?split(",") as role>
                <#if authority == role>
                    <#assign authorized = false>
                </#if>
            </#list>
        </#list>
    </#if>
    <#if authorized>
        <#nested>
    </#if>
</#macro>

I added it to the file security.ftl and import it in a common ftl file using:

<#import "security.ftl" as security />

Unlettered answered 7/5 at 7:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.