2016-12-20

gowsdlでSalesforceのSOAP APIを叩く

今回はgowsdlを使ってSalesforceのSOAP APIを叩いてみます。

gowsdlの使い方

こんな感じでコマンドを叩けばOK

$ gowsdl {URL} // -p で出力先ディレクトリを変更する

ローカルファイルは読み込んでくれないので、Web上に置く必要があります。Salesforceの各種WSDLはWeb上にあるものの、いずれも認証を必要とするので、ダウンロードしたWSDLをS3にPublicなオブジェクトとしてデータをアップロードしたりする必要があります。ただし、Enterprise WSDLはスキーマ情報がそのまま乗るので取扱には注意です。試してませんがローカルにサーバ立てて、そこで配信するのが手軽でセキュアかも。

partner.wsdlでやってみるとこんな感じでエラーが出ます。

$ gowsdl -p partner https://s3-ap-northeast-1.amazonaws.com/{BUCKET}/partner.wsdl

  Downloading file https://s3-ap-northeast-1.amazonaws.com/{BUCKET}partner.wsdl
  Downloading external schema location 
  https://s3-ap-northeast-1.amazonaws.com/{BUCKET}/partner.wsdl
  expected element type schema but have definitions

gowsdlはシングルバイナリなので、デバッグ仕込んだりすることは出来ませんが、ベースとなっているソースコードを直接いじることで原因を調査することができます。

$ go run $GOPATH/src/github.com/hooklift/gowsdl/cmd/gowsdl/main.go \
  https://s3-ap-northeast-1.amazonaws.com/{BUCKET}/partner.wsdl

ということで、ここらへんをコメントアウトして無理矢理動かします(白目)

diff --git a/gowsdl.go b/gowsdl.go
index e136e27..6464a7d 100644
--- a/gowsdl.go
+++ b/gowsdl.go
@@ -176,9 +176,9 @@ func (g *GoWSDL) unmarshal() error {
 
        for _, schema := range g.wsdl.Types.Schemas {
                err = g.resolveXSDExternals(schema, parsedURL)
-               if err != nil {
-                       return err
-               }
+               // if err != nil {
+               //      return err
+               // }
        }
 
        return nil

これでとりあえずクライアントは自動生成できます。自動生成されたパッケージを少し修正して動かしてみます。 まずはパッケージ名をmainに変更します。

@@ -1,4 +1,4 @@
-package myservice
+package main
同一階層にmain.goを作成する
package main 

import (
    "github.com/k0kubun/pp"
)

func main() {
    soap := NewSoap("", true, nil)
    login := Login{
        Username: "xxxxx",
        Password: "xxxxx",
    }
    res, err := soap.Login(login)
    if err != nil {
        pp.Print(err)
    }
    pp.Print(res)
}

そのままgo build .すると、こんなエラーが出ます

$ go build .
# _/Users/mtajitsu/myservice
./myservice.go:2766: DescribeGlobalTheme redeclared in this block
	previous declaration at ./myservice.go:1230
./myservice.go:3344: DescribeApprovalLayout redeclared in this block
	previous declaration at ./myservice.go:1374
./myservice.go:3366: DescribeLayout redeclared in this block
	previous declaration at ./myservice.go:1316
./myservice.go:4516: undefined: QName

リクエスト用の構造体とレスポンスの構造体の名前が重複しているのが原因なので、リクエスト用の構造体はXXXRequestとするように名前を変更します。

またQNameはstringと置き換えても問題なさそうなので、置き換えます。

@@ -1227,7 +1227,7 @@
 	Result *DescribeGlobalResult `xml:"result,omitempty"`
 }
 
-type DescribeGlobalTheme struct {
+type DescribeGlobalThemeRequest struct {
 	XMLName xml.Name `xml:"urn:partner.soap.sforce.com describeGlobalTheme"`
 }
 
@@ -1313,7 +1313,7 @@
 	Result *DescribeAppMenuResult `xml:"result,omitempty"`
 }
 
