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