MindTouch Developer Center > MindTouch Dream > Tutorials > Creating an Address Book service with a Google Map mash-up

Creating an Address Book service with a Google Map mash-up

In this tutorial, we'll put together what we have learned from making our Magic 8-ball and Show-Headers services to build a much more functional service.  So far, all our services have been stateless.  That is, they did not manage any information for us.  The Magic 8-ball service just returned a random answer and the Show-Headers service replied with an XML representation of the received Dream message. It's now time to build something that has more meat to it.

Our Address Book service will provide several capabilities:

  • It will allow new addresses to be added.
  • It will save and restore its state from persistent storage when the service shuts-down and starts-up again.
  • It will allow to query its contents.
  • It will serve as the basis for a mash-up application.

Ok, that's enough goals for one sample.  Let's get started!

Getting Started

Let's begin at the beginning.  First, we need to create a service class.

namespace MindTouch.Dream.Samples {
    using Yield = System.Collections.Generic.IEnumerator<IYield>;

    [DreamService("Dream Tutorial Address-Book", "Copyright (c) 2006, 2007 MindTouch, Inc.",
        Info = "http://doc.opengarden.org/Dream_SDK/Tutorials/Address_Book",
        SID = new string[] { "http://services.mindtouch.com/dream/tutorial/2007/03/addressbook" }
    )]
    public class AddressBookService : DreamService {
    }
}

The first thing we need to add is an instance field to hold our address book.  For our purposes, keeping the address book as an XML document will be easiest.  Also, we want our address book to be saved and restored when the service shuts-down or starts-up.  Fortunately, the Dream framework makes this really easy since every service has automatically an associated storage location.  So, all we need to do is mark our instance field as being part of the service's state.  We do this by using the DreamState attribute.

[DreamState]
private XDoc _addresses;

Now, when the service starts-up, the Dream framework will attempt to restore the _addresses field.  If the service has saved state, the field will remain uninitialized.  We can use this fact in our Start() method to provide default content.

public override Yield Start(XDoc config, Result result) {
    Result res;
    yield return res = Coroutine.Invoke(base.Start, config, new Result());
    res.Confirm();

    // try to restore the address book
    if(_addresses == null) {

        // let's add a dummy address to have some default content
        _addresses = new XDoc("addressbook");
        Self.At("addresses").Post(new XDoc("address")
            .Start("first").Value("Mike").End()
            .Start("last").Value("Church").End()
            .Start("phone").Value("555-0070").End()
            .Start("address").Value("2786 University Ave, San Diego, CA").End()
        );
        Self.At("addresses").Post(new XDoc("address")
            .Start("first").Value("Sara").End()
            .Start("last").Value("Thomas").End()
            .Start("phone").Value("555-5510").End()
            .Start("address").Value("2931 Market St, San Diego, CA").End()
        );
        Self.At("addresses").Post(new XDoc("address")
            .Start("first").Value("Nora").End()
            .Start("last").Value("Church").End()
            .Start("phone").Value("555-0136").End()
            .Start("address").Value("1747 India St, San Diego, CA").End()
        );
    }

    // mount filesystem
    _fs = CreateService("mount", "http://services.mindtouch.com/dream/draft/2006/11/mount", 
        new XDoc("config").Start("mount").Attr("to", "files").Value("%DreamHost%/../address-book").End()
    );
    result.Return();
}

The Start() method is called after the service features have been registered, but before the service is announced as being available.  Thus, it is possible for the service to interact with other services (including itself as shown here), but it cannot be discovered by inspecting the list of active services at http://localhost:8081/host/services .  Also, we're mounting a folder from the local file-system to allow access to it (this will come in handy during the mash-up part).

The Self property is a Plug instance that refers to the current service instance.  A plug is the standard communication mechanism in Dream.  Plugs connect to sockets that are either remote or local.  If a the socket is local, the plug will exchange data by reference.  Otherwise, the data will be automatically serialized.  Plugs can communicate synchronously, thus blocking their invoking context, or asynchronously.

Adding Behavior

We need four features on our service:

  1. A feature to retrieve all addresses.
  2. A feature to add an address.
  3. A feature to find an address by first name
  4. A feature to find an address by last name.

