1 #!/usr/bin/env python 2 3 """ 4 Java Servlet classes. 5 """ 6 7 import Generic 8 from StringIO import StringIO 9 from Helpers.Request import Cookie, get_body_fields, get_storage_items, get_fields_from_query_string, filter_fields 10 import javax.servlet.http 11 12 # Form data decoding. 13 14 import javax.mail.internet 15 import javax.mail 16 import java.util 17 import java.net 18 from WebStack.Generic import HeaderValue 19 20 class Stream: 21 22 """ 23 Wrapper around java.io.BufferedReader. 24 """ 25 26 def __init__(self, stream): 27 28 "Initialise the stream with the given underlying 'stream'." 29 30 self.stream = stream 31 32 def read(self): 33 34 "Read the entire message, returning it as a string." 35 36 characters = StringIO() 37 while 1: 38 c = self.stream.read() 39 if c == -1: 40 return characters.getvalue() 41 else: 42 characters.write(chr(c)) 43 44 def readline(self): 45 46 "Read a line from the stream, returning it as a string." 47 48 line = self.stream.readLine() 49 if line is not None: 50 return line + "\n" 51 else: 52 return "" 53 54 class Transaction(Generic.Transaction): 55 56 """ 57 Java Servlet transaction interface. 58 """ 59 60 def __init__(self, request, response): 61 62 """ 63 Initialise the transaction using the Java Servlet HTTP 'request' and 64 'response'. 65 """ 66 67 self.request = request 68 self.response = response 69 self.status = None 70 self.user = None 71 72 # Remember the cookies received in the request. 73 # NOTE: Discarding much of the information received. 74 75 self.cookies_in = {} 76 for cookie in self.request.getCookies() or []: 77 cookie_name = cookie.getName() 78 self.cookies_in[cookie_name] = Cookie(cookie_name, cookie.getValue()) 79 80 # Cached information. 81 82 self.message_fields = None 83 84 def commit(self): 85 86 """ 87 A special method, synchronising the transaction with framework-specific 88 objects. 89 """ 90 91 self.get_response_stream().close() 92 93 # Request-related methods. 94 95 def get_request_stream(self): 96 97 """ 98 Returns the request stream for the transaction. 99 """ 100 101 return Stream(self.request.getReader()) 102 103 def get_request_method(self): 104 105 """ 106 Returns the request method. 107 """ 108 109 return self.request.getMethod() 110 111 def get_headers(self): 112 113 """ 114 Returns all request headers as a dictionary-like object mapping header 115 names to values. 116 117 NOTE: If duplicate header names are permitted, then this interface will 118 NOTE: need to change. 119 """ 120 121 headers = {} 122 header_names_enum = self.request.getHeaderNames() 123 while header_names_enum.hasMoreElements(): 124 125 # NOTE: Retrieve only a single value (not using getHeaders). 126 127 header_name = header_names_enum.nextElement() 128 headers[header_name] = self.request.getHeader(header_name) 129 130 return headers 131 132 def get_header_values(self, key): 133 134 """ 135 Returns a list of all request header values associated with the given 136 'key'. Note that according to RFC 2616, 'key' is treated as a 137 case-insensitive string. 138 """ 139 140 values = [] 141 headers_enum = self.request.getHeaders(key) 142 while headers_enum.hasMoreElements(): 143 values.append(headers_enum.nextElement()) 144 return values 145 146 def get_content_type(self): 147 148 """ 149 Returns the content type specified on the request, along with the 150 charset employed. 151 """ 152 153 content_types = self.get_header_values("Content-Type") or [] 154 if len(content_types) >= 1: 155 return self.parse_content_type(content_types[0]) 156 else: 157 return None 158 159 def get_content_charsets(self): 160 161 """ 162 Returns the character set preferences. 163 """ 164 165 accept_charsets = self.get_header_values("Accept-Charset") or [] 166 if len(accept_charsets) >= 1: 167 return self.parse_content_preferences(accept_charsets[0]) 168 else: 169 return None 170 171 def get_content_languages(self): 172 173 """ 174 Returns extracted language information from the transaction. 175 """ 176 177 accept_languages = self.get_header_values("Accept-Language") or [] 178 if len(accept_languages) >= 1: 179 return self.parse_content_preferences(accept_languages[0]) 180 else: 181 return None 182 183 def get_path(self): 184 185 """ 186 Returns the entire path from the request. 187 """ 188 189 # NOTE: To be verified. 190 191 path = self.get_path_without_query() 192 qs = self.get_query_string() 193 if qs: 194 path += "?" 195 path += qs 196 return path 197 198 def get_path_without_query(self): 199 200 """ 201 Returns the entire path from the request minus the query string. 202 """ 203 204 return self.request.getContextPath() + self.request.getServletPath() + self.get_path_info() 205 206 def get_path_info(self): 207 208 """ 209 Returns the "path info" (the part of the URL after the resource name 210 handling the current request) from the request. 211 """ 212 213 return self.request.getPathInfo() or "" 214 215 def get_query_string(self): 216 217 """ 218 Returns the query string from the path in the request. 219 """ 220 221 return self.request.getQueryString() or "" 222 223 # Higher level request-related methods. 224 225 def get_fields_from_path(self): 226 227 """ 228 Extracts fields (or request parameters) from the path specified in the 229 transaction. The underlying framework may refuse to supply fields from 230 the path if handling a POST transaction. 231 232 Returns a dictionary mapping field names to lists of values (even if a 233 single value is associated with any given field name). 234 """ 235 236 # There may not be a reliable means of extracting only the fields from 237 # the path using the API. Moreover, any access to the request parameters 238 # disrupts the proper extraction and decoding of the request parameters 239 # which originated in the request body. 240 241 return get_fields_from_query_string(self.get_query_string(), java.net.URLDecoder().decode) 242 243 def get_fields_from_body(self, encoding=None): 244 245 """ 246 Extracts fields (or request parameters) from the message body in the 247 transaction. The optional 'encoding' parameter specifies the character 248 encoding of the message body for cases where no such information is 249 available, but where the default encoding is to be overridden. 250 251 Returns a dictionary mapping field names to lists of values (even if a 252 single value is associated with any given field name). Each value is 253 either a Unicode object (representing a simple form field, for example) 254 or a plain string (representing a file upload form field, for example). 255 """ 256 257 # There may not be a reliable means of extracting only the fields 258 # the message body using the API. Remove fields originating from the 259 # path in the mixture provided by the API. 260 261 all_fields = self._get_fields(encoding) 262 fields_from_path = self.get_fields_from_path() 263 return filter_fields(all_fields, fields_from_path) 264 265 def _get_fields(self, encoding=None): 266 267 # Override the default encoding if requested. 268 269 if encoding is not None: 270 self.request.setCharacterEncoding(encoding) 271 272 # Where the content type is "multipart/form-data", we use javax.mail 273 # functionality. Otherwise, we use the Servlet API's parameter access 274 # methods. 275 276 if self.get_content_type() and self.get_content_type().media_type == "multipart/form-data": 277 if self.message_fields is not None: 278 return self.message_fields 279 else: 280 fields = self.message_fields = self._get_fields_from_message() 281 else: 282 fields = {} 283 parameter_map = self.request.getParameterMap() 284 if parameter_map: 285 for field_name in parameter_map.keySet(): 286 fields[field_name] = parameter_map[field_name] 287 288 return fields 289 290 def get_fields(self, encoding=None): 291 292 """ 293 Extracts fields (or request parameters) from both the path specified in 294 the transaction as well as the message body. The optional 'encoding' 295 parameter specifies the character encoding of the message body for cases 296 where no such information is available, but where the default encoding 297 is to be overridden. 298 299 Returns a dictionary mapping field names to lists of values (even if a 300 single value is associated with any given field name). Each value is 301 either a Unicode object (representing a simple form field, for example) 302 or a plain string (representing a file upload form field, for example). 303 304 Where a given field name is used in both the path and message body to 305 specify values, the values from both sources will be combined into a 306 single list associated with that field name. 307 """ 308 309 # NOTE: The Java Servlet API (like Zope) seems to provide only body 310 # NOTE: fields upon POST requests. 311 312 if self.get_request_method() == "GET": 313 return self._get_fields(encoding) 314 else: 315 fields = {} 316 fields.update(self.get_fields_from_path()) 317 for name, values in self._get_fields(encoding).items(): 318 if not fields.has_key(name): 319 fields[name] = values 320 else: 321 fields[name] += values 322 return fields 323 324 def get_user(self): 325 326 """ 327 Extracts user information from the transaction. 328 329 Returns a username as a string or None if no user is defined. 330 """ 331 332 if self.user is not None: 333 return self.user 334 else: 335 return self.request.getRemoteUser() 336 337 def get_cookies(self): 338 339 """ 340 Obtains cookie information from the request. 341 342 Returns a dictionary mapping cookie names to cookie objects. 343 """ 344 345 return self.cookies_in 346 347 def get_cookie(self, cookie_name): 348 349 """ 350 Obtains cookie information from the request. 351 352 Returns a cookie object for the given 'cookie_name' or None if no such 353 cookie exists. 354 """ 355 356 return self.cookies_in.get(cookie_name) 357 358 # Response-related methods. 359 360 def get_response_stream(self): 361 362 """ 363 Returns the response stream for the transaction. 364 """ 365 366 return self.response.getWriter() 367 368 def get_response_code(self): 369 370 """ 371 Get the response code associated with the transaction. If no response 372 code is defined, None is returned. 373 """ 374 375 return self.status 376 377 def set_response_code(self, response_code): 378 379 """ 380 Set the 'response_code' using a numeric constant defined in the HTTP 381 specification. 382 """ 383 384 self.status = response_code 385 self.response.setStatus(self.status) 386 387 def set_header_value(self, header, value): 388 389 """ 390 Set the HTTP 'header' with the given 'value'. 391 """ 392 393 self.response.setHeader(self.format_header_value(header), self.format_header_value(value)) 394 395 def set_content_type(self, content_type): 396 397 """ 398 Sets the 'content_type' for the response. 399 """ 400 401 self.response.setContentType(str(content_type)) 402 403 # Higher level response-related methods. 404 405 def set_cookie(self, cookie): 406 407 """ 408 Stores the given 'cookie' object in the response. 409 """ 410 411 new_cookie = javax.servlet.http.Cookie(cookie.name, cookie.value) 412 self.response.addCookie(new_cookie) 413 414 def set_cookie_value(self, name, value, path=None, expires=None): 415 416 """ 417 Stores a cookie with the given 'name' and 'value' in the response. 418 419 The optional 'path' is a string which specifies the scope of the cookie, 420 and the optional 'expires' parameter is a value compatible with the 421 time.time function, and indicates the expiry date/time of the cookie. 422 """ 423 424 cookie = javax.servlet.http.Cookie(name, value) 425 if path is not None: 426 cookie.setPath(path) 427 428 # NOTE: The expires parameter seems not to be supported. 429 430 self.response.addCookie(cookie) 431 432 def delete_cookie(self, cookie_name): 433 434 """ 435 Adds to the response a request that the cookie with the given 436 'cookie_name' be deleted/discarded by the client. 437 """ 438 439 # Create a special cookie, given that we do not know whether the browser 440 # has been sent the cookie or not. 441 # NOTE: Magic discovered in Webware. 442 443 cookie = javax.servlet.http.Cookie(cookie_name, "") 444 cookie.setPath("/") 445 cookie.setMaxAge(0) 446 self.response.addCookie(cookie) 447 448 # Session-related methods. 449 450 def get_session(self, create=1): 451 452 """ 453 Gets a session corresponding to an identifier supplied in the 454 transaction. 455 456 If no session has yet been established according to information 457 provided in the transaction then the optional 'create' parameter 458 determines whether a new session will be established. 459 460 Where no session has been established and where 'create' is set to 0 461 then None is returned. In all other cases, a session object is created 462 (where appropriate) and returned. 463 """ 464 465 session = self.request.getSession(create) 466 if session: 467 return Session(session) 468 else: 469 return None 470 471 def expire_session(self): 472 473 """ 474 Expires any session established according to information provided in the 475 transaction. 476 """ 477 478 session = self.request.getSession(0) 479 if session: 480 session.invalidate() 481 482 # Application-specific methods. 483 484 def set_user(self, username): 485 486 """ 487 An application-specific method which sets the user information with 488 'username' in the transaction. This affects subsequent calls to 489 'get_user'. 490 """ 491 492 self.user = username 493 494 # Special Java-specific methods. 495 496 def _get_fields_from_message(self): 497 498 "Get fields from a multipart message." 499 500 session = javax.mail.Session.getDefaultInstance(java.util.Properties()) 501 502 # Fake a multipart message. 503 504 str_buffer = java.io.StringWriter() 505 fp = self.get_request_stream() 506 boundary = fp.readline() 507 str_buffer.write('Content-Type: multipart/mixed; boundary="%s"\n\n' % boundary[2:-1]) 508 str_buffer.write(boundary) 509 str_buffer.write(fp.read()) 510 str_buffer.close() 511 512 # Re-read that message. 513 514 input_stream = java.io.StringBufferInputStream(str_buffer.toString()) 515 message = javax.mail.internet.MimeMessage(session, input_stream) 516 content = message.getContent() 517 return self._get_fields_from_multipart(content) 518 519 def _get_fields_from_multipart(self, content): 520 521 "Get fields from multipart 'content'." 522 523 fields = {} 524 for i in range(0, content.getCount()): 525 part = content.getBodyPart(i) 526 subcontent = part.getContent() 527 528 # Convert input stream content. 529 530 if isinstance(subcontent, java.io.InputStream): 531 subcontent = Stream(subcontent).read() 532 533 # Record string content. 534 535 if type(subcontent) == type(""): 536 537 # Should get: form-data; name="x" 538 539 disposition = self.parse_header_value(HeaderValue, part.getHeader("Content-Disposition")[0]) 540 541 # Store and optionally convert the field. 542 543 if disposition.name is not None: 544 if not fields.has_key(disposition.name[1:-1]): 545 fields[disposition.name[1:-1]] = [] 546 fields[disposition.name[1:-1]].append(subcontent) 547 548 # Otherwise, descend deeper into the multipart hierarchy. 549 550 else: 551 fields.update(self._get_fields_from_multipart(subcontent)) 552 553 return fields 554 555 class Session: 556 557 """ 558 A simple session class with behaviour more similar to the Python framework 559 session classes. 560 """ 561 562 def __init__(self, session): 563 564 "Initialise the session object with the framework 'session' object." 565 566 self.session = session 567 568 def keys(self): 569 keys = [] 570 keys_enum = self.session.getAttributeNames() 571 while keys_enum.hasMoreElements(): 572 keys.append(keys_enum.nextElement()) 573 return keys 574 575 def values(self): 576 values = [] 577 for key in self.keys(): 578 values.append(self[key]) 579 return values 580 581 def items(self): 582 items = [] 583 for key in self.keys(): 584 items.append((key, self[key])) 585 return items 586 587 def __getitem__(self, key): 588 return self.session.getAttribute(key) 589 590 def __setitem__(self, key, value): 591 self.session.setAttribute(key, value) 592 593 def __delitem__(self, key): 594 self.session.removeAttribute(key) 595 596 # vim: tabstop=4 expandtab shiftwidth=4