Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 136 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -604,13 +604,149 @@ project(':sbe-samples') {
}
}

project(':sbe-jackson') {
apply plugin: 'maven-publish'
apply plugin: 'signing'

dependencies {
api project(':sbe-tool')
api libs.jackson.databind
}

def generatedDir = "${layout.buildDirectory.get()}/generated-src"
def generatedClassesDir = "${layout.buildDirectory.get()}/classes/java/generated"
sourceSets {
generated {
java.srcDir generatedDir
compileClasspath += project(':sbe-tool').sourceSets.main.runtimeClasspath
}
test {
resources.srcDir project(':sbe-tool').file('src/test/resources')
}
}

compileGeneratedJava.dependsOn 'generateTestCodecs'
compileTestJava.dependsOn compileGeneratedJava

testing {
suites {
test {
dependencies {
implementation files(generatedClassesDir)
implementation libs.hamcrest
implementation platform(libs.junit.bom)
implementation "org.junit.jupiter:junit-jupiter-params"
implementation(libs.jqwik) {
// Exclude JUnit 5 dependencies that are already provided due to useJUnitJupiter
exclude group: 'org.junit.platform', module: 'junit-platform-commons'
exclude group: 'org.junit.platform', module: 'junit-platform-engine'
}
}
}
}
}

tasks.register('generateTestCodecs', JavaExec) {
mainClass.set('uk.co.real_logic.sbe.SbeTool')
classpath = project(':sbe-tool').sourceSets.main.runtimeClasspath
jvmArgs('--add-opens', 'java.base/jdk.internal.misc=ALL-UNNAMED')
systemProperties(
'sbe.output.dir': generatedDir.toString(),
'sbe.target.language': 'Java',
'sbe.validation.stop.on.error': 'true',
'sbe.validation.xsd': validationXsdPath.toString(),
'sbe.generate.precedence.checks': 'false')
def schemaDir = project(':sbe-tool').file('src/test/resources')
def schemas = [new File(schemaDir, 'json-printer-test-schema.xml'),
new File(schemaDir, 'example-extension-schema.xml'),
new File(schemaDir, 'composite-elements-schema.xml'),
new File(schemaDir, 'group-with-data-schema.xml')]
args = schemas.collect { it.path }
inputs.files(schemas)
inputs.file(validationXsdPath)
inputs.files(project(':sbe-tool').sourceSets.main.runtimeClasspath)
outputs.dir(generatedDir)
doFirst {
delete generatedDir
}
}

// Runs the test suite a second time against the latest Jackson 2.21.x release.
configurations {
latestJacksonTestRuntimeClasspath {
extendsFrom testRuntimeClasspath
resolutionStrategy.force "com.fasterxml.jackson.core:jackson-databind:${libs.versions.jackson.latest.get()}"
}
}

tasks.register('testLatestJackson', Test) {
description = 'Runs the sbe-jackson tests against the latest Jackson 2.21.x release.'
group = 'verification'
dependsOn 'compileTestJava'
testClassesDirs = sourceSets.test.output.classesDirs
classpath = sourceSets.test.output + sourceSets.main.output +
files(generatedClassesDir) + configurations.latestJacksonTestRuntimeClasspath
useJUnitPlatform()
jvmArgs('--add-opens', 'java.base/jdk.internal.misc=ALL-UNNAMED')
javaLauncher.set(toolchainLauncher)
testLogging {
exceptionFormat = 'full'
events = ["FAILED", "STANDARD_OUT", "STANDARD_ERROR"]
}
}

jar {
manifest.attributes(
'Specification-Title': 'Simple Binary Encoding',
'Specification-Version': '1.0',
'Implementation-Title': 'SBE',
'Implementation-Version': sbeVersion,
'Implementation-Vendor': 'Adaptive Financial Consulting Limited',
'Automatic-Module-Name': 'uk.co.real_logic.sbe.jackson'
)
}

java {
withSourcesJar()
withJavadocJar()
}

publishing {
publications {
sbeJackson(MavenPublication) {
from components.java
pom(projectPom)
}
}

repositories {
maven {
url = !isReleaseVersion ? sonatypeCentralPortalSnapshotsRepoUrl : sonatypeCentralPortalReleasesRepoUrl
credentials {
username = sonatypeCentralPortalUsername
password = sonatypeCentralPortalPassword
}
}
}
}

signing {
if (signingKey != null) {
useInMemoryPgpKeys(signingKey, signingPassword)
}
sign publishing.publications.sbeJackson
}
}

