I'm trying to implement simple web service client for PayPal Express Checkout API using JAX WS. PayPal Express Checkout API provides WSDL file, from which I was able to generate Java classes using CXF's wsdl2java utility.
From authentication reasons, it demands adding SOAP Header to each request. This header is quite simple and should look like here: https://cms.paypal.com/us/cgi-bin/?cmd=_render-content&content_ID=developer/e_howto_api_ECSOAPAPIBasics#id09C3I0CF0O6
Generated from WSDL classes include ebay.apis.eblbasecomponents.CustomSecurityHeaderType class which represents header which I need to add to each request.
So the question is: how can I add manually created instance of CustomSecurityHeaderType class to SOAP request's header taking into account following conditions:
wsimport.exe put the soap action in the "Content-Type" http header, the the service says the Action header is missing in the SOAP message. The WSDL specifies the Action as an attribute of the <operation ..> element inside the <binding ..>
The other attribute that must be added only to a SOAPHeaderElement object is mustUnderstand. This attribute says whether or not the recipient (indicated by the actor attribute) is required to process a header entry.
So, it looks like I've found possible answer while combining JAX-WS & JAXB related answers from SO (I would really appreciate if somebody experienced in these technologies can check whether following is correct):
The obvious thing for me is to add SOAP message handler and alter header of SOAPMessage instance in it:
import javax.xml.ws.Binding; import javax.xml.ws.BindingProvider; import javax.xml.ws.handler.Handler; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBElement; import javax.xml.bind.Marshaller; import javax.xml.soap.SOAPHeader; import ebay.api.paypalapi.ObjectFactory; // class generated by wsdl2java // following class is generated by wsdl2java utility Service class final PayPalAPIInterfaceService payPalService = new PayPalAPIInterfaceService(); final PayPalAPIAAInterface expressCheckoutPort = payPalService.getPayPalAPIAA(); final Binding binding = ((BindingProvider) expressCheckoutPort).getBinding(); List<Handler> handlersList = new ArrayList<Handler>(); // now, adding instance of Handler to handlersList which should do our job: // creating header instance final CustomSecurityHeaderType headerObj = new CustomSecurityHeaderType(); final UserIdPasswordType credentials = new UserIdPasswordType(); credentials.setUsername("username"); credentials.setPassword("password"); credentials.setSignature("signature"); headerObj.setCredentials(credentials); // bookmark #1 - please read explanation after code final ObjectFactory objectFactory = new ObjectFactory(); // creating JAXBElement from headerObj final JAXBElement<CustomSecurityHeaderType> requesterCredentials = objectFactory.createRequesterCredentials(headerObj); handlersList.add(new SOAPHandler<SOAPMessageContext>() { @Override public boolean handleMessage(final SOAPMessageContext context) { try { // checking whether handled message is outbound one as per Martin Strauss answer final Boolean outbound = (Boolean) context.get("javax.xml.ws.handler.message.outbound"); if (outbound != null && outbound) { // obtaining marshaller which should marshal instance to xml final Marshaller marshaller = JAXBContext.newInstance(CustomSecurityHeaderType.class).createMarshaller(); // adding header because otherwise it's null final SOAPHeader soapHeader = context.getMessage().getSOAPPart().getEnvelope().addHeader(); // marshalling instance (appending) to SOAP header's xml node marshaller.marshal(requesterCredentials, soapHeader); } } catch (final Exception e) { throw new RuntimeException(e); } return true; } // ... default implementations of other methods go here }); // as per Jean-Bernard Pellerin's comment setting handlerChain list here, after all handlers were added to list binding.setHandlerChain(handlersList);
Explanation of bookmark #1: one should marshal not the header object itself, but JAXBElement representing that object, because otherwise one will get an exception. One should use one of ObjectFactory classes which are generated from WSDL for creating needed JAXBElement instances from original objects. (Thanks @skaffman for answer: No @XmlRootElement generated by JAXB )
Martin Straus
answer which extends this oneThis solution works great, but there's a catch. It generates this error when the inbound message is processed:
dic 19, 2012 7:00:55 PM com.sun.xml.messaging.saaj.soap.impl.EnvelopeImpl addHeader SEVERE: SAAJ0120: no se puede agregar una cabecera si ya hay una Exception in thread "main" javax.xml.ws.WebServiceException: java.lang.RuntimeException: com.sun.xml.messaging.saaj.SOAPExceptionImpl: Can't add a header when one is already present. at com.sun.xml.ws.handler.ClientSOAPHandlerTube.callHandlersOnResponse(ClientSOAPHandlerTube.java:167) at com.sun.xml.ws.handler.HandlerTube.processResponse(HandlerTube.java:174) at com.sun.xml.ws.api.pipe.Fiber.__doRun(Fiber.java:1074) at com.sun.xml.ws.api.pipe.Fiber._doRun(Fiber.java:979) at com.sun.xml.ws.api.pipe.Fiber.doRun(Fiber.java:950) at com.sun.xml.ws.api.pipe.Fiber.runSync(Fiber.java:825) at com.sun.xml.ws.client.Stub.process(Stub.java:443) at com.sun.xml.ws.client.sei.SEIStub.doProcess(SEIStub.java:174) at com.sun.xml.ws.client.sei.SyncMethodHandler.invoke(SyncMethodHandler.java:119) at com.sun.xml.ws.client.sei.SyncMethodHandler.invoke(SyncMethodHandler.java:102) at com.sun.xml.ws.client.sei.SEIStub.invoke(SEIStub.java:154) at $Proxy38.wsRdyCrearTicketDA(Unknown Source) at ar.com.fit.fides.remedy.api.ws.ServicioCreacionTickets.crearTicket(ServicioCreacionTickets.java:55) at ar.com.fit.fides.remedy.api.ws.ConectorRemedyWS.crearTicket(ConectorRemedyWS.java:43) at ar.com.fit.fides.remedy.api.ws.ConectorRemedyWS.main(ConectorRemedyWS.java:90) Caused by: java.lang.RuntimeException: com.sun.xml.messaging.saaj.SOAPExceptionImpl: Can't add a header when one is already present. at ar.com.fit.fides.remedy.api.ws.AuthenticationHandler.handleMessage(AuthenticationHandler.java:50) at ar.com.fit.fides.remedy.api.ws.AuthenticationHandler.handleMessage(AuthenticationHandler.java:23) at com.sun.xml.ws.handler.HandlerProcessor.callHandleMessageReverse(HandlerProcessor.java:341) at com.sun.xml.ws.handler.HandlerProcessor.callHandlersResponse(HandlerProcessor.java:214) at com.sun.xml.ws.handler.ClientSOAPHandlerTube.callHandlersOnResponse(ClientSOAPHandlerTube.java:161) ... 14 more Caused by: com.sun.xml.messaging.saaj.SOAPExceptionImpl: Can't add a header when one is already present. at com.sun.xml.messaging.saaj.soap.impl.EnvelopeImpl.addHeader(EnvelopeImpl.java:128) at com.sun.xml.messaging.saaj.soap.impl.EnvelopeImpl.addHeader(EnvelopeImpl.java:108) at ar.com.fit.fides.remedy.api.ws.AuthenticationHandler.handleMessage(AuthenticationHandler.java:45)
So, the solution is to check whether the message being handled if the outbound message, like this:
public boolean handleMessage(SOAPMessageContext context) { try { Boolean outbound = (Boolean) context.get("javax.xml.ws.handler.message.outbound"); if (outbound != null && outbound) { // obtaining marshaller which should marshal instance to xml final Marshaller marshaller = JAXBContext.newInstance(AuthenticationInfo.class).createMarshaller(); // adding header because otherwise it's null final SOAPHeader soapHeader = context.getMessage().getSOAPPart().getEnvelope().addHeader(); // marshalling instance (appending) to SOAP header's xml node marshaller.marshal(info, soapHeader); } } catch (final Exception e) { throw new RuntimeException(e); } return true; }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With