1
# IVLE - Informatics Virtual Learning Environment
2
# Copyright (C) 2007-2009 The University of Melbourne
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
# GNU General Public License for more details.
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
18
"""Object traversal URL utilities."""
23
ROOT = object() # Marker object for the root.
25
class RoutingError(Exception):
28
class NotFound(RoutingError):
29
"""The path did not resolve to an object."""
32
class RoutelessObject(NotFound):
33
"""The path led to an object with no routes."""
36
class InsufficientPathSegments(NotFound):
37
"""The path led to a route that expected more arguments."""
40
class NoPath(RoutingError):
41
"""There is no path from the given object to the root."""
44
class RouteConflict(RoutingError):
45
"""A route with the same discriminator is already registered."""
48
def _segment_path(path):
49
"""Split a path into its segments, after normalisation.
51
>>> _segment_path('/path/to/something')
52
['path', 'to', 'something']
53
>>> _segment_path('/path/to/something/')
54
['path', 'to', 'something']
55
>>> _segment_path('/')
59
segments = os.path.normpath(path).split('/')
61
# Remove empty segments caused by leading and trailing seperators.
64
if segments[-1] == '':
69
'''Router to resolve and generate paths.
71
Maintains a registry of forward and reverse routes, dealing with paths
72
to objects and views published in the URL space.
75
def __init__(self, root, default='+index'):
76
self.fmap = {} # Forward map.
77
self.rmap = {} # Reverse map.
79
self.default = default
81
def add_forward(self, src, segment, func, argc):
82
"""Register a forward (path resolution) route."""
84
if src not in self.fmap:
87
# If a route already exists with the same source and name, we have a
88
# conflict. We don't like conflicts.
89
if segment in self.fmap[src]:
90
raise RouteConflict((src, segment, func),
91
(src, segment, self.fmap[src][segment][0]))
93
self.fmap[src][segment] = (func, argc)
96
def add_reverse(self, src, func):
97
"""Register a reverse (path generation) route."""
100
raise RouteConflict((src, func), (src, self.rmap[src]))
101
self.rmap[src] = func
103
def resolve(self, path):
104
"""Resolve a path into an object.
106
Traverses the tree of routes using the given path.
109
(obj, todo) = self._traverse(_segment_path(path), self.root)
111
# If we have no segments remaining, let's try the default route.
112
# If there is no default route for this object, we don't care.
114
obj = self._traverse([self.default], obj)[0]
118
def generate(self, obj):
119
"""Resolve an object into a path.
121
Traverse up the tree of reverse routes, generating a path which
122
resolves to the object.
125
# Attempt to get all the way to the top. Each reverse route should
126
# return a (parent, pathsegments) tuple.
130
# None represents the root.
131
while curobj is not ROOT:
132
route = self.rmap.get(type(curobj))
134
raise NoPath(obj, curobj)
135
(curobj, newnames) = route(curobj)
137
# The reverse route can return either a string of one segment,
138
# or a tuple of many.
139
if isinstance(newnames, basestring):
140
names.insert(0, newnames)
142
names = list(newnames) + list(names)
144
# Generate nice URLs for the default route, if it is the last. We need
145
# to have an empty path rather than none at all in order to preserve
147
if names[-1] == self.default:
150
return os.path.join('/', *names)
152
def _traverse(self, todo, obj):
153
"""Populate the object stack given a list of path segments.
155
Traverses the forward route tree, using the given path segments.
157
Intended to be used by route(), and nobody else.
160
# Check if we have any routes for this object at all.
162
names = self.fmap[type(obj)]
166
routebits = names.get(todo[0])
168
if routebits is not None:
169
route, argc = routebits
170
# The first path segment is the route identifier, so we skip
171
# it when identifying arguments.
173
args = todo[1:argc + 1]
174
todo = todo[argc + 1:]
176
# Attempt traversal directly (with no intermediate segment)
179
route, argc = names[None]
184
raise NotFound(obj, todo[0])
186
if len(args) != argc:
187
# There were too few path segments left. Die.
188
raise InsufficientPathSegments(obj, lastseg, len(args))
190
obj = route(obj, *args)