Skip to content

CompositeCodec constructors require at least one delegate #10189

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,24 @@
import java.util.HashMap;
import java.util.Map;

import org.jspecify.annotations.Nullable;

import org.springframework.integration.util.ClassUtils;
import org.springframework.util.Assert;

/**
* A Codec that can delegate to one out of many Codecs, each mapped to a class.
* An implementation of {@link Codec} that combines multiple codecs into a single codec,
* delegating encoding and decoding operations to the appropriate type-specific codec.
* This implementation associates object types with their appropriate codecs while providing a fallback default codec
* for unregistered types.
* This class uses {@code ClassUtils.findClosestMatch} to select the appropriate codec for a given object type.
* When multiple codecs match an object type, {@code ClassUtils.findClosestMatch} offers the
* {@code failOnTie} option. If {@code failOnTie} is {@code false}, it will return any one of the matching codecs.
* If {@code failOnTie} is {@code true} and multiple codecs match, it will throw an {@code IllegalStateException}.
* {@link CompositeCodec} sets {@code failOnTie} to {@code true}, so if multiple codecs match, an
* {@code IllegalStateException} is thrown.
*
* @author David Turanski
* @author Glenn Renfro
*
* @since 4.2
*/
public class CompositeCodec implements Codec {
Expand All @@ -41,63 +51,38 @@ public class CompositeCodec implements Codec {

public CompositeCodec(Map<Class<?>, Codec> delegates, Codec defaultCodec) {
this.defaultCodec = defaultCodec;
this.delegates = new HashMap<Class<?>, Codec>(delegates);
}

public CompositeCodec(Codec defaultCodec) {
this(Map.of(), defaultCodec);
Assert.notEmpty(delegates, "delegates must not be empty");
this.delegates = new HashMap<>(delegates);
}

@Override
public void encode(Object object, OutputStream outputStream) throws IOException {
Assert.notNull(object, "cannot encode a null object");
Assert.notNull(outputStream, "'outputStream' cannot be null");
Codec codec = findDelegate(object.getClass());
if (codec != null) {
codec.encode(object, outputStream);
}
else {
this.defaultCodec.encode(object, outputStream);
}
findDelegate(object.getClass()).encode(object, outputStream);
}

@Override
public byte[] encode(Object object) throws IOException {
Assert.notNull(object, "cannot encode a null object");
Codec codec = findDelegate(object.getClass());
if (codec != null) {
return codec.encode(object);
}
else {
return this.defaultCodec.encode(object);
}
return findDelegate(object.getClass()).encode(object);
}

@Override
public <T> T decode(InputStream inputStream, Class<T> type) throws IOException {
Assert.notNull(inputStream, "'inputStream' cannot be null");
Assert.notNull(type, "'type' cannot be null");
Codec codec = findDelegate(type);
if (codec != null) {
return codec.decode(inputStream, type);
}
else {
return this.defaultCodec.decode(inputStream, type);
}
return findDelegate(type).decode(inputStream, type);
}

@Override
public <T> T decode(byte[] bytes, Class<T> type) throws IOException {
return decode(new ByteArrayInputStream(bytes), type);
}

private @Nullable Codec findDelegate(Class<?> type) {
if (this.delegates.isEmpty()) {
return null;
}

Class<?> clazz = ClassUtils.findClosestMatch(type, this.delegates.keySet(), false);
return this.delegates.get(clazz);
private Codec findDelegate(Class<?> type) {
Class<?> clazz = ClassUtils.findClosestMatch(type, this.delegates.keySet(), true);
return clazz == null ? this.defaultCodec : this.delegates.getOrDefault(clazz, this.defaultCodec);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,68 +17,75 @@
package org.springframework.integration.codec.kryo;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import org.springframework.integration.codec.Codec;
import org.springframework.integration.codec.CompositeCodec;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;

/**
* @author David Turanski
* @author Glenn Renfro
* @since 4.2
*/
public class CompositeCodecTests {

private Codec codec;

@BeforeEach
public void setup() {
Map<Class<?>, Codec> codecs = new HashMap<>();
this.codec = new CompositeCodec(codecs, new PojoCodec(
new KryoClassListRegistrar(SomeClassWithNoDefaultConstructors.class)));
}

@Test
public void testPojoSerialization() throws IOException {
SomeClassWithNoDefaultConstructors foo = new SomeClassWithNoDefaultConstructors("hello", 123);
SomeClassWithNoDefaultConstructors foo2 = this.codec.decode(
this.codec.encode(foo),
void testWithCodecDelegates() throws IOException {
Codec codec = getFullyQualifiedCodec();
SomeClassWithNoDefaultConstructors inputInstance = new SomeClassWithNoDefaultConstructors("hello", 123);
SomeClassWithNoDefaultConstructors outputInstance = codec.decode(
codec.encode(inputInstance),
SomeClassWithNoDefaultConstructors.class);
assertThat(foo2).isEqualTo(foo);
assertThat(outputInstance).isEqualTo(inputInstance);
}

static class SomeClassWithNoDefaultConstructors {
@Test
void testWithCodecDefault() throws IOException {
Codec codec = getFullyQualifiedCodec();
AnotherClassWithNoDefaultConstructors inputInstance = new AnotherClassWithNoDefaultConstructors("hello", 123);
AnotherClassWithNoDefaultConstructors outputInstance = codec.decode(
codec.encode(inputInstance),
AnotherClassWithNoDefaultConstructors.class);
assertThat(outputInstance).isEqualTo(inputInstance);
}

private String val1;
@Test
void testWithUnRegisteredClass() throws IOException {
// Verify that the default encodes and decodes properly
Codec codec = onlyDefaultCodec();
SomeClassWithNoDefaultConstructors inputInstance = new SomeClassWithNoDefaultConstructors("hello", 123);
SomeClassWithNoDefaultConstructors outputInstance = codec.decode(
codec.encode(inputInstance),
SomeClassWithNoDefaultConstructors.class);
assertThat(outputInstance).isEqualTo(inputInstance);

private int val2;
// Verify that an exception is thrown if an unknown type is to be encoded.
assertThatIllegalArgumentException().isThrownBy(() -> codec.decode(
codec.encode(inputInstance),
AnotherClassWithNoDefaultConstructors.class));
}

SomeClassWithNoDefaultConstructors(String val1, int val2) {
this.val1 = val1;
this.val2 = val2;
}
private static Codec getFullyQualifiedCodec() {
Map<Class<?>, Codec> codecs = Map.of(SomeClassWithNoDefaultConstructors.class, new PojoCodec(
new KryoClassListRegistrar(SomeClassWithNoDefaultConstructors.class)));
return new CompositeCodec(codecs, new PojoCodec(
new KryoClassListRegistrar(AnotherClassWithNoDefaultConstructors.class)));
}

@Override
public boolean equals(Object other) {
if (!(other instanceof SomeClassWithNoDefaultConstructors)) {
return false;
}
SomeClassWithNoDefaultConstructors that = (SomeClassWithNoDefaultConstructors) other;
return (this.val1.equals(that.val1) && this.val2 == that.val2);
}
private static Codec onlyDefaultCodec() {
PojoCodec pojoCodec = new PojoCodec();
Map<Class<?>, Codec> codecs = Map.of(java.util.Date.class, pojoCodec);
return new CompositeCodec(codecs, new PojoCodec(
new KryoClassListRegistrar(SomeClassWithNoDefaultConstructors.class)));
}

@Override
public int hashCode() {
int result = this.val1.hashCode();
result = 31 * result + this.val2;
return result;
}
private record SomeClassWithNoDefaultConstructors(String val1, int val2) { }

}
private record AnotherClassWithNoDefaultConstructors(String val1, int val2) { }

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@
import java.io.ByteArrayOutputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import javax.net.SocketFactory;
Expand All @@ -35,7 +35,6 @@
import org.springframework.integration.IntegrationMessageHeaderAccessor;
import org.springframework.integration.codec.Codec;
import org.springframework.integration.codec.CodecMessageConverter;
import org.springframework.integration.codec.CompositeCodec;
import org.springframework.integration.codec.kryo.MessageCodec;
import org.springframework.integration.ip.IpHeaders;
import org.springframework.integration.ip.tcp.serializer.MapJsonSerializer;
Expand All @@ -56,6 +55,7 @@
* @author Gary Russell
* @author Artem Bilan
* @author Gengwu Zhao
* @author Glenn Renfro
* @since 2.0
*
*/
Expand All @@ -67,8 +67,7 @@ public class TcpMessageMapperTests {

@BeforeEach
public void setup() {
Map<Class<?>, Codec> codecs = new HashMap<>();
this.codec = new CompositeCodec(codecs, new MessageCodec());
this.codec = new MessageCodec();
}

@Test
Expand Down Expand Up @@ -339,7 +338,7 @@ public void testMapMessageConvertingOutboundJson() throws Exception {
MapJsonSerializer serializer = new MapJsonSerializer();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
serializer.serialize(map, baos);
assertThat(new String(baos.toByteArray(), "UTF-8"))
assertThat(baos.toString(StandardCharsets.UTF_8))
.isEqualTo("{\"headers\":{\"bar\":\"baz\"},\"payload\":\"foo\"}\n");
}

Expand Down
66 changes: 65 additions & 1 deletion src/reference/antora/modules/ROOT/pages/codec.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,11 @@ See the https://docs.spring.io/spring-integration/api/org/springframework/integr
[[kryo]]
== Kryo

Currently, this is the only implementation of `Codec`, and it provides two kinds of `Codec`:
Currently, this is the only implementation of `Codec`, and it provides three kinds of `Codec`:

* `PojoCodec`: Used in the transformers
* `MessageCodec`: Used in the `CodecMessageConverter`
* `CompositeCodec`: Used in transformers

The framework provides several custom serializers:

Expand All @@ -53,6 +54,69 @@ The framework provides several custom serializers:
The first can be used with the `PojoCodec` by initializing it with the `FileKryoRegistrar`.
The second and third are used with the `MessageCodec`, which is initialized with the `MessageKryoRegistrar`.

[[composite-codec]]
=== CompositeCodec
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blank line after title.

The CompositeCodec is a codec that combines multiple codecs into a single codec, delegating encoding and decoding operations to the appropriate type-specific codec.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrap CompositeCodec into a code snippet.

This implementation associates object types with their appropriate codecs while providing a fallback default codec for unregistered types.

An example implementation can be seen below:
```java
void encodeDecodeSample() {
Codec codec = getFullyQualifiedCodec();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not visible here, but we need to be sure that no tab indents in docs.


//Encode and Decode a Dog Object
Dog dog = new Dog("Wolfy", 3, "woofwoof");
dog = codec.decode(
codec.encode(dog),
Dog.class);
System.out.println(dog);

//Encode and Decode a Cat Object
Cat cat = new Cat("Kitty", 2, 8);
cat = codec.decode(
codec.encode(cat),
Cat.class);
System.out.println(cat);

//Use the default code if the type being decoded and encoded is not Cat or dog.
Animal animal = new Animal("Badger", 5);
Animal animalOut = codec.decode(
codec.encode(animal),
Animal.class);
System.out.println(animalOut);
}

/**
* Create and return a {@link CompositeCodec} that associates {@code Dog} and {@code Cat}
* classes with their respective {@link PojoCodec} instances, while providing a default
* codec for {@code Animal} types.
* <p>
* @return a fully qualified {@link CompositeCodec} for {@code Dog}, {@code Cat},
* and fallback for {@code Animal}
*/
static Codec getFullyQualifiedCodec() {
Map<Class<?>, Codec> codecs = new HashMap<Class<?>, Codec>();
codecs.put(Dog.class, new PojoCodec(new KryoClassListRegistrar(Dog.class)));
codecs.put(Cat.class, new PojoCodec(new KryoClassListRegistrar(Cat.class)));
return new CompositeCodec(codecs, new PojoCodec(
new KryoClassListRegistrar(Animal.class)));
}

// Records that will be encoded and decoded in this sample
record Dog(String name, int age, String tag) {}
record Cat(String name, int age, int lives) {}
record Animal(String name, int age){}
```

In some cases a single type of object may return multiple codecs. In these cases an `IllegalStateException` is thrown.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


NOTE: This class uses `ClassUtils.findClosestMatch` to select the appropriate codec for a given object type.
When multiple codecs match an object type, `ClassUtils.findClosestMatch` offers the `failOnTie` option.
If `failOnTie` is `false`, it will return any one of the matching codecs.
If `failOnTie` is `true` and multiple codecs match, it will throw an `IllegalStateException`.
CompositeCodec` sets `failOnTie` to `true`, so if multiple codecs match, an
`IllegalStateException` is thrown.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DITTO: One sentence per line


[[customizing-kryo]]
=== Customizing Kryo

Expand Down