I should point out that what I am about to show is not necessarily the quickest way of getting the Google Web API doGoogleSearch up and running. In this particular case Google already provides a Google Web API Developer's Kit which you could use to do all of the functionality I am about to describe, indeed some of the precompiled Google Web API package will be interchangeable with what I am about to generate using the Axis WSDL2Java tool but I hope this example will give you some ideas that can be applied to web services in general and not just the Google Web API.
In my last blog entry, I showed how you can access doSpellingSuggestion with Spring and since this service only returns a String there was no particular complexity involved. doGoogleSearch returns a more complex response and so I am going to use Axis' WSDL2Java tool to generate the beans that I will need to capture this more complex response. Remember that WSDL2Java is intended to produce Axis specific code and since I'm using Spring to do the web service access I don't actually need all the generated Axis code. I downloaded Axis 1.3 and created an Ant script containing the relevant Axis WSDL2Java Ant task.
<?xml version="1.0" encoding="UTF-8"?> <project name="googleApi" default="wsdl2java" basedir="."> <path id="axis.classpath"> <fileset dir="./lib"> <include name="**/*.jar" /> </fileset> </path> <taskdef resource="axis-tasks.properties" classpathref="axis.classpath" /> <target name="wsdl2java"> <mkdir dir="src"/> <axis-wsdl2java output="src" testcase="false" helpergen="true" verbose="true" serverside="false" url="http://api.google.com/GoogleSearch.wsdl" > <mapping namespace="urn:GoogleSearch" package="com.google" /> </axis-wsdl2java> </target> </project>
The important thing to notice about this is that I am using the helpergen="true" option by doing this I am asking WSDL2Java to store any Axis specific code away from the valuable beans that I actually want. After running this script I have a collection of generated classes, some of which I don't actually need. The following list shows the classes that are produced by running WSDL2Java against the Google WSDL with the classes I don't need indicated via a strikethrough.
- DirectoryCategory.java
DirectoryCategory_Helper.javaGoogleSearchBindingStub.java- GoogleSearchPort.java
- GoogleSearchResult.java
GoogleSearchResult_Helper.javaGoogleSearchService.javaGoogleSearchServiceLocator.java- ResultElement.java
ResultElement_Helper.java
After deleting all the helper classes, BindingStub and Locator I have removed all the Axis specific code. I am left with the beans I need to capture my responses to doGoogleSearch and also a convenient port interface class.
In this case we are expecting the web service to return something more complicated than a String so we need to extend JaxRpcPortProxyFactoryBean in order to map the service response given to our generated beans. Modifying the example given in the Chapter 16. Remoting and web services using Spring I get:
package fullgoogleapi; import com.google.DirectoryCategory; import com.google.GoogleSearchResult; import com.google.ResultElement; import javax.xml.namespace.QName; import javax.xml.rpc.Service; import javax.xml.rpc.encoding.TypeMapping; import javax.xml.rpc.encoding.TypeMappingRegistry; import org.apache.axis.encoding.ser.BeanDeserializerFactory; import org.apache.axis.encoding.ser.BeanSerializerFactory; import org.springframework.remoting.jaxrpc.JaxRpcPortProxyFactoryBean; public class GooglePortProxyFactoryBean extends JaxRpcPortProxyFactoryBean { protected void postProcessJaxRpcService(Service service) { TypeMappingRegistry registry = service.getTypeMappingRegistry(); TypeMapping mapping = registry.createTypeMapping(); registerBeanMapping(mapping, GoogleSearchResult.class, "GoogleSearchResult"); registerBeanMapping(mapping, ResultElement.class, "ResultElement"); registerBeanMapping(mapping, DirectoryCategory.class, "DirectoryCategory"); registry.register("http://schemas.xmlsoap.org/soap/encoding/", mapping); } protected void registerBeanMapping(TypeMapping mapping, Class type, String name) { QName qName = new QName("urn:GoogleSearch", name); mapping.register(type, qName, new BeanSerializerFactory(type, qName), new BeanDeserializerFactory(type, qName)); } }
Now to add a service interface. This is essentially a slightly modified version of the generated class GoogleSearchPort.java but in the service interface we no longer throw RMI exceptions. Contrast this to the previous doSpellingSuggest example as I will define all the methods properly and not just to satisfy the endpoint matching so that I can now use all of the methods if I want to.
package fullgoogleapi; public interface GoogleSearchService { public byte[] doGetCachedPage(String key, String url); public String doSpellingSuggestion(String key, String phrase); public com.google.GoogleSearchResult doGoogleSearch(String key, String q, int start, int maxResults, boolean filter, String restrict, boolean safeSearch, String lr, String ie, String oe); }
My new applicationContext.xml file incorporating the extended JaxRpcPortProxyFactoryBean, generated port interface class and the service interface above will look something like this:
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd"> <beans> <bean id="googleService" class="fullgoogleapi.GooglePortProxyFactoryBean"> <property name="serviceFactoryClass"> <value>org.apache.axis.client.ServiceFactory</value> </property> <property name="wsdlDocumentUrl"> <value>http://api.google.com/GoogleSearch.wsdl</value> </property> <property name="namespaceUri"> <value>urn:GoogleSearch</value> </property> <property name="serviceName"> <value>GoogleSearchService</value> </property> <property name="portName"> <value>GoogleSearchPort</value> </property> <property name="portInterface"> <value>com.google.GoogleSearchPort</value> </property> <property name="serviceInterface"> <value>fullgoogleapi.GoogleSearchService</value> </property> </bean> </beans>
The test class will now look something like this:
package fullgoogleapi; import com.google.GoogleSearchResult; import org.apache.commons.collections.BeanMap; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class MainApplication { private static ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml"); private static GoogleSearchService service = (GoogleSearchService) context.getBean("googleService"); public static void main(String [] args) throws Exception { String key="<Insert Google Web API key here>"; String test = "webservie"; System.out.println(service.doSpellingSuggestion(key,test)); String q ="shrdlu winograd maclisp teletype"; int start = 0; int maxResults = 10; boolean filter = true; String restrict = ""; boolean safeSearch = true; String lr = ""; String ie = "latin1"; String oe = "latin1"; GoogleSearchResult gsr = service.doGoogleSearch(key, q, start, maxResults, filter, restrict, safeSearch, lr, ie, oe); BeanMap googleResults = new BeanMap(gsr); System.out.println(googleResults.toString()); } }
If you have used Axis before I hope you'll agree that this is considerably simpler than wiring together all the generated BindingStubs and Locators. In this example I've reduced the role of WSDL2Java from an Axis code generator to a simple WSDL aware bean generator. The minute you run into problems you will probably have to go away and learn how to use Axis's TCP Monitor etc. Using this method, if you are lucky, you may get away with not even having to look through a WSDL or even browse a SOAP message. It would be nice if you knew all the intricacies of SOAP, Axis and web services but with this method I think it becomes slightly less important than it was before. I would expect you to be more productive at consuming web services using the Spring enabled web services approach than the method that Axis previously offered.
If it seems to be having problems locating MyService then you need to double check the details of the service WSDL file. Maybe a namespace hasn't been specified correctly, check what targetNamespace is currently set to in your WSDL file and make sure your client "qName" matches (in the above case it was "urn:GoogleSearch").
You shouldn't need to specify anything in your web.xml. For testing purposes you should be able to run this on the command line outside of any web application. If it's any help I used Axis 1.3 and Spring 1.2.6 for the above example.
...
<bean id="googleService" class="fullgoogleapi.GooglePortProxyFactoryBean">
<property name="username">
<value>foo</value>
</property>
<property name="password">
<value>bar</value>
</property>
</bean>
...
Setting username/password in the applicationContext.xml should work if your web service is using basic authentication.
MarkIf the WSDL itself is protected, the following thread suggests an ugly fix (and a possible cause - this was written back in 2004 so it might no longer be true):
http://forum.springframework.org/showthread.php?t=12006
The solution given was to pass the username/password as part of the WSDL URL - not ideal.
http://username:password@server:8080/axis/MySecureService?wsdl
The thread you mentioned was talking more about Spring-WS rather than the Spring Framework. Spring-WS is not part of the main Spring Framework code. Spring-WS is useful for creating document driven web services (the wsdl first kind!).
You can create web service clients using only the main Spring Framework code.
Btw I noticed that my portType uses the parameterOrder option which is intended for RPC style services, and the top-level element in the SOAP response body is of the form <parametername>Return. why is that?
I had to go away and look up the difference between RPC/Encoded and Document/Literal. Fortunately I found a quite simple article explaining it.
Selecting the Best Approach for Designing an Interoperable Web Service
From the article above and various solutions I found on the net I suspect the problem can be fixed by modifying the bean mapping code.
If the Google service were to start using Document/Literal style then GooglePortProxyFactoryBean would need to be modified thus
registry.register("http://schemas.xmlsoap.org/soap/encoding/", mapping);
to
registry.register("", mapping);
I'm guessing that would fix it.
If I understand the article above correctly then the http://schemas.xmlsoap.org/soap/encoding/ part of the mapping basically implies that the web service types are represented via the use style "encoded". If your web service is of the use style "literal" then registering the encoded mapping for this response makes no sense.
See also:
http://forum.springframework.org/showthread.php?t=16827
http://forum.springframework.org/archive/index.php/t-14225.html
As for your second question regarding parameterOrder and parametername, I'm not really sure what you are asking. WSDL and SOAP responses are two completely separate XML formats. portType elements and parameterOrder attributes are usually associated with WSDL documents, whereas parametername elements are used in SOAP documents.
I'm not absolutely sure and I haven't tried this. The API docs for JaxRpcPortClientInterceptor mention using something called the dynamic call mechanism to ensure thread safety. As you say, the documentation states that Apache Axis port stubs are known to be non-thread-safe. AFAICT the dynamic call mechanism can be implemented on beans that extend JaxRpcPortProxyFactoryBean (such as GooglePortProxyFactoryBean above). If I read it right, all you would need to do is implement the alwaysUseJaxRpcCall() method so that it returns true.
HTHMark
I've just noticed it is only the JaxRpcPortClientInterceptor docs for Spring 2.0.x that specifically mention thread safety in terms of alwaysUseJaxRpcCall(). Although I'm not sure if that equates to only Spring 2.0.x implementations being thread safe.
What you should do is import the root certificate from the CA that signed the web service's SSL certificate into your Java keystore. Doing this will allow Java connections to trust this connection.
To do this, I first go to the HTTPS URL of the service and use MS Internet Explorer to obtain the root certifcate. See Export a root certificate from Microsoft Internet Explorer
Then import your certificate into your keystore. There is a Java command line tool that you can use to do this called keytool. Keytool tends to be a bit painful to use, so you might want to use something more user friendly such as portecle.
HTH
MarkAlso this looked relevant:
How can we set the Endpoint address dynamically? I am not exactly sure what you mean. The service endpoint is set inside the WSDL. I cobbled together a SimpleWSDLAnalyzer using Axis 1.3 (and some JBoss code I found) to illustrate this. This program uses the Axis WSDL parsing code to obtain information from a WSDL file. For the Google SOAP Search API this program outputs:
WSDL URL: http://api.google.com/GoogleSearch.wsdl
Namespace URI: urn:GoogleSearch
Service Name: GoogleSearchService
Port Name: GoogleSearchPort
Endpoint URL: http://api.google.com/search/beta2
Operation: doGetCachedPage
Operation: doSpellingSuggestion
Operation: doGoogleSearch
As you see, the endpoint for Google SOAP Search API at the time of writing this is http://api.google.com/search/beta2.
If Google moves the endpoint then, AFAICT, as long as this change is reflected inside the WSDL document then the above technique will continue to work. AFAIK, the endpoint location is not hard coded anywhere in the Axis generated model objects nor is it explicitly contained inside the Spring code.
You can of course change the WSDL address dynamically. e.g.
service.setWsdlDocumentUrl("some new url")
I'm not sure why you would need to do this. Your question got me thinking about more generic means of consuming web services, this lead me to soapui which is very interesting. The problem with the ultra generic approach is that it is not very useful beyond testing very simple web services.
<bean id="messageDispatcher" class="org.springframework.ws.soap.server.SoapMessageDispatcher"> <property name="endpointMappings"> <list> <ref local="payloadMapping" /> </list> </property> </bean>
<bean id="payloadMapping" class="org.springframework.ws.server.endpoint.mapping.PayloadRootQNameEndpointMapping"> <property name="mappings"> <props> <prop key="{urn:ClaimData/1.1}findDataSummaryListBySearchCriteriaRequest">findDataSummaryList</prop> </props> </property> </bean>
<bean id="findDataSummaryList" class="com.yyy.data.service.endpoints.FindDataSummaryListEndpoint"> <property name="marshaller" ref="dataSummaryMarshaller" /> <property name="unmarshaller" ref="dataSummaryMarshaller" /> <property name="dataFacade" ref="dataFacade" /> </bean><bean id="dataSummaryMarshaller" parent="jaxbMarshaller"> <property name="contextPath" value="com.yyy.data.service.dto" /> </bean>
The part of spring-ws-servlet.xml under /WEB-INF dir is as<bean id="handlerMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping"> <property name="defaultHandler" ref="messageDispatcher" /> <property name="mappings"> <props> <prop key="wsdl/ClaimData.wsdl">ClaimData</prop> </props> </property> </bean>
<!-- WSDL Definitions --> <bean id="ClaimData" class="org.springframework.ws.wsdl.wsdl11.DynamicWsdl11Definition"> <property name="builder"> <bean class="org.springframework.ws.wsdl.wsdl11.builder.XsdBasedSoap11Wsdl4jDefinitionBuilder"> <property name="schema" value="/wsdl/ClaimData.xsd" /> <property name="portTypeName" value="ClaimData" /> <property name="schemaPrefix" value="tns" /> <property name="locationUri" value="/data-ws/services/" /> </bean> </property> </bean>
web.xml has<servlet> <servlet-name>spring-ws</servlet-name> <servlet-class>org.springframework.ws.transport.http.MessageDispatcherServlet</servlet-class> <load-on-startup>-1</load-on-startup> </servlet>
<servlet-mapping> <servlet-name>spring-ws</servlet-name> <url-pattern>/services/*</url-pattern> </servlet-mapping>
<servlet-mapping> <servlet-name>spring-ws</servlet-name> <url-pattern>*.wsdl</url-pattern> </servlet-mapping>
On the client side, I used Axis1.1 WSDL2JAVA to create java classes. I followed the example and created the manual interface, proxy classes etc. on the postProcessJaxRpcService method , i have set the proper request/ response class
registerBeanMapping(mapping, FindDataSummaryListBySearchCriteriaResponse.class, "FindDataSummaryListBySearchCriteriaResponse"); registry.register("", mapping);
I used SoapUI to test the web service and it's working fine.. But when i run the Axis client from my Eclipse i get the error as ERROR org.apache.axis.client.Call - Exception: org.xml.sax.SAXException: Deserializing parameter 'findDataSummaryListBySearchCriteriaResponse': could not find deserializer
I checked several other example for Web Service with SPring/ Axis and i found that my project web service implementation do not have interface (as we have in client created by Axis) or any deployment file that tells the mapping with serializing/ deserializing class.. pls help me how to fix this issue.
I am still not sure but could it be case that is the source of the problem? Have you tried?:
registerBeanMapping(mapping, FindDataSummaryListBySearchCriteriaResponse.class, "findDataSummaryListBySearchCriteriaResponse");
instead of:
registerBeanMapping(mapping, FindDataSummaryListBySearchCriteriaResponse.class, "FindDataSummaryListBySearchCriteriaResponse");
<!-- WSDL Definitions --> <bean id="ClaimData" class="org.springframework.ws.wsdl.wsdl11.DynamicWsdl11Definition"> <property name="builder"> <bean class="org.springframework.ws.wsdl.wsdl11.builder.XsdBasedSoap11Wsdl4jDefinitionBuilder"> <property name="schema" value="/wsdl/ClaimData.xsd" /> <property name="portTypeName" value="ClaimData" /> <property name="schemaPrefix" value="tns" /> <property name="locationUri" value="/data-ws/services/" /> </bean> </property> </bean>
and when the wsdl file gets generated, the soap location is displayed as "/data-ws/services". I have to manually chg it to different server as "http://devserver:8080/data-ws/services/", "http://uaServer:8080/data-ws/services/" . Can anyone tell me how to change this remote url for each environment without this manual update?I recently answered a very similar question elsewhere on my blog. The short answer is NO! The longer answer is that you can get hold of the ServletContext through Spring's web application context but Server name, server port and context path are properties of the HTTP servlet request. e.g. I can access my blog using:
http://cse-mjmcl.cse.bris.ac.uk/blog/ and http://localhost/blog/ and the given values for server and port could change! These values are request specific.
That said, if you would rather put the server URL in a properties file your Spring application context could acquire this value using a PropertyPlaceholderConfigurer.





