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