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