Dissecting JRuby on Rails on SAP NetWeaver Cloud
In the previous two articles (part 1, part 2) we developed from scratch a simple Rails application locally, and then ported it to run on the SAP NetWeaver Cloud platform and to make use of its persistence and document services. The last stop of our Rails@SAP NetWeaver Cloud train will be the Connectivity Service. To show how it can be consumed from a Rails environment, we will use again the photo uploader application, although this doesn’t have much to do with its main use case of storing photos. As done in previous steps, you can find the application sources in the connectivity branch in its GitHub repo.
After finishing with the tutorial, we will discuss in more details how the whole Rails integration for the NetWeaver Cloud platform works, as a kind of technical retrospective for what we have actually done in all those steps so far.
We will focus on the Internet Connectivity capabilities of the SAP NetWeaver Cloud platform; consuming the OnDemand-OnPremise scenario is completely identical, and can be achieved in the same way. We will stick to the example from the help page, which consumes an Yahoo weather service by the mechanism of HTTP destinations. This implies that our application will need to use the following weather destination:
Name=weather Type=HTTP Authentication=NoAuthentication URL=http://weather.yahooapis.com
as well as to have a resource reference to it, inside web.xml:
<resource-ref> <res-ref-name>weather</res-ref-name> <res-type>com.sap.core.connectivity.api.http.HttpDestination</res-type> </resource-ref>
We go to the application root folder, and generate a new controller responding only to the default index action:
jruby -S rails generate controller weather index
This will generate controller class, view, helpers and routes for our new controller.
We now modify the weather controller to include some imports which will bridge the Java and Ruby worlds; add the consumption logic inside the index action, following in a straightforward manner the code snippet from the official documentation:
require 'java' java_import javax.naming.InitialContext java_import org.apache.http.client.methods.HttpGet java_import org.apache.http.util.EntityUtils class WeatherController < ApplicationController def index ctx = InitialContext.new destination = ctx.lookup("java:comp/env/weather") httpClient = destination.createHttpClient httpGet = HttpGet.new("forecastrss?p=BUXX0005&u=c") # Execute the request httpResponse = httpClient.execute(httpGet) # Process the response entity = httpResponse.getEntity @respToString = EntityUtils.toString(entity) end end
We must include the new application URL – /weather/index – to the Rack filter mappings: we add this line in the web.xml:
And we have to show the result in the browser, by having the weather view look like this:
<h1>Consuming Internet Connectivity from Rails@SAP NetWeaver Cloud: </h1>
<%= @respToString.html_safe %>
This is it. After assembling again the WAR file, redeploying and providing the weather destination to our application, we can access it at https://photoappi055691trial.hanatrial.ondemand.com/photoapp/weather/index :
For a smooth transition to the technical part of this blog, it is to be mentioned here that this is all possible thanks to the JRuby project. It provides with great capabilities for Ruby-Java integration and interoperability, and this is just a simple example of calling Java code from Ruby. Of course, you won’t get that running on other platforms, such as MRI Ruby. Other notable features include implementing a Java interface in Ruby, generating Java classes at runtime (or ahead-of-time), reopening and subclassing Java classes in Ruby, etc.
One of the greatest benefits of running a Ruby app under JRuby is the ability to seamlessly consume and leverage the enormous and feature-rich set of Java libraries out there – the Apache HTTP client just being shown above. Experienced Rails developers might find this HTTP client manipulation too low-level for a business logic in a Rails app, though. Careful readers have probably noticed that the previous blog showed consumption of our cmis_client library in the same fashion.
Details for the geeks
As a high-level overview, here is how the integration looks like.
The file system structure of the Rails app is sufficient for running it locally, but this is not the case in a Java environment. We use Warbler here, which packages the app into a WAR archive, suitable for running on top of a servlet container environment. Warbler knows to put into the WAR the JRuby runtime together with the JRuby-Rack adapter which is responsible for the plumbing into the servlet container. This runs fine on NetWeaver Cloud’s runtime.
Let’s get to the persistence service part. As this is the key part of the integration, we will put more focus here.
Our sample application uses the ActiveRecord framework for its persistence layer. Although other object-relational mapping solutions have emerged in the Ruby world, like DataMapper or Sequel, ActiveRecord still appears to be the most popular and mature choice in the Rails ecosystem.
Since NW Cloud persistence service is currently powered by the MaxDB database platform, there is the need to make ActiveRecord talk to MaxDB. Native ActiveRecord drivers usually require installing native extensions for communicating with the database (as is the case with gems like mysql2 and pg) which is something that is not supported well in JRuby environment. Aside from that, access to the persistence service is given to an application in terms of a Java object: JNDI based JDBC data source.
That being said, the natural next step is to make use of the great open source project, called ActiveRecord JDBC adapter. It is designed to handle the Active Record persistence by simply using a JDBC driver to your database – all this on top of the JRuby runtime. So, all we have to do is to provide the JDBC driver to the MaxDB database.
However, in constructing the ActiveRecord-SQL translation, the adapter makes some generic assumptions for the SQL queries, which might turn to be invalid for your specific database and its SQL dialect. That’s why this adapter project is designed in a very extensible way, so that different databases could be easily plugged in and integrated within it. So far there is support for popular relational DBs like MySQL, Postgre, SQLite, etc. and with providing the MaxDB extension project to the community this becomes possible for MaxDB as well.
Please note that the project is in an early phase, and its evolution depends very much on the applications that are being run with it. The more complex apps we manage to get running with it, the closer the adapter will get to covering MaxDB’s dialect. So it wouldn’t be weird for the adapter to have some SQL issues with your app. Also, contributions from the community are very welcome.
To make use of NetWeaver Cloud’s persistence service the application doesn’t have to declare anything special, because currently it will be assigned a persistence service instance by default (in the long-term perspective this behavior could be dropped, and the application should have to declare a reference to the persistence service inside the application’s descriptor).
Next, the central question is how to populate the application’s schema, i.e. how to execute the db:migrate Rake task. This is achieved by executing this initializer:
migrations = Rails.root.join('db','migrate') ActiveRecord::Migrator.migrate(migrations)
It effectively calls the ActiveRecord migration logic during application start, and it uses the migrations that need to be packaged inside the app.
In essence, the approach taken here is to have the application injected some initializing code, which executes the migration, that is, this is triggered and executed from inside the application. This is needed because SAP NetWeaver Cloud is a strictly secured platform and OS-level access to its VMs is not possible – hence, the rake task cannot be executed directly from the file system root of the application, as Rails developers usually do.
Consuming connectivity and document service is much simpler: it steps on the feature to easily call Java logic from JRuby. What is worth mentioning in addition to that, is the way we manage to show photos inside the browser by constructing URLs to them. Our pictures are stored as CMIS documents in the application’s ECM repository, and for being able to access them as URLs, we use the Proxy servlet approach described here. This is the servlet that we had to define in the previous blog. However, as opposed to the Rails app which we want to be entirely served by the Rack filter, the servlet lives in the Java domain. This is the reason why we don’t want the servlet to go through Rack: it is not part of the Rails application.
Because there doesn’t seem to exist a simple solution to exclude URL patterns in the web.xml descriptor, using the default /* mapping won’t do: instead, we explicitly list all the Rails URLs of the application, and map them to Rack.
Please note that the first request to the application might be a little bit slower because the JRuby runtime is bootstrapped lazily. Also, the DB schema creation = db:migrate task is executed at this moment of Ruby starting (which is not to be confused with the application start as seen in the NW Cloud’s Account page).
We hope you enjoyed this Rails@NetWeaver Cloud journey, as yet another example of the BYOL – Bring Your Own Language – capabilities of the SAP NetWeaver Cloud platform. Any feedback is welcome. If you are willing to integrate your Rails app on SAP NetWeaver Cloud and have some questions, we would be happy to discuss them.