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