Tuesday, March 19, 2013 - 22:04

Drupal services,REST and mobile devices using Android as an example.

The services modul extends Drupal's core capabilities to present content immensely. it's one of those modules, with Views and some others, that I usually reference when some expert comes up with a Wordpress opinion.

The services module. A hidden jewel for mobile platform development.

A vanilla setup comes with XMLRPC and REST servers. There're also connectors for SOAP, AMF (Flash) and others. It's pretty much a mobile developer's candy shop. The module provides 2 simple schemes of operations. Actions, like a login, and CRUD Operations ( Create, Retrieve, Update and Delete ) on content. Actions are generally POST requests. CRUD Operation are mapped to POST, GET, PUT and DELETE requests. The module can parse and respond with pretty much everything that's relevant. In our example that would be JSON for output and JSON and x-www-form-urlencoded for input.

A simple GET request to node/1.json would deliver the node referenced by nid 1 as JSON representation. node/1.xml would deliver the same node in xml markup.

The basic url scheme is http{s}://domain/<endpoint>/<resource path>/<entity>.format.

For example http{s}://www.test.com/mobile/user/52.json would retrieve the user referenced by uid 52  as JSON object.

Services comes with resources for comment, file, node, user, taxonomy term and vocabulary and system ( i.e. variables ) by default. Extending resources is somewhat fishy documened but it's actually a whole lot easier than it may look after a first glance at the code.

Our Example in plain words

We are going to extend the services module with a resource to deliver a node representing an album. On the mobile side this will be used to fill a list (ListView in Android) to be picked by the user. We could use the node resource but that is way too much information. We only need the title and the nid. On top of that we will handle the login process as albums will be user centric and only those owned by user are supposed to be transferred to the client.

Extending the services to our needs

The hook to extend our services resources is _services_resources(). The format is very similar to a form or a menu hook. The code for the hook looks like this.

function album_services_services_resources()
{
	$resources = array();
 
	$resources["albums"]["operations"]["retrieve"] = array
	(
		'help' => 'Retrieve albums',
		'callback' => '_album_services_albums_get',
		'access callback' => '_albums_access',
	);
 
	return $resources;
}

So What are we doing here. Our module is called album_services so our hook extending service resources would be album_services_services_resources(). The function returns an array $resources, that is pretty similar to a form or menu array. We could also define arguments here. But for the sake of simplicity and the fact that we won't need them we'll omit them here. Arguments would be an array defining names, types and a source. A source could be a URL path position or data for example. The first line of the array definition translates to albums, a new resource group in the service resources ( like node, user, etc ), operations We define a CRUD operation here and not an Action, retrieve This one is going to be our retrieve operation. The rest is actually quite self explanatory. help defines a short help text for the operation. callback is our callback function we'll deal with right away and access callback defines our access handler, which in this example is actually not necessary. The resource definition for this example, BS excluded is actually a one liner.

function _album_services_albums_get()
{
	if( !user_is_logged_in() )
		return services_error( t( "User not logged in." ), 401 );
 
	global $user;
 
	$uid = $user->uid;
 
	if( $uid )
	{
		$result = db_query( "SELECT n.nid, n.title FROM {node} n WHERE type=:type AND uid=:uid ORDER by n.title ASC", array( ":type" => "album", ":uid" => $uid ) );
 
		$rows = array();
 
 
		foreach( $result AS $row )
			$rows[] = $row;
 
		return $rows;
	}
}

This code again is pretty simple, straightforward and self explaining. We are first checking if the request is authorized. If not the services module is reacting to it. Otherwise we retrieve nid and title for the user's albums and return them as array. The final format transferred to the client is handled by the services module.

Our client now receives something like this [{"nid":"1244","title":"Album X"},{"nid":"3683","title":"Album Y"}] A JSON encoded array of album objects, each containing a field for the entity's node id and its title. You can basically ship everything to the client that can be represented in your chosen output form. Which for JSON is pretty much everything you could come up with in php.

We now want this output to populate our ListView in our mockup Android application. But before we can do that we need to authenticate with Drupal or we will be caught on the first line of our callback function.

Authenticating the Client

