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