Skip to content

Commit f322f98

Browse files
GH-206: Add Glue Schema support into KplMH
Fixes spring-projects/spring-integration-aws#206 * Provide a Glue Schema support for `KplMessageHandler`. When `UserRecord` is built from the request message, a `glueSchemaExpression` is evaluated to supply into a `UserRecord` to produce. The `GlueSchemaRegistryConfiguration` must be supplied into a `KinesisProducerConfiguration` * Fix the `buildPutRecordRequest()` algorithm to copy the data from the `UserRecord` in the request message payload instead of false-leading expression evaluations and conversions from the request message **Cherry-pick to `2.5.x`**
1 parent 7375cf5 commit f322f98

File tree

1 file changed

+115
-56
lines changed

1 file changed

+115
-56
lines changed

src/main/java/org/springframework/integration/aws/outbound/KplMessageHandler.java

+115-56
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2019 the original author or authors.
2+
* Copyright 2019-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -23,15 +23,14 @@
2323
import java.util.concurrent.ScheduledFuture;
2424
import java.util.concurrent.atomic.AtomicInteger;
2525

26-
import org.checkerframework.checker.nullness.qual.Nullable;
27-
2826
import org.springframework.context.Lifecycle;
2927
import org.springframework.core.convert.converter.Converter;
3028
import org.springframework.core.serializer.support.SerializingConverter;
3129
import org.springframework.expression.Expression;
3230
import org.springframework.expression.common.LiteralExpression;
3331
import org.springframework.integration.aws.support.AwsHeaders;
3432
import org.springframework.integration.aws.support.AwsRequestFailureException;
33+
import org.springframework.integration.expression.ValueExpression;
3534
import org.springframework.integration.handler.AbstractMessageHandler;
3635
import org.springframework.integration.mapping.HeaderMapper;
3736
import org.springframework.integration.mapping.OutboundMessageMapper;
@@ -55,6 +54,7 @@
5554
import com.amazonaws.services.kinesis.producer.KinesisProducer;
5655
import com.amazonaws.services.kinesis.producer.UserRecord;
5756
import com.amazonaws.services.kinesis.producer.UserRecordResult;
57+
import com.amazonaws.services.schemaregistry.common.Schema;
5858
import com.google.common.util.concurrent.FutureCallback;
5959
import com.google.common.util.concurrent.Futures;
6060
import com.google.common.util.concurrent.ListenableFuture;
@@ -90,6 +90,8 @@ public class KplMessageHandler extends AbstractAwsMessageHandler<Void> implement
9090

9191
private Expression sequenceNumberExpression;
9292

93+
private Expression glueSchemaExpression;
94+
9395
private OutboundMessageMapper<byte[]> embeddedHeadersMapper;
9496

9597
private Duration flushDuration = Duration.ofMillis(0);
@@ -201,6 +203,37 @@ public void setHeaderMapper(HeaderMapper<Void> headerMapper) {
201203
+ "Consider to use 'OutboundMessageMapper<byte[]>' for embedding headers into the record data.");
202204
}
203205

206+
/**
207+
* Set a {@link Schema} to add into a {@link UserRecord} built from the request message.
208+
* @param glueSchema the {@link Schema} to add into a {@link UserRecord}.
209+
* @since 2.5.2
210+
* @see UserRecord#setSchema(Schema)
211+
*/
212+
public void setGlueSchema(Schema glueSchema) {
213+
setPartitionKeyExpression(new ValueExpression<>(glueSchema));
214+
}
215+
216+
/**
217+
* Set a SpEL expression for {@link Schema} to add into a {@link UserRecord}
218+
* built from the request message.
219+
* @param glueSchemaExpression the SpEL expression to evaluate a {@link Schema}.
220+
* @since 2.5.2
221+
* @see UserRecord#setSchema(Schema)
222+
*/
223+
public void setGlueSchemaExpressionString(String glueSchemaExpression) {
224+
setGlueSchemaExpression(EXPRESSION_PARSER.parseExpression(glueSchemaExpression));
225+
}
226+
227+
/**
228+
* Set a SpEL expression for {@link Schema} to add into a {@link UserRecord}
229+
* built from the request message.
230+
* @param glueSchemaExpression the SpEL expression to evaluate a {@link Schema}.
231+
* @since 2.5.2
232+
* @see UserRecord#setSchema(Schema)
233+
*/
234+
public void setGlueSchemaExpression(Expression glueSchemaExpression) {
235+
this.glueSchemaExpression = glueSchemaExpression;
236+
}
204237

