aboutsummaryrefslogtreecommitdiffstats
path: root/vendor/github.com/aws/smithy-go/testing/xml/xmlToStruct.go
blob: 1bfc7ba98cb4443649fca95c26c62c04abfbcd24 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
package xml

import (
	"encoding/xml"
	"fmt"
	"io"
	"sort"
	"strings"
)

// A XMLNode contains the values to be encoded or decoded.
type XMLNode struct {
	Name     xml.Name              `json:",omitempty"`
	Children map[string][]*XMLNode `json:",omitempty"`
	Text     string                `json:",omitempty"`
	Attr     []xml.Attr            `json:",omitempty"`

	namespaces map[string]string
	parent     *XMLNode
}

// NewXMLElement returns a pointer to a new XMLNode initialized to default values.
func NewXMLElement(name xml.Name) *XMLNode {
	return &XMLNode{
		Name:     name,
		Children: map[string][]*XMLNode{},
		Attr:     []xml.Attr{},
	}
}

// AddChild adds child to the XMLNode.
func (n *XMLNode) AddChild(child *XMLNode) {
	child.parent = n
	if _, ok := n.Children[child.Name.Local]; !ok {
		// flattened will have multiple children with same tag name
		n.Children[child.Name.Local] = []*XMLNode{}
	}
	n.Children[child.Name.Local] = append(n.Children[child.Name.Local], child)
}

// XMLToStruct converts a xml.Decoder stream to XMLNode with nested values.
func XMLToStruct(d *xml.Decoder, s *xml.StartElement, ignoreIndentation bool) (*XMLNode, error) {
	out := &XMLNode{}

	for {
		tok, err := d.Token()
		if err != nil {
			if err == io.EOF {
				break
			} else {
				return out, err
			}
		}

		if tok == nil {
			break
		}

		switch typed := tok.(type) {
		case xml.CharData:
			text := string(typed.Copy())
			if ignoreIndentation {
				text = strings.TrimSpace(text)
			}
			if len(text) != 0 {
				out.Text = text
			}
		case xml.StartElement:
			el := typed.Copy()
			out.Attr = el.Attr
			if out.Children == nil {
				out.Children = map[string][]*XMLNode{}
			}

			name := typed.Name.Local
			slice := out.Children[name]
			if slice == nil {
				slice = []*XMLNode{}
			}
			node, e := XMLToStruct(d, &el, ignoreIndentation)
			out.findNamespaces()
			if e != nil {
				return out, e
			}

			node.Name = typed.Name
			node.findNamespaces()

			// Add attributes onto the node
			node.Attr = el.Attr

			tempOut := *out
			// Save into a temp variable, simply because out gets squashed during
			// loop iterations
			node.parent = &tempOut
			slice = append(slice, node)
			out.Children[name] = slice
		case xml.EndElement:
			if s != nil && s.Name.Local == typed.Name.Local { // matching end token
				return out, nil
			}
			out = &XMLNode{}
		}
	}
	return out, nil
}

func (n *XMLNode) findNamespaces() {
	ns := map[string]string{}
	for _, a := range n.Attr {
		if a.Name.Space == "xmlns" {
			ns[a.Value] = a.Name.Local
		}
	}

	n.namespaces = ns
}

func (n *XMLNode) findElem(name string) (string, bool) {
	for node := n; node != nil; node = node.parent {
		for _, a := range node.Attr {
			namespace := a.Name.Space
			if v, ok := node.namespaces[namespace]; ok {
				namespace = v
			}
			if name == fmt.Sprintf("%s:%s", namespace, a.Name.Local) {
				return a.Value, true
			}
		}
	}
	return "", false
}

// StructToXML writes an XMLNode to a xml.Encoder as tokens.
func StructToXML(e *xml.Encoder, node *XMLNode, sorted bool) error {
	var err error
	// Sort Attributes
	attrs := node.Attr
	if sorted {
		sortedAttrs := make([]xml.Attr, len(attrs))
		for _, k := range node.Attr {
			sortedAttrs = append(sortedAttrs, k)
		}
		sort.Sort(xmlAttrSlice(sortedAttrs))
		attrs = sortedAttrs
	}

	st := xml.StartElement{Name: node.Name, Attr: attrs}
	e.EncodeToken(st)
	// return fmt.Errorf("encoder string : %s, %s, %s", node.Name.Local, node.Name.Space, st.Attr)

	if node.Text != "" {
		e.EncodeToken(xml.CharData([]byte(node.Text)))
	} else if sorted {
		sortedNames := []string{}
		for k := range node.Children {
			sortedNames = append(sortedNames, k)
		}
		sort.Strings(sortedNames)

		for _, k := range sortedNames {
			// we should sort the []*xml.Node for each key if len >1
			flattenedNodes := node.Children[k]
			// Meaning this has multiple nodes
			if len(flattenedNodes) > 1 {
				// sort flattened nodes
				flattenedNodes, err = sortFlattenedNodes(flattenedNodes)
				if err != nil {
					return err
				}
			}

			for _, v := range flattenedNodes {
				err = StructToXML(e, v, sorted)
				if err != nil {
					return err
				}
			}
		}
	} else {
		for _, c := range node.Children {
			for _, v := range c {
				err = StructToXML(e, v, sorted)
				if err != nil {
					return err
				}
			}
		}
	}

	e.EncodeToken(xml.EndElement{Name: node.Name})
	return e.Flush()
}

