Skip to content
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

Payload Enricher with request-payload-expression="payload.username" #346

Open
roro0290 opened this issue Mar 23, 2023 · 10 comments
Open

Payload Enricher with request-payload-expression="payload.username" #346

roro0290 opened this issue Mar 23, 2023 · 10 comments

Comments

@roro0290
Copy link

While trying out the enricher example by using Java DSL, I am unable to replace the payload that contains User object with only username field using the method:
.enrich(p -> p.requestPayloadExpression("payload.username"))
--> https://docs.spring.io/spring-integration/reference/html/content-enrichment.html#payload-enricher

However, the payload functionality works when I use the transform method that takes in an expression parameter
.transform("payload.username")

Is the transform method the only way to update the payload?

@roro0290
Copy link
Author

roro0290 commented Mar 23, 2023

Additionally, while trying out the use case 3, I notice that the XML does not do any explicit transformation between the returned User object and the User map. However, with the Java DSL, I am getting the below error. Would you be able to suggest on this payload transformation?

org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [com.spring.integration.basic.enricher.User] to type [java.util.Map<?, ?>]

Referred XML:

<int:enricher id="findUserWithMapEnricher"
                  input-channel="findUserWithMapEnricherChannel"
                  request-channel="findUserByUsernameServiceChannel"
                  request-payload-expression="payload.username">
        <int:property name="user"    expression="payload"/>
    </int:enricher>

@artembilan
Copy link
Member

Would you mind to the share with us a the whole configuration for this task and explain what you try to achieve?

It feels like you are missing the reasoning behind an Enricher pattern: https://www.enterpriseintegrationpatterns.com/patterns/messaging/DataEnricher.html.

In two words it does this: takes some additional data and populates it as an addition to the request data.

To be more specific here is what is going on in that findUserWithMapEnricher sample:

  1. It received some POJO from a findUserWithMapEnricherChannel
  2. Performs a request-payload-expression taking just a username property value from that POJO
  3. Creates a message based on a payload from step 2 and send it to that findUserByUsernameServiceChannel
  4. When replies arrives, it sets its payload into a user property of that request POJO.
  5. A reply message with modified POJO is produced from this component.

The Java DSL for this XML might look like this:

.enrich(e -> e
		.requestChannel("findUserByUsernameServiceChannel")
		.requestPayloadExpression("payload.username")
		.propertyExpression("user", "payload"))

There is no automatic conversion to a Map.

@roro0290
Copy link
Author

roro0290 commented Mar 23, 2023

Example#2: Pass only the username to the request channel by using the request-payload-expression attribute

Implementation: Enrich the payload with the username -> send to a handle method (Service Activator)