GET:addresses

Let's begin with the simplest one: a feature to retrieve all addresses.  (Note how this is expressed in the title of this section.  This notation is common in Dream and worthwhile explaining.  A Dream feature is composed of two parts: a verb and a pattern.  These two parts are concatenated together to provide a concise notation for a feature.  It might look a little funky at first, but it works out quite nicely.) Since our address book is already an XML document, we can simply send it back.  The only thing to be careful about is making sure nobody modifies our address book while we send it out.

[DreamFeature("GET:addresses", "Get all addresses")]
public Yield GetAddresses(DreamContext context, DreamMessage request, Result<DreamMessage> response) {

    // send back the entire address book
    lock(_addresses) {
        response.Return(DreamMessage.Ok(_addresses));
    }
    yield break;
}

POST:addresses

Our next feature is about adding new content.  This is the feature that is being invoked by our Start() method when no content exists yet.  This feature expects the new address to be supplied by the request. DreamMessage provides several methods for accessing the supplied data.  In this case, we expect the request to contain an XML document.  So, we use the Document property.  An exception is thrown if the request body is not an XML document.  Once we have the document, we validate that it contains the elements we need.  And, finally, we add the received document to our address book.  Since XDoc does not differentiate between an XML document and an XML element, we can simply add it without further thought.

[DreamFeature("POST:addresses", "Add an address")]
public Yield PostAddresses(DreamContext context, DreamMessage request, Result<DreamMessage> response) {

    // get the request body
    XDoc address = request.AsDocument();

    // validate the request
    if((address.Name != "address") || address["first"].IsEmpty || address["last"].IsEmpty || address["address"].IsEmpty) {
        response.Return(DreamMessage.BadRequest("address document is not valid"));
        yield break;
    }

    // add the address to the address book
    lock(_addresses) {
        _addresses.Add(address);
    }

    // respond with success
    response.Return(DreamMessage.Ok());
    yield break;
}

GET:firstname/{name} and GET:lastname/{name}

These two features are so similar, we'll implement them simultaneously.  Their purpose is to return an address book with only matching contacts.  Both feature require a parameter in their path.  This is described by the "/{name}" pattern.  Parameters are accessed via the GetParam instance method of context.

The core of the search is performed by an XPath expression.  We simply specify that we want to find all XML elements that match our search criteria.  This will yield a node set.  By default, XDoc will assume the identity of the first node in the set, but we can iterate through all of them by using the Next property, using the returned XDoc in a foreach statement, or just using the AddAll() method as is the case below:

[DreamFeature("GET:firstname/{name}", "Get list of all addresses matching first name")]
public Yield GetFirstName(DreamContext context, DreamMessage request, Result<DreamMessage> response) {
    string firstname = context.GetParam("name");

    // make XPath
    string xpath = string.Format("address[first='{0}']", firstname);

    // reply with found addresses
    response.Return(DreamMessage.Ok(FindAddresses(xpath)));
    yield break;
}

[DreamFeature("GET:lastname/{name}", "Get list of all addresses matching last name")]
public Yield GetLastName(DreamContext context, DreamMessage request, Result<DreamMessage> response) {
    string lastname = context.GetParam("name");

    // make XPath
    string xpath = string.Format("address[last='{0}']", lastname);

    // reply with found addresses
    response.Return(DreamMessage.Ok(FindAddresses(xpath)));
    yield break;
}

private XDoc FindAddresses(string xpath) {
    XDoc result = new XDoc("addressbook");

    // add all matching addresses to result
    lock(_addresses) {
        result.AddAll(_addresses[xpath]);
    }
    return result;
}

Running the Service

Our address book service is done.  Now, let's see what it looks like.  To run the service, we can use the following script.

<script> 
  <!-- Address Book sample -->
  <action verb="POST" path="/host/load?name=dream.sample.address-book" />
  <action verb="POST" path="/host/services">
    <config>
      <path>address-book</path>
      <sid>http://services.mindtouch.com/dream/tutorial/2007/03/addressbook</sid>
    </config>
  </action>
</script>

