JBoss Community Archive (Read Only)

JBoss AS 7.0

Extending JBoss AS 7

This is the old version of the guide, please use the new one

Target Audience

This document is intended for people who want to extend JBoss Application Server (AS) 7 to introduce new capabilities.

Prerequisites

You should know how to download, install and run JBoss Application Server 7. If not please consult the Getting Started Guide. You should also be familiar with the management concepts from the Admin Guide and need Java development experience to follow the example in this guide.

Examples in this guide

Most of the examples in this guide are being expressed as excerpts of the XML configuration files or by using a representation of the de-typed management model.

As discussed elsewhere in this guide, the subsystems are what make up the functionality of JBoss Application Server 7. This section will walk you through the steps needed to create your own subsystem, and will touch on most of the concepts discussed elsewhere in this guide.

Example subsystem

Our example subsystem will keep track of all deployments of certain types containing a special marker file, and expose operations to see how long these deployments have been deployed.

Create the skeleton project

To make your life easier we have provided a maven archetype which will create a skeleton project for implementing subsystems.

mvn archetype:generate \
    -DarchetypeArtifactId=jboss-as-subsystem \
    -DarchetypeGroupId=org.jboss.as.archetypes \
    -DarchetypeVersion=7.0.1.Final \
    -DarchetypeRepository=http://repository.jboss.org/nexus/content/groups/public

Maven will download the archetype and it's dependencies, and ask you some questions:

$ mvn archetype:generate \
    -DarchetypeArtifactId=jboss-as-subsystem \
    -DarchetypeGroupId=org.jboss.as.archetypes \
    -DarchetypeVersion=7.0.1.Final \        
    -DarchetypeRepository=http://repository.jboss.org/nexus/content/groups/public
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building Maven Stub Project (No POM) 1
[INFO] ------------------------------------------------------------------------
[INFO]

.........

Define value for property 'groupId': : com.acme.corp
Define value for property 'artifactId': : acme-subsystem
Define value for property 'version':  1.0-SNAPSHOT: :
Define value for property 'package':  com.acme.corp: : com.acme.corp.tracker
Define value for property 'module': : com.acme.corp.tracker
[INFO] Using property: name = JBoss AS 7 subsystem project
Confirm properties configuration:
groupId: com.acme.corp
artifactId: acme-subsystem
version: 1.0-SNAPSHOT
package: com.acme.corp.tracker
module: com.acme.corp.tracker
name: JBoss AS 7 subsystem project
 Y: : Y
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 1:42.563s
[INFO] Finished at: Fri Jul 08 14:30:09 BST 2011
[INFO] Final Memory: 7M/81M
[INFO] ------------------------------------------------------------------------
$

 

Instruction

1

Enter the  groupId  you wish to use

2

Enter the  artifactId  you wish to use

3

Enter the version you wish to use, or just hit  Enter  if you wish to accept the default  1.0-SNAPSHOT

4

Enter the java package you wish to use, or just hit  Enter  if you wish to accept the default (which is copied from  groupId ).

5

Enter the module name you wish to use for your extension.

6

Finally, if you are happy with your choices, hit  Enter  and Maven will generate the project for you.

You can also do this in Eclipse, see AS7:Creating your own application for more details. We now have a skeleton project that you can use to implement a subsystem. Import the acme-subsystem project into your favourite IDE. A nice side-effect of running this in the IDE is that you can see the javadoc of JBoss AS 7 classes and interfaces imported by the skeleton code. If you do a mvn install in the project it will work if we plug it into JBoss AS 7, but before doing that we will change it to do something more useful.

The rest of this section modifies the skeleton project created by the archetype to do something more useful, and the full code can be found in acme-subsystem.zip.

If you do a mvn install in the created project, you will see some tests being run

$mvn install
[INFO] Scanning for projects...
[...]
[INFO] Surefire report directory: /Users/kabir/sourcecontrol/temp/archetype-test/acme-subsystem/target/surefire-reports

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running com.acme.corp.tracker.extension.SubsystemParsingTestCase
Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.152 sec

Results :

Tests run: 3, Failures: 0, Errors: 0, Skipped: 0
[...]

We will talk about these later in the Testing the parsers section.

Create the schema

First, let us define the schema for our subsystem. Rename src/main/resources/schema/mysubsystem.xsd to src/main/resources/schema/acme.xsd. Then open acme.xsd and modify it to the following

<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
            targetNamespace="urn:com.acme.corp.tracker:1.0"
            xmlns="urn:com.acme.corp.tracker:1.0"
            elementFormDefault="qualified"
            attributeFormDefault="unqualified"
            version="1.0">

   <!-- The subsystem root element -->
   <xs:element name="subsystem" type="subsystemType"/>
   <xs:complexType name="subsystemType">
      <xs:all>
         <xs:element name="deployment-types" type="deployment-typesType"/>
      </xs:all>
   </xs:complexType>
   <xs:complexType name="deployment-typesType">
      <xs:choice minOccurs="0" maxOccurs="unbounded">
         <xs:element name="deploymemt-type" type="deployment-typeType"/>
      </xs:choice>
   </xs:complexType>
   <xs:complexType name="deployment-typeType">
      <xs:attribute name="suffix" use="required"/>
      <xs:attribute name="tick" type="xs:long" use="optional" default="10000"/>
   </xs:complexType>
</xs:schema>
Note that we modified the xmlns and targetNamespace values to urn.com.acme.corp.tracker:1.0. Our new subsystem element has a child called deployment-types, which in turn can have zero or more children called deployment-type. Each deployment-type has a required suffix attribute, and a tick attribute which defaults to 10000.

Now modify the com.acme.corp.tracker.extension.SubsystemExtension class to contain the new namespace.

public class SubsystemExtension implements Extension {

    /** The name space used for the {@code substystem} element */
    public static final String NAMESPACE = "urn:com.acme.corp.tracker:1.0";
    ...

Design and define the model structure

The following example xml contains a valid subsystem configuration, we will see how to plug this in to JBoss AS 7 later in this tutorial.

<subsystem xmlns="urn:com.acme.corp.tracker:1.0">
   <deployment-types>
      <deployment-type suffix="sar" tick="10000"/>
      <deployment-type suffix="war" tick="10000"/>
   </deployment-types>
</subsystem>

Now when designing our model, we can either do a one to one mapping between the schema and the model or come up with something slightly or very different. To keep things simple, let us stay pretty true to the schema so that when executing a :read-resource(recursive=true) against our subsystem we'll see something like:

{
    "outcome" => "success",
    "result" => {"type" => {
        "sar" => {"tick" => "10000"},
        "war" => {"tick" => "10000"}
    }}
}
Each deployment-type in the xml becomes in the model a child resource of the subsystem's root resource. The child resource's child-type is type, and it is indexed by its suffix. Each type resource then contains the tick attribute.

We also need a name for our subsystem, to do that change com.acme.corp.tracker.extension.SubsystemExtension:

public class SubsystemExtension implements Extension {
    ...
    /** The name of our subsystem within the model. */
    public static final String SUBSYSTEM_NAME = "tracker";
    ...
Once we are finished our subsystem will be available under /subsystem=tracker.

The SubsystemExtension.initialize() method defines the model, currently it sets up the basics to add our subsystem to the model:

