Skip to content

Using Slim with Spring Boot

Arndt Kubosch edited this page Jul 16, 2018 · 2 revisions
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!

Configuring Maven to include JRuby and Slim gems

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

Configuring Spring to utilize JRuby

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.

Hooking into the ViewResolver

Add to Spring config:

@Bean
public ViewResolver scriptTemplateViewResolver() {
  ScriptTemplateViewResolver resolver = new ScriptTemplateViewResolver();
  resolver.setPrefix("/WEB-INF/views/");
  resolver.setSuffix(".slim");
  return resolver;
}

Setting up Slim rendering

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

Adding layout handling

Handling context variables

Handling includes from Slim templates

Handling includes from JSP templates