Categories


Loading feed
Loading feed
Loading feed

Synchronizing Drupal Modules with Adobe AIR


By Brice Mason

Source Code this example: synchronizing_drupal_modules_with_air.zip.

Whether you're an enterprise developer working in a large shop or setting up a blog for yourself, you've almost certainly been tasked with keeping your development code in sync with some type of stable release. Whether a project is big or small, you still need to ensure that the core code you work with remains consistent. This article will walk you through the development of an Adobe AIR and AJAX application used to synchronize the modules of a site developed in Drupal, the popular free and open-source content management system used in thousands of sites across the Internet.

Requirements

To make the most of this article, you'll need the following software and files:

  • Adobe AIR beta 2
  • Adobe AIR SDK beta 2
  • Drupal
  • Sample files

Prerequisite knowledge

You must have some basic knowledge of JavaScript and HTML. Some knowledge of PHP and Drupal development is helpful.

Laying the Foundation

Drupal is a popular free, open-source content management system developed in PHP. It's used in everything from everyday blog sites to public-facing corporate web sites and intranet systems. In addition to its content management duties, it also offers a development framework that extends the core functionality of Drupal through the use of modules. Modules are key to developing a Drupal site; even the simplest sites usually add a few modules.

Drupal modules are easy to install and configure, usually requiring just a few steps. First, you'll need to download and extract the module(s) for the version of Drupal you want to include in your site. Next, simply use the web-based Drupal administration screen to enable the module and, optionally, configure it. The Drupal framework does the heavy lifting to recognize when new modules have been added. This high level of simplicity lets even new developers create sophisticated systems with relatively little effort.

As easy as it is to create a complex site with Drupal, it can be even easier to shift your development and production Drupal module codebase out of sync. You could solve this problem using any number of methods, including manual copy/paste routines, file backup utilities, or even developing a complex desktop application. But now there's a better option: Adobe AIR.

Adobe AIR First Steps

Adobe AIR is a cross-operating system runtime that lets you produce sophisticated desktop applications using the HTML and JavaScript skills you use for everyday web development. Because the Adobe AIR framework leverages these core skills and technologies, your adoption of Adobe AIR development will be rapid and intuitive. One of the few learning curves to Adobe AIR development with AJAX is to understand the JavaScript framework that provides access to functionality-such as file access and network monitoring-typically found in a native desktop application.

The Adobe AIR framework's rich functionality makes it the perfect candidate for synchronizing your Drupal module files across various operating environments. The example in this article will take advantage of Adobe AIR's file-access and network-request capabilities to synchronize the Drupal module codebase.

Laying Out the Application

To get started building the application, create the directory path projects/drupal_module_sync under the root of your Adobe AIR SDK installation. This will make it easier to build and test the application consistently. As with any Adobe AIR application, you'll need, at a minimum, an application descriptor file and the main content HTML file for your application (both of which Listings 1 and 2 show).

Listing 1. Application descriptor file

<?xml 
version="1.0" encoding="UTF-8"?>
<application appId="local.air" xmlns="http://ns.adobe.com/air/application/1.0.M5" version="0.1">
    <name>Drupal Module Synchronization</name>
    <initialWindow>
        <content>index.html</content>
        <visible>true</visible>
        <systemChrome>standard</systemChrome>
        <width>640</width>
        <height>480</height>
    </initialWindow>
</application>


Listing 2. Main content HTML file

<html>
<head>
    <title>Drupal Module Synchronization</title>
    <link rel="stylesheet" type="text/css" href="style/main.css" />
    <script type="text/javascript" src="script/AIRAliases.js"></script>
    <script type="text/javascript" src="script/main.js"></script>
</head>
<body>
    <div id="cntr_outer">
        <h1>Drupal Module Synchronization</h1>
		
        <div id="cntr_main"></div>
            <input type="button" value="Sync Sites" onClick="syncSites();"   />
			
        <div id="cntr_results">
            <h3>Results</h3>
        </div>
		
    </div>
</body>
</html>


