Commits

jus...@basho.com  committed 3b736ba Merge

merge binary buckety goodness

  • Participants
  • Parent commits a07a3d9, 3f7ad6b

Comments (0)

Files changed (34)

 Christopher Brown
 Tuncer Ayaz
 Rusty Klophaus
+Jay Doane
 

File client_lib/java/src/com/basho/riak/JiakClient.java

 import java.io.OutputStreamWriter;
 import java.net.URL;
 import java.net.URLConnection;
+import java.net.URLEncoder;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 		final HashMap<String, String> reqHeaders = new HashMap<String, String>();
 		reqHeaders.put("Content-Type", "application/json");
 		reqHeaders.put("Accept", "application/json");
-		final String reqURI = makeURI(object.getBucket() + "/"
-				+ object.getKey() + "?returnbody=true");
+		final String reqURI = makeURI(object.getBucket(),
+                                              object.getKey(),
+                                              "?returnbody=true");
 		final HttpURLConnection requestConn = doRequest("PUT", reqURI, object
 				.toJSONObject(), reqHeaders);
 		final JSONObject updated = expect(200, requestConn);
 		final HashMap<String, String> reqHeaders = new HashMap<String, String>();
 		reqHeaders.put("Content-Type", "application/json");
 		reqHeaders.put("Accept", "application/json");
-		final String reqURI = makeURI(bucket + "/" + key);
+		final String reqURI = makeURI(bucket, key);
 		final HttpURLConnection requestConn = doRequest("GET", reqURI, null,
 				reqHeaders);
 		if (requestConn.getResponseCode() == 404)
 	 *             If an error occurs during communication with the Riak server.
 	 * @throws JiakException
 	 *             If the Riak server returns an error or or an unexpected
