-
Notifications
You must be signed in to change notification settings - Fork 57
Architecture
This is an overview of the components of this library.
Note that it hasn't reached 1.0 state, and the API may still change while it reaches a stable state.
This is, with logging, the core component of this library.
Processors are the main unit of work. The main interface is Processor<IN, OUT>
. It has four helper classes: ProcessorChain
, ProcessorMap
, ProcessorSelector
and CachingProcessor
.
The Processor
interface itself is quite simple:
public interface Processor<IN extends MessageProvider, OUT extends MessageProvider>
{
OUT process(final ProcessingReport report, final IN input)
throws ProcessingException;
}
See the section about logging for more explanations about MessageProvider
.
ProcessorChain
allows to chain processors together: if the output of a processor p1
is compatible with the output of processor p2
, then you can create a new processor p
which is the result of p2
applied to the output of p1
like so:
Processor<X, Y> p1;
Processor<Y, Z> p2;
// Chain them:
final Processor<X, Z> p = ProcessorChain.startWith(p1).chainWith(p2).getProcessor();
This class also provides a .failOnError()
method which will have the effect to abort processing with an exception if the previous processor in the chain returned an error (that is, the associated report declares failure, see below).
ProcessorMap
allows to create a Processor<IN, OUT>
based on a key K
extracted from an input IN
. It is an abstract class which, when you extend it, asks that you implement this method:
protected abstract Function<IN, K> f();
This Function<IN, K>
is what will compute the key out of the input. Of course, you should ensure that the key extracted is actually usable as a Map
key (that is, it obeys the equals()
/hashCode()
contract. When implemented, you can do this:
final ProcessorMap<K, IN, OUT> myMap = new MyMapper()
.addEntry(k1, p1).addEntry(k2, p2).addEntry(k3, p3)
.setDefaultProcessor(defaultProcessor);
final Processor<IN, OUT> processor = myMap.getProcessor();
Note that if you do not set a default processor and no suitable key is found in the map, processing fails with an exception.
ProcessorSelector
can be viewed as ProcessorMap
on steroids. It allows you to select the processor to execute not based on a single key, but on an arbitrary predicate based on the input. Like ProcessorMap
, it may have a default processor when no predicate matches, and like ProcessorMap
, it will throw an exception if there is no default processor and all predicates failed.
Guava's aptly-named Predicate
is used. Sample usage:
final Processor<IN, OUT> processor = new ProcessorSelector<IN, OUT>()
.when(predicate1).then(p1)
.when(predicate2).then(p2)
.otherwise(defaultProcessor)
.getProcessor();
This class implements Processor
, and has the ability to cache the results of previous computations. The most simple usage is:
final Processor<IN, OUT> processor = new CachingProcessor<IN, OUT>(baseProcessor);
You can also use an Equivalence<IN>
on the input as the second argument: this way, if two instances of an input are deemed equivalent, they will be only cached once:
final Processor<IN, OUT> processor = new CachingProcessor<IN, OUT>(baseProcessor, inputEquivalence);
Given that all these utility classes output processors, you can create arbitrarily complex chains. For instance, json-schema-validator uses a combination of the following:
- it uses
ProcessorChain
to chain together ref resolving, syntax validation, schema digesting, keyword building; - it uses
ProcessorMap
to act according to the schema's declared$schema
; - it uses
CachingProcessor
to cache the results of previously computed references and keyword builds.
Checked exceptions are ProcessingException
and derivates. This is the base exception issued by processors when they either throw it by themselves or if the exception threshold of a report has been reached (see below).
Unchecked exceptions are ProcessingError
and derivates. They are meant to be issued when a configuration error occurs, or illegal inputs are detected (such as null values where they are not allowed etc).
As such, they are separate from "normal" processing exceptions.
Logging is based on two components: reports and messages. Each of these are configurable according to your needs.
The main class of a message is ProcessingMessage
. It has various .put()
methods, all returning this
so you can chain them.
There are two closely related interfaces:
-
MessageProvider
is an interface which all processor inputs and outputs must implement: its role is to provide aProcessor
with a message template generated according to the input; -
ExceptionProvider
is an interface which is implementable and which can be set on a message (via.setExceptionProvider()
): this will be used by the.asException()
method ofProcessingMessage
to provide an exception to throw, which also depends on the message.
For instance, if you have a MyProcessorException
exception which you wish to be issued if a message is logged at a level raising an exception, you can write your MessageProvider
of your input like so:
@Override
public ProcessingMessage newMessage()
{
return new ProcessingMessage().put("value", myValue).setExceptionProvider(new ExceptionProvider()
{
@Override
public ProcessingException doException(final ProcessingMessage message)
{
return new MyProcessorException(message);
}
});
}
You therefore need not worry about what type of exception will be thrown: it will be built into each message you will obtain from your processing input.
The main interface is ProcessingReport
. While you could implement this interface directly, it is recommended that you extend AbstractProcessingReport
instead, which ensures that the (documented) ProcessingReport
contract is obeyed (extending this latter class only requires you to implement one method, versus the 8 of ProcessingReport
).
There are two main configurable features of a processing report:
- its log level: all messages with a log level strictly lower than this log level will not be taken into account;
- its exception threshold: all messages with a log level greater than, or equal to, this threshold will raise an exception instead of being logged.
A report has the classical .debug()
, .info()
, .warn()
and .error()
methods which you can use in your reports. A report will be considered a failure if any error message is being injected into the report. Finally, there is also a .mergeWith()
which allows to merge two reports together.
The library comes with two implementations: ListProcessingReport
, which stores all processing messages in a List
, and ConsoleProcessingReport
, which prints all messages to System.out
.