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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 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): 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 77 self.mapping = mapping 78 self.pass_through = pass_through 79 self.directory_redirects = directory_redirects 80 81 def respond(self, trans): 82 83 """ 84 Using the path information from the given transaction 'trans', invoke 85 mapped resources. Otherwise return an error condition. 86 """ 87 88 # Get the path info. 89 90 parts = trans.get_virtual_path_info().split("/") 91 92 # Where the published resource has a path info value defined (ie. its 93 # path info consists of a "/" character plus some other text), the first 94 # part should always be empty and there should always be a second part. 95 # Where the published resource has no path info defined, there will only 96 # be one part. In the latter case, we define the name to be the empty 97 # string, although the name will not be relevant if directory_redirects 98 # is set. 99 100 if len(parts) > 1: 101 name = parts[1] 102 elif self.directory_redirects: 103 self.send_redirect(trans) 104 else: 105 self.send_error(trans) 106 return 107 108 # Get the mapped resource. 109 110 resource = self.mapping.get(name) 111 if resource is None: 112 resource = self.mapping.get(None) 113 catch_all_resource = 1 114 else: 115 catch_all_resource = 0 116 117 # If a resource was found, change the transaction's path info. 118 # eg. "/this/next" -> "/next" 119 # eg. "/this/" -> "/" 120 # eg. "/this" -> "" 121 # Such changes are not made if the resource is in "pass through" mode 122 # and where the "catch all" resource is being used. In such situations 123 # this resource just passes control to the "catch all" resource along 124 # with all the path information intact. 125 126 if not (catch_all_resource and self.pass_through): 127 new_path = parts[0:1] + parts[2:] 128 new_path_info = "/".join(new_path) 129 trans.set_virtual_path_info(new_path_info) 130 131 # Invoke the transaction, transferring control completely. 132 133 if resource is not None: 134 resource.respond(trans) 135 return 136 137 # Otherwise, signal an error. 138 139 self.send_error(trans) 140 141 def send_error(self, trans): 142 143 "Send the error using the given 'trans'." 144 145 trans.set_response_code(404) 146 trans.set_content_type(WebStack.Generic.ContentType("text/plain")) 147 out = trans.get_response_stream() 148 out.write("Resource '%s' not found." % trans.get_path_info()) 149 150 def send_redirect(self, trans): 151 152 """ 153 Send a redirect using the given 'trans', adding a "/" character to the 154 end of the request path. 155 """ 156 157 query_string = trans.get_query_string() 158 if query_string: 159 query_string = "?" + query_string 160 161 trans.set_response_code(302) 162 trans.set_header_value("Location", trans.get_path_without_query() + "/" + query_string) 163 raise WebStack.Generic.EndOfResponse 164 165 # vim: tabstop=4 expandtab shiftwidth=4