-
-
Notifications
You must be signed in to change notification settings - Fork 501
Using Slim with Spring Boot
Work in progress!
Slim is the best web templating framework for Ruby (my opinion :) ), but it is also the best web templating framework for Java!
While maintaining a mature Java web project using the Spring Framework, we had to switch out our JSP templates for something supported by Spring Boot. Maybe we didn't have to, but we had been using Slim for a while in our Rails applications, so we wanted to see if using Slim with Spring Framework, especially Spring MVC, was possible and how well it would play.
Since our application has many templates, an important factor for success would be the ability to migrate one template at a time while keeping the application functional. This would make including JSP into Slim templates and vice versa necessary.
Our JSPs use plenty of Spring MVC specific context like command beans, and these would have to be accessed by the Slim templates.
All in all the conversion has been very pleasant, and we have not met any show-stoppers. We now use Slim templates happily in our Java Spring Framework application.
For the details on how we did the conversion, read on!
Add to pom.xml:
<dependencies>
<dependency>
<groupId>org.jruby</groupId>
<artifactId>jruby</artifactId>
<version>9.1.8.0</version>
</dependency>
<dependency>
<groupId>rubygems</groupId>
<artifactId>slim</artifactId>
<version>3.0.7</version>
<type>gem</type>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>de.saumya.mojo</groupId>
<artifactId>gem-maven-plugin</artifactId>
<version>1.1.5</version>
<configuration>
<includeRubygemsInResources>true</includeRubygemsInResources>
</configuration>
<executions>
<execution>
<goals>
<goal>initialize</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
<pluginManagement>
<plugins>
<!--This plugin's configuration is used to store Eclipse m2e settings
only. It has no influence on the Maven build itself. -->
<plugin>
<groupId>org.eclipse.m2e</groupId>
<artifactId>lifecycle-mapping</artifactId>
<version>1.0.0</version>
<configuration>
<lifecycleMappingMetadata>
<pluginExecutions>
<pluginExecution>
<pluginExecutionFilter>
<groupId>de.saumya.mojo</groupId>
<artifactId>gem-maven-plugin</artifactId>
<versionRange>[1.1.5,)</versionRange>
<goals>
<goal>initialize</goal>
</goals>
</pluginExecutionFilter>
<action>
<execute>
<runOnIncremental>false</runOnIncremental>
</execute>
</action>
</pluginExecution>
</pluginExecutions>
</lifecycleMappingMetadata>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
<repositories>
<repository>
<id>rubygems-release</id>
<url>http://rubygems-proxy.torquebox.org/releases</url>
</repository>
</repositories>
Add a new source folder for target/rubygems
Add to Spring config:
@Bean
public ScriptTemplateConfigurer jrubyConfigurer() {
ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
configurer.setEngineName("jruby");
configurer.setScripts("ruby/load_slim.rb");
configurer.setRenderFunction("render_slim");
return configurer;
}
Add a folder called ruby
in src/main/resources
and create a file called load_slim.rb
:
require 'ruby/render_slim'
# This file is loaded once for every view that is instantiated.
# It uses (requires) a separate file to avoid loading the same code multiple times.
Add to Spring config:
@Bean
public ViewResolver scriptTemplateViewResolver() {
ScriptTemplateViewResolver resolver = new ScriptTemplateViewResolver();
resolver.setPrefix("/WEB-INF/views/");
resolver.setSuffix(".slim");
return resolver;
}
Add a file called render_slim.rb
to src/main/resources/ruby
:
require 'slim'
require 'ruby/message_source_accessor'
NO_LAYOUT = ['/WEB-INF/views/index.slim', '/WEB-INF/views/login.slim']
PARTIAL_ATTR = '__partial'
def render_slim(template, variables, url)
request = Java::OrgSpringframeworkWebContextRequest::RequestContextHolder.getRequestAttributes().getRequest();
locale = Java::OrgSpringframeworkWebServletSupport::RequestContextUtils.getLocale(request)
message_source = Java::ComDatekM2mWebController::ControllerUtils.messageSource
message_source_accessor = MessageSourceAccessor.new(message_source, locale)
default_context = {
user: request.session.getAttribute("user"),
request: request,
session: request.getSession(),
current_location: request.getSession().getAttribute("currentLocation"),
currentLocation: request.getSession().getAttribute("currentLocation"),
ctx: request.contextPath,
locale: locale,
param: request.parameterMap,
params: request.parameterMap,
messages: message_source,
messageSource: message_source,
message: message_source_accessor
}
context_values = default_context.update(variables)
context = Struct.new(*context_values.keys).new(*context_values.values)
is_partial = request.getAttribute(PARTIAL_ATTR)
page_html = Slim::Template.new(url) {template}.render(context)
if is_partial || NO_LAYOUT.include?(url)
page_html
else
stream = JRuby.runtime.jruby_class_loader.getResourceAsStream("WEB-INF/views/layout.slim")
layout = Slim::Template.new() { stream.to_io.read }
layout.render(context) {page_html}
end
rescue Java::JavaLang::Exception => e
raise e.message
end
# self in this context is the Struct with the context variables
def render(view_path, params = {})
viewResolver = Java::ComDatekM2mWebController::ControllerUtils.applicationContext.getBean(org.springframework.web.servlet.view.script.ScriptTemplateViewResolver.java_class)
view = viewResolver.resolveViewName(view_path, Java::ComDatekM2mWebController::ControllerUtils.getLocale(request))
return "Could not find view #{view_path}" if view.nil?
mockResponse = org.springframework.mock.web.MockHttpServletResponse.new
mockResponse.character_encoding = request.character_encoding
r = com.datek.m2m.web.servlet.PartialRequest.new(request, Hash[params.select{|k,v| String === v}.map { |k, v| [k.to_s, [v].to_java(:string)]}]);
r.setAttribute(PARTIAL_ATTR, true)
each_pair do |k, v|
r.setAttribute(k.to_s, v)
end
map = Hash[params.map { |k, v| [k.to_s, v]}]
view.render(map, r, mockResponse)
mockResponse.getContentAsString
rescue Java::JavaLang::Exception => e
raise e.message
end
def present?(arg)
v = eval(arg.to_s)
!v.nil?
rescue NameError => e
false
end
def with_command_bean(command_name)
binding_result = self["org.springframework.validation.BindingResult.#{command_name}"]
binding_result.class.class_eval do
def [](field_name)
getFieldValue(field_name.to_s)
end
end
yield binding_result
end
def formatDateTime(dateTime)
if dateTime.nil?
return ''
end
if Java::OrgJodaTime::DateTime === dateTime
Java::ComDatekM2mKernelUtil::DateUtils.formatJodaDateTime(dateTime)
else
Java::ComDatekM2mKernelUtil::DateUtils.formatDate(dateTime)
end
end
Add another file called message_source_accessor.rb
to src/main/resources/ruby
:
class MessageSourceAccessor
def initialize(message_source, locale)
@message_source = message_source
@locale = locale
end
def [](key, *args)
@message_source.get_message(key, args.to_java, "???#{key}???", @locale)
end
end