Services integrates with OAuth but since everyone and his mother are using session token based authentication we'll stick with that. Drupal's default session based authentication works with a session Cookie that is transferred with every authenticated request. It has the format session_name=sessid. For example:

SESSd57af671041aca5ac87a1b0817a63690=_j4uf6EQoGb-IGJHUTQ_WLFnwMZAnkK3z97-z_aNYIE

This is transferred by the client in a HTTP request header named Cookie. So all we have to do is to obtain the Cookie in the login response and store it for further use when we need to make authenticated requests to our album resource.

Our Login Activity that queries for username and password on Android terminates with this:

LoginTask login = new LoginTask();
 
login.execute( this.app.getUsername(), this.app.getPassword() );
 
this.finish();

Login Task is an AsyncTask background worker that handles the actual network communication. And this is the code:

public class LoginTask extends AsyncTask<String, Void, JSONObject>
{
	private final String TAG = "LoginTask";
 
	private final String login_url = "/user/login";
 
	private AlbumApplication app;
 
	@Override
	protected JSONObject doInBackground(String... params)
	{
		try
		{
			String username = params[0];
			String password = params[1];
 
			JSONObject account = new JSONObject();
			account.put( "username", username );
			account.put( "password", password );
 
			app = (AlbumApplication)AlbumApplication.getAppContext();
 
			URL url = new URL( app.getEndPointURL() + login_url );
 
			HttpURLConnection conn = (HttpURLConnection) url.openConnection();
			conn.setDoOutput( true );
			conn.setDoInput( true );
			conn.setRequestProperty( "Content-Encoding", "UTF-8" );
			conn.setRequestProperty( "Content-Type", "application/json" );
			conn.setRequestProperty( "accept", "application/json" );
			conn.setRequestMethod( "POST" );
 
			OutputStream out = conn.getOutputStream();
 
			byte[] b = account.toString().getBytes();
			out.write( b, 0, b.length );
			out.flush();
			out.close();
 
			String RM = conn.getResponseMessage();
			int RC = conn.getResponseCode();
 
			InputStream in = conn.getInputStream();
			ByteArrayOutputStream baos = new ByteArrayOutputStream();
			b = new byte[1024];
			int nRead;
			while( ( nRead = in.read( b, 0, b.length ) ) != -1 )
				baos.write( b, 0, nRead );
			in.close();
			baos.close();
 
			conn.disconnect();
 
			JSONObject session = new JSONObject( new String( baos.toByteArray() ) );
 
			session.put( "ResponseMessage", RM );
			session.put( "ResponseCode", RC );
 
			return session;
		}
		catch( Throwable t ){ t.printStackTrace(); }
 
		return null;
	}
 
	@Override
	protected void onPostExecute( JSONObject result )
	{
		try
			{
			if( result.has( "session_name" ) && result.has( "sessid" ) )
			{
				this.app.setCookie( result.getString( "session_name"), result.getString( "sessid" ) );
				this.app.setAuthenticated( true );
			}
			else
				this.app.setAuthenticated( false );
			} catch( JSONException e ) { e.printStackTrace(); }
	}
}

So what happens here? the doInBackground method is what we actually call with execute. We turn our username and password into a JSON format Drupal understands, send it over the wire and eval the respone. AlbumApplication is of type android.app.Application. It basically hold our runtime parameters. Our actual url looks like this: http://domain/mobile/user/login

I am using HttpURLConnection in this example but would highly recommend using HttpClient instead. Let's just say the implementation of HttpURLConnection sucks on goat nuts most of the time. Using HttpClient instead of HttpURLConnection will help you stay sane if you ever have to debug connection issues. The debug output of the latter simply sucks. It lies too. In onPostExecute we extract our session_name and id and store that as Cookie in our Application object. So every time we need it from now on we can query the Application for it.

Populating the ListView

Our ListView get's an Adapter that is extending ArrayAdaper. Album is a simple Java Object holding a single Album. Or more precisely it's id and title. In its constructor we again start an AsyncTask that's very similar to the one we already have. The only relevant difference is this

conn.setRequestMethod( "GET" );
conn.setRequestProperty( "Cookie", app.getCookie() );

