Invoke dynamic languages dynamically, Part 2: Find, execute, and change scripts at run time

Alter business logic on the fly

The Java™ scripting API added in Java SE 6 and backward compatibility with Java SE 5 allows dozens of scripting languages to be called at run time from a Java application in a simple, unified way. Part 1 of this two-part article introduces the API's basic features. Part 2 exposes more of its power, demonstrating how external scripts written in Ruby, Groovy, and JavaScript can be executed and altered at run time to change business logic without stopping and restarting the application.

Share:

Tom McQueeney (tom.mcqueeney@gmail.com), Lead Technical Consultant, Idea Integration

Tom McQueeneyTom McQueeney is a Java developer and application architect for Idea Integration, a national consulting firm. He enjoys integrating dynamic languages such as Ruby and Groovy into Java projects to make development faster, more efficient, and more fun. He is a past speaker at O'Reilly's OSCON and ApacheCon Europe, and he formerly served as president of the Denver Java Users Group. He and his wife, also a certified Java architect, live in Washington, D.C.



11 September 2007

Also available in Chinese Russian Japanese

The Java scripting API added to Java SE 6 provides a unified way to run — and share code and data with — external programs written in a variety of dynamic languages. Augmenting a Java application with the power and flexibility of a scripting language is especially useful when the scripting language can perform a job more clearly, simply, or concisely. But the Java scripting API doesn't just allow dozens of scripting languages to be added to a Java program in a unified way: It also allows scripts to be located, read, and executed at run time. These dynamic capabilities let you alter scripts to change an application's logic while the program is running. This article demonstrates how you can use the Java scripting API to invoke external scripts to alter a program's logic dynamically. It also looks at issues you might face when integrating one or more scripting languages into a Java application.

Part 1 introduces the Java scripting API using a Hello World-style application. The more realistic sample application I'll present here uses the scripting API to create a dynamic rules engine that defines the rules as external scripts written in Groovy, JavaScript, and Ruby. The rules decide whether applicants for a home loan qualify for particular mortgage products. Defining the business rules with a scripting language makes the rules easier to write and probably easier for nonprogrammers, such as loan officers, to read. Externalizing the rules with the Java scripting API also allows the rules to change and new mortgage products to be added, while the application is running.

Hello, real world

The sample application processes home loan applications for the fictitious company Shaky Ground Financial. The home mortgage industry constantly invents new loan products and changes the rules on who qualifies for them. Shaky Ground wants not only to add and remove available mortgage products quickly but also to make quick changes to the business rules that determine who qualifies for each product.

The Java scripting API comes to the rescue. The application consists of a ScriptMortgageQualifier class responsible for determining whether a borrower trying to buy a particular property qualifies for a given mortgage loan product. The class is shown in Listing 1.

Listing 1. ScriptMortgageQualifier class
// Imports and Javadoc not shown.
public class ScriptMortgageQualifier {
    private ScriptEngineManager scriptEngineManager = new ScriptEngineManager();

    public MortgageQualificationResult qualifyMortgage(
        Borrower borrower,
        Property property,
        Loan loan,
        File mortgageRulesFile
    ) throws FileNotFoundException, IllegalArgumentException, ScriptException
    {
        ScriptEngine scriptEngine = getEngineForFile(mortgageRulesFile);
        if (scriptEngine == null) {
            throw new IllegalArgumentException(
                "No script engine on classpath to handle file: " + mortgageRulesFile
            );
        }

        // Make params accessible to scripts by adding to engine's context.
        scriptEngine.put("borrower", borrower);
        scriptEngine.put("property", property);
        scriptEngine.put("loan", loan);

        // Make return-value object available to scripts.
        MortgageQualificationResult scriptResult = new MortgageQualificationResult();
        scriptEngine.put("result", scriptResult);

        // Add an object scripts can call to exit early from processing.
        scriptEngine.put("scriptExit", new ScriptEarlyExit());

        try {
            scriptEngine.eval(new FileReader(mortgageRulesFile));
        } catch (ScriptException se) {
            // Re-throw exception unless it's our early-exit exception.
            if (se.getMessage() == null ||
                !se.getMessage().contains("ScriptEarlyExitException")
            ) {
                throw se;
            }
            // Set script result message if early-exit exception embedded.
            Throwable t = se.getCause();
            while (t != null) {
                if (t instanceof ScriptEarlyExitException) {
                    scriptResult.setMessage(t.getMessage());
                    break;
                }
                t = t.getCause();
            }
        }

        return scriptResult;
    }

