Links

  • 1. Sogeti
  • 2. JBoss
  • 3. IBM
  • 4. Oracle
  • 5. SpringSource
  • 6. NL-JUG
  • 7. Java

Archives

Syndication  RSS 2.0

RSS 1.0
RSS 2.0

Bookmark this site

Add 'JCN Blog' site to delicious  Add 'JCN Blog' site to technorati  Add 'JCN Blog' site to digg  Add 'JCN Blog' site to dzone

Posted by Barend Garvelink at 15:23 on Tuesday 5 May    Add 'Apache Felix administration with JavaScript' site to delicious  Add 'Apache Felix administration with JavaScript' site to technorati  Add 'Apache Felix administration with JavaScript' site to digg  Add 'Apache Felix administration with JavaScript' site to dzone

In my current assignment we have built an application with a number of modules that can be deployed in mixed configurations. The application is deployed to portable devices, initially a few dozen, ultimately several thousand instances. To deal with these requirements, we used the OSGi platform. We chose Apache Felix as our implementation. Some of our bundles use ManagedService (or -Factory) for configuration. One of the challenges we ran into was the initial installation of our bundles into the framework. We didn’t want to auto.start all of our bundles, which left us with a long list of shell commands to be entered into the Felix console by hand (multiple lists, in fact, for various configurations).

I came up with what I think is a pretty nifty solution.

We’re on Java 6, which gives us Rhino right out of the box. With a modest amount of code, I was able to execute installation scripts written in JavaScript. This not only automates bundle installation and configuration, but it gives us the ability to do all sorts of checks, conditional configuration and updates should we have the need to. There are script engines available for Python, Ruby, Groovy and various other languages. We stuck with the JavaScript engine provided by default.

Note: all import statements and most javadoc comments have been stripped for brevity. The code snippets in this article may be used subject to the terms of the MIT License.

Sample script

Here’s a sample installation script that installs a couple of bundles, creates and populates a ManagedServiceFactory configuration for one of them, marks all bundles started and finally shuts down the OSGi framework.

01/*
02   Global scope objects:
03   > bundleContext     : org.osgi.framework.BundleContext (scriptrunner bundle)
04   > system            : org.osgi.framework.Bundle (System bundle)
05   > ConfigurationAdmin: org.osgi.service.cm.ConfigurationAdmin
06   > StartLevel        : org.osgi.service.startlevel.StartLevel
07   > EventAdmin        : org.osgi.service.event.EventAdmin
08   > PackageAdmin      : org.osgi.service.packageadmin.PackageAdmin
09   > util              : nl.sogeti.jcn.felixjavascript.ScriptUtil
10*/
11println('=======================================================');
12println('  Sample installation script  ');
13println('=======================================================');
14// We use startlevel 1 for maintenance mode and 2 for operational mode. This is quite convenient.
15if (StartLevel.getStartLevel() > 1) {
16	throw 'Installation aborted: Startlevel too high. (must be 1, got ' + StartLevel.getStartLevel() + ')';
17}
18
19// Install our application bundles at level 2.
20StartLevel.setInitialBundleStartLevel(2);
21
22var allbundles = new Array();
23
24println('[ 1] install bundles');
25
26// You could use conditionals to check for existing versions and selectively upgrade bundles. 
27allbundles[allbundles.length] = bundleContext.installBundle('file:lib/google-collections.jar');
28allbundles[allbundles.length] = bundleContext.installBundle('file:lib/joda-time.jar');
29allbundles[allbundles.length] = bundleContext.installBundle('file:lib/slf4j-api.jar');
30allbundles[allbundles.length] = bundleContext.installBundle('file:lib/slf4j-log4j12.jar');
31
32// This bundle uses managed configuration. Hang on to its Bundle object for convenience.
33var bundle1 = bundleContext.installBundle('file:lib/bundle1.jar');
34allbundles[allbundles.length] = bundle1;
35
36println('[ 1] OK');

So far, pretty straightforward. Each call to bundleContext.intallBundle(url) returns a Bundle object that we store in an array for package resolution. That’s next:

37
38println('[ 2] resolve bundles');
39
40if (!PackageAdmin.resolveBundles(allbundles)) {
41    println('[ 2] FAIL');
42    println('     Bundle resolution failed.');
43    throw 'Installation aborted: Bundle resolution failed.';
44}
45println('[ 2] OK');