This time we do a GET request and present our obtained Cookie to the bouncer. We unwind our JSON response array and turn it into an ArrayList, which then in onPostExecute is fed into our adapter via setItemList. A final notifyDataChanged on the adapter and we have our populated list in the UI.

To actually get a POST request in here ( one with HttpClient ) another routine from the application. In this case we are transferring a picture back to the server.

AlbumApplication app = (AlbumApplication) AlbumApplication.getAppContext();
 
File file = new File( this.getRealPathFromURI( item.getUri() ) );
 
HttpPost request = new HttpPost( params[0] );
request.addHeader( "Cookie", app.getCookie() );
request.addHeader( "Content-Encoding", "UTF-8" );
request.addHeader( "accept", "application/json" );
request.addHeader( "X-ALBUMID", Integer.toString( item.getAlbumId() ) );
request.addHeader( "X-TITLE", item.getTitle() );
request.addHeader( "X-FILENAME", file.getName() );
 
ProgressHttpEntity pew = new ProgressHttpEntity( new FileEntity( file, "application/x-www-form-urlencoded" ), file.getName(), file.length() );
 
request.setEntity( pew );
HttpClient client = new DefaultHttpClient();
HttpResponse response = client.execute( request );
 
int nRead;
InputStream is = response.getEntity().getContent();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
 
byte[] b = new byte[1024];
 
while( ( nRead = is.read( b, 0, b.length ) ) != -1 )
	baos.write( b, 0, nRead );
 
is.close();
baos.close();

As you can see HttpClient is pretty much the same...just in better and IMO cleaner. The response we get here is the new node object that is created on the server. The auxiliary information for the new node is transferred in the header. X-ALBUMID, etc. If you have more complex information you should do a multipart POST but in this case this would be overkill. ProgressHttpEntity is not required. It's simply a wrapper to obtain progress information that is displayed in a notice. On the Drupal side you simply create a node, turn the upload into a manged file. Associate file and album entity with the newly created node. Save it and return it in the same way we already returned the albums.

You can also easily wire in stuff like login via facebook which makes user registration clean, simple and convenient for the user. As noted in the beginning OAuth would also be an option.

If you just do mobile Drupal may be overkill and you most certainly will get more performance out of a dedicated non bootstrapping solution. But if you do both, mobile and web, Drupal is a very good choice to integrate both under one hood.

Comments

vince's picture
vince

what about posting data to drupal? more specifically, any suggestions on how to post a taxonomy field to a node?

admin's picture
admin
in reply to vince

That depends a lot on your actual implementation. Generally speaking you'd need to pass the payload (whatever taxonomy you do) and the node id in question. Taxonomy is more tricky because of the way Taxonomy is handled by Drupal and what you are actually doing with Taxonomy.

In either case you need two things. Node ID and Term ID. Since you are working on a node you obviously have the node id. You might or might not have the term's id. So you have one of 3 situations.

1. You have the term's id.

2. You have it but you don't know it.

3. You don't have a term id because there's no such term as of yet.

If you don't have the term's id you have the supposed term in your payload.

Let's say we are talking about TAGs. So you either have a tid for that tag or your have an actual tag like "coding". What you should have is a vocabulary. In this case TAGs.

So you can obtain a tid via taxonomy_get_term_by_name($term, $vocab) where $term would be "coding" and $vocab the vocabulary in question. In this case the one for TAGs.

This either gives you the tid of "coding" or nothing at all if coding is not a tag (as of yet). Assuming you want that tag to be present you'd have to add it to the vocabulary. Terms can be represented by an array just like most in Drupal.

$term = array( 'name' => NAME, 'VID' => VOCAB_ID, 'description' => DESCR, 'weight' => 0);

You can then add it with taxonomy_save_term($term);

So at this point you'd have both you need. Your node's id and your term's id.

$node = node_load($nid);

$node->field_tags[$node->language][0]['tid'] = $tid;

node_save($node);

would update the node with the correct tag.

If you are actually working with TAGS you might not want to set or replace a single element but rather add the term to the field instead. But that depends on what you are actually doing.

That's pretty much it.

Add new comment

This form is protected by Google Recaptcha. By clicking here you agree to include Google Recaptcha for this session. The page will reload and the form will become avaiable.