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