Skip to content

Commit d731e1c

Browse files
authored
CompositeCodec constructors require at least one delegate
* Adjust the behavior of the class so that delegates are required. * In the current implementation, users could pass in an empty list to the composite codec. * This is an CompositeCodec and if we had no codecs, then why have the class. * All @nullable's were removed from the delegate attribute. * Null returns from the findDelegate have been removed. The default delegate will be returned in the stead. * Update the tests to require at least one codec in the delegates * Update code to meet code review requirements * Add Author tag to affected classes * Add tests to the CompositeCodecTests to tests delegates And also test if a user attempts to an invalid class. * Update deprecated ctor so that it will only establish default codec * Simplify the initialization of the `codec` attribute * Update the javadoc for the CompositeCodec class. Change the `ClassUtils.findClosestMatch` `failOnTie` to true. In the past when finding the appropriate delegate(codec) for a object and their were multiple delegates available the first one was returned. The problem is it could return a different delegate on each launch of the `findDelegate`. Now with `failOnTie` set to true a `IllegalStateException` is returned. Remove deprecated constructor from CompositeCodec * Replace sample classes in CompositeCodecTests with records to reduce maintenance Rename field names of foo and foo2 in CompositeCodecTests to professional descriptive names In the onlyDefaultCodec implementation in the CompositeCodecTests use a class other than a PojoCodec class to simulate a real world case. Enhance javadoc for CompositeCodec to include information on ClassUtils.findClosestMatch and its effects on codec selection. Make sure @see is in the correct location in javadoc * Add CompositeCodec documentation to reference documentation Provide example for using CompositeCodec. Remove @see in CompositeCodec javadocs. This is handled in the links in the javadoc * In codec.adoc all sentences must be on same line Add code snippet into docs for classes Verify no tabs exist in sample
1 parent cb57c96 commit d731e1c

File tree

4 files changed

+136
-80
lines changed

4 files changed

+136
-80
lines changed

spring-integration-core/src/main/java/org/springframework/integration/codec/CompositeCodec.java

Lines changed: 21 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,24 @@
2323
import java.util.HashMap;
2424
import java.util.Map;
2525

26-
import org.jspecify.annotations.Nullable;
27-
2826
import org.springframework.integration.util.ClassUtils;
2927
import org.springframework.util.Assert;
3028

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

4252
public CompositeCodec(Map<Class<?>, Codec> delegates, Codec defaultCodec) {
4353
this.defaultCodec = defaultCodec;
44-
this.delegates = new HashMap<Class<?>, Codec>(delegates);
45-
}
46-
47-
public CompositeCodec(Codec defaultCodec) {
48-
this(Map.of(), defaultCodec);
54+
Assert.notEmpty(delegates, "delegates must not be empty");
55+
this.delegates = new HashMap<>(delegates);
4956
}
5057

5158
@Override
5259
public void encode(Object object, OutputStream outputStream) throws IOException {
5360
Assert.notNull(object, "cannot encode a null object");
5461
Assert.notNull(outputStream, "'outputStream' cannot be null");
55-
Codec codec = findDelegate(object.getClass());
56-
if (codec != null) {
57-
codec.encode(object, outputStream);
58-
}
59-
else {
60-
this.defaultCodec.encode(object, outputStream);
61-
}
62+
findDelegate(object.getClass()).encode(object, outputStream);
6263
}
6364

6465
@Override
6566
public byte[] encode(Object object) throws IOException {
6667
Assert.notNull(object, "cannot encode a null object");
67-
Codec codec = findDelegate(object.getClass());
68-
if (codec != null) {
69-
return codec.encode(object);
70-
}
71-
else {
72-
return this.defaultCodec.encode(object);
73-
}
68+
return findDelegate(object.getClass()).encode(object);
7469
}
7570

