1 #!/usr/bin/env python 2 3 """ 4 Resources for use with WebStack. 5 6 Copyright (C) 2005, 2006, 2007, 2008 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 import WebStack.Generic 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 path_encoding = "utf-8" 108 encoding = "utf-8" 109 template_resources = {} 110 in_page_resources = {} 111 init_resources = {} 112 transform_resources = {} 113 114 def clean_parameters(self, parameters): 115 116 """ 117 Workaround stray zero value characters from Konqueror in XMLHttpRequest 118 communications. 119 """ 120 121 for name, values in parameters.items(): 122 new_values = [] 123 for value in values: 124 if value.endswith("\x00"): 125 new_values.append(value[:-1]) 126 else: 127 new_values.append(value) 128 parameters[name] = new_values 129 130 def prepare_output(self, output_identifier): 131 132 """ 133 Prepare the output stylesheets using the given 'output_identifier' to 134 indicate which templates and stylesheets are to be employed in the 135 production of output from the resource. 136 137 The 'output_identifier' is used as a key to the 'template_resources' 138 dictionary attribute. 139 140 Return the full path to the output stylesheet for use with 'send_output' 141 or 'get_result'. 142 """ 143 144 template_path, output_path = prepare_output(self, output_identifier) 145 return output_path 146 147 def prepare_fragment(self, fragment_identifier): 148 149 """ 150 Prepare the output stylesheets for the given 'fragment_identifier', 151 indicating which templates and stylesheets are to be employed in the 152 production of output from the resource. 153 154 The 'fragment_identifier' is used as a key to the 'in_page_resources' 155 dictionary attribute which in turn obtains an 'output_identifier', which 156 is used as a key to the 'template_resources' dictionary attribute. 157 158 Return the full path to the output stylesheet for use with 'send_output' 159 or 'get_result'. 160 """ 161 162 template_path, fragment_path = prepare_fragment(self, fragment_identifier) 163 return fragment_path 164 165 def prepare_parameters(self, parameters): 166 167 """ 168 Prepare the stylesheet parameters from the given request 'parameters'. 169 This is most useful when preparing fragments for in-page update output. 170 """ 171 172 element_path = parameters.get("element-path", [""])[0] 173 if element_path: 174 return {"element-path" : element_path} 175 else: 176 return {} 177 178 def send_output(self, trans, stylesheet_filenames, document, stylesheet_parameters=None, 179 stylesheet_expressions=None, references=None): 180 181 """ 182 Send the output from the resource to the user employing the transaction 183 'trans', stylesheets having the given 'stylesheet_filenames', the 184 'document' upon which the output will be based, the optional parameters 185 as defined in the 'stylesheet_parameters' dictionary, the optional 186 expressions are defined in the 'stylesheet_expressions' dictionary, and 187 the optional 'references' to external documents. 188 """ 189 190 # Sanity check for the filenames list. 191 192 if isinstance(stylesheet_filenames, str) or isinstance(stylesheet_filenames, unicode): 193 raise ValueError, stylesheet_filenames 194 195 proc = XSLOutput.Processor(stylesheet_filenames, parameters=stylesheet_parameters, 196 expressions=stylesheet_expressions, references=references) 197 proc.send_output(trans.get_response_stream(), trans.get_response_stream_encoding(), 198 document) 199 200 def get_result(self, stylesheet_filenames, document, stylesheet_parameters=None, 201 stylesheet_expressions=None, references=None): 202 203 """ 204 Get the result of applying a transformation using stylesheets with the 205 given 'stylesheet_filenames', the 'document' upon which the result will 206 be based, the optional parameters as defined in the 207 'stylesheet_parameters' dictionary, the optional parameters as defined 208 in the 'stylesheet_parameters' dictionary and the optional 'references' 209 to external documents. 210 """ 211 212 # Sanity check for the filenames list. 213 214 if isinstance(stylesheet_filenames, str) or isinstance(stylesheet_filenames, unicode): 215 raise ValueError, stylesheet_filenames 216 217 proc = XSLOutput.Processor(stylesheet_filenames, parameters=stylesheet_parameters, 218 expressions=stylesheet_expressions, references=references) 219 return proc.get_result(document) 220 221 def prepare_initialiser(self, input_identifier, init_enumerations=1): 222 223 """ 224 Prepare an initialiser/input transformation using the given 225 'input_identifier'. The optional 'init_enumerations' (defaulting to 226 true) may be used to indicate whether enumerations are to be initialised 227 from external documents. 228 229 Return the full path to the input stylesheet for use with 'send_output' 230 or 'get_result'. 231 """ 232 233 template_path, input_path = prepare_initialiser(self, input_identifier, init_enumerations) 234 return input_path 235 236 def prepare_transform(self, transform_identifier): 237 238 """ 239 Prepare a transformation using the given 'transform_identifier'. 240 241 Return a list of full paths to the output stylesheets for use with 242 'send_output' or 'get_result'. 243 """ 244 245 filenames = self.transform_resources[transform_identifier] 246 247 # Sanity check for the filenames list. 248 249 if isinstance(filenames, str) or isinstance(filenames, unicode): 250 raise ValueError, filenames 251 252 paths = [] 253 for filename in filenames: 254 paths.append(os.path.abspath(os.path.join(self.resource_dir, filename))) 255 return paths 256 257 def _get_in_page_resource(self, trans): 258 259 """ 260 Return the in-page resource being referred to in the given transaction 261 'trans'. 262 """ 263 264 if hasattr(self, "path_encoding"): 265 return trans.get_path_info(self.path_encoding).split("/")[-1] 266 else: 267 return trans.get_path_info().split("/")[-1] 268 269 def get_in_page_resource(self, trans): 270 271 """ 272 Return the in-page resource being referred to in the given transaction 273 'trans' or None if no valid in-page resource is being referenced. 274 """ 275 276 name = self._get_in_page_resource(trans) 277 if self.in_page_resources.has_key(name): 278 return name 279 else: 280 return None 281 282 def respond(self, trans): 283 284 """ 285 Respond to the request described by the given transaction 'trans'. 286 """ 287 288 # Only obtain field information according to the stated method. 289 290 content_type = trans.get_content_type() 291 method = trans.get_request_method() 292 in_page_resource = self.get_in_page_resource(trans) 293 294 # Handle typical request methods, processing request information. 295 296 if method == "GET": 297 298 # Get the fields from the request path (URL). 299 300 form = XSLForms.Fields.Form(encoding=None, values_are_lists=1) 301 parameters = trans.get_fields_from_path() 302 form.set_parameters(parameters) 303 304 elif method == "POST" and content_type.media_type == "application/x-www-form-urlencoded": 305 306 # Get the fields from the request body. 307 308 form = XSLForms.Fields.Form(encoding=None, values_are_lists=1) 309 if hasattr(self, "encoding"): 310 parameters = trans.get_fields_from_body(self.encoding) 311 else: 312 parameters = trans.get_fields_from_body() 313 314 # NOTE: Konqueror workaround. 315 self.clean_parameters(parameters) 316 317 form.set_parameters(parameters) 318 319 else: 320 321 # Initialise empty container. 322 323 form = XSLForms.Fields.Form(encoding=None, values_are_lists=1) 324 325 # Call an overridden method with the processed request information. 326 327 self.respond_to_form(trans, form) 328 329 def respond_to_form(self, trans, form): 330 331 """ 332 Respond to the request described by the given transaction 'trans', using 333 the given 'form' object to conveniently retrieve field (request 334 parameter) information and structured form information (as DOM-style XML 335 documents). 336 """ 337 338 self.select_activity(trans, form) 339 self.create_document(trans, form) 340 self.respond_to_input(trans, form) 341 self.init_document(trans, form) 342 self.respond_to_document(trans, form) 343 self.create_output(trans, form) 344 raise WebStack.Generic.EndOfResponse 345 346 # Modular methods for responding to requests. 347 348 def select_activity(self, trans, form): 349 350 """ 351 Using the given transaction 'trans' and 'form' information, select the 352 activity being performed and set the 'current_activity' attribute in the 353 transaction. 354 """ 355 356 pass 357 358 def create_document(self, trans, form): 359 360 """ 361 Using the given transaction 'trans' and 'form' information, create the 362 document involved in the current activity and set the 'current_document' 363 attribute in the transaction. 364 365 Return whether a new document was created. 366 """ 367 368 documents = form.get_documents() 369 activity = form.get_activity() 370 371 if documents.has_key(activity): 372 form.set_document(documents[activity]) 373 return 0 374 else: 375 form.new_document(activity) 376 form.new_documents.add(activity) 377 return 1 378 379 def respond_to_input(self, trans, form): 380 381 """ 382 Using the given transaction 'trans' and 'form' information, perform the 383 parts of the current activity which rely on the information supplied in 384 the current document. 385 """ 386 387 pass 388 389 def init_document(self, trans, form, stylesheet_parameters=None, 390 stylesheet_expressions=None, references=None): 391 392 """ 393 Using the given transaction 'trans' and 'form' information, initialise 394 the current document. 395 """ 396 397 activity = form.get_activity() 398 399 # Transform, adding enumerations/ranges. 400 401 if self.init_resources.has_key(activity): 402 init_xsl = self.prepare_initialiser(activity) 403 form.set_document( 404 self.get_result( 405 [init_xsl], form.get_document(), stylesheet_parameters, 406 stylesheet_expressions, references 407 ) 408 ) 409 410 def respond_to_document(self, trans, form): 411 412 """ 413 Using the given transaction 'trans' and 'form' information, perform the 414 parts of the current activity which rely on a populated version of the 415 current document. 416 """ 417 418 pass 419 420 def create_output(self, trans, form, content_type=None, 421 stylesheet_parameters=None, stylesheet_expressions=None, references=None): 422 423 """ 424 Using the given transaction 'trans' and 'form' information, create the 425 output for the current activity using the previously set attributes in 426 the transaction. 427 """ 428 429 attributes = trans.get_attributes() 430 in_page_resource = self.get_in_page_resource(trans) 431 parameters = form.get_parameters() 432 433 # Start the response. 434 435 encoding = attributes.get("encoding") or self.encoding or trans.default_charset 436 content_type = content_type or WebStack.Generic.ContentType("application/xhtml+xml", encoding) 437 trans.set_content_type(content_type) 438 439 # Ensure that an output stylesheet exists. 440 441 stylesheet_parameters = stylesheet_parameters or {} 442 443 if in_page_resource: 444 trans_xsl = self.prepare_fragment(in_page_resource) 445 stylesheet_parameters.update(self.prepare_parameters(parameters)) 446 else: 447 trans_xsl = self.prepare_output(form.get_activity()) 448 449 # Complete the response. 450 451 self.send_output(trans, [trans_xsl], form.get_document(), 452 stylesheet_parameters, stylesheet_expressions, references) 453 454 # General helper methods. 455 456 def add_elements(self, positions, *element_names): 457 458 """ 459 At the given 'positions', typically obtained as "selectors", add the 460 hierarchy of elements given in the 'element_names' parameters. 461 """ 462 463 XSLForms.Utils.add_elements(positions, *element_names) 464 465 def remove_elements(self, positions): 466 467 """ 468 Remove elements at the given 'positions', typically obtained as 469 "selectors". 470 """ 471 472 XSLForms.Utils.remove_elements(positions) 473 474 def prepare_output(self, output_identifier): 475 476 """ 477 Prepare the output stylesheet for the resource class or object 'self' 478 corresponding to the given 'output_identifier'. Return the template path 479 and the output stylesheet path in a 2-tuple. 480 """ 481 482 template_filename, output_filename = self.template_resources[output_identifier] 483 output_path = os.path.abspath(os.path.join(self.resource_dir, output_filename)) 484 template_path = os.path.abspath(os.path.join(self.resource_dir, template_filename)) 485 XSLForms.Prepare.ensure_stylesheet(template_path, output_path) 486 return template_path, output_path 487 488 def prepare_fragment(self, fragment_identifier): 489 490 """ 491 Prepare the output stylesheet for the resource class or object 'self' 492 corresponding to the given 'fragment_identifier'. Return the template path 493 and the output stylesheet path in a 2-tuple. 494 """ 495 496 output_identifier, fragment_filename, node_identifier = self.in_page_resources[fragment_identifier] 497 fragment_path = os.path.abspath(os.path.join(self.resource_dir, fragment_filename)) 498 template_filename, output_filename = self.template_resources[output_identifier] 499 template_path = os.path.abspath(os.path.join(self.resource_dir, template_filename)) 500 XSLForms.Prepare.ensure_stylesheet_fragment(template_path, fragment_path, node_identifier) 501 return template_path, fragment_path 502 503 def prepare_initialiser(self, input_identifier, init_enumerations): 504 505 """ 506 Prepare the initialising stylesheet for the resource class or object 'self' 507 corresponding to the given 'input_identifier' and 'init_enumerations' flag. 508 Return the template path and the initialising stylesheet path in a 2-tuple. 509 """ 510 511 template_filename, input_filename = self.init_resources[input_identifier] 512 input_path = os.path.abspath(os.path.join(self.resource_dir, input_filename)) 513 template_path = os.path.abspath(os.path.join(self.resource_dir, template_filename)) 514 XSLForms.Prepare.ensure_input_stylesheet(template_path, input_path, init_enumerations) 515 return template_path, input_path 516 517 def prepare_resources(cls): 518 519 "Prepare the resources associated with the class 'cls'." 520 521 for output_identifier in cls.template_resources.keys(): 522 prepare_output(cls, output_identifier) 523 for fragment_identifier in cls.in_page_resources.keys(): 524 prepare_fragment(cls, fragment_identifier) 525 526 # NOTE: Using init_enumerations=1 here. 527 528 for input_identifier in cls.init_resources.keys(): 529 prepare_initialiser(cls, input_identifier, 1) 530 531 # Convenience methods for specifying resources. 532 533 def split(filename): 534 535 """ 536 Return a tuple containing the directory and filename without extension for 537 'filename'. 538 """ 539 540 d, leafname = os.path.split(filename) 541 name, ext = os.path.splitext(leafname) 542 return d, name 543 544 def output(template_filename): 545 546 """ 547 Return a tuple containing the 'template_filename' and a suitable output 548 stylesheet filename. 549 """ 550 551 d, name = split(template_filename) 552 output_name = name.replace("_template", "_output") + os.path.extsep + "xsl" 553 return (template_filename, os.path.join(d, output_name)) 554 555 def input(template_filename): 556 557 """ 558 Return a tuple containing the 'template_filename' and a suitable output 559 stylesheet filename. 560 """ 561 562 d, name = split(template_filename) 563 input_name = name.replace("_template", "_input") + os.path.extsep + "xsl" 564 return (template_filename, os.path.join(d, input_name)) 565 566 def resources(filename, d="Resources"): 567 568 """ 569 Return the resource directory for the given 'filename', using the optional 570 directory name 'd' to indicate the directory relative to the directory of 571 'filename' (or the default directory name, indicating that the directory 572 called "Resources" - a sibling of 'filename' - is the resource directory). 573 574 It is envisaged that callers provide the value of the __file__ special 575 variable to get the resource directory relative to a particular module. 576 """ 577 578 return os.path.join(os.path.split(filename)[0], d) 579 580 # vim: tabstop=4 expandtab shiftwidth=4