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