-	 *             response code.
+	 *             response code.,
 	 */
 	public void delete(final String bucket, final String key)
 			throws IOException, JiakException {
-		final String reqURI = makeURI(bucket + "/" + key);
+		final String reqURI = makeURI(bucket, key);
 		final HashMap<String, String> reqHeaders = new HashMap<String, String>();
 		reqHeaders.put("Accept", "*/*");
 		final HttpURLConnection requestConn = doRequest("DELETE", reqURI, null,
 	 *            <code>bucket,tag-spec,accumulateFlag</code> The
 	 *            <code>tag-spec "_"</code> matches all tags.
 	 *            <code>accumulateFlag</code> is either the String "1" or "0".
-	 * @return An <code>ArrayList</code> of <code>ArrayLists</code>, where each
+	 * @return A <code>List</code> of <code>Lists</code>, where each
 	 *         sub-list corresponds to a <code>walkSpec</code> element that had
 	 *         <code>accumulateFlag</code> equal to 1.
 	 * @throws IOException
 	 *             If the Riak server returns an error or unexpected response
 	 *             code.
 	 */
-	public ArrayList<ArrayList<JiakObject>> walk(final String bucket,
+	public List<? extends List<JiakObject>> walk(final String bucket,
 			final String key, final String walkSpec) throws IOException,
 			JSONException, JiakException {
 		final ArrayList<ArrayList<JiakObject>> results = new ArrayList<ArrayList<JiakObject>>();
-		final String reqURI = makeURI(bucket + "/" + key + "/" + walkSpec);
+		final String reqURI = makeURI(bucket, key, walkSpec);
 		final Map<String, String> reqHeaders = new HashMap<String, String>();
 		reqHeaders.put("Accept", "application/json");
 		final HttpURLConnection requestConn = doRequest("GET", reqURI, null,
 		return results;
 	}
 
+	public List<? extends List<JiakObject>> walk(final String bucket,
+			final String key, final JiakWalkSpec walkSpec) throws IOException,
+			JSONException, JiakException {
+		return walk(bucket, key, walkSpec.toString());
+        }
+
 	protected JSONObject expect(final int responseCode,
 			final HttpURLConnection connection) throws JSONException,
 			JiakException, IOException {
 		throw new JiakException(connection.getResponseMessage());
 	}
 
-	protected String makeURI(final String path) {
-		return "http://" + ip + ":" + port + prefix + path;
+	protected String makeURI(final String bucket) {
+		return "http://" + ip + ":" + port + prefix + URLEncoder.encode(bucket);
+	}
+	protected String makeURI(final String bucket, final String key) {
+		return makeURI(bucket) + "/" + URLEncoder.encode(key);
+	}
+	protected String makeURI(final String bucket, final String key, final String extra) {
+		return makeURI(bucket, key) + "/" + extra;
 	}
 
 	protected static void writeRequestBody(final URLConnection connection,

File client_lib/java/src/com/basho/riak/JiakTest.java

 		client.store(jLeaf1);
 		client.store(jLeaf2);
 		client.store(jLeaf3);
-		ArrayList<ArrayList<JiakObject>> res = client.walk("jiak_example", "jroot", "jiak_example,tag_one,1");
-		for (ArrayList<JiakObject> i : res) {
+		List<? extends List<JiakObject>> res = client.walk("jiak_example", "jroot", "jiak_example,tag_one,1");
+		for (List<JiakObject> i : res) {
 			for (JiakObject j: i) {
 				assert(j.get("foo").equals("in results"));
 			}

File client_lib/java/src/com/basho/riak/JiakWalkSpec.java

+/*
+This file is provided to you under the Apache License,
+Version 2.0 (the "License"); you may not use this file
+except in compliance with the License.  You may obtain
+a copy of the License at
+   http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.  
+ */
+package com.basho.riak;
+
+import java.net.URLEncoder;
+import java.util.ArrayList;
+import java.util.StringTokenizer;
+
+/* JiakWalkSpecStep is the internal representation of a JiakWalkSpec
+ * segment.  It should not be used directly.
+ */
+class JiakWalkSpecStep {
+    public final String bucket;
+    public final String tag;
+    public final String accumulateFlag;
+
+    public JiakWalkSpecStep(final String b, final String t, final String a) {
+        bucket = b; tag = t; accumulateFlag = a;
+    }
+}
+
+/**
+ * Tool for building Jiak/Jaywalker specs.  Using this class to
+ * specify walk specs ensures that bucket and tag names will be
+ * properly URL-escaped.
+ *
+ * @author Bryan Fink <bryan@basho.com>
+ * @version 0.1
+ */
+public class JiakWalkSpec extends ArrayList<JiakWalkSpecStep> {
+    /**
+     * The "don't care" signifier.  Pass this as the bucket, tag,
+     * or accumulate flag to select the default, or match-all option.
+     */
+    public static final String WILDCARD = "_";
+
+    /**
+     * Create an empty Jiak walk spec.
+     */
+    public JiakWalkSpec() {
+        super();
+    }
+
+    /**
+     * Append a step to this walk spec.
+     *
+     * @param bucket
+     *            The bucket of the step, or the wildcard.
+     * @param tag
+     *            The tag of the step, or the wildcard.
+     * @param accumulateFlag
+     *            The string "1" to force this step to be accumulated
+     *            in the results.  "0" to force this step not to be
+     *            accumulated.  WILDCARD to accept the default
+     *            accumulation setting ("yes" for the last step, "no"
+     *            for all others).
+     */
+    public void addStep(final String bucket,
+                        final String tag,
+                        final String accumulateFlag) {
+        this.add(new JiakWalkSpecStep(bucket, tag, accumulateFlag));
+    }
+
+    /**
+     * Append a step to this walk spec.  Same as the other addStep
+     * function, but allows the use of a boolean instead of a string
+     * for the accumulateFlag parameter.
+     */
+    public void addStep(final String bucket,
+                        final String tag,
+                        boolean accumulateFlag) {
+        addStep(bucket, tag, accumulateFlag ? "1" : "0");
+    }
+
+    /**
+     * Append a step to this walk spec, and take the default option
+     * for the accumulate flag.
+     */
+    public void addStep(final String bucket, final String tag) {
+        addStep(bucket, tag, WILDCARD);
+    }
+
+    /**
+     * Convert this walk step to a string.  All bucket and tag names
+     * will be URL-escaped in the return value.
+     */
+    public String toString() {
+        StringBuilder result = new StringBuilder();
+        for (JiakWalkSpecStep s : this) {
+            result.append(URLEncoder.encode(s.bucket));
+            result.append(',');
+            result.append(URLEncoder.encode(s.tag));
+            result.append(',');
+            result.append(s.accumulateFlag);
+            result.append('/');
+        }
+        return result.toString();
+    }
+}

File client_lib/jiak.js

     else
         req.type = 'PUT';
     
-    req.url = this.baseurl+Object.bucket+'/';
+    req.url = this.path(Object.bucket);
     if (Object.key) req.url += Object.key;
     
     if (!(this.opts.noReturnBody || NoReturnBody))
 
 JiakClient.prototype.fetch = function(Bucket, Key, Callback) {
     return $.ajax({
-        url:      this.baseurl+Bucket+'/'+Key,
+        url:      this.path(Bucket, Key),
         dataType: "json",
         success:  Callback
     });
 JiakClient.prototype.remove = function(Bucket, Key, Callback) {
     return $.ajax({
         type:    'DELETE',
-        url:     this.baseurl+Bucket+'/'+Key,
+        url:     this.path(Bucket, Key),
         success: Callback
     });
 }
     // Start can be either and object with {bucket:B, key:K}
     // or a list with [Bucket, Key, ...]
     if ('bucket' in Start)
-        req.url = this.baseurl+Start.bucket+'/'+Start.key+'/';
+        req.url = this.path(Start.bucket, Start.key)+'/';
     else
-        req.url = this.baseurl+Start[0]+'/'+Start[1]+'/';
+        req.url = this.path(Start[0], Start[1])+'/';
 
     // Spec should be a list of objects with
     //    {bucket:B, tag:T, acc:A}
     //   false to have them excluded from the response (always true
     //   for the last step
     for (i in Spec) {
-        req.url += (Spec[i].bucket||'_')+','+
-            (Spec[i].tag||'_')+','+
+        req.url += encodeURIComponent(Spec[i].bucket||'_')+','+
+            encodeURIComponent(Spec[i].tag||'_')+','+
             ((Spec[i].acc || i == Spec.length-1) ? '1' : '_')+'/';
     }
 
 
     $.ajax({
         type:        'PUT',
-        url:         this.baseurl+Bucket,
+        url:         this.path(Bucket),
         contentType: 'application/json',
         data:        JSON.stringify({schema:Schema}),
         success:     Callback ? function() { Callback(true); } : undefined,
         error:       Callback ? function() { Callback(false); } : undefined
     });
+}
+
+JiakClient.prototype.path = function(Bucket, Key) {
+    var p = this.baseurl;
+    if (Bucket) {
+        p += encodeURIComponent(Bucket)+'/';
+        if (Key)
+            p += encodeURIComponent(Key);
+    }
+    return p;
 }

File client_lib/jiak.php

         if ($read_mask == null)
             $read_mask = $allowed_fields;
         return $this->_expect(204,
-            $this->_do_req("PUT", $this->JKP . $bucket,
+            $this->_do_req("PUT", $this->JKP . urlencode($bucket),
                 json_encode( array (
                     "schema" => array (
                         "allowed_fields" => $allowed_fields,
     }
     
     function bucket_info($bucket) {
-        return $this->_expect(200, $this->_do_req("GET", $this->JKP . $bucket));
+        return $this->_expect(200,
+            $this->_do_req("GET", $this->JKP . urlencode($bucket)));
     }
     
     function store_all($ar) {
     function store($obj) {
         $new_data = $this->_expect(200,
             $this->_do_req("PUT",
-                $this->JKP . $obj->bucket . "/" . $obj->key .
-                "?returnbody=true",
+                $this->JKP . urlencode($obj->bucket) . "/" .
+                urlencode($obj->key) . "?returnbody=true",
                 $obj->to_json(),
                 array("Content-type: application/json;charset=UTF-8")));
         $obj->update($new_data);
     }
     
     function fetch($bucket, $key) {
-        $resp = $this->_do_req("GET", $this->JKP . $bucket . "/" . $key);
+        $resp = $this->_do_req("GET",
+                    $this->JKP . urlencode($bucket) . "/" . urlencode($key));
         if ($resp['http_code'] == 404) {
             return null;
         }
     
     function delete($bucket, $key) {
         $resp = $this->_do_req("DELETE", $this->JKP .
-            $bucket . "/" . $key);
+                    urlencode($bucket) . "/" . urlencode($key));
         $http_code = $resp['http_code'];
         if ($http_code == 404) return false;
         else if ($http_code == 204) return true;
     }
     
     function walk($bucket, $key, $spec) {
-        $resp = $this->_do_req("GET", $this->JKP . $bucket . "/" . $key . "/" .
-            $this->_convert_walk_spec($spec));
+        $resp = $this->_do_req("GET", $this->JKP . urlencode($bucket) .
+            "/" . urlencode($key) . "/" . $this->_convert_walk_spec($spec));
         
         if ($resp['http_code'] == 404) {
             return array();
     }
     
     function _convert_walk_spec($spec) {
-        $s = "/";
+        $s = "";
         foreach($spec as $el) {
-            $s .= $el[0] . "," . $el[1] . "," . $el[2] . ",";
+            $s .= urlencode($el[0]) . "," . urlencode($el[1]) . "," . $el[2] . "/";
         }
-        $s = str_replace(",\n", "", $s."\n");
         return $s;
     }
 }

File client_lib/jiak.py

 # under the License.    
 
 import httplib
+import urllib
 try:
     import json
 except ImportError:
                                       'write_mask': write_mask,
                                       'read_mask': read_mask}})
         Resp = self._do_req("PUT",
-                            self.JKP + Bucket,
+                            self.JKP + urllib.quote_plus(Bucket),
                             Body,
                             {"Content-Type": "application/json"})
         if Resp.status == 204:
             return None
         raise JiakException(Resp.status, Resp.reason, Resp.read())
     def list_bucket(self, Bucket):
-        return self._expect(200, self._do_req("GET", self.JKP + Bucket))
+        return self._expect(200,
+                 self._do_req("GET", self.JKP + urllib.quote_plus(Bucket)))
     def store(self, JObj):
         NewData = self._expect(200,
                      self._do_req("PUT",
-                                  self.JKP + JObj.bucket + "/" + JObj.key
-                                     + "?returnbody=true",
+                                  self.JKP
+                                  + urllib.quote_plus(JObj.bucket) + "/"
+                                  + urllib.quote_plus(JObj.key)
+                                  + "?returnbody=true",
                                   JObj.to_json(),
                                   {"Content-Type": "application/json"}))
         JObj.update(NewData)
     def fetch(self, bucket, key):
-        Resp = self._do_req("GET", self.JKP + bucket + "/" + key)
+        Resp = self._do_req("GET",
+                            self.JKP + urllib.quote_plus(bucket)
+                            + "/" + urllib.quote_plus(key))
         if Resp.status == 404:
             return None
         Data = self._expect(200,Resp)
         Obj.update(Data)
         return Obj
     def delete(self, bucket, key):
-        Resp = self._do_req("DELETE", self.JKP + bucket + "/" + key)
+        Resp = self._do_req("DELETE",
+                            self.JKP + urllib.quote_plus(bucket)
+                            + "/" + urllib.quote_plus(key))
         if Resp.status == 404:
             return None
         elif Resp.status == 204:
         # if the walk succeeds, this will return a list, where each
         #  element is a list of JiakObjects corresponding to a spec
         #  element that had acc == "1"
-        Resp = self._do_req("GET", self.JKP + bucket + "/" + key + "/"
+        Resp = self._do_req("GET",
+                            self.JKP + urllib.quote_plus(bucket)
+                            + "/" + urllib.quote_plus(key) + "/"
                             + _convert_walk_spec(spec))
         if Resp.status == 404:
             return None
     return O
 
 def _convert_walk_spec(spec):
-    return "/".join([b + "," + t + "," + a for (b,t,a) in spec])
+    return "/".join([urllib.quote_plus(b) + "," + urllib.quote_plus(t)
+                     + "," + a for (b,t,a) in spec])
 
 class JiakObject:
     def __init__(self, bucket, key, links=None, obj=None):

File client_lib/jiak.rb

+#!/usr/bin/env ruby
+#     This file is provided to you under the Apache License,
+#     Version 2.0 (the "License"); you may not use this file
+#     except in compliance with the License.  You may obtain
+#     a copy of the License at
+   
+#       http://www.apache.org/licenses/LICENSE-2.0
+   
+#     Unless required by applicable law or agreed to in writing,
+#     software distributed under the License is distributed on an
+#     "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#     KIND, either express or implied.  See the License for the
+#     specific language governing permissions and limitations
+#     under the License.    
+
+# A Ruby interface for speaking to Riak.
+# Example usage code can be found at the end of this file.
+# This library requires that you have a library providing 'json'
+# installed ("gem install json" will get you one).
+require 'net/http'
+require 'rubygems'
+require 'json'
+
+class JiakClient
+  def initialize(ip, port, jiakPrefix='/jiak/')
+    @ip = ip
+    @port = port
+    @prefix = jiakPrefix
+  end
+
+  # Set the schema for 'bucket'.  The schema parameter
+  # must be a hash with at least an 'allowed_fields' field.
+  # Other valid fields are 'requried_fields', 'read_mask',
+  # and 'write_mask'
+  def set_bucket_schema(bucket, schema)
+    if (!schema['required_fields'])
+      schema['required_fields'] = []
+    end
+    if (!schema['read_mask'])
+      schema['read_mask'] = schema['allowed_fields']
+    end
+    if (!schema['write_mask'])
+      schema['write_mask'] = schema['read_mask']
+    end
+
+    do_req(set_data(Net::HTTP::Put.new(path(bucket)),
+                    {'schema'=>schema}),
+           '204')
+  end
+
+  # Get the schema and key list for 'bucket'
+  def list_bucket(bucket)
+    do_req(Net::HTTP::Get.new(path(bucket)), '200')
+  end
+  
+  # Get the object stored in 'bucket' at 'key'
+  def fetch(bucket, key)
+    do_req(Net::HTTP::Get.new(path(bucket, key)), '200')
+  end
+
+  # Store 'object' in Riak.  If the object has not defined
+  # its 'key' field, a key will be chosen for it by the server.
+  def store(object)
+    if (object['key'])
+      req = Net::HTTP::Put.new(path(object['bucket'], object['key'])+
+                               '?returnbody=true')
+      code = '200'
+    else
+      req = Net::HTTP::Post.new(path(object['bucket'])+'?returnbody=true')
+      code = '201'
+    end
+
+    do_req(set_data(req, object), code)
+  end
+
+  # Delete the data stored in 'bucket' at 'key'
+  def delete(bucket, key)
+    do_req(Net::HTTP::Delete.new(path(bucket, key)), '204')
+  end
+
+  # Follow links from the object stored in 'bucket' at 'key'
+  # to other objects.  The 'spec' parameter should be an array
+  # of hashes, each hash optinally defining 'bucket', 'tag',
+  # and 'acc' fields.  If a field is not defined in a spec hash,
+  # the wildcard '_' will be used instead.
+  def walk(bucket, key, spec)
+    do_req(Net::HTTP::Get.new(path(bucket, key)+convert_walk_spec(spec)),
+           '200')
+  end
+
+  def convert_walk_spec(spec)
+    acc = ''
+    spec.each do |step|
+      acc += URI.encode(step['bucket']||'_')+','+
+        URI.encode(step['tag']||'_')+','+
+        (step['acc']||'_')+'/'
+    end
+    acc
+  end
+
+  def path(bucket, key='')
+    p = @prefix + URI.encode(bucket) + '/'
+    if (key != '')
+      p += URI.encode(key) + '/'
+    end
+    p
+  end
+
+  def set_data(req, data)
+    req.content_type='application/json'
+    req.body=JSON.generate(data)
+    req
+  end
+
+  def do_req(req, expect)
+    res = Net::HTTP.start(@ip, @port) {|http|
+      http.request(req)
+    }
+    if (res.code == expect)
+      res.body ? JSON.parse(res.body) : true
+    else
+      raise JiakException.new(res.code+' '+res.message+' '+res.body)
+    end
+  end
+  private:convert_walk_spec
+  private:path
+  private:set_data
+  private:do_req
+end
+
+class JiakException<Exception
+ end
+
+# Example usage
+if __FILE__ == $0
+  jc = JiakClient.new("127.0.0.1", 8000)
+
+  puts "Creating bucket foo..."
+  jc.set_bucket_schema('foo', {'allowed_fields'=>['bar','baz']})
+  b = jc.list_bucket('foo')
+  puts "Bucket foo's schema: "
+  p b['schema']
+
+  puts "Creating object..."
+  o = jc.store({'bucket'=>'foo',
+                 'object'=>{'bar'=>'hello'},
+                 'links'=>[]})
+  puts "Created at "+o['key']
+
+  puts "Creating link..."
+  jc.store({'bucket'=>'foo',
+             'key'=>'my known key',
+             'object'=>{'baz'=>'good'},
+             'links'=>[['foo',o['key'],'tagged']]})
+  puts "Following link..."
+  objs = jc.walk('foo', 'my known key', [{'bucket'=>'foo'}])
+  puts "Made it back to: "
+  p objs['results'][0][0]
+
+  puts "Modifying object..."
+  mo = jc.fetch('foo', 'my known key')
+  mo['object']['bar']=42
+  jc.store(mo)
+  puts "Stored."
+
+  puts "Deleting foo/"+o['key']+"..."
+  jc.delete('foo', o['key'])
+  puts "Deleted."
+end

File demo/stickynotes/priv/www/js/jiak.js

 //      Client.store(note);
 //   });
 //
-//   Client.store({'bucket':'note'
+//   Client.store({'bucket':'note',
 //                 'object':{'text':'a new note'},
 //                 'links':[]},
 //                function(note) {
-//                  alert('new note's key: '+note.key);
+//                  alert("new note's key: "+note.key);
 //                });
 //
 //   Client.walk(['note', '456'],
 //               [{'bucket':'person', 'tag':'author'}],
 //               function(data) {
 //                 var authors = data.results[0];
-//                 alert('note's author is: '+
+//                 alert("note's author is: "+
 //                       authors[0].object.name);
 //               });
+
+// This file is provided to you under the Apache License,
+// Version 2.0 (the "License"); you may not use this file
+// except in compliance with the License.  You may obtain
+// a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.    
+
+
 function JiakClient(BaseUrl, Opts) {
     this.baseurl = BaseUrl;
     if (!(this.baseurl.slice(-1) == '/'))
     else
         req.type = 'PUT';
     
-    req.url = this.baseurl+Object.bucket+'/';
+    req.url = this.path(Object.bucket);
     if (Object.key) req.url += Object.key;
     
     if (!(this.opts.noReturnBody || NoReturnBody))
 
 JiakClient.prototype.fetch = function(Bucket, Key, Callback) {
     return $.ajax({
-        url:      this.baseurl+Bucket+'/'+Key,
+        url:      this.path(Bucket, Key),
         dataType: "json",
         success:  Callback
     });
 JiakClient.prototype.remove = function(Bucket, Key, Callback) {
     return $.ajax({
         type:    'DELETE',
-        url:     this.baseurl+Bucket+'/'+Key,
+        url:     this.path(Bucket, Key),
         success: Callback
     });
 }
     // Start can be either and object with {bucket:B, key:K}
     // or a list with [Bucket, Key, ...]
     if ('bucket' in Start)
-        req.url = this.baseurl+Start.bucket+'/'+Start.key+'/';
+        req.url = this.path(Start.bucket, Start.key)+'/';
     else
-        req.url = this.baseurl+Start[0]+'/'+Start[1]+'/';
+        req.url = this.path(Start[0], Start[1])+'/';
 
     // Spec should be a list of objects with
     //    {bucket:B, tag:T, acc:A}
     //   false to have them excluded from the response (always true
     //   for the last step
     for (i in Spec) {
-        req.url += (Spec[i].bucket||'_')+','+
-            (Spec[i].tag||'_')+','+
+        req.url += encodeURIComponent(Spec[i].bucket||'_')+','+
+            encodeURIComponent(Spec[i].tag||'_')+','+
             ((Spec[i].acc || i == Spec.length-1) ? '1' : '_')+'/';
     }
 
 
     $.ajax({
         type:        'PUT',
-        url:         this.baseurl+Bucket,
+        url:         this.path(Bucket),
         contentType: 'application/json',
         data:        JSON.stringify({schema:Schema}),
         success:     Callback ? function() { Callback(true); } : undefined,
         error:       Callback ? function() { Callback(false); } : undefined
     });
+}
+
+JiakClient.prototype.path = function(Bucket, Key) {
+    var p = this.baseurl;
+    if (Bucket) {
+        p += encodeURIComponent(Bucket)+'/';
+        if (Key)
+            p += encodeURIComponent(Key);
+    }
+    return p;
 }

File demo/stickynotes/src/notes.erl

 %%      opportunity to act on that information.
 after_write(_Key, JiakObject, ReqData, Context) ->
     spawn(fun() ->
-                  [[_, GroupKey, _]] = jiak_object:links(JiakObject, groups),
+                  [[_, GroupKey, _]] = jiak_object:links(JiakObject, <<"groups">>),
                   {ok, C} = jiak:local_client(),
-                  {ok, G} = C:get(groups, GroupKey, 2),
+                  {ok, G} = C:get(<<"groups">>, GroupKey, 2),
                   Key = Context:get_prop(key),
-                  C:put(jiak_object:add_link(G, notes, Key, <<"note">>), 2)
+                  C:put(jiak_object:add_link(G, <<"notes">>, Key, <<"note">>), 2)
           end),
     {ok, ReqData, Context}.
 

File demo/stickynotes/src/stickynotes_sup.erl

            proplists:get_value(doorbell_port, RiakConfig),
            proplists:get_value(riak_cookie, RiakConfig)) of
         {ok, C} ->
-            C:set_bucket(notes, [{allow_mult, true}]),
-            C:set_bucket(groups, [{allow_mult, true}]);
+            C:set_bucket(<<"notes">>, [{bucket_mod, notes},{allow_mult, true}]),
+            C:set_bucket(<<"groups">>, [{bucket_mod, groups},{allow_mult, true}]);
         Error ->
             error_logger:error_msg(
               "Unable to connect to riak cluster at ~p:~p"

File riak_demo.escript

 continue_demo(Client) ->
     io:format("Looking for pre-existing object at {riak_demo, \"demo\"}...~n"),
     WrittenValue =
-        case Client:get(riak_demo, <<"demo">>, 1) of
+        case Client:get(<<"riak_demo">>, <<"demo">>, 1) of
             {error, notfound} ->
                 io:format("  No pre-existing object found, creating new~n"),
-                demo_write(Client, riak_object:new(riak_demo, <<"demo">>, undefined));
+                demo_write(Client, riak_object:new(<<"riak_demo">>, <<"demo">>, undefined));
             {ok, Object} ->
                 io:format("  Pre-existing object found, modifying~n"),
                 demo_write(Client, Object);
 
 demo_read(Client, WrittenValue) ->
     io:format("Fetching object at {riak_demo, \"demo\"}...~n"),
-    case Client:get(riak_demo, <<"demo">>, 1) of
+    case Client:get(<<"riak_demo">>, <<"demo">>, 1) of
         {ok, Object} ->
             io:format("  Fetched successfully~n"),
             case lists:member(WrittenValue, riak_object:get_values(Object)) of

File src/jaywalker_resource.erl

      RD, Ctx}.
 
 resource_exists(RD, Ctx=#ctx{jiak_client=JiakClient}) ->
-    Bucket = list_to_atom(wrq:path_info(bucket, RD)),
-    Key = list_to_binary(wrq:path_info(key, RD)),
+    Bucket = list_to_binary(mochiweb_util:unquote(
+                              wrq:path_info(bucket, RD))),
+    Key = list_to_binary(mochiweb_util:unquote(
+                           wrq:path_info(key, RD))),
     case JiakClient:get(Bucket, Key, 2) of
         {ok, Start} ->
             {true, RD, Ctx#ctx{start=Start}};
 parts_to_query([[B,T,A]|Rest], Acc) ->
     parts_to_query(Rest,
                    [{if B == "_" -> '_';
-                        true     -> list_to_atom(B)
+                        true     -> list_to_binary(mochiweb_util:unquote(B))
                      end,
                      if T == "_" -> '_';
-                        true     -> list_to_binary(T)
+                        true     -> list_to_binary(mochiweb_util:unquote(T))
                      end,
                      if A == "1"          -> true;
                         A == "0"          -> false;

File src/jiak_client.erl

 %%       {error, Err :: term()}
 %% @doc Fetch the object at Bucket/Key.  Return a value as soon as R
 %%      nodes have responded with a value or error, or TimeoutMillisecs passes.
-get(Bucket, Key, R, Timeout) when is_atom(Bucket), is_binary(Key) ->
+get(Bucket, Key, R, Timeout) when is_binary(Bucket), is_binary(Key) ->
     case RiakClient:get(Bucket, Key, R, Timeout) of
         {ok, RiakObject} ->
             JiakObject = jiak_object:from_riak_object(RiakObject),

File src/jiak_default_tests.erl

 
 write_test_() ->
     M = jiak_default:new([]),
-    O = jiak_object:new(bucket_ignored, <<"key_ignored">>),
+    O = jiak_object:new(<<"bucket_ignored">>, <<"key_ignored">>),
     {"*_write functions",
      [{"check_write", 
        ?_assertEqual(

File src/jiak_object.erl

 %%        "links" :L
 %%       }
 %%     Important: O should be a valid mochijson2 object
-new(B, K, O={struct, _}, L) when is_atom(B), is_binary(K), is_list(L) ->
+new(B, K, O={struct, _}, L) when is_binary(B), is_binary(K), is_list(L) ->
     {struct, [{<<"bucket">>, B},
               {<<"key">>, K},
               {<<"object">>, O},
 %% @spec add_link(jiak_object(), link()) -> jiak_object()
 %% @doc Add NewLink to the object's links.  If the link is already in
 %%      JiakObject, then JiakObject is not modified.
-add_link(JiakObject, NewLink=[B, K, _T]) when is_atom(B), is_binary(K) ->
+add_link(JiakObject, NewLink=[B, K, _T]) when is_binary(B), is_binary(K) ->
     Links = links(JiakObject),
     case lists:member(NewLink, Links) of
         true  -> JiakObject;
 
 test_to_riak_object() ->
     Object = {struct, [{<<"fake_field">>, <<"fake_value">>}]},
-    Links = [[other_fake_bucket,<<"other_fake_key">>,<<"fake_tag">>]],
-    J0 = jiak_object:new(fake_bucket, <<"fake_key">>, Object, Links),
+    Links = [[<<"other_fake_bucket">>,<<"other_fake_key">>,<<"fake_tag">>]],
+    J0 = jiak_object:new(<<"fake_bucket">>, <<"fake_key">>, Object, Links),
     R0 = to_riak_object(J0),
     ?assertEqual(bucket(J0), riak_object:bucket(R0)),
     ?assertEqual(key(J0), riak_object:key(R0)),
 test_from_riak_object() ->
     R0 = to_riak_object(
            setf(jiak_object:new(
-                  fake_bucket, <<"fake_key">>,
+                  <<"fake_bucket">>, <<"fake_key">>,
                   {struct, [{<<"fake_field">>, <<"fake_value">>}]},
-                  [[other_fake_bucket,<<"other_fake_key">>,
+                  [[<<"other_fake_bucket">>,<<"other_fake_key">>,
                     <<"fake_tag">>]]),
                 <<"vclock">>,
                 vclock_to_headers(vclock:increment(<<"a">>, vclock:fresh())))),
                  binary_to_list(vtag(J1))).
 
 object_props_test() ->
-    A = jiak_object:new(fake_bucket, <<"fake_key">>),
+    A = jiak_object:new(<<"fake_bucket">>, <<"fake_key">>),
     ?assertEqual([], props(A)),
 
     B = setp(A, <<"foo">>, 42),
     ?assertEqual(<<"check">>, getp(E, <<"bar">>)).
 
 links_test() ->
-    A = jiak_object:new(fake_object, <<"fake_key">>),
+    A = jiak_object:new(<<"fake_object">>, <<"fake_key">>),
     ?assertEqual([], links(A)),
     
-    L0 = [other_fake_bucket,<<"other_fake_key">>,<<"fake_tag">>],
+    L0 = [<<"other_fake_bucket">>,<<"other_fake_key">>,<<"fake_tag">>],
     B = add_link(A, L0),
     ?assertEqual([L0], links(B)),
     ?assertEqual([L0], links(B, '_', '_')),
-    ?assertEqual([L0], links(B, other_fake_bucket)),
-    ?assertEqual([],   links(B, wrong_fake_bucket)),
+    ?assertEqual([L0], links(B, <<"other_fake_bucket">>)),
+    ?assertEqual([],   links(B, <<"wrong_fake_bucket">>)),
     ?assertEqual([L0], links(B, '_', <<"fake_tag">>)),
     ?assertEqual([],   links(B, '_', <<"wrong_tag">>)),
-    ?assertEqual([L0], links(B, other_fake_bucket, <<"fake_tag">>)),
-    ?assertEqual([],   links(B, other_fake_bucket, <<"wrong_tag">>)),
-    ?assertEqual([],   links(B, wrong_fake_bucket, <<"fake_tag">>)),
+    ?assertEqual([L0], links(B, <<"other_fake_bucket">>, <<"fake_tag">>)),
+    ?assertEqual([],   links(B, <<"other_fake_bucket">>, <<"wrong_tag">>)),
+    ?assertEqual([],   links(B, <<"wrong_fake_bucket">>, <<"fake_tag">>)),
     
     ?assertEqual(B, add_link(B, L0)), %%don't double-add links
 
     ?assertEqual(remove_link(B, L0),
                  remove_link(B, hd(L0), hd(tl(L0)), hd(tl(tl(L0))))),
     
-    L1 = [other_fake_bucket,<<"second_fake_key">>,<<"new_fake_tag">>],
-    L2 = [new_fake_bucket,<<"third_fake_key">>,<<"fake_tag">>],
+    L1 = [<<"other_fake_bucket">>,<<"second_fake_key">>,<<"new_fake_tag">>],
+    L2 = [<<"new_fake_bucket">>,<<"third_fake_key">>,<<"fake_tag">>],
     C = add_link(add_link(B, L1), L2),
     ?assertEqual(3, length(links(C))),
-    ?assertEqual(2, length(links(C, other_fake_bucket))),
+    ?assertEqual(2, length(links(C, <<"other_fake_bucket">>))),
     ?assertEqual(2, length(links(C, '_', <<"fake_tag">>))),
     ?assertEqual([L1], links(C, '_', <<"new_fake_tag">>)),
-    ?assertEqual([L2], links(C, new_fake_bucket)).
+    ?assertEqual([L2], links(C, <<"new_fake_bucket">>)).
 
 mapreduce_test_() ->
     [fun test_linkfun/0,
 
 mr_riak_object() ->
     R0 = to_riak_object(
-           jiak_object:new(fake_bucket, <<"fake_key">>,
+           jiak_object:new(<<"fake_bucket">>, <<"fake_key">>,
                            {struct, [{<<"a">>, 1}]},
                            [[b1, <<"k1">>, <<"t1">>],
                             [b1, <<"k2">>, <<"t2">>],
 diff_base_empty_test() ->
     %% base empty - no added props or links
     ?assertEqual({[],{[],[]}},
-                 diff(undefined, jiak_object:new(fake, <<"fake">>))).
+                 diff(undefined, jiak_object:new(<<"fake">>, <<"fake">>))).
 
 diff_base_full_test() ->
-    Obj = jiak_object:new(fake, <<"fake">>,
+    Obj = jiak_object:new(<<"fake">>, <<"fake">>,
                           {struct, [{<<"f1">>, <<"v1">>},
                                     {<<"f2">>, <<"v2">>}]},
                           [[fake1, <<"fake1">>, <<"tag1">>],
     ?assertEqual([], RemovedLinks).
 
 diff_complex_test() ->
-    OldObj = jiak_object:new(fake, <<"fake">>,
+    OldObj = jiak_object:new(<<"fake">>, <<"fake">>,
                              {struct, [{<<"changeme">>, <<"v1">>},
                                        {<<"remme">>,    <<"v2">>},
                                        {<<"leaveme">>,   <<"v3">>}]},
-                             [[remove_me, <<"rem1">>, <<"tag1">>],
-                              [leave_me, <<"leave2">>, <<"tag2">>]]),
+                             [[<<"remove_me">>, <<"rem1">>, <<"tag1">>],
+                              [<<"leave_me">>, <<"leave2">>, <<"tag2">>]]),
     NewObj = jiak_object:setp(
                jiak_object:removep(
                  jiak_object:setp(
                    jiak_object:remove_link(
                      jiak_object:add_link(
-                       OldObj, [im_added, <<"added3">>, <<"tag3">>]),
-                     [remove_me, <<"rem1">>, <<"tag1">>]),
+                       OldObj, [<<"im_added">>, <<"added3">>, <<"tag3">>]),
+                     [<<"remove_me">>, <<"rem1">>, <<"tag1">>]),
                    <<"imadded">>, <<"v4">>),
                  <<"remme">>),
                <<"changeme">>, <<"didchange">>),
                  lists:keysearch(<<"imadded">>, 1, PropDiff)),
     ?assertEqual(3, length(PropDiff)),
     
-    ?assert(lists:member(hd(jiak_object:links(NewObj, im_added)),
+    ?assert(lists:member(hd(jiak_object:links(NewObj, <<"im_added">>)),
                          AddedLinks)),
     ?assertEqual(1, length(AddedLinks)),
 
-    ?assert(lists:member(hd(jiak_object:links(OldObj, remove_me)),
+    ?assert(lists:member(hd(jiak_object:links(OldObj, <<"remove_me">>)),
                          RemovedLinks)),
     ?assertEqual(1, length(RemovedLinks)).
 
     %% for that are in jiak:standard_sibling_merge_test/0,
     %% just need to make sure that the sibling merge branch
     %% gets called
-    J0 = jiak_object:new(fake, <<"fake">>,
+    riak_ring_manager:start_link(test),
+    riak_eventer:start_link(test),    
+    riak_bucket:set_bucket(<<"jiak_example">>,
+                           [{bucket_mod, jiak_example}]),
+
+    J0 = jiak_object:new(<<"fake">>, <<"fake">>,
                          {struct, [{<<"a">>, 1}]},
-                         [[a,<<"a">>,<<"a">>]]),
-    J1 = jiak_object:new(fake, <<"fake">>,
+                         [[<<"a">>,<<"a">>,<<"a">>]]),
+    J1 = jiak_object:new(<<"fake">>, <<"fake">>,
                          {struct, [{<<"b">>, 2}]},
-                         [[b,<<"b">>,<<"b">>]]),
+                         [[<<"b">>,<<"b">>,<<"b">>]]),
     [{M0,V0}] = riak_object:get_contents(to_riak_object(J0)),
     [{M1,V1}] = riak_object:get_contents(to_riak_object(J1)),
     M0p = dict:store(<<"X-Riak-Last-Modified">>,
     
     R = riak_object:increment_vclock(
           riak_object:set_contents(
-            riak_object:new(jiak_example, <<"fake">>, ignore),
+            riak_object:new(<<"jiak_example">>, <<"fake">>, ignore),
             [{M0p,V0},{M1p,V1}]),
           <<"tester">>),
 
     J = from_riak_object(R),
 
+    riak_ring_manager:stop(),
+    riak_eventer:stop(),
+
     ?assertEqual(getp(J0, <<"a">>), getp(J, <<"a">>)),
     ?assertEqual(getp(J1, <<"b">>), getp(J, <<"b">>)),
     ?assertEqual(2, length(props(J))),
     
-    ?assertEqual(links(J0, a), links(J, a)),
-    ?assertEqual(links(J1, b), links(J, b)),
+    ?assertEqual(links(J0, <<"a">>), links(J, <<"a">>)),
+    ?assertEqual(links(J1, <<"b">>), links(J, <<"b">>)),
     ?assertEqual(2, length(links(J))).

File src/jiak_resource.erl

 
 %% @type context() = term()
 %% @type jiak_module() = atom()|{jiak_default, list()}
--record(ctx, {bucket,       %% atom() - Bucket name (from uri)
+-record(ctx, {bucket,       %% binary() - Bucket name (from uri)
               key,          %% binary()|container|schema - Key (or sentinal
                             %%   meaning "no key provided")
               module,       %% atom()
     {ServiceAvailable, NewCtx} = 
         case wrq:method(ReqData) of
             'PUT' -> 
-                _ = list_to_atom(wrq:path_info(bucket, ReqData)),
+                _ = list_to_binary(mochiweb_util:unquote(
+                                     wrq:path_info(bucket, ReqData))),
                 Mod = jiak_default:new([]),
                 {true, Context#ctx{module=Mod, key=schema}};
             _ ->
     Key = case Ctx0#ctx.key of
               container -> container;
               schema -> schema;
-              _         -> list_to_binary(wrq:path_info(key, RD))
+              _         -> list_to_binary(mochiweb_util:unquote(
+                                            wrq:path_info(key, RD)))
           end,
-    case jiak_util:bucket_from_uri(RD) of
-        {ok, Bucket} ->
-            {ok, JC} = Mod:init(Key, jiak_context:new(not_diffed_yet, [])),
-            Ctx = Ctx0#ctx{bucket=Bucket, key=Key, jiak_context=JC},
-            case Key of
-                container ->
-                    %% buckets have GET for list_keys, POST for create
-                    {['HEAD', 'GET', 'POST'], RD, Ctx};
-                schema ->
-                    {['PUT'], RD, Ctx};
-                _ ->
-                    %% keys have the "full" doc store set
-                    {['HEAD', 'GET', 'POST', 'PUT', 'DELETE'], RD, Ctx}
-            end;
-        {error, no_such_bucket} ->
-            %% no bucket, nothing but GET/HEAD allowed, and POST for 
-            %% schema mods
-            {['HEAD', 'GET', 'PUT'], RD, Ctx0#ctx{bucket={error, no_such_bucket}}}
+    Bucket = jiak_util:bucket_from_reqdata(RD),
+    {ok, JC} = Mod:init(Key, jiak_context:new(not_diffed_yet, [])),
+    Ctx = Ctx0#ctx{bucket=Bucket, key=Key, jiak_context=JC},
+    case Key of
+        container ->
+            %% buckets have GET for list_keys, POST for create
+            {['HEAD', 'GET', 'POST'], RD, Ctx};
+        schema ->
+            {['PUT'], RD, Ctx};
+        _ ->
+            %% keys have the "full" doc store set
+            {['HEAD', 'GET', 'POST', 'PUT', 'DELETE'], RD, Ctx}
     end.
 
 %% @spec malformed_request(webmachine:wrq(), context()) ->
 %%      GET is always properly constructed
 %%      PUT/POST is malformed if:
 %%        - request body is not a valid JSON object
-%%        - the object contains a link to a bucket that is
-%%          "unknown" (non-existent atom)
+%%        - the object is in a bucket that is "undefined"
 %%      PUT is malformed if:
 %%        - the "bucket" field of the object does not match the
 %%          bucket component of the URI
         false -> {false, ReqData, Context};
         true ->
             case decode_object(wrq:req_body(ReqData)) of
-                {ok, JiakObject0={struct,_}} ->
-                    case atomify_buckets(JiakObject0) of
-                        {ok, JiakObject} ->
-                            PT = wrq:method(ReqData) == 'PUT',
-                            KM = jiak_object:key(JiakObject) == Key,
-                            BM = jiak_object:bucket(JiakObject) == Bucket,
-                            if (not PT); (PT andalso KM andalso BM) ->
-                                    {false, ReqData, Context#ctx{incoming=JiakObject}};
-                               not KM ->
-                                    {true,
-                                     wrq:append_to_response_body("Object key does not match URI",
-                                                                 ReqData),
-                                     Context};
-                               not BM ->
-                                    {true,
-                                     wrq:append_to_response_body("Object bucket does not match URI",
-                                                                 ReqData),
-                                     Context}
-                            end;
-                        _ ->
+                {ok, JiakObject={struct,_}} ->
+                    PT = wrq:method(ReqData) == 'PUT',
+                    KM = jiak_object:key(JiakObject) == Key,
+                    BM = jiak_object:bucket(JiakObject) == Bucket,
+                    if (not PT); (PT andalso KM andalso BM) ->
+                            {false, ReqData, Context#ctx{incoming=JiakObject}};
+                       not KM ->
                             {true,
-                             wrq:append_to_response_body("Unknown bucket in link.",
+                             wrq:append_to_response_body("Object key does not match URI",
+                                                         ReqData),
+                             Context};
+                       not BM ->
+                            {true,
+                             wrq:append_to_response_body("Object bucket does not match URI",
                                                          ReqData),
                              Context}
                     end;
     try {ok, mochijson2:decode(Body)}
     catch _:_ -> {error, bad_json} end.
 
-%% @spec atomify_buckets(mochijson2()) ->
-%%         {ok, mochijson2()}|{error, no_such_bucket}
-%% @doc Convert binary() bucket names into atom() bucket names in the
-%%      "bucket" and "links" fields.  This is necessary because
-%%      mochijson2:encode/1 converts Erlang atoms to JSON strings, but
-%%      mochijson2:decode/1 converts JSON strings to Erlange binaries.
-atomify_buckets({struct,JOProps}) ->
-    try
-        BinBucket = proplists:get_value(<<"bucket">>, JOProps),
-        Bucket = list_to_existing_atom(binary_to_list(BinBucket)),
-        BinLinks = proplists:get_value(<<"links">>, JOProps),
-        Links = [ [list_to_existing_atom(binary_to_list(B)), K, T] || [B,K,T] <- BinLinks],
-        {ok, {struct, [{<<"bucket">>, Bucket},
-                       {<<"links">>, Links}
-                       |[{K,V} || {K,V} <- JOProps,
-                                  K /= <<"bucket">>, K /= <<"links">>]]}}
-    catch _:_ -> {error, no_such_bucket} end.
-
 %% @spec check_required(jiak_object(), [binary()]) -> boolean()
 %% @doc Determine whether Obj contains all of the fields named in
 %%      the Fields parameter.  Returns 'true' if all Fields are
 
 add_container_link(Bucket,ReqData) ->
     Val = io_lib:format("</~s/~s>; rel=\"up\"",
-                    [riak:get_app_env(jiak_name, "jiak"),Bucket]),
+                    [riak:get_app_env(jiak_name, "jiak"),
+                     mochiweb_util:quote_plus(Bucket)]),
     wrq:merge_resp_headers([{"Link",Val}],ReqData).
 
 add_link_head(Bucket,Key,Tag,ReqData) ->
     Val = io_lib:format("</~s/~s/~s>; riaktag=\"~s\"",
-                    [riak:get_app_env(jiak_name, "jiak"),Bucket, Key, Tag]),
+                    [riak:get_app_env(jiak_name, "jiak")|
+                     [mochiweb_util:quote_plus(E) ||
+                         E <- [Bucket, Key, Tag] ]]),
     wrq:merge_resp_headers([{"Link",Val}],ReqData).
 
 %% @spec full_schema(riak_object:bucket()) ->
 %% @spec make_uri(string(), riak_object:bucket(), string()) -> string()
 %% @doc Get the string-path for the bucket and subpath under jiak.
 make_uri(JiakName,Bucket,Path) ->
-    "/" ++ JiakName ++ "/" ++ atom_to_list(Bucket) ++ "/" ++ Path.
+    "/" ++ JiakName ++ 
+        "/" ++ mochiweb_util:quote_plus(Bucket) ++
+        "/" ++ Path.
 
 %% @spec handle_incoming(webmachine:wrq(), context()) ->
 %%          {true, webmachine:wrq(), context()}
 handle_incoming(ReqData, Context=#ctx{key=schema, 
                                       bucket=Bucket,
                                       incoming=SchemaPL}) ->
-    SchemaProps = [{list_to_atom(binary_to_list(K)),V} || {K,V} <- SchemaPL],
+    SchemaProps = [{list_to_atom(binary_to_list(K)), V} || {K,V} <- SchemaPL],
     ok = riak_bucket:set_bucket(Bucket, SchemaProps),
     {<<>>, ReqData, Context};
 handle_incoming(ReqData, Context=#ctx{bucket=Bucket,key=Key,
                                      make_uri(JiakName,Bucket,
                                               wrq:disp_path(ReqData)),
                                      ReqData),
-                 list_to_binary(wrq:disp_path(ReqData))};
+                 list_to_binary(mochiweb_util:unquote(wrq:disp_path(ReqData)))};
             _ ->
                 {item, ReqData, Key}
         end,
 %%      case of a POST to a bucket, or the path for the given object
 %%      in the case of a POST to a specific object.
 create_path(ReqData, Context=#ctx{key=container}) ->
+    %% riak_util:unique_id_62 produces url-safe strings
     {riak_util:unique_id_62(), ReqData, Context};
 create_path(ReqData, Context=#ctx{key=Key}) ->
-    {Key, ReqData, Context}.
+    {mochiweb_util:quote_plus(Key), ReqData, Context}.
 
 %% @spec delete_resource(webmachine:wrq(), context()) ->
 %%          {boolean(), webmachine:wrq(), context()}
 %%
 
 mochijson_roundtrip_test() ->
-    J0 = jiak_object:new(fake_bucket, <<"fake_key">>,
+    J0 = jiak_object:new(<<"fake_bucket">>, <<"fake_key">>,
                          {struct, [{<<"a">>, 1}]},
-                         [[other_bucket, <<"other_key">>, <<"fake_tag">>]]),
+                         [[<<"other_bucket">>, <<"other_key">>, <<"fake_tag">>]]),
     R0 = jiak_object:to_riak_object(J0),
     [{M,V}] = riak_object:get_contents(R0),
     R1 = riak_object:set_vclock(
                V}]),
            vclock:increment(<<"foo">>, vclock:fresh())),
     J1 = jiak_object:from_riak_object(R1),
-    {ok, J2} = atomify_buckets(mochijson2:decode(mochijson2:encode(J1))),
+    J2 = mochijson2:decode(mochijson2:encode(J1)),
     ?assertEqual(jiak_object:bucket(J1), jiak_object:bucket(J2)),
     ?assertEqual(jiak_object:key(J1), jiak_object:key(J2)),
     ?assertEqual(jiak_object:vclock(J1), jiak_object:vclock(J2)),
                             {read_mask,
                              [<<"read0">>, <<"read1">>]}]),
     Masked = jiak_object:new(
-               fake_bucket, <<"fake_key">>,
+               <<"fake_bucket">>, <<"fake_key">>,
                {struct, [{<<"read0">>, <<"val0">>}]},
                []),
     UnMasked = jiak_object:new(
-                 fake_bucket, <<"fake_key">>,
+                 <<"fake_bucket">>, <<"fake_key">>,
                  {struct, [{<<"read0">>, <<"val1">>},
                            {<<"read1">>, <<"val2">>},
                            {<<"unread0">>, <<"val3">>}]},
     Mod = jiak_default:new([{read_mask,
                              [<<"read0">>,<<"read1">>,<<"read2">>]}]),
     UnMasked = jiak_object:new(
-                 fake_bucket, <<"fake_key">>,
+                 <<"fake_bucket">>, <<"fake_key">>,
                  {struct, [{<<"read0">>, <<"val1">>},
                            {<<"read1">>, <<"val2">>},
                            {<<"unreadable0">>, <<"val1">>},

File src/jiak_util.erl

 -export([jiak_required_props/0,
          jiak_module_for_bucket/1, 
          get_jiak_module/1, 
-         default_jiak_module/1,
-         bucket_from_uri/1]).
+         bucket_from_reqdata/1]).
 
 -include_lib("eunit/include/eunit.hrl").
 
 jiak_required_props() -> [allowed_fields,required_fields,read_mask,write_mask].
 
 %% @private
-default_jiak_module(BucketName) when is_atom(BucketName) ->
+jiak_module_for_bucket(BucketName) when is_binary(BucketName) ->
     BucketProps = riak_bucket:get_bucket(BucketName),
-    case lists:filter(
-           fun(I) -> 
-                   proplists:get_value(I, BucketProps) =:= undefined
-           end, 
-           jiak_required_props()) of
-        [] ->
-            jiak_default:new(BucketProps);
-        _ ->
-            undefined
+    case proplists:lookup(bucket_mod, BucketProps) of
+        {bucket_mod, Module} ->
+            Module;
+        none ->
+            case bucket_props_defined(BucketProps) of
+                true ->
+                    jiak_default:new(BucketProps);
+                false ->
+                    undefined
+            end
     end.
 
+bucket_props_defined(BucketProps) ->
+    [] == lists:filter(
+            fun(I) -> 
+                    proplists:get_value(I, BucketProps) =:= undefined
+            end, 
+            jiak_required_props()).
+
 %% @private
 get_jiak_module(ReqData) ->
-    case bucket_from_uri(ReqData) of
-        {ok, Bucket} when is_atom(Bucket) ->
-            jiak_module_for_bucket(Bucket);
-        {error, no_such_bucket} -> 
-            undefined
-    end.
+    jiak_module_for_bucket(bucket_from_reqdata(ReqData)).
 
-%% @private
-jiak_module_for_bucket(Bucket) when is_atom(Bucket) ->
-    case code:which(Bucket) of
-        non_existing ->
-            case default_jiak_module(Bucket) of
-                undefined -> undefined;
-                Mod when is_tuple(Mod) -> Mod
-            end;
-        ModPath when is_list(ModPath) -> Bucket;
-        cover_compiled -> Bucket %% used during eunit testing
-    end.
-
-%% @spec bucket_from_uri(webmachine:wrq()) ->
-%%         {ok, atom()}|{error, no_such_bucket}
-%% @doc Extract the bucket name, as an atom, from the request URI.
-%%      The bucket name must be an existing atom, or this function
-%%      will return {error, no_such_bucket}
-bucket_from_uri(RD) ->
-    try {ok, list_to_existing_atom(wrq:path_info(bucket, RD))}
-    catch _:_ -> {error, no_such_bucket} end.
+%% @spec bucket_from_reqdata(webmachine:wrq()) -> binary()
+%% @doc Extract the bucket name, as a binary, from the request URI.
+bucket_from_reqdata(RD) ->
+    list_to_binary(mochiweb_util:unquote(wrq:path_info(bucket, RD))).
 
 dynamic_bucket_test() ->
     riak_ring_manager:start_link(test),
                    {required_fields, []},
                    {read_mask, [<<"test">>]},
                    {write_mask, [<<"test">>]}],
-    riak_bucket:set_bucket(dynamic_bucket_test, BucketProps),
-    Mod = jiak_module_for_bucket(dynamic_bucket_test),
+    riak_bucket:set_bucket(<<"dynamic_bucket_test">>, BucketProps),
+    Mod = jiak_module_for_bucket(<<"dynamic_bucket_test">>),
     ?assertEqual([<<"test">>], Mod:allowed_fields()),
     ?assertEqual([], Mod:required_fields()),
     ?assertEqual([<<"test">>], Mod:read_mask()),
     riak_ring_manager:stop(),
     riak_eventer:stop().
 
-existing_bucket_from_uri_test() ->
-    foo, %% make sure the atom exists
+module_bucket_test() ->
+    riak_ring_manager:start_link(test),
+    riak_eventer:start_link(test),    
+    BucketProps = [{bucket_mod, jiak_example}],
+    riak_bucket:set_bucket(<<"module_bucket_test">>, BucketProps),
+    Mod = jiak_module_for_bucket(<<"module_bucket_test">>),
+    ?assertEqual([<<"foo">>,<<"bar">>,<<"baz">>,<<"quux">>],
+                 Mod:allowed_fields()),
+    ?assertEqual([<<"foo">>], Mod:required_fields()),
+    ?assertEqual([<<"foo">>,<<"bar">>], Mod:read_mask()),
+    ?assertEqual([<<"foo">>,<<"baz">>], Mod:write_mask()),
+    riak_ring_manager:stop(),
+    riak_eventer:stop().
+    
+
+bucket_from_uri_test() ->
     PI = dict:store(bucket, "foo", dict:new()),
     RD0 = wrq:create('PUT', "1.1", "/jiak/foo", mochiweb_headers:empty()),
     RD = wrq:load_dispatch_data(PI, none, none, none, none, RD0),
-    ?assertEqual({ok, foo}, bucket_from_uri(RD)).
-
-nonexisiting_bucket_from_uri_test() ->
-    PI = dict:store(bucket, "thisatomshouldntexistever", dict:new()),
-    RD0 = wrq:create('PUT', "1.1", "/jiak/foo", mochiweb_headers:empty()),
-    RD = wrq:load_dispatch_data(PI, none, none, none, none, RD0),    
-    ?assertEqual({error, no_such_bucket}, bucket_from_uri(RD)).
-    
+    ?assertEqual(<<"foo">>, bucket_from_reqdata(RD)).

File src/riak_app.erl

     set_erlenv(ConfigPairs),
     check_erlenv(ConfigPath),
     [code:add_path(Path) || Path <- riak:get_app_env(add_paths)],
+    [application:start(App) || App <- riak:get_app_env(start_apps)],
+    StorageBackend = riak:get_app_env(storage_backend),
+    case code:ensure_loaded(StorageBackend) of
+        {error,nofile} ->
+            riak:stop(lists:flatten(io_lib:format(
+                        "storage_backend ~p in ~p non-loadable, failing.",
+                        [StorageBackend, ConfigPath])));
+        _ ->
+            ok
+    end,
     ok.
 
 %% @private
      WantsClaimFun,ChooseClaimFun,GossipInterval,
      DoorbellPort,StorageBackend,RiakCookie,RiakAddPaths,
      RiakNodeName,RiakHostName,RiakHeartCommand,
-     DefaultBucketProps] =
+     DefaultBucketProps,RiakStartApps] =
         [riak:get_app_env(X) || X <- 
            [cluster_name,ring_state_dir,ring_creation_size,
             wants_claim_fun,choose_claim_fun,gossip_interval,
             doorbell_port,storage_backend,riak_cookie,add_paths,
             riak_nodename,riak_hostname,riak_heart_command,
-            default_bucket_props]],
+            default_bucket_props,start_apps]],
     if
         ClusterName =:= undefined ->
             riak:stop(io_lib:format(
         not is_atom(StorageBackend) ->
             riak:stop(io_lib:format(
                     "storage_backend in ~p non-atom, failing.",[ConfigPath]));
-        true ->
-            case code:ensure_loaded(StorageBackend) of
-                {error,nofile} ->
-                    riak:stop(io_lib:format(
-                           "storage_backend ~p in ~p non-loadable, failing.",
-                           [StorageBackend, ConfigPath]));
-                _ ->
-                    ok
-            end
+        true -> ok
     end,
     if
         RiakCookie =:= undefined ->
                         "default_bucket_props in ~p non-list, failing.",
                         [ConfigPath]))
     end,
+    if 
+        RiakStartApps =:= undefined ->
+            error_logger:info_msg(
+              "start_apps unset in ~p, setting to []~n",
+              [ConfigPath]),
+            application:set_env(riak, start_apps, []);
+        not is_list(RiakStartApps) ->
+            riak:stop(io_lib:format(
+                        "start_apps in ~p non-list, failing.",[ConfigPath]));
+        true -> ok
+    end,
     ok.
 
 set_bucket_params(In) ->

File src/riak_backup.erl

     {ok, r_table} = dets:open_file(r_table, [{file, Filename}]),
     {ok, Client} = riak:client_connect(IP,list_to_integer(PortStr),list_to_atom(Cookie)),
     Trav = dets:traverse(r_table,
-      fun({{Bucket,Key},V}) ->
-              RObj0 = binary_to_term(V),
+      fun({{Bucket0,Key},V}) ->
+              RObj00 = binary_to_term(V),
+              {Bucket, RObj0} =
+                  if is_binary(Bucket0) -> {Bucket0, RObj00};
+                     is_atom(Bucket0) ->
+                          BN = list_to_binary(atom_to_list(Bucket0)),
+                          {BN,
+                           riak_object:set_vclock(
+                             riak_object:set_contents(
+                               riak_object:new(BN, Key, backup_placeholder),
+                               riak_object:get_contents(RObj00)),
+                             riak_object:vclock(RObj00))}
+                  end,
               RObj = riak_object:update_metadata(RObj0,
                        dict:store("no_update",no_update,
                          riak_object:get_update_metadata(RObj0))),
     {ok, r_table} = dets:open_file(r_table, [{file, Filename}]),
     {ok, Client} = riak:local_client(),
     Trav = dets:traverse(r_table,
-      fun({{Bucket,Key},V}) ->
-              RObj0 = binary_to_term(V),
+      fun({{Bucket0,Key},V}) ->
+              RObj00 = binary_to_term(V),
+              {Bucket, RObj0} =
+                  if is_binary(Bucket0) -> {Bucket0, RObj00};
+                     is_atom(Bucket0) ->
+                          BN = list_to_binary(atom_to_list(Bucket0)),
+                          {BN,
+                           riak_object:set_vclock(
+                             riak_object:set_contents(
+                               riak_object:new(BN, Key, backup_placeholder),
+                               riak_object:get_contents(RObj00)),
+                             riak_object:vclock(RObj00))}
+                  end,
               MD0 = dict:store("no_update",no_update, 
                                riak_object:get_update_metadata(RObj0)),
               {ObjMD,_} = hd(riak_object:get_contents(RObj0)),

File src/riak_client.erl

 -export([reload_all/1]).
 -export([remove_from_cluster/1]).
 -export([send_event/2]).
+-export ([add_event_handler/2, add_event_handler/3, add_event_handler/4]).
+-export ([remove_event_handler/3]).
 %% @type default_timeout() = 15000
 -define(DEFAULT_TIMEOUT, 15000).
 
 %%       {error, Err :: term()}
 %% @doc Fetch the object at Bucket/Key.  Return a value as soon as R
 %%      nodes have responded with a value or error, or TimeoutMillisecs passes.
-get(Bucket, Key, R, Timeout) when is_atom(Bucket), is_binary(Key),
+get(Bucket, Key, R, Timeout) when is_binary(Bucket), is_binary(Key),
                                   is_integer(R), is_integer(Timeout) ->
     Me = self(),
     spawn(Node, riak_get_fsm, start, [Bucket,Key,R,Timeout,Me]),
 send_event(EventName, EventDetail) ->
     rpc:call(Node,riak_eventer,notify,
              [client_event, EventName, {ClientId, EventDetail}]).
+
+
+%% @doc Attach a new handler pid to Riak events. 
+%% See http://erlang.org/doc/apps/erts/match_spec.html for more 
+%% information about match head and match guard.
+%% Desc is simply a human readable string used by the WebUI.
+add_event_handler(Pid, Desc) -> 
+    add_event_handler(Pid, Desc, {'_', '_', '_', '_'}).
+    
+add_event_handler(Pid, Desc, MatchHead) -> 
+    add_event_handler(Pid, Desc, MatchHead, []).
+    
+add_event_handler(Pid, Desc, MatchHead, MatchGuard) ->
+    rpc:call(Node, riak_eventer, add_handler, [Pid, Desc, MatchHead, MatchGuard]). 
+
+%% remove_handler/N - 
+%% Remove a previously added handler, if it still exists.
+%% Handlers are automatically removed for dead processes
+%% every (gossip_interval) seconds, or upon adding
+%% or deleting.
+remove_event_handler(Pid, MatchHead, MatchGuard) ->
+    rpc:call(Node, riak_eventer, remove_handler, [Pid, MatchHead, MatchGuard]). 

File src/riak_ets_backend.erl

         Err -> {error, Err}
     end.
 
-% put(state(), {B :: atom(), K :: binary()}, Key :: binary(),
-%     Val :: binary()) ->
+% put(state(), riak_object:bkey(), Val :: binary()) ->
 %   ok | {error, Reason :: term()}
 % key must be 160b
 put(SrvRef, BKey, Val) -> gen_server:call(SrvRef,{put,BKey,Val}).
 list([],Acc) -> Acc;
 list([[K]|Rest],Acc) -> list(Rest,[K|Acc]).
 
-% list_bucket(Bucket :: atom(), state()) -> [Key :: binary()]
+% list_bucket(term(), Bucket :: riak_object:bucket()) -> [Key :: binary()]
 list_bucket(SrvRef, Bucket) ->
     gen_server:call(SrvRef,{list_bucket, Bucket}).
 srv_list_bucket(State, {filter, Bucket, Fun}) ->

File src/riak_event_guard.erl

-%% This file is provided to you under the Apache License,
-%% Version 2.0 (the "License"); you may not use this file
-%% except in compliance with the License.  You may obtain
-%% a copy of the License at
-
-%%   http://www.apache.org/licenses/LICENSE-2.0
-
-%% Unless required by applicable law or agreed to in writing,
-%% software distributed under the License is distributed on an
-%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-%% KIND, either express or implied.  See the License for the
-%% specific language governing permissions and limitations
-%% under the License.    
-
-%% @doc A wrapper server for connecting riak_eventer handlers to the gen_event.
-
--module(riak_event_guard).
-
--export([start_link/0]).
--export([add_handler/3]).
--export([init/1, handle_info/2]).
--export([handle_call/3,handle_cast/2,code_change/3,terminate/2]).
-
--behavior(gen_server).
-
-%% @spec start_link() -> {ok, pid()}
-%% @doc The usual gen_server start_link mechanism.
-start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
-
-% @private
-init([]) ->
-    case gen_event:start_link({local,riak_event}) of
-        {ok, Pid} -> {ok, Pid};
-        {error,{already_started,Pid}} -> {ok, Pid};
-        X -> {stop, {error_in_init, X}}
-    end.
-
-%% @spec add_handler(HandlerMod :: atom(), Arg :: term(), 
-%%                   MatchSpec :: string()) ->
-%%       ok | {error, Error :: term()}
-%% @doc Attach a new HandlerMod to riak events, started with Arg.
-add_handler(HandlerMod, Arg, MatchSpec) ->
-    gen_server:call(?MODULE, {add_handler, HandlerMod, Arg, MatchSpec}).
-
-%% @private
-handle_call({add_handler, HandlerMod, Arg, MatchSpec},_From,State) -> 
-    {ok, MyRing} = riak_ring_manager:get_my_ring(),
-    Eventers0 = case riak_ring:get_meta(eventers, MyRing) of
-        undefined -> [];
-        {ok, X} -> sets:to_list(X)
-    end,
-    
-    Eventers = sets:add_element({node(),MatchSpec}, sets:from_list(
-                 [{N,MS} || {N,MS} <- Eventers0,
-                       net_adm:ping(N) =:= pong,
-                       N /= node()])),
-    NewRing = riak_ring:update_meta(eventers, Eventers, MyRing),
-    riak_ring_manager:set_my_ring(NewRing),
-    riak_ring_manager:write_ringfile(),
-    Reply = gen_event:swap_sup_handler(riak_event,
-                                       {{HandlerMod,{node(),now()}}, swap}, 
-                                       {{HandlerMod,{node(),now()}}, Arg}),
-    riak_ring_gossiper:gossip_to(
-      riak_ring:index_owner(NewRing,riak_ring:random_other_index(NewRing))),
-    {reply, Reply, State}.
-
-%% @private
-handle_info({gen_event_EXIT, HandlerMod, Reason},State) ->
-    %% gen_event manager sends this message if a handler was added using
-    %% gen_event:add_sup_handler/3 or gen_event:swap_sup_handler/3 functions
-    riak_eventer:notify(riak_event_guard, gen_event_exit,{HandlerMod, Reason}),
-    {noreply,State}.
-
-%% @private
-handle_cast(_,State) -> {noreply,State}.
-%% @private
-code_change(_OldVsn, State, _Extra) -> {ok, State}.
-%% @private
-terminate(_Reason,_State)  -> ok.

File src/riak_eventer.erl

 %% specific language governing permissions and limitations
 %% under the License.    
 
+
+%% @doc
+%% riak_eventer allows you to attach event handlers to a running Riak cluster to
+%% receive event notifications (in the form of Erlang messages) about what is happening
+%% in the application. 
+%%
+%% Events are generated using notify/1 or notify/3. Each event consists
+%% of a Module, an EventName, the Node on which the event is generated,
+%% and additional detail about the event.
+%%
+%% This is stored in a tuple of the form {event, {Module, EventName, Node, EventDetail}}.
+%% For example, a 'put' operation will generate an event such as 
+%% {event,{riak_vnode,put, 'node@hostname', ...}}.
+%%
+%% Event handlers are added via add_handler(Pid, Description, MatchHead, MatchGuard),
+%% and can be removed via remove_handler(Pid, MatchHead, MatchGuard).
+%%
+%% Full MatchSpec style matching is allowed (see http://erlang.org/doc/apps/erts/match_spec.html)
+%% to filter events at the server level, and the system fully supports registering 
+%% a single process for multiple events.
+%%
+%% Riak monitors running handlers, and automatically removes 
+%% handlers of dead processors.
+
+
 -module(riak_eventer).
 -behaviour(gen_server2).
 -export([start_link/0,start_link/1,stop/0]).
 -export([init/1, handle_call/3, handle_cast/2, handle_info/2,
 	 terminate/2, code_change/3]).
 
--export([notify/1,notify/3,eventer_config/1,do_eventer/1]).
+-export([notify/1, notify/3]).
+-export ([add_handler/4]).
+-export ([remove_handler/1, remove_handler/3]).
+-export ([remove_dead_handlers/0]).
+
+-define(REMOVE_INTERVAL, 5 * 1000).
 
 -include_lib("eunit/include/eunit.hrl").
 
+-record (handler, {
+    id,         % The id of this handler. Made from a combination 
+                % of pid, matchhead, and matchguard, allowing for
+                % multiple handlers from the same pid.
+    desc,       % Human readable description
+    pid,        % Pid of the remote process
+    matchhead,  % MatchHead applied against {Module, EventName, Node, EventDetail}
+    matchguard  % MatchGuard, defaults to []
+}).
+
 %% @private
 start_link() -> gen_server2:start_link({local, ?MODULE}, ?MODULE, [], []).
 start_link(test) -> % when started this way, run a mock server (nop)
 
 notify(Module, EventName, EventDetail) ->
     notify({Module, EventName, node(), EventDetail}).
+    
+add_handler(Pid, Desc, MatchHead, MatchSpec) ->
+    gen_server:call(?MODULE, {add_handler, Pid, Desc, MatchHead, MatchSpec}).
+
+remove_handler(Pid, MatchHead, MatchSpec) ->
+    HandlerID = get_handler_id(Pid, MatchHead, MatchSpec),
+    remove_handler(HandlerID).
+
+remove_handler(HandlerID) ->
+    gen_server:call(?MODULE, {remove_handler, HandlerID}).
+    
+remove_dead_handlers() ->
+    gen_server:call(?MODULE, {remove_dead_handlers, false}).
 
 %% @private (only used for test instances)
 stop() -> gen_server2:cast(?MODULE, stop).
 
 %% @private
+handle_call({add_handler, Pid, Desc, MatchHead, MatchSpec},_From,State) -> 
+    % Monitor the pid, we want to know when to remove it...
+    erlang:monitor(process, Pid),
+
+    % Add the handler...
+    {ok, Ring} = riak_ring_manager:get_my_ring(),
+    Handler = make_handler(Pid, Desc, MatchHead, MatchSpec),
+    Ring1 = add_handler_to_ring(Handler, Ring),
+    
+    % Set and save the new ring...
+    riak_ring_manager:set_my_ring(Ring1),
+    riak_ring_manager:write_ringfile(),
+    
+    % Gossip the new ring...
+    RandomNode = riak_ring:index_owner(Ring1,riak_ring:random_other_index(Ring1)),
+    riak_ring_gossiper:gossip_to(RandomNode),
+    {reply, ok, State};
+    
+handle_call({remove_handler, HandlerID},_From,State) -> 
+    % Remove the handler...
+    {ok, Ring} = riak_ring_manager:get_my_ring(),
+    Ring1 = remove_handler_from_ring(HandlerID, Ring),
+    
+    % Set and save the new ring...
+    riak_ring_manager:set_my_ring(Ring1),
+    riak_ring_manager:write_ringfile(),
+    
+    % Gossip the new ring...
+    RandomNode = riak_ring:index_owner(Ring1,riak_ring:random_other_index(Ring1)),
+    riak_ring_gossiper:gossip_to(RandomNode),
+    {reply, ok, State};
+    
+    
+handle_call(_, _From, State) -> {reply, no_call_support, State}.
+
+%% @private
 handle_cast(stop, State) -> {stop,normal,State};
 
 handle_cast({event, _Event}, test) -> {noreply,test};
+
 handle_cast({event, Event}, State) ->
-    {ok, Ring} = riak_ring_manager:get_my_ring(),    %%%% TEST EVENTS!
-    Eventers = match_eventers(get_eventers(Ring), Event, []),
-    [gen_event:notify({riak_event,Node},Event) || Node <- Eventers],
-    {noreply, State}.
-  
+    % Get the handlers...
+    {ok, Ring} = riak_ring_manager:get_my_ring(),
+    Handlers = get_handlers(Ring),
+    MatchingHandlers = get_matching_handlers(Event, Handlers),
+    
+    % Send the message to all handlers...
+    [begin
+        Pid = X#handler.pid,
+        Pid ! {event, Event}
+    end || X <- MatchingHandlers],
+    {noreply, State};
+    
+handle_cast(_, State) -> {noreply, State}.
 
-eventer_config([Cluster, CookieStr]) ->
-    RipConf = [{no_config, true}, {cluster_name, Cluster},
-       {riak_cookie, list_to_atom(CookieStr)}, {ring_state_dir, "<nostore>"},
-       {ring_creation_size, 12}, {gossip_interval,1000000},
-       {wants_claim_fun, {riak_claim, never_wants_claim}},
-       {riak_web_ip, "undefined"},
-       {doorbell_port, 7000 + random:uniform(1000)},
-       {storage_backend, undefined}],
-    application:stop(sasl),
-    application:unload(sasl),
-    ok = application:load({application,sasl,[{errlog_type,error}]}),
-    ok = application:start(sasl),
-    [application:set_env(riak,K,V) || {K,V} <- RipConf].
-
-parse_matchspec(MatchSpec) when is_list(MatchSpec) ->
-    [NM,MM,TM] = string:tokens(MatchSpec, ":"),
-    {list_to_atom(NM),list_to_atom(MM),list_to_atom(TM)}.
-
-do_eventer([IP, PortStr, HandlerName, HandlerArg]) ->
-    do_eventer([IP, PortStr, HandlerName, HandlerArg, "_:_:_"]);
-do_eventer([IP, PortStr, HandlerName, HandlerArg, MatchSpec]) ->
-    MS = parse_matchspec(MatchSpec),
-    riak_startup:join_cluster([IP, PortStr]),
-    timer:sleep(random:uniform(1000)), % let some gossip happen
-    riak_event_guard:add_handler(list_to_atom(HandlerName),HandlerArg, MS),
-    ok.
-
-get_eventers(Ring) ->
-    case riak_ring:get_meta(eventers, Ring) of
-        undefined -> [];
-        {ok, X} -> sets:to_list(X)
-    end.        
-
-match_eventers([], _, Acc) ->
-    Acc;
-match_eventers([{Eventer,MS}|Rest], Event, Acc) ->
-    case match_event(MS,Event) of
+handle_info({'DOWN', _, process, Pid, _}, State) ->
+    % Get a 'DOWN' message, so remove any handlers from this Pid...
+    {ok, Ring} = riak_ring_manager:get_my_ring(),
+    OldHandlers = get_handlers(Ring),
+    
+    % Filter out any dead handlers...
+    F = fun(Handler) -> Handler#handler.pid /= Pid end,
+    NewHandlers = lists:filter(F, OldHandlers),
+    
+    % Write and gossip the ring if it has changed...
+    RingHasChanged = OldHandlers /= NewHandlers,
+    case RingHasChanged of
         true ->
-            match_eventers(Rest, Event, [Eventer|Acc]);
-        false ->
-            match_eventers(Rest, Event, Acc)
-    end.
-
-% Match an event to the event filter.
-% The idea is that the filter values (in the first tuple)
-% must either be a wildcard ('_'), or must match 
-% the value within the event tuple.
-% Return true if the filter matches the event.
-match_event({Node1, Module1, Type1}, {Module2, Type2, Node2, _}) when
-    (Node1 == '_' orelse Node1 == Node2) andalso
-    (Module1 == '_' orelse Module1 == Module2) andalso
-    (Type1 == '_' orelse Type1 == Type2) -> true;
-match_event(_, _) -> false.
+            % Set and save the new ring...
+            Ring1 = set_handlers(NewHandlers, Ring),
+            riak_ring_manager:set_my_ring(Ring1),
+            riak_ring_manager:write_ringfile(),
     
+            % Gossip the new ring...
+            RandomNode = riak_ring:index_owner(Ring1,riak_ring:random_other_index(Ring1)),
+            riak_ring_gossiper:gossip_to(RandomNode);
+        false -> ignore
+    end,
+    {noreply, State};
 
 handle_info(_Info, State) -> {noreply, State}.
 
 %% @private
 code_change(_OldVsn, State, _Extra) ->  {ok, State}.
 
-%% @private
-handle_call(_, _From, State) -> {reply, no_call_support, State}.
+%% make_handler/4 -
+%% Create an handler record from the supplied params.
+make_handler(Pid, Desc, MatchHead, MatchGuard) ->
+    ID = get_handler_id(Pid, MatchHead, MatchGuard),
+    #handler {
+        id = ID,
+        pid = Pid,
+        desc = Desc,
+        matchhead = MatchHead,
+        matchguard = MatchGuard
+    }.
+    
+%% add_handler_to_ring/5 -
+%% Given an handler and a ring, add the handler to
+%% the ring.
+add_handler_to_ring(Handler, Ring) ->
+    Handlers = get_handlers(Ring),
+    Handlers1 = lists:keystore(Handler#handler.id, 2, Handlers, Handler),
+    _Ring1 = set_handlers(Handlers1, Ring).
 
-match_eventer_test() ->
-    Event1 = {some_mod, some_type, some_node, {some_detail}},
-    Event2 = {some_mod, some_type, other_node, {some_detail}},
-    Event3 = {some_mod, other_type, other_node, {some_detail}},
-    Event4 = {other_mod, other_type, other_node, {some_detail}},
-    MS1 = {'_', '_', '_'},
-    MS2 = {some_node, '_', '_'},
-    MS3 = {some_node, some_mod, '_'},
-    MS4 = {some_node, some_mod, some_type},
-    ?assertEqual(match_event(MS1, Event1), true),
-    ?assertEqual(match_event(MS2, Event1), true),
-    ?assertEqual(match_event(MS2, Event2), false),
-    ?assertEqual(match_event(MS3, Event1), true),
-    ?assertEqual(match_event(MS3, Event3), false),
-    ?assertEqual(match_event(MS4, Event1), true),
-    ?assertEqual(match_event(MS4, Event4), false).
+%% remove_handler_from_ring/4 -
+%% Given part of an handler definition and a Ring, remove
+%% the matching handler from the ring.
+remove_handler_from_ring(Pid, MatchHead, MatchGuard, Ring) -> 
+    HandlerID = get_handler_id(Pid, MatchHead, MatchGuard),
+    remove_handler_from_ring(HandlerID, Ring).
+
+%% remove_handler_from_ring/2 -
+%% Given an HandlerID and a Ring, remove
+%% the matching handler from the ring.
+remove_handler_from_ring(HandlerID, Ring) -> 
+    % Remove the handler from the ring...
+    Handlers = get_handlers(Ring),
+    Handlers1 = lists:keydelete(HandlerID, 2, Handlers),
+    _Ring1 = set_handlers(Handlers1, Ring).
+  
+%% get_matching_handlers/2 -
+%% Given an event and a list of #handlers, look 
+%% through the handlers for all handlers that 
+%% should receive the event based on their matchspec.
+get_matching_handlers(Event, Handlers) ->
+    F = fun(H = #handler { matchhead=MatchHead, matchguard=MatchGuard }, Matches) ->
+        % NOTE: Compiled match_specs cannot be transfered across nodes,
+        % so we have to recompile each time. Don't worry, it's fast.
+        MS = ets:match_spec_compile([{MatchHead, MatchGuard, ['$$']}]),
+        case ets:match_spec_run([Event], MS) of
+            [_] -> [H|Matches];
+            _ -> Matches
+        end
+    end,
+    lists:foldl(F, [], Handlers).
+    
+%% Return the handlers in a ring...        
+get_handlers(Ring) ->
+    case riak_ring:get_meta(handlers, Ring) of
+        undefined -> [];
+        {ok, X} -> X
+    end.
+    
+%% Update a ring with a new set of handlers...
+set_handlers(Handlers, Ring) ->
+    riak_ring:update_meta(handlers, Handlers, Ring).
+    
+get_handler_id(Pid, MatchHead, MatchGuard) ->
+    erlang:md5(term_to_binary({Pid, MatchHead, MatchGuard})).
+    
+%% TESTS %%%
+    
+add_handler_to_ring_test() ->
+    application:set_env(riak, ring_creation_size, 16),
+    
</