    /** Returns a script engine based on the extension of the given file. */
    private ScriptEngine getEngineForFile(File f) {
        String fileExtension = getFileExtension(f);
        return scriptEngineManager.getEngineByExtension(fileExtension);
    }

    /** Returns the file's extension, or "" if the file has no extension */
    private String getFileExtension(File file) {
        String scriptName = file.getName();
        int dotIndex = scriptName.lastIndexOf('.');

        if (dotIndex != -1) {
            return scriptName.substring(dotIndex + 1);
        } else {
            return "";
        }
    }

    /** Internal exception so ScriptEarlyExit.exit can exit scripts early */
    private static class ScriptEarlyExitException extends Exception {
        public ScriptEarlyExitException(String msg) {
            super(msg);
        }
    }

    /** Object passed to all scripts so they can indicate an early exit. */
    private static class ScriptEarlyExit {
        public void noMessage() throws ScriptEarlyExitException {
            throw new ScriptEarlyExitException(null);
        }
        public void withMessage(String msg) throws ScriptEarlyExitException {
            throw new ScriptEarlyExitException(msg);
        }
    }
}

This class is relatively small because it delegates all business decisions to external scripts. Each script represents one mortgage product. The code in each script file contains the business rules to define the type of borrower, property, or loan acceptable to qualify for that mortgage product. That way, new mortgage products can be added by copying a new script file into the script product directory. If the business rules change on who qualifies for a particular mortgage product, the script can be updated to reflect those rules.

Writing the mortgage product business rules in scripting languages shows off the capabilities of the Java scripting API. It also shows that a scripting language can sometimes be easier to read, modify, and understand — even for a nonprogrammer.


How the ScriptMortgageQualifier class works

The primary method in the ScriptMortgageQualifier class is qualifyMortgage(). This method receives as parameters:

  • The borrower
  • The property to purchase
  • The loan details
  • A File object containing the script to be executed

The method's job is to run the script file with the business-entity parameters and return a results object that indicates whether the borrower qualifies for the mortgage product. The code for Borrower, Property, and Loan is not shown here. They are simple entity classes, available in this article's source code (see Download).

To find a ScriptEngine to run the script file, the qualifyMortgage() method uses the getEngineForFile() internal helper method. The getEngineForFile() method uses the scriptEngineManager instance variable — initialized on class construction with a ScriptEngineManager— to find a script engine capable of processing a script with the file's given extension. The getEngineForFile() method uses the ScriptEngineManager.getEngineByExtension() method, shown in bold in Listing 1, to search for and return the ScriptEngine.

Equipped with a script engine, qualifyMortgage() binds its incoming entity parameters to the engine's context to make them available to the script. The first three scriptEngine.put() calls, also shown in bold, perform these bindings. The fourth call to scriptEngine.put() creates a new MortgageQualificationResult Java object and shares it with the script engine. This shared object, which qualifyMortgage() returns, allows the script to communicate its results back to the Java application by setting properties on the object. Scripts access this Java object using the result global variable. Each script is responsible for communicating its results back to the Java program using this shared script object.

The final call to scriptEngine.put() makes an instance of an internal helper class —ScriptEarlyExit, also shown in Listing 1— available to scripts under the scriptExit variable name. ScriptEarlyExit defines two simple methods —withMessage() and noMessage()— whose only duties are to throw an exception. If a script calls scriptExit.withMessage() or scriptExit.noMessage(), the method throws a ScriptEarlyExitException. The script engine catches that exception, effectively halting script processing, and throws a ScriptException to the eval() method that invoked the script.

This roundabout way of prematurely exiting a script provides a consistent way to return from script processing outside of a function or method. Not all scripting languages provide a statement for this purpose. In JavaScript, for example, you can't use a return statement when executing top-level code, which is how the mortgage-processing scripts in this example application are structured. The scriptExit shared object fills this gap, letting a script in any language exit as soon as the script determines the borrower does not qualify for the mortgage product.

The call in qualifyMortgage to the script engine's eval method, shown in bold, uses a try/catch block to catch the ScriptException. By examining the ScriptException error message, the code in the catch block determines whether the script exception is caused by the ScriptEarlyExitException or by a real script error. If the error message contains the ScriptEarlyExitException name, the code assumes everything went as planned and ignores the script exception.

This technique of searching the Java scripting API's script exception error message for a literal string is kludgy, but it works for the Groovy, JavaScript, and Ruby language interpreters used in this example. It would be better if all scripting-language implementations added Java exceptions thrown from invoked Java code into the exception stack, retrievable by using the Throwable.getCause() method. Interpreters like JRuby and Groovy do, but the built-in Rhino JavaScript interpreter does not.


