Saturday, January 30, 2010

Drupal interface to Java using XML-RPC

I haven't been posting to the blog as often as I want to lately. I just came back from a fairly long vacation in India, and over the last month, I have been trying to catch up on stuff, both at home and at work. The good news is that I am almost caught up, so I should return to my normal posting frequency shortly - as long as I have something interesting to write about, that is.

At work, we are setting up Drupal as our internal CMS. This is a bit of a departure from our normal practice, since Drupal is written in PHP, and we are predominantly a Java shop. Personally, I would probably have gone with Alfresco or dotCMS, but I don't know much about either CMS, or much about CMSs in general, so my preference is based solely on the fact that they are written in Java and open-source, so customizing them to interface with other existing (Java) components would be easier.

However, having now spent about a week futzing with Drupal, I am quite impressed with its pragmatic design - there are many good ideas in there that a Java web developer could use. Many thanks to John K VanDyck for writing Pro Drupal Development - this helped me immensely in getting up to speed quickly. Regarding interfacing to existing Java components, its not as huge a deal as I thought it would be either. From my point of view, Drupal (or any other CMS for that matter) is mainly a container of content, and interfacing with it would be done through a narrow (code wise) interface point.

The particular case I was looking at was to allow Drupal to publish/depublish stories to an external data store, from where it would be read by one or more Java application(s). The solution I came up with was to have Drupal send an XML-RPC message to a Java middleware server component on publish or depublish, which would do what is needed to write it out to the external data store. I describe the Drupal side of the interface here.

Installing Drupal