7671
@Override
7772
public <T> T decode(InputStream inputStream, Class<T> type) throws IOException {
7873
Assert.notNull(inputStream, "'inputStream' cannot be null");
7974
Assert.notNull(type, "'type' cannot be null");
80-
Codec codec = findDelegate(type);
81-
if (codec != null) {
82-
return codec.decode(inputStream, type);
83-
}
84-
else {
85-
return this.defaultCodec.decode(inputStream, type);
86-
}
75+
return findDelegate(type).decode(inputStream, type);
8776
}
8877

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

94-
private @Nullable Codec findDelegate(Class<?> type) {
95-
if (this.delegates.isEmpty()) {
96-
return null;
97-
}
98-
99-
Class<?> clazz = ClassUtils.findClosestMatch(type, this.delegates.keySet(), false);
100-
return this.delegates.get(clazz);
83+
private Codec findDelegate(Class<?> type) {
84+
Class<?> clazz = ClassUtils.findClosestMatch(type, this.delegates.keySet(), true);
85+
return clazz == null ? this.defaultCodec : this.delegates.getOrDefault(clazz, this.defaultCodec);
10186
}
10287

10388
}

spring-integration-core/src/test/java/org/springframework/integration/codec/kryo/CompositeCodecTests.java

Lines changed: 45 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -17,68 +17,75 @@
1717
package org.springframework.integration.codec.kryo;
1818

1919
import java.io.IOException;
20-
import java.util.HashMap;
2120
import java.util.Map;
2221

23-
import org.junit.jupiter.api.BeforeEach;
2422
import org.junit.jupiter.api.Test;
2523

2624
import org.springframework.integration.codec.Codec;
2725
import org.springframework.integration.codec.CompositeCodec;
2826

2927
import static org.assertj.core.api.Assertions.assertThat;
28+
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
3029