Running the code: ScriptMortgageQualifierRunner

To test the ScriptMortgageQualifier class, you'll use test data to represent four sample borrowers, a sample property the borrowers will try to purchase, and a sample mortgage loan. You'll run a sample borrower, along with the property and loan, against each of the three scripts to see if that borrower meets the business rules for that script's representative mortgage product.

Listing 2 shows the partial ScriptMortgageQualifierRunner program you'll use to create these test objects, find script files in a directory, and run them through the ScriptMortgageQualifier class from Listing 1. The program's createGoodBorrower(), createAverageBorrower(), createInvestorBorrower(), createRiskyBorrower(), createProperty(), and createLoan() helper methods are not shown to save space. They just create the entity objects and set appropriate values for testing. The full source code with all methods is available (see Download).

Listing 2. ScriptMortgageQualifierRunner program
// Imports and some helper methods not shown.
public class ScriptMortgageQualifierRunner {
    private static File scriptDirectory;
    private static Borrower goodBorrower = createGoodBorrower();
    private static Borrower averageBorrower = createAverageBorrower();
    private static Borrower investorBorrower = createInvestorBorrower();
    private static Borrower riskyBorrower = createRiskyBorrower();
    private static Property property = createProperty();
    private static Loan loan = createLoan();

    /**
     * Main method to create a File for the directory name on the command line,
     * then call the run method if that directory exists.
     */
    public static void main(String[] args) {
        if (args.length > 0 && args[0].contains("-help")) {
            printUsageAndExit();
        }
        String dirName;
        if (args.length == 0) {
            dirName = "."; // Current directory.
        } else {
            dirName = args[0];
        }

        scriptDirectory = new File(dirName);
        if (!scriptDirectory.exists() || !scriptDirectory.isDirectory()) {
            printUsageAndExit();
        }

        run();
    }

    /**
     * Determines mortgage loan-qualification status for four test borrowers by
     * processing all script files in the given directory. Each script will determine
     * whether the given borrower is qualified for a particular mortgage type
     */
    public static void run() {
        ScriptMortgageQualifier mortgageQualifier = new ScriptMortgageQualifier();

        for(;;) { // Requires Ctrl-C to exit
            runQualifications(mortgageQualifier, goodBorrower, loan, property);
            runQualifications(mortgageQualifier, averageBorrower, loan, property);

            loan.setDownPayment(30000.0); // Reduce down payment to 10%
            runQualifications(mortgageQualifier, investorBorrower, loan, property);

            loan.setDownPayment(10000.0); // Reduce down payment to 3 1/3%
            runQualifications(mortgageQualifier, riskyBorrower, loan, property);

            waitOneMinute();
        }
    }

    /**
     * Reads all script files in the scriptDirectory and runs them with this borrower's
     * information to see if he/she qualifies for each mortgage product.
     */
    private static void runQualifications(
        ScriptMortgageQualifier mortgageQualifier,
        Borrower borrower,
        Loan loan,
        Property property
    ) {
        for (File scriptFile : getScriptFiles(scriptDirectory)) {
            // Print info about the borrower, loan and property.
            System.out.println("Processing file: " + scriptFile.getName());
            System.out.println("  Borrower: " + borrower.getName());
            System.out.println("  Credit score: " + borrower.getCreditScore());
            System.out.println("  Sales price: " + property.getSalesPrice());
            System.out.println("  Down payment: " + loan.getDownPayment());

            MortgageQualificationResult result = null;
            try {
                // Run the script rules for this borrower on the loan product.
                result = mortgageQualifier.qualifyMortgage(
                    borrower, property, loan, scriptFile
                );
            } catch (FileNotFoundException fnfe) {
                System.out.println(
                    "Can't read script file: " + fnfe.getMessage()
                );
            } catch (IllegalArgumentException e) {
                System.out.println(
                    "No script engine available to handle file: " +
                    scriptFile.getName()
                );
            } catch (ScriptException e) {
                System.out.println(
                    "Script '" + scriptFile.getName() +
                    "' encountered an error: " + e.getMessage()
                );
            }

            if (result == null) continue; // Must have hit exception.

            // Print results.
            System.out.println(
                "* Mortgage product: " + result.getProductName() +
                ", Qualified? " + result.isQualified() +
                "\n* Interest rate: " + result.getInterestRate() +
                "\n* Message: " + result.getMessage()
            );
            System.out.println();
        }
    }

