1 #!/usr/bin/env python 2 3 """ 4 Java Servlet classes. 5 6 Copyright (C) 2004, 2005, 2006, 2007, 2009 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 WebStack.Generic.Transaction.set_content_type(self, content_type) 502 self.response.setContentType(str(content_type)) 503 504 # Higher level response-related methods. 505 506 def set_cookie(self, cookie): 507 508 """ 509 Stores the given 'cookie' object in the response. 510 """ 511 512 self.set_cookie_value(cookie.name, cookie.value) 513 514 def set_cookie_value(self, name, value, path=None, expires=None): 515 516 """ 517 Stores a cookie with the given 'name' and 'value' in the response. 518 519 The optional 'path' is a string which specifies the scope of the cookie, 520 and the optional 'expires' parameter is a value compatible with the 521 time.time function, and indicates the expiry date/time of the cookie. 522 """ 523 524 cookie = javax.servlet.http.Cookie( 525 self.encode_cookie_value(name), 526 self.encode_cookie_value(value) 527 ) 528 if path is not None: 529 cookie.setPath(path) 530 531 # NOTE: The expires parameter seems not to be supported. 532 533 self.response.addCookie(cookie) 534 535 def delete_cookie(self, cookie_name): 536 537 """ 538 Adds to the response a request that the cookie with the given 539 'cookie_name' be deleted/discarded by the client. 540 """ 541 542 # Create a special cookie, given that we do not know whether the browser 543 # has been sent the cookie or not. 544 # NOTE: Magic discovered in Webware. 545 546 cookie = javax.servlet.http.Cookie(self.encode_cookie_value(cookie_name), "") 547 cookie.setPath("/") 548 cookie.setMaxAge(0) 549 self.response.addCookie(cookie) 550 551 # Session-related methods. 552 553 def get_session(self, create=1): 554 555 """ 556 Gets a session corresponding to an identifier supplied in the 557 transaction. 558 559 If no session has yet been established according to information 560 provided in the transaction then the optional 'create' parameter 561 determines whether a new session will be established. 562 563 Where no session has been established and where 'create' is set to 0 564 then None is returned. In all other cases, a session object is created 565 (where appropriate) and returned. 566 """ 567 568 session = self.request.getSession(create) 569 if session: 570 return Session(session) 571 else: 572 return None 573 574 def expire_session(self): 575 576 """ 577 Expires any session established according to information provided in the 578 transaction. 579 """ 580 581 session = self.request.getSession(0) 582 if session: 583 session.invalidate() 584 585 # Java-specific variants of the generic methods. 586 587 def get_attributes(self): 588 589 """ 590 An application-specific method which obtains a dictionary mapping names 591 to attribute values that can be used to store arbitrary information. 592 593 Since the dictionary of attributes is retained by the transaction during 594 its lifetime, such a dictionary can be used to store information that an 595 application wishes to communicate amongst its components and resources 596 without having to pass objects other than the transaction between them. 597 598 The returned dictionary can be modified using normal dictionary-like 599 methods. If no attributes existed previously, a new dictionary is 600 created and associated with the transaction. 601 """ 602 603 return Session(self.request) 604 605 # Special Java-specific methods. 606 607 def get_servlet(self): 608 609 "Return the deployment servlet." 610 611 return self.servlet 612 613 def _get_fields_from_message(self, encoding): 614 615 "Get fields from a multipart message." 616 617 session = javax.mail.Session.getDefaultInstance(java.util.Properties()) 618 619 # Fake the headers. 620 621 str_buffer = java.io.StringWriter() 622 fp = self.get_request_stream() 623 boundary = fp.readline() 624 str_buffer.write('Content-Type: multipart/mixed; boundary="%s"\n\n' % boundary[2:-2]) 625 str_buffer.write(boundary) 626 str_buffer.close() 627 628 # Concatenate the headers with the rest of the stream. 629 630 header_stream = java.io.StringBufferInputStream(str_buffer.toString()) 631 input_stream = self.request.getInputStream() 632 message = javax.mail.internet.MimeMessage(session, java.io.SequenceInputStream(header_stream, input_stream)) 633 634 # Collect the fields by traversing the message. 635 636 fields = {} 637 self._get_fields_from_multipart(fields, message.getContent(), encoding) 638 return fields 639 640 def _get_fields_from_multipart(self, fields, content, encoding): 641 642 "Get fields from multipart 'content'." 643 644 for i in range(0, content.getCount()): 645 part = content.getBodyPart(i) 646 self._get_field_from_multipart(fields, part, encoding) 647 648 def _get_field_from_multipart(self, fields, part, encoding): 649 650 "Get a field from the given 'part'." 651 652 if not part.getContentType().startswith("multipart"): 653 654 # Should get: form-data; name="x" 655 656 disposition = self.parse_header_value(HeaderValue, part.getHeader("Content-Disposition")[0]) 657 658 # Store and optionally convert the field. 659 660 if disposition.name is not None: 661 field_name = disposition.name[1:-1] 662 663 # Test whether the part should be written to a temporary file. 664 665 if part.getHeader("Content-Type") is not None: 666 667 # Using properly decoded header values. 668 669 headers = HeaderDict() 670 for header in part.getAllHeaders(): 671 headers[header.getName()] = self.parse_header_value(HeaderValue, header.getValue()) 672 673 # Write to a temporary file and then open that file. 674 675 tempfile = java.io.File.createTempFile(str(id(self)), field_name) 676 temp_stream = java.io.FileOutputStream(tempfile) 677 try: 678 part.writeTo(temp_stream) 679 finally: 680 self.tempfiles.append(tempfile) 681 682 # The file must be treated like a message. 683 684 temp_part = javax.mail.internet.MimeBodyPart(java.io.FileInputStream(tempfile)) 685 field_value = FileContent(Stream(temp_part.getRawInputStream()), headers) 686 687 else: 688 subcontent = part.getContent() 689 field_value = self.decode_path(subcontent, encoding) 690 691 # Store the entry in the fields dictionary. 692 693 if not fields.has_key(field_name): 694 fields[field_name] = [] 695 fields[field_name].append(field_value) 696 697 # Otherwise, descend deeper into the multipart hierarchy. 698 699 else: 700 subcontent = part.getContent() 701 fields.update(self._get_fields_from_multipart(subcontent, encoding)) 702 703 class Session: 704 705 """ 706 A simple session class with behaviour more similar to the Python framework 707 session classes. This class can also be instantiated with a request object 708 and used to access attributes on the request. 709 """ 710 711 def __init__(self, session): 712 713 """ 714 Initialise the session object with the framework 'session' object. If a 715 ServletRequest object is given, the attributes on that will be 716 accessible, as opposed to the attributes on an HttpSession object. 717 """ 718 719 self.session = session 720 721 def keys(self): 722 keys = [] 723 keys_enum = self.session.getAttributeNames() 724 while keys_enum.hasMoreElements(): 725 keys.append(keys_enum.nextElement()) 726 return keys 727 728 def values(self): 729 values = [] 730 for key in self.keys(): 731 values.append(self[key]) 732 return values 733 734 def items(self): 735 items = [] 736 for key in self.keys(): 737 items.append((key, self[key])) 738 return items 739 740 def __getitem__(self, key): 741 return self.session.getAttribute(key) 742 743 def __setitem__(self, key, value): 744 self.session.setAttribute(key, value) 745 746 def __delitem__(self, key): 747 self.session.removeAttribute(key) 748 749 # vim: tabstop=4 expandtab shiftwidth=4