diff --git a/activity/json2xml/README.md b/activity/json2xml/README.md new file mode 100644 index 0000000..2552fc9 --- /dev/null +++ b/activity/json2xml/README.md @@ -0,0 +1,57 @@ +# JSON2XML + +Activity for converting JSON object into XML. + +For additional tuneups and syntax take a look at [MXJ](https://github.com/clbanning/mxj) Go package. + + +## Installation + +### Flogo CLI + +```bash +flogo install github.com/project-flogo/contrib/activity/json2xml +``` + +## Configuration + +### Input + +| Name | Type | Description | +|------------|---------|-------------------------------| +| json | object | Input object in JSON format | +| xmlRootTag | string | Optional name of XML root tag | + + +### Output + +| Name | Type | Description | +|------|------|----------------| +| xml | byte | Raw XML Output | + +Example converting raw XML byte array into string. +``` +string.tostring($activity[JsonToXML].xmlData ) +``` + +## Usage + +```json +{ + "id": "JsonToXML", + "name": "JsonToXML", + "activity": { + "ref": "github.com/project-flogo/contrib/activity/json2xml", + "input": { + "xmlRootTag": "", + "json": "{\"hello\":\"world\"}" + } + } +} +``` + +Output: +```xml + +world +``` \ No newline at end of file diff --git a/activity/json2xml/activity.go b/activity/json2xml/activity.go new file mode 100644 index 0000000..d9de293 --- /dev/null +++ b/activity/json2xml/activity.go @@ -0,0 +1,78 @@ +// Package json2xml activity for converting JSON object into XML +package json2xml + +import ( + "encoding/xml" + "fmt" + "github.com/clbanning/mxj" + "github.com/project-flogo/core/activity" + "github.com/project-flogo/core/support/log" + "strings" +) + +type Activity struct{} + +// init Flogo activity +func init() { + _ = activity.Register(&Activity{}) +} + +// Default xmlHeader +var xmlHeader = []byte(xml.Header) + +// Precalculated size of default root tags from MXJ package +var startDefaultRootTagSize = len(mxj.DefaultRootTag) + 2 +var endDefaultRootTagSize = len(mxj.DefaultRootTag) + 3 + +var metadata = activity.ToMetadata(&Input{}, &Output{}) + +// Metadata for the Activity +func (ac *Activity) Metadata() *activity.Metadata { + return metadata +} + +func (ac *Activity) Eval(ctx activity.Context) (bool, error) { + ctx.Logger().Debugf("Activity [%s] JSON2XML", ctx.Name()) + + input := &Input{} + var err = ctx.GetInputObject(input) + if err != nil { + return false, activity.NewError(fmt.Sprintf("Activity [%s] Can't get input JSON object - %s", ctx.Name(), err.Error()), "JSON2XML-01", nil) + } + + var out []byte + out, err = convert(input.Json, input.XmlRootTag, ctx.Logger()) + if err != nil { + return false, activity.NewError(fmt.Sprintf("Activity [%s] Error converting JSON object [%T] to XML - %s", ctx.Name(), input.Json, err.Error()), "JSON2XML-02", nil) + } + + err = ctx.SetOutputObject(&Output{Xml: out}) + if err != nil { + return false, activity.NewError(fmt.Sprintf("Activity [%s] Can't set output XML object - %s", ctx.Name(), err.Error()), "JSON2XML-03", nil) + } + + ctx.Logger().Debugf("Activity [%s] JSON2XML completed", ctx.Name()) + return true, nil +} + +func convert(json map[string]interface{}, xmlRootTag string, log log.Logger) ([]byte, error) { + mxj.XMLEscapeChars(true) + var data []byte + var err error + if len(strings.TrimSpace(xmlRootTag)) == 0 { + data, err = mxj.AnyXml(json) + if json != nil && err == nil { + // remove default root tags added by mxj package + data = data[startDefaultRootTagSize : len(data)-endDefaultRootTagSize] + } + } else { + data, err = mxj.AnyXml(json, xmlRootTag) + } + + var buf []byte + buf = append(buf, xmlHeader...) + buf = append(buf, data...) + + log.Debugf("Converted JSON object [%T] to XML object [%T] with size of %d", json, buf, len(buf)) + return buf, err +} diff --git a/activity/json2xml/activity_test.go b/activity/json2xml/activity_test.go new file mode 100644 index 0000000..3c3d75b --- /dev/null +++ b/activity/json2xml/activity_test.go @@ -0,0 +1,205 @@ +package json2xml + +import ( + "encoding/json" + "encoding/xml" + "fmt" + "github.com/clbanning/mxj" + "github.com/project-flogo/core/data/coerce" + "math" + "testing" + + "github.com/project-flogo/core/activity" + "github.com/project-flogo/core/support/test" + "github.com/stretchr/testify/assert" +) + +func TestRegister(t *testing.T) { + + ref := activity.GetRef(&Activity{}) + act := activity.Get(ref) + + assert.NotNil(t, act) +} + +func TestStructure(t *testing.T) { + + type NestedData struct { + DataA string `json:"data-a" xml:"data-a"` + } + + type Data struct { + Name string `json:"name" xml:"name"` + Special string `json:"spec_chars" xml:"spec_chars"` + List []string `json:"list" xml:"list"` + Number int `json:"number" xml:"number"` + Decimal float32 `json:"decimal" xml:"decimal"` + Utf8String string `json:"utf8" xml:"utf8"` + Nested NestedData `json:"nested" xml:"nested"` + } + + var rawTestData = &Data{ + Name: "Test Name", + Special: "<>&\"'", + List: []string{"one", "two", "three"}, + Number: 43, + Decimal: math.Pi, + Utf8String: "नमस्ते दुनिया", // Hello World in Hindi + Nested: NestedData{ + DataA: "nested data value", + }, + } + + ac := &Activity{} + tc := test.NewActivityContext(ac.Metadata()) + + // given + var jsonObject, _ = coerce.ToObject(rawTestData) // json object type for Flogo framework + t.Logf("Marshaled JSON: %s", jsonObject) + + acInput := &Input{Json: jsonObject, XmlRootTag: "Data"} + tc.SetInputObject(acInput) + + // when + done, _ := ac.Eval(tc) + + // then + acOutput := &Output{} + tc.GetOutputObject(acOutput) + t.Logf("Marshaled XML: %s", string(acOutput.Xml)) + + var outputData = &Data{} + var err = xml.Unmarshal(acOutput.Xml, &outputData) + if err != nil { + t.Error(err) + return + } + + assert.True(t, done) + assert.Equal(t, rawTestData, outputData) +} + +func TestEvalNoXMLRoot(t *testing.T) { + // setup + ac := &Activity{} + tc := test.NewActivityContext(ac.Metadata()) + + // given + jsonString := `{"hello": "world"}` + var jsonObject map[string]interface{} + json.Unmarshal([]byte(jsonString), &jsonObject) + + acInput := &Input{Json: jsonObject, XmlRootTag: ""} + tc.SetInputObject(acInput) + + // when + done, _ := ac.Eval(tc) + + // then + acOutput := &Output{} + tc.GetOutputObject(acOutput) + + assert.True(t, done) + var expectedResult = []byte("\nworld") + assert.Equal(t, expectedResult, acOutput.Xml) +} + +func TestEvalCustomXMLRoot(t *testing.T) { + // setup + ac := &Activity{} + tc := test.NewActivityContext(ac.Metadata()) + + // given + jsonString := `{"hello": "world"}` + var jsonObject map[string]interface{} + json.Unmarshal([]byte(jsonString), &jsonObject) + + acInput := &Input{Json: jsonObject, XmlRootTag: "my"} + tc.SetInputObject(acInput) + + // when + done, _ := ac.Eval(tc) + + // then + acOutput := &Output{} + tc.GetOutputObject(acOutput) + + assert.True(t, done) + assert.Contains(t, string(acOutput.Xml), "my") +} + +func TestJsonContainingXML(t *testing.T) { + // setup + ac := &Activity{} + tc := test.NewActivityContext(ac.Metadata()) + + // given + jsonString := `{"xml": "world"}` + var jsonObject map[string]interface{} + json.Unmarshal([]byte(jsonString), &jsonObject) + + acInput := &Input{Json: jsonObject} + tc.SetInputObject(acInput) + + // when + done, _ := ac.Eval(tc) + + // then + expectedResult := + `` + + "\n" + + `<hello>world</hello>` + acOutput := &Output{} + tc.GetOutputObject(acOutput) + + assert.True(t, done) + assert.Equal(t, expectedResult, string(acOutput.Xml)) +} + +func TestEmptyCustomXMLRootTag(t *testing.T) { + // setup + ac := &Activity{} + tc := test.NewActivityContext(ac.Metadata()) + + // given + acInput := &Input{Json: nil, XmlRootTag: "root"} + tc.SetInputObject(acInput) + + // when + done, _ := ac.Eval(tc) + + // then + expectedResult := + `` + + "\n" + + `` + acOutput := &Output{} + tc.GetOutputObject(acOutput) + + assert.True(t, done) + assert.Equal(t, expectedResult, string(acOutput.Xml)) +} + +func TestEmptyDefaultXMLRootTag(t *testing.T) { + // setup + ac := &Activity{} + tc := test.NewActivityContext(ac.Metadata()) + + // given + acInput := &Input{Json: nil} + tc.SetInputObject(acInput) + + // when + done, _ := ac.Eval(tc) + + // then + expectedResult := + `` + + "\n" + + fmt.Sprintf("<%s/>", mxj.DefaultRootTag) + acOutput := &Output{} + tc.GetOutputObject(acOutput) + + assert.True(t, done) + assert.Equal(t, expectedResult, string(acOutput.Xml)) +} diff --git a/activity/json2xml/descriptor.json b/activity/json2xml/descriptor.json new file mode 100644 index 0000000..2754e50 --- /dev/null +++ b/activity/json2xml/descriptor.json @@ -0,0 +1,27 @@ +{ + "name": "flogo-json2xml", + "type": "flogo:activity", + "ref": "github.com/project-flogo/contrib/activity/json2xml", + "version": "1.0.0", + "title": "JSON2XML Activity", + "description": "Converts given JSON into XML", + "homepage": "https://github.com/project-flogo/contrib/tree/master/activity/json2xml", + "input": [ + { + "name": "json", + "type": "object", + "required": true + }, + { + "name": "xmlRootTag", + "type": "string", + "required": false + } + ], + "output": [ + { + "name": "xml", + "type": "bytes" + } + ] +} diff --git a/activity/json2xml/go.mod b/activity/json2xml/go.mod new file mode 100644 index 0000000..2ecea76 --- /dev/null +++ b/activity/json2xml/go.mod @@ -0,0 +1,9 @@ +module github.com/project-flogo/contrib/activity/json2xml + +go 1.16 + +require ( + github.com/clbanning/mxj v1.8.4 + github.com/project-flogo/core v1.6.0 + github.com/stretchr/testify v1.7.1 +) diff --git a/activity/json2xml/go.sum b/activity/json2xml/go.sum new file mode 100644 index 0000000..4d70f68 --- /dev/null +++ b/activity/json2xml/go.sum @@ -0,0 +1,69 @@ +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/araddon/dateparse v0.0.0-20190622164848-0fb0a474d195 h1:c4mLfegoDw6OhSJXTd2jUEQgZUQuJWtocudb97Qn9EM= +github.com/araddon/dateparse v0.0.0-20190622164848-0fb0a474d195/go.mod h1:SLqhdZcd+dF3TEVL2RMoob5bBP5R1P1qkox+HtCBgGI= +github.com/clbanning/mxj v1.8.4 h1:HuhwZtbyvyOw+3Z1AowPkU87JkJUSv751ELWaiTpj8I= +github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/project-flogo/core v1.6.0 h1:+LH226SGU5961xh5H9lp8iUGp5dWPHaMYGu0pTuSLSE= +github.com/project-flogo/core v1.6.0/go.mod h1:fapTXUhLxDeAHyb6eMkuwnYswO8FpZJAMat055QVdJE= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.1.0/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= +go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.16.0 h1:uFRZXykJGK9lLY4HtgSw44DnIcAM+kRBP7x5m+NpAOM= +go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5 h1:hKsoRgsbwY1NafxrwTs+k64bikrLBkAgPir1TNCj3Zs= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= diff --git a/activity/json2xml/metadata.go b/activity/json2xml/metadata.go new file mode 100644 index 0000000..af8a19f --- /dev/null +++ b/activity/json2xml/metadata.go @@ -0,0 +1,47 @@ +package json2xml + +import ( + "github.com/project-flogo/core/data/coerce" +) + +type Input struct { + Json map[string]interface{} `md:"json"` + XmlRootTag string `md:"xmlRootTag"` +} + +func (i *Input) ToMap() map[string]interface{} { + return map[string]interface{}{ + "json": i.Json, + "xmlRootTag": i.XmlRootTag, + } +} + +func (i *Input) FromMap(values map[string]interface{}) error { + var err error + i.Json, err = coerce.ToObject(values["json"]) + i.XmlRootTag, err = coerce.ToString(values["xmlRootTag"]) + if err != nil { + return err + } + return nil +} + +type Output struct { + Xml []byte `md:"xml"` +} + +func (o *Output) ToMap() map[string]interface{} { + return map[string]interface{}{ + "xml": o.Xml, + } +} + +func (o *Output) FromMap(values map[string]interface{}) error { + var err error + o.Xml, err = coerce.ToBytes(values["xml"]) + if err != nil { + return err + } + + return nil +}