3130
/**
3231
* @author David Turanski
32+
* @author Glenn Renfro
3333
* @since 4.2
3434
*/
3535
public class CompositeCodecTests {
3636

37-
private Codec codec;
38-
39-
@BeforeEach
40-
public void setup() {
41-
Map<Class<?>, Codec> codecs = new HashMap<>();
42-
this.codec = new CompositeCodec(codecs, new PojoCodec(
43-
new KryoClassListRegistrar(SomeClassWithNoDefaultConstructors.class)));
44-
}
45-
4637
@Test
47-
public void testPojoSerialization() throws IOException {
48-
SomeClassWithNoDefaultConstructors foo = new SomeClassWithNoDefaultConstructors("hello", 123);
49-
SomeClassWithNoDefaultConstructors foo2 = this.codec.decode(
50-
this.codec.encode(foo),
38+
void testWithCodecDelegates() throws IOException {
39+
Codec codec = getFullyQualifiedCodec();
40+
SomeClassWithNoDefaultConstructors inputInstance = new SomeClassWithNoDefaultConstructors("hello", 123);
41+
SomeClassWithNoDefaultConstructors outputInstance = codec.decode(
42+
codec.encode(inputInstance),
5143
SomeClassWithNoDefaultConstructors.class);
52-
assertThat(foo2).isEqualTo(foo);
44+
assertThat(outputInstance).isEqualTo(inputInstance);
5345
}
5446

55-
static class SomeClassWithNoDefaultConstructors {
47+
@Test
48+
void testWithCodecDefault() throws IOException {
49+
Codec codec = getFullyQualifiedCodec();
50+
AnotherClassWithNoDefaultConstructors inputInstance = new AnotherClassWithNoDefaultConstructors("hello", 123);
51+
AnotherClassWithNoDefaultConstructors outputInstance = codec.decode(
52+
codec.encode(inputInstance),
53+
AnotherClassWithNoDefaultConstructors.class);
54+
assertThat(outputInstance).isEqualTo(inputInstance);
55+
}
5656

57-
private String val1;
57+
@Test
58+
void testWithUnRegisteredClass() throws IOException {
59+
// Verify that the default encodes and decodes properly
60+
Codec codec = onlyDefaultCodec();
61+
SomeClassWithNoDefaultConstructors inputInstance = new SomeClassWithNoDefaultConstructors("hello", 123);
62+
SomeClassWithNoDefaultConstructors outputInstance = codec.decode(
63+
codec.encode(inputInstance),
64+
SomeClassWithNoDefaultConstructors.class);
65+
assertThat(outputInstance).isEqualTo(inputInstance);
5866

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

61-
SomeClassWithNoDefaultConstructors(String val1, int val2) {
62-
this.val1 = val1;
63-
this.val2 = val2;
64-
}
73+
private static Codec getFullyQualifiedCodec() {
74+
Map<Class<?>, Codec> codecs = Map.of(SomeClassWithNoDefaultConstructors.class, new PojoCodec(
75+
new KryoClassListRegistrar(SomeClassWithNoDefaultConstructors.class)));
76+
return new CompositeCodec(codecs, new PojoCodec(
77+
new KryoClassListRegistrar(AnotherClassWithNoDefaultConstructors.class)));
78+
}
6579

66-
@Override
67-
public boolean equals(Object other) {
68-
if (!(other instanceof SomeClassWithNoDefaultConstructors)) {
69-
return false;
70-
}
71-
SomeClassWithNoDefaultConstructors that = (SomeClassWithNoDefaultConstructors) other;
72-
return (this.val1.equals(that.val1) && this.val2 == that.val2);
73-
}
80+
private static Codec onlyDefaultCodec() {
81+
PojoCodec pojoCodec = new PojoCodec();
82+
Map<Class<?>, Codec> codecs = Map.of(java.util.Date.class, pojoCodec);
83+
return new CompositeCodec(codecs, new PojoCodec(
84+
new KryoClassListRegistrar(SomeClassWithNoDefaultConstructors.class)));
85+
}
7486

75-
@Override
76-
public int hashCode() {
77-
int result = this.val1.hashCode();
78-
result = 31 * result + this.val2;
79-
return result;
80-
}
87+
private record SomeClassWithNoDefaultConstructors(String val1, int val2) { }
8188

82-
}
89+
private record AnotherClassWithNoDefaultConstructors(String val1, int val2) { }
8390

8491
}

spring-integration-ip/src/test/java/org/springframework/integration/ip/tcp/connection/TcpMessageMapperTests.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@
2020
import java.io.ByteArrayOutputStream;
2121
import java.net.InetAddress;
2222
import java.net.Socket;
23+
import java.nio.charset.StandardCharsets;
2324
import java.util.Collections;
24-
import java.util.HashMap;
2525
import java.util.Map;
2626

2727
import javax.net.SocketFactory;
@@ -35,7 +35,6 @@
3535
import org.springframework.integration.IntegrationMessageHeaderAccessor;
3636
import org.springframework.integration.codec.Codec;
3737
import org.springframework.integration.codec.CodecMessageConverter;
38-
import org.springframework.integration.codec.CompositeCodec;
3938
import org.springframework.integration.codec.kryo.MessageCodec;
4039
import org.springframework.integration.ip.IpHeaders;
4140
import org.springframework.integration.ip.tcp.serializer.MapJsonSerializer;
@@ -56,6 +55,7 @@
5655
* @author Gary Russell
5756
* @author Artem Bilan
5857
* @author Gengwu Zhao
58+
* @author Glenn Renfro
5959
* @since 2.0
6060
*
6161
*/
@@ -67,8 +67,7 @@ public class TcpMessageMapperTests {
6767

6868
@BeforeEach
6969
public void setup() {
70-
Map<Class<?>, Codec> codecs = new HashMap<>();
71-
this.codec = new CompositeCodec(codecs, new MessageCodec());
70+
this.codec = new MessageCodec();
7271
}
7372

7473
@Test
@@ -339,7 +338,7 @@ public void testMapMessageConvertingOutboundJson() throws Exception {
339338
MapJsonSerializer serializer = new MapJsonSerializer();
340339
ByteArrayOutputStream baos = new ByteArrayOutputStream();
341340
serializer.serialize(map, baos);
342-
assertThat(new String(baos.toByteArray(), "UTF-8"))
341+
assertThat(baos.toString(StandardCharsets.UTF_8))
343342
.isEqualTo("{\"headers\":{\"bar\":\"baz\"},\"payload\":\"foo\"}\n");
344343
}
345344

src/reference/antora/modules/ROOT/pages/codec.adoc

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,11 @@ See the https://docs.spring.io/spring-integration/api/org/springframework/integr
3939
[[kryo]]
4040
== Kryo
4141

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

4444
* `PojoCodec`: Used in the transformers
4545
* `MessageCodec`: Used in the `CodecMessageConverter`
46+
* `CompositeCodec`: Used in transformers
4647

4748
The framework provides several custom serializers:
4849

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

57+
[[composite-codec]]
58+
=== CompositeCodec
59+
60+
The `CompositeCodec` is a codec that combines multiple codecs into a single codec, delegating encoding and decoding operations to the appropriate type-specific codec.
61+
This implementation associates object types with their appropriate codecs while providing a fallback default codec for unregistered types.
62+
63+
An example implementation can be seen below:
64+
```java
65+
void encodeDecodeSample() {
66+
Codec codec = getFullyQualifiedCodec();
67+
68+
//Encode and Decode a Dog Object
69+
Dog dog = new Dog("Wolfy", 3, "woofwoof");
70+
dog = codec.decode(
71+
codec.encode(dog),
72+
Dog.class);
73+
System.out.println(dog);
74+
75+
//Encode and Decode a Cat Object
76+
Cat cat = new Cat("Kitty", 2, 8);
77+
cat = codec.decode(
78+
codec.encode(cat),
79+
Cat.class);
80+
System.out.println(cat);
81+
82+
//Use the default code if the type being decoded and encoded is not Cat or dog.
83+
Animal animal = new Animal("Badger", 5);
84+
Animal animalOut = codec.decode(
85+
codec.encode(animal),
86+
Animal.class);
87+
System.out.println(animalOut);
88+
}
89+
90+
/**
91+
* Create and return a {@link CompositeCodec} that associates {@code Dog} and {@code Cat}
92+
* classes with their respective {@link PojoCodec} instances, while providing a default
93+
* codec for {@code Animal} types.
94+
* <p>
95+
* @return a fully qualified {@link CompositeCodec} for {@code Dog}, {@code Cat},
96+
* and fallback for {@code Animal}
97+
*/
98+
static Codec getFullyQualifiedCodec() {
99+
Map<Class<?>, Codec> codecs = new HashMap<Class<?>, Codec>();
100+
codecs.put(Dog.class, new PojoCodec(new KryoClassListRegistrar(Dog.class)));
101+
codecs.put(Cat.class, new PojoCodec(new KryoClassListRegistrar(Cat.class)));
102+
return new CompositeCodec(codecs, new PojoCodec(
103+
new KryoClassListRegistrar(Animal.class)));
104+
}
105+
106+
// Records that will be encoded and decoded in this sample
107+
record Dog(String name, int age, String tag) {}
108+
record Cat(String name, int age, int lives) {}
109+
record Animal(String name, int age){}
110+
```
111+
112+
In some cases a single type of object may return multiple codecs.
113+
In these cases an `IllegalStateException` is thrown.
114+
115+
NOTE: This class uses `ClassUtils.findClosestMatch` to select the appropriate codec for a given object type.
116+
When multiple codecs match an object type, `ClassUtils.findClosestMatch` offers the `failOnTie` option.
117+
If `failOnTie` is `false`, it will return any one of the matching codecs.
118+
If `failOnTie` is `true` and multiple codecs match, it will throw an `IllegalStateException`.
119+
CompositeCodec` sets `failOnTie` to `true`, so if multiple codecs match, an `IllegalStateException` is thrown.
120+
56121
[[customizing-kryo]]
57122
=== Customizing Kryo
58123

0 commit comments

Comments
 (0)