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 | path_encoding = "utf-8" |
paulb@599 | 30 | |
paulb@599 | 31 | def __init__(self, mapping, pass_through=0, directory_redirects=1, path_encoding=None, urlencoding=None): |
paulb@312 | 32 | |
paulb@312 | 33 | """ |
paulb@312 | 34 | Initialise the resource with a 'mapping' of names to resources. The |
paulb@312 | 35 | 'mapping' should be a dictionary-like object employing simple names |
paulb@426 | 36 | without "/" characters; the special value None is used to specify a |
paulb@426 | 37 | "catch all" resource which receives all requests whose virtual path |
paulb@426 | 38 | info does not match any of the names in the mapping. For example: |
paulb@426 | 39 | |
paulb@426 | 40 | mapping is {"mammals" : ..., "reptiles" : ..., None : ...} |
paulb@426 | 41 | |
paulb@426 | 42 | /mammals/cat -> matches "mammals" |
paulb@599 | 43 | |
paulb@426 | 44 | /reptiles/python -> matches "reptiles" |
paulb@599 | 45 | |
paulb@426 | 46 | /creatures/goblin -> no match, handled by None |
paulb@426 | 47 | |
paulb@426 | 48 | When this resource matches a name in the virtual path info to one of the |
paulb@426 | 49 | names in the mapping, it removes the section of the virtual path info |
paulb@426 | 50 | corresponding to that name before dispatching to the corresponding |
paulb@426 | 51 | resource. For example: |
paulb@426 | 52 | |
paulb@426 | 53 | /mammals/dog -> match with "mammals" in mapping -> /dog |
paulb@426 | 54 | |
paulb@426 | 55 | By default, where the first part of the virtual path info does not |
paulb@426 | 56 | correspond to any of the names in the mapping, the first piece of the |
paulb@426 | 57 | virtual path info is removed before dispatching to the "catch all" |
paulb@426 | 58 | resource. For example: |
paulb@426 | 59 | |
paulb@426 | 60 | /creatures/unicorn -> no match -> /unicorn |
paulb@426 | 61 | |
paulb@426 | 62 | However, the optional 'pass_through' parameter, if set to a true value |
paulb@426 | 63 | (which is not the default setting), changes the above behaviour in cases |
paulb@426 | 64 | where no matching name is found: in such cases, no part of the virtual |
paulb@426 | 65 | path info is removed, and the request is dispatched to the "catch all" |
paulb@426 | 66 | resource unchanged. For example: |
paulb@426 | 67 | |
paulb@426 | 68 | /creatures/unicorn -> no match -> /creatures/unicorn |
paulb@426 | 69 | |
paulb@426 | 70 | With 'pass_through' set to a true value, care must be taken if this |
paulb@426 | 71 | resource is set as its own "catch all" resource. For example: |
paulb@426 | 72 | |
paulb@426 | 73 | map_resource = MapResource(...) |
paulb@599 | 74 | |
paulb@426 | 75 | map_resource.mapping[None] = map_resource |
paulb@396 | 76 | |
paulb@396 | 77 | The optional 'directory_redirects' parameter, if set to a true value (as |
paulb@396 | 78 | is the default setting), causes a redirect adding a trailing "/" |
paulb@396 | 79 | character if the request path does not end with such a character. |
paulb@446 | 80 | |
paulb@599 | 81 | The optional 'path_encoding' (for which 'urlencoding' is a synonym) is |
paulb@599 | 82 | used to decode "URL encoded" character values in the request path, and |
paulb@599 | 83 | overrides the default encoding wherever possible. |
paulb@312 | 84 | """ |
paulb@312 | 85 | |
paulb@312 | 86 | self.mapping = mapping |
paulb@426 | 87 | self.pass_through = pass_through |
paulb@396 | 88 | self.directory_redirects = directory_redirects |
paulb@599 | 89 | self.path_encoding = path_encoding or urlencoding or self.path_encoding |
paulb@312 | 90 | |
paulb@312 | 91 | def respond(self, trans): |
paulb@312 | 92 | |
paulb@312 | 93 | """ |
paulb@312 | 94 | Using the path information from the given transaction 'trans', invoke |
paulb@312 | 95 | mapped resources. Otherwise return an error condition. |
paulb@312 | 96 | """ |
paulb@312 | 97 | |
paulb@312 | 98 | # Get the path info. |
paulb@312 | 99 | |
paulb@599 | 100 | parts = trans.get_virtual_path_info(self.path_encoding).split("/") |
paulb@312 | 101 | |
paulb@396 | 102 | # Where the published resource has a path info value defined (ie. its |
paulb@396 | 103 | # path info consists of a "/" character plus some other text), the first |
paulb@396 | 104 | # part should always be empty and there should always be a second part. |
paulb@396 | 105 | # Where the published resource has no path info defined, there will only |
paulb@422 | 106 | # be one part. In the latter case, we define the name to be the empty |
paulb@422 | 107 | # string, although the name will not be relevant if directory_redirects |
paulb@422 | 108 | # is set. |
paulb@312 | 109 | |
paulb@396 | 110 | if len(parts) > 1: |
paulb@396 | 111 | name = parts[1] |
paulb@396 | 112 | elif self.directory_redirects: |
paulb@396 | 113 | self.send_redirect(trans) |
paulb@396 | 114 | else: |
paulb@427 | 115 | self.send_error(trans) |
paulb@427 | 116 | return |
paulb@312 | 117 | |
paulb@312 | 118 | # Get the mapped resource. |
paulb@312 | 119 | |
paulb@312 | 120 | resource = self.mapping.get(name) |
paulb@386 | 121 | if resource is None: |
paulb@386 | 122 | resource = self.mapping.get(None) |
paulb@426 | 123 | catch_all_resource = 1 |
paulb@426 | 124 | else: |
paulb@426 | 125 | catch_all_resource = 0 |
paulb@312 | 126 | |
paulb@386 | 127 | # If a resource was found, change the transaction's path info. |
paulb@386 | 128 | # eg. "/this/next" -> "/next" |
paulb@386 | 129 | # eg. "/this/" -> "/" |
paulb@400 | 130 | # eg. "/this" -> "" |
paulb@426 | 131 | # Such changes are not made if the resource is in "pass through" mode |
paulb@426 | 132 | # and where the "catch all" resource is being used. In such situations |
paulb@426 | 133 | # this resource just passes control to the "catch all" resource along |
paulb@426 | 134 | # with all the path information intact. |
paulb@312 | 135 | |
paulb@426 | 136 | if not (catch_all_resource and self.pass_through): |
paulb@426 | 137 | new_path = parts[0:1] + parts[2:] |
paulb@426 | 138 | new_path_info = "/".join(new_path) |
paulb@426 | 139 | trans.set_virtual_path_info(new_path_info) |
paulb@312 | 140 | |
paulb@386 | 141 | # Invoke the transaction, transferring control completely. |
paulb@386 | 142 | |
paulb@312 | 143 | if resource is not None: |
paulb@312 | 144 | resource.respond(trans) |
paulb@312 | 145 | return |
paulb@312 | 146 | |
paulb@312 | 147 | # Otherwise, signal an error. |
paulb@312 | 148 | |
paulb@312 | 149 | self.send_error(trans) |
paulb@312 | 150 | |
paulb@312 | 151 | def send_error(self, trans): |
paulb@312 | 152 | |
paulb@312 | 153 | "Send the error using the given 'trans'." |
paulb@312 | 154 | |
paulb@312 | 155 | trans.set_response_code(404) |
paulb@312 | 156 | trans.set_content_type(WebStack.Generic.ContentType("text/plain")) |
paulb@312 | 157 | out = trans.get_response_stream() |
paulb@599 | 158 | out.write("Resource '%s' not found." % trans.get_path_info(self.path_encoding)) |
paulb@312 | 159 | |
paulb@396 | 160 | def send_redirect(self, trans): |
paulb@396 | 161 | |
paulb@396 | 162 | """ |
paulb@396 | 163 | Send a redirect using the given 'trans', adding a "/" character to the |
paulb@396 | 164 | end of the request path. |
paulb@396 | 165 | """ |
paulb@396 | 166 | |
paulb@599 | 167 | path_without_query = trans.get_path_without_query(self.path_encoding) |
paulb@396 | 168 | query_string = trans.get_query_string() |
paulb@396 | 169 | if query_string: |
paulb@396 | 170 | query_string = "?" + query_string |
paulb@599 | 171 | trans.redirect(trans.encode_path(path_without_query, self.path_encoding) + "/" + query_string) |
paulb@396 | 172 | |
paulb@312 | 173 | # vim: tabstop=4 expandtab shiftwidth=4 |