Writing a Last.FM REST client with Jersey and Java-Gnome

This tutorial will start with a sample top artists (i.e favorite artists) file from http://ws.audioscrobbler.com/2.0/?method=user.gettopartists&user=Firari&api_key=b25b959554ed76058ac220b7b2e0a026. It will follow several steps to write a dynamic client GTK UI for the topartists service. Project depends on java-gnome (4.0.12+), jsr311-api (aka jax-rs), jersey-core and jersey-client libraries. Note that there’s a Java library for accessing Last.FM web services available at http://www.u-mass.de/lastfm Project files can be downloaded as a tarball.

Step 1: Generating XML schema from the sample XML

I downloaded a sample file from the the given URL. I used Trang to reverse engineer the XSD from XML. The command line tool just takes 2 arguments, the input xml and the output xsd files. Here’s the generated XSD file.

<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified">
  <xs:element name="lfm">
    <xs:complexType>
      <xs:sequence>
        <xs:element ref="topartists"/>
      </xs:sequence>
      <xs:attribute name="status" use="required" type="xs:NCName"/>
    </xs:complexType>
  </xs:element>
  <xs:element name="topartists">
    <xs:complexType>
      <xs:sequence>
        <xs:element maxOccurs="unbounded" ref="artist"/>
      </xs:sequence>
      <xs:attribute name="type" use="required" type="xs:NCName"/>
      <xs:attribute name="user" use="required" type="xs:NCName"/>
    </xs:complexType>
  </xs:element>
  <xs:element name="artist">
    <xs:complexType>
      <xs:sequence>
        <xs:element ref="name"/>
        <xs:element ref="playcount"/>
        <xs:element ref="mbid"/>
        <xs:element ref="url"/>
        <xs:element ref="streamable"/>
        <xs:element maxOccurs="unbounded" ref="image"/>
      </xs:sequence>
      <xs:attribute name="rank" use="required" type="xs:integer"/>
    </xs:complexType>
  </xs:element>
  <xs:element name="name" type="xs:string"/>
  <xs:element name="playcount" type="xs:integer"/>
  <xs:element name="mbid" type="xs:string"/>
  <xs:element name="url" type="xs:anyURI"/>
  <xs:element name="streamable" type="xs:integer"/>
  <xs:element name="image">
    <xs:complexType>
      <xs:simpleContent>
        <xs:extension base="xs:anyURI">
          <xs:attribute name="size" use="required" type="xs:NCName"/>
        </xs:extension>
      </xs:simpleContent>
    </xs:complexType>
  </xs:element>
</xs:schema>

Step 2: Generating data classes for mapping the xml

xjc (JAXB Binding Compiler) is used to generate the classes in generated package. Jersey can utilize JAXB to map the result XML to data classes.

Step 3: Writing code to query Last.FM web service

public static Lfm queryTopArtists(String userName) {
	final Client client;
	final WebResource webResource;
	final MultivaluedMap queryParams;
	final Lfm result;

	client = Client.create();
	webResource = client.resource("http://ws.audioscrobbler.com/2.0");
	queryParams = new MultivaluedMapImpl();
	queryParams.add("method", "user.gettopartists");
	queryParams.add("user", userName);
	queryParams.add("api_key", "b25b959554ed76058ac220b7b2e0a026");
	result = webResource.queryParams(queryParams).get(Lfm.class);
	return result;
}

Here we’re building the HTTP request along with the parameters in the query string. And finally we’re calling the service and mapping the result to Lfm class which corresponds to XML’s root element lfm.

Step 4: The GTK+ GUI

Here were initializing the table and its data model.


/*
 * Initialize the table with its DataColumn's.
 */
model = new ListStore(new DataColumn[] { rank = new DataColumnString(),
		artistImage = new DataColumnPixbuf(),
		artist = new DataColumnString(),
		playCount = new DataColumnString(),
		percent = new DataColumnInteger() });
view = new TreeView(model);

Here we’re creating the view columns and binding their properties to data model. Note that were’re binding 2 properties of the CellRendererProgress to different columns in the data model.

/*
 * Create TreeViewColumns and bind the DataColumn's to their properties.
 */
vertical = view.appendColumn();
vertical.setTitle("Rank");
rendererText = new CellRendererText(vertical);
rendererText.setText(rank);

vertical = view.appendColumn();
rendererPixbuf = new CellRendererPixbuf(vertical);
rendererPixbuf.setPixbuf(artistImage);

vertical = view.appendColumn();
vertical.setTitle("Artist");
rendererText = new CellRendererText(vertical);
rendererText.setText(artist);

vertical = view.appendColumn();
vertical.setTitle("# of times played");
rendererProgress = new CellRendererProgress(vertical);
/*
 * It's nice that in GTK+ we can bind multiple DataColumn's to
 * properties of a single TreeViewColumn.
 */
rendererProgress.setText(playCount);
rendererProgress.setValue(percent);

Here we’re querying the service (i.e calling the utility method we wrote) and populating the table. The image data will be fetched asynchroniously by AsyncImageLoader which is a subclass of Thread (Its code wil follow)

/*
 * Query Last.FM user.gettopartists method JAX-RS handles the Webservice
 * call and JAXB handles the unmarshalling of the XML response.
 */
result = LastFMUtil.queryTopArtists("Firari");

/*
 * Add the properties of Artist items as TreeView rows.
 */
if (result.getStatus().equals("ok")) {
	final List<Artist> topArtists = result.getTopartists().getArtist();
	/*
	 * Determining maximum playcount from top of the list.
	 * It will be used to calculate the percentage of the ProgressBar's.
	 */
	maxPlayCount = topArtists.get(0).getPlaycount().floatValue();
	for (final Artist artistItem : topArtists) {
		final TreeIter row = model.appendRow();
		model.setValue(row, rank, artistItem.getRank().toString());
		/*
		 * Asynchronously load the image data and set it as the image
		 * column. The first image URL is the "small" one.
		 */
		new AsyncImageLoader(model, row, artistImage, artistItem.getImage().get(0).getValue()).start();
		model.setValue(row, artist, artistItem.getName());
		model.setValue(row, playCount, artistItem.getPlaycount().toString());
		model.setValue(row, percent, Math.round(artistItem.getPlaycount().intValue() / maxPlayCount * 100));
	}
}

Here’s the code for AsyncImageLoader.

class AsyncImageLoader extends Thread {
	private ListStore model;
	private TreeIter row;
	private DataColumnPixbuf artistImage;
	private String url;

	public AsyncImageLoader(ListStore model, TreeIter row,
			DataColumnPixbuf artistImage, String url) {
		super();
		this.model = model;
		this.row = row;
		this.artistImage = artistImage;
		this.url = url;
	}

	@Override
	public void run() {
		try {
			/*
			 * Fetch the image data and set it as the image column of the
			 * specified row.
			 */
			URL artistImageURL = new URL(url);
			URLConnection artistImageConnection = artistImageURL
					.openConnection();
			DataInputStream in = new DataInputStream(artistImageConnection
					.getInputStream());
			byte[] artistImageData = new byte[artistImageConnection
					.getContentLength()];
			in.readFully(artistImageData);
			/*
			 * The image will have 32 pixels height.
			 */
			model.setValue(row, artistImage, new Pixbuf(artistImageData, -1,
					32, true));
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

Here’s a screenshot of the running application.

TopArtists

The image will have 32 pixels height.
Follow

Get every new post delivered to your Inbox.

Join 534 other followers