Chaining view resolvers

classic Classic list List threaded Threaded
14 messages Options
Reply | Threaded
Open this post in threaded view
|

Chaining view resolvers

emilime
Hello,

we are trying to chain Thymeleaf and standard Spring's InternalResourceViewResolver for jsp/jstl views however it seems that ThymeleafViewResolver has a wrong behaviour when chained; this happens because ThymeleafViewResolver.loadView always returns an instance of ThymeleafView instead of checking if the view exists and returning null if it does not (so if ThymeleafViewResolver doesn't find the view now we have an exception instead of a call to InternalResourceViewResolver which is the last resolver in our chain)

as found in standard spring view resolvers (UrlBasedViewResolver.loadView) there should be a check like:

  return (view.checkResource(locale) ? result : null);
 
checkResource could delegate to the attached template resolver because it should be resolver's implementation to check for existence...
Reply | Threaded
Open this post in threaded view
|

Re: Chaining view resolvers

danielfernandez
Administrator
Hi,

ThymeleafViewResolver does indeed honour the "return null if template not resolvable" contract needed for chaining resolvers. But it does this depending on the "viewNames" property of the ThymeleafViewResolver object. This property allows you to set a comma-separated sequence of patterns for the views that should be resolved by Thymeleaf, like for example "home*,/user/admin/*.html".

If the template you are trying to resolve does not match any of these patterns the view resolver will return null, as is needed for Spring querying the next resolver in the chain.

And you might be asking: Why is this? Why doesn't it just return null if the template file I'm asking for does not exist?

That's because Thymeleaf is far more flexible than JSP regarding template resolution. Whereas JSP only can obtain templates (.jsp files) as files from the webapp folder filesystem (ServletContext resources) and therefore can detect the existence of those files before actually reading them, Thymeleaf allows you to create whichever ITemplateResolver implementation you wish, including some that might not allow to determine whether the template exists or not before actually reading it.

Take the UrlTemplateResolver, for example. The only way to determine whether the template behind the specified URL is present or not is actually reading the URL: connecting to it and reading its contents (maybe from an external HTTP server). And of course you do not want to connect to the URL once to know whether it exists, and then once again for actually reading it if it does...

So, there is no way for Thymeleaf to be sure whether a template will be resolvable or not before trying to process the template. And that's why the ThymeleafViewResolver has to resort to the "viewNames" property as explained above.

Regards,
Daniel.
Reply | Threaded
Open this post in threaded view
|

Re: Chaining view resolvers

sparty02
Daniel,

First off, I want to say great work on Thymeleaf.  It's a nice step forward on the evolution of view technologies for web applications.  I see this thread is quite old, but its the most relevant dialogue I found on the topic of chaining view resolvers).

I understand the flexibility that Thymeleaf provides for view resolution, in particular your note about a view's existence that may not be known ahead of time.  That being said, this design is proving to be a bit burdensome on a current project I'm playing with to implement Thymeleaf.  This application will need to utilize both JSPs and Thymeleaf templates (targeting HTML5) as its a legacy app that makes heavy use of JSPs and will need to be migrated in pieces.  

Whats your recommendation for this scenario?  I'd like to prevent having to name all of my Thymeleaf (or JSP) templates something special (like suffixing with th or jsp) just in order for the resolver to know which one to choose.  Is that the only choice here?
Reply | Threaded
Open this post in threaded view
|

Re: Chaining view resolvers

Zemi
Administrator
Hello,

you need to choose some method to difference between JSP files and Thymeleaf files. I think that file extension is a good choice (.jsp for JSP and .html for Thymeleaf), but for example, you could use different directories for each view technology.

If you are using Spring, another option is ResourceBundleViewResolver, using a properties file to map files with view technologies.
Reply | Threaded
Open this post in threaded view
|

Re: Chaining view resolvers

sparty02
Here's what I originally wanted to do (the view names/etc are fake):

***Project folder***

WEB-INF
----views
--------mythymeleafview.html
--------myjspview.jsp


***Servlet context***

<bean id="templateResolver" class="org.thymeleaf.templateresolver.ServletContextTemplateResolver">
     <property name="prefix" value="/WEB-INF/views/" />
     <property name="suffix" value=".html" />
     <property name="templateMode" value="HTML5" />
