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