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.java
  • GoogleSearchBindingStub.java
  • GoogleSearchPort.java
  • GoogleSearchResult.java
  • GoogleSearchResult_Helper.java
  • GoogleSearchService.java
  • GoogleSearchServiceLocator.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.

Comments
 
Hi, Tutorial is very good. very simple to understand. I am facing a problem when executing the client code. I followed the tutorial and created XML, mapping and client files. when i compile the client, i was getting 'MySearchService' (similar to GoogleSearchService interface) not found. I added the MySearchService.class file to the classpath of client. Now, when i execute the client class, i am getting 'Service not found - MyService(similar to GoogleService). Do i need to do anything more. Can tell me where i am going wrong. Do i need to add something into Web.xml of Webservice application. Thanks in advance.
I can't be sure exactly what the problem is.

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.

Thanks Mark, I got my problem solved. It was becoz of wrong targetNamespace. I just copy/pasted the value of targetNamespace from generated WSDL to the applicationContext.xml. It works now. Thanks for the help. By the way, I need another help. I have to pass Username,Password to my Webservice. I have users.lst file under WEB-INF folder which has some users and their passord. Now, i am getting Axis fault - 'faultString: (401)Unauthorized xxx' -- 'faultStringUser 'null' not authenticated (unknown user)'. Can you help me in sending this parameters while calling Axis Webservice from the Spring Client? Thanks in Advance. Senthil.
Lucky guess! JaxRpcPortProxyFactoryBean accepts username and password parameters. So if you're lucky it might just be a case of specifying username/password in the configuration. e.g. in the above example it would look something like this:


...

<bean id="googleService" class="fullgoogleapi.GooglePortProxyFactoryBean">



        <property name="username">

            <value>foo</value>

        </property>



        <property name="password">

            <value>bar</value>

        </property>



</bean>

...

Hello mark ! What about if I have to set the username and password at the first page of my application ?? What should I do ???
Hi Rodrigo, I am not sure what you mean. Could you please explain what you want to do. Thanks, Mark
Hi Mark, I followed your example for setting web service invocation using the RPC approach given in your example. Do, I need to configure anything other than setting the username and password in the applicationContext.xml for accessing a webservice which requires username/password authentication ? Thank you in advance. -Jilani
Hi Jilani,

Setting username/password in the applicationContext.xml should work if your web service is using basic authentication.

Mark
Hi Mark, I am sure it is using basic authentication because when I try to access the webservice through a browser it prompts me for a username/password. And it does not look for any certificate. Will it make any difference if the webservice on the whole is password protected instead of just the endpoint ? And do I have to modify the stubs as suggested by Alan here http://forum.springframework.org/showthread.php?t=38843 BTW, i tried this approach but does not seem to help. Am I missing any jars ? Thanks, -Jilani
Hi Jilani,

If 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.

Hi Mark, I had already tried that approach of specifying the username and password in the url but it did not seem to solve the problem and I was still getting the 401 error. I am suprised that such a basic thing (username/password authentication) in web service invocation is so complex to configure in Spring. I don't know what else to do as the web service is not under my control (that I could modify to just secure the endpoint instead of the entire url). I looked at so many different forums online but could not see one example which gives a working version for this issue. May be I should see if there are other options using Spring for accomplishing this task. Thanks, -Jilani
Hi Mark, The webservice I was trying to invoke was using windows digest authentication (so you would be prompted for a username and password even to read the WSDL) and my code was failing at the part where Axis tries to parse the WSDL itself. So, I think Spring tries to parse the wsdl before it even sets the username and password for the actual invocation. So, in order to solve this problem, I just downloaded the wsdl locally and put it in the classpath so that Spring could read it from there instead of going through authentication at the remote. Also overriding the afterPropertiesSet() method would also help in setting the username/password dynamically for the web service invocation instead of setting in the config file. public void afterPropertiesSet() { this.setUsername("username"); this.setPassword("password"); super.afterPropertiesSet(); } Thanks once again for your help ! -Jilani
Yes, that works nicely for RPC style web services. I can execute both Google search service and my own Spring-Axis test service. However if I change the service style/use to document/literal for my Spring-Axis service, I start getting a "could not find deserializer for type ..." error on the client side. Any idea why is that happening or where would be a good place to ask this question?

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 have to admit your question scared me! People have told me that my tutorials are simple and easy to understand, the reason for that is I am fairly simple!

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.