    @Override
    public void initialize(ExtensionContext context) {
        final SubsystemRegistration subsystem = context.registerSubsystem(SUBSYSTEM_NAME);
        final ManagementResourceRegistration registration = subsystem.registerSubsystemModel(SubsystemProviders.SUBSYSTEM);
        //We always need to add an 'add' operation
        registration.registerOperationHandler(ADD, SubsystemAdd.INSTANCE, SubsystemProviders.SUBSYSTEM_ADD, false);
        //We always need to add a 'describe' operation
        registration.registerOperationHandler(DESCRIBE, SubsystemDescribeHandler.INSTANCE, SubsystemDescribeHandler.INSTANCE, false, OperationEntry.EntryType.PRIVATE);

        subsystem.registerXMLElementWriter(parser);
    }
The registerSubsystem() call registers our subsystem with the extension context. At the end of the method we register our parser with the returned SubsystemRegistration to be able to marshal our subsystem's model back to the main configuration file when it is modified. We will add more functionality to this method later.

Registering the core subsystem model

Next we obtain a ManagementResourceRegistration by registering the subsystem model. This is a compulsory step for every new subsystem.

        final ManagementResourceRegistration registration = subsystem.registerSubsystemModel(SubsystemProviders.SUBSYSTEM);

It's parameter is an implementation of the DescriptionProvider interface, which means that when you call /subsystem=tracker:read-resource-description the information you see comes from SubsystemProviders.SUBSYSTEM, Let's modify it to reflect our new model:

public class SubsystemProviders {

    /**
     * Used to create the description of the subsystem
     */
    public static DescriptionProvider SUBSYSTEM = new DescriptionProvider() {
        public ModelNode getModelDescription(Locale locale) {
            //The locale is passed in so you can internationalize the strings used in the descriptions

            final ModelNode subsystem = new ModelNode();
            //Change the description
            subsystem.get(DESCRIPTION).set("This subsystem tracks deployments of a given type");
            subsystem.get(HEAD_COMMENT_ALLOWED).set(true);
            subsystem.get(TAIL_COMMENT_ALLOWED).set(true);
            subsystem.get(NAMESPACE).set(SubsystemExtension.NAMESPACE);

            //Add information about the 'type' child
            subsystem.get(CHILDREN, "type", DESCRIPTION).set("Deployment types that should be tracked");
            subsystem.get(CHILDREN, "type", MIN_OCCURS).set(0);
            subsystem.get(CHILDREN, "type", MAX_OCCURS).set(Integer.MAX_VALUE);
            subsystem.get(CHILDREN, "type", MODEL_DESCRIPTION);

            return subsystem;
        }
    };

    ...

Since our node will have a child called type we have modified the end of the method to reflect this. The constants used are static imports from org.jboss.as.controller.descriptions.ModelDescriptionConstants. The DESCRIPTION and MODEL_DESCRIPTION entries are needed for all children, while the MIN_OCCURS and MAX_OCCURS entries give helpful hints about how many type entries are allowed.

The ManagementResourceRegistration obtained in SubsystemExtension.initialize() is then used to add additional operations to the /subsystem=tracker address. Every subsystem must have an ADD method which is achieved by the following line.

        //We always need to add an 'add' operation
        registration.registerOperationHandler(ADD, SubsystemAdd.INSTANCE, SubsystemProviders.SUBSYSTEM_ADD, false);

The parameters when registering an operation handler are:

  1. The name - i.e. ADD.

  2. The handler instance - we will talk more about this below

  3. The handler description provider - we will talk more about this below.

  4. Whether this operation handler is inherited - false means that this operation is not inherited, and will only apply to /subsystem=tracker. The content for this operation handler will be provided by 3.

Let us first look at the description provider which is quite simple since this operation takes no parameters. The addition of type children will be handled by another operation handler, as we will see later on.

public class SubsystemProviders {
    ...
    /**
     * Used to create the description of the subsystem add method
     */
    public static DescriptionProvider SUBSYSTEM_ADD = new DescriptionProvider() {
        public ModelNode getModelDescription(Locale locale) {
            //The locale is passed in so you can internationalize the strings used in the descriptions

            final ModelNode subsystem = new ModelNode();
            subsystem.get(DESCRIPTION).set("Adds the tracker subsystem");

            return subsystem;
        }
    };
}

Next we have the actual operation handler instance, note that we have changed its populateModel() method to initialize the type child of the model.

class SubsystemAdd extends AbstractBoottimeAddStepHandler {

    static final SubsystemAdd INSTANCE = new SubsystemAdd();

    private final Logger log = Logger.getLogger(SubsystemAdd.class);

    private SubsystemAdd() {
    }

    /** {@inheritDoc} */
    @Override
    protected void populateModel(ModelNode operation, ModelNode model) throws OperationFailedException {
        log.info("Populating the model");
        //Initialize the 'type' child node
        model.get("type").setEmptyObject();
    }
    ....

SubsystemAdd also has a performBoottime() method which is used for initializing the deployer chain associated with this subsystem. We will talk about the deployers later on. However, the basic idea for all operation handlers is that we do any model updates before changing the actual runtime state.

Registering the subsystem child

The type child does not exist in our skeleton project so we need to implement the operations to add and remove them from the model.

First we need an add operation to add the type child, create a class called com.acme.corp.tracker.extension.TypeAddHandler. In this case we extend the org.jboss.as.controller.AbstractAddStepHandler class and implement the org.jboss.as.controller.descriptions.DescriptionProvider interface. org.jboss.as.controller.OperationStepHandler is the main interface for the operation handlers, and AbstractAddStepHandler is an implementation of that which does the plumbing work for adding a resource to the model.

class TypeAddHandler extends AbstractAddStepHandler implements DescriptionProvider {

    public static final TypeAddHandler INSTANCE = new TypeAddHandler();

    private TypeAddHandler() {
    }

Then we document the operation in the DescriptionProvider method, in this case we have a boolean property called tick. The attribute is not required, and the default value is 10000

    @Override
    public ModelNode getModelDescription(Locale locale) {
        ModelNode node = new ModelNode();
        node.get(DESCRIPTION).set("Adds a tracked deployment type");
        node.get(REQUEST_PROPERTIES, "tick", DESCRIPTION).set("How often to output information about a tracked deployment");
        node.get(REQUEST_PROPERTIES, "tick", TYPE).set(ModelType.LONG);
        node.get(REQUEST_PROPERTIES, "tick", REQUIRED).set(false);
        node.get(REQUEST_PROPERTIES, "tick", DEFAULT).set(10000);
        return node;
    }

Then we do the work of updating the model by overriding the populateModel() method from the AbstractAddStepHandler, which populates the model's attribute from the operation parameters. First we get hold of the model relative to the address of this operation (we will see later that we will register it against /subsystem=tracker/type=*), so we just specify an empty relative address, and we then populate our model with the parameters from the operation.

