I am using camel.version:2.12.1
I have a route that needs
to parse a CSV file
split each rows
for each row, I need to determine target endpoint(s) and the record needs to routed those destinations.
Till date, I have
org.apache.camel.model.ProcessorDefinition.recipientList().method(beanInstance, "methodName")
to route one incoming Camel Message.
How to implement dynamic routing at row level?
Thanks in advance.
Use the split pattern with a custom dispatcher:
public class MessageRouter {
public String routeTo(final String row) {
String id;
if (row.contains("1")) {
id = "sub1";
} else if (row.contains("2")) {
id = "sub2";
} else {
id = "default";
}
return "direct:" + id;
}
}
Route definition:
#Override
public void configure() {
from("direct:start")
.split(body().tokenize("\n"))
.recipientList()
.method(MessageRouter.class);
from("direct:token1")
.log("Token1: body = ${body}");
from("direct:token2")
.log("Token2: body = ${body}");
from("direct:default")
.log("default: body = ${body}");
}
Testing with:
ProducerTemplate template = context.createProducerTemplate();
template.sendBody("direct:start", "token1\ntoken2\ntoken3");
This leads to following output:
INFO sub1: body = token1
INFO sub2: body = token2
INFO default: body = token3
Related
I have a route as follows:
from(fromEndpoint).routeId("ticketRoute")
.log("Received Tickets : ${body}")
.doTry()
.process(exchange -> {
List<TradeTicketDto> ticketDtos = (List<TradeTicketDto>) exchange.getIn().getBody();
ticketDtos.stream()
.forEach(t -> solaceMessagePublisher.sendAsText("BOOKINGSERVICE.TICKET.UPDATED", t));
ticketToTradeConverter.convert(ticketDtos)
.forEach(t -> solaceMessagePublisher.sendAsText("BOOKINGSERVICE.TRADE.UPDATED", t));
}).doCatch(java.lang.RuntimeException.class)
.log(exceptionMessage().toString() + " --> ${body}");
solaceMessagePublisher is a utility class in application which performs some action on passed object (second argument) and finally converts it to json string and sends to a jms topic (first argument).
SolaceMessagePublisher.java
public void sendAsText(final String destinationKey, Object payload) {
LOGGER.debug("Sending object as text to %s",destinationKey);
String destinationValue = null;
if (StringUtils.isNotEmpty(destinationKey)) {
destinationValue = properties.getProperty(destinationKey);
}
LOGGER.debug("Identified Destination Value = %s from key %s", destinationValue,destinationKey);
if (StringUtils.isEmpty(destinationValue)) {
throw new BaseServiceException("Invalid destination for message");
}
sendAsTextToDestination(destinationValue, payload);
}
public void sendAsTextToDestination(final String destinationValue, Object payload) {
if (payload == null) {
LOGGER.debug(" %s %s",EMPTY_PAYLOAD_ERROR_MESSAGE, destinationValue);
return;
}
final String message = messageCreator.createMessageEnvelopAsJSON(payload, ContextProvider.getUserInContext());
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Created message = " + message);
}
jmsTemplate.send(destinationValue, new MessageCreator() {
#Override
public Message createMessage(Session session) throws JMSException {
LOGGER.debug("Creating JMS Text Message");
return session.createTextMessage(message);
}
});
}
I am having a problem in creating a mock endpoint to listen to messages sent to this topic. Question is how to listen to the messages sent to a topic which is out of camel context?
I have tried in my Test using mock:jms:endpoint. It doesn't work.
My Test is as below
#RunWith(SpringJUnit4ClassRunner.class)
#ContextConfiguration(classes = { SiteMain.class })
public class TicketRouteCamelTest extends CamelSpringTestSupport{
#Autowired
protected BaseMessageEnvelopCreator messageCreator;
private static final String MOCK_TICKET_UPDATED_QUEUE = "direct:mockTicketUpdated";
#Before
public void configureMockEndpoints() throws Exception {
//mock input
final AdviceWithRouteBuilder mockRouteAdvice = new AdviceWithRouteBuilder() {
#Override
public void configure() throws Exception {
replaceFromWith(MOCK_TICKET_UPDATED_QUEUE);
}
};
context().getRouteDefinition("ticketRoute").adviceWith(context(), mockRouteAdvice);
}
#Test
public void testTicketRouteWithListOfTickets() throws Exception {
//create test data
TradeTicketDto tradeTicketDto = TradeTestDataHelper.getTradeTicketDto();
//create an exchange and set its body with test data
List<TradeTicketDto> list = new ArrayList<>();
list.add(tradeTicketDto);
list.add(tradeTicketDto);
Exchange requestExchange = ExchangeBuilder.anExchange(context()).build();
requestExchange.getIn().setBody(list);
//create assert on the mock endpoints
MockEndpoint mockTicketUpdatedEndpoint = getMockEndpoint("mock:DEV/bookingservice/ticket/updated");
mockTicketUpdatedEndpoint.expectedBodiesReceived(
messageCreator.createMessageEnvelopAsJSON(list.get(0), ContextProvider.getUserInContext()),
messageCreator.createMessageEnvelopAsJSON(list.get(1), ContextProvider.getUserInContext()) );
MockEndpoint mockTradeUpdatedEndpoint = getMockEndpoint("mock:DEV/bookingservice/trade/updated");
mockTradeUpdatedEndpoint.expectedBodiesReceived(
messageCreator.createMessageEnvelopAsJSON(list.get(0).getTicketInstruments().get(0), ContextProvider.getUserInContext()),
messageCreator.createMessageEnvelopAsJSON(list.get(0).getTicketInstruments().get(1), ContextProvider.getUserInContext()),
messageCreator.createMessageEnvelopAsJSON(list.get(1).getTicketInstruments().get(0), ContextProvider.getUserInContext()),
messageCreator.createMessageEnvelopAsJSON(list.get(1).getTicketInstruments().get(1), ContextProvider.getUserInContext()));
//send test exchange to request mock endpoint
template.send(MOCK_TICKET_UPDATED_QUEUE, requestExchange);
//test the asserts
assertMockEndpointsSatisfied();
}
}
On running test actual bodies received on mockendpont is 0
Mock is NOT a queue for consumers/producers to exchange data. Its a sink for testing purpose where you can setup expectations on the mock.
If you want to simulate a JMS via some kind of other means, then take a look at the stub component: http://camel.apache.org/stub
Its also listed in the bottom of the testing docs at: http://camel.apache.org/testing
Are there any way to configure log4j2 to read Appender attributes from for example a spring bean? I am curious especially in JmsAppender to dynamically set it's target destination based on a parameter read from database and not from JNDI context.
BR
Zoltán
Your best chance is to extend the JMSAppender and override the append methods in the logger. A good example is here
This case , the class extends and uses AMQ to post these messages into. You should be able to extend this from the DB and use API's to get a handle to the Queue or Topic and start appending messages into it. This assumes that you have the right client libraries and permissions to connect to the messaging provider (e.g. in WMQ you may need the QM Name , Queue , Host, port) from the DB (in your case). The extended JMS appender can then be used in your LOG4J2 configuration for sending log messages.
It seems that i found a hybrid soution which is very useful, custom JmsAppender combined with spring context:
#Plugin(name = "OwnJmsAppender", category = "Core", elementType = "appender", printObject = true)
public class OwnJmsAppender extends AbstractAppender {
private final Lock lock = new ReentrantLock();
private Session session;
private Connection connection;
private Destination destination;
private MessageProducer producer;
protected OwnJmsAppender(String name, Filter filter, Layout<? extends Serializable> layout, final boolean ignoreExceptions) {
super(name, filter, layout, ignoreExceptions);
init();
}
#Override
public void append(LogEvent le) {
this.lock.lock();
try {
if (connection == null) {
init();
}
byte[] bytes = getLayout().toByteArray(le);
TextMessage message = session.createTextMessage(new String(bytes, Charset.forName("UTF-8")));
producer.send(message);
} catch (JMSException e) {
LOGGER.error(e);
} finally {
this.lock.unlock();
}
}
#Override
public void stop() {
super.stop();
try {
session.close();
connection.close();
} catch (JMSException e) {
LOGGER.error(e);
}
}
/**
* Reading attributes from log4j2.xml configuration by {#link PluginElement}
* annotation. Also initiates the logger.
*
* #param name
* #param layout
* #param filter
* #return
*/
#PluginFactory
public static OwnJmsAppender createAppender(#PluginAttribute("name") String name,
#PluginElement("PatternLayout") Layout<? extends Serializable> layout, #PluginElement("Filter") final Filter filter) {
if (name == null) {
LOGGER.error("No name provided for OwnJmsAppender");
return null;
}
return new OwnJmsAppender(name, filter, getLayout(layout), true);
}
private static Layout<? extends Serializable> getLayout(Layout<? extends Serializable> layout) {
Layout<? extends Serializable> finalLayout = layout;
if (finalLayout == null) {
finalLayout = PatternLayout.createDefaultLayout();
}
return finalLayout;
}
private void init() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(CommonDbConfig.class);
ParameterStorage parameterStorage = (DatabaseParameterStorage) context.getBean("databaseParameterStorage");
// the parameterStorage springbean reads params from database
String brokerUri = parameterStorage.getStringValue("broker.url");
String queueName = "logQueue";
context.close();
try {
ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory(brokerUri);
connection = connectionFactory.createConnection();
connection.start();
session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
destination = session.createQueue(queueName);
producer = session.createProducer(destination);
producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);
} catch (JMSException e) {
LOGGER.error(e);
}
}
}
And call it from log4j2.xml:
<Configuration>
<Appenders>
<OwnJmsAppender name="jmsQueue">
<PatternLayout pattern="%maxLen{%d{DEFAULT} [%p] - %m %xEx%n}{500}" />
</OwnJmsAppender>
</Appenders>
<Loggers>
<Logger name="com.your.package" level="info" additivity="false">
<AppenderRef ref="jmsQueue" />
</Logger>
</Loggers>
</Configuration>
I am looking to implement a route where reslet/cxfrs end point will accept file as multipart request and process. (Request may have some JSON data as well.
Thanks in advance.
Regards.
[EDIT]
Have tried following code. Also tried sending file using curl. I can see file related info in headers and debug output, but not able to retrieve attachment.
from("servlet:///hello").process(new Processor() {
#Override
public void process(Exchange exchange) throws Exception {
Message in = exchange.getIn();
StringBuffer v = new StringBuffer();
HttpServletRequest request = (HttpServletRequest) in
.getHeaders().get(Exchange.HTTP_SERVLET_REQUEST);
DiskFileItemFactory diskFile = new DiskFileItemFactory();
FileItemFactory factory = diskFile;
ServletFileUpload upload = new ServletFileUpload(factory);
List items = upload.parseRequest(request);
.....
curl :
curl -vvv -i -X POST -H "Content-Type: multipart/form-data" -F "image=#/Users/navaltiger/1.jpg; type=image/jpg" http://:8080/JettySample/camel/hello
following code works (but can't use as it embeds jetty, and we would like to deploy it on tomcat/weblogic)
public void configure() throws Exception {
// getContext().getProperties().put("CamelJettyTempDir", "target");
getContext().setStreamCaching(true);
getContext().setTracing(true);
from("jetty:///test").process(new Processor() {
// from("servlet:///hello").process(new Processor() {
public void process(Exchange exchange) throws Exception {
String body = exchange.getIn().getBody(String.class);
HttpServletRequest request = exchange.getIn().getBody(
HttpServletRequest.class);
StringBuffer v = new StringBuffer();
// byte[] picture = (request.getParameter("image")).getBytes();
v.append("\n Printing All Request Parameters From HttpSerlvetRequest: \n+"+body +" \n\n");
Enumeration<String> requestParameters = request
.getParameterNames();
while (requestParameters.hasMoreElements()) {
String paramName = (String) requestParameters.nextElement();
v.append("\n Request Paramter Name: " + paramName
+ ", Value - " + request.getParameter(paramName));
}
I had a similar problem and managed to resolve inspired by the answer of brentos. The rest endpoint in my case is defined via xml:
<restContext id="UploaderServices" xmlns="http://camel.apache.org/schema/spring">
<rest path="/uploader">
<post bindingMode="off" uri="/upload" produces="application/json">
<to uri="bean:UploaderService?method=uploadData"/>
</post>
</rest>
</restContext>
I had to use "bindingMode=off" to disable xml/json unmarshalling because the HttpRequest body contains multipart data (json/text+file) and obviously the standard unmarshaling process was unable to process the request because it's expecting a string in the body and not a multipart payload.
The file and other parameters are sent from a front end that uses the file upload angular module: https://github.com/danialfarid/ng-file-upload
To solve CORS problems I had to add a CORSFilter filter in the web.xml like the one here:
public class CORSFilter implements Filter {
#Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException,
ServletException {
HttpServletResponse httpResp = (HttpServletResponse) resp;
HttpServletRequest httpReq = (HttpServletRequest) req;
httpResp.setHeader("Access-Control-Allow-Methods", "GET, HEAD, POST, PUT, DELETE, TRACE, OPTIONS, CONNECT, PATCH");
httpResp.setHeader("Access-Control-Allow-Origin", "*");
if (httpReq.getMethod().equalsIgnoreCase("OPTIONS")) {
httpResp.setHeader("Access-Control-Allow-Headers",
httpReq.getHeader("Access-Control-Request-Headers"));
}
chain.doFilter(req, resp);
}
#Override
public void init(FilterConfig arg0) throws ServletException {
}
#Override
public void destroy() {
}
}
Also, I had to modify a little bit the unmarshaling part:
public String uploadData(Message exchange) {
String contentType=(String) exchange.getIn().getHeader(Exchange.CONTENT_TYPE);
MediaType mediaType = MediaType.valueOf(contentType); //otherwise the boundary parameter is lost
InputRepresentation representation = new InputRepresentation(exchange
.getBody(InputStream.class), mediaType);
try {
List<FileItem> items = new RestletFileUpload(
new DiskFileItemFactory())
.parseRepresentation(representation);
for (FileItem item : items) {
if (!item.isFormField()) {
InputStream inputStream = item.getInputStream();
// Path destination = Paths.get("MyFile.jpg");
// Files.copy(inputStream, destination,
// StandardCopyOption.REPLACE_EXISTING);
System.out.println("found file in request:" + item);
}else{
System.out.println("found string in request:" + new String(item.get(), "UTF-8"));
}
}
} catch (Exception e) {
e.printStackTrace();
}
return "200";
}
I'm using the Camel REST DSL with Restlet and was able to get file uploads working with the following code.
rest("/images").description("Image Upload Service")
.consumes("multipart/form-data").produces("application/json")
.post().description("Uploads image")
.to("direct:uploadImage");
from("direct:uploadImage")
.process(new Processor() {
#Override
public void process(Exchange exchange) throws Exception {
MediaType mediaType =
exchange.getIn().getHeader(Exchange.CONTENT_TYPE, MediaType.class);
InputRepresentation representation =
new InputRepresentation(
exchange.getIn().getBody(InputStream.class), mediaType);
try {
List<FileItem> items =
new RestletFileUpload(
new DiskFileItemFactory()).parseRepresentation(representation);
for (FileItem item : items) {
if (!item.isFormField()) {
InputStream inputStream = item.getInputStream();
Path destination = Paths.get("MyFile.jpg");
Files.copy(inputStream, destination,
StandardCopyOption.REPLACE_EXISTING);
}
}
} catch (FileUploadException | IOException e) {
e.printStackTrace();
}
}
});
you can do this with restdsl even if you are not using restlet (exemple jetty) for your restdsl component.
you need to turn restdinding of first for that route and reate two classes to handle the multipart that is in your body.
you need two classes :
DWRequestContext
DWFileUpload
and then you use them in your custom processor
here is the code :
DWRequestContext.java
import org.apache.camel.Exchange;
import org.apache.commons.fileupload.RequestContext;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
public class DWRequestContext implements RequestContext {
private Exchange exchange;
public DWRequestContext(Exchange exchange) {
this.exchange = exchange;
}
public String getCharacterEncoding() {
return StandardCharsets.UTF_8.toString();
}
//could compute here (we have stream cache enabled)
public int getContentLength() {
return (int) -1;
}
public String getContentType() {
return exchange.getIn().getHeader("Content-Type").toString();
}
public InputStream getInputStream() throws IOException {
return this.exchange.getIn().getBody(InputStream.class);
}
}
DWFileUpload.java
import org.apache.camel.Exchange;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileItemFactory;
import org.apache.commons.fileupload.FileUpload;
import org.apache.commons.fileupload.FileUploadException;
import java.util.List;
public class DWFileUpload extends
FileUpload {
public DWFileUpload() {
super();
}
public DWFileUpload(FileItemFactory fileItemFactory) {
super(fileItemFactory);
}
public List<FileItem> parseInputStream(Exchange exchange)
throws FileUploadException {
return parseRequest(new DWRequestContext(exchange));
}
}
you can define your processor like this:
routeDefinition.process(new Processor() {
#Override
public void process(Exchange exchange) throws Exception {
// Create a factory for disk-based file items
DiskFileItemFactory factory = new DiskFileItemFactory();
factory.setRepository(new File(System.getProperty("java.io.tmpdir")));
DWFileUpload upload = new DWFileUpload(factory);
java.util.List<FileItem> items = upload.parseInputStream(exchange);
//here I assume I have only one, but I could split it here somehow and link them to camel properties...
//with this, the first file sended with your multipart replaces the body
// of the exchange for the next processor to handle it
exchange.getIn().setBody(items.get(0).getInputStream());
}
});
I stumbled into the same requirement of having to consume a multipart request (containing file data including binary) through Apache Camel Restlet component.
Even though 2.17.x is out, since my project was part of a wider framework / application, I had to be using version 2.12.4.
Initially, my solution drew a lot from restlet-jdbc example yielded data in exchange that although was successfully retrieving text files but I was unable to retrieve correct binary content.
I attempted to dump the data directly into a file to inspect the content using following code (abridged).
from("restlet:/upload?restletMethod=POST")
.to("direct:save-files");
from("direct:save-files")
.process(new org.apache.camel.Processor(){
public void process(org.apache.camel.Exchange exchange){
/*
* Code to sniff exchange content
*/
}
})
.to("file:///C:/<path to a folder>");
;
I used org.apache.commons.fileupload.MultipartStream from apache fileuplaod library to write following utility class to parse Multipart request from a file. It worked successfully when the output of a mulitpart request from Postman was fed to it. However, failed to parse content of the file created by Camel (even through to eyes content of both files looked similar).
public class MultipartParserFileCreator{
public static final String DELIMITER = "\\r?\\n";
public static void main(String[] args) throws Exception {
// taking it from the content-type in exchange
byte[] boundary = "------5lXVNrZvONBWFXxd".getBytes();
FileInputStream fis = new FileInputStream(new File("<path-to-file>"));
extractFile(fis, boundary);
}
public static void extractFile(InputStream is, byte[] boundary) throws Exception {
MultipartStream multipartStream = new MultipartStream(is, boundary, 1024*4, null);
boolean nextPart = multipartStream.skipPreamble();
while (nextPart) {
String headers = multipartStream.readHeaders();
if(isFileContent(headers)) {
String filename = getFileName(headers);
File file = new File("<dir-where-file-created>"+filename);
if(!file.exists()) {
file.createNewFile();
}
FileOutputStream fos = new FileOutputStream(file);
multipartStream.readBodyData(fos);
fos.flush();
fos.close();
}else {
multipartStream.readBodyData(System.out);
}
nextPart = multipartStream.readBoundary();
}
}
public static String[] getContentDispositionTokens(String headersJoined) {
String[] headers = headersJoined.split(DELIMITER, -1);
for(String header: headers) {
System.out.println("Processing header: "+header);
if(header != null && header.startsWith("Content-Disposition:")) {
return header.split(";");
}
}
throw new RuntimeException(
String.format("[%s] header not found in supplied headers [%s]", "Content-Disposition:", headersJoined));
}
public static boolean isFileContent(String header) {
String[] tokens = getContentDispositionTokens(header);
for (String token : tokens) {
if (token.trim().startsWith("filename")) {
return true;
}
}
return false;
}
public static String getFileName(String header) {
String[] tokens = getContentDispositionTokens(header);
for (String token : tokens) {
if (token.trim().startsWith("filename")) {
String filename = token.substring(token.indexOf("=") + 2, token.length()-1);
System.out.println("fileName is " + filename);
return filename;
}
}
return null;
}
}
On debugging through the Camel code, I noticed that at one stage Camel is converting the entire content into String. After a point I had to stop pursuing this approach as there was very little on net applicable for version 2.12.4 and my work was not going anywhere.
Finally, I resorted to following solution
Write an implementation of HttpServletRequestWrapper to allow
multiple read of input stream. One can get an idea from
How to read request.getInputStream() multiple times
Create a filter that uses the above to wrap HttpServletRequest object, reads and extract the file to a directory Convenient way to parse incoming multipart/form-data parameters in a Servlet and attach the path to the request using request.setAttribute() method. With web.xml, configure this filter on restlet servlet
In the process method of camel route, type cast the
exchange.getIn().getBody() in HttpServletRequest object, extract the
attribute (path) use it to read the file as ByteStreamArray for
further processing
Not the cleanest, but I could achieve the objective.
When working with bindy, I have create a test that provides invalid CSV input.
When looking at the documentation ( http://camel.apache.org/bindy.html ), it states:
If this field is not present in the record, than an error will be raised by the parser with the following information :
Some fields are missing (optional or mandatory), line :
But when I run my test, the invalid line is simply ignored, no errors are raised. I declare three required fields, so I'd expect an error.... What am I doing wrong?
Barry
Here are some code-snippets to clarify
The route
#Override
protected RouteBuilder createRouteBuilder() throws Exception {
return new RouteBuilder() {
#Override
public void configure() throws Exception {
JaxbDataFormat xmlFormat = new JaxbDataFormat();
xmlFormat.setContextPath("be.smals.dp.asktutor.response");
BindyCsvDataFormat csvFormat = new BindyCsvDataFormat ("be.smals.dp.asktutor.response");
context.setTracing(true);
from("direct:marshall")
.wireTap("log:test")
.unmarshal(csvFormat)
.to("mock:marshall");
from("direct:unmarshall")
.marshal(xmlFormat)
.wireTap("log:test")
.to("mock:unmarshall");
}
};
}
Part of my test
#Test
public void testTransformFromCSVToXML() throws Exception {
// Create CSV input and process it
String payload = AskTutorResponseCSVMother.getInvalidCSVLines();
template.sendBody("direct:marshall", payload);
AskTutorsResponse askTutorsResponse =
ExchangeToObjectHelper.getAskTutorsResponseObjectFromExchange(
mockMarshall.getExchanges().get(0));
assertEquals("00000000123", askTutorsResponse.getAskTutorResponses().get(0).getSsinChild());
The input csv string
public static String getInvalidCSVLines () {
String payload = "";
payload += "00000000321;20121212" + NEWLINE;
payload += "10000000123;10000000321;20131010" + NEWLINE;
payload += "20000000123;20000000321;20100909" + NEWLINE;
return payload;
}
And my (straight-forward) bindings:
#XmlType
#XmlAccessorType(XmlAccessType.NONE)
#CsvRecord(separator = ";", skipFirstLine = false)
public class AskTutorResponse {
#DataField(pos = 1, required = true)
#XmlElement(name = "SINNChild", required = true)
private String ssinChild;
#DataField(pos = 2)
#XmlElement(name = "SINNTutor", required = true)
private String ssinTutor;
#DataField(pos = 3)
#XmlElement(name = "date", required = true)
private String date;
I've had problems where multiple classes with bindy annotations in the same package failed to properly unmarshal. The reason is that bindy tried to unmarshall each CSV line into an instance of each annotated class. My first fix was to put each bindy class into its own package. I've since written my own bindy data format that allows a single class to be specified as the unmarshal target. Here is the code.
I would like to log some information in case of fault.
In particular I'd like to log the ip address and port of the client contacting the server, the username if the security in active, and, if possible, also the incoming message.
I added an interceptor in the getOutFaultInterceptors chain of the endpoint, but in the handleMessage I don't know which properties I can use.
Some ideas?
Thank you
In your endpoint xml definition, you could add the following to log incoming messages:
<bean id="logInInterceptor"
class="org.apache.cxf.interceptor.LoggingInInterceptor" />
<jaxws:inInterceptors>
<ref bean="logInInterceptor"/>
</jaxws:inInterceptors>
and then use the bus to limit how many characters you want logged:
<cxf:bus>
<cxf:features>
<cxf:logging limit="102400"/>
</cxf:features>
<cxf:bus>
You haven't mentioned what your method of authentication is, so ff you are using an implementation of UsernameTokenValidator, you could log the incoming username there.
To log details like the client's ip address and port, extend LoggingInInterceptor, then use the following code in handleMessage():
handleMessage() {
HttpServletRequest request =
(HttpServletRequest)message.get(AbstractHTTPDestination.HTTP_REQUEST);
if (null != request) {
String clientAddress = request.getRemoteAddr();
int remotePort = request.getRemotePort();
// log them
}
}
Also have a look at this thread.
I solved in this way
public FaultInterceptor() {
super(Phase.MARSHAL);
}
public void handleMessage(SoapMessage message) throws Fault {
Fault fault = (Fault) message.getContent(Exception.class);
Message inMessage = message.getExchange().getInMessage();
if (inMessage == null) return;
String xmlMessage = null;
InputStream is = inMessage.getContent(InputStream.class);
String rawXml = null;
if (is != null) {
rawXml = is.toString();
}
String username = null;
if (rawXml != null && rawXml.length() > 0) {
try {
XPath xpath = XPathFactory.newInstance().newXPath();
XPathExpression xpathExpression;
xpathExpression = xpath.compile("//*[local-name()=\'Envelope\']/*[local-name()=\'Header\']/*[local-name()=\'Security\']" +
"/*[local-name()=\'UsernameToken\']/*[local-name()=\'Username\']");
InputSource source = new InputSource(new StringReader(rawXml));
username = xpathExpression.evaluate(source);
} catch (XPathExpressionException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
xmlMessage = XMLUtils.prittyPrinter(is.toString());
}
String clientAddress = "<unknown>";
int clientPort = -1;
HttpServletRequest request = (HttpServletRequest)inMessage.get(AbstractHTTPDestination.HTTP_REQUEST);
if (null != request) {
clientAddress = request.getRemoteAddr();
clientPort = request.getRemotePort();
}
logger.warn("User: " + username + " [" + clientAddress + ":" + clientPort + "] caused fault: " + fault +
"\nMessage received: \n" + xmlMessage);
}
I found the "inMessage" property and on it I found the original message (and I can retrieve the username) and the "request" from which I retrieved the host and port.
Thank you.
I think you should view the request input stream as consumed when handling the fault.
I suggest you always log the incoming message, and extract some kind of message correlation id - for example username. Keep it as a Message header.
For fault logging, use a fault interceptor which is limited to looking at the input request.
Tie regular + fault logging together with the message correlation id.
Log the full soap request, not just the payload. Soap requests might have headers in addition to the body.
See this question for regular logging, add in addition an output fault interceptor like so:
public class SoapFaultLoggingOutInterceptor extends AbstractPhaseInterceptor<Message> {
private static final String LOCAL_NAME = "MessageID";
private static final int PROPERTIES_SIZE = 128;
private String name = "<interceptor name not set>";
protected Logger logger = null;
protected Level level;
public SoapFaultLoggingOutInterceptor() {
this(LogUtils.getLogger(SoapFaultLoggingOutInterceptor.class), Level.WARNING);
}
public SoapFaultLoggingOutInterceptor(Logger logger, Level reformatSuccessLevel) {
super(Phase.MARSHAL);
this.logger = logger;
this.level = reformatSuccessLevel;
}
public void setName(String name) {
this.name = name;
}
public void handleMessage(Message message) throws Fault {
if (!logger.isLoggable(level)) {
return;
}
StringBuilder buffer = new StringBuilder(PROPERTIES_SIZE);
// perform local logging - to the buffer
buffer.append(name);
logProperties(buffer, message);
logger.log(level, buffer.toString());
}
/**
* Gets theMessageID header in the list of headers.
*
*/
protected String getIdHeader(Message message) {
return getHeader(message, LOCAL_NAME);
}
protected String getHeader(Message message, String name) {
List<Header> headers = (List<Header>) message.get(Header.HEADER_LIST);
if(headers != null) {
for(Header header:headers) {
if(header.getName().getLocalPart().equalsIgnoreCase(name)) {
return header.getObject().toString();
}
}
}
return null;
}
protected void logProperties(StringBuilder buffer, Message message) {
final String messageId = getIdHeader(message);
if(messageId != null) {
buffer.append(" MessageId=");
buffer.append(messageId);
}
Message inMessage = message.getExchange().getInMessage();
HttpServletRequest request = (HttpServletRequest)inMessage.get(AbstractHTTPDestination.HTTP_REQUEST);
buffer.append(" RemoteAddr=");
buffer.append(request.getRemoteAddr());
}
public Logger getLogger() {
return logger;
}
public String getName() {
return name;
}
public void setLogger(Logger logger) {
this.logger = logger;
}
}
where MessageID is the correlation / breadcrumb id.