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