-type DescribeLayout struct {
+type DescribeLayoutRequest struct {
 	XMLName xml.Name `xml:"urn:partner.soap.sforce.com describeLayout"`
 
 	SObjectType string `xml:"sObjectType,omitempty"`
@@ -1371,7 +1371,7 @@
 	Result *DescribePathAssistantsResult `xml:"result,omitempty"`
 }
 
-type DescribeApprovalLayout struct {
+type DescribeApprovalLayoutRequest struct {
 	XMLName xml.Name `xml:"urn:partner.soap.sforce.com describeApprovalLayout"`
 
 	SObjectType string `xml:"sObjectType,omitempty"`
@@ -4513,7 +4514,7 @@
 	ExceptionCodeXMLPARSERERROR ExceptionCode = "XMLPARSERERROR"
 )
 
-type FaultCode *QName
+type FaultCode string
 
 const (
 	FaultCodeFnsAPEXTRIGGERCOUPLINGLIMIT FaultCode = "fnsAPEXTRIGGERCOUPLINGLIMIT"
@@ -5065,7 +5066,7 @@
 //
 //   - UnexpectedErrorFault
 /* Describe Gloal and Themes */
-func (service *Soap) DescribeGlobalTheme(request *DescribeGlobalTheme) (*DescribeGlobalThemeResponse, error) {
+func (service *Soap) DescribeGlobalTheme(request *DescribeGlobalThemeRequest) (*DescribeGlobalThemeResponse, error) {
 	response := new(DescribeGlobalThemeResponse)
 	err := service.client.Call("", request, response)
 	if err != nil {
@@ -5095,7 +5096,7 @@
 //   - UnexpectedErrorFault
 //   - InvalidIdFault
 /* Describe the layout of the given sObject or the given actionable global page. */
-func (service *Soap) DescribeLayout(request *DescribeLayout) (*DescribeLayoutResponse, error) {
+func (service *Soap) DescribeLayout(request *DescribeLayoutRequest) (*DescribeLayoutResponse, error) {
 	response := new(DescribeLayoutResponse)
 	err := service.client.Call("", request, response)
 	if err != nil {
@@ -5179,7 +5180,7 @@
 }
 
 /* Describe the approval layouts of the given sObject */
-func (service *Soap) DescribeApprovalLayout(request *DescribeApprovalLayout) (*DescribeApprovalLayoutResponse, error) {
+func (service *Soap) DescribeApprovalLayout(request *DescribeApprovalLayoutRequest) (*DescribeApprovalLayoutResponse, error) {
 	response := new(DescribeApprovalLayoutResponse)
 	err := service.client.Call("", request, response)
 	if err != nil {

ここで go buildしてもまだエラーが出ます。

errors.errorString{
  s: "xml: name \"result\" in tag of main.LoginResponse.Result conflicts with name \"LoginResult\" in *main.LoginResult.XMLName",
}(*main.LoginResponse)(nil)

タグ名が一致していない、というエラーです。gowsdlのコメントアウトに起因している可能性もありますが、修正不要なwsdlでやっても同じエラーが出たのでそういう仕様なのかもしれません。とりあえずピンポイントにタグ名を一致させるか、子のタグ名表記を消します。

@@ -2214,7 +2214,8 @@
 }
 
 type GetUserInfoResult struct {
-	XMLName xml.Name `xml:"urn:partner.soap.sforce.com GetUserInfoResult"`
+	// resultとuserInfoの両パターンあるためコメントアウト
+	// XMLName xml.Name `xml:"urn:partner.soap.sforce.com GetUserInfoResult"`
 
 	AccessibilityMode bool `xml:"accessibilityMode,omitempty"`

@@ -2264,7 +2265,7 @@
 }
 
 type LoginResult struct {
-	XMLName xml.Name `xml:"urn:partner.soap.sforce.com LoginResult"`
+	XMLName xml.Name `xml:"urn:partner.soap.sforce.com result"`
 
 	MetadataServerUrl string `xml:"metadataServerUrl,omitempty"`

そうするとgo buildは通りますが、SOAP RequestのFaultが発生します。

<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
  <soapenv:Body>
    <soapenv:Fault>
      <faultcode>soapenv:Client</faultcode>
      <faultstring>SOAPAction HTTP header missing</faultstring>
    </soapenv:Fault>
  </soapenv:Body>
</soapenv:Envelope>

SalesforceのSOAP APIはSOAPActionに空文字でも良いので入れる必要があるので無理矢理入れます。

@@ -5883,10 +5884,8 @@
 	}
 
 	req.Header.Add("Content-Type", "text/xml; charset=\"utf-8\"")
-	if soapAction != "" {
-		req.Header.Add("SOAPAction", soapAction)
-	}
-
+	req.Header.Add("SOAPAction", "''")
+	
 	req.Header.Set("User-Agent", "gowsdl/0.1")
 	req.Close = true

これでログインが通るのですが、SOAPのログ出力がうざいので取ります。

@@ -5872,7 +5873,7 @@
 		return err
 	}
 
-	log.Println(buffer.String())
+	// log.Println(buffer.String())
 
 	req, err := http.NewRequest("POST", s.url, buffer)
 	if err != nil {

@@ -5913,7 +5912,7 @@
 		return nil
 	}
 
-	log.Println(string(rawbody))
+	// log.Println(string(rawbody))
 	respEnvelope := new(SOAPEnvelope)
 	respEnvelope.Body = SOAPBody{Content: response}
 	err = xml.Unmarshal(rawbody, respEnvelope)

ということでgowsdlの修正と生成されたクライアント側の修正がかなり多いです…。とはいえ、ここまでやってしまえばタグ名の不一致以外のエラーは発生しないので安心してSOAP APIをコールすることができます。 Metadata APIの場合 Metadata WSDLの場合は修正不要でgowsdlが利用できますが、Partner WSDLと違って以下の点を修正する必要があります。

リクエスト用のDeploy構造体のZipFileメンバの型名の変更

@@ -4382,7 +4382,7 @@
 type Deploy struct {
 	XMLName xml.Name `xml:"http://soap.sforce.com/2006/04/metadata deploy"`
 
-	ZipFile []byte `xml:"ZipFile,omitempty"`
+	ZipFile string `xml:"ZipFile,omitempty"`
 
 	DeployOptions *DeployOptions `xml:"DeployOptions,omitempty"`
 }

Loginコールが無いので追記する

@@ -12921,10 +12922,35 @@
 	Level *LogCategoryLevel `xml:"level,omitempty"`
 }
 
+type LoginRequest struct {
+	XMLName  xml.Name `xml:"urn:partner.soap.sforce.com login"`
+	Username string   `xml:"username"`
+	Password string   `xml:"password"`
+}
+
+type LoginResponse struct {
+	XMLName     xml.Name    `xml:"urn:partner.soap.sforce.com loginResponse"`
+	LoginResult LoginResult `xml:"result"`
+}
+
+type LoginResult struct {
+	MetadataServerUrl string `xml:"metadataServerUrl"`
+	PasswordExpired   bool   `xml:"passwordExpired"`
+	Sandbox           bool   `xml:"sandbox`
+	ServerUrl         string `xml:"serverUrl"`
+	SessionId         string `xml:"sessionId"`
+	UserId            *ID    `xml:"userId"`
+	//	UserInfo *UserInfo `xml:"userInfo"` //UserInfo構造体は面倒なので一旦コメントアウト
+}
+
 type MetadataPortType struct {
 	client *SOAPClient
 }
 
+func (service *MetadataPortType) SetServerUrl(url string) {
+	service.client.SetServerUrl(url)
+}
+

@@ -13105,6 +13132,17 @@
 	return response, nil
 }
 
+/* Upserts metadata entries synchronously. */
+func (service *MetadataPortType) Login(request *LoginRequest) (*LoginResponse, error) {
+	response := new(LoginResponse)
+	err := service.client.Call("''", request, response)
+	if err != nil {
+		return nil, err
+	}
+
+	return response, nil
+}
+

@@ -13217,6 +13255,10 @@
 	s.header = header
 }
 
+func (s *SOAPClient) SetServerUrl(url string) {
+	s.url = url
+}
+

あとはこんな感じで使えばOK

portType := NewMetadataPortType("https://"+endpoint+"/services/Soap/u/"+apiversion, true, nil)

loginRequest := LoginRequest{Username: username, Password: password}
loginResponse, err := portType.Login(loginRequest)
if err != nil {
	return err
}
loginResult := loginResponse.LoginResult
request := Deploy{
	ZipFile:       base64.StdEncoding.EncodeToString(buf),
	DeployOptions: nil,
}
sessionHeader := SessionHeader{
	SessionId: loginResult.SessionId,
}
portType.SetHeader(sessionHeader)
portType.SetServerUrl(loginResult.MetadataServerUrl)

portType.Deploy(request)
このエントリーをはてなブックマークに追加