1 #!/usr/bin/env python 2 3 """ 4 Java Servlet classes. 5 6 Copyright (C) 2004, 2005, 2006, 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 from StringIO import StringIO 25 from WebStack.Helpers.Request import Cookie, FileContent, get_body_fields, \ 26 get_storage_items, get_fields_from_query_string, filter_fields, \ 27 HeaderDict, HeaderValue 28 import javax.servlet.http 29 import jarray 30 31 # Java API form data decoding. 32 33 import javax.mail.internet 34 import javax.mail 35 import java.util 36 import java.net 37 import java.io 38 39 class Stream: 40 41 """ 42 Wrapper around java.io.InputStream. 43 """ 44 45 bufsize = 1024 46 47 def __init__(self, stream): 48 49 "Initialise the stream with the given underlying 'stream'." 50 51 self.stream = stream 52 53 def read(self, limit=None): 54 55 "Read the entire message, returning it as a string." 56 57 bufsize = limit or self.bufsize 58 characters = StringIO() 59 a = jarray.zeros(bufsize, 'b') 60 while 1: 61 nread = self.stream.read(a, 0, bufsize) 62 if nread != -1: 63 self._copy(a, characters, nread) 64 if nread != bufsize or nread == limit: 65 return characters.getvalue() 66 67 def readline(self, bufsize=None): 68 69 "Read a line from the stream, returning it as a string." 70 71 bufsize = bufsize or self.bufsize 72 characters = StringIO() 73 a = jarray.zeros(bufsize, 'b') 74 while 1: 75 nread = self.stream.readLine(a, 0, bufsize) 76 if nread != -1: 77 self._copy(a, characters, nread) 78 if nread != bufsize: 79 return characters.getvalue() 80 81 def reset(self): 82 83 "Attempt to reset the stream." 84 85 self.stream.reset() 86 87 def _unsigned(self, i): 88 if i < 0: 89 return chr(256 + i) 90 else: 91 return chr(i) 92 93 def _copy(self, source, target, length): 94 i = 0 95 while i < length: 96 target.write(self._unsigned(source[i])) 97 i += 1 98 99 class Transaction(WebStack.Generic.Transaction): 100 101 """ 102 Java Servlet transaction interface. 103 """ 104 105 def __init__(self, request, response, servlet): 106 107 """ 108 Initialise the transaction using the Java Servlet HTTP 'request' and 109 'response', along with the deployment 'servlet'. 110 """ 111 112 self.request = request 113 self.response = response 114 self.servlet = servlet 115 self.status = None 116 117 # Remember the cookies received in the request. 118 # NOTE: Discarding much of the information received. 119 120 self.cookies_in = {} 121 for cookie in self.request.getCookies() or []: 122 cookie_name = self.decode_cookie_value(cookie.getName()) 123 self.cookies_in[cookie_name] = Cookie(cookie_name, self.decode_cookie_value(cookie.getValue())) 124 125 # Cached information. 126 127 self.message_fields = None 128 129 # Resource management. 130 131 self.tempfiles = [] 132 133 def commit(self): 134 135 """ 136 A special method, synchronising the transaction with framework-specific 137 objects. 138 """ 139 140 self.get_response_stream().close() 141 for tempfile in self.tempfiles: 142 tempfile.delete() 143 144 # Server-related methods. 145 146 def get_server_name(self): 147 148 "Returns the server name." 149 150 return self.request.getServerName() 151 152 def get_server_port(self): 153 154 "Returns the server port as a string." 155 156 return str(self.request.getServerPort()) 157 158 # Request-related methods. 159 160 def get_request_stream(self): 161 162 """ 163 Returns the request stream for the transaction. 164 """ 165 166 return Stream(self.request.getInputStream()) 167 168 def get_request_method(self): 169 170 """ 171 Returns the request method. 172 """ 173 174 return self.request.getMethod() 175 176 def get_headers(self): 177 178 """ 179 Returns all request headers as a dictionary-like object mapping header 180 names to values. 181 182 NOTE: If duplicate header names are permitted, then this interface will 183 NOTE: need to change. 184 """ 185 186 headers = HeaderDict() 187 header_names_enum = self.request.getHeaderNames() 188 while header_names_enum.hasMoreElements(): 189 190 # NOTE: Retrieve only a single value (not using getHeaders). 191 192 header_name = header_names_enum.nextElement() 193 headers[header_name] = self.request.getHeader(header_name) 194 195 return headers 196 197 def get_header_values(self, key): 198 199 """ 200 Returns a list of all request header values associated with the given 201 'key'. Note that according to RFC 2616, 'key' is treated as a 202 case-insensitive string. 203 """ 204 205 values = [] 206 headers_enum = self.request.getHeaders(key) 207 while headers_enum.hasMoreElements(): 208 values.append(headers_enum.nextElement()) 209 return values 210 211 def get_content_type(self): 212 213 """ 214 Returns the content type specified on the request, along with the 215 charset employed. 216 """ 217 218 content_types = self.get_header_values("Content-Type") or [] 219 if len(content_types) >= 1: 220 return self.parse_content_type(content_types[0]) 221 else: 222 return None 223 224 def get_content_charsets(self): 225 226 """ 227 Returns the character set preferences. 228 """ 229 230 accept_charsets = self.get_header_values("Accept-Charset") or [] 231 if len(accept_charsets) >= 1: 232 return self.parse_content_preferences(accept_charsets[0]) 233 else: 234 return None 235 236 def get_content_languages(self): 237 238 """ 239 Returns extracted language information from the transaction. 240 """ 241 242 accept_languages = self.get_header_values("Accept-Language") or [] 243 if len(accept_languages) >= 1: 244 return self.parse_content_preferences(accept_languages[0]) 245 else: 246 return None 247 248 def get_path(self, encoding=None): 249 250 """ 251 Returns the entire path from the request as a Unicode object. Any "URL 252 encoded" character values in the part of the path before the query 253 string will be decoded and presented as genuine characters; the query 254 string will remain "URL encoded", however. 255 256 If the optional 'encoding' is set, use that in preference to the default 257 encoding to convert the path into a form not containing "URL encoded" 258 character values. 259 """ 260 261 path = self.get_path_without_query(encoding) 262 qs = self.get_query_string() 263 if qs: 264 return path + "?" + qs 265 else: 266 return path 267 268 def get_path_without_query(self, encoding=None): 269 270 """ 271 Returns the entire path from the request minus the query string as a 272 Unicode object containing genuine characters (as opposed to "URL 273 encoded" character values). 274 275 If the optional 'encoding' is set, use that in preference to the default 276 encoding to convert the path into a form not containing "URL encoded" 277 character values. 278 """ 279 280 # NOTE: We do not actually use the encoding - this may be a servlet 281 # NOTE: container option. 282 283 return self.request.getContextPath() + self.request.getServletPath() + self.get_path_info(encoding) 284 285 def get_path_info(self, encoding=None): 286 287 """ 288 Returns the "path info" (the part of the URL after the resource name 289 handling the current request) from the request as a Unicode object 290 containing genuine characters (as opposed to "URL encoded" character 291 values). 292 293 If the optional 'encoding' is set, use that in preference to the default 294 encoding to convert the path into a form not containing "URL encoded" 295 character values. 296 """ 297 298 # NOTE: We do not actually use the encoding - this may be a servlet 299 # NOTE: container option. 300 301 return self.request.getPathInfo() or "" 302 303 def get_query_string(self): 304 305 """ 306 Returns the query string from the path in the request. 307 """ 308 309 return self.request.getQueryString() or "" 310 311 # Higher level request-related methods. 312 313 def get_fields_from_path(self, encoding=None): 314 315 """ 316 Extracts fields (or request parameters) from the path specified in the 317 transaction. The underlying framework may refuse to supply fields from 318 the path if handling a POST transaction. The optional 'encoding' 319 parameter specifies the character encoding of the query string for cases 320 where the default encoding is to be overridden. 321 322 Returns a dictionary mapping field names to lists of values (even if a 323 single value is associated with any given field name). 324 """ 325 326 # There may not be a reliable means of extracting only the fields from 327 # the path using the API. Moreover, any access to the request parameters 328 # disrupts the proper extraction and decoding of the request parameters 329 # which originated in the request body. 330 331 return get_fields_from_query_string(self.get_query_string(), java.net.URLDecoder().decode) 332 333 def get_fields_from_body(self, encoding=None): 334 335 """ 336 Extracts fields (or request parameters) from the message body in the 337 transaction. The optional 'encoding' parameter specifies the character 338 encoding of the message body for cases where no such information is 339 available, but where the default encoding is to be overridden. 340 341 Returns a dictionary mapping field names to lists of values (even if a 342 single value is associated with any given field name). Each value is 343 either a Unicode object (representing a simple form field, for example) 344 or a WebStack.Helpers.Request.FileContent object (representing a file 345 upload form field). 346 """ 347 348 # There may not be a reliable means of extracting only the fields 349 # the message body using the API. Remove fields originating from the 350 # path in the mixture provided by the API. 351 352 all_fields = self._get_fields(encoding) 353 fields_from_path = self.get_fields_from_path() 354 return filter_fields(all_fields, fields_from_path) 355 356 def _get_fields(self, encoding=None): 357 358 # Override the default encoding if requested. 359 360 if encoding is not None: 361 self.request.setCharacterEncoding(encoding) 362 363 # Where the content type is "multipart/form-data", we use javax.mail 364 # functionality. Otherwise, we use the Servlet API's parameter access 365 # methods. 366 367 if self.get_content_type() and self.get_content_type().media_type == "multipart/form-data": 368 if self.message_fields is not None: 369 return self.message_fields 370 else: 371 fields = self.message_fields = self._get_fields_from_message(encoding) 372 else: 373 fields = {} 374 parameter_map = self.request.getParameterMap() 375 if parameter_map: 376 for field_name in parameter_map.keySet(): 377 fields[field_name] = parameter_map[field_name] 378 379 return fields 380 381 def get_fields(self, encoding=None): 382 383 """ 384 Extracts fields (or request parameters) from both the path specified in 385 the transaction as well as the message body. The optional 'encoding' 386 parameter specifies the character encoding of the message body for cases 387 where no such information is available, but where the default encoding 388 is to be overridden. 389 390 Returns a dictionary mapping field names to lists of values (even if a 391 single value is associated with any given field name). Each value is 392 either a Unicode object (representing a simple form field, for example) 393 or a WebStack.Helpers.Request.FileContent object (representing a file 394 upload form field). 395 396 Where a given field name is used in both the path and message body to 397 specify values, the values from both sources will be combined into a 398 single list associated with that field name. 399 """ 400 401 # NOTE: The Java Servlet API (like Zope) seems to provide only body 402 # NOTE: fields upon POST requests. 403 404 if self.get_request_method() == "GET": 405 return self._get_fields(encoding) 406 else: 407 fields = {} 408 fields.update(self.get_fields_from_path()) 409 for name, values in self._get_fields(encoding).items(): 410 if not fields.has_key(name): 411 fields[name] = values 412 else: 413 fields[name] += values 414 return fields 415 416 def get_user(self): 417 418 """ 419 Extracts user information from the transaction. 420 421 Returns a username as a string or None if no user is defined. 422 """ 423 424 if self.user is not None: 425 return self.user 426 else: 427 return self.request.getRemoteUser() 428 429 def get_cookies(self): 430 431 """ 432 Obtains cookie information from the request. 433 434 Returns a dictionary mapping cookie names to cookie objects. 435 """ 436 437 return self.cookies_in 438 439 def get_cookie(self, cookie_name): 440 441 """ 442 Obtains cookie information from the request. 443 444 Returns a cookie object for the given 'cookie_name' or None if no such 445 cookie exists. 446 """ 447 448 return self.cookies_in.get(cookie_name) 449 450 # Response-related methods. 451 452 def get_response_stream(self): 453 454 """ 455 Returns the response stream for the transaction. 456 """ 457 458 return self.response.getWriter() 459 460 def get_response_stream_encoding(self): 461 462 """ 463 Returns the response stream encoding. 464 """ 465 466 return self.response.getCharacterEncoding() 467 468 def get_response_code(self): 469 470 """ 471 Get the response code associated with the transaction. If no response 472 code is defined, None is returned. 473 """ 474 475 return self.status 476 477 def set_response_code(self, response_code): 478 479 """ 480 Set the 'response_code' using a numeric constant defined in the HTTP 481 specification. 482 """ 483 484 self.status = response_code 485 self.response.setStatus(self.status) 486 487 def set_header_value(self, header, value): 488 489 """ 490 Set the HTTP 'header' with the given 'value'. 491 """ 492 493 self.response.setHeader(self.format_header_value(header), self.format_header_value(value)) 494 495 def set_content_type(self, content_type): 496 497 """ 498 Sets the 'content_type' for the response. 499 """ 500 501 self.response.setContentType(str(content_type)) 502 503 # Higher level response-related methods. 504 505 def set_cookie(self, cookie): 506 507 """ 508 Stores the given 'cookie' object in the response. 509 """ 510 511 self.set_cookie_value(cookie.name, cookie.value) 512 513 def set_cookie_value(self, name, value, path=None, expires=None): 514 515 """ 516 Stores a cookie with the given 'name' and 'value' in the response. 517 518 The optional 'path' is a string which specifies the scope of the cookie, 519 and the optional 'expires' parameter is a value compatible with the 520 time.time function, and indicates the expiry date/time of the cookie. 521 """ 522 523 cookie = javax.servlet.http.Cookie( 524 self.encode_cookie_value(name), 525 self.encode_cookie_value(value) 526 ) 527 if path is not None: 528 cookie.setPath(path) 529 530 # NOTE: The expires parameter seems not to be supported. 531 532 self.response.addCookie(cookie) 533 534 def delete_cookie(self, cookie_name): 535 536 """ 537 Adds to the response a request that the cookie with the given 538 'cookie_name' be deleted/discarded by the client. 539 """ 540 541 # Create a special cookie, given that we do not know whether the browser 542 # has been sent the cookie or not. 543 # NOTE: Magic discovered in Webware. 544 545 cookie = javax.servlet.http.Cookie(self.encode_cookie_value(cookie_name), "") 546 cookie.setPath("/") 547 cookie.setMaxAge(0) 548 self.response.addCookie(cookie) 549 550 # Session-related methods. 551 552 def get_session(self, create=1): 553 554 """ 555 Gets a session corresponding to an identifier supplied in the 556 transaction. 557 558 If no session has yet been established according to information 559 provided in the transaction then the optional 'create' parameter 560 determines whether a new session will be established. 561 562 Where no session has been established and where 'create' is set to 0 563 then None is returned. In all other cases, a session object is created 564 (where appropriate) and returned. 565 """ 566 567 session = self.request.getSession(create) 568 if session: 569 return Session(session) 570 else: 571 return None 572 573 def expire_session(self): 574 575 """ 576 Expires any session established according to information provided in the 577 transaction. 578 """ 579 580 session = self.request.getSession(0) 581 if session: 582 session.invalidate() 583 584 # Java-specific variants of the generic methods. 585 586 def get_attributes(self): 587 588 """ 589 An application-specific method which obtains a dictionary mapping names 590 to attribute values that can be used to store arbitrary information. 591 592 Since the dictionary of attributes is retained by the transaction during 593 its lifetime, such a dictionary can be used to store information that an 594 application wishes to communicate amongst its components and resources 595 without having to pass objects other than the transaction between them. 596 597 The returned dictionary can be modified using normal dictionary-like 598 methods. If no attributes existed previously, a new dictionary is 599 created and associated with the transaction. 600 """ 601 602 return Session(self.request) 603 604 # Special Java-specific methods. 605 606 def get_servlet(self): 607 608 "Return the deployment servlet." 609 610 return self.servlet 611 612 def _get_fields_from_message(self, encoding): 613 614 "Get fields from a multipart message." 615 616 session = javax.mail.Session.getDefaultInstance(java.util.Properties()) 617 618 # Fake the headers. 619 620 str_buffer = java.io.StringWriter() 621 fp = self.get_request_stream() 622 boundary = fp.readline() 623 str_buffer.write('Content-Type: multipart/mixed; boundary="%s"\n\n' % boundary[2:-2]) 624 str_buffer.write(boundary) 625 str_buffer.close() 626 627 # Concatenate the headers with the rest of the stream. 628 629 header_stream = java.io.StringBufferInputStream(str_buffer.toString()) 630 input_stream = self.request.getInputStream() 631 message = javax.mail.internet.MimeMessage(session, java.io.SequenceInputStream(header_stream, input_stream)) 632 633 # Collect the fields by traversing the message. 634 635 fields = {} 636 self._get_fields_from_multipart(fields, message.getContent(), encoding) 637 return fields 638 639 def _get_fields_from_multipart(self, fields, content, encoding): 640 641 "Get fields from multipart 'content'." 642 643 for i in range(0, content.getCount()): 644 part = content.getBodyPart(i) 645 self._get_field_from_multipart(fields, part, encoding) 646 647 def _get_field_from_multipart(self, fields, part, encoding): 648 649 "Get a field from the given 'part'." 650 651 if not part.getContentType().startswith("multipart"): 652 653 # Should get: form-data; name="x" 654 655 disposition = self.parse_header_value(HeaderValue, part.getHeader("Content-Disposition")[0]) 656 657 # Store and optionally convert the field. 658 659 if disposition.name is not None: 660 field_name = disposition.name[1:-1] 661 662 # Test whether the part should be written to a temporary file. 663 664 if part.getHeader("Content-Type") is not None: 665 666 # Using properly decoded header values. 667 668 headers = HeaderDict() 669 for header in part.getAllHeaders(): 670 headers[header.getName()] = self.parse_header_value(HeaderValue, header.getValue()) 671 672 # Write to a temporary file and then open that file. 673 674 tempfile = java.io.File.createTempFile(str(id(self)), field_name) 675 temp_stream = java.io.FileOutputStream(tempfile) 676 try: 677 part.writeTo(temp_stream) 678 finally: 679 self.tempfiles.append(tempfile) 680 681 # The file must be treated like a message. 682 683 temp_part = javax.mail.internet.MimeBodyPart(java.io.FileInputStream(tempfile)) 684 field_value = FileContent(Stream(temp_part.getRawInputStream()), headers) 685 686 else: 687 subcontent = part.getContent() 688 field_value = self.decode_path(subcontent, encoding) 689 690 # Store the entry in the fields dictionary. 691 692 if not fields.has_key(field_name): 693 fields[field_name] = [] 694 fields[field_name].append(field_value) 695 696 # Otherwise, descend deeper into the multipart hierarchy. 697 698 else: 699 subcontent = part.getContent() 700 fields.update(self._get_fields_from_multipart(subcontent, encoding)) 701 702 class Session: 703 704 """ 705 A simple session class with behaviour more similar to the Python framework 706 session classes. This class can also be instantiated with a request object 707 and used to access attributes on the request. 708 """ 709 710 def __init__(self, session): 711 712 """ 713 Initialise the session object with the framework 'session' object. If a 714 ServletRequest object is given, the attributes on that will be 715 accessible, as opposed to the attributes on an HttpSession object. 716 """ 717 718 self.session = session 719 720 def keys(self): 721 keys = [] 722 keys_enum = self.session.getAttributeNames() 723 while keys_enum.hasMoreElements(): 724 keys.append(keys_enum.nextElement()) 725 return keys 726 727 def values(self): 728 values = [] 729 for key in self.keys(): 730 values.append(self[key]) 731 return values 732 733 def items(self): 734 items = [] 735 for key in self.keys(): 736 items.append((key, self[key])) 737 return items 738 739 def __getitem__(self, key): 740 return self.session.getAttribute(key) 741 742 def __setitem__(self, key, value): 743 self.session.setAttribute(key, value) 744 745 def __delitem__(self, key): 746 self.session.removeAttribute(key) 747 748 # vim: tabstop=4 expandtab shiftwidth=4