Reactive streaming parser for EPCIS 2.0 JSON documents. Provides ObjectNodePublisher for non-blocking parsing of EPCIS events with full Reactive Streams backpressure support and TCK compliance.
- Non-blocking JSON parsing using Jackson's NonBlockingJsonParser
- Full backpressure support - stops requesting input when subscriber demand is satisfied
- Reactive Streams TCK compliant - verified against the official test compatibility kit
- Support for early-eventList documents via automatic retry mechanism
- Compatible with Mutiny Multi and other reactive frameworks (Project Reactor, RxJava)
- Netty ByteBuf support for zero-copy integration with Quarkus/Vert.x
- Minimal dependencies - no logging framework required (uses JDK Platform Logging)
<dependency>
<groupId>io.openepcis</groupId>
<artifactId>openepcis-reactive-event-publisher</artifactId>
<version>${openepcis.version}</version>
</dependency>// Simple usage
ObjectNodePublisher.fromInputStream(inputStream)
.toMulti()
.subscribe().with(node -> process(node));
// With custom buffer size
ObjectNodePublisher.fromInputStream(inputStream, 16384)
.toMulti()
.subscribe().with(node -> process(node));
// With retry support for early-eventList documents
ObjectNodePublisher.fromInputStream(inputStream, () -> new FileInputStream("data.json"))
.toMulti()
.subscribe().with(node -> process(node));// Blocking convenience method
byte[] json = Files.readAllBytes(path);
List<ObjectNode> nodes = ObjectNodePublisher.parseAll(json);
// Reactive
ObjectNodePublisher.fromBytes(json)
.toMulti()
.filter(EPCISNodes::isEvent)
.subscribe().with(event -> process(event));// From any Flow.Publisher<ByteBuffer>
Flow.Publisher<ByteBuffer> httpBody = ...;
ObjectNodePublisher.from(httpBody)
.toMulti()
.subscribe().with(node -> process(node));
// From Mutiny Multi
Multi<ByteBuffer> body = ...;
ObjectNodePublisher.fromMulti(body)
.toMulti()
.subscribe().with(node -> process(node));// With InputStream
ObjectNodePublisher.builder()
.inputStream(myInputStream)
.bufferSize(16384) // optional, default 65536 (64KB)
.retryInputStream(() -> new FileInputStream("data.json"))
.build();
// With reactive source
ObjectNodePublisher.builder()
.source(byteBufferPublisher)
.retrySource(() -> createNewPublisher())
.build();@POST
@Path("/events")
@Consumes(MediaType.APPLICATION_JSON)
public Multi<String> processEvents(Multi<ByteBuffer> body) throws IOException {
return ObjectNodePublisher.fromMulti(body)
.toMulti()
.filter(EPCISNodes::isEvent)
.map(event -> EPCISNodes.getType(event) + ": " + EPCISNodes.getEventTime(event));
}The EPCISNodes utility class provides convenient methods:
List<ObjectNode> nodes = ObjectNodePublisher.parseAll(json);
// Extract header and events
ObjectNode header = EPCISNodes.header(nodes).orElseThrow();
List<ObjectNode> events = EPCISNodes.events(nodes);
// Filter by event type
List<ObjectNode> objectEvents = EPCISNodes.filterByType(nodes, "ObjectEvent");
// Check node types
if (EPCISNodes.isHeader(node)) { ... }
if (EPCISNodes.isEvent(node)) { ... }
// Get common fields
String eventTime = EPCISNodes.getEventTime(event);
String bizStep = EPCISNodes.getBizStep(event);
String action = EPCISNodes.getAction(event);
// Counts and checks
long eventCount = EPCISNodes.countEvents(nodes);
boolean hasEvents = EPCISNodes.hasEvents(nodes);
boolean hasHeader = EPCISNodes.hasHeader(nodes);For Netty-based applications (Quarkus, Vert.x):
import io.openepcis.reactive.publisher.ByteBufSupport;
// From Mutiny Multi<ByteBuf>
Multi<ByteBuf> nettyBody = ...;
ByteBufSupport.fromMulti(nettyBody)
.toMulti()
.subscribe().with(node -> process(node));
// Quick one-liner (auto-releases ByteBuf)
List<ObjectNode> nodes = ByteBufSupport.parseAll(byteBuf);
// Check if Netty is available
if (ByteBufSupport.isAvailable()) {
// Netty is on classpath
}Note: Netty dependency is optional. Methods throw NoClassDefFoundError if Netty is not on classpath.
If you're using the old constructor-based API, it still works but is deprecated:
// OLD (deprecated) - still works for backward compatibility
new ObjectNodePublisher<>(inputStream);
new ObjectNodePublisher<>(inputStream, retryCallable);
new ObjectNodePublisher<>(reader); // requires commons-io on classpath
new ObjectNodePublisher<>(reader, retryCallable);
// NEW (recommended) - use factory methods
ObjectNodePublisher.fromInputStream(inputStream);
ObjectNodePublisher.fromInputStream(inputStream, retryCallable);
ObjectNodePublisher.fromPath(path); // auto-handles retry
ObjectNodePublisher.builder().inputStream(inputStream).build();Note: Reader constructors require Apache Commons IO (commons-io) on the classpath for ReaderInputStream support.
EPCIS JSON documents should have @context before epcisBody.eventList, but some producers emit them in different order:
// PROBLEM: eventList appears before @context
{"epcisBody":{"eventList":[...]}, "@context":[...], "type":"EPCISDocument"}When this happens, provide a retry callable to handle the second pass:
ObjectNodePublisher.fromInputStream(
inputStream,
() -> new FileInputStream("same-file.json") // Retry for second pass
);Without retry callable: early-eventList documents emit only the header, no events.
io.openepcis.reactive.publisher/
├── ObjectNodePublisher.java # Main reactive publisher (Flow.Publisher<ObjectNode>)
├── AsyncObjectNodeParser.java # Non-blocking JSON parser using Jackson
├── EPCISNodes.java # Utilities for working with parsed nodes
├── ByteBufSupport.java # Netty ByteBuf adapter (optional dependency)
└── ObjectNodeUtil.java # Internal validation utilities
Publishers are designed for single-threaded use. Subscribe and consume from one thread only. The publisher handles backpressure correctly - it will pause upstream requests when downstream demand is exhausted.
- Header - EPCISDocument ObjectNode (must have
type,@context,schemaVersion,creationDate) - Events - Each event as separate ObjectNode (with
typelikeObjectEvent,AggregationEvent, etc.)
Apache License 2.0