diff --git a/README.md b/README.md index 2fce9fa..990f955 100644 --- a/README.md +++ b/README.md @@ -29,10 +29,6 @@ type GetGeoIPResult struct { CountryCode string } -var ( - r GetGeoIPResponse -) - func main() { soap, err := gosoap.SoapClient("http://www.webservicex.net/geoipservice.asmx?WSDL") if err != nil { @@ -43,12 +39,14 @@ func main() { "IPAddress": "8.8.8.8", } - err = soap.Call("GetGeoIP", params) + res, err := soap.Call("GetGeoIP", params) if err != nil { fmt.Errorf("error in soap call: %s", err) } - soap.Unmarshal(&r) + r := GetGeoIPResponse{} + + res.Unmarshal(&r) if r.GetGeoIPResult.CountryCode != "USA" { fmt.Errorf("error: %+v", r) } diff --git a/encode.go b/encode.go index cb44fdc..b4ea4ef 100644 --- a/encode.go +++ b/encode.go @@ -6,22 +6,19 @@ import ( "reflect" ) -var tokens []xml.Token - // MarshalXML envelope the body and encode to xml -func (c Client) MarshalXML(e *xml.Encoder, _ xml.StartElement) error { - - tokens = []xml.Token{} +func (c process) MarshalXML(e *xml.Encoder, _ xml.StartElement) error { + tokens := &tokenData{} //start envelope - if c.Definitions == nil { + if c.Client.Definitions == nil { return fmt.Errorf("definitions is nil") } - startEnvelope() - if len(c.HeaderParams) > 0 { - startHeader(c.HeaderName, c.Definitions.Types[0].XsdSchema[0].TargetNamespace) - for k, v := range c.HeaderParams { + tokens.startEnvelope() + if len(c.Client.HeaderParams) > 0 { + tokens.startHeader(c.Client.HeaderName, c.Client.Definitions.Types[0].XsdSchema[0].TargetNamespace) + for k, v := range c.Client.HeaderParams { t := xml.StartElement{ Name: xml.Name{ Space: "", @@ -29,24 +26,24 @@ func (c Client) MarshalXML(e *xml.Encoder, _ xml.StartElement) error { }, } - tokens = append(tokens, t, xml.CharData(v), xml.EndElement{Name: t.Name}) + tokens.data = append(tokens.data, t, xml.CharData(v), xml.EndElement{Name: t.Name}) } - endHeader(c.HeaderName) + tokens.endHeader(c.Client.HeaderName) } - err := startBody(c.Method, c.Definitions.Types[0].XsdSchema[0].TargetNamespace) + err := tokens.startBody(c.Request.Method, c.Client.Definitions.Types[0].XsdSchema[0].TargetNamespace) if err != nil { return err } - recursiveEncode(c.Params) + tokens.recursiveEncode(c.Request.Params) //end envelope - endBody(c.Method) - endEnvelope() + tokens.endBody(c.Request.Method) + tokens.endEnvelope() - for _, t := range tokens { + for _, t := range tokens.data { err := e.EncodeToken(t) if err != nil { return err @@ -56,7 +53,11 @@ func (c Client) MarshalXML(e *xml.Encoder, _ xml.StartElement) error { return e.Flush() } -func recursiveEncode(hm interface{}) { +type tokenData struct { + data []xml.Token +} + +func (tokens *tokenData) recursiveEncode(hm interface{}) { v := reflect.ValueOf(hm) switch v.Kind() { @@ -69,21 +70,21 @@ func recursiveEncode(hm interface{}) { }, } - tokens = append(tokens, t) - recursiveEncode(v.MapIndex(key).Interface()) - tokens = append(tokens, xml.EndElement{Name: t.Name}) + tokens.data = append(tokens.data, t) + tokens.recursiveEncode(v.MapIndex(key).Interface()) + tokens.data = append(tokens.data, xml.EndElement{Name: t.Name}) } case reflect.Slice: for i := 0; i < v.Len(); i++ { - recursiveEncode(v.Index(i).Interface()) + tokens.recursiveEncode(v.Index(i).Interface()) } case reflect.String: content := xml.CharData(v.String()) - tokens = append(tokens, content) + tokens.data = append(tokens.data, content) } } -func startEnvelope() { +func (tokens *tokenData) startEnvelope() { e := xml.StartElement{ Name: xml.Name{ Space: "", @@ -96,10 +97,10 @@ func startEnvelope() { }, } - tokens = append(tokens, e) + tokens.data = append(tokens.data, e) } -func endEnvelope() { +func (tokens *tokenData) endEnvelope() { e := xml.EndElement{ Name: xml.Name{ Space: "", @@ -107,10 +108,10 @@ func endEnvelope() { }, } - tokens = append(tokens, e) + tokens.data = append(tokens.data, e) } -func startHeader(m, n string) { +func (tokens *tokenData) startHeader(m, n string) { h := xml.StartElement{ Name: xml.Name{ Space: "", @@ -119,7 +120,7 @@ func startHeader(m, n string) { } if m == "" || n == "" { - tokens = append(tokens, h) + tokens.data = append(tokens.data, h) return } @@ -133,12 +134,12 @@ func startHeader(m, n string) { }, } - tokens = append(tokens, h, r) + tokens.data = append(tokens.data, h, r) return } -func endHeader(m string) { +func (tokens *tokenData) endHeader(m string) { h := xml.EndElement{ Name: xml.Name{ Space: "", @@ -147,7 +148,7 @@ func endHeader(m string) { } if m == "" { - tokens = append(tokens, h) + tokens.data = append(tokens.data, h) return } @@ -158,11 +159,11 @@ func endHeader(m string) { }, } - tokens = append(tokens, r, h) + tokens.data = append(tokens.data, r, h) } // startToken initiate body of the envelope -func startBody(m, n string) error { +func (tokens *tokenData) startBody(m, n string) error { b := xml.StartElement{ Name: xml.Name{ Space: "", @@ -184,13 +185,13 @@ func startBody(m, n string) error { }, } - tokens = append(tokens, b, r) + tokens.data = append(tokens.data, b, r) return nil } // endToken close body of the envelope -func endBody(m string) { +func (tokens *tokenData) endBody(m string) { b := xml.EndElement{ Name: xml.Name{ Space: "", @@ -205,5 +206,5 @@ func endBody(m string) { }, } - tokens = append(tokens, r, b) + tokens.data = append(tokens.data, r, b) } diff --git a/encode_test.go b/encode_test.go index 965e5f5..3479a14 100644 --- a/encode_test.go +++ b/encode_test.go @@ -23,7 +23,7 @@ func TestClient_MarshalXML(t *testing.T) { } for _, test := range tests { - err = soap.Call("checkVat", test.Params) + _, err = soap.Call("checkVat", test.Params) if err == nil { t.Errorf(test.Err) } diff --git a/go.mod b/go.mod index c3b7bc2..9feb501 100644 --- a/go.mod +++ b/go.mod @@ -1 +1,6 @@ module github.com/tiaguinho/gosoap + +require ( + golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3 + golang.org/x/text v0.3.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d586547 --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3 h1:ulvT7fqt0yHWzpJwI57MezWnYDVpCAYBVuYst/L+fAY= +golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/request.go b/request.go new file mode 100644 index 0000000..f4d894c --- /dev/null +++ b/request.go @@ -0,0 +1,30 @@ +package gosoap + +import ( + "fmt" +) + +// Soap Request +type Request struct { + Method string + Params Params +} + +func NewRequest(m string, p Params) *Request { + return &Request{ + Method: m, + Params: p, + } +} + +type RequestStruct interface { + SoapBuildRequest() *Request +} + +func NewRequestByStruct(s RequestStruct) (*Request, error) { + if s == nil { + return nil, fmt.Errorf("'s' cannot be 'nil'") + } + + return s.SoapBuildRequest(), nil +} diff --git a/response.go b/response.go new file mode 100644 index 0000000..6f5adca --- /dev/null +++ b/response.go @@ -0,0 +1,28 @@ +package gosoap + +import ( + "encoding/xml" + "fmt" +) + +// Soap Response +type Response struct { + Body []byte + Header []byte + Payload []byte +} + +// Unmarshal get the body and unmarshal into the interface +func (r *Response) Unmarshal(v interface{}) error { + if len(r.Body) == 0 { + return fmt.Errorf("Body is empty") + } + + var f Fault + xml.Unmarshal(r.Body, &f) + if f.Code != "" { + return fmt.Errorf("[%s]: %s", f.Code, f.Description) + } + + return xml.Unmarshal(r.Body, v) +} diff --git a/soap.go b/soap.go index 4a5a04b..2c21e00 100644 --- a/soap.go +++ b/soap.go @@ -9,7 +9,9 @@ import ( "net/http" "net/url" "strings" - + "sync" + "time" + "golang.org/x/net/html/charset" ) @@ -26,15 +28,9 @@ func SoapClient(wsdl string) (*Client, error) { return nil, err } - d, err := getWsdlDefinitions(wsdl) - if err != nil { - return nil, err - } - c := &Client{ - WSDL: wsdl, - URL: strings.TrimSuffix(d.TargetNamespace, "/"), - Definitions: d, + wsdl: wsdl, + HttpClient: &http.Client{}, } return c, nil @@ -44,52 +40,109 @@ func SoapClient(wsdl string) (*Client, error) { // request and response of the server type Client struct { HttpClient *http.Client - WSDL string URL string - Method string - SoapAction string - Params Params HeaderName string HeaderParams HeaderParams Definitions *wsdlDefinitions - Body []byte - Header []byte - Username string - Password string + // Must be set before first request otherwise has no effect, minimum is 15 minutes. + RefreshDefinitionsAfter time.Duration + Username string + Password string + + once sync.Once + definitionsErr error + onRequest sync.WaitGroup + onDefinitionsRefresh sync.WaitGroup + wsdl string +} - payload []byte +// Call call's the method m with Params p +func (c *Client) Call(m string, p Params) (res *Response, err error) { + return c.Do(NewRequest(m, p)) } -// GetLastRequest returns the last request -func (c *Client) GetLastRequest() []byte { - return c.payload +// Call call's by struct +func (c *Client) CallByStruct(s RequestStruct) (res *Response, err error) { + req, err := NewRequestByStruct(s) + if err != nil { + return nil, err + } + + return c.Do(req) } -// Call call's the method m with Params p -func (c *Client) Call(m string, p Params) (err error) { +func (c *Client) waitAndRefreshDefinitions(d time.Duration) { + for { + time.Sleep(d) + c.onRequest.Wait() + c.onDefinitionsRefresh.Add(1) + c.initWsdl() + c.onDefinitionsRefresh.Done() + } +} + +func (c *Client) initWsdl() { + c.Definitions, c.definitionsErr = getWsdlDefinitions(c.wsdl) + if c.definitionsErr == nil { + c.URL = strings.TrimSuffix(c.Definitions.TargetNamespace, "/") + } +} + +func (c *Client) SetWSDL(wsdl string) { + c.onRequest.Wait() + c.onDefinitionsRefresh.Wait() + c.onRequest.Add(1) + c.onDefinitionsRefresh.Add(1) + defer c.onRequest.Done() + defer c.onDefinitionsRefresh.Done() + c.wsdl = wsdl + c.initWsdl() +} + +// Process Soap Request +func (c *Client) Do(req *Request) (res *Response, err error) { + c.onDefinitionsRefresh.Wait() + c.onRequest.Add(1) + defer c.onRequest.Done() + + c.once.Do(func() { + c.initWsdl() + // 15 minute to prevent abuse. + if c.RefreshDefinitionsAfter >= 15*time.Minute { + go c.waitAndRefreshDefinitions(c.RefreshDefinitionsAfter) + } + }) + + if c.definitionsErr != nil { + return nil, c.definitionsErr + } + if c.Definitions == nil { - return errors.New("WSDL definitions not found") + return nil, errors.New("wsdl definitions not found") } if c.Definitions.Services == nil { - return errors.New("No Services found in WSDL definitions") + return nil, errors.New("No Services found in wsdl definitions") + } + + p := &process{ + Client: c, + Request: req, + SoapAction: c.Definitions.GetSoapActionFromWsdlOperation(req.Method), } - c.Method = m - c.Params = p - c.SoapAction = c.Definitions.GetSoapActionFromWsdlOperation(c.Method) - if c.SoapAction == "" { - c.SoapAction = fmt.Sprintf("%s/%s", c.URL, c.Method) + if p.SoapAction == "" { + p.SoapAction = fmt.Sprintf("%s/%s", c.URL, req.Method) } - c.payload, err = xml.MarshalIndent(c, "", " ") + p.Payload, err = xml.MarshalIndent(p, "", " ") if err != nil { - return err + return nil, err } - b, err := c.doRequest(c.Definitions.Services[0].Ports[0].SoapAddresses[0].Location) + b, err := p.doRequest(c.Definitions.Services[0].Ports[0].SoapAddresses[0].Location) if err != nil { - return err + return nil, ErrorWithPayload{err, p.Payload} } var soap SoapEnvelope @@ -98,54 +151,48 @@ func (c *Client) Call(m string, p Params) (err error) { // https://stackoverflow.com/questions/6002619/unmarshal-an-iso-8859-1-xml-input-in-go // https://github.com/golang/go/issues/8937 - decoder := xml.NewDecoder( bytes.NewReader(b) ) - decoder.CharsetReader = charset.NewReaderLabel - err = decoder.Decode(&soap) - - c.Body = soap.Body.Contents - c.Header = soap.Header.Contents + decoder := xml.NewDecoder(bytes.NewReader(b)) + decoder.CharsetReader = charset.NewReaderLabel + err = decoder.Decode(&soap) - return err -} - -// Unmarshal get the body and unmarshal into the interface -func (c *Client) Unmarshal(v interface{}) error { - if len(c.Body) == 0 { - return fmt.Errorf("Body is empty") + res = &Response{ + Body: soap.Body.Contents, + Header: soap.Header.Contents, + Payload: p.Payload, } - - var f Fault - xml.Unmarshal(c.Body, &f) - if f.Code != "" { - return fmt.Errorf("[%s]: %s", f.Code, f.Description) + if err != nil { + return res, ErrorWithPayload{err, p.Payload} } - return xml.Unmarshal(c.Body, v) + return res, nil +} + +type process struct { + Client *Client + Request *Request + SoapAction string + Payload []byte } // doRequest makes new request to the server using the c.Method, c.URL and the body. -// body is enveloped in Call method -func (c *Client) doRequest(url string) ([]byte, error) { - req, err := http.NewRequest("POST", url, bytes.NewBuffer(c.payload)) +// body is enveloped in Do method +func (p *process) doRequest(url string) ([]byte, error) { + req, err := http.NewRequest("POST", url, bytes.NewBuffer(p.Payload)) if err != nil { return nil, err } - if c.Username != "" && c.Password != "" { - req.SetBasicAuth(c.Username, c.Password) + if p.Client.Username != "" && p.Client.Password != "" { + req.SetBasicAuth(p.Client.Username, p.Client.Password) } - if c.HttpClient == nil { - c.HttpClient = &http.Client{} - } - - req.ContentLength = int64(len(c.payload)) + req.ContentLength = int64(len(p.Payload)) req.Header.Add("Content-Type", "text/xml;charset=UTF-8") req.Header.Add("Accept", "text/xml") - req.Header.Add("SOAPAction", c.SoapAction) + req.Header.Add("SOAPAction", p.SoapAction) - resp, err := c.HttpClient.Do(req) + resp, err := p.httpClient().Do(req) if err != nil { return nil, err } @@ -154,6 +201,25 @@ func (c *Client) doRequest(url string) ([]byte, error) { return ioutil.ReadAll(resp.Body) } +func (p *process) httpClient() *http.Client { + if p.Client.HttpClient != nil { + return p.Client.HttpClient + } + return http.DefaultClient +} + +type ErrorWithPayload struct { + error + Payload []byte +} + +func GetPayloadFromError(err error) []byte { + if err, ok := err.(ErrorWithPayload); ok { + return err.Payload + } + return nil +} + // SoapEnvelope struct type SoapEnvelope struct { XMLName struct{} `xml:"Envelope"` diff --git a/soap_test.go b/soap_test.go index 9f72273..0664fe5 100644 --- a/soap_test.go +++ b/soap_test.go @@ -1,6 +1,7 @@ package gosoap import ( + "net/http" "testing" ) @@ -33,6 +34,18 @@ func TestSoapClient(t *testing.T) { } } +type CheckVatRequest struct { + CountryCode string + VatNumber string +} + +func (r CheckVatRequest) SoapBuildRequest() *Request { + return NewRequest("checkVat", Params{ + "countryCode": r.CountryCode, + "vatNumber": r.VatNumber, + }) +} + type CheckVatResponse struct { CountryCode string `xml:"countryCode"` VatNumber string `xml:"vatNumber"` @@ -69,24 +82,25 @@ func TestClient_Call(t *testing.T) { t.Errorf("error not expected: %s", err) } + var res *Response + params["vatNumber"] = "6388047V" params["countryCode"] = "IE" - err = soap.Call("", params) + res, err = soap.Call("", params) if err == nil { t.Errorf("method is empty") } - err = soap.Unmarshal(&rv) - if err == nil { + if res != nil { t.Errorf("body is empty") } - err = soap.Call("checkVat", params) + res, err = soap.Call("checkVat", params) if err != nil { t.Errorf("error in soap call: %s", err) } - soap.Unmarshal(&rv) + res.Unmarshal(&rv) if rv.CountryCode != "IE" { t.Errorf("error: %+v", rv) } @@ -96,12 +110,12 @@ func TestClient_Call(t *testing.T) { t.Errorf("error not expected: %s", err) } - err = soap.Call("CapitalCity", Params{"sCountryISOCode": "GB"}) + res, err = soap.Call("CapitalCity", Params{"sCountryISOCode": "GB"}) if err != nil { t.Errorf("error in soap call: %s", err) } - soap.Unmarshal(&rc) + res.Unmarshal(&rc) if rc.CapitalCityResult != "London" { t.Errorf("error: %+v", rc) @@ -112,12 +126,12 @@ func TestClient_Call(t *testing.T) { t.Errorf("error not expected: %s", err) } - err = soap.Call("NumberToWords", Params{"ubiNum": "23"}) + res, err = soap.Call("NumberToWords", Params{"ubiNum": "23"}) if err != nil { t.Errorf("error in soap call: %s", err) } - soap.Unmarshal(&rn) + res.Unmarshal(&rn) if rn.NumberToWordsResult != "twenty three " { t.Errorf("error: %+v", rn) @@ -128,45 +142,71 @@ func TestClient_Call(t *testing.T) { t.Errorf("error not expected: %s", err) } - err = soap.Call("Whois", Params{"DomainName": "google.com"}) + res, err = soap.Call("Whois", Params{"DomainName": "google.com"}) if err != nil { t.Errorf("error in soap call: %s", err) } - soap.Unmarshal(&rw) + res.Unmarshal(&rw) if rw.WhoisResult != "0" { t.Errorf("error: %+v", rw) } c := &Client{} - err = c.Call("", Params{}) + res, err = c.Call("", Params{}) if err == nil { t.Errorf("error expected but nothing got.") } - c.WSDL = "://test." + c.SetWSDL("://test.") - err = c.Call("checkVat", params) + res, err = c.Call("checkVat", params) if err == nil { t.Errorf("invalid WSDL") } } +func TestClient_CallByStruct(t *testing.T) { + soap, err := SoapClient("http://ec.europa.eu/taxation_customs/vies/checkVatService.wsdl") + if err != nil { + t.Errorf("error not expected: %s", err) + } + + var res *Response + + res, err = soap.CallByStruct(CheckVatRequest{ + CountryCode: "IE", + VatNumber: "6388047V", + }) + if err != nil { + t.Errorf("error in soap call: %s", err) + } + + res.Unmarshal(&rv) + if rv.CountryCode != "IE" { + t.Errorf("error: %+v", rv) + } +} + func TestClient_Call_NonUtf8(t *testing.T) { soap, err := SoapClient("https://demo.ilias.de/webservice/soap/server.php?wsdl") if err != nil { t.Errorf("error not expected: %s", err) } - soap.Call("login", Params{"client": "demo", "username": "robert", "password": "iliasdemo"}) + _, err = soap.Call("login", Params{"client": "demo", "username": "robert", "password": "iliasdemo"}) if err != nil { t.Errorf("error in soap call: %s", err) } } -func TestClient_doRequest(t *testing.T) { - c := &Client{} +func TestProcess_doRequest(t *testing.T) { + c := &process{ + Client: &Client{ + HttpClient: &http.Client{}, + }, + } _, err := c.doRequest("") if err == nil {