Fork me on GitHub

Tutorial 2

E02: Reading a Jenkins Job Configuration

In the last example we demonstrated a sub projection pretending a non existing element.

This time we like to define some model classes for a Jenkins job configuration and project existing elements to them. But there is one little hurdle: We can not know the exact XML structure, because Jenkins plugins contribute new elements with different names. Our model would have to include one class for each contributed element, but we like to keep the number of model classes low.

The solution to this shows:

  • Usage of XPath wildcards. Mapping of varying elements to one Java object.
  • Automatic conversion of sequences to lists and arrays utilizing Java generics to provide a static typed API.
  • Declarative document origins. Just add a source URL via annotation, let the framework get the document.
  • Inheritance in projection interface. Java interface inheritance is still supported in projection interfaces.

Instead of defining one model class for each element, we project all elements doing the same stuff to the same model object. This is done by using XPath wildcards that will simply select all elements in a defined subtree. Of course we define a getter method to give us the element name, so we can use our model to find out what which elements are really in there.

XML Content

Model Interfaces

Our example should read all builder and all publisher elements in the configuration file. We like to know the element names of both, so we define a super interface "ModelElement" and let our model objects extend it.

public interface ModelElement {
     
    /**
     * Getter may use XPath functions.
     * @return Name of the XML Element which is projected to this object.
     */
    @XBRead("name()")
    String getName();
}
public interface Publisher extends ModelElement{
 
    /**
     * The Plugin name is located in an attribute of the configuration element.
     * @return The plugin name which contributed this element.
     */
    @XBRead("@plugin")
    String getPlugin();
     
}
public interface Builder extends ModelElement {
     
    /**
     * Builder may invoke ant targets, maven goals or shell commands.
     * @return The builders task, whatever this element is.
     */
    @XBRead("child::targets | child::command")
    String getTargetsOrCommands();
 
}

Projection API

Now that we have our model objects defined, we need to define how to retrieve them.

@org.xmlbeam.annotation.XBDocURL("resource://config.xml")
public interface JenkinsJobConfig {
     
    @XBRead("//publishers/*")
    List<Publisher> getPublishers();
     
    @XBRead("//prebuilders/* | //builders/* | //postbuilders/*")
    List<Builder> getAllBuilders();
 
}

Example Code

public class TestJenkinsConfigParsing extends TutorialTestCase {
    private JenkinsJobConfig config;
 
    @Before
    public void readJobConfig() throws IOException {
        config = new XBProjector().io().fromURLAnnotation(JenkinsJobConfig.class);
    }
 
    @Test
    public void testBuilderReading() {
        for (Builder builder: config.getAllBuilders()) {
            System.out.println("Builder: "+builder.getName()+" executes "+builder.getTargetsOrCommands() );
        }
    }
 
    @Test
    public void testPublishers() {
        List<Publisher> publishers = config.getPublishers();
        for (Publisher p : publishers) {
            System.out.println("Publisher:"+ p.getName() + " contributed by plugin "+p.getPlugin());
        }
    }
 
}