After I burned the midnight oil yesterday to debug into a problem I was having with getting the fastercsv gem to work with JRuby in my OSGI environment I decided to write a post about it and how I finally got it working.The solution is not super clean but it works and at least it helped me to understand some of the inner workings of how JRuby determines file locations and classpaths and uses classloaders to load files….and that all that is different in an OSGI Environment.
- i want to use the fastercsv gem using require ‘fastercsv’ in the jruby script i am calling using a ‘new ScriptingContainer(…)‘ instance
- jruby always errored out telling me that it can’t find the required gem
- I have read JRuby 1.1.6: Gems-in-a-jar and lots of other blogs about it but it didn’t help so far
- custom web-application build from multiple OSGI-bundles and using the embedded Jetty server
- there is one ‘scripting’ bundle where all code related to Java-scripting is included (e.g. JRuby)
- in this ‘scripting’ bundle there is a lib-folder containing my jRuby-complete.jar which is added to the local classpath of that bundle (jruby-complete is NOT in my targetplatform and is not started as an OSGI-bundle here, but I use it inside my own OSGI bundle.
- my OSGI Container is Eclipse Equinox (Equinox SDK Version 3.5m4)
The reason why all existing information out there didn’t really work for me was because I am using JRuby in an OSGI environment and the main difference is that classloading in OSGI environments is different, as each bundle has its own classloader. It seems that JRuby uses the wrong classloader when using ScriptingContainer so that it determines a wrong path of jruby.home environment.
Also the way I added the fastercsv gem to my system is a bit unusual and hacky, but I will improve this once I find a better way which is my next step. I have create installed the gem as described here (i didn’t create a jar) but I have copied the gem in my jruby-complete.jar (rename jar to zip, unpack, put fastercsv into the gems folder, zip it and rename it to jar again). That is hacky but I did that because JRuby is trying to find the gems in jruby.home which is by default in /META-INF/jruby.home inside the jruby-complete.jar.
In my opinion that would be enough actually so that JRuby can determine my new gem, but because of my OSGI container it was using the wrong classloader to load the file so JRuby ended up detemining a wrong ‘jruby.home’ path. You can test this yourself by setting a breakpoint and debugging the method org.jruby.runtime.load.LoadService.init(List). There is a line
String canonNormal = new File(jrubyHome).getCanonicalPath();
In my case this path resolved to the current directory which was my Eclipse installation directory of the eclipse binary, but basically it was the wrong path. The path which was constructed there doesn’t even exist, and that’s the reason why my fastercsv gem could not be found.
We don’t need the path of the current directory – we need the the path of the current bundle which contains the jruby-complete.jar.
One solution was to set an environment variable (-Djruby.home=”/absolute/path/to/my/jruby-installation”) but I didn’t want to do that as I also had to unpack the jruby jar for that purpose, and I also don’t want to maintain another environment variable for each different server.
Later I found out that there is OSGiScriptingContainer which should take care of that and resolves jruby.home for the current bundle. But unfortunately I am running an old version of Eclipse Equinox and I got a NoClassDefFoundException as the class BundleReference was missing. An update of Eclipse Equinox in my target platform would have helped probably but I don’t want to upgrade now. So I had to find another solution.
My solution was to check the source of OSGiScriptingContainer to find out what is actually going on there and I saw that they have a special way to determine the ‘jruby.home‘ in order to get an absolute path to the actual jruby-complete.jar which is used as a basis for all other lookups where relative paths are used (e.g. path like classpath:/…). The code which I have borrowed comes from OSGiFileLocator.java.
Long story short:
In order to let my ScriptingContainer use the correct jruby.home in my environment I have used the following code:
// SETTING JRUBY.HOME path // Copyright notice:Code partly borrowed from org.jruby.embed.osgi.utils.OSGiFileLocator.getFileURL(URL) // pretty hacky way abusing reflection // might only work and is only required for Eclipse Equinox OSGI Environment // we do it because we cannot use jruby's OSGiScriptingContainer. // we couldn't use it because we are probably running on a old // Equinox version which doesn't have the class BundleReference yet // TODO upgrade Equinox to be able to use OSGiScriptingContainer to hopefully remove the code below URL url = Activator.getContext().getBundle().getResource("/META-INF/jruby.home"); URLConnection conn = url.openConnection(); Method method = conn.getClass().getMethod("getFileURL"); method.setAccessible(true); URL url2 = (URL)method.invoke(conn); String path = url2.getPath(); if (path.endsWith("/")) path = path.substring(0, path.length() - 1); // remove trailing slash System.setProperty("jruby.home", path); container = new ScriptingContainer(LocalContextScope.THREADSAFE, LocalVariableBehavior.GLOBAL);
After that change jruby got the correct path to jruby.home which was point to the real absolute path of my jruby-complete.jar so that all lookups for the gems where using the correct path.
Note, that you should be aware that this code is very specific for equinox osgi, a very hacky approach. It should only serve for educational purposes and should give you a hint in which direction to investigate if you are also having problems with jruby being unable to find your gems even though you are sure that it is there and you have already banged your head on the wall too many times …
Using OSGIScriptingContainer is much cleaner, but as I spent hours figuring this out here, I am sharing this with you anyway. Once I have a better solution I’ll try to update this post.
My next step will be to figure out how I could use the gems-in-a-jar approach to just add new gems to the classpath instead of repackaging the jruby-complete.jar.