// sortFlattenedNodes sorts nodes with nodes having same element tag
// but overall different values. The function will return list of pointer to
// XMLNode and an error.
//
// Overall sort order is followed is:
// Nodes with concrete value (no nested node as value) are given precedence
// and are added to list after sorting them
//
// Next nested nodes within a flattened list are given precedence.
//
// Next nodes within a flattened map are sorted based on either key or value
// which ever has lower value and then added to the global sorted list.
// If value was initially chosen, but has nested nodes; key will be chosen as comparable
// as it is unique and will always have concrete data ie. string.
func sortFlattenedNodes(nodes []*XMLNode) ([]*XMLNode, error) {
	var sortedNodes []*XMLNode

	// concreteNodeMap stores concrete value associated with a list of nodes
	// This is possible in case multiple members of a flatList has same values.
	concreteNodeMap := make(map[string][]*XMLNode, 0)

	// flatListNodeMap stores flat list or wrapped list members associated with a list of nodes
	// This will have only flattened list with members that are Nodes and not concrete values.
	flatListNodeMap := make(map[string][]*XMLNode, 0)

	// flatMapNodeMap stores flat map or map entry members associated with a list of nodes
	// This will have only flattened map concrete value members. It is possible to limit this
	// to concrete value as map key is expected to be concrete.
	flatMapNodeMap := make(map[string][]*XMLNode, 0)

	// nodes with concrete value are prioritized and appended based on sorting order
	sortedNodesWithConcreteValue := []string{}

	// list with nested nodes are second in priority and appended based on sorting order
	sortedNodesWithListValue := []string{}

	// map are last in priority and appended based on sorting order
	sortedNodesWithMapValue := []string{}

	for _, node := range nodes {
		// node has no children element, then we consider it as having concrete value
		if len(node.Children) == 0 {
			sortedNodesWithConcreteValue = append(sortedNodesWithConcreteValue, node.Text)
			if v, ok := concreteNodeMap[node.Text]; ok {
				concreteNodeMap[node.Text] = append(v, node)
			} else {
				concreteNodeMap[node.Text] = []*XMLNode{node}
			}
		}

		// if node has a single child, then it is a flattened list node
		if len(node.Children) == 1 {
			for _, nestedNodes := range node.Children {
				nestedNodeName := nestedNodes[0].Name.Local

				// append to sorted node name for list value
				sortedNodesWithListValue = append(sortedNodesWithListValue, nestedNodeName)

				if v, ok := flatListNodeMap[nestedNodeName]; ok {
					flatListNodeMap[nestedNodeName] = append(v, nestedNodes[0])
				} else {
					flatListNodeMap[nestedNodeName] = []*XMLNode{nestedNodes[0]}
				}
			}
		}

		// if node has two children, then it is a flattened map node
		if len(node.Children) == 2 {
			nestedPair := []*XMLNode{}
			for _, k := range node.Children {
				nestedPair = append(nestedPair, k[0])
			}

			comparableValues := []string{nestedPair[0].Name.Local, nestedPair[1].Name.Local}
			sort.Strings(comparableValues)

			comparableValue := comparableValues[0]
			for _, nestedNode := range nestedPair {
				if comparableValue == nestedNode.Name.Local && len(nestedNode.Children) != 0 {
					// if value was selected and is nested node, skip it and use key instead
					comparableValue = comparableValues[1]
					continue
				}

				// now we are certain there is no nested node
				if comparableValue == nestedNode.Name.Local {
					// get chardata for comparison
					comparableValue = nestedNode.Text
					sortedNodesWithMapValue = append(sortedNodesWithMapValue, comparableValue)

					if v, ok := flatMapNodeMap[comparableValue]; ok {
						flatMapNodeMap[comparableValue] = append(v, node)
					} else {
						flatMapNodeMap[comparableValue] = []*XMLNode{node}
					}
					break
				}
			}
		}

		// we don't support multiple same name nodes in an xml doc except for in flattened maps, list.
		if len(node.Children) > 2 {
			return nodes, fmt.Errorf("malformed xml: multiple nodes with same key name exist, " +
				"but are not associated with flattened maps (2 children) or list (0 or 1 child)")
		}
	}

	// sort concrete value node name list and append corresponding nodes
	// to sortedNodes
	sort.Strings(sortedNodesWithConcreteValue)
	for _, name := range sortedNodesWithConcreteValue {
		for _, node := range concreteNodeMap[name] {
			sortedNodes = append(sortedNodes, node)
		}
	}

	// sort nested nodes with a list and append corresponding nodes
	// to sortedNodes
	sort.Strings(sortedNodesWithListValue)
	for _, name := range sortedNodesWithListValue {
		// if two nested nodes have same name, then sort them separately.
		if len(flatListNodeMap[name]) > 1 {
			// return nodes, fmt.Errorf("flat list node name are %s %v", flatListNodeMap[name][0].Name.Local, len(flatListNodeMap[name]))
			nestedFlattenedList, err := sortFlattenedNodes(flatListNodeMap[name])
			if err != nil {
				return nodes, err
			}
			// append the identical but sorted nodes
			for _, nestedNode := range nestedFlattenedList {
				sortedNodes = append(sortedNodes, nestedNode)
			}
		} else {
			// append the sorted nodes
			sortedNodes = append(sortedNodes, flatListNodeMap[name][0])
		}
	}

	// sorted nodes with a map and append corresponding nodes to sortedNodes
	sort.Strings(sortedNodesWithMapValue)
	for _, name := range sortedNodesWithMapValue {
		sortedNodes = append(sortedNodes, flatMapNodeMap[name][0])
	}

	return sortedNodes, nil
}