Spring-boot: Support for JSP taglibs in freemarker

Created on 20 May 2014  ·  19Comments  ·  Source: spring-projects/spring-boot

Freemarker autoconfiguration does not pick up tagslibs, if they are present in the classpath.

enhancement

Most helpful comment

I agree that JSP Tags is old tech and should not be used for new projects, but it can be heavily used in old legacy spring projects still.

If someone wants to migrate old spring project, which uses JSP Tags in FreeMarker templates, to spring-boot, they can't easily do it. And one of the reasons is this issue - it is hard to understand how to use spring's and spring-security's and even custom JSP Tags from classpath instead of servlet context.

But it is actually easy task if you know how to do it. 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.

@dsyer @philwebb @wilkinsona Please consider to include some kind of this solution into spring-boot's freemarker autoconfiguration, if you don't see any problems with it. Of course it should be configurable and switchable, but I think in spring-boot it can be even enabled by default because without it, JSP tags just don't work with embedded servlet containers.

All 19 comments

I'm not sure that was ever intended to work. If you want to propose a change that enables it in a web app, feel free to send a pull request.

BTW - I hacked this issue the following way - https://github.com/isopov/fan/blob/master/fan-web/src/main/java/com/sopovs/moradanen/fan/WebApplicationConfiguration.java
It required to refer to taglibs with
<#assign security=JspTaglibs["/META-INF/security.tld"] />
instead of
<#assign security=JspTaglibs["http://www.springframework.org/security/tags"] />

What was the intended way to work with spring-security from freemarker based spring-boot application?

any word on this?

Still waiting for a pull request. JSP is an inferior technology and using it undermines Freemarker's advantages (in my opinion), but if anyone needs this feature we are open for contributions.

I would like to see this incorporated as well so we can have the full freemarker experience when using it outside of boot, I would prefer to see Spring Security shipping a macro library though.

+1 for the macros in Spring Security. Do you have a JIRA ticket link?

Just made one in a haste https://jira.spring.io/browse/SEC-3072

Any workaround for this issue with latest stable boot?

Has the problem been solved?

@Xiaoshuai 这个问题你怎么解决的?

We're cleaning out the issue tracker and closing issues that we've not seen much demand to fix. Feel free to comment with additional justifications if you feel that this one should not have been closed.

Our feeling is that this should be tackled in Spring Security (see https://github.com/spring-projects/spring-security/issues/3275).

老铁 666666

I agree that JSP Tags is old tech and should not be used for new projects, but it can be heavily used in old legacy spring projects still.

If someone wants to migrate old spring project, which uses JSP Tags in FreeMarker templates, to spring-boot, they can't easily do it. And one of the reasons is this issue - it is hard to understand how to use spring's and spring-security's and even custom JSP Tags from classpath instead of servlet context.

But it is actually easy task if you know how to do it. 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.

@dsyer @philwebb @wilkinsona Please consider to include some kind of this solution into spring-boot's freemarker autoconfiguration, if you don't see any problems with it. Of course it should be configurable and switchable, but I think in spring-boot it can be even enabled by default because without it, JSP tags just don't work with embedded servlet containers.

We'll take another look at this and reconsider the original decision.

Was this page helpful?
0 / 5 - 0 ratings