Jun 2008

New Design

A new, easier to read, design for my personal site.

As much as I liked the last design for the site, it just didn't work anymore. The columns were too narrow: the code samples had to be edited to fit the narrow width and images had to be scaled fairly small to fit the narrow colum.

I designed and almost finished the RapidWeaver template almost year ago. Whilst adding recent posts with copious code examples, I saw that I really needed to get the new template used. I wanted the new design to be easy to read longer articles and typographically mature. I do think the site, is perhaps, not as distinctive, but is far less painful to read.

I'm using RapidWeaver 4.0 now, and have made a few small changes, partly due to new features in RapidWeaver, partly due to changes at Watershed and partly because I just haven't completed the template. Comments are back, iChat status and links are gone and so is Twitter messages.

|

Using RESTful WebObjects

Using requests with the WebObjects REST Handler for HTTP requests.

The previous post described building a new request handler allowing REST behaviour in WebObjects applications. This post will explain how the handler responds to specific requests. The current test application was built by simply, creating a new WebObjects application in Eclipse, adding a business objects framework and adding the REST handler. The test framework is the same framework that provides the foundation for www.watershed.co.uk so the examples will deal with films, screenings, seasons & festivals. The examples below use two Entities from the model, Exhibits and Programmes. Essentially, a Programme represents a season or festival of films. An Exhibit, generally, represents a film. There is a to-many relationship from Programme to Exhibits; i.e. A Programme contains a collection of Exhibits. This can be seen in use at the Watershed site. The reverse relationship is also modelled, so Exhibits have a to-one relationship to a Programme.

exhibit_programme

Rewriting

REST URLs describe the location of the resource in a hierarchy, so arguments are sent mimicking a directory structure: www.watershed.co.uk/cgi-bin/WebObjects/WatershedAPI.woa/REST/exhibits/1527 The request URLs are often written into executable code or synthesised from String components so keeping the URLs simple and short aids their use. URLs generated by the data modules are 'pretty' and incoming requests are rewritten in Apache so the previous URL becomes: api.watershed.co.uk/exhibits/1527.

Tools

Although not particular to our WebObjects REST Handler, using special tools can make testing much more efficient and pleasant. Whilst a web browser can be used to generate GET and POST requests they struggle with the other HTTP methods. Using CURL, from Terminal, can be useful, optionally adding headers to, or, setting the method or body of, the HTTP request. Typing man curl into Terminal will display a page explaining the options. RESTClient is a Java Swing application to test RESTful web services. It can be used to test variety of HTTP communications and has GUI options for most of the HTTP 1.1 specification, including setting the method, authentication values and the message body as well as viewing the response headers. RESTClient is a useful tool for any type of web application development.

REST URLs

URL Structure

Just as a WebObjects' component action URL has a structure, then URLs accepted by the REST Handler have a structure. Each action has a target and a method. The method is part of the HTTP specification and is explained later. The URL structure is essentially:

host/entity_name/primary_key/relationship/...

The entity name and the primary key together define the target. The entity names in the URLs follow a casual, but established, convention and are lower-case, underscore-separated and plural. So the URL /exhibits/1527 will map to an Exhibit Enterprise Object with a primary key value of 1527. Using the model described earlier, we can find the Programme with the primary key of 57 by using the URL: api.watershed.co.uk/programmes/57/ . As the entity name is underscore separated then to set the target for the MediaSet entity with a primary key of 1234, we can use the URL: api.watershed.co.uk/media_sets/1234.

The REST Handler uses the EOModels in the application, or linked frameworks, to map the URL to the correct Entity names and thence to a table in the database. The Handler then retreives the correct Enterprise Objects and delivers these targets to the appropriate data module for output in the response.

Relationships & Key paths

Objects in isolation are often of little practical use. Modelling real world environments requires a hierarchy of objects and containers. Creating a new season isn't useful unless we can insert films into it.