</bean>

<bean id="jspViewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
     <property name="viewClass" value="org.springframework.web.servlet.view.JstlView" />
     <property name="prefix" value="/WEB-INF/views/" />
     <property name="suffix" value=".jsp" />
</bean>


***Controller***

@RequestMapping("/view1")
public String thymeleafView() {
     return "mythymeleafview";
}

@RequestMapping("/view2")
public String jspView() {
     return "myjspview";
}


HOWEVER, based on Daniel's comments above, this doesn't seem to work because the Thymeleaf view resolver (I do have them ordered in my servlet context) depends on the viewNames property of the ViewResolver in order to determine whether to pass along null OR to try and instantiate the view.  Here is one solution I found, but am not in love with.

***Project folder***

WEB-INF
----views
--------jsp
------------myjspview.jsp
--------thymeleaf
------------mythymeleafview.html



***Servlet context***

<bean id="templateResolver" class="org.thymeleaf.templateresolver.ServletContextTemplateResolver">
     <property name="prefix" value="/WEB-INF/views/" />
     <property name="suffix" value=".html" />
     <property name="viewNames" value="thymeleaf/*" />
     <property name="templateMode" value="HTML5" />
</bean>

<bean id="jspViewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
     <property name="viewClass" value="org.springframework.web.servlet.view.JstlView" />
     <property name="prefix" value="/WEB-INF/views/" />
     <property name="viewNames" value="jsp/*" />
     <property name="suffix" value=".jsp" />
</bean>



***Controller***

@RequestMapping("/view1")
public String thymeleafView() {
     return "thymeleaf/mythymeleafview";
}

@RequestMapping("/view2")
public String jspView() {
     return "jsp/myjspview";
}


While this allows the ThymeLeafViewResolver to determine whether or not it can handle the view (because the views are somewhat "namespaced" now), it forces you to organize your views into folders based on the type of view (jsp vs thymeleaf) before further organizing (jsp/admin, thymeleaf/admin, etc...).  This is why I'm not thrilled by this approach.  Another option I see is to suffix each of your views with something (i.e. mythymeleafview-th for a Thymeleaf view and myjspview-jsp for a JSP view), but this muddles up the name of the view and forces you to add one more hierarchy to the view definitions.


Maybe sub-classing the ThymeleafViewResolver and overwriting the canHandle method is an option???  I'd just rather stick with the framework and not extend too much if I don't have too.....
Reply | Threaded
Open this post in threaded view
|

Re: Chaining view resolvers

Zemi
Administrator
Try removing suffix from configuration

     <property name="suffix" value=".html" />

and passing it in the controller

     @RequestMapping("/view1")
         public String thymeleafView() {
             return "mythymeleafview.html";
      }

Reply | Threaded
Open this post in threaded view
|

Re: Chaining view resolvers

sparty02
Zemi,