    /** Returns files with a '.' other than as the first or last character. */
    private static File[] getScriptFiles(File directory) {
        return directory.listFiles(new FilenameFilter() {
            public boolean accept(File dir, String name) {
                int indexOfDot = name.indexOf('.');
                // Ignore files w/o a dot, or with dot as first or last char.
                if (indexOfDot < 1 || indexOfDot == (name.length() - 1)) {
                    return false;
                } else {
                    return true;
                }
            }
        });
    }

    private static void waitOneMinute() {
        System.out.println(
            "\nSleeping for one minute before reprocessing files." +
            "\nUse Ctrl-C to exit..."
        );
        System.out.flush();
        try {
            Thread.sleep(1000 * 60);
        } catch (InterruptedException e) {
            System.exit(1);
        }
    }
}

The main() method in ScriptMortgageQualifierRunner finds the directory on the command line from which to read the script files, sets a static variable with the directory's File object if the directory exists, and calls the run() method to perform further processing.

The run() method instantiates the ScriptMortgageQualifier class from Listing 1, then uses an endless loop to call the internal runQualifications() method with four borrower/loan scenarios. The endless loop simulates continuous live processing of mortgage applications. The loop lets you add or change the script files (mortgage loan products) in the directory being processed, with those changes taking effect dynamically without stopping the application. The fact that the application's business logic lives in external scripts provides this dynamic ability to change business logic at run time.

The runQualifications() helper method makes the actual calls to ScriptMortgageQualifer.qualifyMortgage, one call for each script file found in the script directory. Each call is preceded by print statements to describe the script file and borrower being processed and followed by more print statements to show whether the borrower qualified for each mortgage product. Qualification results are determined by attributes of the shared MortgageQualificationResult Java object, which the scripting code uses to return its results.

The source code ZIP file for this article contains three sample script files written in Groovy, JavaScript, and Ruby. Each represents a different type of standard 30-year mortgage loan product with a fixed interest rate. Code in the scripts determines whether the borrower qualifies for its mortgage type and returns its results by calling methods on the shared result global variable supplied in the script engine put() method I described earlier. The result global variable is an instance of the MortgageQualificationResult class, partially shown in Listing 3.

Listing 3. MortgageQualificationResult results class
public class MortgageQualificationResult {
    private boolean qualified;
    private double interestRate;
    private String message;
    private String productName;

    // .. Standard setters and getters not shown.
}

The scripts set the result properties shown in Listing 3 to return whether the borrower qualifies for the mortgage and at what interest rate. The message and productName properties allow the script to set the reason the borrower fails to qualify for the mortgage and to return the relevant product name.


The script files

Before I show the output from running ScriptMortgageQualifierRunner, let's look at the Groovy, JavaScript, and Ruby script files the program runs. The business logic in the Groovy script defines a mortgage product that is relatively easy to qualify for, but comes with a higher interest rate to account for the higher financial risk. The JavaScript script represents a government-sponsored mortgage loan that has a maximum-income and other restrictions borrowers must meet. The mortgage product in the Ruby script contains business rules that restrict qualification to borrowers with good credit histories who can make a substantial down payment. It rewards them with a lower interest rate.

Listing 4 shows the Groovy script, which you can probably read even if you don't know Groovy.

Listing 4. Groovy mortgage script
/*
   This Groovy script defines the "Groovy Mortgage" product.
   This product is relaxed in its requirements of borrowers.
   There is a higher interest rate to make up for the looser standard.
   All borrowers will be approved if their credit history is good, they can
   make a down payment of at least 5%, and they either earn more than
   $2,000/month or have a net worth (assets minus liabilities) of $25,000.
*/

// Our product name.
result.productName = 'Groovy Mortgage'

//  Check for the minimum income and net worth
def netWorth = borrower.totalAssets - borrower.totalLiabilities
if (borrower.monthlyIncome < 2000 && netWorth < 25000) {
    scriptExit.withMessage "Low monthly income of ${borrower.monthlyIncome}" +
       ' requires a net worth of at least $25,000.'
}

def downPaymentPercent = loan.downPayment / property.salesPrice * 100
if (downPaymentPercent < 5) {
    scriptExit.withMessage 'Down payment of ' +
        "${String.format('%1$.2f', downPaymentPercent)}% is insufficient." +
        ' 5% minimum required.'
}
if (borrower.creditScore < 600) {
    scriptExit.withMessage 'Credit score of 600 required.'
}