Once the target object has been established key paths defined in the model are then available. The relationship between Exhibit and Programme, illustrated in the diagram above, can now be used. To find the Programme of an Exhibit we use the URL: api.watershed.co.uk/exhibits/1527/programme/. We can follow the key path, defined in the EOModel, even further by using the to-many relationship back to an array of Exhibits: api.watershed.co.uk/exhibits/1527/programme/exhibits/. In a similar fashion to entity names, key paths in the model are renamed to maintain the pretty URLs. Although not illustrated, Exhibit has a to-many relationship to an array of MediaSets, named mediaSets. Using this relationship results in a URL similar to: api.watershed.co.uk/exhibits/1527/media_sets/.

Now that we have URLs that point to relationships, we can use these to add objects into and remove objects from collections.

Model Delegates

Comparing the illustration of the Entities, above, and the actual XML generated by a GET request displays an inconsistency. Although, attributes with null values are not currently listed, the attributes notes and history are never included. The REST Handler and the data modules use the model delegate to provide some information about how to render the data. To hide the notes and history attributes:

  1. public NSArray excludeAttributesForEntity(String entityName) {
  2. // Always remove notes and history regardless of
  3. // the entity
  4. return new NSArray(new String[] {"notes", "history"});
  5. }

The use of the model delegate is optional and if the delegate is not set, the entity and attribute names will be taken from the model and adjusted.

Varying Data Representations

Outlined in the last post was a technique whereby changing the Accept HTTP header changed the response data format or allowed a client to send updated data to the web application in a variety of formats. Sending a request using cURL in Terminal shows the effect of the accept header.

curl -H 'Accept: text/xml' http://api.watershed.co.uk/exhibits/1530/
produces an XML response, whilst:

curl -H 'Accept: text/plist' http://api.watershed.co.uk/exhibits/1530/
produces a response in Apple's Property List format.

Actions

As the URL specifies the resource then the HTTP method specifies the action to perform on it. The REST Handler responds to the HTTP actions defined in the 1.1 specification. Four of the HTTP methods are mapped to the CRUD actions. The currently deployed application will not receive requests other than GET, HEAD & POST. The FreeBSD web server adapter, for watershed.co.uk applications, was compiled from the Apple source code and does not support the PUT & DELETE methods. The sites will be moving to a new virtual server in the next couple of weeks and we will compile a new adapter from the WebObjects 5.4 adapter source.

To read an Exhibit from the data store:
curl -X GET http://api.watershed.co.uk/exhibits/1530/

To delete an Exhibit from the data store:
curl -X DELETE http://api.watershed.co.uk/exhibits/1530/