project(':sbe-benchmarks') {
apply plugin: 'com.gradleup.shadow'

dependencies {
implementation libs.jmh.core
annotationProcessor libs.jmh.generator.annprocess
implementation project(':sbe-tool')
implementation project(':sbe-jackson')
implementation libs.jackson.databind
implementation files("${layout.buildDirectory.get()}/classes/java/generated")
}

Expand Down
3 changes: 3 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ commons-codec = "1.15"
commons-lang3 = "3.8.1"
hamcrest = "3.0"
httpcore = "4.4.14"
jackson = "2.16.1"
jackson-latest = "2.21.6"
jqwik = "1.10.1"
jmh = "1.37"
json = "20260814"
Expand All @@ -20,6 +22,7 @@ commons-codec = { group = "commons-codec", name = "commons-codec", version.ref =
commons-lang3 = { group = "org.apache.commons", name = "commons-lang3", version.ref = "commons-lang3" }
hamcrest = { group = "org.hamcrest", name = "hamcrest", version.ref = "hamcrest" }
httpcore = { group = "org.apache.httpcomponents", name = "httpcore", version.ref = "httpcore" }
jackson-databind = { group = "com.fasterxml.jackson.core", name = "jackson-databind", version.ref = "jackson" }
jqwik = { group = "net.jqwik", name = "jqwik", version.ref = "jqwik" }
json = { group = "org.json", name = "json", version.ref = "json" }
jmh-core = { group = "org.openjdk.jmh", name = "jmh-core", version.ref = "jmh" }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
/*
* Copyright 2013-2025 Real Logic Limited.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package uk.co.real_logic.sbe.benchmarks;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.agrona.DirectBuffer;
import uk.co.real_logic.sbe.PrimitiveType;
import uk.co.real_logic.sbe.ir.Encoding;
import uk.co.real_logic.sbe.ir.Token;
import uk.co.real_logic.sbe.otf.AbstractTokenListener;
import uk.co.real_logic.sbe.otf.Types;

import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.List;

/**
* Deliberately simple stock-node baseline for this car.xml corpus, not a general SBE adapter.
* Re-walks tokens and enum values on every decode; no compiled field plan or cached value nodes.
* Optional fields, version transitions, binary data and uint64 are outside this corpus.
*/
final class NaiveCarTokenListener extends AbstractTokenListener
{
private static final JsonNodeFactory FACTORY = JsonNodeFactory.instance;
private final ObjectNode[] objects = new ObjectNode[8];
private final ArrayNode[] groups = new ArrayNode[8];
private int depth;
private int compositeDepth;

ObjectNode root()
{
return objects[0];
}

@Override
public void onBeginMessage(final Token token)
{
depth = 0;
compositeDepth = 0;
objects[0] = FACTORY.objectNode();
}

@Override
public void onEncoding(
final Token fieldToken, final DirectBuffer buffer, final int bufferIndex,
final Token typeToken, final int actingVersion)
{
final Encoding encoding = typeToken.encoding();
final String name = compositeDepth > 0 ? typeToken.name() : fieldToken.name();
final PrimitiveType type = encoding.primitiveType();
final JsonNode value;
if (typeToken.isConstantEncoding())
{
value = PrimitiveType.CHAR == type ? FACTORY.textNode(encoding.constValue().toString()) :
integerNode(type, encoding.constValue().longValue());
}
else if (PrimitiveType.CHAR == type)
{
final byte[] bytes = new byte[typeToken.arrayLength()];
buffer.getBytes(bufferIndex, bytes);
int length = 0;
while (length < bytes.length && bytes[length] != 0)
{
length++;
}
value = FACTORY.textNode(new String(bytes, 0, length, StandardCharsets.US_ASCII));
}
else if (typeToken.arrayLength() > 1)
{
final ArrayNode array = FACTORY.arrayNode();
for (int i = 0; i < typeToken.arrayLength(); i++)
{
array.add(primitive(buffer, bufferIndex + i * type.size(), encoding));
}
value = array;
}
else
{
value = primitive(buffer, bufferIndex, encoding);
}
objects[depth].set(name, value);
}

@Override
public void onEnum(
final Token fieldToken, final DirectBuffer buffer, final int bufferIndex,
final List<Token> tokens, final int fromIndex, final int toIndex, final int actingVersion)
{
final long raw = Types.getLong(buffer, bufferIndex, tokens.get(fromIndex + 1).encoding());
for (int i = fromIndex + 1; i < toIndex; i++)
{
final Token value = tokens.get(i);
if (raw == value.encoding().constValue().longValue())
{
objects[depth].put(fieldToken.name(), value.name());
return;
}
}
objects[depth].set(fieldToken.name(), integerNode(tokens.get(fromIndex + 1).encoding().primitiveType(), raw));
}

@Override
public void onBitSet(
final Token fieldToken, final DirectBuffer buffer, final int bufferIndex,
final List<Token> tokens, final int fromIndex, final int toIndex, final int actingVersion)
{
final Encoding encoding = tokens.get(fromIndex + 1).encoding();
objects[depth].set(fieldToken.name(), integerNode(encoding.primitiveType(),
Types.getLong(buffer, bufferIndex, encoding)));
}

@Override
public void onBeginComposite(
final Token fieldToken, final List<Token> tokens, final int fromIndex, final int toIndex)
{
final ObjectNode child = FACTORY.objectNode();
objects[depth].set(fieldToken.name(), child);
objects[++depth] = child;
compositeDepth++;
}

@Override
public void onEndComposite(
final Token fieldToken, final List<Token> tokens, final int fromIndex, final int toIndex)
{
objects[depth--] = null;
compositeDepth--;
}

@Override
public void onGroupHeader(final Token token, final int numInGroup)
{
final ArrayNode group = FACTORY.arrayNode();
objects[depth].set(token.name(), group);
groups[depth] = numInGroup == 0 ? null : group;
}

@Override
public void onBeginGroup(final Token token, final int groupIndex, final int numInGroup)
{
final ObjectNode entry = FACTORY.objectNode();
groups[depth].add(entry);
objects[++depth] = entry;
}

@Override
public void onEndGroup(final Token token, final int groupIndex, final int numInGroup)
{
objects[depth--] = null;
if (groupIndex + 1 == numInGroup)
{
groups[depth] = null;
}
}

@Override
public void onVarData(
final Token fieldToken, final DirectBuffer buffer, final int bufferIndex,
final int length, final Token typeToken)
{
final byte[] bytes = new byte[length];
buffer.getBytes(bufferIndex, bytes);
objects[depth].put(fieldToken.name(),
new String(bytes, Charset.forName(typeToken.encoding().characterEncoding())));
}

private static JsonNode primitive(final DirectBuffer buffer, final int index, final Encoding encoding)
{
switch (encoding.primitiveType())
{
case FLOAT:
return FACTORY.numberNode(buffer.getFloat(index, encoding.byteOrder()));

case DOUBLE:
return FACTORY.numberNode(buffer.getDouble(index, encoding.byteOrder()));

default:
return integerNode(encoding.primitiveType(), Types.getLong(buffer, index, encoding));
}
}

private static JsonNode integerNode(final PrimitiveType type, final long value)
{
return PrimitiveType.UINT32 == type || PrimitiveType.INT64 == type ?
FACTORY.numberNode(value) : FACTORY.numberNode((int)value);
}
}
Loading
Loading