As mentioned, I have been using Drupal for about a week, so the first step was to install it. On my CentOS desktop at work, I installed (using yum) PHP, PHP-MySQL and re-installed MySQL (since the PHP-MySQL RPM did not seem to play well with the already installed MySQL RPMs from MySQL. I also set up Lighttpd as my webserver, then copied the Drupal tarball into my document root. On my Mac OS X notebook, I had to remove MySQL and install MAMP, which bundles Apache, MySQL and PHP, then install the Drupal tarball into my Document root.

From there, all you have to do is to create your Drupal database and user and then navigate to http://localhost/install.php on your browser. The installation process will walk you through a few pages, and you are all set up.

The Interface Module

Extension to Drupal are made via Modules. Drupal modules are typically written by super-experienced Drupal/PHP coders, but this one is really short and simple. I looked around a bit on the Internet for something similar, but perhaps my use case is too trivial for someone to build and contribute a module for. All it does is define an Advanced Action which is triggered on a Node insert, update or delete, along with its form for configuration. All this stuff is adapted from Chapters 3 and 19 of the Pro Drupal Development book.

My module is named dxi (Drupal XML Interface). The code lives under sites/all/modules/custom/dxi. The dxi.info file contains the meta information for the module. It looks like this:

1
2
3
4
5
; Source: sites/all/modules/custom/dxi/dxi.info
name = dxi
description = Drupal Interface to Java via XML-RPC
core = 6.x
package = My Company

The actual code is in dxi.module. I initially started with the send_request() and dxi_nodeapi() methods. This would have the effect of being called on every node event. So in the dxi_nodeapi() method, I was checking and firing the send_request() call only when the operation was "insert", "update" and "delete". This is really all that is needed to get my interface up and running.

However, using an Advanced Action instead allows you to let the user choose whether the event should be fired in the future on different events, and to set the URL for the remote XML-RPC server from the Administrator GUI instead of having to hardcode it into the code. So I ended up using the Advanced Action approach. Actions in the dxi module are defined in dxi_action_info() - there is only one, dxi_call_action(). Because it is an Advanced (Configurable) action, it needs a configuration form, which is defined by dxi_call_action_form(), the form validation is defined in dxi_call_action_validate() and the populated form values are returned from dxi_call_action_submit().

Both approaches call the send_request() method, which does the actual XML-RPC call to the remote server. The code for the entire module is shown below, with the first (hook_nodeapi) approach commented out for posterity/just in case.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
<?php
/*
 * Source: sites/all/modules/custom/dxi/dxi.module
 */

/**
 * Function to send the actual XML-RPC request over to the remote
 * server. Will throw a drupal error message if the XML-RPC request
 * fails for any reason.
 * @param $server_url - the URL of the remote server.
 * @param $op - the name of the operation to invoke.
 * @param $node - the reference to the node.
 * @return 0 or -1.
 */
function send_request($server_url, $op, $node) {
  watchdog('dxi', 'Sending request publish_' . $op . '(' . $node->nid . ')');
  $result = xmlrpc($server_url, 'publisher.' . $op, 
    $node->nid, $node->title, $node->body);
  if ($error = xmlrpc_error()) {
    if ($error->code <= 0) {
      $error->message = t('Remote server appears to be down');
    }
    drupal_set_message(t('Operation publish_' . $op . '(' . $node->nid .
      ') failed: %message (@code).', array(
      '%message' => $error->message,
      '@code' => $error->code
      )
    ));
    return -1;
  }
  return 0;
}

/**
 * Implementation of hook_nodeapi().
 * This is automatically called by the hook_nodeapi() on all nodeapi
 * events. This is the simplest approach to sending an XML-RPC request
 * for the desired operations.
 * @deprecated - use the dxi_call_action approach instead.
 * @param &$node - the node object.
 * @param $op - the operation.
 * @param $a3 - optional argument, set to NULL.
 * @param $a4 - optional argument, set to NULL.
 */
//function dxi_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) {
//  if ($op == 'insert' || $op == 'update' || $op == 'delete') {
//    watchdog('dxi', 'dxi_nodeapi' . $op);
//    return send_request('http://localhost:8081/publisher', $op, $node);
//  }
//}

/**
 * Implementation of hook_action_info().
 * @return the $info object.
 */
function dxi_action_info() {
  $info['dxi_call_action'] = array(
    'type' => 'node',
    'description' => t('Send XML-RPC request'),
    'configurable' => TRUE,
    'hooks' => array(
      'nodeapi' => array('insert', 'update', 'delete')
    )
  );
  return $info;
}

/**
 * This function traps the nodeapi_insert, nodeapi_update and nodeapi_delete
 * events, and sends an XML-RPC request over to the Java server. This approach
 * is slightly more flexible than the dxi_nodeapi() approach, since you can
 * set up the component and operation mappings to the action from the Drupal
 * administration GUI.
 * @param $object - the node object.
 * @param $context - the context object.
 */
function dxi_call_action($object, $context) {
  $node = $object;
  if ($context['hook'] == 'nodeapi') {
    watchdog('dxi', 'dxi_call_action');
    // we only want to trigger this action on a node publish or unpublish
    // but we let the user decide that through the GUI
    $op = $context['op'];
    $remote_url = $context['remote_url'];
    return send_request($remote_url, $op, $node);
  }
}

/**
 * Implementation of ${action_name}_form. This returns field information
 * for the configuration form for this action.
 * @param $context - the context.
 * @return the $form object.
 */
function dxi_call_action_form($context) {
  $form['remote_url'] = array (
    '#type' => 'textfield',
    '#title' => t('Remote Server'),
    '#description' => t('Enter URL of Remote Server'),
    '#default_value' => isset($context['remote_url']) ?
      $context['remote_url'] : 'http://localhost:8080/publisher',
    '#required' => TRUE
  );
  return $form;
}

/**
 * Implementation of the ${action_name}_validate. This contains validation
 * for the form inputs. This is a NO-OP here.
 * @param $form - the form.
 * @param $form_state - the $form_state
 */
function dxi_call_action_validate($form, $form_state) {
  // Nothing
}

/**
 * Implementation of the ${action_name}_submit. This returns the validated
 * values of the form.
 * @param $form - the form.
 * @param $form_state - the form state.
 * @return the map of names and values of form fields.
 */
function dxi_call_action_submit($form, $form_state) {
  return array(
    'remote_url' => $form_state['values']['remote_url']
  );
}

Interface Installation and Configuration

Install the Module: Go to Administer :: Site building :: Modules and you should see the dxi module at the bottom of the page. Click the Enabled checkbox and save the configuration.

Add the Action: Go to Administer :: Site configuration :: Actions to see a list of Actions currently available to Drupal. At the bottom is a dropdown list of Advanced Actions. Choose the one that says "Send XML-RPC request" and click the Create button. This will bring up the configuration screen for this action. Set the URL for the XML-RPC server and click Save. You will see the action now associated with node Action type.

Associate the Action with the Trigger: Go to Administer :: Site building :: Triggers. Choose the Content tab. You will see a list of trigger types for Nodes. Add the new action to each of "After saving a new post", "After saving an updated post" and "After deleting a post" triggers.

Testing

If you don't have a server available (I don't yet), then just comment out lines 17-30 of dxi.module (the block which has the XML-RPC call and the error handling), then create/update/delete a story. After each step, take a look at the log entries generated - navigate to Administer :: Reports :: Recent log entries and look for the log messages with the type "dxi" - you should see entries with "Sending request..." which should convince you that the stuff is working.

4 comments (moderated to prevent spam):

Jeremy Evans said...

Saw your article this mornning Sujit,

Came up in the number 1 slot on Google for "Posting with xml-rpc using Drupal"

I am at home working on the Popup.

Sujit Pal said...

Cool, thanks for the info! I did a search before I wrote this post, and it looks like no one has considered (or at least written about) using Drupal in this way, so I guess it fills a niche that bumps it to the top spot on Google.

Samaddar said...

i like your blog very much :-) actually i also want to do some thing like that , so can you elaborate it more for me ?

Sujit Pal said...

Thanks Samaddar. To answer your question, though, there is not much to elaborate, this is a simple module which hooks into Drupal's event framework - so when a node is saved or published for example, all modules that implement hook_nodeapi() or modules that implement hook_action_info() AND are configured to trigger on certain actions are invoked. I found the Pro Drupal book very useful when building this, it may be helpful for you too.