Skip to content
fge edited this page Feb 21, 2013 · 15 revisions

Introduction

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.

Processors, processor chains and selectors

This is, with logging, the core component of this library.

Processor

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

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

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

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();

CachingProcessor

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);

Putting all this together

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.

Exceptions

Checked exceptions

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

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

Introduction

Logging is based on two components: reports and messages. Each of these are configurable according to your needs.

Messages

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 a Processor 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 of ProcessingMessage 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.

Reports

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.

Clone this wiki locally