// 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 "" instead of " ) // 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 "value". // - 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] --> string_valtrue 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] --> string_valtrue 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] --> string_valtrue 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 += `" } } else if UseGoEmptyElementSyntax { *s += ">" } 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 += `" } } else if UseGoEmptyElementSyntax { *s += ">" } else { *s += "/>" } if doIndent { if p.cnt > p.start { *s += "\n" } p.Outdent() } return nil }