To update the title attribute of an Exhibit in the data store:
curl -X PUT -d "A new title</exhibit>" http://api.watershed.co.uk/exhibits/1530/</code></p> <p>To <em>create</em> a new Exhibit in the data store:<br /> <code>curl -X POST -d "<exhibit><title>A new Exhibit<title><copyText>What a wonderful…<title></copyText>" http://api.watershed.co.uk/exhibits/1530/</code></p> <p>By following the key paths in the URLs we can insert and remove objects from a relationship. This part is still very much a work in progress and is still not 'quite right'.</p> <p>To insert a new Exhibit in the data store and add it to the array of exhibits in a programme, specify the array as the target resource:<br /> <code>curl -X POST -d "<exhibit><title>A new Exhibit<title><copyText>What a wonderful…<title></copyText>"<br />http://api.watershed.co.uk/programmes/57/exhibits/</code></p> <p>To remove an Exhibit from the to many relationship of a Programme but leave it in the data store:<br /> <code>curl -X DELETE http://api.watershed.co.uk/programmes/57/exhibits/1530/</code></p> <h2>Notes and Incomplete</h2> <p>As mentioned previously, the adapter installed on the web server does not handle PUT & DELETE requests but we will compile a new version that supports these methods shortly. Manipulating relationship collections feels 'clunky' at the moment and this needs some work. The data modules, both the XML and the Property List, need significant work. A JSON data module would be a welcome addition and would compliment the XML and Property List moduels.</p> <p>One of the original reasons for REST Handler was to provide an efficient and quick data flow to a new persistence framework for Cocoa applications. We already have a framework that consumes large chunks of data from a database, we need to deliver data in a constant trickle just as it's needed. Large collections, and objects in a collection, should be available in a more terse output to reduce response times and bandwidth.</p><p class="blog-entry-tags">Tags: <a href="tag-webobjects.html" title="WebObjects" rel="tag">WebObjects</a>, <a href="tag-java.html" title="Java" rel="tag">Java</a>, <a href="tag-xml.html" title="XML" rel="tag">XML</a>, <a href="tag-web-development.html" title="web development" rel="tag">web development</a>, <a href="tag-rest.html" title="REST" rel="tag">REST</a></p><div class="blog-entry-comments"><a class="blog-comment-link" href="javascript:HaloScan('rw_unique_entry_id_42_page0');"><script type="text/javascript">postCount('rw_unique_entry_id_42_page0');</script></a> | <a class="blog-trackback-link" href="javascript:HaloScanTB('rw_unique_entry_id_42=page0');"><script type="text/javascript">postCountTB('rw_unique_entry_id_42_page0'); </script></a></div></div></div><div id="unique-entry-id-40" class="blog-entry"><h1 class="blog-entry-title"><a href="restful_webobjects.html" class="blog-permalink">RESTful WebObjects</a></h1><div class="blog-entry-date">02/06/08 10:13 Filed in: <span class="blog-entry-category"><a href="category-watershed.html">Watershed</a></span></div><div class="blog-entry-body"><p class="summary">Development of a REST request handler for WebObjects applications.</p> <p>As part of watershed.co.uk's ongoing development, we have a number of immediate requirements:</p> <ul> <li>Start building a replacement for the FBTFPersistentObjects framework used to provide object persistence and network database connectivity for Communicate.</li> <li>Vend XML and other data types using a technology with easier to implement clients than SOAP.</li> <li>Transfer data to iPhone applications efficiently using XML or binary plists.</li> <li>Do all the above across a number of different databases and business object frameworks.</li> </ul> <p>REST web services vending XML & plist data appeared a good fit for all the above but we didn't want to have to write a whole collection of direct actions and components for each site. So we are in the process of building a REST Request Handler for new and existing applications. This is still a work in progress and is only, a day or two old, but hopefully in the near future there should be useable code alongside the explanation.</p> <h2>So how do we use our REST Request Handler in applications?</h2> <p>First we create a new instance of the handler and assign it to handle particular requests. We do this in Application's constructor in Application.java</p> <ol class="code"> <li><code>RESTHandler restRequestHandler = new RESTHandler();</code></li> <li><code>restRequestHandler.setSecurityDelegate(new MyRESTSecurity());</code></li> <li><code>registerRequestHandler(restRequestHandler, RESTHandler.REQUEST_HANDLER_KEY);</code></li> </ol> <p>We also set a security delegate here which is explained below. The request handler key is "REST" so any request sent to www.domain.com/cgi-bin/WebObjects/App.woa/REST/... will be processed by our handler.</p> <p>Unfortunately, the REST Handler is not quite the simple, drop-in unit we would like. WebObjects will only create and initialise a WORequest with a HTTP GET, HEAD & POST method. Sending a HTTP DELETE or PUT action to a WebObjects application results in an exception. The solution is quite simple, subclass WORequest and return it from a re-implemented createRequest(...) in your Application class.</p> <h2>Security Delegate</h2> <p>The REST Handler can automatically provide public access to any database so as a precaution a security delegate must be implemented and added to the request handler before any requests can be accepted. The security delegate can be any object that implements the few methods declared in the security delegate interface, RESTSecurityDelegate.</p> <ol class="code"> <li><code>public interface RESTSecurityDelegate {</code></li> <li class="indent1"><code>public int requiredHTTPAuthentication(WORequest aRequest);</code></li> <li class="indent1"><code>public boolean requireSecureTransport();</code></li> <li class="indent1"><code>public boolean shouldAllowRequest(WORequest aRequest);</code></li> <li><code>}</code></li> </ol> <p>The security delegate methods are simple to implement and can be varied per request. There are some general REST authentication and authorisation practices in use and these leverage existing features of HTTP: with a secure HTTPS connection to prevent eavesdropping and use, either, HTTP Basic or Digest authentication, or, include a security token as a HTTP header key such as:</p> <p><code><pre>GET / HTTP/1.1 User-Agent: curl/7.16.3 (powerpc-apple-darwin9.0) libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3 Host: www.watershed.co.uk X-Watershed-Auth: U2FsdGVkX1zuHyuXFLEBoBjEHtQO. Accept: */*</pre></code></p> <p>Currently only HTTP Basic support is implemented and we will add Digest at a later date. The REST Handler will first check if it is allowed receive requests over plain HTTP by calling requireSecureTransport(). If the return value is true then the handler will redirect the client to a secure connection. Next the REST Handler asks the delegate, using <code>requiredHTTPAuthentication()</code>, if the request will require authentication and, if the client hasn't provided any, will ask the client to provide authentication. Currently, only Basic HTTP authentication is supported. Finally, the delegate decides if the handler should process the request in the <code>shouldAllowRequest(WORequest aRequest)</code> method.</p> <p>The listing below illustrates a possible implementation of the required methods.</p> <ol class="code"> <li><code>public class SecurityDelegate implements RESTSecurityDelegate {</code></li> <li><code></code></li> <li class="indent1"><code>public int requiredHTTPAuthentication(WORequest aRequest) {</code></li> <li class="indent2"><code class="comment">// Give everyone read-only access</code></li> <li class="indent2"><code>if (aRequest.method().equals("GET")) return SecurityDelegate.AUTHENTICATE_NONE;</code></li> <li class="indent2"><code>return SecurityDelegate.AUTHENTICATE_BASIC;</code></li> <li class="indent1"><code>}</code></li> <li><code></code></li> <li class="indent1"><code>public boolean requireSecureTransport() {</code></li> <li class="indent2"><code>return false;</code></li> <li class="indent1"><code>}</code></li> <li><code></code></li> <li class="indent1"><code>public boolean shouldAllowRequest(WORequest aRequest) {</code></li> <li class="indent2"><code class="comment">// Give everyone read-only access</code></li> <li class="indent2"><code>if (aRequest.method().equals("GET")) return true;</code></li> <li><code></code></li> <li class="indent2"><code class="comment">// A user must be authenticated to change resources</code></li> <li class="indent2"><code>if (aRequest.headerForKey("authorization") != null) {</code></li> <li class="indent3"><code>String encodedAuth = aRequest.headerForKey("authorization");</code></li> <li class="indent3"><code>String decodedAuth = null;</code></li> <li class="indent3"><code>BASE64Decoder decoder = new BASE64Decoder();</code></li> <li class="indent3"><code class="comment">//encoded string starts after "Basic " </code></li> <li class="indent3"><code>encodedAuth=encodedAuth.substring(encodedAuth.indexOf(" ")+1); </code></li> <li class="indent3"><code>try{</code></li> <li class="indent4"><code>decodedAuth=new String(decoder.decodeBuffer((new ByteArrayInputStream (encodedAuth.getBytes())))); </code></li> <li class="indent3"><code>} catch(IOException ex) {} </code></li> <li><code></code></li> <li class="indent3"><code>String tokens[] = decodedAuth.split(":");</code></li> <li class="indent3"><code>if(tokens.length != 2) return false;</code></li> <li><code></code></li> <li class="indent3"><code class="comment">// Check the username & password</code></li> <li class="indent3"><code>if (tokens[0].equals("benjamin") && tokens[1].equals("mypassword")) return true;</code></li> <li class="indent2"><code>}</code></li> <li class="indent2"><code>return false;</code></li> <li class="indent1"><code>}</code></li> <li><code></code></li> <li><code>}</code></li> </ol> <h2>Data Modules</h2> <p>Once the security delegate has approved the request, a data module is then invoked with the task of handling of the request. Using a number of installed data modules allow the application to vend REST web services in any number of different data formats. Data modules render Enterprise Objects out in a response and update, or insert, EO's from parsed data in a request. The data module also deletes EO's from the store for some requests. Each data module implements an interface based on HTTP actions and the REST Handler invokes a method passing the original WORequest and the target EO, or array of EO's.</p> <p>Currently, the REST Handler selects an appropriate data module based on the Accept HTTP header sent in the request by the client. It was assumed that clients would have fine grained control over the request and the client would not always be a browser window so the REST Handler treats each acceptable MIME Type as HTTP 1.1 would, MIME Types are listed in order of preference: most desirable first. There is one exception to the HTTP 1.1 specification; the relative quality parameter is currently ignored. If a header such as <code>Accept: image/jpeg, video/mp4, text/xml;q=0.9, text/json, text/plist, */*</code> were received then the request handler would look first for a module that handled JPEG images and so on.</p> <p>Each available data module is mapped to one or more MIME types. Currently we are just using a particular XML data module for testing and in the process of writing a plist module for use with Cocoa desktop and iPhone clients. A module is not required to support all four actions create, read update, and delete. Extra modules, such as JSON or vCard can be written relatively quickly and registered with the REST handler before accepting requests. There currently isn't a way to request a particular data format based upon the 'file' suffix in the request.</p> <ol class="code"> <li><code>restRequestHandler.registerDataModuleForMIMEType(new DataModuleXML(), "*/*");</code></li> </ol> <h2>Model Delegate</h2> <p>The REST Handler will pass the request and the target Enterprise Objects to the data module but on some occasions we may not want make every entity available to the public. We may also want to make some attributes, such as private internal notes, unavailable. When a request needs to render 10,000 objects to XML we may not need to include every attribute, just an essential subset. An optional model delegate provides the REST Handler and the data modules with answers and information they need to handle the requests efficiently. If the model delegate is not provided then the handler use the default model group.</p> <ol class="code"> <li><code>public interface RESTModelDelegate { </code></li> <li class="indent1"><code>public boolean cacheModelAdjustments();</code></li> <li class="indent1"><code>public boolean includeEntity(WORequest aRequest, String entityName);</code></li> <li><code></code></li> <li class="indent1"><code>public NSArray removeAttributesForEntity(String entityName);</code></li> <li class="indent1"><code>public NSArray includeExtraAttributesForEntity(String entityName);</code></li> <li><code></code></li> <li class="indent1"><code>public NSArray attributesForCollection(String entityName);</code></li> <li class="indent1"><code>public NSArray attributesForObject(String entityName);</code></li> <li><code></code></li> <li class="indent1"><code>public String aliasForEntity(String entityName);</code></li> <li><code></code></li> <li class="indent1"><code>public String labelForEntity(String entityName);</code></li> <li class="indent1"><code>public String labelForEntities(String entityName);</code></li> <li class="indent1"><code>public String labelForProperty(String propertyName);</code></li> <li><code>}</code></li> </ol> <h2>To be Implemented…</h2> <p>The REST Handler has only had a handful of train journeys of development so there are plenty of rough edges. It's currently difficult to adjust or influence the target enterprise objects provided to the data modules which would be useful for situations where we may not want to include all the available films, just the ones screening this month. There is no HTTP Digest authentication and the ability to specify query strings, such as <code>commence<2007-05-29</code> or <code>isPublic=true</code>, in the URL or a request header would be very useful. There is still plenty of work to be done until the REST Handler is complete but it will be a valuable tool as we make architectural, storage and communication changes in watershed.co.uk and dshed.net.</p><p class="blog-entry-tags">Tags: <a href="tag-webobjects.html" title="WebObjects" rel="tag">WebObjects</a>, <a href="tag-web-development.html" title="web development" rel="tag">web development</a>, <a href="tag-java.html" title="Java" rel="tag">Java</a>, <a href="tag-xml.html" title="XML" rel="tag">XML</a>, <a href="tag-rest.html" title="REST" rel="tag">REST</a></p><div class="blog-entry-comments"><a class="blog-comment-link" href="javascript:HaloScan('rw_unique_entry_id_40_page0');"><script type="text/javascript">postCount('rw_unique_entry_id_40_page0');</script></a> | <a class="blog-trackback-link" href="javascript:HaloScanTB('rw_unique_entry_id_40=page0');"><script type="text/javascript">postCountTB('rw_unique_entry_id_40_page0'); </script></a></div></div></div> </div> </div> <!-- End Content --> <div id="controls"> <h2><span class="t-down">odds</span>&<span class="t-up">ends…</span><br /><span class="subTitle">Benjamin Miller</span></h2> <div id="mainNavigation"><ul><li><a href="../" rel="self" id="current">Blog</a></li><li><a href="../links/" rel="self">Links</a></li><li><a href="../me/" rel="self">Me</a></li></ul></div> <div id="blog-categories"><div class="blog-category-link-disabled">Personal</div><a href="category-watershed.html" class="blog-category-link-enabled">Watershed</a><br /><a href="category-other-work.html" class="blog-category-link-enabled">Other Work</a><br /><a href="category-dshed.html" class="blog-category-link-enabled">dShed</a><br /><div class="blog-category-link-disabled">Church Hall</div><a href="category-this-site.html" class="blog-category-link-enabled">This Site</a><br /><a href="category-software.html" class="blog-category-link-enabled">Software</a><br /><div class="blog-category-link-disabled">Web de</div></div><div id="blog-archives"><a class="blog-archive-link-enabled" href="archive-feb-2009.html">Feb 2009</a><br /><a class="blog-archive-link-enabled" href="archive-jan-2009.html">Jan 2009</a><br /><a class="blog-archive-link-enabled" href="archive-aug-2008.html">Aug 2008</a><br /><a class="blog-archive-link-enabled" href="archive-jul-2008.html">Jul 2008</a><br /><a class="blog-archive-link-enabled" href="archive-jun-2008.html">Jun 2008</a><br /><a class="blog-archive-link-enabled" href="archive-jan-2008.html">Jan 2008</a><br /><a class="blog-archive-link-enabled" href="archive-jul-2007.html">Jul 2007</a><br /><a class="blog-archive-link-enabled" href="archive-jun-2007.html">Jun 2007</a><br /><a class="blog-archive-link-enabled" href="archive-jan-2007.html">Jan 2007</a><br /><a class="blog-archive-link-enabled" href="archive-jul-2006.html">Jul 2006</a><br /><a class="blog-archive-link-enabled" href="archive-jun-2006.html">Jun 2006</a><br /><a class="blog-archive-link-enabled" href="archive-may-2006.html">May 2006</a><br /><a class="blog-archive-link-enabled" href="archive-apr-2006.html">Apr 2006</a><br /><a class="blog-archive-link-enabled" href="archive-feb-2006.html">Feb 2006</a><br /><a class="blog-archive-link-enabled" href="archive-jan-2006.html">Jan 2006</a><br /><a class="blog-archive-link-enabled" href="archive-dec-2005.html">Dec 2005</a><br /><a class="blog-archive-link-enabled" href="archive-nov-2005.html">Nov 2005</a><br /></div><ul class="blog-tag-cloud"><li><a href="tag-3gp.html" title="3GP" class="blog-tag-size-6" rel="tag">3GP</a></li> <li><a href="tag-applescript.html" title="AppleScript" class="blog-tag-size-7" rel="tag">AppleScript</a></li> <li><a href="tag-art.html" title="Art" class="blog-tag-size-5" rel="tag">Art</a></li> <li><a href="tag-bluetooth.html" title="Bluetooth" class="blog-tag-size-5" rel="tag">Bluetooth</a></li> <li><a href="tag-cocoa.html" title="Cocoa" class="blog-tag-size-10" rel="tag">Cocoa</a></li> <li><a href="tag-core-audio.html" title="Core Audio" class="blog-tag-size-4" rel="tag">Core Audio</a></li> <li><a href="tag-core-data.html" title="Core Data" class="blog-tag-size-4" rel="tag">Core Data</a></li> <li><a href="tag-core-image.html" title="Core Image" class="blog-tag-size-6" rel="tag">Core Image</a></li> <li><a href="tag-eof.html" title="EOF" class="blog-tag-size-4" rel="tag">EOF</a></li> <li><a href="tag-iphone.html" title="iPhone" class="blog-tag-size-3" rel="tag">iPhone</a></li> <li><a href="tag-java.html" title="Java" class="blog-tag-size-8" rel="tag">Java</a></li> <li><a href="tag-plug-in.html" title="Plug-In" class="blog-tag-size-8" rel="tag">Plug-In</a></li> <li><a href="tag-print.html" title="Print" class="blog-tag-size-3" rel="tag">Print</a></li> <li><a href="tag-quicktime.html" title="QuickTime" class="blog-tag-size-9" rel="tag">QuickTime</a></li> <li><a href="tag-rapidweaver.html" title="RapidWeaver" class="blog-tag-size-10" rel="tag">RapidWeaver</a></li> <li><a href="tag-rest.html" title="REST" class="blog-tag-size-7" rel="tag">REST</a></li> <li><a href="tag-safari.html" title="Safari" class="blog-tag-size-2" rel="tag">Safari</a></li> <li><a href="tag-unfuddle.html" title="unfuddle" class="blog-tag-size-2" rel="tag">unfuddle</a></li> <li><a href="tag-web-development.html" title="web development" class="blog-tag-size-10" rel="tag">web development</a></li> <li><a href="tag-web-kit.html" title="Web Kit" class="blog-tag-size-1" rel="tag">Web Kit</a></li> <li><a href="tag-webobjects.html" title="WebObjects" class="blog-tag-size-9" rel="tag">WebObjects</a></li> <li><a href="tag-wwdc.html" title="WWDC" class="blog-tag-size-1" rel="tag">WWDC</a></li> <li><a href="tag-xml.html" title="XML" class="blog-tag-size-7" rel="tag">XML</a></li> </ul> <div id="blog-rss-feeds"><a class="blog-rss-link" href="feed.xml" rel="alternate" type="application/rss+xml" title="Odds & Ends">RSS Feed</a><br /><a class="blog-comments-rss-link" href="http://www.haloscan.com/members/rss.php?user=benjaminmiller">Comments Feed</a></div> <!--<script type="text/javascript" src="http://www.makepovertyhistory.org/whiteband_small_right.js"> </script><noscript><a href="http://www.makepovertyhistory.org/"> http://www.makepovertyhistory.org</a></noscript> <p>Posts that contain <a href="http://technorati.com/search/%22watershed+media+centre%22">"watershed Media Centre"</a> per day for the last 30 days.<br /><a href="http://technorati.com/search/%22watershed+media+centre%22"><img src="http://technorati.com/chartimg/%28%22watershed%20media%20centre%22%29?totalHits=23&size=s&days=30" style="border:0" alt="Technorati Chart" /> </a></p>--> </div> <!-- End Controls --> <div id="footer"><p><a href="/benjamin_miller/odds_ends/files/feed.xml" title="Subscribe to the Rss News Feed">rss feed</a> & <a href="/benjamin_miller/odds_ends/colophon/index.html" title="How and what is used to make this site">colophon…</p></div> </div> <script type="text/javascript" src="http://cetrk.com/pages/scripts/0008/8709.js"> </script> <!-- Start Google Analytics --> <script type="text/javascript"> var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www."); document.write(unescape("%3Cscript src='" + gaJsHost + "google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E")); </script> <script type="text/javascript"> var pageTracker = _gat._getTracker("UA-200933-2"); pageTracker._initData(); pageTracker._trackPageview(); </script><!-- End Google Analytics --></body> </html>