205238
@Override
206239
public synchronized void start() {
@@ -247,6 +280,7 @@ else if (message.getPayload() instanceof UserRecord) {
247280
userRecord.setData(putRecordRequest.getData());
248281
userRecord.setPartitionKey(putRecordRequest.getPartitionKey());
249282
userRecord.setStreamName(putRecordRequest.getStreamName());
283+
setGlueSchemaIntoUserRecordIfAny(userRecord, message);
250284
return handleUserRecord(message, putRecordRequest, userRecord);
251285
}
252286
}
@@ -268,6 +302,7 @@ private Future<PutRecordsResult> handlePutRecordsRequest(Message<?> message, Put
268302
userRecord.setData(putRecordsRequestEntry.getData());
269303
userRecord.setPartitionKey(putRecordsRequestEntry.getPartitionKey());
270304
userRecord.setStreamName(putRecordsRequest.getStreamName());
305+
setGlueSchemaIntoUserRecordIfAny(userRecord, message);
271306
return userRecord;
272307
})
273308
.concatMap((userRecord) ->
@@ -303,6 +338,13 @@ private Future<PutRecordsResult> handlePutRecordsRequest(Message<?> message, Put
303338
return putRecordsResultFuture;
304339
}
305340

341+
private void setGlueSchemaIntoUserRecordIfAny(UserRecord userRecord, Message<?> message) {
342+
if (this.glueSchemaExpression != null) {
343+
Schema schema = this.glueSchemaExpression.getValue(getEvaluationContext(), message, Schema.class);
344+
userRecord.setSchema(schema);
345+
}
346+
}
347+
306348
private Future<?> handleUserRecord(Message<?> message, PutRecordRequest putRecordRequest, UserRecord userRecord) {
307349
ListenableFuture<UserRecordResult> recordResult = this.kinesisProducer.addUserRecord(userRecord);
308350
applyCallbackForAsyncHandler(message, putRecordRequest, recordResult);
@@ -333,71 +375,88 @@ public void onSuccess(R result) {
333375
}
334376

335377
private PutRecordRequest buildPutRecordRequest(Message<?> message) {
336-
MessageHeaders messageHeaders = message.getHeaders();
337-
String stream = messageHeaders.get(AwsHeaders.STREAM, String.class);
338-
if (!StringUtils.hasText(stream) && this.streamExpression != null) {
339-
stream = this.streamExpression.getValue(getEvaluationContext(), message, String.class);
340-
}
341-
Assert.state(stream != null,
342-
"'stream' must not be null for sending a Kinesis record. "
343-
+ "Consider configuring this handler with a 'stream'( or 'streamExpression') or supply an "
344-
+ "'aws_stream' message header.");
345-
346-
String partitionKey = messageHeaders.get(AwsHeaders.PARTITION_KEY, String.class);
347-
if (!StringUtils.hasText(partitionKey) && this.partitionKeyExpression != null) {
348-
partitionKey = this.partitionKeyExpression.getValue(getEvaluationContext(), message, String.class);
349-
}
350-
Assert.state(partitionKey != null, "'partitionKey' must not be null for sending a Kinesis record. "
351-
+ "Consider configuring this handler with a 'partitionKey'( or 'partitionKeyExpression') or supply an "
352-
+ "'aws_partitionKey' message header.");
353-
354-
String explicitHashKey = (this.explicitHashKeyExpression != null
355-
? this.explicitHashKeyExpression.getValue(getEvaluationContext(), message, String.class) : null);
356-
357-
String sequenceNumber = messageHeaders.get(AwsHeaders.SEQUENCE_NUMBER, String.class);
358-
if (!StringUtils.hasText(sequenceNumber) && this.sequenceNumberExpression != null) {
359-
sequenceNumber = this.sequenceNumberExpression.getValue(getEvaluationContext(), message, String.class);
360-
}
361-
362378
Object payload = message.getPayload();
363379

364380
ByteBuffer data = null;
365-
366-
Message<?> messageToEmbed = null;
367-
368-
if (payload instanceof ByteBuffer) {
369-
data = (ByteBuffer) payload;
370-
if (this.embeddedHeadersMapper != null) {
371-
messageToEmbed = new MutableMessage<>(data.array(), messageHeaders);
372-
}
381+
String sequenceNumber = null;
382+
String stream;
383+
String partitionKey;
384+
String explicitHashKey;
385+
386+
if (payload instanceof UserRecord) {
387+
UserRecord userRecord = (UserRecord) payload;
388+
data = userRecord.getData();
389+
stream = userRecord.getStreamName();
390+
partitionKey = userRecord.getPartitionKey();
391+
explicitHashKey = userRecord.getExplicitHashKey();
373392
}
374393
else {
375-
byte[] bytes =
376-
(byte[]) (payload instanceof byte[]
377-
? payload
378-
: this.messageConverter.fromMessage(message, byte[].class));
379-
Assert.notNull(bytes, "payload cannot be null");
380-
if (this.embeddedHeadersMapper != null) {
381-
messageToEmbed = new MutableMessage<>(bytes, messageHeaders);
394+
MessageHeaders messageHeaders = message.getHeaders();
395+
stream = messageHeaders.get(AwsHeaders.STREAM, String.class);
396+
if (!StringUtils.hasText(stream) && this.streamExpression != null) {
397+
stream = this.streamExpression.getValue(getEvaluationContext(), message, String.class);
382398
}
383-
else {
384-
data = ByteBuffer.wrap(bytes);
399+
Assert.state(stream != null,
400+
"'stream' must not be null for sending a Kinesis record. "
401+
+ "Consider configuring this handler with a 'stream'( or 'streamExpression') or supply an "
402+
+ "'aws_stream' message header.");
403+
404+
partitionKey = messageHeaders.get(AwsHeaders.PARTITION_KEY, String.class);
405+
if (!StringUtils.hasText(partitionKey) && this.partitionKeyExpression != null) {
406+
partitionKey = this.partitionKeyExpression.getValue(getEvaluationContext(), message, String.class);
407+
}
408+
Assert.state(partitionKey != null, "'partitionKey' must not be null for sending a Kinesis record. "
409+
+ "Consider configuring this handler with a 'partitionKey'( or 'partitionKeyExpression') " +
410+
"or supply an 'aws_partitionKey' message header.");
411+
412+
explicitHashKey = (this.explicitHashKeyExpression != null
413+
? this.explicitHashKeyExpression.getValue(getEvaluationContext(), message, String.class) : null);
414+
415+
sequenceNumber = messageHeaders.get(AwsHeaders.SEQUENCE_NUMBER, String.class);
416+
if (!StringUtils.hasText(sequenceNumber) && this.sequenceNumberExpression != null) {
417+
sequenceNumber = this.sequenceNumberExpression.getValue(getEvaluationContext(), message, String.class);
385418
}
386-
}
387419

388-
if (messageToEmbed != null) {
389-
try {
390-
byte[] bytes = this.embeddedHeadersMapper.fromMessage(messageToEmbed);
420+
Message<?> messageToEmbed = null;
421+
422+
if (payload instanceof ByteBuffer) {
423+
data = (ByteBuffer) payload;
424+
if (this.embeddedHeadersMapper != null) {
425+
messageToEmbed = new MutableMessage<>(data.array(), messageHeaders);
426+
}
427+
}
428+
else {
429+
byte[] bytes =
430+
(byte[]) (payload instanceof byte[]
431+
? payload
432+
: this.messageConverter.fromMessage(message, byte[].class));
391433
Assert.notNull(bytes, "payload cannot be null");
392-
data = ByteBuffer.wrap(bytes);
434+
if (this.embeddedHeadersMapper != null) {
435+
messageToEmbed = new MutableMessage<>(bytes, messageHeaders);
436+
}
437+
else {
438+
data = ByteBuffer.wrap(bytes);
439+
}
393440
}
394-
catch (Exception ex) {
395-
throw new MessageConversionException(message, "Cannot embedded headers to payload", ex);
441+
442+
if (messageToEmbed != null) {
443+
try {
444+
byte[] bytes = this.embeddedHeadersMapper.fromMessage(messageToEmbed);
445+
Assert.notNull(bytes, "payload cannot be null");
446+
data = ByteBuffer.wrap(bytes);
447+
}
448+
catch (Exception ex) {
449+
throw new MessageConversionException(message, "Cannot embedded headers to payload", ex);
450+
}
396451
}
397452
}
398453

399-
return new PutRecordRequest().withStreamName(stream).withPartitionKey(partitionKey)
400-
.withExplicitHashKey(explicitHashKey).withSequenceNumberForOrdering(sequenceNumber).withData(data);
454+
return new PutRecordRequest()
455+
.withStreamName(stream)
456+
.withPartitionKey(partitionKey)
457+
.withExplicitHashKey(explicitHashKey)
458+
.withSequenceNumberForOrdering(sequenceNumber)
459+
.withData(data);
401460
}
402461

403462
@Override
@@ -427,7 +486,7 @@ public boolean cancel(boolean mayInterruptIfRunning) {
427486
Futures.addCallback(listenableFuture, new FutureCallback<T>() {
428487

429488
@Override
430-
public void onSuccess(@Nullable T result) {
489+
public void onSuccess(T result) {
431490
completable.complete(result);
432491
}
433492

0 commit comments

Comments
 (0)