123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490 |
- // Copyright 2012-2016 xiaolipeng. All rights reserved.
- // Use of this source code is governed by a BSD-style
- // license that can be found in the LICENSE file
- // xml.go - basically the core of X2j for map[string]interface{} values.
- // NewMapXml, NewMapXmlReader, mv.Xml, mv.XmlWriter
- // see x2j and j2x for wrappers to provide end-to-end transformation of XML and JSON messages.
- package anyxml
- import (
- "encoding/xml"
- "errors"
- "fmt"
- "html/template"
- "time"
- )
- // --------------------------------- Xml, XmlIndent - from mxj -------------------------------
- const (
- DefaultRootTag = "doc"
- UseGoEmptyElementSyntax = false // if 'true' encode empty element as "<tag></tag>" instead of "<tag/>
- )
- // From: github.com/clbanning/mxj/xml.go with functions relabled: Xml() --> anyxml().
- // Encode a Map as XML. The companion of NewMapXml().
- // The following rules apply.
- // - The key label "#text" is treated as the value for a simple element with attributes.
- // - Map keys that begin with a hyphen, '-', are interpreted as attributes.
- // It is an error if the attribute doesn't have a []byte, string, number, or boolean value.
- // - Map value type encoding:
- // > string, bool, float64, int, int32, int64, float32: per "%v" formating
- // > []bool, []uint8: by casting to string
- // > structures, etc.: handed to xml.Marshal() - if there is an error, the element
- // value is "UNKNOWN"
- // - Elements with only attribute values or are null are terminated using "/>".
- // - If len(mv) == 1 and no rootTag is provided, then the map key is used as the root tag, possible.
- // Thus, `{ "key":"value" }` encodes as "<key>value</key>".
- // - To encode empty elements in a syntax consistent with encoding/xml call UseGoXmlEmptyElementSyntax().
- func anyxml(m map[string]interface{}, rootTag ...string) ([]byte, error) {
- var err error
- s := new(string)
- p := new(pretty) // just a stub
- if len(m) == 1 && len(rootTag) == 0 {
- for key, value := range m {
- // if it an array, see if all values are map[string]interface{}
- // we force a new root tag if we'll end up with no key:value in the list
- // so: key:[string_val, bool:true] --> <doc><key>string_val</key><bool>true</bool></key></doc>
- switch value.(type) {
- case []interface{}:
- for _, v := range value.([]interface{}) {
- switch v.(type) {
- case map[string]interface{}: // noop
- default: // anything else
- err = mapToXmlIndent(false, s, DefaultRootTag, m, p)
- goto done
- }
- }
- }
- err = mapToXmlIndent(false, s, key, value, p)
- }
- } else if len(rootTag) == 1 {
- err = mapToXmlIndent(false, s, rootTag[0], m, p)
- } else {
- err = mapToXmlIndent(false, s, DefaultRootTag, m, p)
- }
- done:
- return []byte(*s), err
- }
- func anyxmlWithDateFormat(dateFormat string, m map[string]interface{}, rootTag ...string) ([]byte, error) {
- var err error
- s := new(string)
- p := new(pretty) // just a stub
- if len(m) == 1 && len(rootTag) == 0 {
- for key, value := range m {
- // if it an array, see if all values are map[string]interface{}
- // we force a new root tag if we'll end up with no key:value in the list
- // so: key:[string_val, bool:true] --> <doc><key>string_val</key><bool>true</bool></key></doc>
- switch value.(type) {
- case []interface{}:
- for _, v := range value.([]interface{}) {
- switch v.(type) {
- case map[string]interface{}: // noop
- default: // anything else
- err = mapToXmlIndentWithDateFormat(dateFormat, false, s, DefaultRootTag, m, p)
- goto done
- }
- }
- }
- err = mapToXmlIndentWithDateFormat(dateFormat, false, s, key, value, p)
- }
- } else if len(rootTag) == 1 {
- err = mapToXmlIndentWithDateFormat(dateFormat, false, s, rootTag[0], m, p)
- } else {
- err = mapToXmlIndentWithDateFormat(dateFormat, false, s, DefaultRootTag, m, p)
- }
- done:
- return []byte(*s), err
- }
- func anyxmlIndentWithDateFormat(dateFormat string, m map[string]interface{}, prefix string, indent string, rootTag ...string) ([]byte, error) {
- var err error
- s := new(string)
- p := new(pretty) // just a stub
- if len(m) == 1 && len(rootTag) == 0 {
- for key, value := range m {
- // if it an array, see if all values are map[string]interface{}
- // we force a new root tag if we'll end up with no key:value in the list
- // so: key:[string_val, bool:true] --> <doc><key>string_val</key><bool>true</bool></key></doc>
- switch value.(type) {
- case []interface{}:
- for _, v := range value.([]interface{}) {
- switch v.(type) {
- case map[string]interface{}: // noop
- default: // anything else
- err = mapToXmlIndentWithDateFormat(dateFormat, true, s, DefaultRootTag, m, p)
- goto done
- }
- }
- }
- err = mapToXmlIndentWithDateFormat(dateFormat, true, s, key, value, p)
- }
- } else if len(rootTag) == 1 {
- err = mapToXmlIndentWithDateFormat(dateFormat, true, s, rootTag[0], m, p)
- } else {
- err = mapToXmlIndentWithDateFormat(dateFormat, true, s, DefaultRootTag, m, p)
- }
- done:
- return []byte(*s), err
- }
- // Encode a map[string]interface{} as a pretty XML string.
- // See Xml for encoding rules.
- func anyxmlIndent(m map[string]interface{}, prefix string, indent string, rootTag ...string) ([]byte, error) {
- var err error
- s := new(string)
- p := new(pretty)
- p.indent = indent
- p.padding = prefix
- if len(m) == 1 && len(rootTag) == 0 {
- // this can extract the key for the single map element
- // use it if it isn't a key for a list
- for key, value := range m {
- if _, ok := value.([]interface{}); ok {
- err = mapToXmlIndent(true, s, DefaultRootTag, m, p)
- } else {
- err = mapToXmlIndent(true, s, key, value, p)
- }
- }
- } else if len(rootTag) == 1 {
- err = mapToXmlIndent(true, s, rootTag[0], m, p)
- } else {
- err = mapToXmlIndent(true, s, DefaultRootTag, m, p)
- }
- return []byte(*s), err
- }
- type pretty struct {
- indent string
- cnt int
- padding string
- mapDepth int
- start int
- }
- func (p *pretty) Indent() {
- p.padding += p.indent
- p.cnt++
- }
- func (p *pretty) Outdent() {
- if p.cnt > 0 {
- p.padding = p.padding[:len(p.padding)-len(p.indent)]
- p.cnt--
- }
- }
- // where the work actually happens
- // returns an error if an attribute is not atomic
- func mapToXmlIndent(doIndent bool, s *string, key string, value interface{}, pp *pretty) error {
- var endTag bool
- var isSimple bool
- p := &pretty{pp.indent, pp.cnt, pp.padding, pp.mapDepth, pp.start}
- key = template.HTMLEscapeString(key)
- switch value.(type) {
- case map[string]interface{}, []byte, string, float64, bool, int, int32, int64, float32:
- if doIndent {
- *s += p.padding
- }
- *s += `<` + key
- }
- switch value.(type) {
- case map[string]interface{}:
- vv := value.(map[string]interface{})
- lenvv := len(vv)
- // scan out attributes - keys have prepended hyphen, '-'
- var cntAttr int
- for k, v := range vv {
- if k[:1] == "-" {
- switch v.(type) {
- case string:
- *s += ` ` + k[1:] + `="` + fmt.Sprintf("%v", template.HTMLEscapeString(v.(string))) + `"`
- cntAttr++
- case float64, bool, int, int32, int64, float32:
- *s += ` ` + k[1:] + `="` + fmt.Sprintf("%v", v) + `"`
- cntAttr++
- case []byte: // allow standard xml pkg []byte transform, as below
- *s += ` ` + k[1:] + `="` + fmt.Sprintf("%v", string(v.([]byte))) + `"`
- cntAttr++
- default:
- return fmt.Errorf("invalid attribute value for: %s", k)
- }
- }
- }
- // only attributes?
- if cntAttr == lenvv {
- break
- }
- // simple element? Note: '#text" is an invalid XML tag.
- if v, ok := vv["#text"]; ok {
- if cntAttr+1 < lenvv {
- return errors.New("#text key occurs with other non-attribute keys")
- }
- *s += ">" + fmt.Sprintf("%v", v)
- endTag = true
- break
- }
- // close tag with possible attributes
- *s += ">"
- if doIndent {
- *s += "\n"
- }
- // something more complex
- p.mapDepth++
- var i int
- for k, v := range vv {
- if k[:1] == "-" {
- continue
- }
- switch v.(type) {
- case []interface{}:
- default:
- if i == 0 && doIndent {
- p.Indent()
- }
- }
- i++
- mapToXmlIndent(doIndent, s, k, v, p)
- switch v.(type) {
- case []interface{}: // handled in []interface{} case
- default:
- if doIndent {
- p.Outdent()
- }
- }
- i--
- }
- p.mapDepth--
- endTag = true
- case []interface{}:
- for _, v := range value.([]interface{}) {
- if doIndent {
- p.Indent()
- }
- mapToXmlIndent(doIndent, s, key, v, p)
- if doIndent {
- p.Outdent()
- }
- }
- return nil
- case nil:
- // terminate the tag
- *s += "<" + key
- break
- default: // handle anything - even goofy stuff
- switch value.(type) {
- case string:
- *s += ">" + fmt.Sprintf("%v", template.HTMLEscapeString(value.(string)))
- case float64, bool, int, int32, int64, float32:
- *s += ">" + fmt.Sprintf("%v", value)
- case []byte: // NOTE: byte is just an alias for uint8
- // similar to how xml.Marshal handles []byte structure members
- *s += ">" + string(value.([]byte))
- default:
- var v []byte
- var err error
- if doIndent {
- v, err = xml.MarshalIndent(value, p.padding, p.indent)
- } else {
- v, err = xml.Marshal(value)
- }
- if err != nil {
- *s += ">UNKNOWN"
- } else {
- *s += string(v)
- }
- }
- isSimple = true
- endTag = true
- }
- if endTag {
- if doIndent {
- if !isSimple {
- // if p.mapDepth == 0 {
- // p.Outdent()
- // }
- *s += p.padding
- }
- }
- switch value.(type) {
- case map[string]interface{}, []byte, string, float64, bool, int, int32, int64, float32:
- *s += `</` + key + ">"
- }
- } else if UseGoEmptyElementSyntax {
- *s += "></" + key + ">"
- } else {
- *s += "/>"
- }
- if doIndent {
- if p.cnt > p.start {
- *s += "\n"
- }
- p.Outdent()
- }
- return nil
- }
- // where the work actually happens
- // returns an error if an attribute is not atomic
- func mapToXmlIndentWithDateFormat(dateFormat string, doIndent bool, s *string, key string, value interface{}, pp *pretty) error {
- var endTag bool
- var isSimple bool
- p := &pretty{pp.indent, pp.cnt, pp.padding, pp.mapDepth, pp.start}
- key = template.HTMLEscapeString(key)
- //start tag
- switch value.(type) {
- case map[string]interface{}, []byte, string, float64, bool, int, int32, int64, float32, time.Time:
- if doIndent {
- *s += p.padding
- }
- *s += `<` + key
- }
- switch value.(type) {
- case map[string]interface{}:
- vv := value.(map[string]interface{})
- lenvv := len(vv)
- var cntAttr int
- for k, v := range vv {
- if k[:1] == "-" {
- switch v.(type) {
- case string:
- *s += ` ` + k[1:] + `="` + fmt.Sprintf("%v", template.HTMLEscapeString(v.(string))) + `"`
- cntAttr++
- case float64, bool, int, int32, int64, float32:
- *s += ` ` + k[1:] + `="` + fmt.Sprintf("%v", v) + `"`
- cntAttr++
- case []byte: // allow standard xml pkg []byte transform, as below
- *s += ` ` + k[1:] + `="` + fmt.Sprintf("%v", string(v.([]byte))) + `"`
- cntAttr++
- default:
- return fmt.Errorf("invalid attribute value for: %s", k)
- }
- }
- }
- // only attributes?
- if cntAttr == lenvv {
- break
- }
- // simple element? Note: '#text" is an invalid XML tag.
- if v, ok := vv["#text"]; ok {
- if cntAttr+1 < lenvv {
- return errors.New("#text key occurs with other non-attribute keys")
- }
- *s += ">" + fmt.Sprintf("%v", v)
- endTag = true
- break
- }
- // close tag with possible attributes
- *s += ">"
- if doIndent {
- *s += "\n"
- }
- // something more complex
- p.mapDepth++
- var i int
- for k, v := range vv {
- if k[:1] == "-" {
- continue
- }
- switch v.(type) {
- case []interface{}:
- default:
- if i == 0 && doIndent {
- p.Indent()
- }
- }
- i++
- mapToXmlIndentWithDateFormat(dateFormat, doIndent, s, k, v, p)
- switch v.(type) {
- case []interface{}: // handled in []interface{} case
- default:
- if doIndent {
- p.Outdent()
- }
- }
- i--
- }
- p.mapDepth--
- endTag = true
- case []interface{}:
- for _, v := range value.([]interface{}) {
- if doIndent {
- p.Indent()
- }
- mapToXmlIndentWithDateFormat(dateFormat, doIndent, s, key, v, p)
- if doIndent {
- p.Outdent()
- }
- }
- return nil
- case nil:
- *s += "<" + key
- break
- default: // handle anything - even goofy stuff
- switch value.(type) {
- case string:
- *s += ">" + fmt.Sprintf("%v", template.HTMLEscapeString(value.(string)))
- case float64, bool, int, int32, int64, float32:
- *s += ">" + fmt.Sprintf("%v", value)
- case []byte: // NOTE: byte is just an alias for uint8
- // similar to how xml.Marshal handles []byte structure members
- *s += ">" + string(value.([]byte))
- case time.Time:
- *s += ">" + (value.(time.Time)).Format(dateFormat)
- default:
- var v []byte
- var err error
- if doIndent {
- v, err = xml.MarshalIndent(value, p.padding, p.indent)
- } else {
- v, err = xml.Marshal(value)
- }
- if err != nil {
- *s += ">UNKNOWN"
- } else {
- *s += string(v)
- }
- }
- isSimple = true
- endTag = true
- }
- if endTag {
- if doIndent {
- if !isSimple {
- // if p.mapDepth == 0 {
- // p.Outdent()
- // }
- *s += p.padding
- }
- }
- switch value.(type) {
- case map[string]interface{}, []byte, string, float64, bool, int, int32, int64, float32, time.Time:
- *s += `</` + key + ">"
- }
- } else if UseGoEmptyElementSyntax {
- *s += "></" + key + ">"
- } else {
- *s += "/>"
- }
- if doIndent {
- if p.cnt > p.start {
- *s += "\n"
- }
- p.Outdent()
- }
- return nil
- }
|