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