Skip to content
Francis Galiegue edited this page Apr 14, 2014 · 15 revisions

Introduction

This is an overview of the components of this library on the master branch.

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.

RawProcessor

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

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

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

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.

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

ProcessingResult

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.

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;
  • it uses ProcessingResult to operate in unchecked mode.

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.