On a clean installation, bundle resolution shouldn’t fail. If you need more control over the resolution process, you can iterate the allbundles array and resolve one bundle at a time. If you don’t intend to do that, you can leave the allbundles array behind and just pass null to resolve all unresolved bundles. Aborting the script as I do here may be unpretty, but at least it prevents the script from breaking anything else pending human intervention. In the third part of the script, we deploy a managed service configuration and get some user input on it.

46// Configure bundle1
47println('[ 3] configure bundle1');
48
49var config_bundle1 = ConfigurationAdmin.createFactoryConfiguration('somepackage.bundle1.FACTORY_CONFIG', bundle1.getLocation());
50var props_bundle1 = util.loadPropertiesFile('file:install/bundle1-defaults.properties');
51
52// Query user for some config values, input validation on the second one
53var newFooProperty = util.prompt('What should I use for FooProperty?', props_bundle1.get('foo-property'));
54var newBarProperty = props_bundle1.get('bar-property');
55do {
56	newBarProperty = util.prompt('What should I use for BarProperty? (must be a positive seven-digit integer)', newBarProperty);
57} while (!/^[\d]{7}$/.test(numeriekProperty));
58props_bundle1.put('foo-property', newFooProperty);
59props_bundle1.put('bar-property', newBarProperty);
60
61config_bundle1.update(props_bundle1);
62
63println('[ 3] OK');
64

Finally, we start all the bundles. This only marks them as started or one should say "startable". They’re not started immediately because they’ve been installed at bundle level 2 and the framework is still at startlevel 1. We use two separate shortcuts to launch our application in maintenance mode (startlevel 1) or normal startup which immediately boots to start level 2.

66println('[ 4] starting bundles');
67for (i = 0; i < allbundles.length; i++) {
68    bundle = allbundles[i];
69	bundle.start();
70}
71println('[ 4] OK');
72
73println('=======================================================');
74println('INSTALLATION COMPLETED ');
75println('=======================================================');
76
77// Shutdown the OSGi framework by stopping the system bundle.
78// Launch Felix at start level 2 to launch the application. 
79system.stop();

Now that you’ve seen what an installation script for OSGi might look like, let’s dive into the code that runs it.

Launching scripts

Executing a script with javax.script is easy: just new up a ScriptEngineManager, obtain a ScriptEngine, and pass the script as a String or a Reader. The following code sample wraps this in a class to which I’ll add some features further on.

01package nl.sogeti.jcn.felixjavascript;
02/**
03 * Uses <code>javax.script</code> to execute scripts inside the OSGi container.
04 * @author Barend Garvelink
05 */
06public class ScriptExecutor {
07    private final BundleContext context;
08    private final Bindings bindings;
09    private ScriptEngineManager sem;
10    
11    public static ScriptExecutor createAndInit(BundleContext context) {
12        ScriptExecutor result = new ScriptExecutor(context);
13        result.init();
14        return result;
15    }
16    
17    public void unregister() {
18        this.bindings.clear();
19        this.sem = null;
20    }
21    
22    private ScriptExecutor(BundleContext context) {
23        super();
24        assert context != null;
25        this.context = context;
26        this.bindings = new SimpleBindings();
27    }
28    
29    private void init() {
30        this.sem = new ScriptEngineManager();
31        
32        // Expose the System bundle and our BundleContext in JavaScript so that they can be used to install
33        // bundles, obtain services, etcetera. Note: can't use "context", it's taken.
34        Bundle systembundle = this.context.getBundle(0L);
35        this.bindings.put("bundleContext", this.context);
36        this.bindings.put("system", systembundle);
37        
38        // We'll revisit this in a moment.
39    
40        this.sem.setBindings(this.bindings);
41    }
42    
43    public Object executeByExtension(String extension, Reader script) throws ScriptException {
44        ScriptEngine engine = this.sem.getEngineByExtension(extension);
45        if (engine == null) {
46            throw new ScriptException("No script engine for file extension " + extension);
47        }
48        return engine.eval(script);
49    }
50    
51    // +equivalent executeByXxxxx() methods for getEngineByMimeType(String) and getEngineByName(String)
52}

With the ScriptExecutor class in place, we still need a ShellCommand to launch it from Felix’ command prompt. This command takes an URL as its only parameter. We didn’t bother handling quoted whitespace or any of that, it’s what URLEncoding is for. In addition to what’s shown here our implementation does some checks on the URL scheme (protocol) and location parts.

01package nl.sogeti.jcn.felixjavascript;
02/**
03 * A felix shell command that launches the {@link ScriptExecutor}.
04 * @author Barend Garvelink
05 */
06public class ScriptCommand implements org.apache.felix.shell.Command {
07    
08    private final static String SHELL_COMMAND = "runscript";
09    
10    private ServiceRegistration registration;
11    private ScriptExecutor scriptExecutor;
12    
13    // Constructor, static createAndRegister() method and unregister() method similar to previous example.
14    
15    @Override
16    public void execute(String line, PrintStream out, PrintStream err) {
17        // parameter parsing and exception handling left out for brevity
18        Object result = executeScriptFromURL(args[1]);
19        if (result != null) {
20            out.println(result);
21        }
22    }
23
24    public Object executeScriptFromURL(String urlString) throws MalformedURLException, IOException, ScriptException {
25        InputStream inputStream = null;
26        InputStreamReader reader = null;
27        try {
28            URL url = new URL(urlString);
29   
30            String ext = Util.determineFileExtension(urlString);
31   
32            inputStream = url.openStream();
33            reader = new InputStreamReader(inputStream);
34            
35            return this.scriptExecutor.executeByExtension(ext, reader);
36        }
37        finally {
38            Util.close(reader);
39            Util.close(inputStream);
40        }
41    }
42
43    @Override
44    public String getName() {
45        return SHELL_COMMAND;
46    }
47
48    @Override
49    public String getShortDescription() {
50        return "Executes an admin script.";
51    }
52
53    @Override
54    public String getUsage() {
55        return SHELL_COMMAND + " <URL>";
56    }
57}

The scripting support is the only auto.start bundle we added to the defaults. A single ScriptCommand instance is created by the BundleActivator and published in the service registry. Apache Felix’ Shell-TUI service automatically picks it up from there.

Populating ScriptContext

Altough the above code is all that’s strictly needed to run JavaScript in the OSGi container, accessing everything through BundleContext in JavaScript is verbose and error-prone. We can do better. Revisiting ScriptExecutor, we expose some interesting OSGi services in the script context directly. This also ensures all services are properly “ungotten” if the script terminates in error:

01package nl.sogeti.java.osgi.script;
02class ScriptExecutor {
03    
04    private void init() {
05        this.sem = // Original contents of ScriptExecutor#init() left out for brevity.
06        
07        // ...
08        
09        // Here's the bit more I promised:
10        this.cmTracker = new ServiceRepublisher<ConfigurationAdmin>(this.context, ConfigurationAdmin.class);
11        this.slTracker = new ServiceRepublisher<StartLevel>(this.context, StartLevel.class);
12        this.eaTracker = new ServiceRepublisher<EventAdmin>(this.context, EventAdmin.class);
13        this.paTracker = new ServiceRepublisher<PackageAdmin>(this.context, PackageAdmin.class);
14        this.cmTracker.open();
15        this.slTracker.open();
16        this.eaTracker.open();
17        this.paTracker.open();
18    
19        this.sem.setBindings(this.bindings);
20    }
21    
22    // Don't forget to close all those trackers in unregister()! 
23    
24    /**
25     * Monitors OSGi services of a given interface type and publishes them as a script binding using their service
26     * interface simplename. Does not currently handle multiple providers of the same service interface, behaviour
27     * is unspecified.
28     *
29     * @author Barend Garvelink
30     * @param <T> the service interface type.
31     */
32    private class ServiceRepublisher<T> extends ServiceTracker {
33        private final String key;
34        private final Class<? extends T> targetClass;
35        
36        ServiceRepublisher(BundleContext context, Class<? extends T> targetClass) {
37            super(context, targetClass.getName(), null);
38            this.targetClass = targetClass;
39            this.key = targetClass.getSimpleName();
40        }
41
42        @Override
43        public Object addingService(ServiceReference reference) {
44            Object service = super.addingService(reference);
45            assert this.targetClass.isAssignableFrom(service.getClass());
46            ScriptExecutor.this.bindings.put(this.key, service);
47            return service;
48        }
49
50        @Override
51        public void removedService(ServiceReference reference, Object service) {
52            ScriptExecutor.this.bindings.remove(this.key);
53            super.removedService(reference, service);
54        }
55    }
56}

