1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
1.2 +++ b/XSLForms/Fields.py Mon Dec 20 00:30:39 2004 +0000
1.3 @@ -0,0 +1,332 @@
1.4 +#!/usr/bin/env python
1.5 +
1.6 +"""
1.7 +Classes which process field collections, producing instance
1.8 +documents. Each field entry consists of a field name mapped
1.9 +to a string value, where the field name may have the following
1.10 +formats:
1.11 +
1.12 + /name1#n1/name2
1.13 + /name1#n1/name2#n2/name3
1.14 + /name1#n1/name2#n2/name3#n3/name4
1.15 + ...
1.16 +
1.17 +The indexes n1, n2, n3, ... indicate the position of elements
1.18 +(starting from 1) in the entire element list, whose elements
1.19 +may have different names. For example:
1.20 +
1.21 + /zoo#1/name
1.22 + /zoo#1/cage#1:name
1.23 + /zoo#1/cage#2:name
1.24 + /zoo#1/funding#3/contributor#1/name
1.25 +
1.26 +Some fields may contain the "=" string. This string is
1.27 +reserved and all text following it is meant to specify a path
1.28 +into a particular document. For example:
1.29 +
1.30 + _action_add_animal=/zoo#1/cage#2
1.31 +"""
1.32 +
1.33 +import libxml2dom
1.34 +from xml.dom import EMPTY_NAMESPACE
1.35 +
1.36 +class FieldsError(Exception):
1.37 + pass
1.38 +
1.39 +class Fields:
1.40 +
1.41 + """
1.42 + A class which converts fields in the documented form to XML
1.43 + instance documents.
1.44 + """
1.45 +
1.46 + _path_separator = "/"
1.47 + _pair_separator = "#"
1.48 + _selector_indicator = "="
1.49 +
1.50 + def __init__(self, encoding="utf-8"):
1.51 +
1.52 + """
1.53 + Initialise the fields processor with the given 'encoding',
1.54 + which is optional and which only applies to field data in
1.55 + Python string form (and not Unicode objects).
1.56 + """
1.57 +
1.58 + self.encoding = encoding
1.59 +
1.60 + def complete_documents(self, documents, fields):
1.61 +
1.62 + """
1.63 + Complete the given 'documents' using the 'fields' items list.
1.64 + """
1.65 +
1.66 + for field, value in fields:
1.67 +
1.68 + # Ignore selectors.
1.69 +
1.70 + if field.find(self._selector_indicator) != -1:
1.71 + continue
1.72 +
1.73 + model_name, components = self._get_model_name_and_components(field)
1.74 + if model_name is None:
1.75 + continue
1.76 +
1.77 + # Convert the value to Unicode if necessary.
1.78 +
1.79 + if type(value) == type(""):
1.80 + value = unicode(value, encoding=self.encoding)
1.81 +
1.82 + # Get a new instance document if none has been made for the
1.83 + # model.
1.84 +
1.85 + if not documents.has_key(model_name):
1.86 + documents[model_name] = self._new_instance(model_name)
1.87 + node = documents[model_name]
1.88 +
1.89 + # Traverse the components within the instance.
1.90 +
1.91 + for component in components:
1.92 + t = component.split(self._pair_separator)
1.93 + if len(t) == 1:
1.94 + node.setAttributeNS(EMPTY_NAMESPACE, t[0], value)
1.95 + break
1.96 +
1.97 + elif len(t) == 2:
1.98 +
1.99 + # Convert from one-based indexing (the position() function)
1.100 + # to zero-based indexing.
1.101 +
1.102 + name, index = t[0], int(t[1]) - 1
1.103 + if index < 0:
1.104 + break
1.105 + node = self._enter_element(node, name, index)
1.106 +
1.107 + def complete_selectors(self, selectors, fields, documents):
1.108 +
1.109 + """
1.110 + Fill in the given 'selectors' dictionary using the given
1.111 + 'fields' so that it contains mappings from selector names to
1.112 + parts of the specified 'documents'.
1.113 + """
1.114 +
1.115 + for field, value in fields:
1.116 +
1.117 + # Process selectors only.
1.118 +
1.119 + selector_components = field.split(self._selector_indicator)
1.120 + if len(selector_components) < 2:
1.121 + continue
1.122 +
1.123 + # Get the selector name and path.
1.124 + # Note that the joining of the components uses the separator,
1.125 + # but the separator really should not exist in the path.
1.126 +
1.127 + selector_name = selector_components[0]
1.128 + path = self._selector_indicator.join(selector_components[1:])
1.129 +
1.130 + model_name, components = self._get_model_name_and_components(path)
1.131 + if model_name is None:
1.132 + continue
1.133 +
1.134 + # Go to the instance element.
1.135 +
1.136 + if not documents.has_key(model_name) or documents[model_name] is None:
1.137 + continue
1.138 +
1.139 + node = documents[model_name]
1.140 +
1.141 + # Traverse the path to find the part of the document to be
1.142 + # selected.
1.143 +
1.144 + for component in components:
1.145 + t = component.split(self._pair_separator)
1.146 + if len(t) == 1:
1.147 +
1.148 + # Select attribute.
1.149 +
1.150 + node = node.getAttributeNodeNS(EMPTY_NAMESPACE, t[0])
1.151 + break
1.152 +
1.153 + elif len(t) == 2:
1.154 +
1.155 + # Convert from one-based indexing (the position() function)
1.156 + # to zero-based indexing.
1.157 +
1.158 + name, index = t[0], int(t[1]) - 1
1.159 + if index < 0:
1.160 + break
1.161 +
1.162 + # NOTE: Controversial creation of potentially non-existent
1.163 + # NOTE: nodes.
1.164 +
1.165 + node = self._enter_element(node, name, index)
1.166 +
1.167 + if not selectors.has_key(selector_name):
1.168 + selectors[selector_name] = []
1.169 + selectors[selector_name].append(node)
1.170 +
1.171 + def _enter_element(self, node, name, index):
1.172 +
1.173 + """
1.174 + From 'node' enter the element with the given 'name' at the
1.175 + given 'index' position amongst the child elements. Create
1.176 + missing child elements if necessary.
1.177 + """
1.178 +
1.179 + self._ensure_elements(node, index)
1.180 +
1.181 + elements = node.xpath("*")
1.182 + if elements[index].localName == "placeholder":
1.183 + new_node = node.ownerDocument.createElementNS(EMPTY_NAMESPACE, name)
1.184 + node.replaceChild(new_node, elements[index])
1.185 + else:
1.186 + new_node = elements[index]
1.187 + if new_node.localName != name:
1.188 + raise FieldsError, (new_node.localName, name, elements, index)
1.189 +
1.190 + # Enter the newly-created element.
1.191 +
1.192 + return new_node
1.193 +
1.194 + def _get_model_name_and_components(self, field):
1.195 +
1.196 + """
1.197 + From 'field', return the model name and components which
1.198 + describe the path within the instance document associated
1.199 + with that model.
1.200 + """
1.201 +
1.202 + # Get the components of the field name.
1.203 + # Example: /name1#n1/name2#n2/name3
1.204 + # Expected: ['', 'name1#n1', 'name2#n2', 'name3']
1.205 +
1.206 + components = field.split(self._path_separator)
1.207 + if len(components) < 2:
1.208 + return None, None
1.209 +
1.210 + # Extract the model name from the top-level element
1.211 + # specification.
1.212 + # Expected: ['name1', 'n1']
1.213 +
1.214 + model_name_and_index = components[1].split(self._pair_separator)
1.215 + if len(model_name_and_index) != 2:
1.216 + return None, None
1.217 +
1.218 + # Expected: 'name1', ['', 'name1#n1', 'name2#n2', 'name3']
1.219 +
1.220 + return model_name_and_index[0], components[1:]
1.221 +
1.222 + def _ensure_elements(self, document, index):
1.223 +
1.224 + """
1.225 + In the given 'document', extend the child elements list
1.226 + so that a node can be stored at the given 'index'.
1.227 + """
1.228 +
1.229 + elements = document.xpath("*")
1.230 + i = len(elements)
1.231 + while i <= index:
1.232 + new_node = document.ownerDocument.createElementNS(EMPTY_NAMESPACE, "placeholder")
1.233 + document.appendChild(new_node)
1.234 + i += 1
1.235 +
1.236 + def make_documents(self, fields):
1.237 +
1.238 + """
1.239 + Make a dictionary mapping model names to new documents prepared
1.240 + from the given 'fields' dictionary.
1.241 + """
1.242 +
1.243 + documents = {}
1.244 + self.complete_documents(documents, fields)
1.245 +
1.246 + # Fix the dictionary to return the actual document root.
1.247 +
1.248 + for model_name, instance_root in documents.items():
1.249 + documents[model_name] = instance_root
1.250 + return documents
1.251 +
1.252 + def get_selectors(self, fields, documents):
1.253 +
1.254 + """
1.255 + Get a dictionary containing a mapping of selector names to
1.256 + selected parts of the given 'documents'.
1.257 + """
1.258 +
1.259 + selectors = {}
1.260 + self.complete_selectors(selectors, fields, documents)
1.261 + return selectors
1.262 +
1.263 + def _new_instance(self, name):
1.264 +
1.265 + "Return an instance root of the given 'name' in a new document."
1.266 +
1.267 + return libxml2dom.createDocument(EMPTY_NAMESPACE, name, None)
1.268 +
1.269 +if __name__ == "__main__":
1.270 +
1.271 + d = [
1.272 + ("_action_update", "Some value"),
1.273 + ("_action_delete=/zoo#1/cage#2", "Some value"),
1.274 + ("/actions#1/update#1/selected", "Some value"), # Not actually used in output documents or input.
1.275 + ("/zoo#1/name", "The Zoo זרו"),
1.276 + ("/zoo#1/cage#1/name", "reptiles"),
1.277 + ("/zoo#1/cage#1/capacity", "5"),
1.278 + ("/zoo#1/cage#1/animal#1/name", "Monty"),
1.279 + ("/zoo#1/cage#1/animal#1/species#1/name", "Python"),
1.280 + ("/zoo#1/cage#1/animal#1/property#2/name", "texture"),
1.281 + ("/zoo#1/cage#1/animal#1/property#2/value", "scaled"),
1.282 + ("/zoo#1/cage#1/animal#1/property#3/name", "length"),
1.283 + ("/zoo#1/cage#1/animal#1/property#3/value", "5m"),
1.284 + ("/zoo#1/cage#1/animal#2/name", "Vincent"),
1.285 + ("/zoo#1/cage#1/animal#2/species#1/name", "Lizard"),
1.286 + ("/zoo#1/cage#1/animal#2/property#2/name", "colour"),
1.287 + ("/zoo#1/cage#1/animal#2/property#2/value", "variable"),
1.288 + ("/zoo#1/cage#1/animal#2/property#3/name", "length"),
1.289 + ("/zoo#1/cage#1/animal#2/property#3/value", "1m"),
1.290 + ("/zoo#1/cage#2/name", "mammals"),
1.291 + ("/zoo#1/cage#2/capacity", "25"),
1.292 + ("/zoo#1/cage#2/animal#1/name", "Simon"),
1.293 + ("/zoo#1/cage#2/animal#1/species#1/name", "Giraffe"),
1.294 + ("/zoo#1/cage#2/animal#2/name", "Leonard"),
1.295 + ("/zoo#1/cage#2/animal#2/species#1/name", "Lion"),
1.296 + ("/zoo#1/cage#2/animal#2/property#2/name", "danger"),
1.297 + ("/zoo#1/cage#2/animal#2/property#2/value", "high"),
1.298 + ("/zoo#1/funding#3/type", "private"),
1.299 + ("/zoo#1/funding#3/contributor#1/name", "Animal Corporation"),
1.300 + ("/zoo#1/funding#3/contributor#1/amount", "543210.987")
1.301 + ]
1.302 +
1.303 + import time
1.304 + import sys, cmdsyntax
1.305 +
1.306 + # Find the documents.
1.307 +
1.308 + syntax = cmdsyntax.Syntax("""
1.309 + --plain-output=OUTPUT_FILE
1.310 + --instance-name=NAME
1.311 + """)
1.312 +
1.313 + syntax_matches = syntax.get_args(sys.argv[1:])
1.314 +
1.315 + try:
1.316 + args = syntax_matches[0]
1.317 + except IndexError:
1.318 + print syntax.syntax
1.319 + sys.exit(1)
1.320 +
1.321 + # Create an object to interpret the test data.
1.322 +
1.323 + fields = Fields("iso-8859-1")
1.324 +
1.325 + t = time.time()
1.326 + documents = fields.make_documents(d)
1.327 + print "Building time", time.time() - t
1.328 +
1.329 + t = time.time()
1.330 + libxml2dom.toStream(documents[args["instance-name"]], stream=open(args["plain-output"], "wb"), encoding="utf-8")
1.331 + print "Prettyprinting time", time.time() - t
1.332 +
1.333 + print "Selectors", repr(fields.get_selectors(d, documents))
1.334 +
1.335 +# vim: tabstop=4 expandtab shiftwidth=4