Bean
    IntegrationFlow findUserByUsernameEnricherChannel(){
        return f -> f
                .enrich(p -> p.requestPayloadExpression("payload.username"))
                .handle(systemService,"findUserByUsername");
    }```

```public User findUserByUsername(String username) {

        LOGGER.info(String.format("Calling method 'findUserByUsername' with parameter: %s", username));

        return new User(username, "secret", username + "@springintegration.org");

    }```

However, from the debug of this code, I can see that username is passed as User [username=foo, password=moo, email=fooMooEmail]

thus resulting in an output of: 
**Expected** :User [username=foo, password=secret, [email protected]]
**Actual**   :User [username=User [username=foo, password=moo, email=fooMooEmail], password=secret, email=User [username=foo, password=moo, email=fooMooEmail]@springintegration.org]

The expected output could be achieved when I used the transform method together with a SPeL expression. But as you mentioned, this defeats the purpose of the enricher where we should add on to the object instead of modifying the existing payload 

```Bean
    IntegrationFlow findUserByUsernameEnricherChannel(){
        return f -> f
                .transform("payload.username")
                .handle(systemService,"findUserByUsername");
    }```

@artembilan
Copy link
Member

Your enrich() doesn't call that findUserByUsername.
You also don't show what is an input payload.
And I don't see to what property you try to set a result of the enrichment.
If your goal is to fully replace the payload for downstream process, then indeed a transform() is the right way.

@artembilan
Copy link
Member

Yeah... You definitely need to learn what is that Enricher pattern.
Right now it looks like you trying to do something with it which is essentially a transform(): you just take a property from the payload and try to produce it as a reply: there is just no any enrichment in your current configuration.
No any property to populate, so a request payload is produced as a reply without any changes.

@roro0290
Copy link
Author

roro0290 commented Mar 28, 2023

Based on your comments, I used the requestSubFlow() method to handle the sample payload

@Bean
    public IntegrationFlow practiceFlow(){
        return f -> f
                .enrich(enricherSpec -> enricherSpec
                        .requestPayloadExpression("payload.username") // use the value of the username as the payload of this message
                        .requestSubFlow(flow -> flow
                                .log()
                                .handle(String.class, (payload,headers)->new User(payload,"password","email"))
                                .enrichHeaders(h -> h.headerExpression("email","payload.email"))
                                .enrichHeaders(h -> h.headerExpression("password","payload.password"))
                                .log()
                        )
                )
                .log()
                .channel("outputChannel");
    }

Based on this I observe the below changes in payload through the flow but am unable to get any message in the main flow. Is there still some issue in the implementation?

  • Input payload: User [username=foo, password=null, email=null]
  • After requestPayloadExpression: payload=foo
  • After requestSubFlow: User [username=foo, password=password, email=email]
  • In the main flow (outputChannel): No available message (test case is loading)

@artembilan
Copy link
Member

You still use enrich() a wrong way.
You are fully missing its purpose: you request some extra info and you have to populate it into the output.
There is no any propertyExpression() or headerExpression() for your enricherSpec.
What you are doing now is fully out of enricher scope.
With your current logic you could just do:

@Bean
    public IntegrationFlow practiceFlow(){
        return f -> f
                .log()
                .handle(User.class, (payload,headers)->new User(payload.getUsername(),"password","email"))
                .enrichHeaders(h -> h.headerExpression("email","payload.email"))
                .enrichHeaders(h -> h.headerExpression("password","payload.password"))
                .log()
                .channel("outputChannel");
    }

Not sure if you will still need those enrichHeader() though...

@roro0290
Copy link
Author

roro0290 commented Mar 31, 2023

After analyzing, the confusion started due to 2 issues:

  1. we pass a User object but the service method accepts a String username
  2. the service handle method must return the object to the main thread

Issue 1 is resolved by using the requestPayloadExpression since it can "send only a subset of the original payload by using the request-payload-expression attribute." - https://docs.spring.io/spring-integration/reference/html/content-enrichment.html#payload-enricher

I am unable to resolve issue 2. I get the response: User [username=foo, password=null, email=null]

I tried it together with replyChannel where the handle method should send the output to a channel on which the gateway will listen for a response

Gateway

@Gateway(requestChannel = "findUserByUsernameEnricherChannel.input", replyChannel = "findUserReplyChannel")
    User findUserByUsername(User user);

Service

@Bean
    public IntegrationFlow findUserByUsernameEnricherChannel() {
        return f->f
                .enrich((enricher) -> enricher
                                .requestChannel("findUserByUsernameServiceChannel")
                                .replyChannel("findUserReplyChannel")
                                .requestPayloadExpression("payload.username")
                )
                .handle(String.class,(p, h) -> SystemService.findUserByUsername(p));
    }
@Bean
    public IntegrationFlow findUserByUsernameServiceFlow() {
        return IntegrationFlows.
                from("findUserByUsernameServiceChannel")
                .handle(String.class,(p, h) -> SystemService.findUserByUsername(p))
                .channel("findUserReplyChannel")
                .get();
    }

If I provide the handle method directly, I get this as response: User [username=User [username=foo, password=null, email=null], password=secret, email=User [username=foo, password=null, email=null]@springintegration.org]

@Bean
    public IntegrationFlow findUserByUsernameEnricherChannel() {
        return f->f
                .enrich((enricher) -> enricher
                                .requestPayloadExpression("payload.username")
                )
                .handle(String.class,(p, h) -> SystemService.findUserByUsername(p));
    }

How do we ensure the output of the handle method goes back to the main flow?

@roro0290
Copy link
Author

Based on your earlier comments, I tried this and it worked. Would this be the right approach to this?

Output: User [username=foo, password=secret, email=[email protected]]

@Bean
    public IntegrationFlow findUserByUsernameEnricherChannel() {
        return f->f
                .enrich((enricher) -> enricher
                                .requestChannel("findUserByUsernameServiceChannel")
                                .requestPayloadExpression("payload.username")
                                .propertyExpression("username", "payload.username")
                        .propertyExpression("email", "payload.email")
                        .propertyExpression("password","payload.password")
                );
    }

the propertyExpression method is used to set values to the properties. If the property is not defined in the User object, it throws the error:
org.springframework.expression.spel.SpelEvaluationException: EL1010E: Property or field 'User' cannot be set on object of type 'com.spring.integration.basic.enricher.User' - maybe not public or not writable?

@artembilan
Copy link
Member

You need to step back and see if just a transform() is OK for you. Because from your explanation it sounds like you got a full User as a result of your service call.
That means you fully replace the request payload with a reply payload.
And therefore you don't need an enrich() since it doesn't fit into your service model.
The point of an enrich() is to add info into an existing data.
In your case you got the full data back, so you just simply can discard the quest and go on with that that reply.
What you have with those propertyExpression()s are fully an overhead since you got a full User object back.

What else would you expect if there is no getter for the property you request from SpEL?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants