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