    @Override
    protected void populateModel(ModelNode operation, ModelNode model) throws OperationFailedException {
        //The default value is 10000 if it has not been specified
        long tick = 10000;
        //Read the value from the operation
        if (operation.hasDefined("tick")) {
            tick = operation.get("tick").asLong();
        }
        model.get("tick").set(tick);
    }

We then override the performRuntime() method to perform our runtime changes, which in this case involves installing a service into the controller at the heart of JBoss AS 7. (AbstractAddStepHandler.performRuntime() is similar to AbstractBoottimeAddStepHandler.performBoottime() in that the model is updated before runtime changes are made.

    @Override
    protected void performRuntime(OperationContext context, ModelNode operation, ModelNode model,
            ServiceVerificationHandler verificationHandler, List<ServiceController<?>> newControllers)
            throws OperationFailedException {
        String suffix = PathAddress.pathAddress(operation.get(ModelDescriptionConstants.ADDRESS)).getLastElement().getValue();
        TrackerService service = new TrackerService(suffix, model.get("tick").asLong());
        ServiceName name = TrackerService.createServiceName(suffix);
        ServiceController<TrackerService> controller = context.getServiceTarget()
                .addService(name, service)
                .addListener(verificationHandler)
                .setInitialMode(Mode.ACTIVE)
                .install();
        newControllers.add(controller);
    }
}

Since the add methods will be of the format /subsystem=tracker/suffix=war:add(tick=1234), we look for the last element of the operation address, which is war in the example just given and use that as our suffix. We then create an instance of TrackerService and install that into the service target of the context and add the created service controller to the newControllers list.

The tracker service is quite simple. All services installed into JBoss AS 7 must implement the org.jboss.msc.service.Service interface.

public class TrackerService implements Service<TrackerService>{

We then have some fields to keep the tick count and a thread which when run outputs all the deployments registered with our service.

    private AtomicLong tick = new AtomicLong(10000);

    private Set<String> deployments = Collections.synchronizedSet(new HashSet<String>());

    private Set<String> coolDeployments = Collections.synchronizedSet(new HashSet<String>());

    private final String suffix;

    private Thread OUTPUT = new Thread() {
        @Override
        public void run() {
            while (true) {
                try {
                    Thread.sleep(tick.get());
                    System.out.println("Current deployments deployed while " + suffix + " tracking active:\n" + deployments 
                       + "\nCool: " + coolDeployments.size());
                } catch (InterruptedException e) {
                    interrupted();
                    break;
                }
            }
        }
    };

    public TrackerService(String suffix, long tick) {
        this.suffix = suffix;
        this.tick.set(tick);
    }

Next we have three methods which come from the Service interface. getValue() returns this service, start() is called when the service is started by the controller, stop is called when the service is stopped by the controller, and they start and stop the thread outputting the deployments.

    @Override
    public TrackerService getValue() throws IllegalStateException, IllegalArgumentException {
        return this;
    }

    @Override
    public void start(StartContext context) throws StartException {
        OUTPUT.start();
    }

    @Override
    public void stop(StopContext context) {
        OUTPUT.interrupt();
    }
Next we have a utility method to create the ServiceName which is used to register the service in the controller.

    public static ServiceName createServiceName(String suffix) {
        return ServiceName.JBOSS.append("tracker", suffix);
    }

Finally we have some methods to add and remove deployments, and to set and read the tick. The 'cool' deployments will be explained later.

    public void addDeployment(String name) {
        deployments.add(name);
    }

    public void addCoolDeployment(String name) {
        coolDeployments.add(name);
    }

    public void removeDeployment(String name) {
        deployments.remove(name);
        coolDeployments.remove(name);
    }

    void setTick(long tick) {
        this.tick.set(tick);
    }

    public long getTick() {
        return this.tick.get();
    }
}//TrackerService - end

Since we are able to add type children, we need a way to be able to remove them, so we create a com.acme.corp.tracker.extension.TypeRemoveHandler. In this case we extend AbstractRemoveStepHandler which takes care of removing the resource from the model so we don't need to override its performRemove() operationa. But we need to implement the DescriptionProvider method to provide the model description, and since the add handler installs the TrackerService, we need to remove that in the performRuntime() method.

public class TypeRemoveHandler extends AbstractRemoveStepHandler implements DescriptionProvider {

    public static final TypeRemoveHandler INSTANCE = new TypeRemoveHandler();

    private TypeRemoveHandler() {
    }

    @Override
    public ModelNode getModelDescription(Locale locale) {
        ModelNode node = new ModelNode();
        node.get(DESCRIPTION).set("Removes a tracked deployment type");
        return node;
    }

    @Override
    protected void performRuntime(OperationContext context, ModelNode operation, ModelNode model) throws OperationFailedException {
        String suffix = PathAddress.pathAddress(operation.get(ModelDescriptionConstants.ADDRESS)).getLastElement().getValue();
        ServiceName name = TrackerService.createServiceName(suffix);
        context.removeService(name);
    }

}

We then need a description provider for the type part of the model itself, so we modify SubsystemProviders to add the description provider.

class SubsystemProviders {
    ...
    /**
     * Used to create the description of the {@code type}  child
     */
    public static DescriptionProvider TYPE_CHILD = new DescriptionProvider() {
        @Override
        public ModelNode getModelDescription(Locale locale) {
            ModelNode node = new ModelNode();
            node.get(DESCRIPTION).set("Contains information about a tracked deployment type");
            node.get(ATTRIBUTES, "tick", DESCRIPTION).set("How often in milliseconds to output the information about the tracked deployments");
            node.get(ATTRIBUTES, "tick", TYPE).set(ModelType.LONG);
            node.get(ATTRIBUTES, "tick", REQUIRED).set(true);
            node.get(ATTRIBUTES, "tick", DEFAULT).set(10000);
            return node;
        }
    };
}

Then finally we need to specify that our new type child and associated handlers go under /subsystem=tracker/type=* in the model by adding registering it with the model in SubsystemExtension.initialize(). So we add the following just before the end of the method.

    @Override
    public void initialize(ExtensionContext context) {
        final SubsystemRegistration subsystem = context.registerSubsystem(SUBSYSTEM_NAME);
        final ManagementResourceRegistration registration = subsystem.registerSubsystemModel(SubsystemProviders.SUBSYSTEM);
        ...

        //Add the type child
        ManagementResourceRegistration typeChild = registration.registerSubModel(PathElement.pathElement("type"), SubsystemProviders.TYPE_CHILD);
        typeChild.registerOperationHandler(ModelDescriptionConstants.ADD, TypeAddHandler.INSTANCE, TypeAddHandler.INSTANCE);
        typeChild.registerOperationHandler(ModelDescriptionConstants.REMOVE, TypeRemoveHandler.INSTANCE, TypeRemoveHandler.INSTANCE);
        typeChild.registerReadWriteAttribute("tick", null, TrackerTickHandler.INSTANCE, Storage.CONFIGURATION);

        subsystem.registerXMLElementWriter(parser);
    }

The above first creates a child of our main subsystem registration for the relative address type=*, and gets the typeChild registration. To this we add the TypeAddHandler and TypeRemoveHandler. The add variety is added under the name add and the remove handler under the name remove, and for each registered operation handler we use the handler singleton instance as both the handler parameter and as the DescriptionProvider.

Finally, we register tick as a read/write attribute, the null parameter means we don't do anything special with regards to reading it, for the write handler we supply it with an operation handler called TrackerTickHandler. Registering it as a read/write attribute means we can use the :write-attribute operation to modify the value of the parameter, and it will be handled by TrackerTickHandler.

Not registering a write attribute handler makes the attribute read only.

TrackerTickHandler implements OperationStepHandler directly, and so must implement its execute method. This means there is no 'plumbing' taking care of separating the updates to the model from the runtime updates, so we need to do this ourselves.

class TrackerTickHandler implements OperationStepHandler {

