Web Services in Ruby, Python and Java

Today I’ve had to prepare some examples to show that web services are interoperable. So I’ve created a simple web service in Java using Metro and launched it on Tomcat. Then tried to consume them using Python and Ruby. Here’s how it all finished…

Web service in Java
I’ve started with simple web service in Java:

package com.wordpress.jdevel.ws;

import java.io.Serializable;

import java.util.List;

import java.io.File;
import java.io.FilenameFilter;
import java.io.FileFilter;

import javax.jws.WebMethod;
import javax.jws.WebParam;
import javax.jws.WebService;

import java.util.ArrayList;

@WebService(serviceName = "Music")
public class Music {

    private static final int MEGABYTE = 1048576;
    private static final File LOCATION = new File("D:/TEMP/SONGS");

    public static class Song implements Serializable {

        private static final long serialVersionUIDe = 1L;
        int size;
        String fileName;
        String artist;

        public Song() {
        }

        public Song(int size, String fileName, String artist) {
            this.size = size;
            this.fileName = fileName;
            this.artist = artist;
        }

        public String getArtist() {
            return artist;
        }

        public void setArtist(String artist) {
            this.artist = artist;
        }

        public String getFileName() {
            return fileName;
        }

        public void setFileName(String fileName) {
            this.fileName = fileName;
        }

        public int getSize() {
            return size;
        }

        public void setSize(int size) {
            this.size = size;
        }
    }

    @WebMethod(operationName = "listSongs")
    public Song[] listSongs(@WebParam(name = "artist") String artist) {

        List<Song> songsOnServer = new ArrayList<Song>();
        if (artist != null) {
            File folderFile = new File(LOCATION, artist);
            if (folderFile.exists() && folderFile.isDirectory()) {
                File[] filesList = folderFile.listFiles(new FilenameFilter() {

                    @Override
                    public boolean accept(File dir, String fileName) {
                        return fileName.toLowerCase().endsWith(".mp3");
                    }
                });

                for (File singleFile : filesList) {
                    int size = (int) (singleFile.length() / MEGABYTE);
                    String folder = singleFile.getParentFile().getName();
                    String fileName = singleFile.getName();
                    Song song = new Song(size, fileName, folder);
                    songsOnServer.add(song);
                }
            }
        }

        return songsOnServer.toArray(new Song[songsOnServer.size()]);
    }

    private File[] getServerFolders(File parentFolder) {
        FileFilter filter = new FileFilter() {

            @Override
            public boolean accept(File path) {
                return path.isDirectory();
            }
        };

        File[] serverFolders = parentFolder.listFiles(filter);

        return serverFolders;
    }

    @WebMethod(operationName = "listArtists")
    public String[] listArtists() {
        File[] serverFolders = getServerFolders(LOCATION);
        List<String> folders = new ArrayList<String>(serverFolders.length);
        for (File folder : serverFolders) {
            folders.add(folder.getName());
        }
        return folders.toArray(new String[folders.size()]);
    }
}

It’s just listing all sub-directories in hardcoded FOLDER directory and treats it as list of artists in music collection. Then you can execute listSongs method and get list of mp3 files in artist sub-folder. To make it a web service all you have to do is annotate class with @WebService(serviceName = "Music") and every method you want to expose as web service operation has to be marked with @WebMethod(operationName = "listArtists"). This should be all if you’re deploying it on GlassFish, but I’ve used Tomcat, so 3 more steps were needed:

  1. Add Metro 2.0 jars to WEB-INF/lib
  2. Add Metro servlet and listener to web.xml:
    <listener>
            <listener-class>com.sun.xml.ws.transport.http.servlet.WSServletContextListener</listener-class>
        </listener>
        <servlet>
            <servlet-name>Music</servlet-name>
            <servlet-class>com.sun.xml.ws.transport.http.servlet.WSServlet</servlet-class>
            <load-on-startup>1</load-on-startup>
        </servlet>
        <servlet-mapping>
            <servlet-name>Music</servlet-name>
            <url-pattern>/Music</url-pattern>
        </servlet-mapping>
    

    You probably shouldn’t change anything here. Just paste it to your web.xml in web-app node.

  3. Add sun-jaxws.xml file to WEB-INF with endpoint declaration:
    <?xml version="1.0" encoding="UTF-8"?>
    <endpoints version="2.0" xmlns="http://java.sun.com/xml/ns/jax-ws/ri/runtime">
      <endpoint implementation="com.wordpress.jdevel.ws.Music" name="Music" url-pattern="/Music"/>
    </endpoints>
    
    • implementation has to match your @WebService class
    • name has to match serviceName in @WebService annotation
    • url-pattern has to match url-pattern you have declared in servlet mapping