This is a generic ServiceTracker to publish OSGi services in the script context, making each available in a global variable. The init() method has been expanded to create one of these for each of the PackageAdmin, ConfigurationAdmin, EventAdmin and StartLevel framework services. Every one of those trackers is closed and disposed of in the unregister() method (not shown).

Autolaunching a script on startup

We added a bit of code to the BundleActivator that checks for a Java system property and uses its value as a script URL. This gives us the ability to automatically launch scripts by doubleclicking a shortcut with a -Dparameter in it, finally making our installation a one-click (well, two…) process. One thing to keep in mind is that the BundleActivator#start() method runs in the framework thread that’s launching the bundle. Your bundle’s state at this time is Bundle.STARTING. This is not not the right time (and probably not the right thread) to execute an installation script, so spin a thread of your own and have it wait until your bundle state reaches Bundle.ACTIVE.

Adding utility

Finally, we created a helper object that’s put into the script context as a toolbox for all kinds of things that are easier done in Java than JavaScript.

01package nl.sogeti.java.osgi.script;
02/**
03 * An instance of this class is exposed in the script context for easy access to commonly used functions.
04 *
05 * @author Barend Garvelink
06 */
07public class ScriptUtil {
08    
09    // With OSGi's reliance on java.util.Dictionary objects, having this as a library function is incredibly convenient.
10    public Properties loadPropertiesFile(String url) throws IOException, MalformedURLException {
11        InputStream str = null;
12        BufferedInputStream binstr = null;
13        try {
14            Properties props = new Properties();
15            str = new URL(url).openStream();
16            binstr = new BufferedInputStream(str);
17            props.load(binstr);
18            return props;
19        }
20        finally {
21            Util.close(binstr);
22            Util.close(str);
23        }
24    }
25
26    // This uses a Swing dialog. You could use System.in instead.
27    public boolean confirm(String message) {
28        Component parent = null;
29        String title = "Scriptrunner";
30        int type = JOptionPane.OK_CANCEL_OPTION;
31        return (JOptionPane.OK_OPTION == JOptionPane.showConfirmDialog(parent, message, title, type));
32    }
33
34    // This uses a Swing dialog. You could use System.in instead.
35    public String prompt(String message, String defaultVal) {
36        Component parent = null;
37        String title = "Scriptrunner";
38        int type = JOptionPane.QUESTION_MESSAGE;
39        Icon icon = null;
40        Object[] choices = null;
41        return (String) JOptionPane.showInputDialog(parent, message, title, type, icon, choices, defaultVal);
42    }
43    
44    public String getSystemProperty(String key, String def) {
45        return System.getProperty(key, def);
46    }
47}

This code was written for Apache Felix, but only the ScriptCommand is specific to that implementation. The ScriptExecutor and ServiceRepublisher should run on any OSGi framework implementation that supports the JavaSE-1.6 Excecution Environment. It can be backported to earlier execution environments by invoking the script engine (Rhino or any other) directly instead of through javax.script.

Security implications

These scripts run inside the JVM with complete access to the Felix runtime. Through the magic of LiveConnect, any Java class that can be reached by the bundle classloader which hosts the ScriptExecutor is reachable by JavaScript. This includes any static variables (e.g. singletons) in those classes. In effect, the scripts have far more access to your applications internals than an admin on the command shell. This is something to keep in mind. A simple way to mitigate this is to uninstall the script executor bundle after running your installer (so that no other scripts can run) or to prompt for an admin password before executing any script (so that scripts can’t run unseen). Fancier solutions might involve Java 2 Security, only running signed code and/or whitelisting the API calls the script can make (I’m not sure if that’s feasible, it would probably be a project in its own right). In any case, use with caution or don’t use at all.

Update 2009-08-04: I came across an article describing how to sandbox Rhino JavaScript in the JVM.

Conclusion

A scripting interface is a great way to ease the deployment and administration of a complex application. This article demonstrates how such an interface was successfully added to an application based on Apache Felix and points out that this approach does come with some security considerations.


© 2020 Java Competence Network. All Rights Reserved.