The application.xml file is the special file Adobe AIR uses to configure application properties such as window size, transparency, visibility, versioning, and so on. During development you can name this file anything you want; after you decide to package it for deployment, it will be renamed application.xml. Just as our descriptor file has remained simple and light, so has the main content file defined in Listing 2. The purpose of this code is to define a couple of containers used to display some basic information throughout the application's lifecycle. Notice that we have also hooked in a stylesheet and two JavaScript files, which are used to maintain the application's styles and logic, respectively. Although the AIRAliases.js script ships with the Adobe AIR framework, we define the main.js script to drive the Adobe AIR application. Taking into account the added Cascading Style Sheet (CSS) and JavaScripts, our project directory should be configured as follows:

drupal_module_sync
   +--- script
   |      +--- AIRAliases.js
   |      +--- main.js
   |
   +--- style
   |      +--- main.css
   |
   +--- sys
   |      +--- config.xml
   |
   +--- application.xml
   +--- index.html


The directory listing above should look familiar, with the exception of the sys/config.xml file. We'll discuss this further in a bit. Throughout the development of our application, we'll need to test the application. To accomplish this, Adobe AIR provides the AIR Debug Launcher (ADL) tool, which you can use to quickly and easily run your application. For this application, the easiest way to do this is to open a command prompt, change directories to the bin directory in the Adobe AIR SDK, and run the following command: adl ../projects/drupal_module_sync/application.xml

Although this tool has more advanced options, the command listed above is sufficient to properly test and debug your application. You don't even need to install the Adobe AIR runtime to use it.

Digging Deeper

In the simplest terms, Drupal modules are a group of files stored under a special directory of the Drupal installation. Each module is identified in the Drupal framework by its directory name in the sites/all/modules directory under the Drupal installation root. After a new module shows up under this directory, Drupal detects it and includes it in the module administration screen, where it's ready to be enabled and configured.

The main.js script has a few functions defined that drive the application, the first of which Listing 3 shows.

Listing 3: Initialization code

// globals
var configurationFilePath = "sys\\config.xml";
var configXML;

window.onload = function() {
    // get the configuration
    configXML = getConfiguration();
	
    // initialize the user interface
    initUI();

}


The code in Listing 3 basically works out to be the application's initialization code. The two global variables are used to define and store the application's custom configuration, which will be used throughout. The getConfiguration function retrieves the XML configuration from a file, and the initUI function initializes and loads the user interface. Because the goal of this application is to consistently manage Drupal modules across any number of environments, it's important that we explore the concept of using XML files for configuration management and the impact it has on our code.

Managing the Configuration

When working on any sort of software project, it's important to separate and maintain the software properties consistently. You can accomplish this by hardcoding values within the code (bad), including or linking source code (better), and using external configuration files such as INI or XML files (best). In our case, the following format was chosen for the sys/config.xml configuration file:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <environment name="Drupal Example Site" update_service="http://localhost/drupal-dev/moduleSync.php">
        <site name="prod" modules_path="C:\data\www\drupal-prod\sites\all\modules" />
        <site name="dev"  modules_path="C:\data\www\drupal-dev\sites\all\modules" />
    </environment>		
</configuration>


The configuration defined above provides for n number of environments to be synced. The example consists of an tag, which defines a friendly name for the environment as well as the location of a simple REST web service used to automatically enable the modules installed in our environment. The tags are used to define the two Drupal sites to be synced. The first site defined is always the stable release, which will basically be used to query for the installed modules and copy them to the development site. Each site has a module path defined, which helps identify the modules in each site and copy them to the proper location.

Now that we're familiar with the configuration, let's examine the code used to read and store the information. The full source code of the getConfiguration function is as follows:

function getConfiguration() {
    // the path to the config file begins in the app resource directory
    var configFile = air.File.applicationResourceDirectory;
	
    // resolve the path to the config file from the app resource directory
    configFile = configFile.resolvePath( configurationFilePath );
	
    // create a new FileStream object
    var fileStream = new air.FileStream();
	
    // set the config file for reading
    fileStream.open( configFile, air.FileMode.READ );
	
    // read the entire file synchronously
    var xmlConfig = fileStream.readUTFBytes( fileStream.bytesAvailable );
	
    // close the stream
    fileStream.close();
	
    // create a new DOM parser
    var domParser = new DOMParser();
	
    // get the config as XML
    xmlConfig = domParser.parseFromString( xmlConfig, "text/xml" );
	
    return xmlConfig;
}


