paulb@312 | 1 | #!/usr/bin/env python |
paulb@312 | 2 | |
paulb@403 | 3 | """ |
paulb@403 | 4 | Mapping from names to resources. |
paulb@403 | 5 | |
paulb@599 | 6 | Copyright (C) 2004, 2005, 2007 Paul Boddie <paul@boddie.org.uk> |
paulb@403 | 7 | |
paulb@403 | 8 | This library is free software; you can redistribute it and/or |
paulb@403 | 9 | modify it under the terms of the GNU Lesser General Public |
paulb@403 | 10 | License as published by the Free Software Foundation; either |
paulb@403 | 11 | version 2.1 of the License, or (at your option) any later version. |
paulb@403 | 12 | |
paulb@403 | 13 | This library is distributed in the hope that it will be useful, |
paulb@403 | 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of |
paulb@403 | 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
paulb@403 | 16 | Lesser General Public License for more details. |
paulb@403 | 17 | |
paulb@403 | 18 | You should have received a copy of the GNU Lesser General Public |
paulb@403 | 19 | License along with this library; if not, write to the Free Software |
paulb@489 | 20 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA |
paulb@403 | 21 | """ |
paulb@312 | 22 | |
paulb@312 | 23 | import WebStack.Generic |
paulb@312 | 24 | |
paulb@312 | 25 | class MapResource: |
paulb@312 | 26 | |
paulb@312 | 27 | "A resource mapping names to other resources." |
paulb@312 | 28 | |
paulb@599 | 29 | def __init__(self, mapping, pass_through=0, directory_redirects=1, path_encoding=None, urlencoding=None): |
paulb@312 | 30 | |
paulb@312 | 31 | """ |
paulb@312 | 32 | Initialise the resource with a 'mapping' of names to resources. The |
paulb@312 | 33 | 'mapping' should be a dictionary-like object employing simple names |
paulb@426 | 34 | without "/" characters; the special value None is used to specify a |
paulb@426 | 35 | "catch all" resource which receives all requests whose virtual path |
paulb@426 | 36 | info does not match any of the names in the mapping. For example: |
paulb@426 | 37 | |
paulb@426 | 38 | mapping is {"mammals" : ..., "reptiles" : ..., None : ...} |
paulb@426 | 39 | |
paulb@426 | 40 | /mammals/cat -> matches "mammals" |
paulb@599 | 41 | |
paulb@426 | 42 | /reptiles/python -> matches "reptiles" |
paulb@599 | 43 | |
paulb@426 | 44 | /creatures/goblin -> no match, handled by None |
paulb@426 | 45 | |
paulb@426 | 46 | When this resource matches a name in the virtual path info to one of the |
paulb@426 | 47 | names in the mapping, it removes the section of the virtual path info |
paulb@426 | 48 | corresponding to that name before dispatching to the corresponding |
paulb@426 | 49 | resource. For example: |
paulb@426 | 50 | |
paulb@426 | 51 | /mammals/dog -> match with "mammals" in mapping -> /dog |
paulb@426 | 52 | |
paulb@426 | 53 | By default, where the first part of the virtual path info does not |
paulb@426 | 54 | correspond to any of the names in the mapping, the first piece of the |
paulb@426 | 55 | virtual path info is removed before dispatching to the "catch all" |
paulb@426 | 56 | resource. For example: |
paulb@426 | 57 | |
paulb@426 | 58 | /creatures/unicorn -> no match -> /unicorn |
paulb@426 | 59 | |
paulb@426 | 60 | However, the optional 'pass_through' parameter, if set to a true value |
paulb@426 | 61 | (which is not the default setting), changes the above behaviour in cases |
paulb@426 | 62 | where no matching name is found: in such cases, no part of the virtual |
paulb@426 | 63 | path info is removed, and the request is dispatched to the "catch all" |
paulb@426 | 64 | resource unchanged. For example: |
paulb@426 | 65 | |
paulb@426 | 66 | /creatures/unicorn -> no match -> /creatures/unicorn |
paulb@426 | 67 | |
paulb@426 | 68 | With 'pass_through' set to a true value, care must be taken if this |
paulb@426 | 69 | resource is set as its own "catch all" resource. For example: |
paulb@426 | 70 | |
paulb@426 | 71 | map_resource = MapResource(...) |
paulb@599 | 72 | |
paulb@426 | 73 | map_resource.mapping[None] = map_resource |
paulb@396 | 74 | |
paulb@396 | 75 | The optional 'directory_redirects' parameter, if set to a true value (as |
paulb@396 | 76 | is the default setting), causes a redirect adding a trailing "/" |
paulb@396 | 77 | character if the request path does not end with such a character. |
paulb@446 | 78 | |
paulb@599 | 79 | The optional 'path_encoding' (for which 'urlencoding' is a synonym) is |
paulb@599 | 80 | used to decode "URL encoded" character values in the request path, and |
paulb@599 | 81 | overrides the default encoding wherever possible. |
paulb@312 | 82 | """ |
paulb@312 | 83 | |
paulb@312 | 84 | self.mapping = mapping |
paulb@426 | 85 | self.pass_through = pass_through |
paulb@396 | 86 | self.directory_redirects = directory_redirects |
paulb@752 | 87 | self.path_encoding = path_encoding or urlencoding |
paulb@312 | 88 | |
paulb@312 | 89 | def respond(self, trans): |
paulb@312 | 90 | |
paulb@312 | 91 | """ |
paulb@312 | 92 | Using the path information from the given transaction 'trans', invoke |
paulb@312 | 93 | mapped resources. Otherwise return an error condition. |
paulb@312 | 94 | """ |
paulb@312 | 95 | |
paulb@312 | 96 | # Get the path info. |
paulb@312 | 97 | |
paulb@599 | 98 | parts = trans.get_virtual_path_info(self.path_encoding).split("/") |
paulb@312 | 99 | |
paulb@396 | 100 | # Where the published resource has a path info value defined (ie. its |
paulb@396 | 101 | # path info consists of a "/" character plus some other text), the first |
paulb@396 | 102 | # part should always be empty and there should always be a second part. |
paulb@396 | 103 | # Where the published resource has no path info defined, there will only |
paulb@422 | 104 | # be one part. In the latter case, we define the name to be the empty |
paulb@422 | 105 | # string, although the name will not be relevant if directory_redirects |
paulb@422 | 106 | # is set. |
paulb@312 | 107 | |
paulb@396 | 108 | if len(parts) > 1: |
paulb@396 | 109 | name = parts[1] |
paulb@396 | 110 | elif self.directory_redirects: |
paulb@396 | 111 | self.send_redirect(trans) |
paulb@396 | 112 | else: |
paulb@427 | 113 | self.send_error(trans) |
paulb@427 | 114 | return |
paulb@312 | 115 | |
paulb@312 | 116 | # Get the mapped resource. |
paulb@312 | 117 | |
paulb@312 | 118 | resource = self.mapping.get(name) |
paulb@386 | 119 | if resource is None: |
paulb@386 | 120 | resource = self.mapping.get(None) |
paulb@426 | 121 | catch_all_resource = 1 |
paulb@426 | 122 | else: |
paulb@426 | 123 | catch_all_resource = 0 |
paulb@312 | 124 | |
paulb@386 | 125 | # If a resource was found, change the transaction's path info. |
paulb@386 | 126 | # eg. "/this/next" -> "/next" |
paulb@386 | 127 | # eg. "/this/" -> "/" |
paulb@400 | 128 | # eg. "/this" -> "" |
paulb@426 | 129 | # Such changes are not made if the resource is in "pass through" mode |
paulb@426 | 130 | # and where the "catch all" resource is being used. In such situations |
paulb@426 | 131 | # this resource just passes control to the "catch all" resource along |
paulb@426 | 132 | # with all the path information intact. |
paulb@312 | 133 | |
paulb@426 | 134 | if not (catch_all_resource and self.pass_through): |
paulb@426 | 135 | new_path = parts[0:1] + parts[2:] |
paulb@426 | 136 | new_path_info = "/".join(new_path) |
paulb@426 | 137 | trans.set_virtual_path_info(new_path_info) |
paulb@312 | 138 | |
paulb@386 | 139 | # Invoke the transaction, transferring control completely. |
paulb@386 | 140 | |
paulb@312 | 141 | if resource is not None: |
paulb@312 | 142 | resource.respond(trans) |
paulb@312 | 143 | return |
paulb@312 | 144 | |
paulb@312 | 145 | # Otherwise, signal an error. |
paulb@312 | 146 | |
paulb@312 | 147 | self.send_error(trans) |
paulb@312 | 148 | |
paulb@312 | 149 | def send_error(self, trans): |
paulb@312 | 150 | |
paulb@312 | 151 | "Send the error using the given 'trans'." |
paulb@312 | 152 | |
paulb@312 | 153 | trans.set_response_code(404) |
paulb@312 | 154 | trans.set_content_type(WebStack.Generic.ContentType("text/plain")) |
paulb@312 | 155 | out = trans.get_response_stream() |
paulb@599 | 156 | out.write("Resource '%s' not found." % trans.get_path_info(self.path_encoding)) |
paulb@312 | 157 | |
paulb@396 | 158 | def send_redirect(self, trans): |
paulb@396 | 159 | |
paulb@396 | 160 | """ |
paulb@396 | 161 | Send a redirect using the given 'trans', adding a "/" character to the |
paulb@396 | 162 | end of the request path. |
paulb@396 | 163 | """ |
paulb@396 | 164 | |
paulb@599 | 165 | path_without_query = trans.get_path_without_query(self.path_encoding) |
paulb@396 | 166 | query_string = trans.get_query_string() |
paulb@396 | 167 | if query_string: |
paulb@396 | 168 | query_string = "?" + query_string |
paulb@599 | 169 | trans.redirect(trans.encode_path(path_without_query, self.path_encoding) + "/" + query_string) |
paulb@396 | 170 | |
paulb@752 | 171 | class SimpleMap(MapResource): |
paulb@752 | 172 | |
paulb@752 | 173 | "A simple mapping of names to resources, focused on the most common case." |
paulb@752 | 174 | |
paulb@752 | 175 | def __init__(self, mapping, directory_redirects=1): |
paulb@752 | 176 | |
paulb@752 | 177 | """ |
paulb@752 | 178 | Initialise the resource with the given 'mapping' and optional |
paulb@752 | 179 | 'directory_redirects' settings. Unlike MapResource, other options are |
paulb@752 | 180 | not exposed: if the 'mapping' has an entry for None, the 'pass_through' |
paulb@752 | 181 | option of MapResource is enabled. |
paulb@752 | 182 | """ |
paulb@752 | 183 | |
paulb@752 | 184 | MapResource.__init__(self, mapping, mapping.has_key(None), directory_redirects) |
paulb@752 | 185 | |
paulb@312 | 186 | # vim: tabstop=4 expandtab shiftwidth=4 |