Fig - Generic configuration language interpreter

Introduction

Many applications need configuration files. In the old days people built their own syntax and parser. Then people tried to use simple property files with Java. Now people use XML for everything, but XML is a horrible Human interface (see Humans should not have to grok XML); XML is fine for data if you don't mind the verboseness. This article describes Fig, a little config file reader that I made for my talk at the Sydney Java users group June 20, 2007. The basic goal is to allow an application to include the fig.jar and then use the Fig parser to suck in files, the result of which is a list of objects. Fig supports strings, ints, lists, and references to other configuration objects. Fig uses reflection to create instances and call setters or set fields directly (if no setter exists). One can imagine all sorts of extensions like nested objects and so on, but this basic example should get you going.

I figured this would be a nice tutorial. I'm mainly just dumping stuff here rather than really explaining in detail, but it's simple enough you can just read it.

Example

The following example creates 3 object instances: 2 Site objects and 1 Server object:

Site jguru {
        port = 80;
        answers = "www.jguru.com";
        aliases = ["jguru.com", "www.magelang.com"];
        menus = ["FAQ", "Forum", "Search"];
}

Site bea {
        answers = "bea.jguru.com";
        menus = ["FAQ", "Forum"];
}

Server {
        sites = [$jguru, $bea];
}

where $jguru refers to the instance of type Site called jguru (the first object defined).

This assumes that you have class definitions:

import java.util.*;

/** An object type referred to in jguru.fig */
public class Site {
    public int port;
    private String answers;
    public List aliases;
    public List menus;

    /** The reflection support code looks for setters first then fields */
    public void setAnswers(String answers) {
        this.answers = answers;
    }

    public String toString() {
        return "Site "+answers+":"+port+"; "+aliases+", "+menus;
    }
}

and

import java.util.*;

public class Server {
    public List sites;
}

Running Fig gives the following output:

$ java RunFig jguru.fig
Site www.jguru.com:80; [jguru.com, www.magelang.com], [FAQ, Forum, Search]
Site bea.jguru.com:0; null, [FAQ, Forum]
Server@837697

The output is generated by the following snippet in RunFig.java:

List config_objects = g.file();
for ( Object o : config_objects ) {
    System.out.println(o);
}

Spring IOC interface

Those familiar with Spring IOC will see a similarity with Fig. Fig could easily be extended to handle all IOC functionality. For now, here is a sample ioc.fig example:

/* This demonstrates setter injection. */
com.ociweb.springdemo.Config config1 {
        color = yellow;
        number = 19;
}

/* This demonstrates setter injection of another bean. */
com.ociweb.springdemo.MyServiceImpl myService1 {
        config = $config1;
}

com.ociweb.springdemo.Car {
        make = "Honda";
        model = "Prelude";
        year = 1997;
}

The actual IOC looks like the following. If you ask me, it's pretty gross compared to the functionally equivalent Fig notation (taken from here).

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans
  PUBLIC "-//SPRING//DTD BEAN//EN"
  "http://www.springframework.org/dtd/spring-beans.dtd">

<beans>

  <!-- This demonstrates setter injection. -->
  <bean id="config1" class="com.ociweb.springdemo.Config">
    <!-- can specify value with a child element -->
    <property name="color">
      <value>yellow</value>
    </property>
    <!-- can specify value with an attribute -->
    <property name="number" value="19"/>
  </bean>

  <!-- This demonstrates setter injection of another bean. -->
  <bean id="myService1" class="com.ociweb.springdemo.MyServiceImpl">
     <property name="config" ref="config1"/>
  </bean>

  <!-- This bean doesn't need an id because it will be
       associated with another bean via autowire by type. -->
  <bean class="com.ociweb.springdemo.Car">
    <property name="make" value="Honda"/>
    <property name="model" value="Prelude"/>
    <property name="year" value="1997"/>
  </bean>

</beans>

I'm not saying you need Fig to do config files. I'm just saying that XML is an easy, but hideous way to do config files. Fig is much smaller and easier on the eyes.

The Grammar

The syntax is pretty simple as shown in the following grammar.

grammar Fig;

file : object+ ;

object
    :   qid ID? '{' assign* '}'
    ;
    
assign
    :   ID '=' expr ';'
    ;
    
expr:   STRING
    |   INT
    |   '$' ID
    |   '[' ']'
    |   '[' expr (',' expr)* ']'
    ;
    
qid :   ID ('.' ID)* ;

STRING : '"' .* '"' ;
INT :   '0'..'9'+ ;
ID  :   ('_'|'a'..'z'|'A'..'Z') ('_'|'a'..'z'|'A'..'Z'|'0'..'9')* ;
WS  :   (' '|'\n'|'\t')+ {$channel=HIDDEN;} ;
CMT :   '/*' .* '*/'     {$channel=HIDDEN;} ;

To actually create instances and set fields, you need to add actions, return values, and parameters.

grammar Fig;

@header {
import java.util.Map;
import java.util.HashMap;
import java.util.ArrayList;
}

@members {
Map instances = new HashMap();
}

file returns [List objects]
    :   {$objects = new ArrayList();}
        (object {$objects.add($object.o);})+
    ;

object returns [Object o]
    :   qid v=ID?
        {
        $o = RunFig.newInstance($qid.text);
        if ( $v!=null ) {
            instances.put($v.text, $o);
        }
        }
        '{' assign[$o]* '}'
    ;
    
assign[Object o]
    :   ID '=' expr ';' {RunFig.setObjectProperty(o,$ID.text,$expr.value);}
    ;
    
expr returns [Object value]
    :   STRING  {$value = $STRING.text;}
    |   INT  {$value = Integer.valueOf($INT.text);}
    |   '$' ID  {$value = instances.get($ID.text);}
    |   '[' ']' {$value = new ArrayList();}
    |   {ArrayList elements = new ArrayList();}
        '[' e=expr {elements.add($e.value);}
            (',' e=expr {elements.add($e.value);})*
        ']'
        {$value = elements;}
    ;
    
qid :   ID ('.' ID)*
    ;

STRING : '"' .* '"' {setText(getText().substring(1, getText().length()-1));} ;
INT :   '0'..'9'+ ;
ID  :   ('_'|'a'..'z'|'A'..'Z') ('_'|'a'..'z'|'A'..'Z'|'0'..'9')* ;
WS  :   (' '|'\n'|'\t')+ {$channel=HIDDEN;} ;
CMT :   '/*' .* '*/'     {$channel=HIDDEN;} ;

The reflection stuff is all hidden in RunFig:

import org.antlr.runtime.CommonTokenStream;
import org.antlr.runtime.RecognitionException;
import org.antlr.runtime.ANTLRFileStream;

import java.lang.reflect.Method;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.List;


public class RunFig {
    public String foo;

    public static void main(String args[]) throws Exception {
        FigLexer lex = new FigLexer(new ANTLRFileStream(args[0]));
        CommonTokenStream tokens = new CommonTokenStream(lex);

        FigParser g = new FigParser(tokens);
        try {
            // begin parsing and get list of config'd objects
            List config_objects = g.file();
            for ( Object o : config_objects ) {
                System.out.println(o);
            }
        } catch (RecognitionException e) {
            e.printStackTrace();
        }
    }

    public static void setObjectProperty(Object o, String propertyName, Object value) {
        Class c = o.getClass();

        // First see if name is a property ala javabeans
        String methodSuffix = Character.toUpperCase(propertyName.charAt(0))+
            propertyName.substring(1,propertyName.length());
        Method m = getMethod(c,"set"+methodSuffix, new Class[] {value.getClass()});
        if ( m != null ) {
            try {
                invokeMethod(m, o, value);
            }
            catch (Exception e) {
                System.err.println("Can't set property "+propertyName+" using method set"+methodSuffix+
                    " from "+c.getName()+" instance: "+e);
            }
        }
        else {
            // try for a visible field
            try {
                Field f = c.getField(propertyName);
                try {
                    f.set(o, value);
                }
                catch (IllegalAccessException iae) {
                    System.err.println("Can't access property "+propertyName+" using direct field access from "+
                            c.getName()+" instance: "+ iae);
                }
            }
            catch (NoSuchFieldException nsfe) {
                System.err.println("Class "+c.getName()+" has no such property/field: "+propertyName+
                    ": "+nsfe);
            }
        }
    }

    protected static Object newInstance(String name) {
        Object o = null;
        try {
            o = Class.forName(name).newInstance();
        }
        catch (Exception e) {
            System.err.println("can't make instance of "+name);
        }
        return o;
    }

    protected static Method getMethod(Class c, String methodName, Class[] args) {
        Method m;
        try {
            m = c.getMethod(methodName, args);
        }
        catch (NoSuchMethodException nsme) {
            m = null;
        }
        return m;
    }

    protected static Object invokeMethod(Method m, Object o)
        throws IllegalAccessException, InvocationTargetException
    {
        return invokeMethod(m, o, null);
    }

    protected static Object invokeMethod(Method m, Object o, Object value)
        throws IllegalAccessException, InvocationTargetException
    {
        Object[] args = null;
        if ( value!=null ) {
            args = new Object[] {value};
        }
        value = m.invoke(o, args);
        return value;
    }
}

All of these files are attached if you don't want to cut/paste.

Using Fig in your application

Just create a lexer attached to some source of char like ANTLRFileStream("yourconfigfile.fig") and attach the parser to it. Call the file() method (generated from rule file) and use the return value as a list of objects. Here is a helper method you can call:

public List readFig(String fileName) {
    FigLexer lexer = new FigLexer(new ANTLRFileStream(fileName));
    CommonTokenStream tokens = new CommonTokenStream(lexer);
    FigParser g = new FigParser(tokens);
    // begin parsing and get list of config'd objects
    List config_objects = g.file();
    return config_objects;
}