Spring Integration Web Service - Jaxb2Marshaller - How I can use SAX Parser? - jaxb

I am using Jaxb2Marshaller with Spring Integration. I have an inbound gateway web service, when someone call it, it will auto parse into JAXB generated classes.
But when I debug into the source, I see Jaxb2Marshaller using DOM. I thought it would use SAX as for binding XML data into Java object, SAX is faster. Why Jaxb2Marshaller use DOM by default ? How can I configure it to use SAX ?
As I checked the document
The
unmarshaller requires an instance of Source. If the message payload is not an instance of Source,
conversion will be attempted. Currently String, File and org.w3c.dom.Document payloads are
supported. Custom conversion to a Source is also supported by injecting an implementation of a
SourceFactory.
Note
If a SourceFactory is not set explicitly, the property on the UnmarshallingTransformer will
by default be set to a DomSourceFactory
About SourceFactory
http://docs.spring.io/spring-integration/api/org/springframework/integration/xml/source/SourceFactory.html
We can see that currently, it only has DomSourceFactory and StringSourceFactory. There is no SaxSourceFactory.
So we can't use SAX with Jaxb2Marshaller, right ?
Will it have SaxSourceFactory in the future ? or never ?
The weird thing is when I check Jaxb2Marshaller , I see the code already handle SAX
XMLReader xmlReader = null;
InputSource inputSource = null;
if (source instanceof SAXSource) {
SAXSource saxSource = (SAXSource) source;
xmlReader = saxSource.getXMLReader();
inputSource = saxSource.getInputSource();
}
else if (source instanceof StreamSource) {
StreamSource streamSource = (StreamSource) source;
if (streamSource.getInputStream() != null) {
inputSource = new InputSource(streamSource.getInputStream());
}
else if (streamSource.getReader() != null) {
inputSource = new InputSource(streamSource.getReader());
}
else {
inputSource = new InputSource(streamSource.getSystemId());
}
}
So, the final question is CAN I configure use Spring Integration Web Service with JAXB with SAX ? Am I missed something?
Here is my configurations:
<ws:inbound-gateway id="inbound-gateway" request-channel="RequestChannel" reply-channel="ResponseChannel"
marshaller="marshaller" unmarshaller="marshaller" />
<int:channel id="RequestChannel" />
<int:channel id="ResponseChannel" />
<bean id="marshaller" class="org.springframework.oxm.jaxb.Jaxb2Marshaller">
<property name="contextPath" value="com.example.webservice.api"/>
</bean>
Thank you and best regards,
Nha Nguyen

I'm using WebServiceGatewaySupport classes and tried adding a AxiomSoapMessageFactory as Bean to the application context, until I found out that WebServiceTemplate does not load the WebServiceMessageFactory from the application context. So I ended up with adding a constructor:
public class SomeServiceImpl extends WebServiceGatewaySupport implements SomeService {
public SomeServiceImpl(WebServiceMessageFactory messageFactory) {
super(messageFactory);
}
}
and building the service myself with a #Configuration class:
#Configuration
public class WebServicesConfiguration {
private WebServiceMessageFactory webServiceMessageFactory = new AxiomSoapMessageFactory();
#Bean
public SomeService someService() {
return new SomeServiceImpl(webServiceMessageFactory);
}
}

