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