The getConfiguration function is quite useful for demonstrating some of the basic concepts related to file access in Adobe AIR. This function uses the three main classes for performing file operations: File, FileStream, and FileMode. The File class represents information about the target file system, which can include path names, system settings, and creation dates. Although this type of information might differ across operating systems, Adobe AIR provides a consistent interface to all the key file system information without having to compensate for different platforms. Coupled with the File class, the FileStream class can be used to read and write files to the file system, and the FileMode class consists of string constants used to determine the capabilities of the FileStream object.

The code above starts off by defining a new Adobe AIR File object, which consists of the path to the application root using the File class's applicationResourceDirectory property. From here, the resolvePath File object method is used to point to the custom XML configuration file. A new FileStream object is created so we can synchronously read the file contents. You can choose to read the file asynchronously using the openAsync method, but you'd also need to define the proper event handlers for a complete solution. After the raw data is read, the FileStream is closed and a new DOM object representing the configuration is created and stored in the configXML global variable.

The configuration we've defined in our XML file can now be used throughout the application. The initUI function (see below) is pivotal in this regard, as it's responsible for loading and updating the user interface based on the environments we defined in the configuration using some basic DOM processing.

function initUI() {
    var classStyle = "";
    // get all the environments defined in the config file.
    var environments = configXML.getElementsByTagName( "environment" );
    // clear the UI
    document.getElementById( "cntr_main" ).innerHTML = "";
	
    for( var i = 0; i < environments.length; i++ ) {
        // get the site name and module paths for the current environment
        var siteName   = environments[i].getAttribute( "name" );
        var masterPath = environments[i].getElementsByTagName( "site" )[0].getAttribute( "modules_path" );
        var slavePath  = environments[i].getElementsByTagName( "site" )[1].getAttribute( "modules_path" );
		
        classStyle = i % 2 == 0 ? "multirow" : "";
		
        document.getElementById( "cntr_main" ).innerHTML += "
" + siteName + "
";
    }
}


When the application runs, it will look like Figure 1 below.

Figure 1. Main application window

You can already see how powerful it is to factor out the properties that drive an application to a configuration file. For this example, if you need to add any number of environments you want to manage, you can just define them in the configuration file and it's ready to go.

Moving Forward

Now that we have a consistent method for managing the application, let's take a look at the core code used to manage the synchronization of Drupal modules across the sites we've defined. The button in our main HTML content invokes the syncSites function shown below.

function syncSites() {
    // get all the environments defined in the config file.
    var environments = configXML.getElementsByTagName( "environment" );
	
    for( var i = 0; i < environments.length; i++ ) {
        // get the module paths for the current environment
        var remotePath = environments[i].getElementsByTagName( "site" )[0].getAttribute( "modules_path" );
        var localPath  = environments[i].getElementsByTagName( "site" )[1].getAttribute( "modules_path" );
		
        // get the path to the update service
        var updateService = environments[i].getAttribute( "update_service" );
		
        // create AIR File objects from the paths
        var remoteDir = new air.File( remotePath );
        var localDir  = new air.File( localPath );
		
        // get all the directory (module) names in the local environment
        var arr_localModules = getModuleNames( localPath );
        // get all the directory (module) names in the remote environment
        var arr_remoteModules = getModuleNames( remotePath );
		
        // disable the modules in the local environment
        execModuleSyncService( updateService, "disable", arr_localModules.join( "," ) );
		
        // delete all the module files in the local environment
        var localContents = localDir.getDirectoryListing();
        for( var i = 0; i < localContents.length; i++ ) {
            if( localContents[i].isDirectory ) {
                localContents[i].deleteDirectory( true );	
            }
        }
		
        // copy over all the module files from the remote environment
        var remoteContents = remoteDir.getDirectoryListing();
        for( var i = 0; i < remoteContents.length; i++ ) {
            if( remoteContents[i].isDirectory ) {
                // save a copy of the current absolute path in the remote environment
                var tmpRemoteDir = remoteContents[i].nativePath;	
				
                // replace the remote path with the local path
                var tmpReplacedPath = tmpRemoteDir.replace( remotePath, localPath );
				
                // copy the remote module files to the local environment
                remoteContents[i].copyTo( new air.File( tmpReplacedPath ) );
            }
        }
		
        // enable the modules that were just copied over from the remote environment
        execModuleSyncService( updateService, "enable", arr_remoteModules.join( "," ) );
    }
	
    // refresh the user interface
    initUI();
} 


