############################################################################## # # Zope Public License (ZPL) Version 1.0 # ------------------------------------- # # Copyright (c) Digital Creations. All rights reserved. # # This license has been certified as Open Source(tm). # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # 1. Redistributions in source code must retain the above copyright # notice, this list of conditions, and the following disclaimer. # # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions, and the following disclaimer in # the documentation and/or other materials provided with the # distribution. # # 3. Digital Creations requests that attribution be given to Zope # in any manner possible. Zope includes a "Powered by Zope" # button that is installed by default. While it is not a license # violation to remove this button, it is requested that the # attribution remain. A significant investment has been put # into Zope, and this effort will continue if the Zope community # continues to grow. This is one way to assure that growth. # # 4. All advertising materials and documentation mentioning # features derived from or use of this software must display # the following acknowledgement: # # "This product includes software developed by Digital Creations # for use in the Z Object Publishing Environment # (http://www.zope.org/)." # # In the event that the product being advertised includes an # intact Zope distribution (with copyright and license included) # then this clause is waived. # # 5. Names associated with Zope or Digital Creations must not be used to # endorse or promote products derived from this software without # prior written permission from Digital Creations. # # 6. Modified redistributions of any form whatsoever must retain # the following acknowledgment: # # "This product includes software developed by Digital Creations # for use in the Z Object Publishing Environment # (http://www.zope.org/)." # # Intact (re-)distributions of any official Zope release do not # require an external acknowledgement. # # 7. Modifications are encouraged but must be packaged separately as # patches to official Zope releases. Distributions that do not # clearly separate the patches from the original work must be clearly # labeled as unofficial distributions. Modifications which do not # carry the name Zope may be packaged in any form, as long as they # conform to all of the clauses above. # # # Disclaimer # # THIS SOFTWARE IS PROVIDED BY DIGITAL CREATIONS ``AS IS'' AND ANY # EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL DIGITAL CREATIONS OR ITS # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF # USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT # OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF # SUCH DAMAGE. # # # This software consists of contributions made by Digital Creations and # many individuals on behalf of Digital Creations. Specific # attributions are listed in the accompanying credits file. # ############################################################################## """ Zopeish DOMish XML Node classes. """ import Acquisition from OFS import ZDOM from OFS.ZDOM import ELEMENT_NODE, TEXT_NODE, DOCUMENT_NODE, \ PROCESSING_INSTRUCTION_NODE, COMMENT_NODE, CDATA_SECTION_NODE, \ DOCUMENT_FRAGMENT_NODE from OFS.PropertyManager import PropertyManager from AccessControl.Role import RoleManager from OFS.SimpleItem import Item from Globals import Persistent import string import copy # Node classes # ------------ class Node: """ DOM Node base class Mix-in with ZDOM classes """ # _child_map maps child ids to child element objects # _child_ids is a list of child element object ids in order # id is the Node id # _name is the Node name # note: _child_map and _child_ids might be replaced with # higher performance map and set objects in Nodes with # large numbers of children. also methods that return # lists of nodes might be done with some kind of lazy # iteration, rather than computing the whole big list # at once. isXMLNode=1 def __init__(self, name, attributes=None): self._name=name self._child_ids=[] self._child_map={} # private methods # --------------- def _make_id(self, parent=None): # create a unique id for this node if parent is None: id=0 else: while 1: if hasattr(parent,'aq_parent') \ and hasattr(parent.aq_parent, 'isXMLNode') \ and parent.aq_parent.isXMLNode: parent=parent.aq_parent else: break if hasattr(parent, '_next_id'): id=parent._next_id parent._next_id=id + 1 else: id=0 parent._next_id=1 self._id=id self.id=self._index2id(id) def _id2index(self,id): # convert Node id to index in _child_map return string.atoi(id[1:]) def _index2id(self,index): # convert id in _child_map to Node id return 'e%d' % index def _clear(self): # used by builder to clear the node of children and attributes self._clear_attributes() self._child_ids=[] self._child_map.clear() def _clear_attributes(self): pass # sequence interface to children by index # mapping interface to childen by id # --------------------------------------- # TODO add more of the sequence interface, def __getitem__(self, key): if not hasattr(self, '_child_map'): raise ZDOM.HierarchyRequestException('Node type does not support children.') try: key=string.atoi(key) except: pass if type(key)==type(1): try: return self._child_map[self._child_ids[key]].__of__(self) except: raise IndexError('index out of range') try: key=self._id2index(key) return self._child_map[key].__of__(self) except: raise KeyError(key) def __setitem__(self, key, node): oldNode=self.__getitem__(self,key) self.replaceChild(node, oldNode) def __delitem__(self, key): oldNode=self.__getitem__(key) self.removeChild(oldNode) def __getslice__(self, i, j): if not hasattr(self, '_child_map'): raise ZDOM.HierarchyRequestException('Node type does not support children.') result=[] for id in self._child_ids[i:j]: result.append(self._child_map[id].__of__(self)) return result def append(self, node): self.appendChild(node) def insert(self, i, node): self.insertBefore(node, self[i]) def __len__(self): return len(self._child_ids) # DOM interface # ------------- nodeType=None def getNodeType(self): "The type code of this node." return self.nodeType def getNodeName(self): "The name of this node." return self._name def getParentNode(self): """ The parent of this node, or None if it doesn't exist. """ try: if self.aq_parent.isXMLNode: return self.aq_parent except AttributeError: return None def getPreviousSibling(self): """ The node immediately preceding this node. If there is no such node, this returns None. """ try: i=self.aq_parent._child_ids.index(self._id) if i-1 <= 0: return None return self.aq_parent[i-1] except: return None def getNextSibling(self): """ The node immediately following this node. If there is no such node, this returns None. """ try: i=self.aq_parent._child_ids.index(self._id) if i+1 >= len(self.aq_parent): return None return self.aq_parent[i+1] except: return None def getOwnerDocument(self): """ The Document object associated with this node. If there is no such node, this returns None. """ node=self while 1: if node.getNodeType() == DOCUMENT_NODE: return node elif hasattr(node,'aq_parent'): node=node.aq_parent else: return None # DOM write methods # ----------------- def normalize(self): """ Consolidate child Text and CDATA nodes. """ last=None remove_ids=[] for child in self: if child.getNodeType() == TEXT_NODE and last is not None \ and last.getNodeType() == TEXT_NODE: last._data=last._data + child._data remove_ids.append(child._id) elif child.getNodeType() == CDATA_SECTION_NODE and last is not None \ and last.getNodeType() == CDATA_SECTION_NODE: last._data=last._data + child._data remove_ids.append(child._id) else: last=child if hasattr(last, '_child_map'): last.normalize() cids=self._child_ids cmap=self._child_map for id in remove_ids: cids.remove(id) del cmap[id] self._child_ids=cids self._child_map=cmap def insertBefore(self, newChild, refChild=None): """ Inserts the node newChild before the existing child node refChild. If refChild is None, insert newChild at the end of the list of children. If newChild is a DocumentFragment object, all of its children are inserted, in the same order, before refChild. If the newChild is already in the tree, it is first removed. Returns the inserted node. """ if not hasattr(self, '_child_map'): raise ZDOM.HierarchyRequestException('Node type does not support children.') if refChild is None: return self.appendChild(newChild) document=newChild.getOwnerDocument() if document and document != self.getOwnerDocument(): raise ZDOM.WrongDocumentException() try: i=self._child_ids.index(refChild._id) except IndexError: raise ZDOM.NotFoundException(refChild) if newChild.getNodeType() == DOCUMENT_FRAGMENT_NODE: newChildren=newChild else: newChildren=[newChild] cids=self._child_ids cmap=self._child_map for child in newChildren: if child.getParentNode(): child.getParentNode().removeChild(child) child=child.aq_base child._make_id(self) cids.insert(i, child._id) cmap[newChild._id]=child i = i + 1 self._child_ids=cids self._child_map=cmap if newChild.getNodeType() != DOCUMENT_FRAGMENT_NODE: return self[newChild.id] else: return newChild def replaceChild(self, newChild, oldChild): """ Replaces the child node oldChild with newChild in the list of children, and returns the oldChild node. If the newChild is already in the tree, it is first removed. Returns the replaced node. """ if not hasattr(self, '_child_map'): raise ZDOM.HierarchyRequestException('Node type does not support children.') document=newChild.getOwnerDocument() if document and document != self.getOwnerDocument(): raise ZDOM.WrongDocumentException() try: i=self._child_ids.index(oldChild._id) except IndexError: raise ZDOM.NotFoundException(oldChild) if newChild.getNodeType() == DOCUMENT_FRAGMENT_NODE: newChildren=newChild else: newChildren=[newChild] cids=self._child_ids cmap=self._child_map for child in newChildren: if child.getParentNode(): child.getParentNode().removeChild(child) child=child.aq_base child._make_id(self) cids[i]=child._id cmap[child._id]=child i = i + 1 del cmap[oldChild._id] self._child_ids=cids self._child_map=cmap return oldChild.aq_base def removeChild(self, oldChild): """ Removes the child node indicated by oldChild from the list of children, and returns it. """ if not hasattr(self, '_child_map'): raise ZDOM.HierarchyRequestException('Node type does not support children.') if not oldChild._id in self._child_ids: raise ZDOM.NotFoundException(oldChild) cids=self._child_ids cmap=self._child_map cids.remove(oldChild._id) del cmap[oldChild._id] self._child_ids=cids self._child_map=cmap return oldChild.aq_base def appendChild(self, newChild): """ Adds the node newChild to the end of the list of children of this node. If the newChild is already in the tree, it is first removed. If the appened node is a DocumentFragment object, the entire contents of the document fragment are moved into the child list of this node Returns the node added. """ if not hasattr(self, '_child_map'): raise ZDOM.HierarchyRequestException('Node type does not support children.') document=newChild.getOwnerDocument() if document and document != self.getOwnerDocument(): raise ZDOM.WrongDocumentException() if newChild.getNodeType() == DOCUMENT_FRAGMENT_NODE: newChildren=newChild else: newChildren=[newChild] cids=self._child_ids cmap=self._child_map for child in newChildren: if child.getParentNode(): child.getParentNode().removeChild(child) child=child.aq_base child._make_id(self) cids.append(child._id) cmap[child._id]=child self._child_ids=cids self._child_map=cmap if newChild.getNodeType() != DOCUMENT_FRAGMENT_NODE: return self[newChild.id] else: return newChild def cloneNode(self, deep=0): """ Returns a duplicate of this node, i.e., serves as a generic copy constructor for nodes. The duplicate node has no parent. Cloning an Element copies all attributes and their values, including those generated by the XML processor to represent defaulted attributes, but this method does not copy any text it contains unless it is a deep clone, since the text is contained in a child Text node. Cloning any other type of node simply returns a copy of this node. """ return self.aq_acquire('_build_tree')(self.toXML(deep)) # object manager methods # ---------------------- def _objectIds(self, spec=None): # objectIds in native format if spec is not None: if type(spec)==type('s'): spec=[spec] return filter(lambda i, l=spec, m=self._child_map: m[i]._name in l, self._child_ids) return self._child_ids def objectIds(self, spec=None): """ Returns a list of sub-Element ids. """ return map(lambda i, f=self._index2id: f(i), self._objectIds(spec)) def objectValues(self, spec=None): """ Returns a list of sub-Elements. """ if not hasattr(self, '_child_map'): return [] return map(lambda i, s=self, m=self._child_map: m[i].__of__(s), self._objectIds(spec)) def objectItems(self, spec=None): """ Returns a list of tuples (id, Element) of sub-Elements. """ if not hasattr(self, '_child_map'): return [] return map(lambda i, s=self, f=self._index2id, m=self._child_map: (f(i), m[i].__of__(s)), self._objectIds(spec)) # misc Zope methods # ----------------- def meta_type(self): # meta type is the node name return self.getNodeName() def tpValues(self): """ Returns children of type Element. """ return filter(lambda x: x.getNodeType()==ELEMENT_NODE, self.objectValues()) def tpURL(self): """ URL for tree navigation. """ return self.id def text_content(self, name=None): """ Concatination of all child Text nodes. Generally this only makes sense to call on leaf nodes. If 'name' is not none then this returns a concatination of the text content of all sub-Elements with name 'name'. """ result="" if name is None: for child in self: if child.getNodeType() in (TEXT_NODE, CDATA_SECTION_NODE): result=result + child.getNodeValue() else: for child in self: if child.getNodeName()==name: result=result + child.text_content() return result def toXML(self, deep=1, RESPONSE=None): """ DOM tree as XML from this Node. If 'deep' is false, just return the XML of this node and its attributes. """ if RESPONSE is not None: RESPONSE.setHeader('content-type','text/xml') result=[] result.append('<%s' % self._name) for a in self.getAttributes(): if a.getNodeValue(): result.append(' %s="%s"' % (a.getName(),a.getNodeValue())) if deep and self._child_ids: result.append('>') for child in self: ghost=hasattr(child, '_p_changed') and child._p_changed==None result.append(child.toXML()) if ghost: child._p_deactivate() result.append('' % self._name) else: result.append('/>') return string.join(result,'') document_src=toXML class CharacterData: """ Character Data mix-in class """ # DOM attributes # -------------- def getData(self): """ The character data of this node. """ return self._data def getLength(self): """ The length character data of this node. """ return len(self._data) # DOM Methods # ----------- def substringData(self,offset,count): """ Extracts a range of data from the node. """ return self._data[offset:offset+count] def appendData(self, arg): """ Append the string to the end of the character data of the node. Upon success, data provides access to the concatenation of data and the string specified. """ self._data=self._data + arg def insertData(self, offset, arg): """ Insert a string at the specified character offset. """ if offset < 0 or offset > len(self._data): raise ZDOM.IndexSizeException() self._data=self._data[:offset] + arg + self._data[:offset] def deleteData(self, offset, count): """ Remove a range of characters from the node. Upon success, data and length reflect the change. """ if offset < 0 or offset > len(self._data) or count < 0: raise ZDOM.IndexSizeException() self._data=self._data[:offset] + self._data[offset + count:] def replaceData(self, offset, count, arg): """ Replace the characters starting at the specified character offset with the specified string. """ if offset < 0 or offset > len(self._data) or count < 0: raise ZDOM.IndexSizeException() self._data=self._data[:offset] + arg + self._data[offset + count:] class Text(Acquisition.Explicit, CharacterData, Node, Item, Persistent): """ Text Node """ nodeType=TEXT_NODE _name="#text" _child_ids=() def __init__(self, data=""): self._data=data def toXML(self,deep=1,RESPONSE=None): """ DOM tree as XML from this Node. If 'deep' is false, just return the XML of this node and its attributes. """ if RESPONSE is not None: RESPONSE.setHeader('content-type','text/xml') return self._data # DOM Methods # ----------- getNodeValue=CharacterData.getData def splitText(self, offset): """ Breaks this Text node into two Text nodes at the specified offset, keeping both in the tree as siblings. This node then only contains all the content up to the offset point. And a new Text node, which is inserted as the next sibling of this node, contains all the content at and after the offset point. """ if offset < 0 or offset > len(self._data) or count < 0: raise ZDOM.IndexSizeException() data, self._data = self._data[:offset], self._data[offset:] node=self.getOwnerDocument().createTextNode(data) self.getParent().insertBefore(self.nextSibling(), node) def cloneNode(self, deep=0): """ Returns a duplicate of this node, i.e., serves as a generic copy constructor for nodes. The duplicate node has no parent. Cloning an Element copies all attributes and their values, including those generated by the XML processor to represent defaulted attributes, but this method does not copy any text it contains unless it is a deep clone, since the text is contained in a child Text node. Cloning any other type of node simply returns a copy of this node. """ return self.getOwnerDocument().createTextNode(self._data) class Element(Acquisition.Implicit, Node, ZDOM.ElementWithAttributes, Item, Persistent, RoleManager, PropertyManager): """ Element Node """ def __init__(self, name, attributes=None): self._name=name self._child_ids=[] self._child_map={} if attributes is not None: for k, v in attributes.items(): self.setAttribute(k,v) def _clear_attributes(self): for id in self.propertyIds(): self._delProperty(id) # DOM Methods # ----------- nodeType=ELEMENT_NODE def getTagName(self): return self._name # DOM write methods # ----------------- def setAttribute(self, name, value): """ Adds a new attribute. If an attribute with that name is already present in the element, its value is changed to be that of the value parameter. This value is a simple string, it is not parsed as it is being set. So any markup (such as syntax to be recognized as an entity reference) is treated as literal text, and needs to be appropriately escaped by the implementation when it is written out. In order to assign an attribute value that contains entity references, the user must create an Attr node plus any Text and EntityReference nodes, build the appropriate subtree, and use setAttributeNode to assign it as the value of an attribute. """ try: self._updateProperty(name, value) except: try: self._setProperty(name, value) except 'BadRequest': raise ZDOM.InvalidCharacterException() def removeAttribute(self, name): """ Removes an attribute by name. If the removed attribute has a default value it is immediately replaced. """ try: self._delProperty(name) except: pass def setAttributeNode(self, newAttr): """ Adds a new attribute. If an attribute with that name is already present in the element, it is replaced by the new one. """ if hasattr(newAttr,'aq_parent'): raise ZDOM.InUseAttributeException() old_attr=self.getAttributeNode(newAttr.getName()) if old_attr is not None: self._updateProperty(newAttr.getName(), newAttr.getValue()) return old_attr self._setProperty(newAttr.getName(), newAttr.getValue()) def removeAttributeNode(self, oldAttr): """ Removes the specified attribute. """ if not hasattr(oldAttr, 'aq_parent') and oldAttr.aq_parent is self: raise ZDOM.NotFoundException() self._delProperty(oldAttr.getName()) return oldAttr # Object Manager-ish extensions # ----------------------------- # Hmm. Are these worth it? def nextObject(self, spec=None): """ Returns the next sibling element of type spec. """ e=self if type(spec)==type(''): spec=[spec] while 1: e=e.getNextSibling() if e is None: return None if spec is None or e._name in spec: return e def previousObject(self, spec=None): """ Returns the previous sibling element of type spec. """ e=self if type(spec)==type(''): spec=[spec] while 1: e=e.getPreviousSibling() if e is None: return None if spec is None or e._name in spec: return e class Document(Acquisition.Implicit, Node, ZDOM.ElementWithAttributes, Item, Persistent, RoleManager, PropertyManager): """ Document Node """ nodeType=DOCUMENT_NODE docType=None _name="#document" def __init__(self, attributes=None): self._child_map={} self._child_ids=[] self._next_id=0 def toXML(self,deep=1,RESPONSE=None): """ DOM tree as XML from this Node. If 'deep' is false, just return the XML of this node and its attributes. """ if RESPONSE is not None: RESPONSE.setHeader('content-type','text/xml') result = '\n' if deep: for child in self: try: s=object._p_changed except: s=0 result=result + child.toXML() if s is None: child._p_deactivate() return result # DOM Methods # ----------- def getDocType(self): """ The Document Type Declaration associated with this document. """ return self.docType def getImplementation(self): """ The DOMImplementation object that handles this document. """ return ZDOM.DOMImplementation() def getDocumentElement(self): """ The child node that is the root element of the document. """ for child in self: if child.getNodeType() == ELEMENT_NODE: return child class CDATASection(Text): """ CDATA Section Node """ nodeType=CDATA_SECTION_NODE _name="#cdata-section" _child_ids=() def toXML(self, deep=1, RESPONSE=None): """ DOM tree as XML from this Node. If 'deep' is false, just return the XML of this node and its attributes. """ if RESPONSE is not None: RESPONSE.setHeader('content-type','text/xml') return "" % (self._data) def cloneNode(self, deep=0): """ Returns a duplicate of this node, i.e., serves as a generic copy constructor for nodes. The duplicate node has no parent. Cloning an Element copies all attributes and their values, including those generated by the XML processor to represent defaulted attributes, but this method does not copy any text it contains unless it is a deep clone, since the text is contained in a child Text node. Cloning any other type of node simply returns a copy of this node. """ return self.getOwnerDocument().createCDATASection(self._data) class ProcessingInstruction(Acquisition.Explicit, Node, ZDOM.Node, Persistent): """ Processing Instruction Node """ nodeType=PROCESSING_INSTRUCTION_NODE _child_ids=() def __init__(self, target, data): self._target=target self._name=target self._data=data def toXML(self, deep=1, RESPONSE=None): """ DOM tree as XML from this Node. If 'deep' is false, just return the XML of this node and its attributes. """ if RESPONSE is not None: RESPONSE.setHeader('content-type','text/xml') return "" % (self._target, self._data) def cloneNode(self, deep=0): """ Returns a duplicate of this node, i.e., serves as a generic copy constructor for nodes. The duplicate node has no parent. Cloning an Element copies all attributes and their values, including those generated by the XML processor to represent defaulted attributes, but this method does not copy any text it contains unless it is a deep clone, since the text is contained in a child Text node. Cloning any other type of node simply returns a copy of this node. """ return self.getOwnerDocument().createProcessingInstruction(self._data) class Comment(Acquisition.Explicit, CharacterData, Node, ZDOM.Node, Persistent): """ Comment Node """ nodeType=COMMENT_NODE _name="#comment" _child_ids=() def __init__(self, data): self._data=data def toXML(self, deep=1, RESPONSE=None): """ DOM tree as XML from this Node. If 'deep' is false, just return the XML of this node and its attributes. """ if RESPONSE is not None: RESPONSE.setHeader('content-type','text/xml') return "" % self._data def cloneNode(self, deep=0): """ Returns a duplicate of this node, i.e., serves as a generic copy constructor for nodes. The duplicate node has no parent. Cloning an Element copies all attributes and their values, including those generated by the XML processor to represent defaulted attributes, but this method does not copy any text it contains unless it is a deep clone, since the text is contained in a child Text node. Cloning any other type of node simply returns a copy of this node. """ return self.getOwnerDocument().createComment(self._data) class DocumentFragment(Acquisition.Implicit, Node, ZDOM.Node, Persistent): """ Document Fragment """ nodeType=DOCUMENT_FRAGMENT_NODE _name="#document-fragment" def __init__(self): self._child_ids=[] self._child_map={}