Adding macro support to your Java application

Macros enable one to customize applications so that the operations they perform can be shared among users in an organization or over the Internet.

One of the most powerful uses of scripting languages is in "application automation", that is, the idea of automating a long sequence of steps such as: "select paragraph at cursor, indent 3" right, change font, ...".

Most of us will recognize this as application macros. A macro program is typically a scripting language such as VBA that calls into an application, like StarOffice or Microsoft Office to programmatically direct the application to perform a series of operations that is normally performed by a user, such as formatting a paragraph.

Macros are important as they allow users to customize an application. And because macros are essentially programs, the operations that they perform can be shared among users in an organization or over the Internet.

Applications that can be controlled by script languages are said to be scriptable.

EXECUTING MACRO IN A HOST APPLICATION

One way of scripting an application is to run the macro script inside the application. There are several ways macros can be executed in an application.

Explicitly Invoking a Macro: You start an application like StarOffice to load a text document. You then go to the 'Macros' menu and select a macro to be applied to a text document that is currently opened. Visually, you have a macro running inside the host application.

We can implement macros in Java SE6 with JSR-223. Let us assume that we have a simple text editor like so:

public class MyTextEditor extends JFrame {
   private JTextArea textArea = null;
   //Setup editor, not shown
   ...
   public String getText() {
      return (textArea.getText());
   }
   public void setText(String s) {
      textArea.setText(s);
   }
   ... 

When adding macro support to your application you need to consider what functions you want the macros to perform. In the simple MyTextEditor above, I have indicated only one function--to be able to read and write content to the text editor. This is defined by a pair of JavaBean text property.

The next step is to expose the application object, MyTextEditor in this case, into the script and allow the script to control the application object. The following code shows how to do this:

MyTextEditor editor = ...
    //Get an instance of JavaScript engine
   ScriptEngineManager manager = new ScriptEngineManager();
   ScriptEngine engine = manager.getEngineByName("JavaScript");

   //Export application object as text_editor
   engine.put("text_editor", editor);
   //Execute the macro
   try   {
      engine.eval(new FileReader("macro.js"));
   } catch (Exception e) {   ...

The simple editor does not have a function to check and set to uppercase, the first letter after a period. We can now very easily add this function by writing macro.js:

//Get the content from the editor
   var txt = text_editor.text;
   //Search and convert all word after a fullstop to uppercase
   var result = txt.replace(/\.\s*./g, function(e) { return
  (e.toUpperCase()) });
   //Set the result back into the editor
   text_editor.text = result;

Application Events that Trigger a Macro:

You can also define macros that respond to certain events in your application. For example, when a user sends a document to the printer, your application might run a macro before and after printing. The "before printing" macro might run a spell check; if there are spelling mistakes the function returns a false to indicate that the print job should not go ahead. The "after printing" macro may send an e-mail to notify the user. Other application events may include opening a document, capturing keystrokes, etc.

The following code snippet shows how such event macros may be implemented with the Invocable interface. What we do is to define specific methods called by the host application. For our printing example, we define two specific methods named "beforePrinting" and "afterPrinting"; macros related to the before printing event implement the former, and for after printing, the latter method.

...
   //Export application object as text_editor
   engine.put("text_editor", editor);
   //Evaluate the events
   try {
      engine.eval(new FileReader("printEvents.js"));
      Invocable inv = (Invocable)engine;
      Boolean b = (Boolean)inv.invokeFunction("beforePrinting", null);
      if (!b.booleanValue()) {
         //The beforePrinting has indicated for us to stop but  
         //give the user an opportunity to override it
         int result = JOptionPane.showConfirmDialog(null, "Proceed?", 
               "Proceed", JOptionPane.YES_NO_OPTION);
         if (result != JOptionPane.YES_OPTION)
            return;
      }
      //Print document
      ...      inv.invokeFunction("afterPrinting", null);
   } catch (Exception e) {

And here is an example of how the beforePrinting function is written:


importPackage(javax.swing) function beforePrinting() { var txt = text_editor.text; if (hasSpellingMistakes(txt)) { JOptionPane.showMessageDialog(null, "There are spelling errors in document", "Spelling errors", JOptionPane.WARNING_MESSAGE); return (false); } return (true); }

Application Functions as Macros: Another way of executing macros is to "attach" a macro to, say, a button on the text editor. When the button is pressed, then the macro is executed. One cool feature in JSR-223 is to allow scripts to implement Java interfaces. For example, we wish to implement the ActionListener interface. What you have to do is to write the methods with method names that match the interface. For ActionListener, we do the following:

function actionPerformed(e) {
      //Do something here...

In the Java application we can request for an implementation to an interface from a script; the following code snippet demonstrates how we request an ActionListener implementation:

//Evaluate the script
   try {
      engine.eval(new FileReader("scriptInterface.js"));
      Invocable inv = (Invocable)engine;
      ActionListener listener = inv.getInterface
                                (ActionListener.class);
      //macroButton is a JButton on the text editor
      if (listener != null) 
         macroButton.addActionListener(listener);   } catch (Exception e) {

Now whenever macroButton is pressed, it will cast to the implementation defined in scriptInterface.js.

EXECUTING MACROS OUTSIDE AN APPLICATION

Another way of running a macro is to have the macro call an application to perform an operation on its behalf. For example, you have many text documents that you wish to convert to PDF format. StarOffice has a PDF converter. Since StarOffice is scriptable, you can write a macro to start StarOffice, then feed the documents into StarOffice for the PDF conversion. It is important to realize that here, it is the macro that starts the host application.

In JSR-223, all Java classes are accessible to scripts, provided the right imports are specified. Let's look at the following example:

   var editor = new MyTextEditor();
   editor.text = new Date() + "\n";
   editor.visible = true;

As you can see from the above example, we wrote a script to start the editor and populate the new document with the current time and date. The above script can be run with jrunscript. One way to increase the "scriptablity" of your application is to partition your application so that the functions in your application are "standalone". For example, if the text editor has a formating feature, your design is that you can just use the format feature without starting the entire application. In the following example,

   var editor = new MyTextEditor();
   var formatter = editor.textFormatter;
   formatter.format(some_text);

We first have to create a text editor, before we can access the text formatter feature in the editor. Ideally, we should be able to access the formatter without having to start the application like so:

   var formatter = new TextFormatter();
   formatter.format(some_text);

Although our text editor is a contrived example, some application startups are quite involved.

Making an application scriptable adds a tremendous amount of flexibility to your IT. It opens up your application to user customization and third party developers.

Lee Chuk-Munn is a staff engineer at Sun Microsystems.