The process we use to update the development site with the modules installed in the production site is as follows:

  1. Disable all modules in the development site.
  2. Delete all module files in the development site and replace them with the production site module files.
  3. Enable all modules in the development site.

The process of disabling and enabling the modules for the development site is accomplished through the execModuleSyncService function (see Listing 4). This function uses the Adobe AIR framework, which crafts HTTP requests, in our case requesting a simple REST web service we developed in Listing 5.

Listing 4. execModuleSyncService

function execModuleSyncService( serviceUrl, action, moduleList ) {
    // create a URLLoader object
    var serviceLoader = new air.URLLoader();
	
    // configure the request to the module sync service
    var serviceRequest = new air.URLRequest( serviceUrl );
	
    // parameters will be posted
    serviceRequest.method = "post";
	
    // configure the arguments to the service
    var serviceVariables = new air.URLVariables();
    serviceVariables.module_list = moduleList;
    serviceVariables.action = action;
	
    // attach the arguments to the request
    serviceRequest.data = serviceVariables;
	
    // set the request completion event handler
    serviceLoader.addEventListener( air.Event.COMPLETE, moduleSyncServiceCompleteHandler );
	
    // call the service
    serviceLoader.load( serviceRequest );	
}

function moduleSyncServiceCompleteHandler( evt ) {

    document.getElementById( "cntr_results" ).innerHTML += evt.target.data + "
"; }


Listing 5. REST web service

<?php
    // load drupal api
    require_once './includes/bootstrap.inc';
    drupal_bootstrap( DRUPAL_BOOTSTRAP_FULL );
	
    // enable or disable the modules
    $action = $_POST[ "action" ];
	
    // get the list of module names
    $arr_module_list = explode( ",", $_POST[ "module_list" ] );
	
    if( $action == "enable" ) {
        module_enable( $arr_module_list );
        echo "modules enabled successfully";
    }
    elseif( $action == "disable" ) {
        module_disable( $arr_module_list );
        echo "modules disabled successfully";
    }
?>


The execModuleSyncService function uses the URLLoader, URLRequest, and URLVariables classes to prepare and execute HTTP requests to the web service. Because the URLLoader class is most useful for downloading data typically used in any type of data-driven application, it's ideal for handling the interaction with our web service. The URLRequest class is used in conjunction with the URLVariables class to prepare the request. We are simply creating a request that will post the parameters defined in the serviceVariables object to our service. Finally, an event handler is attached to the request that will fire when the request completes successfully. This event handler updates the results container in the user interface with the content returned from the completed request.

The web service we created in Listing 5 is a simple extension to the Drupal framework. The code that includes and runs the bootstrap logic is used to load the Drupal settings and framework functionality to our service. Finally, the parameters posted to the service are processed and passed to the module_enable and module_disable module framework functions based on the action requested.

Wrap Up

Although this solution is useful, it's also important to note what it didn't do. First, the modules managed in this scheme are comprised of only those we added. Any core modules that we might have enabled or disabled in the production site aren't accounted for. Also, this scheme doesn't move configurations of third-party modules to our development site; it handles only the physical file/directory movement and the enabling/disabling of the modules. However, the good news is that incorporating these features can be very easy using Adobe AIR and a little additional PHP hacking in Drupal.

This article demonstrates some of Adobe AIR's more powerful and elegant capabilities. Not only does it offer an intuitive framework catered to the traditional web developer, it also serves the useful purpose of making our jobs a little easier.

Resources

Comments