// Everyone else qualifies. Find interest rate based on down payment percent.
result.qualified = true
result.message = 'Groovy! You qualify.'

switch (downPaymentPercent) {
    case 0..5:   result.interestRate = 0.08; break
    case 6..10:  result.interestRate = 0.075; break
    case 11..15: result.interestRate = 0.07; break
    case 16..20: result.interestRate = 0.065; break
    default:     result.interestRate = 0.06; break
}

Note the global result, borrower, loan, and property variables the script uses to access and set values in the shared Java objects. These are the variable names set by the calls to the ScriptEngine.put() method.

Note also Groovy statements like result.productName = 'Groovy Mortgage'. This appears to set the MortgageQualificationResult object's productName string property directly, even though Listing 3 clearly shows it to be a private instance variable. This is not the Java scripting API sneakily allowing a violation of encapsulation. Instead, Groovy and most other scripting language interpreters you can use with the Java scripting API cooperate well with shared Java objects. If a Groovy statement tries to set or read a private property value for a Java object, Groovy looks for and uses a JavaBean-like public setter or getter method if one is defined. For example, the result.productName = 'Groovy Mortgage' statement is translated automatically to the expected Java statement: result.setProductName("Groovy Mortgage"). This equivalent Java setter statement is also valid Groovy and would have worked just fine in the script, but using the property-assignment statement directly is Groovier.

Now look at the JavaScript mortgage product in Listing 5. The JavaScript mortgage is intended to represent a government-sponsored loan to encourage home ownership. Business rules thus require that this is the borrower's first home purchase and that the borrower intends to live in the home, rather than rent it out for income.

Listing 5. JavaScript mortgage script
/**
 * This script defines the "JavaScript FirstTime Mortgage" product.
 * It is a government-sponsored mortgage intended for low-income, first-time
 * home buyers without a lot of assets who intend to live in the home.
 * Bankruptcies and bad (but not terrible!) credit are OK.
 */
result.productName = 'JavaScript FirstTime Mortgage'

if (!borrower.intendsToOccupy) {
    result.message = 'This mortgage is not intended for investors.'
    scriptExit.noMessage()
}
if (!borrower.firstTimeBuyer) {
    result.message = 'Only first-time home buyers qualify for this mortgage.'
    scriptExit.noMessage()
}
if (borrower.monthlyIncome > 4000) {
    result.message = 'Monthly salary of $' + borrower.monthlyIncome +
        ' exceeds the $4,000 maximum.'
    scriptExit.noMessage()
}
if (borrower.creditScore < 500) {
    result.message = 'Your credit score of ' + borrower.creditScore +
        ' does not meet the 500 requirement.'
    scriptExit.noMessage()
}

// Qualifies. Determine interest rate based on loan amount and credit score.
result.qualified = true
result.message = 'Congratulations, you qualify.'

if (loan.loanAmount > 450000) {
    result.interestRate = 0.08 // Big loans and poor credit require higher rate.
} else if (borrower.creditScore < 550) {
    result.interestRate = 0.08
} else if (borrower.creditScore < 600) {
    result.interestRate = 0.07
} else if (borrower.creditScore < 700) {
    result.interestRate = 0.065
} else { // Good credit gets best rate.
    result.interestRate = 0.06
}

Note that the JavaScript code can't use the Java scriptExit.withMessage() method the Groovy script uses to set the disqualification message and exit the script in one statement. That's because the Rhino JavaScript interpreter does not bubble-up a thrown Java exception as an embedded "cause" in the resulting ScriptException stack trace. The script-exception message thrown from Java code is, thus, more difficult to find in the stack trace. So the JavaScript code in Listing 5 needs to set the result message reason separately before calling scriptExit.noMessage() to invoke the exception that causes script processing to end.

The third and final mortgage product, shown in Listing 6, is written in Ruby. It is intended for borrowers with good credit histories who can make a 20-percent down payment.

Listing 6. Ruby mortgage script
# This Ruby script defines the "Ruby Mortgage" product.
# It is intended for premium borrowers with its low interest rate
# and 20% down payment requirement.

# Our product name
$result.product_name = 'Ruby Mortgage'

# Borrowers with credit unworthiness do not qualify.
if $borrower.credit_score < 700
    $scriptExit.with_message "Credit score of #{$borrower.credit_score}" +
        " is lower than 700 minimum"
end

$scriptExit.with_message 'No bankruptcies allowed' if $borrower.hasDeclaredBankruptcy

