-
Notifications
You must be signed in to change notification settings - Fork 57
Architecture
This is an overview of the components of this library as of 1.1.x.
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
.
This is a helper abstract class over Processor
. It uses ValueHolder
to wrap the input and output of a processor. As ValueHolder
implements MessageProvider
, it means you don't have to worry about implementing this interface.
ProcessorChain
allows to chain processors together: if the output of a processor p1
is compatible with the input 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();
There is one important difference with ProcessorMap
: predicates are evaluated in their order of appearance. As such, if you have more specific predicates, they should appear first.
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);
This class is a wrapper over a Processor
allowing two modes of operation: normal processing, and unchecked processing. In unchecked mode, all checked exceptions make it into the generated report instead of being thrown like they normally are. A ProcessingResult
gives access to the result and the report, and can also be used to declare success or failure:
// OUT is the output type of the processor
// Normal mode
final ProcessingResult<OUT> result = ProcessingResult.of(processor, report, input);
// Unchecked mode
final ProcessingResult<OUT> result = ProcessingResult.uncheckedResult(processor, report, input);
// Checking for success
result.isSuccess();
// Grab the output value
result.getResult();
// Grab the processing report
result.getReport();
Note that unchecked mode should not be abused of: it can mask exceptions which would be very important to treat by yourself. It can however be very useful for use with a processor you know cannot throw a checked exception.
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; - it uses
ProcessingResult
to operate in unchecked mode.
A schema walker can walk a JSON Schema recursively. While it walks the schema, it triggers certain events which a schema listener implementation can operate on. A listener produces a value which you can then use.
There are two distinct walkers: a simple walker, which will not try and resolve JSON References; and another one which will try and resolve them. Note that in the second case, the walking process can be interrupted not only if reference resolution fails, but also if reference processing would lead to information loss (resolving to a chile node of the current tree) or infinite recursion (resolving to a parent node of the current tree).
You can also use a walker as a processor. For this, you need to provide both a SchemaWalkerProvider
and a SchemaListenerProvider
at build time. The input is a JSON Schema (as a SchemaHolder
-- note that this class will be obsoleted), the output is the value produced by the listener, wrapped into a ValueHolder
.
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
.