Friday, September 21, 2018

Eclipse RAP with Spring Boot

Intro

I like Eclipse :-). I've been using it for many years. I think the IDE is great and it is free! But Eclipse is much more than just the IDE. It is more like an ecosystem of applications, plugins and projects. Anyone can use the Eclipse Framework to create his/her own Eclipse-based application. It is called Eclipse RCP - Rich Client Application. Any RCP is based on the same principles as the Eclipse IDE: the product includes some features, any feature includes some plugins. 


I heard it is possible to include a plugin directly into the product but this is deprecated.
The UI in Eclipse Framework is based on 2 things:
  • The Application model
  • SWT - Standard Widget Toolkit
The Application Model is like a big "Mediator' (remember the pattern?) that handles interactions of many UI components at a high level. It is defined in an XML-like format. It defines windows, parts, commands, handlers, menus etc. 
SWT is a collection of widgets: buttons, labels, dialogs. These widgets are used to create the UI at the lower level than the Application Model. For example the UI within a part (MPart class) is created with SWT.

The Eclipse Framework can also be used to create web applications. It is called Eclipse RAP - Remote Application Platform. The idea is simple and impressive at the same time. The UI is created in Java with SWT widgets. In fact the UI is created with SWT widgets adapted for RAP. Then it is rendered in the browser. There is a special servlet that does the rendering RWTServlet (RWT may stand for Remote Widget Toolkit). Here is the link to the Developer's Guide for RAP.

And finally to the point. We all know that Spring Boot is awesome :-))! It must be the dominant platform for creating web applications. It offers some technologies to create the UI like JSP or Thymeleaf. But JSP is a little old and Thymeleaf could be a little less interactive than RAP. It would be great to make the two work together.

RAP Application with Spring Boot in a WAR file

The first option is to create a WAR with Spring Boot and RAP and deploy it to Tomcat. It is the option described in the Developer's Guide. Here is the link. I copied the important files.
The web.xml:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/j2ee"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
  version="2.4">

  <context-param>
    <param-name>org.eclipse.rap.applicationConfiguration</param-name>
    <param-value>com.example.HelloWorldConfiguration</param-value>
  </context-param>

  <listener>
    <listener-class>org.eclipse.rap.rwt.engine.RWTServletContextListener</listener-class>
  </listener>

  <servlet>
    <servlet-name>rwtServlet</servlet-name>
    <servlet-class>org.eclipse.rap.rwt.engine.RWTServlet</servlet-class>
  </servlet>

  <servlet-mapping>
    <servlet-name>rwtServlet</servlet-name>
    <url-pattern>/hello</url-pattern>
  </servlet-mapping>
</web-app> 

The application configuration:
public class HelloWorldConfiguration implements ApplicationConfiguration {
  public void configure( Application application ) {
    application.addEntryPoint( "/hello", HelloWorld.class, null );
  }
}
And the class:
public class HelloWorld extends AbstractEntryPoint {
  public void createContents( Composite parent ) {
    Label label = new Label( parent, SWT.NONE );
    label.setText( "Hello RAP World" );
  }
}
In order to make it work with the Maven WAR packaging put the web.xml into src/main/webapp/WEB-INF. The entry point in the application configuration must be configured for rwtServlet in web.xml.

For Spring Boot add the following:
@SpringBootApplication
public class SpringApp extends SpringBootServletInitializer {
  public SpringApplicationBuilder configure( SpringApplicationBuilder builder ) {
    return builder.web(WebApplicationType.NONE).sources(<configurations>);
  }
}

In pom.xml set the packaging to war.
<packaging>war</packaging>

There is though one important issue. Eclipse jars are not available in the Maven Central. Maybe it is possible to somehow use the p2 repository but I didn't try that. After all only one jar is necessary. I used the latest available version RAP 3.5. The jar is org.eclipse.rap.rwt_3.5.0.20180613-1038.jar.
I installed it manually to the local Maven repository and used the dependency in the pom.

This was enough to build a WAR and use it in Tomcat 9. No hacks necessary.

RAP Application with Spring Boot in an executable jar

This option looks more convenient. It would be really great to be able to use the RWTServlet inside Spring Boot. Then all the functionality like the executable jar and embedded Tomcat will work out of the box.
First return the main Spring Boot class to normal:

@SpringBootApplication
public class SpringApp {
    public static void main(String[] args) {
        SpringApplication.run(SpringApp.classargs);
    }
}
In fact Spring Boot can add a servlet with a ServletRegistrationBean:

@Bean
public ServletRegistrationBean<RWTServlet> servletRegistrationBean() {
    ServletRegistrationBean<RWTServlet> rwt = new 
ServletRegistrationBean<RWTServlet>(new RWTServlet(),"/hello""/hello2");
    return rwt;
}

But unfortunately this is not enough. This RWTServlet requires a special context to be bound to the ServletContext. Previously in the WAR example the RWTServletContextListener handled this :-(.

But Spring Boot even in this desperate situation showed was it was worth :-). It can bind custom listeners by declaring them as beans! The default functionality of RWTServletContextListener  required some rework because as it turned out binding this listener from inside Spring Boot had some limitations. This is what I ended up with:

public class RWTSCLSimple extends RWTServletContextListener {

    private ApplicationRunner applicationRunner;

    @Override
    public void contextInitialized( ServletContextEvent event ) {
        ServletContext servletContext = event.getServletContext();
        ApplicationConfiguration configuration = new 
HelloWorldConfiguration();
        applicationRunner = new ApplicationRunner( configuration
servletContext );
        applicationRunner.start();
    }

    @Override
    public void contextDestroyed( ServletContextEvent event ) {
        applicationRunner.stop();
        applicationRunner = null;
    }
}

This class simplifies the initial functionality of RWTServletContextListener and removes some method calls which were "not acceptable for a listener not declared in web.xml" :-)). That error message was very funny.

Now this class must be declared as a bean and that's it:
@Bean
public RWTSCLSimple rwtSCListener() {
    return new RWTSCLSimple();
}

All Spring Boot functionality works out of the box, even server.servlet.context-path. If the reader doesn't mind the little workaround this option looks better than the first one.

1 comment:

  1. Nice trick Vadim! I will save this into my records. It might be useful in the future.

    ReplyDelete