# Check other negatives
down_payment_percent = $loan.down_payment / $property.sales_price * 100
if down_payment_percent < 20
    $scriptExit.with_message 'Down payment must be at least 20% of sale price.'
end

# Borrower qualifies. Determine interest rate of loan
$result.message = "Qualified!"
$result.qualified = true

# Give the best interest rate to the best credit risks.
if $borrower.credit_score > 750 || down_payment_percent > 25
    $result.interestRate = 0.06
elsif $borrower.credit_score > 700 && $borrower.totalAssets > 100000
    $result.interestRate = 0.062
else
    $result.interestRate = 0.065
end

Don't forget the $ in JRuby 1.0

Ruby global-variable syntax is important to remember when you access shared Java objects from Ruby scripts. If you leave out the $ for a global variable, JRuby 1.0 and the current JRuby 1.0.1 binary release throw a RaiseException with no information on what went wrong. This bug is fixed in the JRuby source-code repository, so it should be fixed in a future binary release.

As you read through Listing 6, note that shared Java objects placed in the script engine's scope need to be accessed from Ruby with a $ in front of their names. This is Ruby global-variable syntax. Scripting engines share Java variables with script languages as global variables, so the Ruby global-variable syntax must be used.

Notice also in the listing how JRuby automatically converts Ruby-isms to Java-isms when invoking shared Java objects. For example, if JRuby sees a method on a Java object being invoked using the Ruby convention of underscores to separate words, such as $result.product_name = 'Ruby Mortgage', JRuby looks alternatively for a method name using mixed case if it fails to find the name with underscores. Thus, the product_name= Ruby-style method name is translated correctly to the Java result.setProductName("Ruby Mortgage") call.


Program output

Now take a look at the ScriptMortgageQualifierRunner program's output when it is run with the three mortgage product script files. You can run this program using the Ant script included with the source code download. If you prefer Maven, the README.txt file in the download ZIP file includes instructions for building and running the project with Maven. The Ant command is ant run. The run task ensures that the scripting engines and language JAR files are in the classpath. Listing 7 shows the output from Ant.

Listing 7. Program output from Ant
> ant run
Buildfile: build.xml

compile:
    [mkdir] Created dir: C:\temp\script-article\build-main\classes
    [javac] Compiling 10 source files to C:\temp\script-article\build-main\classes

