Joran in your own applications
As apparent in previous chapters, logback relies on Joran, a mature, flexible and powerful configuration framework. Many of the capabilities offered by logback modules are only possible on account of Joran. This chapter focuses on Joran, its basic design and its salient features.
Joran is actually a generic configuration system which can be used independently of logging. To emphaises this point, we should mention that the logback-core module does not have a notion of loggers. In that spirit, most of the examples in this chapter have nothing to do with loggers, appenders or layouts.
The examples presented in this chapter can be found under LOGBACK_HOME/logback-examples/src/main/java/chapter10 folder.
To install joran, simply download logback and add logback-core-${version}.jar to your classpath.
Historical perspective
Reflection is a powerful feature of the Java language, making it possible to configure software systems declaratively. For example, many important properties of an EJB are configured with the ejb.xml file. While EJBs are written in Java, many of their properties are specified within the ejb.xml file. Similarly, logback settings can be specified in a configuration file, expressed in XML format. Annotations available in JDK 1.5 and heavily used in EJB 3.0 replace many of directives previously found in XML files. Joran also makes use of annonations but at a much smaller extent. Due to the dynamic nature of logback configuration data (compared to EJBs) Joran's use of annonations is rather limited.
In log4j, logback's predecessor, the
DOMConfigurator
class, which is part of log4j version
1.2.x and later, could also parse configuration files written in
XML. DOMConfigurator
was written in a way that forced
us, the developers, to tweak the code each time the structure of
the configuration file changed. The modified code had to be
recompiled and redeployed. Just as importantly, the code of the
DOMConfigurator
consisted of loops dealing with
children elements containing many interspersed if/else
statements. One could not help but notice that that particular
code reeked of redundancy and duplication. The commons-digester
project had shown us that it was possible to parse XML files
using pattern matching rules. At parse time, digester would apply
rules that matched designated patterns. Rule classes were usually
quite small and specialized. Consequently, they were relatively
easy to understand and maintain.
Armed with the DOMConfigurator
experience, we
began developing Joran
, a powerful configuration
framework to be used in logback. Joran was largely inspired by the
commons-digester project. Nevertheless, it uses a slightly
different terminology. In commons-digester, a rule can be seen as
consisting of a pattern and a rule, as shown by the
Digester.addRule(String pattern, Rule rule)
method. We find it unnecessarily confusing to have a rule to
consist of itself, not recursively but with a different
meaning. In Joran, a rule consists of a pattern and an action. An
action is invoked when a match occurs for the corresponding
pattern. This relation between patterns and actions lies at the
core of Joran. Quite remarkably, one can deal with quite complex
requirements by using simple patterns, or more precisely with
exact matches and wildcard matches.
SAX or DOM?
Due to the event-based architecture of the SAX API, a tool based on SAX cannot easily deal with forward references, that is, references to elements which are defined later than the current element being processed. Elements with cyclical references are equally problematic. More generally, the DOM API allows the user to perform searches on all the elements and make forward jumps.
This extra flexibility initially led us to choose the DOM API as the underlying parsing API for Joran. After some experimentation, it quickly became clear that dealing with jumps to distant elements while parsing the DOM tree did not make sense when the interpretation rules were expressed in the form of patterns and actions. Joran only needs to be given the elements in the XML document in a sequential, depth-first order.
Joran was first implemented in DOM. However, the author migrated to SAX in order to benefit from element location information, available only with the SAX API. Location information allows Joran to display the exact line and column number where an error occured, which comes in very handy in indentifying parsing problems.
Pattern
A Joran pattern is essentially a string. There are two kind of
patterns, exact and wildcard. The pattern
"a/b" can be used to match <b>
element nested
within a top-level <a>
element but not a
<c>
element, even if nested within a
<b>
element.
Wildcards can be used to match suffixes or prefixes. For
example, the "*/a" pattern can be used to match any suffix ending
with "a", that is any <a>
element within an XML
document but not any elements nested within <a>
.
The "a/*" pattern will match any element prefixed by
<a>
, that is any element nested within an
<a>
element.
When several rules match the current pattern, then exact matches override suffix matches, and suffix matches overide prefix matches. For exact details of implementation, please see the SimpleRuleStore class.
Actions
As mentioned above, Joran parsing rules consists of the
association of patterns. Actions extend the Action
class, consisting of the following abstract methods. Other methods
have been omitted for berevity.
package ch.qos.logback.core.joran.action; import org.xml.sax.Attributes; import ch.qos.logback.core.joran.spi.ExecutionContext; public abstract class Action { /** * Called when the parser encounters an element matching a * {@link ch.qos.logback.core.joran.spi.Pattern Pattern}. */ public abstract void begin(InterpretationContext ec, String name, Attributes attributes) throws ActionException; /* * Called when the parser encounters an endElement event matching a * {@link ch.qos.logback.core.joran.spi.Pattern Pattern}. */ public abstract void end(InterpretationContext ec, String name) throws ActionException; }
Thus, every action must implement the begin and end methods.
RuleStore
Interpretation context
To allow various actions to collaborate, the invocation of begin
and end methods include an interpretation context as the first
parameter. The interpretation context includes an object stack, an
object map, an error list and a reference to the Joran interpreter
invoking the action. Please see the InterpretationContext
class for the exact list of fields contained in the interpretation
context.
Actions can collaborate together by fetching, pushing or popping
objects from the common object stack, or by putting and fetching
keyed objects on the common object map. Actions can report any error
conditions by adding error items on the interpretation context's
StatusManager
.
Hello world
The first example in this chapter illustrates the minimal
plumbing required for using Joran. The example consists of a
trivial action called
HelloWorldAction
which prints "Hello World" on the
console when it's begin()
method is invoked. The
parsing of XML files is done by a configurator. For the purposes of
this chapter, we have developped a very simple configurator called
SimpleConfigurator
.
The HelloWorld
application brings all pieces together.
- It creates a map of rules and a
Context
- It creates a parsing rule by associating the
hello-world pattern with a corresponding
HelloWorldAction
instance - It creates a
SimpleConfigutator
, passing it the aforementioned rules map - it then invokes the
doConfigure
method of the configurator, passing the designated XML file as parameter - as a last step, the accumulated Status message in the context, if any, are printed
The hello.xml file contains one <hello-world> element, without any other nested elements. See the logback-examples/src/main/java/chapter10/helloWorld/ folder for exact contents.
Running the HelloWorld application with hello.xml file will print "Hello World" on the console.
java chapter10.helloWorld.HelloWorld src/main/java/chapter10/helloWorld/hello.xml
Collaborating actions
The logback-examples/src/main/java/joran/calculator/ directory includes several actions which collaborate together through the common object stack in order to accomplish simple computations.
The calculator1.xml file contains a
computation
element, with a nested
literal
element.
In the
Calculator1
class, we declare various patterns
and actions, that will collaborate and calculate a result based on
the xml file. The simple calculator1.xml file only
creates a computation and declares a literal value. The resulting
parsing is pretty simple:
- The
ComputationAction1
class'begin()
method is called - The
LiteralAction
class'begin()
andend()
methods are called - The
ComputationAction1
class'end()
method is called
What is interesting here is the way that the Actions
collaborate. The LiteralAction
reads a literal value
and pushes it in the object stack maintained by the
InterpretationContext
. Once done, any other action
can pop the value to read or modify it. Here, the
end()
method of the ComputationAction1
class pops the value from the stack and prints it.
The calculator2.xml file is a bit more complex, but much more interesting.
It contains the following elements:
Example 3.: Calculator configuration file (logback-examples/src/main/java/chapter3/calculator/calculator2.xml)<computation name="toto"> <literal value="7"/> <literal value="3"/> <add/> <literal value="3"/> <multiply/> </computation>
Here, there are obviously more actions that will be part of the computation.
When called, the
AddAction
class will remove the two integers at the
bottom of the stack, add them and push the resulting integer at the
top of the stack, for further use.
Later in the computation, the
MultiplyAction
class will be called. It will take
the last two integers from the stack, multiply them and push the
result in the stack.
We have here two examples of action whose begin()
method behaves in a certain, predictable way, but whose
end()
methods are empty.
Finally, a calculator3.xml is also provided, to demonstrate the possibility elements that contain instances of the same element. Here's the content of calculator3.xml:
Example 3.: Calculator configuration file (logback-examples/src/main/java/chapter3/calculator/calculator3.xml)<computation name="toto"> <computation> <literal value="7"/> <literal value="3"/> <add/> </computation> <literal value="3"/> <multiply/> </computation>
Much like the use of parentheses in an algebrical equation, the
presence of a computation
element nested in another is
managed by the
ComputationAction2
class using an internal
stack. The well-formedness of XML will guarantee that a value saved
by one begin()
will be consumed only by the matching
end()
method.
Implicit actions
The rules defined thus far are called explicit rules because they require an explicit pattern, hence fixing the tag name of the elements for which they apply.
In highly extensible systems, the number and type of components to handle are innumerable so that it would become very tedious or even impossible to list all the applicable patterns by name.
At the same time, even in highly extensible systems one can
observe well-defined patterns linking the various parts
together. Implicit rules come in very handy when processing
components composed of sub-components unknown ahead of time. For
example, Apache Ant is capable of handling tasks which contain tags
unknown at compile time by looking at methods whose names start with
add, as in addFile
, or
addClassPath
. When Ant encounters an embedded tag
within a task, it simply instantiates an object that matches the
signature of the task class' add method and attaches the resulting
object to the parent.
Joran includes similar capability in the form of implicit actions. Joran keeps a list of implicit actions which can be applied if no explicit pattern matches the current XML element. However, applying an implicit action may not be always appropriate. Before executing the implicit action, Joran asks an implicit action whether it is appropriate in the current context. Only if the action replies affirmatively does Joran interpreter invoke the (implicit) action. This extra step makes it possible to support multiple implicit actions or obviously none, if no implicit action is appropriate for a given situation.
For example, the
NestedComponentIA
extending
ImplicitAction
, will instantiate the class
specified in a nested component and attach it to the parent
component by using setter method of the parent component and the
nested element's name. Under certain circumstances, a nested action
needs to be applied to an element say <a> and also to another
element <b> nested within <a>. The current implementation of
NestedComponentIA
is capable of handling multiply
nested elements requiring intervention by the same implicit action.
Both ImplicitAction
and
NestedComponentIA
are located in the
ch.qos.logback.core.joran.action
package.
Refer to the logback-examples/src/main/java/joran/implicit directory for an example of an implicit action.
In that directory, you will find two actions classes, one xml file and one class containing the setup of Joran.
The
NOPAction
class does nothing. It is used to set the
context of the foo element, using this line:
ruleStore.addRule(new Pattern("*/foo"), new NOPAction());
After that, the implicit action, namely
PrintMeImplicitAction
, is added to the
RuleStore
. This is done by simply adding a new instance
of the action to the Joran interpreter
ji.addImplicitAction(new PrintMeImplicitAction());
When called, the isApplicable()
method of
PrintMeImplicitAction
checks the value of the
printme attribute. If the value is true
, the
implicit action is applicable: its begin()
method will
be called.
The implicit1.xml file contains the following lines:
Example 3.: Usage of implicit rules (logback-examples/src/main/java/chapter3/implicit/implicit1.xml)<foo> <xyz printme="true"> <abc printme="true"/> </xyz> <xyz/> <foo printme="true"/> </foo>
As you can see, the first element will be printed, since it has a
printme attribute, which bears the value
true
.
The second element will not be printed, because no printme attibute is present.
The last element will not be printed, although the required
attribute is present. This is because implicit rules are called
only if no explicit rules are defined. Since we added a
NOPAction
with the */foo pattern, it will be
used instead of the PrintMeImplicitAction
.
Running the example yields the following output:
Element <xyz> asked to be printed. Element <abc> asked to be printed. ERROR in ch.qos.logback.core.joran.spi.InterpretationContext@1c5c1 - no applicable action \ for <xyz>, current pattern is [/foo/xyz]
The last line was printed because of a call to
StatusPrinter
at the end of the main class.
New-rule action
Joran includes an action which allows the Joran interpreter to lean new rules on the fly while interpreting the XML file containing the new rules. See the logback-examples/src/main/java/joran/newRule/ directory for sample code.
In this package, the
NewRuleCalculator
class contains the same setup as
we have seen so far, but for one line:
ruleStore.addRule(new Pattern("/computation/new-rule"), new NewRuleAction());
By adding this line, we ask Joran to allow new rules to be learnt
at parsing time. It works pretty much like the other rules: it has a
begin()
and end()
method, and is called each time
the parser finds a new-rule element.
When called, the begin()
method looks for a
pattern and a actionClass attribute. The action
class is then instanciated and added to the RuleStore
,
along with its corresponding pattern.
Here is how new rules can be declared in an xml file:
<new-rule pattern="*/computation/literal" actionClass="chapter10.calculator.LiteralAction"/>
Using new rule declarations, the preceding example, involving the calculation, could be expressed this way:
Example 3.: Configuration file using new rules on the fly (logback-examples/src/main/java/chapter3/newrule/new-rule.xml)<computation name="toto"> <new-rule pattern="*/computation/literal" actionClass="chapter3.calculator.LiteralAction"/> <new-rule pattern="*/computation/add" actionClass="chapter3.calculator.AddAction"/> <new-rule pattern="*/computation/multiply" actionClass="chapter3.calculator.MultiplyAction"/> <computation> <literal value="7"/> <literal value="3"/> <add/> </computation> <literal value="3"/> <multiply/> </computation>
Non goals
The Joran API is not intended to be used to parse documents with thousands of elements.