Embedding DSLs in Java using JRuby
By Mario Aquino, OCI Principal Software Engineer
January 2007
Introduction
Software in the hands of a knowledgeable user should be empowering. Applications can act as the very extension of their user's will; performing tasks, solving problems, and generally being helpful. However, no matter how well-intentioned a program may be, the user interaction can either be fluid and intuitive or haphazard and clumsy. The best user interfaces are the simple ones, where the user is neither constrained nor overwhelmed by the choices to be made to properly express a need. One trick is to provide a way for the user to communicate with the system using a language the user is comfortable with, shifting the burden of learning how to respond from the user to the system. This article will describe how to introduce a mini-language of commands for interacting with a program (rather than components like radio buttons and combo boxes) using Java, the new Java 6 Scripting API, and JRuby.
JRuby
JRuby is an open-source Java implementation of the Ruby programming language. Like Java, Ruby is an object-oriented programming language (though, technically Java is not a fully OO language because primitive values like int
and float
are not objects, unlike in Ruby where everything is an object). Another difference between Ruby and Java is that Java is a compiled, statically-typed language while Ruby is a dynamically-typed, runtime-interpreted language (what some refer to as a "scripting" language).
The Java 6 Scripting API
Among the useful features of the recently released Java 6 Standard Edition is a new API for integrating "scripting" languages with Java applications. This API is akin to the Bean Scripting Framework, in that it allows objects to be passed to and back from non-Java language contexts running in the VM.
To interact with a scripting resource, a Java class can call the Java 6 Scripting API, which loads a scripting engine for the desired language. The engine interfaces with the language implementation.
There is a long list of supported languages for the Java 6 scripting API including: Beanshell, Groovy, JavaScript, Python (via Jython), and Ruby (via JRuby). Engines for all languages supported by the Java 6 Scripting API can be downloaded from https://scripting.java.net.
Using the API is very simple. Here is some sample code to demonstrate:
- import java.io.InputStream;
- import java.io.InputStreamReader;
-
- import javax.script.ScriptEngine;
- import javax.script.ScriptEngineManager;
- import javax.script.ScriptException;
-
- public class ScriptingDemo {
-
- public static void main(String[] args) throws ScriptException {
- ScriptEngineManager manager = new ScriptEngineManager();
- ScriptEngine engine = manager.getEngineByName("ruby");
- engine.put("message", "Hello, world!");
- InputStream resource = ScriptingDemo.class.getClassLoader().getResourceAsStream("simple_demo.rb");
- engine.eval(new InputStreamReader(resource));
- }
- }
The Java code creates a ScriptEngineManager
and uses it to retrieve an engine for the Ruby programming language. The script engine JAR file for Ruby as well as the JRuby JAR file must be in the classpath for this to work. The Java code then adds a key/value pair to the engine to be retrieved by the Ruby source file. Finally, the Ruby source file is loaded and executed.
Here is the Ruby code:
- require 'java'
-
- def show(message)
- # Passing in nil for dialog parent so it will show by itself
- javax.swing.JOptionPane.showMessageDialog(nil, message)
- end
-
- show($message)
This code uses a facility provided by JRuby to allow Ruby code to execute Java code. It passes the message (the value of the $message
global variable set in the ScriptingDemo
Java class) to the show()
method, which pops up a JOptionPane
.
It's worth noting that there is no parameter type checking going on in the Ruby code; the show()
method accepts a parameter and passes it on to the JOptionPane.
Domain-Specific Languages (DSLs)
DSLs are a very powerful idea. A lot has been written about DSLs (see the References section for links) so this article will limit the scope of discussion to the use of DSLs as a means for facilitating user interaction. They represent a way to drive an application using terminology and concepts that originate from the problem domain. You may use DSLs everyday and not even realize it. For example, the text interface to http://local.google.com that allows you to find points of interest near you by typing "pizza near 12140 Woodcrest Executive Drive" is an example of a domain-specific language. Though there can be programming symbols and technical syntax introduced in DSLs, the most "user friendly" ones are simple and straight-forward but still very powerful.
DSLs ought to be free text, though they should have a well-defined syntax (the "L" in DSL is language, after all, and all languages have a syntax). The challenge for adding a DSL to a Java application is how interpret and then respond to the user's instruction. One approach is to write (or generate) a lexical analyzer to parse a user's input, map keywords to methods, call the methods, and then return the results to the user. Another approach is to write Ruby code that can respond to the users input directly, treating the text as a series of method calls.
DSL for Rich Text Editing
A simple rich text editor has a place to put text and controls for marking up the content. For this article, the controls for marking up the text will be a single text input field and a button to execute a command expression. The full source code for the sample editor is available for download. As well, the application itself can be run directly using Java WebStart, though you will have to have the Java 6 runtime installed (see the References section for links).
The editor needs to be able to highlight or clear the markup (remove the highlighting) of text content. The DSL should support expressions like "highlight each sentence" or "highlight the last word of the first sentence" or "clear every sentence" or variations of those combinations. With these simple expressions, we can take advantage of the Ruby parser to convert the plain text instructions from the user into method calls to retrieve the user's text and then apply markup to it.
The expression "highlight the first sentence" can be thought of as a series of chained method calls: highlight( the( first( sentence() ) ) )
. Here, the sentence()
method returns its value to the first()
method, which uses the value returned by sentence()
and does something with it, then returns some value to its caller (the the()
method). One of the things we are taking advantage of here is Ruby's convenient syntax which makes the presence of parenthesis denoting method calls optional. All of the methods for determining the boundaries of what the user intends to highlight are at the end of the expression, leaving the action keyword (for highlighting or clearing the markup) at the beginning.
The Java code for this editor is trivial. Its responsibilities are creating two JTextPane
s and arranging them in the frame. Both of these are passed to a Ruby script that will parse the input text and apply whatever markup instructions have been supplied by the user. The script is invoked in response to the "Evaluate" button being pressed.
The following is the portion of the Ruby code (from a file called swing_ui.rb
) that handles calls to markup text from the uppermost textpane:
- require 'java'
- require 'text_dsl'
- include_class 'java.awt.Color'
- include_class 'java.awt.event.ActionEvent'
-
- class UI < Text
-
- def initialize(component)
- # replace double spaces with single spaces to simplify text parsing
- text = component.text.gsub(/ /, ' ')
- super(text)
- @component = component
- @component.text = text
- end
-
- # expects an array of arrays of integer values representing the
- # ranges of text content needing to be highlighted in Yellow
- # background
- def highlight(ranges)
- markup(ranges, 'Yellow', Color::yellow)
- end
-
- # expects an array of arrays of integer values representing the
- # ranges of text content needing have their background color set
- # to White
- def clear(ranges)
- markup(ranges, 'White', Color::white)
- end
-
- private
-
- # ranges is expected to be an array of arrays of integer values,
- # where the inner-most array has two values: the starting point
- # of the text that needs to be marked up and the length of text
- # to apply the markup to
- # label should be a string and color is expected to be a
- # java.awt.Color object
- def markup(ranges, label, color)
- background = com.ociweb.BackgroundAction.new(label, color)
- ranges.each do |range|
- @component.select(range[0], range[0] + range[1])
- event = ActionEvent.new(@component, ActionEvent::ACTION_PERFORMED, 'apply markup')
- background.actionPerformed(event)
- end
- @component.select(0,0)
- end
- end
-
- UI.new($textpane).instance_eval($dslpane.text) unless $textpane.text.empty? or $dslpane.text.empty?
From the JRuby point of view, this code calls require 'java'
to make all the Java integration facilities available to the script. The require
keyword is somewhat similar to the import
keyword in Java; it is a signal to load resources from a file whose name follows the keyword. Below the 'require' directives, the code uses a JRuby mechanism, include_class
, to make Color
and ActionEvent
Java classes "visible" for the script to use. Both the highlight()
and clear()
methods defer to a common method (markup()
) to issue requests for marking up the background of the ranges of text supplied as a parameter to the call.
The last line of the file passes references to the main text content textpane and the DSL textpane (which it expects to find as the global variables $textpane
and $dslpane
) to an instance of the Ruby class defined in this file. The call to instance_eval()
is where the magic happens; this method accepts a string and attempts to execute it as Ruby code. This is a very powerful capability that Ruby (and many other dynamic languages) can provide: the capacity to define and execute arbitrary code at runtime.
The remainder of the Ruby code comes from a Ruby file (text_dsl.rb
) which provides the methods that parse the input content and setup arrays of integers representing the boundaries of text to which the user intends to apply markup.
- class DSL
-
- # Adds methods to the current class that return their arguments
- # on to their callers. Useful for adding non-processing methods
- # to the DSL that improve the readability of the syntax
- # This idea is described in Jay Fields blog (http://jayfields.blogspot.com)
- def self.bubble(*methods)
- methods.each do |method|
- define_method(method) { |args| args }
- end
- end
- end
-
- class Text < DSL
- # Adding these methods allows the DSL to be more English-like.
- bubble :each, :every, :of, :the
-
- def initialize(text)
- @text = text
- end
-
- # This returns an array containing an element for each first item
- # (word or sentence) that is found.
- # Each element is an array containing two integers.
- # The first is the index where the item begins
- # and the second is the length of the item.
- def first(sentences=nil)
- end_point :first, sentences
- end
-
- # This returns an array containing an element for each last item
- # (word or sentence) that is found.
- # Each element is an array containing two integers.
- # The first is the index where the item begins
- # and the second is the length of the item.
- def last(sentences=nil)
- end_point :last, sentences
- end
-
- # called in response to a keyword not defined
- # as a method for this class
- def method_missing(method_id, *args)
- raise "don't understand #{method_id}"
- end
-
- # This returns an array containing an element
- # for each sentence that is found.
- # Each element is an array containing two integers.
- # The first is the index where the item begins
- # and the second is the length of the item.
- # Returns an array of sentence strings.
- def sentence
- raise "invalid use of sentence" unless @last_token.nil?
-
- @last_token = @last_unit = :sentence
-
- arr = []
- cursor = 0
-
- @text.split(/\.|\?|!/).each do |sentence|
- sentence.lstrip! # removes leading whitespace
- len = sentence.length + 1 # +1 for punctuation
- arr << [cursor, len]
- cursor += len
- cursor += 1 # for space between sentences
- end
-
- arr
- end
-
- # The parameter is an array of arrays.
- # The inner elements can be integer pairs (index and length)
- # or words in a sentence.
- # Returns an array with an element for each sentence in the text.
- # Each element is an array of the words in the corresponding sentence.
- def word(arg=nil)
- raise "invalid use of word" unless arg
- unless [nil, :sentence].include?(@last_unit)
- raise "invalid use of word"
- end
-
- @last_token = @last_unit = :word
-
- all_words = []
-
- # Start cursor at index of first word in first sentence passed in.
- cursor = arg[0][0]
-
- arg.each do |sentence|
- if sentence.instance_of? Array then
- index, length = *sentence
- sentence = @text[index, length]
- end
-
- all_words << split_by_words(cursor, sentence)
- cursor += sentence.length
- cursor += 1 # for space between sentences
- end
-
- all_words
- end
-
- private
-
- def split_by_words(cursor, sentence)
- words = sentence.split(' ')
-
- # Remove punctuation from last word in sentence.
- words[-1] = words[-1][0..-2]
-
- arr = []
-
- words.each do |word|
- arr << [cursor, word.length]
- cursor += word.length
- cursor += 1 # for space between words
- end
-
- arr
- end
-
- def end_point(position, sentences=nil)
- raise "invalid use of #{position}" unless sentences
- unless [:sentence, :word].include?(@last_token)
- raise "invalid use of #{position}"
- end
-
- @last_token = position
-
- case @last_unit
- when :sentence
- [sentences.send(position)]
- when :word
- words = []
- sentences.each { |sentence| words << sentence.send(position) }
- words
- else
- nil # should never happen
- end
- end
- end
Summary
According to the imagination of science fiction writers, in the future you will be able to express your need to the "Computer" by raising your voice and barking out orders. If we are ever going to get there, software developers will have to shift away from things like buttons and checkboxes toward more elementary means of human-computer interaction. This article has demonstrated how to introduce an interpretive textual interface to execute the system's capabilities. While input for the sample application came from the keyboard, it wouldn't be difficult to bolt on a speech-to-text engine to convert voice requests into DSL expression input. That sounds pretty good, doesn't it?
Mario Aquino would like to thank Mark Volkmann for all his help in getting the sample application for this article working. He also thanks Jeff Brown for reviewing the article and Matz for creating a great language!
References
- [1] Java WebStart sample application and source code for this article
- [2] Java 6 Scripting Reference (API and Programmers Guide)
- [3] Scripting Engines for JSR-223
- [4] JRuby - Pure Java implementation of the Ruby programming language
- [5] Domain Specific Language - Martin Fowler
- [6] The Pragmatic Programmer - Andy Hunt & Dave Thomas (sections on Domain Languages and Metaprogramming)
- [7] Executing an internal DSL in multiple contexts - Jay Fields