run:
     [java] Processing file: GroovyMortgage.groovy
     [java]   Borrower: Good Borrower
     [java]   Credit score: 800
     [java]   Sales price: 300000.0
     [java]   Down payment: 60000.0
     [java] * Mortgage product: Groovy Mortgage, Qualified? true
     [java] * Interest rate: 0.06
     [java] * Message: Groovy! You qualify.

     [java] Processing file: JavaScriptFirstTimeMortgage.js
     [java]   Borrower: Good Borrower
     [java]   Credit score: 800
     [java]   Sales price: 300000.0
     [java]   Down payment: 60000.0
     [java] * Mortgage product: JavaScript FirstTime Mortgage, Qualified? false
     [java] * Interest rate: 0.0
     [java] * Message: Only first-time home buyers qualify for this mortgage.

     [java] Processing file: RubyPrimeMortgage.rb
     [java]   Borrower: Good Borrower
     [java]   Credit score: 800
     [java]   Sales price: 300000.0
     [java]   Down payment: 60000.0
     [java] * Mortgage product: Ruby Mortgage, Qualified? true
     [java] * Interest rate: 0.06
     [java] * Message: Qualified!

     [java] Processing file: GroovyMortgage.groovy
     [java]   Borrower: Average Borrower
     [java]   Credit score: 700
     [java]   Sales price: 300000.0
     [java]   Down payment: 60000.0
     [java] * Mortgage product: Groovy Mortgage, Qualified? true
     [java] * Interest rate: 0.06
     [java] * Message: Groovy! You qualify.

     [java] Processing file: JavaScriptFirstTimeMortgage.js
     [java]   Borrower: Average Borrower
     [java]   Credit score: 700
     [java]   Sales price: 300000.0
     [java]   Down payment: 60000.0
     [java] * Mortgage product: JavaScript FirstTime Mortgage, Qualified? false
     [java] * Interest rate: 0.0
     [java] * Message: Monthly salary of $4500 exceeds the $4,000 maximum.

     [java] Processing file: RubyPrimeMortgage.rb
     [java]   Borrower: Average Borrower
     [java]   Credit score: 700
     [java]   Sales price: 300000.0
     [java]   Down payment: 60000.0
     [java] * Mortgage product: Ruby Mortgage, Qualified? true
     [java] * Interest rate: 0.065
     [java] * Message: Qualified!

     [java] Processing file: GroovyMortgage.groovy
     [java]   Borrower: Investor Borrower
     [java]   Credit score: 720
     [java]   Sales price: 300000.0
     [java]   Down payment: 30000.0
     [java] * Mortgage product: Groovy Mortgage, Qualified? true
     [java] * Interest rate: 0.06
     [java] * Message: Groovy! You qualify.

     [java] Processing file: JavaScriptFirstTimeMortgage.js
     [java]   Borrower: Investor Borrower
     [java]   Credit score: 720
     [java]   Sales price: 300000.0
     [java]   Down payment: 30000.0
     [java] * Mortgage product: JavaScript FirstTime Mortgage, Qualified? false
     [java] * Interest rate: 0.0
     [java] * Message: This mortgage is not intended for investors.

     [java] Processing file: RubyPrimeMortgage.rb
     [java]   Borrower: Investor Borrower
     [java]   Credit score: 720
     [java]   Sales price: 300000.0
     [java]   Down payment: 30000.0
     [java] * Mortgage product: Ruby Mortgage, Qualified? false
     [java] * Interest rate: 0.0
     [java] * Message: Down payment must be at least 20% of sale price.

     [java] Processing file: GroovyMortgage.groovy
     [java]   Borrower: Risk E. Borrower
     [java]   Credit score: 520
     [java]   Sales price: 300000.0
     [java]   Down payment: 10000.0
     [java] * Mortgage product: Groovy Mortgage, Qualified? false
     [java] * Interest rate: 0.0
     [java] * Message: Down payment of 3.33% is insufficient. 5% minimum required.

     [java] Processing file: JavaScriptFirstTimeMortgage.js
     [java]   Borrower: Risk E. Borrower
     [java]   Credit score: 520
     [java]   Sales price: 300000.0
     [java]   Down payment: 10000.0
     [java] * Mortgage product: JavaScript FirstTime Mortgage, Qualified? true
     [java] * Interest rate: 0.08
     [java] * Message: Congratulations, you qualify.

     [java] Processing file: RubyPrimeMortgage.rb
     [java]   Borrower: Risk E. Borrower
     [java]   Credit score: 520
     [java]   Sales price: 300000.0
     [java]   Down payment: 10000.0
     [java] * Mortgage product: Ruby Mortgage, Qualified? false
     [java] * Interest rate: 0.0
     [java] * Message: Credit score of 520 is lower than 700 minimum


     [java] Sleeping for one minute before reprocessing files.
     [java] Use Ctrl-C to exit...

The output has 12 sections because the program submits each of the four sample borrowers' loan profiles to each of the three scripts to see if the borrower qualifies for any (or all) of the three mortgage products. For the sake of this article's demonstration, the program then waits one minute and repeats the processing of the mortgage scripts. During the pause, you can edit any of the script files to change its business rules or add new script files to the scripts directory to represent mortgage products of your own invention. The program scans the script directory on each pass and processes any new script file it finds.

