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:
Ok, that's enough goals for one sample. Let's get 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.
We need four features on our service:
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;
}
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;
}
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;
}
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 .
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 & 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:
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);
}
}
}
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.
| File | Size | Date | Attached by | |||
|---|---|---|---|---|---|---|
| addressbook.html Address-Book html page | 2.13 kB | 22:08, 23 Mar 2007 | SteveB | Actions | ||
| addressbook.startup.xml Address-Book XML configuration file | 335 bytes | 22:08, 23 Mar 2007 | SteveB | Actions | ||
| AddressBookService.cs Address-Book service sample | 6.33 kB | 15:08, 14 Oct 2007 | SteveB | Actions | ||
| prototype.js Prototype.js for html sample | 46.33 kB | 22:08, 23 Mar 2007 | SteveB | Actions | ||