I'm looking for a solution to integrate CXF into an application as a provider for Web services implementation. The application should be able to implement a web-service in a dynamic manner (this means that SEI classes are not available) based on the provided WSDL file. Since the application manages http requests and url mappings via its own servlet, its not feasible to use standard CXF servlet to publish endpoints. Also, I want to use JAXB databinding. Ideally CXF should call my Object invoke(String oper, Object... args)
to do real processing of web service. Overall it should look like dynamic client but implemented for the server part.
I've managed to get the code almost working, but bumped into a couple of things that I don't understand - about this later.
First I read WSDL into a String and create its definition. Definition is precached in wsdlManager to be accessible via unique reference. Than I create JaxWS dynamic client and grab JAXB databinding that was generated for it by CXF.
WSDLManager wsdlManager = serviceBus.getExtension(WSDLManager.class);
WSDLFactory wsdlFactory = wsdlManager.getWSDLFactory();
// Reader
WSDLReader reader = wsdlFactory.newWSDLReader();
reader.setFeature("javax.wsdl.verbose", true);
reader.setFeature("javax.wsdl.importDocuments", true);
Definition def = reader.readWSDL(null, TypeCast.getInputSource(wsdl)); // wsdl is a String containing wsdl definition
// Precache definition using listenerRef as unique identifier
wsdlManager.addDefinition(listenerRef, def);
String[] compileOptions = null;
// Create JAXWS dynamic client using precached address
Client client = createClient(listenerRef, simpleDataBinding, allowElementReferences, compileOptions);
if (Logger.isDebugEnabled())
Logger.debug("WebServiceProcessor.initServiceListener: service client is created succefully: " + listenerName);
EndpointInfo cei = client.getEndpoint().getEndpointInfo();
// Use JAXB generated databinding
DataBinding db = client.getEndpoint().getService().getDataBinding();
if (Logger.isDebugEnabled())
Logger.debug("WebServiceProcessor.initServiceListener: databinding: " + db);
The creation of dynamic client is straightforward
public Client createClient(String serviceDescription, boolean simpleDataBinding, boolean allowElementReferences, String[] schemaOptions) {
ClassLoader old = Thread.currentThread().getContextClassLoader();
try {
ParentClassLoader dynaLoader = new ParentClassLoader();
JaxWsDynamicClientFactory dynamicClientFactory = JaxWsDynamicClientFactory.newInstance(serviceBus);
dynamicClientFactory.setSimpleBindingEnabled(simpleDataBinding);
dynamicClientFactory.setAllowElementReferences(allowElementReferences);
if (schemaOptions != null) {
dynamicClientFactory.setSchemaCompilerOptions(schemaOptions);
}
return dynamicClientFactory.createClient(serviceDescription, dynaLoader);
} catch (Throwable ex) {
Logger.error("WebServiceProcessor.createClient: exception is caught: " + ex, ex);
return null;
} finally {
Thread.currentThread().setContextClassLoader(old);
}
}
Next, to create the server stuff some helper classes are declared
protected class MyWSDLServiceFactory extends WSDLServiceFactory {
public MyWSDLServiceFactory(Bus b, Definition d) {
super(b, d);
}
@Override
public Service create() {
Service svc = super.create();
// Post init
initializeDefaultInterceptors();
initializeDataBindings();
return svc;
}
}
public class MyInvoker extends AbstractInvoker {
protected final Object implementor = new Object();
public MyInvoker() {
}
@Override
public Object getServiceObject(Exchange context) {
return implementor;
}
protected void throwable() throws Exception {
}
@Override
public Object invoke(Exchange exchange, Object o) {
List<Object> params = null;
if (o instanceof List) {
params = CastUtils.cast((List<?>)o);
} else if (o != null) {
params = new MessageContentsList(o);
}
if (Logger.isTraceEnabled()) {
for (Object arg : params)
Logger.trace("MyInvoker.invoke: arg: " + arg);
}
// Method holding declararions of throwable exceptions
Method m = null;
try {
m = MsyInvoker.class.getMethod("throwable");
} catch (NoSuchMethodException ex) {
// Strange
}
return invoke(exchange, null, m, params);
}
@Override
protected Object performInvocation(Exchange exchange, Object serviceObject, Method m, Object[] paramArray) throws Exception {
Message inMessage = exchange.getInMessage();
BindingOperationInfo bop = exchange.getBindingOperationInfo();
String oper = bop.getName().getLocalPart();
// Process request
return processWebListenerRequest(oper, paramArray);
}
}
protected class MyDestinationFactory implements DestinationFactory {
protected final Set<String> prefixes = Collections.unmodifiableSet(new HashSet<String> (Arrays.asList("http://", "https://")));
@Override
public Destination getDestination(EndpointInfo ei, Bus bus) throws IOException {
return new MyDestination(ei, ei.getAddress());
}
@Override
public Set<String> getUriPrefixes() {
return prefixes;
}
@Override
public List<String> getTransportIds() {
return null;
}
}
protected class MyDestination extends ServletDestination {
public MyDestination(EndpointInfo ei, String path) throws IOException {
super(serviceBus, null, ei, path, false);
// Disable async support
isServlet3 = false;
}
@Override
protected void setupMessage(final Message inMessage, final ServletConfig config, final ServletContext context, final HttpServletRequest req, final HttpServletResponse resp) throws IOException {
super.setupMessage(inMessage, config, context, req, resp);
}
@Override
protected String getBasePath(String contextPath) throws IOException {
if (endpointInfo.getAddress() == null) {
return "";
}
return endpointInfo.getAddress();
}
}
Then we are ready to create the server:
MyWSDLServiceFactory sf = new MyWSDLServiceFactory(serviceBus, def);
sf.setAllowElementRefs(allowElementReferences);
sf.setDataBinding(db);
Service svc = sf.create();
// Clear cached definition
wsdlManager.removeDefinition(def);
svc.setInvoker(new MyInvoker());
// Create endpoints
for (ServiceInfo inf : svc.getServiceInfos()) {
for (EndpointInfo ei : inf.getEndpoints()) {
if (ei.getName().equals(cei.getName())) {
if (Logger.isDebugEnabled())
Logger.debug("WebServiceProcessor.initServiceListener: endpoint: " + ei.getName());
String addr = "/" + listenerRef;
try {
ei.setAddress(addr);
JaxWsEndpointImpl ep = new JaxWsEndpointImpl(serviceBus, svc, ei);
svc.getEndpoints().put(ei.getName(), ep);
ep.addHandlerInterceptors();
ep.getInInterceptors().add(new SoapUtil.SoapInLogger());
BindingFactoryManager bfm = serviceBus.getExtension(BindingFactoryManager.class);
// tried this but no effect
// ei.getBinding().setProperty("soap.force.doclit.bare", Boolean.TRUE);
String bindingId = ei.getBinding().getBindingId();
if (Logger.isDebugEnabled())
Logger.debug("WebServiceProcessor.initServiceListener: binding id: " + bindingId);
BindingFactory bindingFactory = bfm.getBindingFactory(bindingId);
Server server = new ServerImpl(serviceBus, ep, new MyDestinationFactory(), bindingFactory);
if (Logger.isDebugEnabled())
Logger.debug("WebServiceProcessor.initServiceListener: starting server: " + ei.getName());
server.start();
if (Logger.isDebugEnabled())
Logger.debug("WebServiceProcessor.initServiceListener: server is started: " + server.isStarted());
// Set reference
listeners.put(listenerRef, server); // Our map to keep web server listeners
} catch (EndpointException e) {
throw new ServiceConstructionException(e);
}
}
}
}
The server invocation is looks like
String address = "/" + listenerRef;
Server server = listeners.get(listenerRef); // Find our server listener in a map
if (server != null) {
Endpoint ep = server.getEndpoint();
EndpointInfo ei = ep.getEndpointInfo();
if (Logger.isDebugEnabled())
Logger.debug("WebServiceProcessor.invoke: endpoint: " + listenerName);
try {
AbstractHTTPDestination dest = (AbstractHTTPDestination) server.getDestination();
AsyncContext asyncCtx = requestContext.getAsyncContext();
HttpServletRequest req = (HttpServletRequest) asyncCtx.getRequest();
HttpServletResponse resp = (HttpServletResponse) asyncCtx.getResponse();
ServletContext sctx = req.getServletContext();
ServletConfig scfg = null;
if (Logger.isDebugEnabled())
Logger.debug("WebServiceProcessor.invoke: destination resolved successfully: " + listenerName);
// Trigger CXF processing
dest.invoke(scfg, sctx, req, resp);
if (Logger.isDebugEnabled())
Logger.debug("WebServiceProcessor.invoke: endpoint processed successfully: " + listenerName);
} catch (Exception ex) {
Logger.error("WebServiceProcessor.invoke: exception is caught: " + ex, ex);
}
}
As I already mentioned, the solution almost works, I tried to test it with CXF 3.3 and one WSDL I took as example http://www.dneonline.com/calculator.asmx?WSDL. I managed to call the service with SoapUI and get the response. But now is the strange part. When I call the webservice with standard request
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">
<soap:Header/>
<soap:Body>
<Add xmlns="http://tempuri.org/">
<intA>1</intA><intB>2</intB>
</Add>
</soap:Body>
</soap:Envelope>
It fails with Unmarshalling Error
org.apache.cxf.interceptor.Fault: Unmarshalling Error: unexpected element (uri:"http://tempuri.org/", local:"intA"). Expected elements are <{http://tempuri.org/}Add>,<{http://tempuri.org/}AddResponse>,<{http://tempuri.org/}Divide>,<{http://tempuri.org/}DivideResponse>,<{http://tempuri.org/}Multiply>,<{http://tempuri.org/}MultiplyResponse>,<{http://tempuri.org/}Subtract>,<{http://tempuri.org/}SubtractResponse>
at org.apache.cxf.jaxb.JAXBEncoderDecoder.unmarshall(JAXBEncoderDecoder.java:932) ~[cxf-rt-databinding-jaxb-3.3.6.jar:3.3.6]
at org.apache.cxf.jaxb.JAXBEncoderDecoder.unmarshall(JAXBEncoderDecoder.java:738) ~[cxf-rt-databinding-jaxb-3.3.6.jar:3.3.6]
at org.apache.cxf.jaxb.io.DataReaderImpl.read(DataReaderImpl.java:170) ~[cxf-rt-databinding-jaxb-3.3.6.jar:3.3.6]
at org.apache.cxf.wsdl.interceptors.DocLiteralInInterceptor.getPara(DocLiteralInInterceptor.java:325) ~[cxf-rt-wsdl-3.3.6.jar:3.3.6]
at org.apache.cxf.wsdl.interceptors.DocLiteralInInterceptor.handleMessage(DocLiteralInInterceptor.java:127) ~[cxf-rt-wsdl-3.3.6.jar:3.3.6]
But it successfully passes validation with
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">
<soap:Header/>
<soap:Body>
<Add xmlns="http://tempuri.org/">
<Add>
<intA>1</intA><intB>2</intB>
</Add>
</Add>
</soap:Body>
</soap:Envelope>
But in this case the parameters that are passed to MyInvoker are an array of two null elements. Nevertheless it generates the correctly formatted (except that the calculated value is wrong because input parameters are null) response
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">
<soap:Body>
<ns1:AddResponse xmlns:ns1="http://tempuri.org/">
<AddResult xmlns="http://tempuri.org/">0</AddResult>
</ns1:AddResponse>
</soap:Body>
</soap:Envelope>
So the questions are - what could go wrong with unmarshalling of the following valid request?
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">
<soap:Header/>
<soap:Body>
<Add xmlns="http://tempuri.org/">
<intA>1</intA><intB>2</intB>
</Add>
</soap:Body>
</soap:Envelope>
I have checked the CXF dynamic client invocation with the tested WSDL, from which I borrowed JAXB databinding, and it generates the exactly the same request when calling this service, but seems not to be able to unmarshall it for some reason.
Another question, I think it relates to the first one, is why the unmarshalled parameters in are nulls in case of second request? Any suggestion to where to look next?
Thanks in advance
Here is the answer to my own question. Things get right if reusing the CXF service instance from dynamic client with addition of some server part interceptors:
Service svc = client.getEndpoint().getService();
// Server part interceptors
svc.getInInterceptors().add(new ServiceInvokerInterceptor());
svc.getInInterceptors().add(new OutgoingChainInterceptor());
svc.getInInterceptors().add(new OneWayProcessorInterceptor());