* make RESTful interface use absolute URIs throughout.  there's little price to this and it makes the system more consistent, makes responses valid standalone data, simplifies the job of the clients, and could be useful in building services that operate across multiple autotest servers in the future.
* since generating abolute URIs requires the request object (we need to examine headers), there were some substantial refactorings to pass around the request object more widely.
* also fixing a little bug where the django exceptions module was being hidden.
* finally, refactored request code in rest_client a bit to improve testability

Signed-off-by: Steve Howard <showard@google.com>


git-svn-id: http://test.kernel.org/svn/autotest/trunk@4195 592f7852-d20e-0410-864c-8624ca9c26a4
diff --git a/frontend/afe/resources.py b/frontend/afe/resources.py
index 966bfd9..52ac751 100644
--- a/frontend/afe/resources.py
+++ b/frontend/afe/resources.py
@@ -5,22 +5,22 @@
 from autotest_lib.client.common_lib import host_protections
 
 class EntryWithInvalid(resource_lib.Entry):
-    def put(self, request):
+    def put(self):
         if self.instance.invalid:
-            raise http.Http404
-        return super(EntryWithInvalid, self).put(request)
+            raise http.Http404('%s has been deleted' % self.instance)
+        return super(EntryWithInvalid, self).put()
 
 
-    def delete(self, request):
+    def delete(self):
         if self.instance.invalid:
-            raise http.Http404
-        return super(EntryWithInvalid, self).delete(request)
+            raise http.Http404('%s has already been deleted' % self.instance)
+        return super(EntryWithInvalid, self).delete()
 
 
 class AtomicGroupClass(EntryWithInvalid):
     @classmethod
-    def from_uri_args(cls, name):
-        return cls(models.AtomicGroup.objects.get(name=name))
+    def from_uri_args(cls, request, name):
+        return cls(request, models.AtomicGroup.objects.get(name=name))
 
 
     def _uri_args(self):
@@ -81,8 +81,8 @@
 
 
     @classmethod
-    def from_uri_args(cls, name):
-        return cls(models.Label.objects.get(name=name))
+    def from_uri_args(cls, request, name):
+        return cls(request, models.Label.objects.get(name=name))
 
 
     def _uri_args(self):
@@ -98,9 +98,10 @@
 
     def full_representation(self):
         rep = super(Label, self).full_representation()
