1 #!/usr/bin/env python 2 3 """ 4 Resources for use with WebStack. 5 6 Copyright (C) 2005, 2006, 2007, 2008, 2009 Paul Boddie <paul@boddie.org.uk> 7 8 This program is free software; you can redistribute it and/or modify it under 9 the terms of the GNU Lesser General Public License as published by the Free 10 Software Foundation; either version 3 of the License, or (at your option) any 11 later version. 12 13 This program is distributed in the hope that it will be useful, but WITHOUT 14 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 15 FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 16 details. 17 18 You should have received a copy of the GNU Lesser General Public License along 19 with this program. If not, see <http://www.gnu.org/licenses/>. 20 """ 21 22 from WebStack.Generic import EndOfResponse, ContentType 23 import XSLForms.Fields 24 import XSLForms.Prepare 25 import XSLForms.Output 26 import XSLForms.Utils 27 import XSLForms.Resources.Common 28 from XSLTools import XSLOutput 29 import os 30 31 class XSLFormsResource(XSLForms.Resources.Common.CommonResource): 32 33 """ 34 A generic XSLForms resource for use with WebStack. 35 36 When overriding this class, define the following attributes appropriately: 37 38 * template_resources - a dictionary mapping output identifiers to 39 (template_filename, output_filename) tuples, 40 indicating the template and stylesheet filenames 41 to be employed 42 43 * in_page_resources - a dictionary mapping fragment identifiers to 44 (output_identifier, output_filename, 45 node_identifier) tuples, indicating the output 46 identifier for which the fragment applies, the 47 stylesheet filename to be employed, along with 48 the node identifier used in the original 49 template and output documents to mark a region 50 of those documents as the fragment to be updated 51 upon "in-page" requests 52 53 * init_resources - a dictionary mapping initialiser/input 54 identifiers to (template_filename, 55 input_filename) tuples, indicating the template 56 and initialiser/input stylesheet filenames to be 57 employed 58 59 * transform_resources - a dictionary mapping transform identifiers to 60 lists of stylesheet filenames for use with the 61 transformation methods 62 63 * document_resources - a dictionary mapping document identifiers to 64 single filenames for use as source documents or 65 as references with the transformation methods 66 67 * resource_dir - the absolute path of the directory in which 68 stylesheet resources are to reside 69 70 All filenames shall be simple leafnames for files residing in the resource's 71 special resource directory 'resource_dir'. 72 73 The following attributes may also be specified: 74 75 * path_encoding - the assumed encoding of characters in request 76 paths 77 78 * encoding - the assumed encoding of characters in request 79 bodies 80 81 To provide actual functionality to resources, either override the 82 'respond_to_form' method and write the code for obtaining input, 83 initialising documents, creating output, and so on in that method, or 84 provide implementations for the following methods: 85 86 * select_activity - sets the activity name which will be used by the 87 default implementations of the other methods 88 89 * create_document - creates or obtains a document for the resource's 90 activity (need not be overridden) 91 92 * respond_to_input - application logic relying on any input from the 93 request, including submitted document 94 information 95 96 * init_document - initialises the document according to the 97 'init_resources' attribute described above (need 98 not be overridden) 99 100 * respond_to_document - application logic relying on any information 101 from the initialised document 102 103 * create_output - creates and sends final output to the user (need 104 not be overridden) 105 """ 106 107 EMPTY_NAMESPACE = XSLForms.Fields.EMPTY_NAMESPACE 108 FILE_NAMESPACE = XSLForms.Fields.FILE_NAMESPACE 109 110 #path_encoding = "utf-8" 111 #encoding = "utf-8" 112 113 template_resources = {} 114 in_page_resources = {} 115 init_resources = {} 116 transform_resources = {} 117 document_resources = {} 118 119 def clean_parameters(self, parameters): 120 121 """ 122 Workaround stray zero value characters from Konqueror in XMLHttpRequest 123 communications. 124 """ 125 126 for name, values in parameters.items(): 127 new_values = [] 128 for value in values: 129 if isinstance(value, (str, unicode)) and value.endswith("\x00"): 130 new_values.append(value[:-1]) 131 else: 132 new_values.append(value) 133 parameters[name] = new_values 134 135 def prepare_output(self, output_identifier): 136 137 """ 138 Prepare the output stylesheets using the given 'output_identifier' to 139 indicate which templates and stylesheets are to be employed in the 140 production of output from the resource. 141 142 The 'output_identifier' is used as a key to the 'template_resources' 143 dictionary attribute. 144 145 Return the full path to the output stylesheet for use with 'send_output' 146 or 'get_result'. 147 """ 148 149 template_path, output_path = prepare_output(self, output_identifier) 150 return output_path 151 152 def prepare_fragment(self, fragment_identifier): 153 154 """ 155 Prepare the output stylesheets for the given 'fragment_identifier', 156 indicating which templates and stylesheets are to be employed in the 157 production of output from the resource. 158 159 The 'fragment_identifier' is used as a key to the 'in_page_resources' 160 dictionary attribute which in turn obtains an 'output_identifier', which 161 is used as a key to the 'template_resources' dictionary attribute. 162 163 Return the full path to the output stylesheet for use with 'send_output' 164 or 'get_result'. 165 """ 166 167 template_path, fragment_path = prepare_fragment(self, fragment_identifier) 168 return fragment_path 169 170 def prepare_parameters(self, parameters): 171 172 """ 173 Prepare the stylesheet parameters from the given request 'parameters'. 174 This is most useful when preparing fragments for in-page update output. 175 """ 176 177 element_path = parameters.get("element-path", [""])[0] 178 if element_path: 179 return {"element-path" : element_path} 180 else: 181 return {} 182 183 def send_output(self, trans, stylesheet_filenames, document, stylesheet_parameters=None, 184 stylesheet_expressions=None, references=None): 185 186 """ 187 Send the output from the resource to the user employing the transaction 188 'trans', stylesheets having the given 'stylesheet_filenames', the 189 'document' upon which the output will be based, the optional parameters 190 as defined in the 'stylesheet_parameters' dictionary, the optional 191 expressions are defined in the 'stylesheet_expressions' dictionary, and 192 the optional 'references' to external documents. 193 """ 194 195 # Provide support for deficient browsers like Internet Explorer. 196 197 content_type = trans.content_type 198 199 # NOTE: Introduce proper content type parsing into WebStack. 200 201 accept = (trans.get_header_values("Accept") or [""])[0] 202 xhtml = "application/xhtml+xml" 203 204 if content_type is not None and content_type.media_type == xhtml and accept.find(xhtml) == -1: 205 206 # Get the conversion template. 207 208 stylesheet_filenames.append(os.path.join(XSLForms.Prepare.resource_dir, "XHTMLToHTML.xsl")) 209 trans.set_content_type(ContentType("text/html", content_type.charset or trans.default_encoding)) 210 211 # Sanity check for the filenames list. 212 213 if isinstance(stylesheet_filenames, str) or isinstance(stylesheet_filenames, unicode): 214 raise ValueError, stylesheet_filenames 215 216 proc = XSLOutput.Processor(stylesheet_filenames, parameters=stylesheet_parameters, 217 expressions=stylesheet_expressions, references=references) 218 proc.send_output(trans.get_response_stream(), trans.get_response_stream_encoding(), 219 document) 220 221 def get_result(self, stylesheet_filenames, document, stylesheet_parameters=None, 222 stylesheet_expressions=None, references=None): 223 224 """ 225 Get the result of applying a transformation using stylesheets with the 226 given 'stylesheet_filenames', the 'document' upon which the result will 227 be based, the optional parameters as defined in the 228 'stylesheet_parameters' dictionary, the optional parameters as defined 229 in the 'stylesheet_parameters' dictionary and the optional 'references' 230 to external documents. 231 """ 232 233 # Sanity check for the filenames list. 234 235 if isinstance(stylesheet_filenames, str) or isinstance(stylesheet_filenames, unicode): 236 raise ValueError, stylesheet_filenames 237 238 proc = XSLOutput.Processor(stylesheet_filenames, parameters=stylesheet_parameters, 239 expressions=stylesheet_expressions, references=references) 240 return proc.get_result(document) 241 242 def prepare_initialiser(self, input_identifier, init_enumerations=1): 243 244 """ 245 Prepare an initialiser/input transformation using the given 246 'input_identifier'. The optional 'init_enumerations' (defaulting to 247 true) may be used to indicate whether enumerations are to be initialised 248 from external documents. 249 250 Return the full path to the input stylesheet for use with 'send_output' 251 or 'get_result'. 252 """ 253 254 template_path, input_path = prepare_initialiser(self, input_identifier, init_enumerations) 255 return input_path 256 257 def prepare_transform(self, transform_identifier): 258 259 """ 260 Prepare a transformation using the given 'transform_identifier'. 261 262 Return a list of full paths to the output stylesheets for use with 263 'send_output' or 'get_result'. 264 """ 265 266 filenames = self.transform_resources[transform_identifier] 267 268 # Sanity check for the filenames list. 269 270 if isinstance(filenames, str) or isinstance(filenames, unicode): 271 raise ValueError, filenames 272 273 paths = [] 274 for filename in filenames: 275 paths.append(os.path.abspath(os.path.join(self.resource_dir, filename))) 276 return paths 277 278 def _get_in_page_resource(self, trans): 279 280 """ 281 Return the in-page resource being referred to in the given transaction 282 'trans'. 283 """ 284 285 if hasattr(self, "path_encoding"): 286 return trans.get_path_info(self.path_encoding).split("/")[-1] 287 else: 288 return trans.get_path_info().split("/")[-1] 289 290 def get_in_page_resource(self, trans): 291 292 """ 293 Return the in-page resource being referred to in the given transaction 294 'trans' or None if no valid in-page resource is being referenced. 295 """ 296 297 name = self._get_in_page_resource(trans) 298 if self.in_page_resources.has_key(name): 299 return name 300 else: 301 return None 302 303 def respond(self, trans): 304 305 """ 306 Respond to the request described by the given transaction 'trans'. 307 """ 308 309 # Only obtain field information according to the stated method. 310 311 content_type = trans.get_content_type() 312 method = trans.get_request_method() 313 in_page_resource = self.get_in_page_resource(trans) 314 315 # Handle typical request methods, processing request information. 316 317 if method == "GET": 318 319 # Get the fields from the request path (URL). 320 321 form = XSLForms.Fields.Form(encoding=None, values_are_lists=1) 322 parameters = trans.get_fields_from_path() 323 form.set_parameters(parameters) 324 325 elif method == "POST" and content_type.media_type in ( 326 "application/x-www-form-urlencoded", "multipart/form-data"): 327 328 # Get the fields from the request body. 329 330 form = XSLForms.Fields.Form(encoding=None, values_are_lists=1) 331 if hasattr(self, "encoding"): 332 parameters = trans.get_fields_from_body(self.encoding) 333 else: 334 parameters = trans.get_fields_from_body() 335 336 # NOTE: Konqueror workaround. 337 self.clean_parameters(parameters) 338 339 form.set_parameters(parameters) 340 341 else: 342 343 # Initialise empty container. 344 345 form = XSLForms.Fields.Form(encoding=None, values_are_lists=1) 346 347 # Call an overridden method with the processed request information. 348 349 self.respond_to_form(trans, form) 350 351 def respond_to_form(self, trans, form): 352 353 """ 354 Respond to the request described by the given transaction 'trans', using 355 the given 'form' object to conveniently retrieve field (request 356 parameter) information and structured form information (as DOM-style XML 357 documents). 358 """ 359 360 self.select_activity(trans, form) 361 self.create_document(trans, form) 362 self.respond_to_input(trans, form) 363 self.init_document(trans, form) 364 self.respond_to_document(trans, form) 365 self.create_output(trans, form) 366 raise EndOfResponse 367 368 # Modular methods for responding to requests. 369 370 def select_activity(self, trans, form): 371 372 """ 373 Using the given transaction 'trans' and 'form' information, select the 374 activity being performed and set the 'current_activity' attribute in the 375 transaction. 376 """ 377 378 pass 379 380 def create_document(self, trans, form): 381 382 """ 383 Using the given transaction 'trans' and 'form' information, create the 384 document involved in the current activity and set the 'current_document' 385 attribute in the transaction. 386 387 Return whether a new document was created. 388 """ 389 390 documents = form.get_documents() 391 activity = form.get_activity() 392 393 if documents.has_key(activity): 394 form.set_document(documents[activity]) 395 return 0 396 else: 397 form.new_document(activity) 398 form.new_documents.add(activity) 399 return 1 400 401 def respond_to_input(self, trans, form): 402 403 """ 404 Using the given transaction 'trans' and 'form' information, perform the 405 parts of the current activity which rely on the information supplied in 406 the current document. 407 """ 408 409 pass 410 411 def init_document(self, trans, form, stylesheet_parameters=None, 412 stylesheet_expressions=None, references=None): 413 414 """ 415 Using the given transaction 'trans' and 'form' information, initialise 416 the current document. 417 """ 418 419 activity = form.get_activity() 420 421 # Transform, adding enumerations/ranges. 422 423 if self.init_resources.has_key(activity): 424 init_xsl = self.prepare_initialiser(activity) 425 form.set_document( 426 self.get_result( 427 [init_xsl], form.get_document(), stylesheet_parameters, 428 stylesheet_expressions, references 429 ) 430 ) 431 432 def respond_to_document(self, trans, form): 433 434 """ 435 Using the given transaction 'trans' and 'form' information, perform the 436 parts of the current activity which rely on a populated version of the 437 current document. 438 """ 439 440 pass 441 442 def create_output(self, trans, form, content_type=None, 443 stylesheet_parameters=None, stylesheet_expressions=None, references=None): 444 445 """ 446 Using the given transaction 'trans' and 'form' information, create the 447 output for the current activity using the previously set attributes in 448 the transaction. 449 """ 450 451 attributes = trans.get_attributes() 452 in_page_resource = self.get_in_page_resource(trans) 453 parameters = form.get_parameters() 454 455 # Start the response. 456 457 if attributes.has_key("encoding"): 458 encoding = attributes["encoding"] # NOTE: Potentially redundant. 459 elif hasattr(self, "encoding"): 460 encoding = self.encoding 461 else: 462 encoding = trans.default_charset 463 464 content_type = content_type or ContentType("application/xhtml+xml", encoding) 465 trans.set_content_type(content_type) 466 467 # Ensure that an output stylesheet exists. 468 469 stylesheet_parameters = stylesheet_parameters or {} 470 471 if in_page_resource: 472 trans_xsl = self.prepare_fragment(in_page_resource) 473 stylesheet_parameters.update(self.prepare_parameters(parameters)) 474 else: 475 trans_xsl = self.prepare_output(form.get_activity()) 476 477 # Complete the response. 478 479 self.send_output(trans, [trans_xsl], form.get_document(), 480 stylesheet_parameters, stylesheet_expressions, references) 481 482 # General helper methods. 483 484 def add_elements(self, positions, *element_names): 485 486 """ 487 At the given 'positions', typically obtained as "selectors", add the 488 hierarchy of elements given in the 'element_names' parameters. 489 """ 490 491 XSLForms.Utils.add_elements(positions, *element_names) 492 493 def remove_elements(self, positions): 494 495 """ 496 Remove elements at the given 'positions', typically obtained as 497 "selectors". 498 """ 499 500 XSLForms.Utils.remove_elements(positions) 501 502 def prepare_output(self, output_identifier): 503 504 """ 505 Prepare the output stylesheet for the resource class or object 'self' 506 corresponding to the given 'output_identifier'. Return the template path 507 and the output stylesheet path in a 2-tuple. 508 """ 509 510 template_filename, output_filename = self.template_resources[output_identifier] 511 output_path = os.path.abspath(os.path.join(self.resource_dir, output_filename)) 512 template_path = os.path.abspath(os.path.join(self.resource_dir, template_filename)) 513 XSLForms.Prepare.ensure_stylesheet(template_path, output_path) 514 return template_path, output_path 515 516 def prepare_fragment(self, fragment_identifier): 517 518 """ 519 Prepare the output stylesheet for the resource class or object 'self' 520 corresponding to the given 'fragment_identifier'. Return the template path 521 and the output stylesheet path in a 2-tuple. 522 """ 523 524 output_identifier, fragment_filename, node_identifier = self.in_page_resources[fragment_identifier] 525 fragment_path = os.path.abspath(os.path.join(self.resource_dir, fragment_filename)) 526 template_filename, output_filename = self.template_resources[output_identifier] 527 template_path = os.path.abspath(os.path.join(self.resource_dir, template_filename)) 528 XSLForms.Prepare.ensure_stylesheet_fragment(template_path, fragment_path, node_identifier) 529 return template_path, fragment_path 530 531 def prepare_initialiser(self, input_identifier, init_enumerations): 532 533 """ 534 Prepare the initialising stylesheet for the resource class or object 'self' 535 corresponding to the given 'input_identifier' and 'init_enumerations' flag. 536 Return the template path and the initialising stylesheet path in a 2-tuple. 537 """ 538 539 template_filename, input_filename = self.init_resources[input_identifier] 540 input_path = os.path.abspath(os.path.join(self.resource_dir, input_filename)) 541 template_path = os.path.abspath(os.path.join(self.resource_dir, template_filename)) 542 XSLForms.Prepare.ensure_input_stylesheet(template_path, input_path, init_enumerations) 543 return template_path, input_path 544 545 def prepare_resources(cls): 546 547 "Prepare the resources associated with the class 'cls'." 548 549 for output_identifier in cls.template_resources.keys(): 550 prepare_output(cls, output_identifier) 551 for fragment_identifier in cls.in_page_resources.keys(): 552 prepare_fragment(cls, fragment_identifier) 553 554 # NOTE: Using init_enumerations=1 here. 555 556 for input_identifier in cls.init_resources.keys(): 557 prepare_initialiser(cls, input_identifier, 1) 558 559 # Convenience methods for specifying resources. 560 561 def split(filename): 562 563 """ 564 Return a tuple containing the directory and filename without extension for 565 'filename'. 566 """ 567 568 d, leafname = os.path.split(filename) 569 name, ext = os.path.splitext(leafname) 570 return d, name 571 572 def output(template_filename): 573 574 """ 575 Return a tuple containing the 'template_filename' and a suitable output 576 stylesheet filename. 577 """ 578 579 d, name = split(template_filename) 580 output_name = name.replace("_template", "_output") + os.path.extsep + "xsl" 581 return (template_filename, os.path.join(d, output_name)) 582 583 def input(template_filename): 584 585 """ 586 Return a tuple containing the 'template_filename' and a suitable output 587 stylesheet filename. 588 """ 589 590 d, name = split(template_filename) 591 input_name = name.replace("_template", "_input") + os.path.extsep + "xsl" 592 return (template_filename, os.path.join(d, input_name)) 593 594 def resources(filename, d="Resources"): 595 596 """ 597 Return the resource directory for the given 'filename', using the optional 598 directory name 'd' to indicate the directory relative to the directory of 599 'filename' (or the default directory name, indicating that the directory 600 called "Resources" - a sibling of 'filename' - is the resource directory). 601 602 It is envisaged that callers provide the value of the __file__ special 603 variable to get the resource directory relative to a particular module. 604 """ 605 606 return os.path.join(os.path.split(filename)[0], d) 607 608 # vim: tabstop=4 expandtab shiftwidth=4