Thanks for the quick response.  I tried that approach too and, while it works, it still isn't ideal as now you are somewhat tying the view name to the view technology.  In your example, removing the View Resolver's suffix property ".html" and using the view name "mythymeleafview.html" is almost the same as keeping that suffix and just tying a pseudo-suffix to the actual view name (something like "mythymeleafview-th".

What I'd really like to do is leave any notion of the view technology out of it and just use view names representing the actual view/activity the user is working with.  I would have initially expected that, based on the resolver setup in the context and the order in which my resolvers were placed, that my app would seamlessly fall back on one view technology or the other based on their order setup in the spring context.  Unfortunately, both the JSP view resolver and the Thymeleaf view resolver seam to try and force view resolution and not pass along null down the chain without setting the viewNames property.
Reply | Threaded
Open this post in threaded view
|

Re: Chaining view resolvers

Thibault Duchateau
Hi,

Michael Isvy has done something's cool based on the resolver setup in his mvc layout samples.

I'm not sure this is exactly what you're looking for but take a look at the spring-*-config.xml files here.

Regards,
Thibault
Reply | Threaded
Open this post in threaded view
|

Re: Chaining view resolvers

sparty02
Thanks for the suggestion Thibault.  However, Michael is more or less doing what I suggested as an initial solution up in my previous post:

WEB-INF
----views
--------jsp
------------myjspview.jsp
--------thymeleaf
------------mythymeleafview.html

While this works, it forces you to somewhat namespace your views by folder.  It's probably the cleanest solution at the moment, but I'd rather not have to add in that one additional 'hierarchy' to my view definitions.  For instance, when forwarding to one of these views from a Spring MVC controller, you'd need to include the view type, i.e. 'jsp/myjspview' or 'thymeleaf/mythymeleafview'.  I'd rather just supply a pure view name and let the resolver's duke it out.
Reply | Threaded
Open this post in threaded view
|

Re: Chaining view resolvers

hereticpriest
In reply to this post by sparty02
If the behaviour of the Thymeleaf resolver is dependent on a Thymeleaf setting ("viewNames") then it does not universally honour the the resolver contract which it should. I would like to migrate a JSP based application to Thymeleaf without the need to a) restructure the view directory layout or b) amend the controller so that it becomes partially responsible for view resolution ( "jsp/view" vs "th/view" or including the extension both of which is the job of the resolver chain in the first place). I certainly appreciate the the desire to extend view resolution beyond the scope of the file system but not at the expense of altering the way in which the majority of views are resolved in any case.
Reply | Threaded
Open this post in threaded view
|

Re: Chaining view resolvers

lupons
Hello, I am facing the same problem as mentioned. I have a legacy application built on JSP and trying to implement new features using Thymeleaf leads to the same proble Sparty02 is saying. His solution is the same I am forced to use if I want to implement new requirements with Thymeleaf on the app but is "ugly" as it is not a seamless integration because it ties the view name with the view technology. Nonetheless is the only working way I found in this application to make both technologies live together.

Maybe a postprocessing annotation or AOP that could add the required path while migration takes place could be a solution?
Reply | Threaded
Open this post in threaded view
|

Re: Chaining view resolvers

Paul Chapman
It is not hard to overload the view-resolver's canHandle() method to actually look for the template instead of checking the viewNames and excludedViewNames list.  The code is below (I use Spring's Resource classes to do the checking).

