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 name = "" 106 107 # Get the mapped resource. 108 109 resource = self.mapping.get(name) 110 if resource is None: 111 resource = self.mapping.get(None) 112 catch_all_resource = 1 113 else: 114 catch_all_resource = 0 115 116 # If a resource was found, change the transaction's path info. 117 # eg. "/this/next" -> "/next" 118 # eg. "/this/" -> "/" 119 # eg. "/this" -> "" 120 # Such changes are not made if the resource is in "pass through" mode 121 # and where the "catch all" resource is being used. In such situations 122 # this resource just passes control to the "catch all" resource along 123 # with all the path information intact. 124 125 if not (catch_all_resource and self.pass_through): 126 new_path = parts[0:1] + parts[2:] 127 new_path_info = "/".join(new_path) 128 trans.set_virtual_path_info(new_path_info) 129 130 # Invoke the transaction, transferring control completely. 131 132 if resource is not None: 133 resource.respond(trans) 134 return 135 136 # Otherwise, signal an error. 137 138 self.send_error(trans) 139 140 def send_error(self, trans): 141 142 "Send the error using the given 'trans'." 143 144 trans.set_response_code(404) 145 trans.set_content_type(WebStack.Generic.ContentType("text/plain")) 146 out = trans.get_response_stream() 147 out.write("Resource '%s' not found." % trans.get_path_info()) 148 149 def send_redirect(self, trans): 150 151 """ 152 Send a redirect using the given 'trans', adding a "/" character to the 153 end of the request path. 154 """ 155 156 query_string = trans.get_query_string() 157 if query_string: 158 query_string = "?" + query_string 159 160 trans.set_response_code(302) 161 trans.set_header_value("Location", trans.get_path_without_query() + "/" + query_string) 162 raise WebStack.Generic.EndOfResponse 163 164 # vim: tabstop=4 expandtab shiftwidth=4