Good Article, Thanks Mark. I am just wonder ing if the bean created by SpringFrame is thread safe ? I know the sub create by Axis is not .

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.

HTH
Mark
Aha!

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.

Your are article is really good. i have small problem with my web service. It is an RPC/encoded web service and deployed in https. And i save the wsdl file into local file and configured the spring by extending the JaxRpcPortProxyFactoryBean. But i am exception "PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target". I appreciate your help on this.
I think this is a SSL certificate trust issue. The certificate of the HTTPS web service that you are accessing is not signed by one of the certificate authorities (CAs) that Java trusts by default (such as VeriSign and Thawte). It could be that the certicificate on the server is self signed.

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

Mark
Thanks a lot. It helped me a lot. I have one more question. We have requirement of keeping an customizable "TimeOut" option for the service. I know that, we can do this using Stub class/Call using setTimeout option. So how can i accomplish this using the "JaxRpcPortProxyFactoryBean". I appreciate your help on this.
I'm not exactly sure if this is the best way to do it but the following code extends JaxRpcPortProxyFactoryBean so that you can set a timeout.

AxisClientFactoryBean

Also this looked relevant:

Eric Hauser's Weblog: Axis and Spring

Thanks Mark for the information. And i have one more last question :) how can we set the Endpoint address dynamically?

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.

I am using http host and proxy to connect to internet. When i run the MainApplication it gives me ConnectionTimedOut exception How can I fix this problem
I am consuming web service through spring 1.2.5+axis 1.2.1. The speciality of that particular web service is that at the time of wrong/invalid input the request is coming back as response. So when I am passing valid inputs my code is running fine but when I am passing wrong/invalid input I am getting org.xml.sax.SAXException Invalid Element-<Response Object>.I have tried same web service with weblogic 7 client & it's working fine for valid/invalid inputs. So what may be the problem in Spring+Axis when request is coming back as response?
just thanks for this article!
When I call the web service i get an error could not find deserializer for type. I have used Eclipse for the client. The web service is implemented using Spring. The service endpoints classes for all the requests are extended from AbstractMarshallingPayloadEndpoint. The part of applicationContext-container.xml under src/main/resources dir is as

<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 would love to help but this looks a little beyond me and I have not yet used Spring-WS. I have only used XFire for producing services, (as CXF is not quite able to do what I want yet). Did you try this without deleting any of the Axis generated classes as suggested by Martin Zdila below?
yes Marc. i tried deleting those files and without deleting them.. but always get this error.

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");

i had tried that too. but didnt help..
Mark, I looked at several other examples and found that if xalan jar is added to sprin-ws , the Axis client would work it seems.. I tried adding xalan 2.7.0 jar and after then due to several errors, i kept adding the required jars to pom.xml I tested the Spring WS using soapUI and getting a error like this (the request is received by WS).... DEBUG transport.http.MessageDispatcherServlet - Could not complete request java.lang.NoClassDefFoundError: com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl Do you have any idea what should be done to this?
Hi, With Axis client and Spring-ws i could not resolve the issues that i had. Finally i used IBM web service to generate my client code and it works fine now.. I have a different question now... on the spring-ws-servlet.xml i have the locationUri as

<!-- 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.

you can encounter a problem with mappings if you delete helper classes and YourServiceBindingStub. You should rather leave helpergen="false" and not delete YourServiceBindingStub class. i've almost converted to XFire because of this problem :-)
Martin is absolutely right; If you set helpergen=true and rely on deleting the Helper and SoapBindingStub classes you run the risk of getting the, "Could not find deserialiser for type . . ." error. Just set helpergen=false, and leave all the generated files intact(though deleting the ServiceLocator clas doesnt seem to unsettle things); just make sure you have the SoapBindingStub in there. Top class tutorial by the way ! ! !