For instance, suppose you want to raise the minimum credit score required to qualify for the loan. During the one-minute pause, you could edit the JavaScriptFirstTimeMortgage.js script in the src/main/scripts/mortgage-products directory (see Listing 5) to change the business rule on line 23 from if (borrower.creditScore < 500) { to if (borrower.creditScore < 550) {. The next time the rules are run, you'll notice that Risk E. Borrower no longer qualifies for the JavaScript FirstTime Mortgage. That borrower's credit score of 520 would now be too low. The error message still says "Your credit score of 520 does not meet the 500 requirement," but you could fix that now-outdated error message on the fly, as well.


Avoiding dynamic scripting dangers

The ability to change your program at run time is powerful. And potentially dangerous. New functionality and new business rules can be added to a running application without stopping and restarting it. Similarly, new bugs — potentially fatal ones — can be added just as easily.

However, dynamically changing a running application is no more dangerous than changing a stopped application. The static technique just means you must restart the application before finding those new errors. Good software development practice dictates that any change — dynamic or static — to a production application should be tested before it's introduced in production. The Java scripting API does not change that rule.

External script files can be tested during development as part of regular unit testing. You can use JUnit or another testing harness with mock Java objects that a script needs at run time, then execute the script with the mocks to ensure that the script runs without errors and produces expected results. Externalizing application logic into non-Java script files is no excuse for not testing those scripts.

If you are a past or current Web CGI script programmer, it will come as no surprise that you must use caution with what you pass to a ScriptEngine's eval() method. The script engine immediately executes code passed to an eval method. You should, therefore, never pass a string or a Reader object containing text from an untrusted source to a script engine for execution.

For example, it's possible to use the scripting API to monitor a Web application remotely. You could give a script engine access to key Java objects that provide status information about the Web application and create a simple Web page that accepts arbitrary script expressions to feed to the script engine for evaluation and output back to the Web page. That way, you could query and execute methods on the running Java objects to help determine the application's status and health.

In such a scenario, anyone with access to that Web page could execute any statements available in the scripting language and access any of the shared Java objects. Careless programming, misconfiguration, or a security breach could leak confidential information to unauthorized users or expose your application to a denial-of-service attack when the hacker executes a scripting statement equivalent to System.exit or /bin/rm -fr /. Like any powerful tool, the Java scripting API requires you to exercise appropriate caution.


Further exploration

This article focuses on the ability for Java applications to read and execute external scripts dynamically at run time, and for those scripts to access Java objects explicitly provided to them. The Java scripting API provides additional features. For example, you can:

  • Implement Java interfaces using a scripting language and invoke the script code from Java code as you would any other Java interface reference.
  • Instantiate and use Java objects from inside scripts, potentially making those objects available later back to the Java application.
  • Precompile dynamic scripts when they're loaded in order to make subsequent execution faster.
  • Set the input and output streams the script will use, making it easy to use a file as a script's console input source and to capture a script's console output to a file or other stream.
  • Set positional parameters a script can use as command-line arguments.

The Java scripting API defines some of these capabilities as optional for script-engine implementers, so not all scripting engines provide them. See Resources for further reading and online references for these and other features.


Download

DescriptionNameSize
Source code and all JAR filesj-javascripting2.zip4.5MB

Resources

Learn

  • "Invoke dynamic languages dynamically, Part 1": This article introduces the Java scripting API's features and uses a simple application to show how Java code can execute scripting code and how scripts can, in turn, execute Java code.
  • JSR-223: Scripting for the Java Platform: This Java specification request defines the Java scripting API added to Java SE 6.
  • Java Scripting Programmer's Guide: Sun's JDK 6 documentation includes a programmer's guide to the Java scripting API.
  • Jakarta Bean Scripting Framework: The BSF project provides the foundation for the Java scripting API.
  • "Making Scripting Languages JSR-223-Aware" (Thomas Künneth, java.net, September 2006): This article shows how to make a scripting language available to the Java scripting API when no existing script engine is available.
  • Groovy: Learn more about Groovy, an agile dynamic language for the Java platform, from the project Web site.
  • Practically Groovy: Dig deeper into Groovy with this developerWorks column.
  • Mozilla Rhino: Documentation and other resources for learning more about the JavaScript engine that comes bundled with Java run times available from Sun Microsystems and BEA Systems.
  • JRuby: JRuby is a pure-Java implementation of the Ruby programming language. The project Web site features the latest project news and other resources for using JRuby.
  • Browse the technology bookstore for books on these and other technical topics.
  • developerWorks Java technology zone: Hundreds of articles about every aspect of Java programming.

Get products and technologies

  • Groovy: Download the latest Groovy release.
  • Java SE 6 and BEA JRockit: Development kits and run-time environments that natively support the Java scripting API and include a slimmed-down version of the Mozilla Rhino JavaScript engine.
  • Scripting project: The open source Scripting project at java.net provides script-engine interfaces for about two dozen languages and links to other known Java scripting engines. To use one of these scripting languages, install the script-engine implementation JAR file from this project, along with the scripting-language interpreter JAR file itself.
  • Scripting for the Java Platform 1.0 Reference Implementation: The JSR-223 reference implementation provides three JAR files that allow the Java scripting API to run with Java SE 5. Download and unzip the sjp-1_0-fr-ri.zip file and place the js.jar, script-api.jar, and script-js.jar files in your classpath.

Discuss

Comments

developerWorks: Sign in

Required fields are indicated with an asterisk (*).


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Information in your profile (your name, country/region, and company name) is displayed to the public and will accompany any content you post, unless you opt to hide your company name. You may update your IBM account at any time.

All information submitted is secure.

Choose your display name



The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

Required fields are indicated with an asterisk (*).

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


All information submitted is secure.

Dig deeper into Java technology on developerWorks


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Java technology
ArticleID=253876
ArticleTitle=Invoke dynamic languages dynamically, Part 2: Find, execute, and change scripts at run time
publish-date=09112007