1// Package xmlutil provides XML serialization of AWS requests and responses.
2package xmlutil
3
4import (
5	"encoding/base64"
6	"encoding/xml"
7	"fmt"
8	"reflect"
9	"sort"
10	"strconv"
11	"strings"
12	"time"
13
14	"github.com/aws/aws-sdk-go/private/protocol"
15)
16
17// BuildXML will serialize params into an xml.Encoder. Error will be returned
18// if the serialization of any of the params or nested values fails.
19func BuildXML(params interface{}, e *xml.Encoder) error {
20	return buildXML(params, e, false)
21}
22
23func buildXML(params interface{}, e *xml.Encoder, sorted bool) error {
24	b := xmlBuilder{encoder: e, namespaces: map[string]string{}}
25	root := NewXMLElement(xml.Name{})
26	if err := b.buildValue(reflect.ValueOf(params), root, ""); err != nil {
27		return err
28	}
29	for _, c := range root.Children {
30		for _, v := range c {
31			return StructToXML(e, v, sorted)
32		}
33	}
34	return nil
35}
36
37// Returns the reflection element of a value, if it is a pointer.
38func elemOf(value reflect.Value) reflect.Value {
39	for value.Kind() == reflect.Ptr {
40		value = value.Elem()
41	}
42	return value
43}
44
45// A xmlBuilder serializes values from Go code to XML
46type xmlBuilder struct {
47	encoder    *xml.Encoder
48	namespaces map[string]string
49}
50
51// buildValue generic XMLNode builder for any type. Will build value for their specific type
52// struct, list, map, scalar.
53//
54// Also takes a "type" tag value to set what type a value should be converted to XMLNode as. If
55// type is not provided reflect will be used to determine the value's type.
56func (b *xmlBuilder) buildValue(value reflect.Value, current *XMLNode, tag reflect.StructTag) error {
57	value = elemOf(value)
58	if !value.IsValid() { // no need to handle zero values
59		return nil
60	} else if tag.Get("location") != "" { // don't handle non-body location values
61		return nil
62	}
63
64	xml := tag.Get("xml")
65	if len(xml) != 0 {
66		name := strings.SplitAfterN(xml, ",", 2)[0]
67		if name == "-" {
68			return nil
69		}
70	}
71
72	t := tag.Get("type")
73	if t == "" {
74		switch value.Kind() {
75		case reflect.Struct:
76			t = "structure"
77		case reflect.Slice:
78			t = "list"
79		case reflect.Map:
80			t = "map"
81		}
82	}
83
84	switch t {
85	case "structure":
86		if field, ok := value.Type().FieldByName("_"); ok {
87			tag = tag + reflect.StructTag(" ") + field.Tag
88		}
89		return b.buildStruct(value, current, tag)
90	case "list":
91		return b.buildList(value, current, tag)
92	case "map":
93		return b.buildMap(value, current, tag)
94	default:
95		return b.buildScalar(value, current, tag)
96	}
97}
98
99// buildStruct adds a struct and its fields to the current XMLNode. All fields and any nested
100// types are converted to XMLNodes also.
101func (b *xmlBuilder) buildStruct(value reflect.Value, current *XMLNode, tag reflect.StructTag) error {
102	if !value.IsValid() {
103		return nil
104	}
105
106	// unwrap payloads
107	if payload := tag.Get("payload"); payload != "" {
108		field, _ := value.Type().FieldByName(payload)
109		tag = field.Tag
110		value = elemOf(value.FieldByName(payload))
111
112		if !value.IsValid() {
113			return nil
114		}
115	}
116
117	child := NewXMLElement(xml.Name{Local: tag.Get("locationName")})
118
119	// there is an xmlNamespace associated with this struct
120	if prefix, uri := tag.Get("xmlPrefix"), tag.Get("xmlURI"); uri != "" {
121		ns := xml.Attr{
122			Name:  xml.Name{Local: "xmlns"},
123			Value: uri,
124		}
125		if prefix != "" {
126			b.namespaces[prefix] = uri // register the namespace
127			ns.Name.Local = "xmlns:" + prefix
128		}
129
130		child.Attr = append(child.Attr, ns)
131	}
132
133	var payloadFields, nonPayloadFields int
134
135	t := value.Type()
136	for i := 0; i < value.NumField(); i++ {
137		member := elemOf(value.Field(i))
138		field := t.Field(i)
139
140		if field.PkgPath != "" {
141			continue // ignore unexported fields
142		}
143		if field.Tag.Get("ignore") != "" {
144			continue
145		}
146
147		mTag := field.Tag
148		if mTag.Get("location") != "" { // skip non-body members
149			nonPayloadFields++
150			continue
151		}
152		payloadFields++
153
154		if protocol.CanSetIdempotencyToken(value.Field(i), field) {
155			token := protocol.GetIdempotencyToken()
156			member = reflect.ValueOf(token)
157		}
158
159		memberName := mTag.Get("locationName")
160		if memberName == "" {
161			memberName = field.Name
162			mTag = reflect.StructTag(string(mTag) + ` locationName:"` + memberName + `"`)
163		}
164		if err := b.buildValue(member, child, mTag); err != nil {
165			return err
166		}
167	}
168
169	// Only case where the child shape is not added is if the shape only contains
170	// non-payload fields, e.g headers/query.
171	if !(payloadFields == 0 && nonPayloadFields > 0) {
172		current.AddChild(child)
173	}
174
175	return nil
176}
177
178// buildList adds the value's list items to the current XMLNode as children nodes. All
179// nested values in the list are converted to XMLNodes also.
180func (b *xmlBuilder) buildList(value reflect.Value, current *XMLNode, tag reflect.StructTag) error {
181	if value.IsNil() { // don't build omitted lists
182		return nil
183	}
184
185	// check for unflattened list member
186	flattened := tag.Get("flattened") != ""
187
188	xname := xml.Name{Local: tag.Get("locationName")}
189	if flattened {
190		for i := 0; i < value.Len(); i++ {
191			child := NewXMLElement(xname)
192			current.AddChild(child)
193			if err := b.buildValue(value.Index(i), child, ""); err != nil {
194				return err
195			}
196		}
197	} else {
198		list := NewXMLElement(xname)
199		current.AddChild(list)
200
201		for i := 0; i < value.Len(); i++ {
202			iname := tag.Get("locationNameList")
203			if iname == "" {
204				iname = "member"
205			}
206
207			child := NewXMLElement(xml.Name{Local: iname})
208			list.AddChild(child)
209			if err := b.buildValue(value.Index(i), child, ""); err != nil {
210				return err
211			}
212		}
213	}
214
215	return nil
216}
217
218// buildMap adds the value's key/value pairs to the current XMLNode as children nodes. All
219// nested values in the map are converted to XMLNodes also.
220//
221// Error will be returned if it is unable to build the map's values into XMLNodes
222func (b *xmlBuilder) buildMap(value reflect.Value, current *XMLNode, tag reflect.StructTag) error {
223	if value.IsNil() { // don't build omitted maps
224		return nil
225	}
226
227	maproot := NewXMLElement(xml.Name{Local: tag.Get("locationName")})
228	current.AddChild(maproot)
229	current = maproot
230
231	kname, vname := "key", "value"
232	if n := tag.Get("locationNameKey"); n != "" {
233		kname = n
234	}
235	if n := tag.Get("locationNameValue"); n != "" {
236		vname = n
237	}
238
239	// sorting is not required for compliance, but it makes testing easier
240	keys := make([]string, value.Len())
241	for i, k := range value.MapKeys() {
242		keys[i] = k.String()
243	}
244	sort.Strings(keys)
245
246	for _, k := range keys {
247		v := value.MapIndex(reflect.ValueOf(k))
248
249		mapcur := current
250		if tag.Get("flattened") == "" { // add "entry" tag to non-flat maps
251			child := NewXMLElement(xml.Name{Local: "entry"})
252			mapcur.AddChild(child)
253			mapcur = child
254		}
255
256		kchild := NewXMLElement(xml.Name{Local: kname})
257		kchild.Text = k
258		vchild := NewXMLElement(xml.Name{Local: vname})
259		mapcur.AddChild(kchild)
260		mapcur.AddChild(vchild)
261
262		if err := b.buildValue(v, vchild, ""); err != nil {
263			return err
264		}
265	}
266
267	return nil
268}
269
270// buildScalar will convert the value into a string and append it as a attribute or child
271// of the current XMLNode.
272//
273// The value will be added as an attribute if tag contains a "xmlAttribute" attribute value.
274//
275// Error will be returned if the value type is unsupported.
276func (b *xmlBuilder) buildScalar(value reflect.Value, current *XMLNode, tag reflect.StructTag) error {
277	var str string
278	switch converted := value.Interface().(type) {
279	case string:
280		str = converted
281	case []byte:
282		if !value.IsNil() {
283			str = base64.StdEncoding.EncodeToString(converted)
284		}
285	case bool:
286		str = strconv.FormatBool(converted)
287	case int64:
288		str = strconv.FormatInt(converted, 10)
289	case int:
290		str = strconv.Itoa(converted)
291	case float64:
292		str = strconv.FormatFloat(converted, 'f', -1, 64)
293	case float32:
294		str = strconv.FormatFloat(float64(converted), 'f', -1, 32)
295	case time.Time:
296		format := tag.Get("timestampFormat")
297		if len(format) == 0 {
298			format = protocol.ISO8601TimeFormatName
299		}
300
301		str = protocol.FormatTime(format, converted)
302	default:
303		return fmt.Errorf("unsupported value for param %s: %v (%s)",
304			tag.Get("locationName"), value.Interface(), value.Type().Name())
305	}
306
307	xname := xml.Name{Local: tag.Get("locationName")}
308	if tag.Get("xmlAttribute") != "" { // put into current node's attribute list
309		attr := xml.Attr{Name: xname, Value: str}
310		current.Attr = append(current.Attr, attr)
311	} else if len(xname.Local) == 0 {
312		current.Text = str
313	} else { // regular text node
314		current.AddChild(&XMLNode{Name: xname, Text: str})
315	}
316	return nil
317}
318