Skip to content

Commit 451374d

Browse files
tzolovartembilan
authored andcommitted
GH-3772 Add Protobuf transformation support
Fixes #3772 * Add basic To/From Protocol Buffer's com.google.protobuf.Message transformers. * Allow the proto_type header to specify the type. * Add tests. * add Protobuf docs * Leverage the Spring ProtobufMessageConverter * move protobuf-java-util to test dependecies as optional * protobuf docs improvements * improve the expected type handling * expected type expression * fix indentation * fix indentation for generated test classes * suppress style check for proto generated classes * address the transformer doc format * fix whats new merge conflict * fix doc sample code * Some code clean up; fixing typos
1 parent acd8a03 commit 451374d

File tree

15 files changed

+2054
-5
lines changed

15 files changed

+2054
-5
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,4 @@ vf.gf.dmn-*
3131
hostkey.ser
3232
.springBeans
3333
.sts4-cache
34+
.vscode/

build.gradle

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ ext {
9494
mysqlVersion = '8.0.32'
9595
pahoMqttClientVersion = '1.2.5'
9696
postgresVersion = '42.5.4'
97+
protobufVersion = '3.21.12'
9798
r2dbch2Version = '1.0.0.RELEASE'
9899
reactorVersion = '2022.0.3'
99100
resilience4jVersion = '2.0.2'
@@ -542,7 +543,7 @@ project('spring-integration-core') {
542543
optionalApi('com.fasterxml.jackson.module:jackson-module-kotlin') {
543544
exclude group: 'org.jetbrains.kotlin'
544545
}
545-
546+
optionalApi "com.google.protobuf:protobuf-java:$protobufVersion"
546547
optionalApi "com.jayway.jsonpath:json-path:$jsonpathVersion"
547548
optionalApi "com.esotericsoftware:kryo:$kryoVersion"
548549
optionalApi 'io.micrometer:micrometer-core'
@@ -553,6 +554,7 @@ project('spring-integration-core') {
553554
optionalApi "org.apache.avro:avro:$avroVersion"
554555
optionalApi 'org.jetbrains.kotlinx:kotlinx-coroutines-reactor'
555556

557+
testImplementation "com.google.protobuf:protobuf-java-util:$protobufVersion"
556558
testImplementation "org.aspectj:aspectjweaver:$aspectjVersion"
557559
testImplementation 'io.micrometer:micrometer-observation-test'
558560
testImplementation ('io.micrometer:micrometer-tracing-integration-test') {
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
/*
2+
* Copyright 2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.integration.transformer;
18+
19+
import org.springframework.beans.factory.BeanClassLoaderAware;
20+
import org.springframework.expression.EvaluationContext;
21+
import org.springframework.expression.Expression;
22+
import org.springframework.integration.context.IntegrationContextUtils;
23+
import org.springframework.integration.expression.FunctionExpression;
24+
import org.springframework.integration.expression.ValueExpression;
25+
import org.springframework.integration.transformer.support.ProtoHeaders;
26+
import org.springframework.messaging.Message;
27+
import org.springframework.messaging.converter.ProtobufMessageConverter;
28+
import org.springframework.util.Assert;
29+
import org.springframework.util.ClassUtils;
30+
31+
/**
32+
* A Protocol Buffer transformer to instantiate {@link com.google.protobuf.Message} objects
33+
* from either {@code byte[]} if content type is {@code application/x-protobuf}
34+
* or from {@code String} in case of {@code application/json} content type.
35+
*
36+
* @author Christian Tzolov
37+
*
38+
* @since 6.1
39+
*/
40+
public class FromProtobufTransformer extends AbstractTransformer implements BeanClassLoaderAware {
41+
42+
private final ProtobufMessageConverter protobufMessageConverter;
43+
44+
private ClassLoader beanClassLoader;
45+
46+
private Expression expectedTypeExpression =
47+
new FunctionExpression<Message<?>>((message) -> message.getHeaders().get(ProtoHeaders.TYPE));
48+
49+
private EvaluationContext evaluationContext;
50+
51+
/**
52+
* Construct an instance with the supplied default type to create.
53+
*/
54+
public FromProtobufTransformer() {
55+
this(new ProtobufMessageConverter());
56+
}
57+
58+
/**
59+
* Construct an instance with the supplied default type and ProtobufMessageConverter instance.
60+
* @param protobufMessageConverter the message converter used.
61+
*/
62+
public FromProtobufTransformer(ProtobufMessageConverter protobufMessageConverter) {
63+
Assert.notNull(protobufMessageConverter, "'protobufMessageConverter' must not be null");
64+
this.protobufMessageConverter = protobufMessageConverter;
65+
}
66+
67+
@Override
68+
public void setBeanClassLoader(ClassLoader classLoader) {
69+
this.beanClassLoader = classLoader;
70+
}
71+
72+
/**
73+
* Set an expected protobuf class type.
74+
* Mutually exclusive with {@link #setExpectedTypeExpression} and
75+
* {@link #setExpectedTypeExpressionString}.
76+
* @param expectedType expected protobuf class type.
77+
* @return updated FromProtobufTransformer instance.
78+
*/
79+
public FromProtobufTransformer setExpectedType(Class<? extends com.google.protobuf.Message> expectedType) {
80+
return setExpectedTypeExpression(new ValueExpression<>(expectedType));
81+
}
82+
83+
/**
84+
* Set an expression to evaluate against the message to determine the type id.
85+
* Defaults to{@code headers['proto_type']}. Mutually exclusive with
86+
* {@link #setExpectedType} and {@link #setExpectedTypeExpression}.
87+
* @param expression the expression.
88+
* @return updated FromProtobufTransformer instance.
89+
*/
90+
public FromProtobufTransformer setExpectedTypeExpressionString(String expression) {
91+
return setExpectedTypeExpression(EXPRESSION_PARSER.parseExpression(expression));
92+
}
93+
94+
/**
95+
* Set an expression to evaluate against the message to determine the type.
96+
* Default {@code headers['proto_type']}.
97+
* Mutually exclusive with {@link #setExpectedType} and
98+
* {@link #setExpectedTypeExpressionString}.
99+
* @param expression the expression.
100+
* @return updated FromProtobufTransformer instance.
101+
*/
102+
public FromProtobufTransformer setExpectedTypeExpression(Expression expression) {
103+
Assert.notNull(expression, "'expression' must not be null");
104+
this.expectedTypeExpression = expression;
105+
return this;
106+
}
107+
108+
@Override
109+
protected void onInit() {
110+
this.evaluationContext = IntegrationContextUtils.getEvaluationContext(getBeanFactory());
111+
}
112+
113+
@SuppressWarnings("unchecked")
114+
@Override
115+
protected Object doTransform(Message<?> message) {
116+
Class<? extends com.google.protobuf.Message> targetClass = null;
117+
Object value = this.expectedTypeExpression.getValue(this.evaluationContext, message);
118+
if (value instanceof Class<?>) {
119+
targetClass = (Class<? extends com.google.protobuf.Message>) value;
120+
}
121+
else if (value instanceof String) {
122+
try {
123+
targetClass =
124+
(Class<? extends com.google.protobuf.Message>)
125+
ClassUtils.forName((String) value, this.beanClassLoader);
126+
}
127+
catch (ClassNotFoundException | LinkageError e) {
128+
throw new IllegalStateException(e);
129+
}
130+
}
131+
132+
if (targetClass == null) {
133+
throw new MessageTransformationException(message,
134+
"The 'expectedTypeExpression' (" + this.expectedTypeExpression + ") returned 'null'.");
135+
}
136+
137+
return this.protobufMessageConverter.fromMessage(message, targetClass);
138+
}
139+
140+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* Copyright 2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.integration.transformer;
18+
19+
import org.springframework.integration.transformer.support.ProtoHeaders;
20+
import org.springframework.messaging.Message;
21+
import org.springframework.messaging.MessageHeaders;
22+
import org.springframework.messaging.converter.ProtobufMessageConverter;
23+
import org.springframework.messaging.support.MessageHeaderAccessor;
24+
import org.springframework.util.Assert;
25+
26+
/**
27+
* A Protocol Buffer transformer for generated {@link com.google.protobuf.Message} objects.
28+
* <p>
29+
* If the content type is set to {@code application/x-protobuf} (default if no content type),
30+
* then the output message payload is of type byte array.
31+
* <p>
32+
* If the content type is set to {@code application/json} and
33+
* the {@code com.google.protobuf:protobuf-java-util} dependency is on the
34+
* classpath, the output message payload if of type String.
35+
*
36+
* @author Christian Tzolov
37+
*
38+
* @since 6.1
39+
*/
40+
public class ToProtobufTransformer extends AbstractTransformer {
41+
42+
private final ProtobufMessageConverter protobufMessageConverter;
43+
44+
public ToProtobufTransformer() {
45+
this(new ProtobufMessageConverter());
46+
}
47+
48+
public ToProtobufTransformer(ProtobufMessageConverter protobufMessageConverter) {
49+
Assert.notNull(protobufMessageConverter, "'protobufMessageConverter' must not be null");
50+
this.protobufMessageConverter = protobufMessageConverter;
51+
}
52+
53+
@Override
54+
protected Object doTransform(Message<?> message) {
55+
Assert.isInstanceOf(com.google.protobuf.Message.class, message.getPayload(),
56+
"Payload must be an implementation of 'com.google.protobuf.Message'");
57+
58+
MessageHeaderAccessor accessor = new MessageHeaderAccessor(message);
59+
accessor.setHeader(ProtoHeaders.TYPE, message.getPayload().getClass().getName());
60+
if (!message.getHeaders().containsKey(MessageHeaders.CONTENT_TYPE)) {
61+
accessor.setContentType(ProtobufMessageConverter.PROTOBUF);
62+
}
63+
64+
return this.protobufMessageConverter.toMessage(message.getPayload(), accessor.getMessageHeaders());
65+
}
66+
67+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.integration.transformer.support;
18+
19+
/**
20+
* Pre-defined names and prefixes for Protocol Buffers related headers.
21+
*
22+
* @author Christian Tzolov
23+
*
24+
* @since 6.1
25+
*/
26+
public final class ProtoHeaders {
27+
28+
private ProtoHeaders() {
29+
}
30+
31+
/**
32+
* The prefix for Protocol Buffers specific message headers.
33+
*/
34+
public static final String PREFIX = "proto_";
35+
36+
/**
37+
* The {@code com.google.protobuf.Message} type. By default, it's the fully qualified
38+
* {@code com.google.protobuf.Message} type but can be a key that is mapped to the actual type.
39+
*/
40+
public static final String TYPE = PREFIX + "type";
41+
42+
}

0 commit comments

Comments
 (0)