This is an extension question to How to use SpEL to read payload and header content in a Spring Integration Router
Technologies in my project
Spring Boot 2
Spring Integration (XML style)
Java 8
Tomcat 9.x/Liberty 19.0.0.1
As a part of my Spring Integration project (REST API with an inbound-http-gateway, that takes an XML input and produces an XML output), this is the setup for my question:
There is a Builder-pattern-based Java object (say, MyPOJO) that is the payload in the flow.
MyPOJO has a String property/instance variable (say, String response) along with a getter and setter.
Somewhere in the flow, MyPOJO gets built and response gets set.
Inside response, there is a keyword/specific piece of text that will determine the further course of the flow.
This said, is it possible to write a Router (using XML configuration) that can check if the response inside Message<MyPOJO> contains that keyword/specific piece of text to determine where to go next?
Illustratively, something like this:
<int:router input-channel="inputChannel" expression="payload.getResponse().contains("keyword")">
<int:mapping value="true" channel="oneRoute"/>
<int:mapping value="false" channel="anotherRoute"/>
</int:router>
When I do this and launch the application, the error is:
nested exception is org.xml.sax.SAXParseException; lineNumber: 44; columnNumber: 98; Element type "int:router" must be followed by either attribute specifications, ">" or "/>"
Specifically speaking, the expression="payload.getResponse().contains("keyword")" part seems to be having an issue getting resolved and this could be something to do with the quotation marks around the keyword
Any help on this is greatly appreciated.
Sincerely,
Bharath
OK. I see you have an XML syntax error. The fix is like this:
expression="payload.getResponse().contains('keyword')"
The keyword is in single quotes, not double.
This is definitely how SpEL works: when you would like to specify a literal in the expression, you need to use single quotes: https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#expressions-evaluation
Related
I've started with SI and kind of stuck right now as we want to use SI in one of our existing project avoiding changes where we can.
A bean which we would be using as a service activator accepts an constructor argument of a java object.
that object is in the payload but then I'm unable to set it using inner bean usage of service-activator
<service-activator input-channel="ADMIN_TEST_CONNECTION" method="testConnection">
<beans:bean class="mypackage.request.AdminRequestProcessor">
<beans:constructor-arg value="payload"/>
</beans:bean>
</service-activator>
it's complaining about Could not convert argument value of type [java.lang.String] to required type.
Please help in how to access payload and set it as an constructor argument.
If I go via non- constructor arg route and change existing java object then it works with this call in the service activator
expression="#bean.testConnection(payload)"/>
but I don't wish you to change the existing java code until there is no other way.
I think you don't have choice unless change something or add code around existing.
Service-Activator performs its functionality against each incoming message in the input-channel. And that functionality is exactly method invocation where Message is used as a context for method arguments.
Not sure what you are going to do with that payload, but that doesn't look correct to use statefull object like your AdminRequestProcessor. Just don't forget that you may have many incoming message, but service-activator should be consistent.
Plus don't forget that <beans:bean> is singleton, so your AdminRequestProcessor is instantiated only once.
Looking to your sample I'd suggest something like this:
expression="new AdminRequestProcessor(payload).testConnection()"/>
If you really don't want to change anything in your code.
Everything rest you can find in the Reference Manual.
I've installed Spring XD in my PaaS environment and for legacy issue I'm stuck with version 1.0.0.M1. ( see reference doc).
My goal is to call a http rest API using the http-client module. My stream definition is:
http | httpclient --url='''<my_url>''' --http-method=POST --mappedRequestHeaders=HTTP_REQUEST_HEADERS | log --expression=#root
Unfortunately, since the http module only sends the payload to the httpclient, the httpclient returns a 415 error due to the absence of the content-type header.
Considering that I can neither add new modules nor modify existing ones (in such a version you can only reference to the spring repository), I would like to use a tranform module to inject the content-type header.
How can I achieve such a goal?
Many thanks for your help.
EDIT:
I just found that httpclient processor (link) supports headersExpression A SpEL expression used to derive the http headers map to use. However:
--headers-expression='{Content-Type:'application/json'}'
Gives the following parse exception:
org.springframework.expression.spel.SpelEvaluationException: EL1008E:
(pos 1): Property or field 'Content' cannot be found on object of type 'org.springframework.messaging.support.GenericMessage' - maybe not public?
See GH issue for more StackTrace.
First of all it isn't Spring XD any more. Spring Cloud Dataflow is different product and its behavior maybe not that which you had with Spring XD before.
Second: it is in version 1.0.0.RC1 already. So, consider to upgrade.
Now on the problem. Look:
headersExpression
A SpEL expression used to derive the http headers map to use.
So, this expression must return a Map and it is confirmed by the code:
if (properties.getHeadersExpression() != null) {
Map<?, ?> headersMap = properties.getHeadersExpression().getValue(message, Map.class);
Now let's take a look into the problem:
org.springframework.expression.spel.SpelEvaluationException: EL1008E:(pos 1): Property or field 'Content' cannot be found on object of type 'org.springframework.messaging.support.GenericMessage' - maybe not public?
org.springframework.expression.spel.ast.PropertyOrFieldReference.readProperty(PropertyOrFieldReference.java:224) ~[spring-expression-4.2.4.RELEASE.jar!/:4.2.4.RELEASE]
org.springframework.expression.spel.ast.PropertyOrFieldReference.getValueInternal(PropertyOrFieldReference.java:94) ~[spring-expression-4.2.4.RELEASE.jar!/:4.2.4.RELEASE]
org.springframework.expression.spel.ast.PropertyOrFieldReference.getValueInternal(PropertyOrFieldReference.java:81) ~[spring-expression-4.2.4.RELEASE.jar!/:4.2.4.RELEASE]
org.springframework.expression.spel.ast.OpMinus.getValueInternal(OpMinus.java:98) ~[spring-expression-4.2.4.RELEASE.jar!/:4.2.4.RELEASE]
OpMinus is a root of cause. So, SpEL treats Content-Type expression as a minus operator.
Sad, of course, but the workaround is like wrapping the key into quotes as well:
--headers-expression={'Content-Type':'application/json'}
I'd like to use a bean with a method annotated with #Transformer to transform a message accessing part of its headers with the #Header annotation. Is there a way to do this with the Java DSL (with Java 7, so no lambdas)? It does not seem like so.
You can do it like this:
.handle("myTransformer", "myMethod")
if your transformer doesn't return Message.
From other side, if you already use #Transformer there you can add channel attributes to make the real endpoint for that method and use those channels from the IntegrationFlow, e.g. .gateway("transformChannel")
Since 1.1 we are going to add more EIP-methods to make it more flexible for similar cases.
Feel free to raise GH issue on the matter!
This...
.transform("#transformerBean.someMethod(payload, headers['foo'])")
...should work.
How to get Spring boot to load external properties for Groovy?
Need something similar to java mechanism (application.properties in resources and ConfigBean with #Value annotations)?
When trying to use the same mechanism as with java, I don't know how to annotate the ConfigBean
#Component
public class ConfigBean {
#Value("${seleniumAddress}")
private String seleniumAddress; ...
and then in application.properties
seleniumAddress=http://localhost:4444/wd/hub
but with groovy I cannot annotate the field with #Value("${seleniumAddress}"
It throws an error complaining about "${}" - this is a special sequence in groovy.
So what mechanism should I use here?
Thank you
If you use "${}" for Spring placeholders in Groovy you have to make sure it's a String (not a GString): i.e. use '${}' (single quotes).
After using Spring Integration in a project, my observation is to use jdbc adapter or gateway only at the start or end of the flow. If we use them in the middle of flow then it will become too verbose and complex.
For example:
<jdbc:outbound-gateway
query="select * from foo where
c1=:headers[c1] AND
c2=:headers[c2] AND
c3=:headers[c3] AND
c4=:headers[c4]"
row-mapper="fooMapper" data-source="myDataSource" max-rows-per-poll="100000" />
<int:service-activator ref="serviceActivator" method="processFoo" />
In the above <jdbc:outbound-gateway>, we need to pass all the placeholders (c1, c2, c3, c4) in the header of Message. We need to look back and forth in java code and xml file for any change in where condition or when there are too many where clauses.
It is also error prone. For example, if we misspelled :headers[c1] to :headers[d1] then it will not throw any exception and replace :headers[d1] with null.
If query does not return any row then it will throw exception by default. So, we have to use requires-reply="false" to change default behaviour.
If we want to proceed when query does not return any value then we have to add advice to gateway, as shown below:
<jdbc:outbound-gateway ... >
<jdbc:request-handler-advice-chain>
<bean class="com.service.NullReplyAdvice" />
</jdbc:request-handler-advice-chain>
</jdbc:outbound-gateway>
Please correct me if there are flaws in understanding of the concept.
We need to look back and forth in java code and xml file for any change in where condition or when there are too many where clauses.
It's true even for raw Java code around the JDBC: if you change the model you, of course, should change the SELECT, because it is just a String. And that's why there is a lot of work to make it type-safe - ORM, QueryDSL, Spring-Data etc.
if we misspelled :headers[c1] to :headers[d1] then it will not throw any exception and replace :headers[d1] with null.
That's because the headers is just a Map and it's truth that you get null, if there is no such a key in the map. To overcome that typo issue you can use POJO payload with getters, or some custom header, and again - the POJO with getters. In this case you end up with exception that there is no such a property against object. Although you'll see that issue only at runtime, not on compile. And again the same is with Hashtable - only at runtime.
So, we have to use requires-reply="false" to change default behaviour.
You should understand it at design time: allow or not to return nothing for the component.
The last idea is good. Wouldn't you mind to share your NullReplyAdvice?
Actually I achieve the same with <filter> before the JDBC gateway: to determine if there is something to fetch by count(*) query. From there I can lead my flow to the different logic, rather than the direct flow, when SELECT returns rows.
UPDATE
When you want to use Model object to keep business-specific values within Message, it's just enough to put this object to the header:
public class Foo {
private String foo1;
private String foo2;
public String getFoo1() {
return foo1;
}
public String getFoo2() {
return foo2;
}
}
...
MessageBuilder.withPayload(payload).setHeader("foo", foo).build();
...
<jdbc:outbound-gateway
query="select * from foo where
c1=:headers[foo].foo1 AND
c1=:headers[foo].foo2"/>