1 package xml
2 3 import (
4 "encoding/xml"
5 "fmt"
6 "strings"
7 )
8 9 // NodeDecoder is a XML decoder wrapper that is responsible to decoding
10 // a single XML Node element and it's nested member elements. This wrapper decoder
11 // takes in the start element of the top level node being decoded.
12 type NodeDecoder struct {
13 Decoder *xml.Decoder
14 StartEl xml.StartElement
15 }
16 17 // WrapNodeDecoder returns an initialized XMLNodeDecoder
18 func WrapNodeDecoder(decoder *xml.Decoder, startEl xml.StartElement) NodeDecoder {
19 return NodeDecoder{
20 Decoder: decoder,
21 StartEl: startEl,
22 }
23 }
24 25 // Token on a Node Decoder returns a xml StartElement. It returns a boolean that indicates the
26 // a token is the node decoder's end node token; and an error which indicates any error
27 // that occurred while retrieving the start element
28 func (d NodeDecoder) Token() (t xml.StartElement, done bool, err error) {
29 for {
30 token, e := d.Decoder.Token()
31 if e != nil {
32 return t, done, e
33 }
34 35 // check if we reach end of the node being decoded
36 if el, ok := token.(xml.EndElement); ok {
37 return t, el == d.StartEl.End(), err
38 }
39 40 if t, ok := token.(xml.StartElement); ok {
41 return restoreAttrNamespaces(t), false, err
42 }
43 44 // skip token if it is a comment or preamble or empty space value due to indentation
45 // or if it's a value and is not expected
46 }
47 }
48 49 // restoreAttrNamespaces update XML attributes to restore the short namespaces found within
50 // the raw XML document.
51 func restoreAttrNamespaces(node xml.StartElement) xml.StartElement {
52 if len(node.Attr) == 0 {
53 return node
54 }
55 56 // Generate a mapping of XML namespace values to their short names.
57 ns := map[string]string{}
58 for _, a := range node.Attr {
59 if a.Name.Space == "xmlns" {
60 ns[a.Value] = a.Name.Local
61 break
62 }
63 }
64 65 for i, a := range node.Attr {
66 if a.Name.Space == "xmlns" {
67 continue
68 }
69 // By default, xml.Decoder will fully resolve these namespaces. So if you had <foo xmlns:bar=baz bar:bin=hi/>
70 // then by default the second attribute would have the `Name.Space` resolved to `baz`. But we need it to
71 // continue to resolve as `bar` so we can easily identify it later on.
72 if v, ok := ns[node.Attr[i].Name.Space]; ok {
73 node.Attr[i].Name.Space = v
74 }
75 }
76 return node
77 }
78 79 // GetElement looks for the given tag name at the current level, and returns the element if found, and
80 // skipping over non-matching elements. Returns an error if the node is not found, or if an error occurs while walking
81 // the document.
82 func (d NodeDecoder) GetElement(name string) (t xml.StartElement, err error) {
83 for {
84 token, done, err := d.Token()
85 if err != nil {
86 return t, err
87 }
88 if done {
89 return t, fmt.Errorf("%s node not found", name)
90 }
91 switch {
92 case strings.EqualFold(name, token.Name.Local):
93 return token, nil
94 default:
95 err = d.Decoder.Skip()
96 if err != nil {
97 return t, err
98 }
99 }
100 }
101 }
102 103 // Value provides an abstraction to retrieve char data value within an xml element.
104 // The method will return an error if it encounters a nested xml element instead of char data.
105 // This method should only be used to retrieve simple type or blob shape values as []byte.
106 func (d NodeDecoder) Value() (c []byte, err error) {
107 t, e := d.Decoder.Token()
108 if e != nil {
109 return c, e
110 }
111 112 endElement := d.StartEl.End()
113 114 switch ev := t.(type) {
115 case xml.CharData:
116 c = ev.Copy()
117 case xml.EndElement: // end tag or self-closing
118 if ev == endElement {
119 return []byte{}, err
120 }
121 return c, fmt.Errorf("expected value for %v element, got %T type %v instead", d.StartEl.Name.Local, t, t)
122 default:
123 return c, fmt.Errorf("expected value for %v element, got %T type %v instead", d.StartEl.Name.Local, t, t)
124 }
125 126 t, e = d.Decoder.Token()
127 if e != nil {
128 return c, e
129 }
130 131 if ev, ok := t.(xml.EndElement); ok {
132 if ev == endElement {
133 return c, err
134 }
135 }
136 137 return c, fmt.Errorf("expected end element %v, got %T type %v instead", endElement, t, t)
138 }
139 140 // FetchRootElement takes in a decoder and returns the first start element within the xml body.
141 // This function is useful in fetching the start element of an XML response and ignore the
142 // comments and preamble
143 func FetchRootElement(decoder *xml.Decoder) (startElement xml.StartElement, err error) {
144 for {
145 t, e := decoder.Token()
146 if e != nil {
147 return startElement, e
148 }
149 150 if startElement, ok := t.(xml.StartElement); ok {
151 return startElement, err
152 }
153 }
154 }
155