-        atomic_group_class_rep = AtomicGroupClass.from_optional_instance(
-                self.instance.atomic_group).short_representation()
-        rep.update({'atomic_group_class': atomic_group_class_rep,
+        atomic_group_class = AtomicGroupClass.from_optional_instance(
+                self._request, self.instance.atomic_group)
+        rep.update({'atomic_group_class':
+                        atomic_group_class.short_representation(),
                     'hosts': LabelHosts(self).link()})
         return rep
 
@@ -139,10 +140,10 @@
 
 
     @classmethod
-    def from_uri_args(cls, username):
+    def from_uri_args(cls, request, username):
         if username == '@me':
-            return cls(models.User.current_user())
-        return cls(models.User.objects.get(login=username))
+            username = models.User.current_user().login
+        return cls(request, models.User.objects.get(login=username))
 
 
     def _uri_args(self):
@@ -199,8 +200,8 @@
     _permitted_methods = ('GET',)
 
     @classmethod
-    def from_uri_args(cls, name):
-        return cls(models.AclGroup.objects.get(name=name))
+    def from_uri_args(cls, request, name):
+        return cls(request, models.AclGroup.objects.get(name=name))
 
 
     def _uri_args(self):
@@ -284,8 +285,8 @@
 
 
     @classmethod
-    def from_uri_args(cls, hostname):
-        return cls(models.Host.objects.get(hostname=hostname))
+    def from_uri_args(cls, request, hostname):
+        return cls(request, models.Host.objects.get(hostname=hostname))
 
 
     def _uri_args(self):
@@ -295,7 +296,8 @@
     def short_representation(self):
         rep = super(Host, self).short_representation()
         # TODO calling platform() over and over is inefficient
-        platform_rep = (Label.from_optional_instance(self.instance.platform())
+        platform_rep = (Label.from_optional_instance(self._request,
+                                                     self.instance.platform())
                         .short_representation())
         rep.update({'hostname': self.instance.hostname,
                     'locked': bool(self.instance.locked),
@@ -308,7 +310,8 @@
         rep = super(Host, self).full_representation()
         protection = host_protections.Protection.get_string(
                 self.instance.protection)
-        locked_by = (User.from_optional_instance(self.instance.locked_by)
+        locked_by = (User.from_optional_instance(self._request,
+                                                 self.instance.locked_by)
                      .short_representation())
         rep.update({'locked_by': locked_by,
                     'locked_on': self._format_datetime(self.instance.lock_time),
@@ -331,7 +334,7 @@
                                           locked=True)
 
         if 'acls' in input_dict:
-            entry = Host(instance)
+            entry = Host(containing_collection._request, instance)
             HostAcls(entry).update(input_dict['acls'])
 
         instance.locked = False # restore default
@@ -350,8 +353,7 @@
         self.instance.update_object(**data)
 
         if 'platform' in input_dict:
-            label = (resource_lib.Resource.resolve_link(input_dict['platform'])
-                     .instance)
+            label = self.resolve_link(input_dict['platform']) .instance
             if not label.platform:
                 raise BadRequest('Label %s is not a platform' % label.name)
             for label in self.instance.labels.filter(platform=True):
@@ -415,8 +417,8 @@
 
 class Test(resource_lib.Entry):
     @classmethod
-    def from_uri_args(cls, name):
-        return cls(models.Test.objects.get(name=name))
+    def from_uri_args(cls, request, name):
+        return cls(request, models.Test.objects.get(name=name))
 
 
     def _uri_args(self):
@@ -569,9 +571,10 @@
                     machines_per_execution=cf_info['synch_count'])
 
 
-    def handle_request(self, request):
+    def handle_request(self):
         result = self.link()
-        result['execution_info'] = self._get_execution_info(request.REQUEST)
+        result['execution_info'] = self._get_execution_info(
+                self._request.REQUEST)
         return self._basic_response(result)
 
 
@@ -597,8 +600,8 @@
         return []
 
 
-    def handle_request(self, request):
-        request_dict = request.REQUEST
+    def handle_request(self):
+        request_dict = self._request.REQUEST
         hosts = self._read_list(request_dict.get('hosts'))
         one_time_hosts = self._read_list(request_dict.get('one_time_hosts'))
         meta_hosts = self._read_list(request_dict.get('meta_hosts'))
@@ -610,10 +613,10 @@
         for hostname in one_time_hosts:
             models.Host.create_one_time_host(hostname)
         for hostname in hosts:
-            entry = Host.from_uri_args(hostname)
+            entry = Host.from_uri_args(self._request, hostname)
             entries.append({'host': entry.link()})
         for label_name in meta_hosts:
-            entry = Label.from_uri_args(label_name)
+            entry = Label.from_uri_args(self._request, label_name)
             entries.append({'meta_host': entry.link()})
 
         result = self.link()
@@ -659,8 +662,8 @@
 
 
     @classmethod
-    def from_uri_args(cls, job_id):
-        return cls(models.Job.objects.get(id=job_id))
+    def from_uri_args(cls, request, job_id):
+        return cls(request, models.Job.objects.get(id=job_id))
 
 
     def _uri_args(self):
@@ -726,14 +729,14 @@
             if 'host' in queue_entry:
                 host = queue_entry['host']
                 if host: # can be None, indicated a hostless job
-                    host_entry = resource_lib.Resource.resolve_link(host)
+                    host_entry = containing_collection.resolve_link(host)
                     host_objects.append(host_entry.instance)
             elif 'meta_host' in queue_entry:
-                label_entry = resource_lib.Resource.resolve_link(
+                label_entry = containing_collection.resolve_link(
                         queue_entry['meta_host'])
                 metahost_label_objects.append(label_entry.instance)
             if 'atomic_group' in queue_entry:
-                atomic_group_entry = resource_lib.Resource.resolve_link(
+                atomic_group_entry = containing_collection.resolve_link(
                         queue_entry['atomic_group'])
                 if atomic_group:
                     assert atomic_group_entry.instance.id == atomic_group.id
@@ -773,12 +776,12 @@
     _permitted_methods = ('GET', 'PUT')
 
     @classmethod
-    def from_uri_args(cls, job_id, queue_entry_id):
+    def from_uri_args(cls, request, job_id, queue_entry_id):
         instance = models.HostQueueEntry.objects.get(id=queue_entry_id)
         if instance.job.id != int(job_id):
             raise http.Http404('Incorrect job ID %r (expected %r)'
                                % (job_id, instance.job.id))
-        return cls(instance)
+        return cls(request, instance)
 
 
     def _uri_args(self):
@@ -788,18 +791,22 @@
     def short_representation(self):
         rep = super(QueueEntry, self).short_representation()
         if self.instance.host:
-            host = Host(self.instance.host).short_representation()
+            host = (Host(self._request, self.instance.host)
+                    .short_representation())
         else:
             host = None
+        job = Job(self._request, self.instance.job)
+        host = Host.from_optional_instance(self._request, self.instance.host)
+        label = Label.from_optional_instance(self._request,
+                                             self.instance.meta_host)
+        atomic_group_class = AtomicGroupClass.from_optional_instance(
+                self._request, self.instance.atomic_group)
         rep.update(
-                {'job': Job(self.instance.job).short_representation(),
-                 'host': (Host.from_optional_instance(self.instance.host)
-                          .short_representation()),
-                 'label': (Label.from_optional_instance(self.instance.meta_host)
-                           .short_representation()),
+                {'job': job.short_representation(),
+                 'host': host.short_representation(),
+                 'label': label.short_representation(),
                  'atomic_group_class':
-                 AtomicGroupClass.from_optional_instance(
-                     self.instance.atomic_group).short_representation(),
+                     atomic_group_class.short_representation(),
                  'status': self.instance.status,
                  'execution_path': self.instance.execution_subdir,
                  'started_on': self._format_datetime(self.instance.started_on),
@@ -821,12 +828,12 @@
     _permitted_methods = ('GET',)
 
     @classmethod
-    def from_uri_args(cls, hostname, task_id):
+    def from_uri_args(cls, request, hostname, task_id):
         instance = models.SpecialTask.objects.get(id=task_id)
         if instance.host.hostname != hostname:
             raise http.Http404('Incorrect hostname %r (expected %r)'
                                % (hostname, instance.host.hostname))
-        return cls(instance)
+        return cls(request, instance)
 
 
     def _uri_args(self):
@@ -835,14 +842,16 @@
 
     def short_representation(self):
         rep = super(HealthTask, self).short_representation()
+        host = Host(self._request, self.instance.host)
+        queue_entry = QueueEntry.from_optional_instance(
+                self._request, self.instance.queue_entry)
         rep.update(
-                {'host': Host(self.instance.host).short_representation(),
+                {'host': host.short_representation(),
                  'task_type': self.instance.task,
                  'started_on':
                      self._format_datetime(self.instance.time_started),
                  'status': self.instance.status,
-                 'queue_entry': QueueEntry.from_optional_instance(
-                        self.instance.queue_entry).short_representation()
+                 'queue_entry': queue_entry.short_representation()
                  })
         return rep
 
@@ -864,17 +873,19 @@
 class ResourceDirectory(resource_lib.Resource):
     _permitted_methods = ('GET',)
 
-    def handle_request(self, request):
+    def handle_request(self):
         result = self.link()
         result.update({
-                'atomic_group_classes': AtomicGroupClassCollection().link(),
-                'labels': LabelCollection().link(),
-                'users': UserCollection().link(),
-                'acl_groups': AclCollection().link(),
-                'hosts': HostCollection().link(),
-                'tests': TestCollection().link(),
-                'execution_info': ExecutionInfo().link(),
-                'queue_entries_request': QueueEntriesRequest().link(),
-                'jobs': JobCollection().link(),
+                'atomic_group_classes':
+                AtomicGroupClassCollection(self._request).link(),
+                'labels': LabelCollection(self._request).link(),
+                'users': UserCollection(self._request).link(),
+                'acl_groups': AclCollection(self._request).link(),
+                'hosts': HostCollection(self._request).link(),
+                'tests': TestCollection(self._request).link(),
+                'execution_info': ExecutionInfo(self._request).link(),
+                'queue_entries_request':
+                QueueEntriesRequest(self._request).link(),
+                'jobs': JobCollection(self._request).link(),
                 })
         return self._basic_response(result)
diff --git a/frontend/afe/resources_test.py b/frontend/afe/resources_test.py
index 0ea5a79..afc6356 100644
--- a/frontend/afe/resources_test.py
+++ b/frontend/afe/resources_test.py
@@ -10,7 +10,7 @@
 
 class ResourceTestCase(unittest.TestCase,
                        frontend_test_utils.FrontendTestMixin):
-    URI_PREFIX = '/afe/server/resources'
+    URI_PREFIX = 'http://testserver/afe/server/resources'
 
     CONTROL_FILE_CONTENTS = 'my control file contents'
 
diff --git a/frontend/shared/resource_lib.py b/frontend/shared/resource_lib.py
index cf98acd..a26acd4 100644
--- a/frontend/shared/resource_lib.py
+++ b/frontend/shared/resource_lib.py
@@ -1,6 +1,7 @@
-import cgi, datetime, time, urllib
+import cgi, datetime, re, time, urllib
 from django import http
-from django.core import exceptions, urlresolvers
+import django.core.exceptions
+from django.core import urlresolvers
 from django.utils import simplejson
 from autotest_lib.frontend.shared import exceptions, query_lib
 from autotest_lib.frontend.afe import model_logic
@@ -32,27 +33,28 @@
     _permitted_methods = None # subclasses must override this
 
 
-    def __init__(self):
+    def __init__(self, request):
         assert self._permitted_methods
+        self._request = request
 
 
     @classmethod
     def dispatch_request(cls, request, *args, **kwargs):
         # handle a request directly
         try:
-            instance = cls.from_uri_args(*args, **kwargs)
-        except exceptions.ObjectDoesNotExist, exc:
+            instance = cls.from_uri_args(request, *args, **kwargs)
+        except django.core.exceptions.ObjectDoesNotExist, exc:
             raise http.Http404(exc)
-        return instance.handle_request(request)
+        return instance.handle_request()
 
 
-    def handle_request(self, request):
-        if request.method.upper() not in self._permitted_methods:
+    def handle_request(self):
+        if self._request.method.upper() not in self._permitted_methods:
             return http.HttpResponseNotAllowed(self._permitted_methods)
 
-        handler = getattr(self, request.method.lower())
+        handler = getattr(self, self._request.method.lower())
         try:
-            return handler(request)
+            return handler()
         except exceptions.RequestError, exc:
             return exc.response
 
@@ -60,7 +62,7 @@
     # the handler methods below only need to be overridden if the resource
     # supports the method
 
-    def get(self, request):
+    def get(self):
         """Handle a GET request.
 
         @returns an HttpResponse
@@ -68,7 +70,7 @@
         raise NotImplementedError
 
 
-    def post(self, request):
+    def post(self):
         """Handle a POST request.
 
         @returns an HttpResponse
@@ -76,7 +78,7 @@
         raise NotImplementedError
 
 
-    def put(self, request):
+    def put(self):
         """Handle a PUT request.
 
         @returns an HttpResponse
@@ -84,7 +86,7 @@
         raise NotImplementedError
 
 
-    def delete(self, request):
+    def delete(self):
         """Handle a DELETE request.
 
         @returns an HttpResponse
@@ -93,12 +95,12 @@
 
 
     @classmethod
-    def from_uri_args(cls):
+    def from_uri_args(cls, request):
         """Construct an instance from URI args.
 
         Default implementation for resources with no URI args.
         """
-        return cls()
+        return cls(request)
 
 
     def _uri_args(self):
@@ -121,26 +123,37 @@
     def href(self):
         """Return URI to this resource."""
         args, kwargs = self._uri_args()
-        return urlresolvers.reverse(self.dispatch_request, args=args,
+        path = urlresolvers.reverse(self.dispatch_request, args=args,
                                     kwargs=kwargs)
+        return self._request.build_absolute_uri(path)
 
 
-    @classmethod
-    def resolve_uri(cls, uri):
+    def resolve_uri(self, uri):
+        # check for absolute URIs
+        match = re.match(r'(?P<root>https?://[^/]+)(?P<path>/.*)', uri)
+        if match:
+            # is this URI for a different host?
+            my_root = self._request.build_absolute_uri('/')
+            request_root = match.group('root') + '/'
+            if my_root != request_root:
+                # might support this in the future, but not now
+                raise exceptions.BadRequest('Unable to resolve remote URI %s'
+                                            % uri)
+            uri = match.group('path')
+
         view_method, args, kwargs = urlresolvers.resolve(uri)
         resource_class = view_method.im_self # class owning this classmethod
-        return resource_class.from_uri_args(*args, **kwargs)
+        return resource_class.from_uri_args(self._request, *args, **kwargs)
 
 
-    @classmethod
-    def resolve_link(cls, link):
+    def resolve_link(self, link):
         if isinstance(link, dict):
             uri = link['href']
         elif isinstance(link, basestring):
             uri = link
         else:
             raise exceptions.BadRequest('Unable to understand link %s' % link)
-        return cls.resolve_uri(uri)
+        return self.resolve_uri(uri)
 
 
     def link(self):
@@ -163,10 +176,10 @@
                                  content_type=_JSON_CONTENT_TYPE)
 
 
-    @classmethod
-    def _decoded_input(cls, request):
-        content_type = request.META.get('CONTENT_TYPE', _JSON_CONTENT_TYPE)
-        raw_data = request.raw_post_data
+    def _decoded_input(self):
+        content_type = self._request.META.get('CONTENT_TYPE',
+                                              _JSON_CONTENT_TYPE)
+        raw_data = self._request.raw_post_data
         if content_type == _JSON_CONTENT_TYPE:
             try:
                 raw_dict = simplejson.loads(raw_data)
@@ -232,16 +245,16 @@
     QueryProcessor = query_lib.BaseQueryProcessor
 
 
-    def __init__(self, instance):
-        super(Entry, self).__init__()
+    def __init__(self, request, instance):
+        super(Entry, self).__init__(request)
         self.instance = instance
 
 
     @classmethod
-    def from_optional_instance(cls, instance):
+    def from_optional_instance(cls, request, instance):
         if instance is None:
             return cls._null_entry
-        return cls(instance)
+        return cls(request, instance)
 
 
     def short_representation(self):
@@ -252,19 +265,19 @@
         return self.short_representation()
 
 
-    def get(self, request):
+    def get(self):
         return self._basic_response(self.full_representation())
 
 
-    def put(self, request):
+    def put(self):
         try:
-            self.update(self._decoded_input(request))
+            self.update(self._decoded_input())
         except model_logic.ValidationError, exc:
             raise exceptions.BadRequest('Invalid input: %s' % exc)
         return self._basic_response(self.full_representation())
 
 
-    def delete(self, request):
+    def delete(self):
         self.instance.delete()
         return http.HttpResponse(status=204) # No content
 
@@ -287,8 +300,8 @@
     entry_class = None
 
 
-    def __init__(self):
-        super(Collection, self).__init__()
+    def __init__(self, request):
+        super(Collection, self).__init__(request)
         assert self.entry_class is not None
         if isinstance(self.entry_class, basestring):
             type(self).entry_class = _resolve_class_path(self.entry_class)
@@ -305,7 +318,7 @@
     def _representation(self, entry_instances):
         members = []
         for instance in entry_instances:
-            entry = self.entry_class(instance)
+            entry = self.entry_class(self._request, instance)
             members.append(entry.short_representation())
 
         rep = self.link()
@@ -313,7 +326,8 @@
         return rep
 
 
-    def _read_int_parameter(self, query_dict, name, default):
+    def _read_int_parameter(self, name, default):
+        query_dict = self._request.GET
         if name not in query_dict:
             return default
         input_value = query_dict[name]
@@ -324,9 +338,9 @@
                                         % (name, input_value))
 
 
-    def _apply_form_query(self, request, queryset):
+    def _apply_form_query(self, queryset):
         """Apply any query selectors passed as form variables."""
-        for parameter, values in request.GET.lists():
+        for parameter, values in self._request.GET.lists():
             if not self._query_processor.has_selector(parameter):
                 continue
             for value in values: # forms keys can have multiple values
@@ -336,16 +350,16 @@
         return queryset
 
 
-    def _filtered_queryset(self, request):
-        return self._apply_form_query(request, self._fresh_queryset())
+    def _filtered_queryset(self):
+        return self._apply_form_query(self._fresh_queryset())
 
 
-    def get(self, request):
-        queryset = self._filtered_queryset(request)
+    def get(self):
+        queryset = self._filtered_queryset()
 
-        items_per_page = self._read_int_parameter(request.GET, 'items_per_page',
+        items_per_page = self._read_int_parameter('items_per_page',
                                                   self._DEFAULT_ITEMS_PER_PAGE)
-        start_index = self._read_int_parameter(request.GET, 'start_index', 0)
+        start_index = self._read_int_parameter('start_index', 0)
         page = queryset[start_index:(start_index + items_per_page)]
 
         rep = self._representation(page)
@@ -364,11 +378,11 @@
         return self._representation(self._fresh_queryset())
 
 
-    def post(self, request):
-        input_dict = self._decoded_input(request)
+    def post(self):
+        input_dict = self._decoded_input()
         try:
             instance = self.entry_class.create_instance(input_dict, self)
-            entry = self.entry_class(instance)
+            entry = self.entry_class(self._request, instance)
             entry.update(input_dict)
         except model_logic.ValidationError, exc:
             raise exceptions.BadRequest('Invalid input: %s' % exc)
@@ -393,7 +407,7 @@
                     self.base_entry_class)
         assert isinstance(base_entry, self.base_entry_class)
         self.base_entry = base_entry
-        super(Relationship, self).__init__()
+        super(Relationship, self).__init__(base_entry._request)
 
 
     def _fresh_queryset(self):
@@ -402,8 +416,9 @@
 
 
     @classmethod
-    def from_uri_args(cls, *args, **kwargs):
-        base_entry = cls.base_entry_class.from_uri_args(*args, **kwargs)
+    def from_uri_args(cls, request, *args, **kwargs):
+        base_entry = cls.base_entry_class.from_uri_args(request, *args,
+                                                        **kwargs)
         return cls(base_entry)
 
 
@@ -412,16 +427,12 @@
 
 
     @classmethod
-    def _read_hrefs(cls, links):
-        return [link['href'] for link in links]
-
-
-    @classmethod
-    def _input_collection_hrefs(cls, input_data):
+    def _input_collection_links(cls, input_data):
         """Get the members of a user-provided collection.
 
         Tries to be flexible about formats accepted from the user.
-        @returns a list of hrefs
+        @returns a list of links, possibly only href strings (use
+                resolve_link())
         """
         if isinstance(input_data, dict) and 'members' in input_data:
             # this mirrors the output representation for collections
@@ -435,30 +446,22 @@
                 raise exceptions.BadRequest('You must retreive the full '
                                             'collection to perform updates')
 
-            return cls._read_hrefs(input_data['members'])
+            return input_data['members']
         if isinstance(input_data, list):
-            if not input_data:
-                return input_data
-            if isinstance(input_data[0], dict):
-                # assume it's a list of links
-                return cls._read_hrefs(input_data)
-            if isinstance(input_data[0], basestring):
-                # assume it's a list of hrefs
-                return input_data
+            return input_data
         raise exceptions.BadRequest('Cannot understand collection in input: %r'
                                     % input_data)
 
 
-    def put(self, request):
-        input_data = self._decoded_input(request)
+    def put(self):
+        input_data = self._decoded_input()
         self.update(input_data)
-        return self.get(request)
+        return self.get()
 
 
     def update(self, input_data):
-        hrefs = self._input_collection_hrefs(input_data)
-        instances = [self.entry_class.resolve_uri(href).instance
-                     for href in hrefs]
+        links = self._input_collection_links(input_data)
+        instances = [self.resolve_link(link).instance for link in links]
         self._update_relationship(instances)
 
 
diff --git a/frontend/shared/rest_client.py b/frontend/shared/rest_client.py
index d6aca5d..8525e8d 100644
--- a/frontend/shared/rest_client.py
+++ b/frontend/shared/rest_client.py
@@ -3,9 +3,6 @@
 from django.utils import simplejson
 
 
-_RESOURCE_DIRECTORY_PATH = '/afe/server/resources/'
-
-
 _http = httplib2.Http()
 
 
@@ -37,9 +34,7 @@
 
 
 class Resource(object):
-    def __init__(self, base_uri, representation_dict):
-        self._base_uri = base_uri
-
+    def __init__(self, representation_dict):
         assert 'href' in representation_dict
         for key, value in representation_dict.iteritems():
             setattr(self, str(key), value)
@@ -55,8 +50,8 @@
 
 
     @classmethod
-    def directory(cls, base_uri):
-        directory = cls(base_uri, {'href': _RESOURCE_DIRECTORY_PATH})
+    def load(cls, uri):
+        directory = cls({'href': uri})
         return directory.get()
 
 
@@ -68,7 +63,7 @@
             converted_dict = dict((key, self._read_representation(sub_value))
                                   for key, sub_value in value.iteritems())
             if 'href' in converted_dict:
-                return type(self)(self._base_uri, converted_dict)
+                return type(self)(converted_dict)
             return converted_dict
         return value
 
@@ -92,30 +87,37 @@
                     and not callable(value))
 
 
-    def _request(self, method, query_parameters=None, encoded_body=None):
-        uri_parts = []
-        if not re.match(r'^https?://', self.href):
-            uri_parts.append(self._base_uri)
-        uri_parts.append(self.href)
+    @classmethod
+    def _do_request(self, method, uri, query_parameters, encoded_body):
         if query_parameters:
-            query_string = urllib.urlencode(query_parameters)
-            uri_parts.extend(['?', query_string])
+            query_string = '?' + urllib.urlencode(query_parameters)
+        else:
+            query_string = ''
+        full_uri = uri + query_string
 
         if encoded_body:
             entity_body = simplejson.dumps(encoded_body)
         else:
             entity_body = None
 
-        full_uri = ''.join(uri_parts)
         logging.debug('%s %s', method, full_uri)
         if entity_body:
             logging.debug(entity_body)
         headers, response_body = _http.request(
-                ''.join(uri_parts), method, body=entity_body,
+                full_uri, method, body=entity_body,
                 headers={'Content-Type': 'application/json'})
         logging.debug('Response: %s', headers['status'])
 
-        response = Response(headers, response_body)
+        return Response(headers, response_body)
+
+
+    def _request(self, method, query_parameters=None, encoded_body=None):
+        if query_parameters is None:
+            query_parameters = {}
+
+        response = self._do_request(method, self.href, query_parameters,
+                                    encoded_body)
+
         if 300 <= response.status < 400: # redirection
             raise NotImplementedError(str(response)) # TODO
         if 400 <= response.status < 500: