10.5. XML processing with Swift

Swift [85] is a statically typed script programming language with a syntax inspired by functional languages such as Haskell and script like Python. In this section, we will show how to use it for both DOM and SAX parsing, which we used in XML processing in Java.

10.5.1. DOM parsing using Swift

To show how to process an existing XML structure, we will use the compacting process that we programmed in Java in Section 8.1. In Swift, DOM parsing is achieved simply by calling the XMLDocument constructor available as Foundation class. This creates a DOM structure composed of instances of the XMLNode class.

Example 10.29. [DOMCompact.swift] Text compaction of the cellar book (Example 2.2) with Swift using the DOM model

Compare this program with Example 8.1.

  1 import Foundation                                                    (1)
    
    func processDOM(_ string:String)->String{                            (2)
        var res=""
  5     
        func compact(_ nodeIn:XMLNode?,_ indent:String){                 (3)
            guard (nodeIn != nil) else {return}
            let node = nodeIn!
            var newIndent=indent
 10         switch node.kind {
            case .element:                                               (4)
                let name = node.name!
                let elem = node as! XMLElement
                res += name+"["
 15             newIndent += String(repeating:" ", count: name.count+1)
                var first = true
                if elem.attributes != nil {                              (5)
                    for attr in elem.attributes! {
                        if !first {res += "\n\(newIndent)"}
 20                     res += "@\(attr.name!)[\(attr.stringValue!)]"
                        first = false
                    }
                }
                if elem.children != nil {                                (6)
 25                 for child in elem.children! {
                        if !first {res += "\n\(newIndent)"}
                        compact(child, newIndent)                        (7)
                        first = false
                    }
 30             }
                res += "]"
            case .text:                                                  (8)
                res += node.stringValue!.trimmingCharacters(in:
                                             .whitespacesAndNewlines)
 35         default:
                print("compact:unprocessed kind:\(node.kind)")
            }
        }
        
 40     do {
            let doc = try XMLDocument(data: string.data(using: .utf16)!, (9)
                                      options: [])
            compact(doc.rootElement(),"")                                (10)
            return res+"\n"
 45     } catch {
            print("Error in the input file:\(error)")
            return ""
        }
    }
 50                                                                      (11)
    func processDOM(path:String)->String {
        return processDOM(try! String(contentsOf: URL(fileURLWithPath: path)))
    }
    

1

Imports the Foundation framework needed for XML processing.

2

Function that takes the XML input as a atring and returns the compacted form as a string, here the res variable.

3

A recursive function that processes an XML node in order to add to res indented lines corresponding to the input structure. The second parameter is a string with the current number of spaces for indentation of each new line.

4

If the node is an element, it adds to the the name of the node followed by an opening bracket and adds the appropriate number of spaces to the current indentation.

5

Deals with attributes which are contained in a list of nodes over which we iterate. For each attribute, prints a @, its name and the corresponding value within square brackets. A new line is started for each attribute except the first.

6

Loops over the list of children possibly changing line if it not the first child.

7

Recursively calls compact on the child node with the updated indentation.

8

Processes a text node by adding it to res after removing starting and endind spaces, tabs and newlines.

9

Parses the file by creating a new XML document from the content of the input string.

10

Calls the compacting process on the document node with an empty indentation string and ends the last line with a newline.

11

Transforms the XML file identified by a path string to produce a string containing the compacted form by calling the processDOM function on the string content of the file.


10.5.2. SAX parsing using Swift

In order to process an XML structure with the SAX approach, we will use a similar compacting process to the one we programmed in Java in Section 8.2. SAX parsing is achieved by creating an instance of the xmlParserDelegate class in which we define handler methods for some parsing events. The parsing process will send call-backs to the handler of these events, called a delegate in Swift, to create the compacted document as a string. SAX parsing in Swift is only a matter of defining the appropriate functions in the delegate. Because nodes are not normalized, many successive text nodes can appear within an element, so some care has to be taken to deal with this fact. Unfortunately, in Swift 5, internal entities such as the ones used at the start of the CellarBook.xml file cannot be properly dealt with.

Example 10.30. [SAXCompact.swift] Text compaction of the cellar book (Example 2.2) with Swift using the SAX model