Compile the service into an assembly file and copy into your bin folder where the other dream binaries are.  (Note: this is automatically done by the Visual Studio.Net samples solution file and the makefile.)

Now, let's start our service:

mindtouch.host.exe script addressbook.startup.xml

To check the list of all addresses, we can use http://localhost:8081/address-book/addresses.

Or, we query for an address by first name using http://localhost:8081/address-book/firstname/Sara .

Mashing things up

Now that we have our address-book service running, it's pretty straight forward to consume the data from another application.  For this example, let's build a little mash-up between our address-book and the Yahoo! map service.

First, let's put in place a skeleton file that invokes the Yahoo! maps service.  Let's call it addressbook.html.

<html>
    <head>
        <title>MindTouch Dream Adress-Book &amp; Yahoo! Maps mash-up</title>
        <script type="text/javascript" src="http://api.maps.yahoo.com/ajaxymap?v=3.0&appid=MindTouchDemo"></script>
        <script type="text/javascript" src="http://localhost:8081/address-book/mount/files/prototype.js"></script>
        <style type="text/css">
            #mapContainer { 
                height: 90%;
                width: 100%; 
            }
        </style>
    </head>
    <body>
        <div id="mapContainer"></div>
        <script type="text/javascript">

            // Create a map object
            var map = new YMap($('mapContainer'));
            map.setMapType(YAHOO_MAP_SAT);

            // Display the map centered on given address
            map.drawZoomAndCenter("San Diego", 5);

            // *****************************
            // *** DREAM CODE TO GO HERE ***
            // *****************************
        </script>
    </body>
</html>

Next, let's add an AJAX request to our address-book service.  For this, we'll use the prototype library that we already included.  The code goes at the above marked position.

var request = new Ajax.Request(
    "/address-book/addresses?dream.out.format=jsonp", 
    {
        method: "get",
        asynchronous : false
    }
);

A couple of notes about the code:

  • First, we use a synchronous request to keep our example simple (so, strictly speaking, we're using JAX, not AJAX).
  • Second, we take advantage of the built-in multi-format support in Dream to convert the XML data returned by the address book service into JSON.  This will make it easier to process the results.

All that remains is to convert the returned addresses into markers on the map.

var result = new Function("return "+ request.transport.responseText)();
if(result && result.addressbook && result.addressbook.address) {
    for(i = 0; i < result.addressbook.address.length; ++i) {
        if(result.addressbook.address[i].address) {
            var marker = new YMarker(result.addressbook.address[i].address, 'id' + i);
 
            // Add auto expand
            var _txt = '<div style="width:160px;height:80px;">';
            _txt += '<b>Name</b>: ' + result.addressbook.address[i].first + ' ' + result.addressbook.address[i].last + '<br>';
            _txt += '<b>Phone</b>: ' + result.addressbook.address[i].phone + '<br>';
            _txt += '<b>Address</b>: ' + result.addressbook.address[i].address + '<br>';
            _txt += '</div>';
            marker.addAutoExpand(_txt);
            map.addOverlay(marker);
        }
    }
}

GET:

The last piece is to provide an easy access to the html file.  For this, we'll provide a feature that handles a simple GET operation:

[DreamFeature("GET:", "Get address-book map")]
public Yield GetHome(DreamContext context) {
    context.Redirect(_fs.At("files", "addressbook.html"));
    yield break;
}

This feature will handle all requests to another URIs by acting as a proxy.

Now we're ready to give our application a test drive.  Go to http://localhost:8081/address-book and enjoy your handy work.

Tag page

Files 5

FileSizeDateAttached by 
addressbook.html
Address-Book html page
2.13 kB22:08, 23 Mar 2007SteveBActions
 addressbook.startup.xml
Address-Book XML configuration file
335 bytes22:08, 23 Mar 2007SteveBActions
AddressBookService.cs
Address-Book service sample
6.33 kB15:08, 14 Oct 2007SteveBActions
 prototype.js
Prototype.js for html sample
46.33 kB22:08, 23 Mar 2007SteveBActions
 screenshot.png
Mashup screenshot
923.47 kB22:31, 23 Mar 2007SteveBActions
You must login to post a comment.
Powered by MindTouch Deki Enterprise Edition v.8.08 RC2