    public static final TrackerTickHandler INSTANCE = new TrackerTickHandler();

    private TrackerTickHandler() {
    }

    @Override
    public void execute(OperationContext context, ModelNode operation) throws OperationFailedException {
        //Update the model
        final String suffix = PathAddress.pathAddress(operation.get(ModelDescriptionConstants.ADDRESS)).getLastElement().getValue();
        final long tick = operation.require("value").asLong();
        ModelNode node = context.readResourceForUpdate(PathAddress.EMPTY_ADDRESS).getModel();
        node.get("tick").set(tick);

        //Add a step to perform the runtime update
        context.addStep(new OperationStepHandler() {
            @Override
            public void execute(OperationContext context, ModelNode operation) throws OperationFailedException {
                TrackerService service = (TrackerService)context.getServiceRegistry(true).getRequiredService(TrackerService.createServiceName(suffix)).getValue();
                service.setTick(tick);
                context.completeStep();
            }
        }, Stage.RUNTIME);
        context.completeStep();
    }
}

The operation used to execute this will be of the form /subsystem=tracker/type=war:write-attribute(name=tick,value=12345 so we first get the suffix from the operation address, and the tick value from the operation parameter's value parameter, and use that to update the model.

We then add a new step associated with the RUNTIME stage to update the tick of the TrackerService for our suffix. This is essential since the call to context.getServiceRegistry() will fail unless the step accessing it belongs to the RUNTIME stage.

When implementing execute(), you must call context.completeStep() when you are done.

Parsing and marshalling of the subsystem xml

JBoss AS 7 uses the Stax API to parse the xml files. This is initialized in SubsystemExtension by mapping our parser onto our namespace:

public class SubsystemExtension implements Extension {

    /** The name space used for the {@code subsystem} element */
    public static final String NAMESPACE = "urn:com.acme.corp.tracker:1.0";
    ...

    /** The parser used for parsing our subsystem */
    private final SubsystemParser parser = new SubsystemParser();

    @Override
    public void initializeParsers(ExtensionParsingContext context) {
        context.setSubsystemXmlMapping(NAMESPACE, parser);
    }
    ...

We then need to write the parser. The contract is that we read our subsystem's xml and create the operations that will populate the model with the state contained in the xml. These operations will then be executed on our behalf as part of the parsing process. The entry point is the readElement() method.

public class SubsystemExtension implements Extension {
    private static ModelNode createAddSubsystemOperation() {
        final ModelNode subsystem = new ModelNode();
        subsystem.get(OP).set(ADD);
        subsystem.get(OP_ADDR).add(SUBSYSTEM, SUBSYSTEM_NAME);
        return subsystem;
    }

    /**
     * The subsystem parser, which uses stax to read and write to and from xml
     */
    private static class SubsystemParser implements XMLStreamConstants, XMLElementReader<List<ModelNode>>, XMLElementWriter<SubsystemMarshallingContext> {

        /** {@inheritDoc} */
        @Override
        public void readElement(XMLExtendedStreamReader reader, List<ModelNode> list) throws XMLStreamException {
            // Require no attributes
            ParseUtils.requireNoAttributes(reader);

            //Add the main subsystem 'add' operation
            list.add(createAddSubsystemOperation());

            //Read the children
            while (reader.hasNext() && reader.nextTag() != END_ELEMENT) {
                if (!reader.getLocalName().equals("deployment-types")) {
                    throw ParseUtils.unexpectedElement(reader);
                }
                while (reader.hasNext() && reader.nextTag() != END_ELEMENT) {
                    if (reader.isStartElement()) {
                        readDeploymentType(reader, list);
                    }
                }
            }
        }

        private void readDeploymentType(XMLExtendedStreamReader reader, List<ModelNode> list) throws XMLStreamException {
            if (!reader.getLocalName().equals("deployment-type")) {
                throw ParseUtils.unexpectedElement(reader);
            }
            String suffix = null;
            Long tick = null;
            for (int i = 0 ; i < reader.getAttributeCount() ; i++) {
                String attr = reader.getAttributeLocalName(i);
                if (attr.equals("tick")){
                    tick = Long.valueOf(reader.getAttributeValue(i));
                } else if (attr.equals("suffix")){
                    suffix = reader.getAttributeValue(i);
                } else {
                    throw ParseUtils.unexpectedAttribute(reader, i);
                }
            }
            ParseUtils.requireNoContent(reader);
            if (suffix == null) {
                throw ParseUtils.missingRequiredElement(reader, Collections.singleton("suffix"));
            }

            //Add the 'add' operation for each 'type' child
            ModelNode addType = new ModelNode();
            addType.get(OP).set(ModelDescriptionConstants.ADD);
            PathAddress addr = PathAddress.pathAddress(PathElement.pathElement(SUBSYSTEM, SUBSYSTEM_NAME), PathElement.pathElement("type", suffix));
            addType.get(OP_ADDR).set(addr.toModelNode());
            if (tick != null) {
                addType.get("tick").set(tick);
            }
            list.add(addType);
        }
        ...

So in the above we always create the add operation for our subsystem (createAddSubsystemOperation(). Due to its address /subsystem=tracker this will trigger the SubsystemAddHandler we created earlier when we invoke /subsystem=tracker:add. We then parse the child elements and create an add operation for the child address for each type child. Since the address will for example be /subsystem=tracker/type=sar and TypeAddHandler is registered for all type subaddresses the TypeAddHandler will get invoked for those operations.

The parser is also used to marshal the model to xml whenever something modifies the model, for which the entry point is the writeContent() method:

    private static class SubsystemParser implements XMLStreamConstants, XMLElementReader<List<ModelNode>>, XMLElementWriter<SubsystemMarshallingContext> {
        ...
        /** {@inheritDoc} */
        @Override
        public void writeContent(final XMLExtendedStreamWriter writer, final SubsystemMarshallingContext context) throws XMLStreamException {

            //Write out the main subsystem element
            context.startSubsystemElement(SubsystemExtension.NAMESPACE, false);

            writer.writeStartElement("deployment-types");

            ModelNode node = context.getModelNode();
            ModelNode type = node.get("type");
            for (Property property : type.asPropertyList()) {

                //write each child element to xml
                writer.writeStartElement("deployment-type");
                writer.writeAttribute("suffix", property.getName());
                ModelNode entry = property.getValue();
                if (entry.hasDefined("tick")) {
                    writer.writeAttribute("tick", entry.get("tick").asString());
                }
                writer.writeEndElement();
            }

            //End deployment-types
            writer.writeEndElement();
            //End subsystem
            writer.writeEndElement();
        }
    }

Then we have to implement the SubsystemDescribeHandler which translates the current state of the model into operations similar to the ones created by the parser. The SubsystemDescribeHandler is only used when running in a managed domain, and is used when the host controller queries the domain controller for the configuration of the profile used to start up each server. In our case the SubsystemDescribeHandler adds the operation to add the subsystem and then adds the operation to add each type child:

    private static class SubsystemDescribeHandler implements OperationStepHandler, DescriptionProvider {
        static final SubsystemDescribeHandler INSTANCE = new SubsystemDescribeHandler();

        public void execute(OperationContext context, ModelNode operation) throws OperationFailedException {
            //Add the main operation
            context.getResult().add(createAddSubsystemOperation());

            //Add the operations to create each child

            ModelNode node = context.readModel(PathAddress.EMPTY_ADDRESS);
            for (Property property : node.get("type").asPropertyList()) {

                ModelNode addType = new ModelNode();
                addType.get(OP).set(ModelDescriptionConstants.ADD);
                PathAddress addr = PathAddress.pathAddress(PathElement.pathElement(SUBSYSTEM, SUBSYSTEM_NAME), PathElement.pathElement("type", property.getName()));
                addType.get(OP_ADDR).set(addr.toModelNode());
                if (property.getValue().hasDefined("tick")) {
                    addType.get("tick").set(property.getValue().get("tick").asLong());
                }
                context.getResult().add(addType);
            }
            context.completeStep();
        }

        @Override
        public ModelNode getModelDescription(Locale locale) {
            return CommonDescriptions.getSubsystemDescribeOperation(locale);
        }
    }

Testing the parsers

Changes to tests between 7.0.0 and 7.0.1

The testing framework was moved from the archetype into the core JBoss AS 7 sources between JBoss AS 7.0.0 and JBoss AS 7.0.1, and has been improved upon and is used internally for testing JBoss AS 7's subsystems. The differences between the two versions is that in 7.0.0.Final the testing framework is bundled with the code generated by the archetype (in a sub-package of the package specified for your subsystem, e.g. com.acme.corp.tracker.support), and the test extends the AbstractParsingTest class.

From 7.0.1 the testing framework is now brought in via the org.jboss.as:jboss-as-subsystem-test maven artifact, and the test's superclass is org.jboss.as.subsystem.test.AbstractSubsystemTest. The concepts are the same but more and more functionality will be available as JBoss AS 7 is developed.

Now that we have modified our parsers we need to update our tests to reflect the new model. There are currently three tests testing the basic functionality, something which is a lot easier to debug from your IDE before you plug it into the application server. We will talk about these tests in turn and they all live in com.acme.corp.tracker.extension.SubsystemParsingTestCase. SubsystemParsingTestCase extends AbstractSubsystemTest which does a lot of the setup for you and contains utility methods for verifying things from your test. See the javadoc of that class for more information about the functionality available to you. And by all means feel free to add more tests for your subsystem, here we are only testing for the best case scenario while you will probably want to throw in a few tests for edge cases.

The first test we need to modify is testParseSubsystem(). It tests that the parsed xml becomes the expected operations that will be parsed into the server, so let us tweak this test to match our subsystem. First we tell the test to parse the xml into operations

    @Test
    public void testParseSubsystem() throws Exception {
        //Parse the subsystem xml into operations
        String subsystemXml =
                "<subsystem xmlns=\"" + SubsystemExtension.NAMESPACE + "\">" +
                "   <deployment-types>" +
                "       <deployment-type suffix=\"tst\" tick=\"12345\"/>" +
                "   </deployment-types>" +
                "</subsystem>";
        List<ModelNode> operations = super.parse(subsystemXml);

There should be one operation for adding the subsystem itself and an operation for adding the deployment-type, so check we got two operations

        ///Check that we have the expected number of operations
        Assert.assertEquals(2, operations.size());

Now check that the first operation is add for the address /subsystem=tracker:

        //Check that each operation has the correct content
        //The add subsystem operation will happen first
        ModelNode addSubsystem = operations.get(0);
        Assert.assertEquals(ADD, addSubsystem.get(OP).asString());
        PathAddress addr = PathAddress.pathAddress(addSubsystem.get(OP_ADDR));
        Assert.assertEquals(1, addr.size());
        PathElement element = addr.getElement(0);
        Assert.assertEquals(SUBSYSTEM, element.getKey());
        Assert.assertEquals(SubsystemExtension.SUBSYSTEM_NAME, element.getValue());

Then check that the second operation is add for the address /subsystem=tracker, and that 12345 was picked up for the value of the tick parameter:

        //Then we will get the add type operation
        ModelNode addType = operations.get(1);
        Assert.assertEquals(ADD, addType.get(OP).asString());
        Assert.assertEquals(12345, addType.get("tick").asLong());
        addr = PathAddress.pathAddress(addType.get(OP_ADDR));
        Assert.assertEquals(2, addr.size());
        element = addr.getElement(0);
        Assert.assertEquals(SUBSYSTEM, element.getKey());
        Assert.assertEquals(SubsystemExtension.SUBSYSTEM_NAME, element.getValue());
        element = addr.getElement(1);
        Assert.assertEquals("type", element.getKey());
        Assert.assertEquals("tst", element.getValue());
    }

The second test we need to modify is testInstallIntoController() which tests that the xml installs properly into the controller. In other words we are making sure that the add operations we created earlier work properly. First we create the xml and install it into the controller. Behind the scenes this will parse the xml into operations as we saw in the last test, but it will also create a new controller and boot that up using the created operations

    @Test
    public void testInstallIntoController() throws Exception {
        //Parse the subsystem xml and install into the controller
        String subsystemXml =
                "<subsystem xmlns=\"" + SubsystemExtension.NAMESPACE + "\">" +
                "   <deployment-types>" +
                "       <deployment-type suffix=\"tst\" tick=\"12345\"/>" +
                "   </deployment-types>" +
                "</subsystem>";
        KernelServices services = super.installInController(subsystemXml);

The returned KernelServices allow us to execute operations on the controller, and to read the whole model.

        //Read the whole model and make sure it looks as expected
        ModelNode model = services.readWholeModel();
        //Useful for debugging :-)
        //System.out.println(model);

Now we make sure that the structure of the model within the controller has the expected format and values

        Assert.assertTrue(model.get(SUBSYSTEM).hasDefined(SubsystemExtension.SUBSYSTEM_NAME));
        Assert.assertTrue(model.get(SUBSYSTEM, SubsystemExtension.SUBSYSTEM_NAME).hasDefined("type"));
        Assert.assertTrue(model.get(SUBSYSTEM, SubsystemExtension.SUBSYSTEM_NAME, "type").hasDefined("tst"));
        Assert.assertTrue(model.get(SUBSYSTEM, SubsystemExtension.SUBSYSTEM_NAME, "type", "tst").hasDefined("tick"));
        Assert.assertEquals(12345, model.get(SUBSYSTEM, SubsystemExtension.SUBSYSTEM_NAME, "type", "tst", "tick").asLong());
    }

The last test provided is called testParseAndMarshalModel(). It's main purpose is to make sure that our SubsystemParser.writeContent() works as expected. This is achieved by starting a controller in the same way as before

    @Test
    public void testParseAndMarshalModel() throws Exception {
        //Parse the subsystem xml and install into the first controller
        String subsystemXml =
                "<subsystem xmlns=\"" + SubsystemExtension.NAMESPACE + "\">" +
                "   <deployment-types>" +
                "       <deployment-type suffix=\"tst\" tick=\"12345\"/>" +
                "   </deployment-types>" +
                "</subsystem>";
        KernelServices servicesA = super.installInController(subsystemXml);

Now we read the model and the xml that was persisted from the first controller, and use that xml to start a second controller

        //Get the model and the persisted xml from the first controller
        ModelNode modelA = servicesA.readWholeModel();
        String marshalled = servicesA.getPersistedSubsystemXml();

        //Install the persisted xml from the first controller into a second controller
        KernelServices servicesB = super.installInController(marshalled);

Finally we read the model from the second controller, and make sure that the models are identical by calling compare() on the test superclass.

        ModelNode modelB = servicesB.readWholeModel();

        //Make sure the models from the two controllers are identical
        super.compare(modelA, modelB);
    }

We then have a test that needs no changing from what the archetype provides us with. As we have seen before we start a controller

    @Test
    public void testDescribeHandler() throws Exception {
        //Parse the subsystem xml and install into the first controller
        String subsystemXml =
                "<subsystem xmlns=\"" + SubsystemExtension.NAMESPACE + "\">" +
                "</subsystem>";
        KernelServices servicesA = super.installInController(subsystemXml);

We then call /subsystem=tracker:describe which outputs the subsystem as operations needed to reach the current state (Done by our SubsystemDescribeHandler)

        //Get the model and the describe operations from the first controller
        ModelNode modelA = servicesA.readWholeModel();
        ModelNode describeOp = new ModelNode();
        describeOp.get(OP).set(DESCRIBE);
        describeOp.get(OP_ADDR).set(
                PathAddress.pathAddress(
                        PathElement.pathElement(SUBSYSTEM, SubsystemExtension.SUBSYSTEM_NAME)).toModelNode());
        List<ModelNode> operations = super.checkResultAndGetContents(servicesA.executeOperation(describeOp)).asList();

Then we create a new controller using those operations

        //Install the describe options from the first controller into a second controller
        KernelServices servicesB = super.installInController(operations);

And then we read the model from the second controller and make sure that the two subsystems are identical
ModelNode modelB = servicesB.readWholeModel();

        //Make sure the models from the two controllers are identical
        super.compare(modelA, modelB);

    }

For good measure let us throw in another test which adds a deployment-type and also changes its attribute at runtime. So first of all boot up the controller with the same xml we have been using so far

    @Test
    public void testExecuteOperations() throws Exception {
        String subsystemXml =
                "<subsystem xmlns=\"" + SubsystemExtension.NAMESPACE + "\">" +
                "   <deployment-types>" +
                "       <deployment-type suffix=\"tst\" tick=\"12345\"/>" +
                "   </deployment-types>" +
                "</subsystem>";
        KernelServices services = super.installInController(subsystemXml);

Now create an operation which does the same as the following CLI command /subsystem=tracker/type=foo:add(tick=1000)

        //Add another type
        PathAddress fooTypeAddr = PathAddress.pathAddress(
                PathElement.pathElement(SUBSYSTEM, SubsystemExtension.SUBSYSTEM_NAME),
                PathElement.pathElement("type", "foo"));
        ModelNode addOp = new ModelNode();
        addOp.get(OP).set(ADD);
        addOp.get(OP_ADDR).set(fooTypeAddr.toModelNode());
        addOp.get("tick").set(1000);

Execute the operation and make sure it was successful

        ModelNode result = services.executeOperation(addOp);
        Assert.assertEquals(SUCCESS, result.get(OUTCOME).asString());

Read the whole model and make sure that the original data is still there (i.e. the same as what was done by testInstallIntoController()

        ModelNode model = services.readWholeModel();
        Assert.assertTrue(model.get(SUBSYSTEM).hasDefined(SubsystemExtension.SUBSYSTEM_NAME));
        Assert.assertTrue(model.get(SUBSYSTEM, SubsystemExtension.SUBSYSTEM_NAME).hasDefined("type"));
        Assert.assertTrue(model.get(SUBSYSTEM, SubsystemExtension.SUBSYSTEM_NAME, "type").hasDefined("tst"));
        Assert.assertTrue(model.get(SUBSYSTEM, SubsystemExtension.SUBSYSTEM_NAME, "type", "tst").hasDefined("tick"));
        Assert.assertEquals(12345, model.get(SUBSYSTEM, SubsystemExtension.SUBSYSTEM_NAME, "type", "tst", "tick").asLong());

Then make sure our new type has been added:

        Assert.assertTrue(model.get(SUBSYSTEM, SubsystemExtension.SUBSYSTEM_NAME, "type").hasDefined("foo"));
        Assert.assertTrue(model.get(SUBSYSTEM, SubsystemExtension.SUBSYSTEM_NAME, "type", "foo").hasDefined("tick"));
        Assert.assertEquals(1000, model.get(SUBSYSTEM, SubsystemExtension.SUBSYSTEM_NAME, "type", "foo", "tick").asLong());

Then we call write-attribute to change the tick value of /subsystem=tracker/type=foo:

        //Call write-attribute
        ModelNode writeOp = new ModelNode();
        writeOp.get(OP).set(WRITE_ATTRIBUTE_OPERATION);
        writeOp.get(OP_ADDR).set(fooTypeAddr.toModelNode());
        writeOp.get(NAME).set("tick");
        writeOp.get(VALUE).set(3456);
        result = services.executeOperation(writeOp);
        Assert.assertEquals(SUCCESS, result.get(OUTCOME).asString());

To give you exposure to other ways of doing things, now instead of reading the whole model to check the attribute, we call read-attribute instead, and make sure it has the value we set it to.

        //Check that write attribute took effect, this time by calling read-attribute instead of reading the whole model
        ModelNode readOp = new ModelNode();
        readOp.get(OP).set(READ_ATTRIBUTE_OPERATION);
        readOp.get(OP_ADDR).set(fooTypeAddr.toModelNode());
        readOp.get(NAME).set("tick");
        result = services.executeOperation(readOp);
        Assert.assertEquals(3456, checkResultAndGetContents(result).asLong());

Since each type installs its own copy of TrackerService, we get the TrackerService for type=foo from the service container exposed by the kernel services and make sure it has the right value

        TrackerService service = (TrackerService)services.getContainer().getService(TrackerService.createServiceName("foo")).getValue();
        Assert.assertEquals(3456, service.getTick());
    }

Add the deployers

When discussing SubsystemAddHandler we did not mention the work done to install the deployers, which is done in the following method:

    @Override
    public void performBoottime(OperationContext context, ModelNode operation, ModelNode model,
            ServiceVerificationHandler verificationHandler, List<ServiceController<?>> newControllers)
            throws OperationFailedException {

        log.info("Populating the model");

        //Add deployment processors here
        //Remove this if you don't need to hook into the deployers, or you can add as many as you like
        //see SubDeploymentProcessor for explanation of the phases
        context.addStep(new AbstractDeploymentChainStep() {
            public void execute(DeploymentProcessorTarget processorTarget) {
                processorTarget.addDeploymentProcessor(SubsystemDeploymentProcessor.PHASE, SubsystemDeploymentProcessor.priority, new SubsystemDeploymentProcessor());

            }
        }, OperationContext.Stage.RUNTIME);

    }

This adds an extra step which is responsible for installing deployment processors. You can add as many as you like, or avoid adding any all together depending on your needs. Each processor has a Phase and a priority. Phases are sequential, and a deployment passes through each phases deployment processors. The priority specifies where within a phase the processor appears. See org.jboss.as.server.deployment.Phase for more information about phases.

In our case we are keeping it simple and staying with one deployment processor with the phase and priority created for us by the maven archetype. The phases will be explained in the next section. The deployment processor is as follows:

public class SubsystemDeploymentProcessor implements DeploymentUnitProcessor {
    ...

    @Override
    public void deploy(DeploymentPhaseContext phaseContext) throws DeploymentUnitProcessingException {
        String name = phaseContext.getDeploymentUnit().getName();
        TrackerService service = getTrackerService(phaseContext.getServiceRegistry(), name);
        if (service != null) {
            ResourceRoot root = phaseContext.getDeploymentUnit().getAttachment(Attachments.DEPLOYMENT_ROOT);
            VirtualFile cool = root.getRoot().getChild("META-INF/cool.txt");
            service.addDeployment(name);
            if (cool.exists()) {
                service.addCoolDeployment(name);
            }
        }
    }

    @Override
    public void undeploy(DeploymentUnit context) {
        context.getServiceRegistry();
        String name = context.getName();
        TrackerService service = getTrackerService(context.getServiceRegistry(), name);
        if (service != null) {
            service.removeDeployment(name);
        }
    }

    private TrackerService getTrackerService(ServiceRegistry registry, String name) {
        int last = name.lastIndexOf(".");
        String suffix = name.substring(last + 1);
        ServiceController<?> container = registry.getService(TrackerService.createServiceName(suffix));
        if (container != null) {
            TrackerService service = (TrackerService)container.getValue();
            return service;
        }
        return null;
    }
}

The deploy() method is called when a deployment is being deployed. In this case we look for the TrackerService instance for the service name created from the deployment's suffix. If there is one it means that we are meant to be tracking deployments with this suffix (i.e. TypeAddHandler was called for this suffix), and if we find one we add the deployment's name to it. Similarly undeploy() is called when a deployment is being undeployed, and if there is a TrackerService instance for the deployment's suffix, we remove the deployment's name from it.

Deployment phases and attachments

The code in the SubsystemDeploymentProcessor uses an attachment, which is the means of communication between the individual deployment processors. A deployment processor belonging to a phase may create an attachment which is then read further along the chain of deployment unit processors. In the above example we look for the Attachments.DEPLOYMENT_ROOT attachment, which is a view of the file structure of the deployment unit put in place before the chain of deployment unit processors is invoked.

As mentioned above, the deployment unit processors are organized in phases, and have a relative order within each phase. A deployment unit passes through all the deployment unit processors in that order. A deployment unit processor may choose to take action or not depending on what attachments are available. Let's take a quick look at what the deployment unit processors for in the phases described in org.jboss.as.server.deployment.Phase.

STRUCTURE

The deployment unit processors in this phase determine the structure of a deployment, and looks for sub deployments and metadata files.

PARSE

In this phase the deployment unit processors parse the deployment descriptors and build up the annotation index. Class-Path entries from the META-INF/MANIFEST.MF are added.

DEPENDENCIES

Extra class path dependencies are added. For example if deploying a war file, the commonly needed dependencies for a web application are added.

CONFIGURE_MODULE

In this phase the modular class loader for the deployment is created. No attempt should be made loading classes from the deployment until after this phase.

POST_MODULE

Now that our class loader has been constructed we have access to the classes. In this stage deployment processors may use the Attachments.REFLECTION_INDEX attachment which is a deployment index used to obtain members of classes in the deployment, and to invoke upon them, bypassing the inefficiencies of using java.lang.reflect directly.

INSTALL

Install new services coming from the deployment.

CLEANUP

Attachments put in place earlier in the deployment unit processor chain may be removed here.

Integrate with JBoss AS 7

Now that we have all the code needed for our subsystem, we can build our project by running mvn install

[kabir ~/sourcecontrol/temp/archetype-test/acme-subsystem]
$mvn install
[INFO] Scanning for projects...
[...]
main:
   [delete] Deleting: /Users/kabir/sourcecontrol/temp/archetype-test/acme-subsystem/null1004283288
   [delete] Deleting directory /Users/kabir/sourcecontrol/temp/archetype-test/acme-subsystem/target/module
     [copy] Copying 1 file to /Users/kabir/sourcecontrol/temp/archetype-test/acme-subsystem/target/module/com/acme/corp/tracker/main
     [copy] Copying 1 file to /Users/kabir/sourcecontrol/temp/archetype-test/acme-subsystem/target/module/com/acme/corp/tracker/main
     [echo] Module com.acme.corp.tracker has been created in the target/module directory. Copy to your JBoss AS 7 installation.
[INFO] Executed tasks
[INFO]
[INFO] --- maven-install-plugin:2.3.1:install (default-install) @ acme-subsystem ---
[INFO] Installing /Users/kabir/sourcecontrol/temp/archetype-test/acme-subsystem/target/acme-subsystem.jar to /Users/kabir/.m2/repository/com/acme/corp/acme-subsystem/1.0-SNAPSHOT/acme-subsystem-1.0-SNAPSHOT.jar
[INFO] Installing /Users/kabir/sourcecontrol/temp/archetype-test/acme-subsystem/pom.xml to /Users/kabir/.m2/repository/com/acme/corp/acme-subsystem/1.0-SNAPSHOT/acme-subsystem-1.0-SNAPSHOT.pom
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 5.851s
[INFO] Finished at: Mon Jul 11 23:24:58 BST 2011
[INFO] Final Memory: 7M/81M
[INFO] ------------------------------------------------------------------------

This will have built our project and assembled a module for us that can be used for installing it into JBoss AS 7. If you go to the target/module folder where you built the project you will see the module

$ls target/module/com/acme/corp/tracker/main/
acme-subsystem.jar  module.xml

The module.xml comes from src/main/resources/module/main/module.xml and is used to define your module. It says that it contains the acme-subsystem.jar:

<module xmlns="urn:jboss:module:1.0" name="com.acme.corp.tracker">
    <resources>
        <resource-root path="acme-subsystem.jar"/>
    </resources>

And has a default set of dependencies needed by every subsystem created. If your subsystem requires additional module dependencies you can add them here before building and installing.

    <dependencies>
        <module name="javax.api"/>
        <module name="org.jboss.staxmapper"/>
        <module name="org.jboss.as.controller"/>
        <module name="org.jboss.as.server"/>
        <module name="org.jboss.modules"/>
        <module name="org.jboss.msc"/>
        <module name="org.jboss.logging"/>
        <module name="org.jboss.vfs"/>
    </dependencies>
</module>

Note that the name of the module corresponds to the directory structure containing it. Now copy the target/module/com/acme/corp/tracker/main/ directory and its contents to $AS7/modules/com/acme/corp/tracker/main/ (where $AS7 is the root of your JBoss AS 7 install).

Next we need to modify $AS7/standalone/configuration/standalone.xml. First we need to add our new module to the <extensions> section:

    <extensions>
        ...
        <extension module="org.jboss.as.weld"/>
        <extension module="com.acme.corp.tracker"/>
    </extensions>

And then we have to add our subsystem to the <profile> section:

    <profile>
    ...

        <subsystem xmlns="urn:com.acme.corp.tracker:1.0">
            <deployment-types>
                <deployment-type suffix="sar" tick="10000"/>
                <deployment-type suffix="war" tick="10000"/>
            </deployment-types>
        </subsystem>
    ...
    </profile>

Adding this to a managed domain works exactly the same apart from in this case you need to modify $AS7/domain/configuration/domain.xml.

Now start up JBoss AS 7 by running $AS7/bin/standalone.sh and you should see messages like these after the server has started, which means our subsystem has been added and our TrackerService is working:

15:27:33,838 INFO  [org.jboss.as] (Controller Boot Thread) JBoss AS 7.0.0.Final "Lightning" started in 2861ms - Started 94 of 149 services (55 services are passive or on-demand)
15:27:42,966 INFO  [stdout] (Thread-8) Current deployments deployed while sar tracking active:
15:27:42,966 INFO  [stdout] (Thread-8) []
15:27:42,967 INFO  [stdout] (Thread-8) Cool: 0
15:27:42,967 INFO  [stdout] (Thread-9) Current deployments deployed while war tracking active:
15:27:42,967 INFO  [stdout] (Thread-9) []
15:27:42,967 INFO  [stdout] (Thread-9) Cool: 0
15:27:52,967 INFO  [stdout] (Thread-8) Current deployments deployed while sar tracking active:
15:27:52,967 INFO  [stdout] (Thread-8) []
15:27:52,967 INFO  [stdout] (Thread-8) Cool: 0

If you run the command line interface you can execute some commands to see more about the subsystem. For example

[standalone@localhost:9999 /] /subsystem=tracker/:read-resource-description(recursive=true, operations=true)

will return a lot of information, including what we provided in the DescriptionProviders we created to document our subsystem.

To see the current subsystem state you can execute

[standalone@localhost:9999 /] /subsystem=tracker/:read-resource(recursive=true)
{
    "outcome" => "success",
    "result" => {"type" => {
        "war" => {"tick" => 10000L},
        "sar" => {"tick" => 10000L}
    }}
}

We can remove both the deployment types which removes them from the model:

[standalone@localhost:9999 /] /subsystem=tracker/type=sar:remove
{"outcome" => "success"}
[standalone@localhost:9999 /] /subsystem=tracker/type=war:remove
{"outcome" => "success"}
[standalone@localhost:9999 /] /subsystem=tracker/:read-resource(recursive=true)
{
    "outcome" => "success",
    "result" => {"type" => undefined}
}

You should now see the output from the TrackerService instances having stopped.

Now, let's add the war tracker again:

[standalone@localhost:9999 /] /subsystem=tracker/type=war:add
{"outcome" => "success"}
[standalone@localhost:9999 /] /subsystem=tracker/:read-resource(recursive=true)
{
    "outcome" => "success",
    "result" => {"type" => {"war" => {"tick" => 10000L}}}
}

and the JBoss AS 7 console should show the messages coming from the war TrackerService again.

Now let us deploy something. You can find two maven projects for test wars already built at test1.zip and test2.zip. If you download them and extract them to /Downloads/test1 and /Downloads/test2, you can see that /Downloads/test1/target/test1.war contains a META-INF/cool.txt while /Downloads/test2/target/test2.war does not contain that file. From CLI deploy test1.war first:

[standalone@localhost:9999 /] deploy ~/Downloads/test1/target/test1.war
'test1.war' deployed successfully.

And you should now see the output from the war TrackerService list the deployments:

15:35:03,712 INFO  [org.jboss.as.server.deployment] (MSC service thread 1-2) Starting deployment of "test1.war"
15:35:03,988 INFO  [org.jboss.web] (MSC service thread 1-1) registering web context: /test1
15:35:03,996 INFO  [org.jboss.as.server.controller] (pool-2-thread-9) Deployed "test1.war"
15:35:13,056 INFO  [stdout] (Thread-9) Current deployments deployed while war tracking active:
15:35:13,056 INFO  [stdout] (Thread-9) [test1.war]
15:35:13,057 INFO  [stdout] (Thread-9) Cool: 1

So our test1.war got picked up as a 'cool' deployment. Now if we deploy test2.war

[standalone@localhost:9999 /] deploy ~/sourcecontrol/temp/archetype-test/test2/target/test2.war
'test2.war' deployed successfully.

You will see that deployment get picked up as well but since there is no META-INF/cool.txt it is not marked as a 'cool' deployment:

15:37:05,634 INFO  [org.jboss.as.server.deployment] (MSC service thread 1-4) Starting deployment of "test2.war"
15:37:05,699 INFO  [org.jboss.web] (MSC service thread 1-1) registering web context: /test2
15:37:05,982 INFO  [org.jboss.as.server.controller] (pool-2-thread-15) Deployed "test2.war"
15:37:13,075 INFO  [stdout] (Thread-9) Current deployments deployed while war tracking active:
15:37:13,075 INFO  [stdout] (Thread-9) [test1.war, test2.war]
15:37:13,076 INFO  [stdout] (Thread-9) Cool: 1

An undeploy

[standalone@localhost:9999 /] undeploy test1.war
Successfully undeployed test1.war.

is also reflected in the TrackerService output:

15:38:47,901 INFO  [org.jboss.as.server.controller] (pool-2-thread-21) Undeployed "test1.war"
15:38:47,934 INFO  [org.jboss.as.server.deployment] (MSC service thread 1-3) Stopped deployment test1.war in 40ms
15:38:53,091 INFO  [stdout] (Thread-9) Current deployments deployed while war tracking active:
15:38:53,092 INFO  [stdout] (Thread-9) [test2.war]
15:38:53,092 INFO  [stdout] (Thread-9) Cool: 0

Finally, we registered a write attribute handler for the tick property of the type so we can change the frequency

[standalone@localhost:9999 /] /subsystem=tracker/type=war:write-attribute(name=tick,value=1000)
{"outcome" => "success"}

You should now see the output from the TrackerService happen every second

15:39:43,100 INFO  [stdout] (Thread-9) Current deployments deployed while war tracking active:
15:39:43,100 INFO  [stdout] (Thread-9) [test2.war]
15:39:43,101 INFO  [stdout] (Thread-9) Cool: 0
15:39:44,101 INFO  [stdout] (Thread-9) Current deployments deployed while war tracking active:
15:39:44,102 INFO  [stdout] (Thread-9) [test2.war]
15:39:44,105 INFO  [stdout] (Thread-9) Cool: 0
15:39:45,106 INFO  [stdout] (Thread-9) Current deployments deployed while war tracking active:
15:39:45,106 INFO  [stdout] (Thread-9) [test2.war]

If you open AS7/standalone/configuration/standalone.xml you can see that our subsystem entry reflects the current state of the subsystem:

        <subsystem xmlns="urn:com.acme.corp.tracker:1.0">
            <deployment-types>
                <deployment-type suffix="war" tick="1000"/>
            </deployment-types>
        </subsystem>

All JBoss AS 7 documentation

There are several guides in the JBoss Application Server 7 documentation series. This list gives an overview of each of the guides:

*Getting Started Guide - Explains how to download and start JBoss Application Server 7.
*Getting Started Developing Applications Guide - Talks you through developing your first applications on JBoss Application Server 7, and introduces you to JBoss Tools and how to deploy your applications.
*JavaEE 6 Tutorial - A Java EE 6 Tutorial.
*Admin Guide - Tells you how to configure and manage your JBoss Application Server 7 instances.
*Developer Guide - Contains concepts that you need to be aware of when developing applications for JBoss Application Server 7. Classloading is explained in depth.
*High Availability Guide - Reference guide for how to set up clustered JBoss Application Server 7 instances.
*Extending JBoss AS 7 - A guide to adding new functionality to JBoss Application Server 7.

JBoss.org Content Archive (Read Only), exported from JBoss Community Documentation Editor at 2020-03-13 13:20:07 UTC, last content change 2012-06-27 18:17:53 UTC.