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