It would be nice if the ThymeleafViewResolver came with both options and a simple property to switch between them.  IMHO the default behaviour _should_ be to look for the resolver and return null if not found, as that is the contract ViewResolver specifies.  (It would be nice is Spring's own InternalResourceViewResolver also did this!)

The code below looks for the template.  I wrote my own ThymeleafViewResolver subclass and overloaded the canHandle() method.  A small gotcha is that the prefix and suffix are not available from the template-engine (not yet initialised), so I have passed them into the view-resolver too.

-Paul

- - - Code here - - - >

        public class CustomThymeleafViewResolver extends ThymeleafViewResolver {

                public String templateDir; // Prefix
                public String fileType; // Suffix

                /**
                 * Stash away the suffix (template directory) and suffix (file-type).
                 * This data is also in the Template Engine but is not available when
                 * this method is called (the engine is not initialised).
                 *
                 * @param templateDir
                 * @param fileType
                 */
                public CustomThymeleafViewResolver(String templateDir, String fileType) {
                        this.fileType = fileType;
                        this.templateDir = templateDir;
                }

                /**
                 * Override to actually look for the resource to see if it
                 * exists. This view resolver caches its views after the first time, so
                 * the overhead of this check may be acceptible.
                 */
                @Override
                protected boolean canHandle(final String viewName, final Locale locale) {
                        Boolean exists = thymeleafViewExists(viewName,
                                        templateDir, fileType);
                        // Fallback on other approach.
                        return (exists == null) ? super.canHandle(viewName, locale)
                                        : exists;
                }

                protected Boolean thymeleafViewExists(String viewName, String prefix,
                        String suffix) {
                        String viewPath = prefix + viewName + suffix;
                        logger.info("Checking view: " + viewName + " looking for " + viewPath);

                        Resource res = null;

                        if (viewPath.startsWith("classpath:"))
                                res = new ClassPathResource(viewPath.substring(10));
                        else if (viewPath.startsWith("file:"))
                                res = new FileSystemResource(viewPath.substring(5));
                        else {
                                try {
                                        res = new UrlResource(viewPath);
                                } catch (MalformedURLException e) {
                                        logger.info("Unrecognised resource " + viewName);
                                        return null;   // Can't decide, give up
                                }
                        }

                        if (!res.exists()) {
                                logger.info(viewPath + " not found, skipping Thymeleaf");
                                return Boolean.FALSE;
                        }

                        logger.info("Found " + viewName + ", using Thymeleaf");
                        return Boolean.TRUE;
                }
        }
Reply | Threaded
Open this post in threaded view
|

Re: Chaining view resolvers

Harmeet Singh
Hello, I am new for thymeleaf, I ma using both JSP an Thymeleaf view resolver, but unfortunate the JSP resolver is call not thy me leaf. I am using Java Based Configuration.

@Configuration
public class MultiViewResolver {

        @Bean
        public TilesConfigurer tilesConfigure() {
                TilesConfigurer configurer = new TilesConfigurer();
                configurer.setDefinitions(new String[]{"classpath:tiles/admin-def.xml"});
                return configurer;
        }
       
        @Bean
        public TilesViewResolver viewResolver() {
                TilesViewResolver resolver = new TilesViewResolver();
                resolver.setOrder(0);
                return resolver;
        }
       
       
        /* Thymeleaf configuration */
        @Bean
        public ServletContextTemplateResolver templateResolver() {
                ServletContextTemplateResolver templateResolver = new ServletContextTemplateResolver();
                templateResolver.setPrefix("/WEB-INF/views/");
                templateResolver.setSuffix(".html");
                templateResolver.setTemplateMode("HTML5");
                templateResolver.setOrder(1);
                return templateResolver;
        }
       
        @Bean
        @Autowired
        public SpringTemplateEngine templateEngine(ServletContextTemplateResolver templateResolver) {
                SpringTemplateEngine templateEngine = new SpringTemplateEngine();
                templateEngine.setTemplateResolver(templateResolver);
                return templateEngine;
        }
       
        @Bean
        @Autowired
        public ThymeleafViewResolver thymeleafViewResolver(SpringTemplateEngine templateEngine) {
                ThymeleafViewResolver thymeleafViewResolver = new ThymeleafViewResolver();
                thymeleafViewResolver.setViewNames(new String[]{"thymeleaf/*"});
                thymeleafViewResolver.setTemplateEngine(templateEngine);
                return thymeleafViewResolver;
        }
       
        /* JSP view resolver */
       
        @Bean
        public InternalResourceViewResolver jspViewResolver() {
                InternalResourceViewResolver resolver  = new InternalResourceViewResolver();
                resolver.setPrefix("/WEB-INF/views/");
                resolver.setSuffix(".jsp");
                resolver.setViewClass(JstlView.class);
                resolver.setOrder(2);
                return resolver;
        }
}


@Configuration
@EnableWebMvc
@Import(value=MultiViewResolver.class)
@ComponentScan(basePackages="com.harmeetsingh13")
public class WebConfiguration extends WebMvcConfigurerAdapter{

        @Override
        public void addResourceHandlers(ResourceHandlerRegistry registry) {
                registry.addResourceHandler("/assets/**").addResourceLocations("/assets/");
        }
}


@Controller
@RequestMapping(value="/")
public class SampleController {

        @RequestMapping(value="/tiles-url")
        public String tilesTest() {
                System.out.println("This is Tiles Controller");
               
                return "dashboard";
        }
       
        @RequestMapping(value="/thm-url")
        public String thmTest() {
                System.out.println("This is Thymeleaf Controller");
               
                return "thymeleaf/thm-dashboard";
        }
}

When call to the "thm-url" URL, i get following error.

Reply | Threaded
Open this post in threaded view
|

Re: Chaining view resolvers

Harmeet Singh
In my above sample, when i comment the oreder from JSP, now the application is run, why this happen?

       @Bean
        public InternalResourceViewResolver jspViewResolver() {
                InternalResourceViewResolver resolver  = new InternalResourceViewResolver();
                resolver.setPrefix("/WEB-INF/views/");
                resolver.setSuffix(".jsp");
                resolver.setViewClass(JstlView.class);
                //resolver.setOrder(2);
                return resolver;
        }