Compare this program with Example 8.3.

  1 import Foundation                                                    (1)
    
    //  Caution: internal entities are not dealt properly with this API.
    func processSAX(string:String)->String {
  5     
        class xmlParserDelegate:NSObject, XMLParserDelegate {            (2)
            var res=""
            var closed=false
            var indent=""
 10         var lastNodeWasText=false
    
            func parser(_ parser: XMLParser,                             (3)
                        didStartElement elementName: String,
                        namespaceURI: String?, qualifiedName qName: String?,
 15                     attributes attributeDict: [String : String] = [:]){
                if closed {
                    res += "\n\(indent)"
                    closed = false
                }
 20             indent += String(repeating: " ", count:elementName.count+1) (4)
                res += "\(elementName)["
                var first = true
                for (name,val) in attributeDict {                        (5)
                    if !first {res += "\n\(indent)"}
 25                 res += "@\(name)[\(val)]"
                    first=false
                    closed=true
                }
                lastNodeWasText=false
 30         }
    
            func parser(_ parser: XMLParser,                             (6)
                        didEndElement elementName: String,
                        namespaceURI: String?, qualifiedName qName: String?){
 35             res += "]"
                closed=true
                indent=String(indent.dropLast(elementName.count+1))
                lastNodeWasText=false
            }
 40 
            func parser(_ parser: XMLParser,                             (7)
                        foundCharacters string: String){
                let s=string.trimmingCharacters(in: .whitespacesAndNewlines)
                if s.count>0 {
 45                 if closed && !lastNodeWasText{
                        res += "\n\(indent)"
                        closed = false
                    }
                    closed=true
 50                 res += "\(s)"
                }
                lastNodeWasText=true
            }
    
 55         public func parserDidEndDocument(_ parser: XMLParser){       (8)
                res += "\n"
            }
    
            func parser(_ parser: XMLParser,                             (9)
 60                     parseErrorOccurred parseError: Error){
                print("** parseErrorOccurred:\(parseError)")
            }
    
            func parser(_ parser: XMLParser,
 65                     validationErrorOccurred validationError: Error){
                print("** validationErrorOccurred:\(validationError)")
            }
        }
    
 70     let myDelegate=xmlParserDelegate()                               (10)
        let parser=XMLParser(data: string.data(using: .utf16)!)          (11)
        parser.delegate = myDelegate                                     (12)
        parser.parse()                                                   (13)
        return myDelegate.res
 75 }
    
    func processSAX(path:String)->String{                                (14)
        return processSAX(string:try! String(contentsOf: URL(fileURLWithPath: path)))
    }
 80 

1

Imports the Foundation framework for the XML processing.

2

Defines the constructor of the class and initializes instance variables needed to output the element with the correct indentation and for keeping track of the parsing state. The last flag is used when many successive text nodes appears.

3

When a new element is started, finishes the current indentation if needed.

4

Updates the current indentation by adding the length of the element name.

5

Prints the first attribute on the same line and the others on the subsequent lines properly indented.

6

When an element finishes, closes the current bracket and updates the current indentation.

7

For a non-empty text node, ends current line if needed and if the last node was not a text node. Writes the content of the node and indicates that the last node was a text node.

8

When the document parsing finishes, add a terminating newline.

9

Deals with a parsing error by printing an error message.

10

Creates an instance of the SAX parser delegate.

11

Creates an instance of the SAX parser.

12

Sets the parser delegate to which the parsing will send events.

13

Starts the parsing process and return the string containing the result of the compacting process.

14

Transforms the XML file identified by a path string to produce a string containing the compacted form by calling the processSax function on the string content of the file.


10.5.3. Creating an XML document using Swift

In order to demonstrate the creation of new XML document, we will parse the compact form produced in Section 10.5.1 or in Section 10.5.2 like we did in Chapter 9. We first need a way to access appropriate tokens corresponding to important signals in the input file. This is achieved by defining a CompactTokenizer class that will return opening and closing square brackets, at-signs and the rest as a single string. Newlines will also delimit tokens but will be ignored in the document processing. The file is processed by a series of calls to nextToken or skip. skip provides a useful redundancy check for tokens that are encountered in the input but are ignored for output.

In Swift, a new document is created using the constructor of the XMLDocument class. Adding a new node is done with the addChild method that adds the new node as the last child of a given node. The attributes of a node are added using the addAttribute method.

Example 10.31. [DOMExpand.swift] Swift compact form parsing to create an XML document

A sample input for this program is Example 5.8 , which should yield Example 2.2. Compare this program with Example 9.2.

  1 import Foundation                                                    (1)
    
    class CompactTokenizer {                                             (2)
        var tokens:[String]
  5     var iTok:Int
        
        init(string:String){                                             (3)
            let pattern = "(\\[|\\]|@|[^\\[\\]@\\n]+)"
            let regex = try! NSRegularExpression(pattern: pattern, options: [])
 10         let nsrange = NSRange(string.startIndex..<string.endIndex,in:string)
            iTok=0
            tokens = regex.matches(in: string, options: [],
                                   range: nsrange).map {match in
                    let matchedRange=Range(match.range(at:1),in:string)!
 15                 return string[matchedRange]
                             .trimmingCharacters(in:.whitespacesAndNewlines)
                }.filter{!$0.isEmpty}
        }
        
 20     func nextToken()->String {                                       (4)
            let token = tokens[iTok]
            iTok+=1
            return token
        }
 25     
        func skip(_ string:String){                                      (5)
            if nextToken() != string {
                print("\(string) expected at position \(iTok-1)")
            }
 30     }
        
        func isAtEnd()->Bool {                                           (6)
            return iTok>=tokens.count
        }
 35 }
    
    func expand(string:String) -> XMLNode {                              (7)
        let ct=CompactTokenizer(string: string)                          (8)
    
 40     func expandElem(_ elem:XMLElement,_ inTok:String){               (9)
            var tok = inTok
            while tok == "@" {                                           (10)
                let attName=ct.nextToken()
                ct.skip("[")
 45             elem.addAttribute(                                       (11)
                    XMLNode.attribute(withName: attName,
                                     stringValue: ct.nextToken()) as! XMLNode)
                ct.skip("]")
                tok=ct.nextToken()
 50         }
            while tok != "]" && !ct.isAtEnd() {                          (12)
                let s=tok
                let nextTok=ct.nextToken()
                if  nextTok == "[" {                                     (13)
 55                 let child = XMLNode.element(withName: s) as! XMLElement
                    expandElem(child,ct.nextToken())
                    elem.addChild(child)
                    tok = ct.nextToken()
                } else {                                                 (14)
 60                 elem.addChild(XMLNode.text(withStringValue: s) as! XMLNode)
                    tok = nextTok
                }
            }
        }
 65     
        var rootName=""
        var token=ct.nextToken()
        while token != "[" {
            rootName = token
 70         token = ct.nextToken()
        }
        let root = XMLElement(name: rootName)                            (15)
        let doc=XMLDocument(rootElement: root)                           (16)
        expandElem(root,ct.nextToken())
 75     return doc
    }
    
    func expand(path:String)->XMLNode {                                  (17)
        return expand(string: try! String(contentsOf: URL(fileURLWithPath: path)))
 80 }
    

1

Imports the Foundation framework for the XML processing.

2

Defines the CompactTokenizer class.

3

Defines the pattern regular expression to split an input line on an ampersand, an opening or closing square bracket. The square brackets characters in the regular expression must be preceded by a backslash as square brackets are part of the regular expression language, also used in the same expression. The list of all non-empty tokens is created by matching the regular expression on the whole input string.

4

Returns the next token.

5

Checks that the current token is the same as the one given as parameter and retrieves the next one. If the current token does not match, then print an error message.

6

Checks if there are still tokens left to process.

7

Creates an XML document corresponding to the content of the input string.

8

Creates an instance of the CompactTokenizer class that will be used to get the tokens.

9

Adds to an XML element the content starting with the current token inTok.

10

Because all attribute names start with an @, we loop while the current token is equal to @. The name of the attribute is saved and the following opening square bracket is skipped, the value is kept and the ] is skipped.

11

Adding an attribute is done by calling the addAttribute method having an XMLNode as parameter. The attribute is created by calling the XMLNode.attribute function with the name of the attribute and its corresponding value.

12

All children are then processed in turn until a closing square bracket is encountered. s is the current token, which is either a child element name (if followed by an opening square bracket) or the content of a text node.

13

A child node is expanded by a recursive call whose result is added as the last child of the current element.

14

A text node is added as the last child of the current element.

15

The name of the root is the first token immediately followed by an opening square bracket.

16

Create the document node by calling XMLDocument with an element that serves as its root.

17

Calls the expand function on the content of the file at the given path. The output of this function is an XMLNode that can be easily transformed into a prettyprinted string using the xmlString(options: .nodePrettyPrint) method.