There should also be no need to edit these xml files if you create it in NetBeans.

Now start Tomcat and deploy your app. You should be able to access your service via something like http://localhost:8080/WSServer/Music and see something like this:

WSDL will be accessible via http://localhost:8080/WSServer/Music?wsdl
Schema for complex types: http://localhost:8080/WSServer/Music?xsd=1
If you got this working, you can start with

Python client
I’ve started googling for some nice web services library for python and found Suds. I haven’t really used anything like this. Implementing WS client took me about 15 minutes. Supporting complex types of course and last time I’ve been using Python for something bigger than 5 lines was about 3 years ago. You really have to try it out. So here’s the code:

import suds

class ClientApp:    
	def __init__(self):
		self.clientApp = suds.client.Client("http://localhost:8080/WSServer/Music?wsdl")

	def get_songs(self, folder):
		return self.clientApp.service.listSongs(folder)
	
	def get_artists(self):
		return self.clientApp.service.listArtists()

if(__name__ == "__main__"):
    clientApp = ClientApp()
    folders = clientApp.get_artists()
    for folder in folders:
        print folder
        songsOnServer = clientApp.get_songs(folder)
        for songOnServer in songsOnServer:
            print "\t%s : %s : %d%s" % (songOnServer.fileName, songOnServer.artist, songOnServer.size, "MB")

That’s it. Plain simple. WSDL is parsed, complex types get generated on the fly. Something beautiful. It was little bit more difficult for me to implement something like this in

Ruby
Using SOAP4R library. Just execute gem install soap4r to get it (really love this tool :)).
First let’s start with the code:

require 'soap/rpc/driver'
require 'soap/wsdlDriver'

class ClientApp
  def initialize
    driverFactory = SOAP::WSDLDriverFactory.new("http://localhost:8080/WSServer/Music?wsdl")
    @driver = driverFactory.create_rpc_driver
  end  
  
  def get_artists
    artists = @driver.listArtists(nil)
    return artists
  end
  
  def get_songs(artist)
    songsOnServer = @driver.listSongs(:artist => artist)
    return songsOnServer
  end
end

clientApp = ClientApp.new
artists = clientApp.get_artists

artists["return"].each{|artist|
  puts artist
  songsOnServer = clientApp.get_songs(artist);
  if songsOnServer["return"]
	songsOnServer["return"].each {|song| puts "\t%s : %s : %d%s" % [song.fileName, song.artist, song.size, "MB"]}
  end
}

It does exactly the same. Calls web service for list of artists and then, for each artist calls for mp3 files. Then just prints everything out to console. It took me quite a while to get it working. First of all – it’s quite hard to find any documentation. Second – SOAP4R does not work with ruby 1.9 without little hacking: http://railsforum.com/viewtopic.php?id=41231. Next – when you don’t use WSDL to create driver object results are little bit better, but then you exactly have to know what services you have and want to execute. It’s not a problem in this simple example, but if you need to make it little bit more generic… you’ll in trouble. What do I mean by “little bit better”? First, the code:

 @driver = SOAP::RPC::Driver.new("http://localhost:8080/WSServer/Music", "http://ws.jdevel.wordpress.com/");
 @driver.add_method(ARTISTS_METHOD)
 @driver.add_method(SONGS_METHOD, "artist")