Try to configure AxiomSoapMessageFactory bean with the name MessageDispatcherServlet.DEFAULT_MESSAGE_FACTORY_BEAN_NAME.
By default it is SaajSoapMessageFactory which does exactly this before unmarshalling:
public Source getPayloadSource() {
SOAPElement bodyElement = SaajUtils.getFirstBodyElement(getSaajBody());
return bodyElement != null ? new DOMSource(bodyElement) : null;
}
where Axiom is based on STaX:
XMLStreamReader streamReader = getStreamReader(payloadElement);
return StaxUtils.createCustomStaxSource(streamReader);
And class StaxSource extends SAXSource {

Related

Spring Integration - Customize ObjectMapper used by WebFlux OutboundGateway

How do we customize the Jackson ObjectMapper used by WebFlux OutboundGateway? The normal customization done via Jackson2ObjectMapperBuilder or Jackson2ObjectMapperBuilderCustomizer is NOT respected.
Without this customization, LocalDate is serialized as SerializationFeature.WRITE_DATES_AS_TIMESTAMPS. Sample output - [2022-10-20] and there is NO way to customize the format
I assume you really talk about Spring Boot auto-configuration which is applied to the WebFlux instance. Consider to use an overloaded WebFlux.outboundGateway(String uri, WebClient webClient) to be able to auto-wire a WebClient.Builder which might be already configured with the mentioned customized ObjectMapper.
Registering a bean of type com.fasterxml.jackson.databind.module.SimpleModule will automatically be used by the pre-configured ObjectMapper bean. In SimpleModule, it is possible to register custom serialization and deserialization specifications.
To put that into code, a very simple solution would be the following:
#Bean
public SimpleModule odtModule() {
SimpleModule module = new SimpleModule();
JsonSerializer<LocalDate> serializer = new JsonSerializer<>() {
#Override
public void serialize(LocalDate odt, JsonGenerator jgen, SerializerProvider provider) throws IOException {
String formatted = odt.format(DateTimeFormatter.ISO_LOCAL_DATE);
jgen.writeString(formatted);
}
};
JsonDeserializer<LocalDate> deserializer = new JsonDeserializer<>() {
#Override
public LocalDate deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
return LocalDate.parse(jsonParser.getValueAsString());
}
};
module.addSerializer(LocalDate.class, serializer);
module.addDeserializer(LocalDate.class, deserializer);
return module;
}
Note that using lambdas for the implementations has sometimes resulted in weird behaviors for me, so I tend not to do that.

Flowable bpmn access Spring beans inside groovy script

We are setting up a flowable application to use multiple bpmns with reusable java delegates. In application.yml, we have properties set for each bpmn
e.g.
app1.mail.active: true
app2.mail.active: false
ApplicationProperties file is set with #ConfigurationProperties() to get the properties.
Created a Spring bean:
#Bean("applicationProperties")
public ApplicationProperties applicationProperties(){
return new ApplicationProperties();
}
I am trying to use a script task using groovy and initialize the appropriate properties. It seems Spring beans are not available inside the script.
<scriptTask id="sid-C8B8BE3F-F6CB-4559-B48C-5BC14AB76494" name="Initialize Process" scriptFormat="groovy">
<script><![CDATA[
// I want to be able to access applicationProperties bean here
def _properties = applicationProperties.getApp1()
execution.setVariable("properties", _properties)
]]></script>
</scriptTask>
When initializing bpmn for app1, I want to set a variable for properties related to app1 and similar to app2. I am able to use existing spring beans elsewhere in the same bpmn but not inside groovy script task.
Thank you in advance.
The solution that I came up was to create a service task with java delegate to initialize application properties and common variables based on the bpmn instance and set a properties variable, then as part of the same service task I added an executionListener to set variables that are specific to that instance, using groovy script.
<serviceTask id="initProcess" name="Init Process" flowable:delegateExpression="${initProcessDelegate}">
<extensionElements>
<flowable:executionListener event="end" class="org.flowable.engine.impl.bpmn.listener.ScriptExecutionListener">
<flowable:field name="script">
<flowable:string><![CDATA[
import config.ApplicationProperties;
ApplicationProperties.Props props = (ApplicationProperties.Props) execution.getVariable("properties");
System.out.println("properties " + properties.getUrl());
]]></flowable:string>
</flowable:field>
<flowable:field name="language">
<flowable:string><![CDATA[groovy]]></flowable:string>
</flowable:field>
</flowable:executionListener>
</extensionElements>
</serviceTask>
public class ProcessInitDelegate implements JavaDelegate {
Logger log = LoggerFactory.getLogger(getClass());
#Autowired
ApplicationProperties applicationProperties;
public void execute(DelegateExecution execution) {
log.info("<--------------- ProcessInitDelegate ------------->");
try {
// set up properties for current instance
String instance = (String) execution.getVariable("processInstance");
Object properties = applicationProperties.getInnerInstance(instance);
if (instance.equals("<your_instance_name>")) {
ApplicationProperties.Props instanceProperties = (ApplicationProperties.Props) properties;
execution.setVariable("properties, instanceProperties);
}
} catch (Exception e) {
log.error("Error setting up process {} ", e.getMessage());
}
}
}
You can swop over to SecureJavaScript and add the bean to the trusted beans in your custom config.
<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-secure-javascript</artifactId>
<version>6.6.0</version>
</dependency>
#Bean
public SecureJavascriptConfigurator secureJavascriptConfigurator() {
SecureJavascriptConfigurator configurator =
new SecureJavascriptConfigurator()
.setMaxStackDepth(10)
.setMaxScriptExecutionTime(3000L)
.setMaxMemoryUsed(3145728L)
.setNrOfInstructionsBeforeStateCheckCallback(10)
.setEnableAccessToBeans(true);
return configurator;
}
#Bean
#ConditionalOnClass(SpringProcessEngineConfiguration.class)
public EngineConfigurationConfigurer<SpringProcessEngineConfiguration> customProcessEngineConfigurer() {
return configuration -> {
configuration.addConfigurator(secureJavascriptConfigurator());
Map<Object, Object> beans = new HashMap<>();
beans.put("applicationProperties", applicationProperties);
configuration.setBeans(beans);
};
}

Misunderstanding with JobLaunchRequest

this is my previous question: Spring Integration + Spring Batch: the job doesn`t stop.
Project works good with annotation configuration, but I want the same on xml config :)
xml configuration:
<int:service-activator input-channel="fileInputChannel"
method="fileWritingMessageHandler"
output-channel="jobLaunchRequestChannel">
<bean class="service.impl.IntegrationServiceImpl"/>
</int:service-activator>
<int:service-activator input-channel="jobLaunchRequestChannel"
method="jobLaunchRequest"
output-channel="jobLaunchingGatewayChannel">
<bean class="service.impl.IntegrationServiceImpl"/>
</int:service-activator>
<batch-int:job-launching-gateway request-channel="jobLaunchingGatewayChannel"
reply-channel="finish"/>
<int:service-activator input-channel="finish"
ref="integrationServiceImpl"
method="finishJob">
</int:service-activator>
IntegrationConfiguration.java:
#Bean
public FtpInboundFileSynchronizer ftpInboundFileSynchronizer() {
FtpInboundFileSynchronizer fileSynchronizer = new FtpInboundFileSynchronizer(defaultFtpSessionFactory);
fileSynchronizer.setRemoteDirectory(remoteDirectory);
fileSynchronizer.setDeleteRemoteFiles(false);
return fileSynchronizer;
}
#Bean
#InboundChannelAdapter(channel = "fileInputChannel", poller = #Poller(cron = "*/5 * * * * ?"))
public FtpInboundFileSynchronizingMessageSource ftpInboundFileSynchronizingMessageSource(FtpInboundFileSynchronizer fileSynchronizer) throws Exception {
FtpInboundFileSynchronizingMessageSource messageSource = new FtpInboundFileSynchronizingMessageSource(fileSynchronizer);
messageSource.setAutoCreateLocalDirectory(true);
messageSource.setLocalDirectory(new File(localDirectory));
messageSource.setLocalFilter(new AcceptOnceFileListFilter<>());
return messageSource;
}
IntegrationServiceImpl:
#Override
public FileWritingMessageHandler fileWritingMessageHandler() {
FileWritingMessageHandler messageHandler = new FileWritingMessageHandler(new File(storageDirectory));
messageHandler.setDeleteSourceFiles(true);
messageHandler.setFileNameGenerator(message -> {
Long timestamp = new Date().getTime();
log.info(timestamp);
return "test_" + timestamp;
});
return messageHandler;
}
#Override
public JobLaunchRequest jobLaunchRequest(File file) throws IOException {
// public JobLaunchRequest jobLaunchRequest(FileWritingMessageHandler fileWritingMessageHandler) throws IOException {
String[] content = FileUtils.readFileToString(file, "UTF-8").split("\\s+");
JobParameters jobParameters = new JobParametersBuilder()
.addString("filename", file.getAbsolutePath())
.addString("id", content[0])
.addString("salary", content[1])
.toJobParameters();
log.info(jobParameters);
return new JobLaunchRequest(increaseSalaryJob, jobParameters);
}
#Override
public void finishJob() {
log.info("Job finished");
}
As you can see this xml config like previous post annotation config, BUT i have an error:
Caused by: org.springframework.expression.spel.SpelEvaluationException: EL1004E: Method call: Method jobLaunchRequest(org.springframework.integration.file.FileWritingMessageHandler) cannot be found on type service.impl.IntegrationServiceImpl
Why i can't use jobLaunchRequest(File)? And if i need to use jobLaunchRequest(FileWritingMessageHandler) how can I operate with file?
The jobLaunchRequest() method definitely has to be with a File argument because that is indeed what produced as a payload in the reply message from the FileWritingMessageHandler.
Your <int:service-activator input-channel="fileInputChannel"
method="fileWritingMessageHandler"> definition is wrong.
Since you would like to use a FileWritingMessageHandler as a service, you need to consider to use an <int-file:outbound-gateway> instead.
The service-activator is for calling POJO methods. Since FileWritingMessageHandler is a MessageHandler implementation it has to be used in the <service-activator> directly from the ref attribute without any method attribute usage.

Exception Router using Annotation

I am trying to convert my code into java annotation but i am stuck with
<int:exception-type-router input-channel="failed-email-fetch" default-output-channel="errorChannel">
<int:mapping exception-type="com.XXXXXX.RateException" channel="customError" />
</int:exception-type-router>
if i used #Router i did not know what to return and this what i used but did not work
#ServiceActivator(inputChannel = "failedEmailFetch")
public ErrorMessageExceptionTypeRouter handleError(MessageHandlingException messageHandlingException) {
ErrorMessageExceptionTypeRouter errorMessageExceptionTypeRouter = new ErrorMessageExceptionTypeRouter();
errorMessageExceptionTypeRouter.setChannelMapping("com.XXXXXX.exception.MessageException","customError");
errorMessageExceptionTypeRouter.setDefaultOutputChannelName("errorChannel");
return errorMessageExceptionTypeRouter;
}
You also need #Bean when the #ServiceActivator annotation is on a MessageHandler.
#ServiceActivator alone is for POJO messaging.
See Annotations on Beans.
Consuming endpoints have 2 beans, the handler and a consumer; the #ServiceActivator defines the consumer. The #Bean is the hander.
I ended up using the below not sure if it is the best way
#Router(inputChannel = "failedEmailFetch",defaultOutputChannel = "errorChannel")
public String handleError(Message<AggregateMessageDeliveryException> message) {
log.info("{}",message.getPayload().getCause().getCause());
if( message.getPayload().getRootCause() instanceof MessageException)
return "customError";
else
return "errorChannel";
}

Spring Integration Cassandra persistence workflow

I try to realize the following workflow with Spring Integration:
1) Poll REST API
2) store the POJO in Cassandra cluster
It's my first try with Spring Integration, so I'm still a bit overwhelmed about the mass of information from the reference. After some research, I could make the following work.
1) Poll REST API
2) Transform mapped POJO JSON result into a string
3) save string into file
Here's the code:
#Configuration
public class ConsulIntegrationConfig {
#InboundChannelAdapter(value = "consulHttp", poller = #Poller(maxMessagesPerPoll = "1", fixedDelay = "1000"))
public String consulAgentPoller() {
return "";
}
#Bean
public MessageChannel consulHttp() {
return MessageChannels.direct("consulHttp").get();
}
#Bean
#ServiceActivator(inputChannel = "consulHttp")
MessageHandler consulAgentHandler() {
final HttpRequestExecutingMessageHandler handler =
new HttpRequestExecutingMessageHandler("http://localhost:8500/v1/agent/self");
handler.setExpectedResponseType(AgentSelfResult.class);
handler.setOutputChannelName("consulAgentSelfChannel");
LOG.info("Created bean'consulAgentHandler'");
return handler;
}
#Bean
public MessageChannel consulAgentSelfChannel() {
return MessageChannels.direct("consulAgentSelfChannel").get();
}
#Bean
public MessageChannel consulAgentSelfFileChannel() {
return MessageChannels.direct("consulAgentSelfFileChannel").get();
}
#Bean
#ServiceActivator(inputChannel = "consulAgentSelfFileChannel")
MessageHandler consulAgentFileHandler() {
final Expression directoryExpression = new SpelExpressionParser().parseExpression("'./'");
final FileWritingMessageHandler handler = new FileWritingMessageHandler(directoryExpression);
handler.setFileNameGenerator(message -> "../../agent_self.txt");
handler.setFileExistsMode(FileExistsMode.APPEND);
handler.setCharset("UTF-8");
handler.setExpectReply(false);
return handler;
}
}
#Component
public final class ConsulAgentTransformer {
#Transformer(inputChannel = "consulAgentSelfChannel", outputChannel = "consulAgentSelfFileChannel")
public String transform(final AgentSelfResult json) throws IOException {
final String result = new StringBuilder(json.toString()).append("\n").toString();
return result;
}
This works fine!
But now, instead of writing the object to a file, I want to store it in a Cassandra cluster with spring-data-cassandra. For that, I commented out the file handler in the config file, return the POJO in transformer and created the following, :
#MessagingGateway(name = "consulCassandraGateway", defaultRequestChannel = "consulAgentSelfFileChannel")
public interface CassandraStorageService {
#Gateway(requestChannel="consulAgentSelfFileChannel")
void store(AgentSelfResult agentSelfResult);
}
#Component
public final class CassandraStorageServiceImpl implements CassandraStorageService {
#Override
public void store(AgentSelfResult agentSelfResult) {
//use spring-data-cassandra repository to store
LOG.info("Received 'AgentSelfResult': {} in Cassandra cluster...");
LOG.info("Trying to store 'AgentSelfResult' in Cassandra cluster...");
}
}
But this seems to be a wrong approach, the service method is never triggered.
So my question is, what would be a correct approach for my usecase? Do I have to implement the MessageHandler interface in my service component, and use a #ServiceActivator in my config. Or is there something missing in my current "gateway-approach"?? Or maybe there is another solution, that I'm not able to see..
Like mentioned before, I'm new to SI, so this may be a stupid question...
Nevertheless, thanks a lot in advance!
It's not clear how you are wiring in your CassandraStorageService bean.
The Spring Integration Cassandra Extension Project has a message-handler implementation.
The Cassandra Sink in spring-cloud-stream-modules uses it with Java configuration so you can use that as an example.
So I finally made it work. All I needed to do was
#Component
public final class CassandraStorageServiceImpl implements CassandraStorageService {
#ServiceActivator(inputChannel="consulAgentSelfFileChannel")
#Override
public void store(AgentSelfResult agentSelfResult) {
//use spring-data-cassandra repository to store
LOG.info("Received 'AgentSelfResult': {}...");
LOG.info("Trying to store 'AgentSelfResult' in Cassandra cluster...");
}
}
The CassandraMessageHandler and the spring-cloud-streaming seemed to be a to big overhead to my use case, and I didn't really understand yet... And with this solution, I keep control over what happens in my spring component.

Resources