This way I’m responsible for declaring endpoint and namespace for service I want to use. I also need to declare all operations I’m going to use, also with parameters (“author”). What’s the difference? When I don’t use WSDL SOAP4R library gives much better return types from invoking services. I can simply omit ["return"] and get something like using Python. What i do need to know in Ruby is how does each complex type look like thus makes my implementation more sensitive for web service change. How to know what key you should use to get data you have in complex type? Check WSDL and look for operation you want to call:

<operation name="listArtists">
    <input wsam:Action="http://ws.jdevel.wordpress.com/Music/listArtistsRequest" message="tns:listArtists"/>
    <output wsam:Action="http://ws.jdevel.wordpress.com/Music/listArtistsResponse" message="tns:listArtistsResponse"/>
</operation>

Next find output complex type in xsd (http://localhost:8080/WSServer/Music?xsd=1):

<xs:complexType name="listArtistsResponse">
    <xs:sequence>
        <xs:element name="return" type="xs:string" nillable="true" minOccurs="0" maxOccurs="unbounded"/>
    </xs:sequence>
</xs:complexType>

What you need is value of name attribute.

Anyway, both implementations look really nice and, whats more important, work fine. Both Ruby and Python have nice web services libraries that handle complex types and WSDL parsing.

About these ads

15 Responses to Web Services in Ruby, Python and Java

  1. robert mac says:

    Could you place a example for consume with a Java application ?
    Thank you in advance

  2. Marek Piechut says:

    Posted update to code. There were some issues. Sorry, posted it some time after using and issues got in. Thanks for pointing it out. Please add a comment if you have any more issues with it.

  3. Ceyhana says:

    I have a problem with Python ,

    C:/Ruby193/lib/ruby/1.9.1/rubygems/custom_require.rb:36:in `require': cannot load such file — soap/rpc/driver (LoadError)
    from C:/Ruby193/lib/ruby/1.9.1/rubygems/custom_require.rb:36:in `require’
    from C:/Users/ME/Documents/NetBeansProjects/Client/lib/Client.rb:1:in `’

    I don’t know how can I resolv it

  4. Ceyhana says:

    I have a problem with Ruby ,

    C:/Ruby193/lib/ruby/1.9.1/rubygems/custom_require.rb:36:in `require’: cannot load such file — soap/rpc/driver (LoadError)
    from C:/Ruby193/lib/ruby/1.9.1/rubygems/custom_require.rb:36:in `require’
    from C:/Users/ME/Documents/NetBeansProjects/Client/lib/Client.rb:1:in `’

    I don’t know how can I resolv it

    • Marek Piechut says:

      You need to have soap4r installed.
      Execute “gem install soap4r” in terminal to get it.

      You’ll need to have /bin folder from ruby install in your PATH or use full path to gem.bat

      You’re using ruby 1.9 so you’ll need to do a small change in installed gem from here: http://railsforum.com/viewtopic.php?id=41231

      Just search for “c.downcase” and replace it with “c.to_s.downcase” in xmlparser.rb from this forum.

  5. Pingback: Web Services – Java client « Development world stories

  6. Anonymous says:

    in the client python i have this message:

    No handlers could be found for logger “suds.client”
    Traceback (most recent call last):
    File “C:\Users\Administrateur\Documents\NetBeansProjects\SaidaClient\src\Client.py”, line 17, in
    folders = clientApp.get_artists()
    File “C:\Users\Administrateur\Documents\NetBeansProjects\SaidaClient\src\Client.py”, line 13, in get_artists
    return self.clientApp.service.listArtists()
    File “C:\Users\Administrateur\.netbeans\7.0\jython-2.5.1\Lib\site-packages\suds\client.py”, line 542, in __call__
    return client.invoke(args, kwargs)
    File “C:\Users\Administrateur\.netbeans\7.0\jython-2.5.1\Lib\site-packages\suds\client.py”, line 602, in invoke
    result = self.send(soapenv)
    File “C:\Users\Administrateur\.netbeans\7.0\jython-2.5.1\Lib\site-packages\suds\client.py”, line 649, in send
    result = self.failed(binding, e)
    File “C:\Users\Administrateur\.netbeans\7.0\jython-2.5.1\Lib\site-packages\suds\client.py”, line 702, in failed
    r, p = binding.get_fault(reply)
    File “C:\Users\Administrateur\.netbeans\7.0\jython-2.5.1\Lib\site-packages\suds\bindings\binding.py”, line 265, in get_fault
    raise WebFault(p, faultroot)
    suds.WebFault: Server raised fault: ‘A required header representing a Message Addressing Property is not present’
    i don’t konw the reason

    • Marek Piechut says:

      Please try with Python 2.6. It might be some incompatibility in logger implementation.

    • Marek Piechut says:

      Here’s what I’ve used: Java 6u30, Metro 2.0, Tomcat 6.0.32, Ruby 1.9.3p0, Soap4r 1.5.8, Python 2.7.2, Suds 0.4.

  7. Anonymous says:

    i change version OF python .i Work with 2.7.2 it give the same problem.

    No handlers could be found for logger “suds.client”
    Traceback (most recent call last):
    File “C:\Users\Administrateur\Documents\NetBeansProjects\SaidaClient\src\main.py”, line 17, in
    folders = clientApp.get_artists()
    File “C:\Users\Administrateur\Documents\NetBeansProjects\SaidaClient\src\main.py”, line 13, in get_artists
    return self.clientApp.service.listArtists()
    File “build\bdist.win32\egg\suds\client.py”, line 542, in __call__
    File “build\bdist.win32\egg\suds\client.py”, line 602, in invoke
    File “build\bdist.win32\egg\suds\client.py”, line 649, in send
    File “build\bdist.win32\egg\suds\client.py”, line 702, in failed
    File “build\bdist.win32\egg\suds\bindings\binding.py”, line 265, in get_fault
    suds.WebFault: Server raised fault: ‘A required header representing a Message Addressing Property is not present’

  8. Anonymous says:

    i work with ruby 1.9.3 when i run the project

    C:/Ruby193/lib/ruby/gems/1.9.1/gems/soap4r-1.5.8/lib/xsd/charset.rb:13: warning: variable $KCODE is no longer effective
    C:/Ruby193/lib/ruby/1.9.1/rubygems/custom_require.rb:36:in `require': iconv will be deprecated in the future, use String#encode instead.
    C:/Ruby193/lib/ruby/gems/1.9.1/gems/soap4r-1.5.8/lib/soap/property.rb:68: warning: encoding option is ignored – u
    C:/Ruby193/lib/ruby/gems/1.9.1/gems/soap4r-1.5.8/lib/soap/property.rb:69: warning: encoding option is ignored – u
    C:/Ruby193/lib/ruby/gems/1.9.1/gems/soap4r-1.5.8/lib/soap/property.rb:70: warning: encoding option is ignored – u
    ignored element: {http://www.w3.org/ns/ws-policy}Policy
    ignored attr: {http://www.w3.org/2007/05/addressing/metadata}Action
    ignored element: {http://www.w3.org/ns/ws-policy}PolicyReference
    C:/Ruby193/lib/ruby/gems/1.9.1/gems/soap4r-1.5.8/lib/soap/generator.rb:276: warning: encoding option is ignored – UTF8
    : A required header representing a Message Addressing Property is not present (SOAP::FaultError)

    where is the problem

  9. Zhou says:

    whoah this blog is great i really like studying your posts.
    Keep up the great work! You already know, many persons are searching around for
    this information, you can aid them greatly.

  10. Hexafish says:

    I got this website from my buddy who told me about this site and at
    the moment this time I am browsing this web page and reading very
    informative posts at this place.

  11. I am really loving the theme/design of your blog. Do
    you ever run into any browser compatibility problems? A few
    of my blog readers have complained about my site not operating correctly in Explorer but looks great in Opera.

    Do you have any advice to help fix this issue?

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

Join 28 other followers

%d bloggers like this: