プログラマ38の日記

主にプログラムメモです。

Java: wsimportでSalesforce APIを使用する

まずはサンプルコード

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.ws.BindingProvider;
import javax.xml.ws.handler.MessageContext;

import com.sforce.soap.partner.LoginResult;
import com.sforce.soap.partner.ObjectFactory;
import com.sforce.soap.partner.SessionHeader;
import com.sforce.soap.partner.SforceService;
import com.sforce.soap.partner.Soap;
import com.sun.xml.ws.api.message.Header;
import com.sun.xml.ws.api.message.Headers;
import com.sun.xml.ws.developer.WSBindingProvider;    
    
-------------------------------------------------------------------------
    Soap soapBinding = new SforceService().getSoap();

WSBindingProvider provider = (WSBindingProvider)soapBinding;

Map<String, Object> reqContext = provider.getRequestContext();
reqContext.put(WSBindingProvider.ENDPOINT_ADDRESS_PROPERTY, "https://login.salesforce.com/services/Soap/u/39.0");
reqContext.put("javax.xml.ws.client.connectionTimeout", "180");
reqContext.put("javax.xml.ws.client.receiveTimeout", "180");

//Enable GZip compression
Map<String, List<String>> httpHeaders = new HashMap<String, List<String>>();
httpHeaders.put("Content-Encoding", Collections.singletonList("gzip"));
httpHeaders.put("Accept-Encoding", Collections.singletonList("gzip"));
reqContext.put(MessageContext.HTTP_REQUEST_HEADERS, httpHeaders);

//httpプロキシを指定する
{
System.setProperty("https.proxyHost", "samplehost");
System.setProperty("https.proxyPort", "9999");
ProxyUserPasswordConfig.setProxyUserPassword("sampleproxyuser", "sampleproxypasword");
}

LoginResult loginResult = soapBinding.login("sampleuser@sample.username", "samplepassword");

String serverUrl = loginResult.getServerUrl();

reqContext.put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY, serverUrl);

SessionHeader sh = new SessionHeader();
String sessionId = loginResult.getSessionId();
sh.setSessionId(sessionId);

JAXBContext jaxbContext = null;
try {
jaxbContext = JAXBContext.newInstance(ObjectFactory.class);
} catch (JAXBException e) {}

ArrayList<Header> sessionheaderlst = new ArrayList<Header>();
sessionheaderlst.add(Headers.create((JAXBContext) jaxbContext, sh));
provider.setOutboundHeaders(sessionheaderlst);

System.out.println(soapBinding.getServerTimestamp().getTimestamp());

httpproxyのユーザ、パスワードは「PasswordAuthentication」を使うため上記で次のクラスを使っています。※他のやり方もあるとは思いますがこのやり方にしました。

import java.net.Authenticator;
import java.net.PasswordAuthentication;

public class ProxyUserPasswordConfig extends Authenticator {

private String user;
private String password;

private ProxyUserPasswordConfig(String proxyuser, String proxypassword){
user = proxyuser;
password = proxypassword;
}

public static void setProxyUserPassword(String proxyuser, String proxypassword){
Authenticator.setDefault(new ProxyUserPasswordConfig(proxyuser,proxypassword));
}
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(user, password.toCharArray());
}
}

コードの生成とライブラリの作成

jdk付属のwsimportコマンドを使います。wsdlを含んだjarまで自動で作成してくれます。

mkdir partner
mkdir metadata
wsimport -B-XautoNameResolution -d partner -clientjar partner.jar -b _jaxb.xml partner.wsdl
wsimport -B-XautoNameResolution -d metadata -clientjar metadata.jar -b _jaxb.xml metadata.wsdl

_jaxb.xmlというのを上記コマンドで指定していますが、_jaxb.xmlの中身は次のテキストです。 typesafeEnumMaxMembersの上限エラーになったのでサイズを増やしています。

<jaxb:bindings
xmlns:jaxb="http://java.sun.com/xml/ns/jaxb"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xjc="http://java.sun.com/xml/ns/jaxb/xjc"
jaxb:extensionBindingPrefixes="xjc"
jaxb:version="1.0">
<jaxb:globalBindings typesafeEnumMaxMembers="2000">
<xjc:serializable/>
</jaxb:globalBindings>
</jaxb:bindings>

httpproxyの設定

proxyhostとproxyportはシステムプロパティで指定するのですが、proxyuserとproxypasswordはコード内で指定となっています。

      System.setProperty("https.proxyHost", "samplehost");
System.setProperty("https.proxyPort", "9999");
ProxyUserPasswordConfig.setProxyUserPassword("sampleproxyuser", "sampleproxypasword");

エンドポイントの設定

wsimportを標準のまま使うとエンドポイントが変更できません。そのため外部のライブラリJAXWS-RIを使います。(現在まとめてダウンロードできないのでこちらにアップしました。)このライブラリ内のWSBindingProviderを使います。

    WSBindingProvider provider = (WSBindingProvider)soapBinding;

Map<String, Object> reqContext = provider.getRequestContext();
reqContext.put(WSBindingProvider.ENDPOINT_ADDRESS_PROPERTY, "https://login.salesforce.com/services/Soap/u/39.0");

セッションIDと、インスタンスURLのセット

ここも、エンドポイントと同様に、WSBindingProviderを使います。

やりたいことは、SOAPHeaderにセッションヘッダをつけて、エンドポイントを変えたいだけなのに、結構書く必要があります。

    String serverUrl = loginResult.getServerUrl();

reqContext.put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY, serverUrl);

SessionHeader sh = new SessionHeader();
String sessionId = loginResult.getSessionId();
sh.setSessionId(sessionId);

JAXBContext jaxbContext = null;
try {
jaxbContext = JAXBContext.newInstance(ObjectFactory.class);
} catch (JAXBException e) {}

ArrayList<Header> sessionheaderlst = new ArrayList<Header>();
sessionheaderlst.add(Headers.create((JAXBContext) jaxbContext, sh));
provider.setOutboundHeaders(sessionheaderlst);

これで上記コードのsoapBindingでSOAP APIを使用できます。

以前(java6頃)のwsimportはデータバインディングが非常に遅かったのですが、java8ではかなり改善しているように思います。

Java: axis2でSalesforce APIを使用する

まずはサンプルコード

    String url = "https://login.salesforce.com/services/Soap/u/39.0";   
SforceServiceStub soapBinding = new SforceServiceStub(url);


Options options = soapBinding._getServiceClient().getOptions();
options.setProperty(HTTPConstants.MC_ACCEPT_GZIP, Boolean.TRUE);
options.setProperty(HTTPConstants.MC_GZIP_REQUEST, Boolean.TRUE);

//httpプロキシを指定する
{
HttpTransportProperties.ProxyProperties proxyProperties = new HttpTransportProperties.ProxyProperties();
proxyProperties.setProxyName("samplehost");
proxyProperties.setProxyPort(9999);
proxyProperties.setUserName("sampleproxyuser");
proxyProperties.setPassWord("sampleproxypasword");
options.setProperty(HTTPConstants.PROXY, proxyProperties);
}


LoginDocument logininfo = LoginDocument.Factory.newInstance();
Login login = Login.Factory.newInstance();
login.setUsername("sampleuser@sample.username");
login.setPassword("samplepassword");
logininfo.setLogin(login);


LoginResult loginResult = soapBinding.login(logininfo, null,null).getLoginResponse().getResult();

options.setTo(new EndpointReference(loginResult.getServerUrl()));

SessionHeaderDocument sh = SessionHeaderDocument.Factory.newInstance();

SessionHeader sheader = SessionHeaderDocument.SessionHeader.Factory.newInstance();
sheader.setSessionId(loginResult.getSessionId());
sh.setSessionHeader(sheader);

GetServerTimestampDocument getServerTimestampDocument = GetServerTimestampDocument.Factory.newInstance();
getServerTimestampDocument.setGetServerTimestamp(GetServerTimestamp.Factory.newInstance());
System.out.println(soapBinding.getServerTimestamp(getServerTimestampDocument, sh, null).getGetServerTimestampResponse().getResult().getTimestamp());

コードの生成とライブラリの作成

apache2のサイト(Apache Axis2 – Apache Axis2/Java - Next Generation Web Services

)からライブラリをダウンロード

ダウンロードしたaxis2のライブラリにクラスパスを指定しコマンドを実行する。

set classpath=.;..\lib\*

java -cp %classpath% org.apache.axis2.wsdl.WSDL2Java -Euwc -Ejavaversion 1.5 -d xmlbeans -uri partner.wsdl
java -cp %classpath% org.apache.axis2.wsdl.WSDL2Java -Euwc -Ejavaversion 1.5 -d xmlbeans -uri metadata.wsdl

コマンドを実行するとJavaコードと共にresourcesのフォルダにxsbが出力されます。

resourcesフォルダのファイルは実行時に必要なので、Javaコードをコンパイルした後、コンパイルしたクラスとresourcesフォルダのファイルを含めてライブラリを作成します。

httpproxyの設定

httpproxyはプログラム内部の指定となります。

          HttpTransportProperties.ProxyProperties proxyProperties = new HttpTransportProperties.ProxyProperties();
proxyProperties.setProxyName("samplehost");
proxyProperties.setProxyPort(9999);
proxyProperties.setUserName("sampleproxyuser");
proxyProperties.setPassWord("sampleproxypasword");
options.setProperty(HTTPConstants.PROXY, proxyProperties);

エンドポイントの設定

コンストラクタで受けてくれるタイプです。

    String url = "https://login.salesforce.com/services/Soap/u/39.0";    
SforceServiceStub soapBinding = new SforceServiceStub(url);

セッションIDと、インスタンスURLのセット

optionsで、エンドポイントを指定しますが、SoapHeaderにセッションヘッダをセットはしません。セッションヘッダは実行時に毎回引数で指定することになります。

        options.setTo(new EndpointReference(loginResult.getServerUrl()));

SessionHeaderDocument sh = SessionHeaderDocument.Factory.newInstance();

SessionHeader sheader = SessionHeaderDocument.SessionHeader.Factory.newInstance();
sheader.setSessionId(loginResult.getSessionId());
sh.setSessionHeader(sheader);

 

これで上記コードのsoapBindingはSOAP APIを使うことができます。サンプルコードの最後に、Salesforceのサーバ時刻を取得するAPIをコールしています。

サーバ時刻を取得するAPIをコールするだけで、このコード量ですから、アプリケーションで書く場合のコード量はとても多くなります。

Java: axis1.4でSalesforce APIを使用する

まずはサンプルコード

    SoapBindingStub soapBinding = (SoapBindingStub)new SforceServiceLocator().getSoap();

soapBinding._setProperty(org.apache.axis.transport.http.HTTPConstants.MC_ACCEPT_GZIP , "true");
soapBinding._setProperty(org.apache.axis.transport.http.HTTPConstants.MC_GZIP_REQUEST , "true");

soapBinding._setProperty(Stub.ENDPOINT_ADDRESS_PROPERTY, "https://login.salesforce.com/services/Soap/u/39.0");

//httpプロキシを指定する
{
Properties systemprops = System.getProperties();
systemprops.setProperty("http.useProxy", "true");
systemprops.setProperty("http.proxyHost", "samplehost");
systemprops.setProperty("http.proxyPort", "9999");
systemprops.setProperty("http.proxyUser", "sampleproxyuser");
systemprops.setProperty("http.proxyPassword", "sampleproxypasword");
}


LoginResult loginResult = soapBinding.login("sampleuser@sample.username", "samplepassword");

String serverUrl = loginResult.getServerUrl();

soapBinding._setProperty(Stub.ENDPOINT_ADDRESS_PROPERTY, serverUrl);

SessionHeader sh = new SessionHeader();
sh.setSessionId(loginResult.getSessionId());
soapBinding.setHeader((new SforceServiceLocator()).getServiceName().getNamespaceURI(), "SessionHeader", sh);

System.out.println(soapBinding.getServerTimestamp().getTimestamp());

コードの生成とライブラリの作成

axis1.4のライブラリとcommonshttp3.1のライブラリを手に入れます。

ダウンロードサイトが見つからない場合はこちらにアップロードしました。(いいのかなこれ、何か問題ありましたら教えてください。)

 

axis1.4のライブラリへクラスパスを指定し次のコマンドでコードをジェネレートします。

set classpath=..lib*
java -cp %classpath% org.apache.axis.wsdl.WSDL2Java -a partner.wsdl

出力されたファイルをコンパイルし、実行時にaxis1.4のライブラリと共にクラスパスに含めます。

httpproxyの設定

通信時にhttpproxyを使う場合は、次のようにシステムプロパティをセットします。システムプロパティなので、javaコマンドの引数からも指定可能です。

      Properties systemprops = System.getProperties();
systemprops.setProperty("http.useProxy", "true");
systemprops.setProperty("http.proxyHost", "samplehost");
systemprops.setProperty("http.proxyPort", "9999");
systemprops.setProperty("http.proxyUser", "sampleproxyuser");
systemprops.setProperty("http.proxyPassword", "sampleproxypasword");

エンドポイントの設定

本番やSandBoxのapiのエンドポイントを指定します。何も指定しないと、wsdlに記載のものを使います。

        soapBinding._setProperty(Stub.ENDPOINT_ADDRESS_PROPERTY, "https://login.salesforce.com/services/Soap/u/39.0");

インスタンスURLとセッションIDのセット

login後に、セッションIDとインスタンスURLが返却されます。インスタンスURLでエンドポイントを再設定し、セッションIDをSOAP Headerにセットします。

        String serverUrl = loginResult.getServerUrl();
soapBinding._setProperty(Stub.ENDPOINT_ADDRESS_PROPERTY, serverUrl);

SessionHeader sh = new SessionHeader();
sh.setSessionId(loginResult.getSessionId());
soapBinding.setHeader((new SforceServiceLocator()).getServiceName().getNamespaceURI(), "SessionHeader", sh);

transportにhttpcommons3を使う

axis1.4の標準のtransportはaccept-Encodingでgzipに対応していないので、httpcommons3を使うように変更します。

client-config.wsddファイルに次の記載をします。

<?xml version="1.0" encoding="UTF-8"?>
<deployment name="defaultClientConfig"
xmlns="http://xml.apache.org/axis/wsdd/"
xmlns:java="http://xml.apache.org/axis/wsdd/providers/java">
<globalConfiguration>
<parameter name="disablePrettyXML" value="true"/>
<parameter name="enableNamespacePrefixOptimization" value="false"/>
</globalConfiguration>
<transport name="http" pivot="java:org.apache.axis.transport.http.CommonsHTTPSender"/>
<transport name="https" pivot="java:org.apache.axis.transport.http.CommonsHTTPSender"/>
<transport name="local" pivot="java:org.apache.axis.transport.local.LocalSender"/>
<transport name="java" pivot="java:org.apache.axis.transport.java.JavaSender"/>
</deployment>

そして、実行時のjavaコマンドのシステムプロパティで、

-Daxis.ClientConfigFile=client-config.wsdd

を指定します。

 

以上を行うことで、上記コードのsoapBindingは、Salesforce APIとやりとりができる状態になります。

Java: 色々なJavaライブラリでSalesforceAPIを使う

SOAP APIJavaクライアントには、色々なライブラリがあります。それぞれのJavaライブラリでSalesforce APIを使ってみた結果をまとめてみます。

サマリ

Java ライブラリ SOAP API Metadata API xsd:intにバインドされる型 XML中の不正な制御コード データバインディングの性能
Axis1.4 × Integer エラー 普通
Axis2 adb × × Integer エラー
xmlbeans int(対応するboolean変数で値がセットされたかどうかは判定可能) エラー やや早い
wsimport Integer エラー 普通
apache cxf Integer やや遅い
wsc int(対応するbooleanで値がセットされたかどうか判定可能だが、変数がprivateであり値を取得するためにリフレクションが必要) 除去する やや早い

 

比較軸について

  • SOAP API、METADATA APIが使えるかどうかにしました。”○”が対応していて、”×”が対応していません。
    また、SOAP APIはパートナーWSDLを対象としています。
  • xsd:intをJavaコードにした際にバインドされるクラスを書きました。理由は、xsd:int の値でSalesforceからnullが返却される時があるからです。
    Java側でIntegerではなくintでうけると、nullは0となります。nullに意味がある場合は注意が必要となります。
  • データバインディングの性能を記載しました。300項目、300行を返却するSOQLをqueryし、結果を取得するまでかかった時間で測定しています。各ライブラリで計測した時間は次の通りでした。apache cxfがやや遅いです。
Java ライブラリ 1回目(ms) 2回目(ms) 3回目(ms) 平均(ms)
axis1.4 5726 5196 5165 5362.33
aixs2(xmlbeans) 5054 4212 4571 4612.33
wsimport 5928 6037 5819 5928
apache cxf 7572 7894 7474 7646.67
wsc 4181 4508 4358 4349
実行環境:
java : jdk8 32bit u121
axis1.4 : axis1.4_7 , commons-httpclient-3.1
wsimport : version 2.2.9 , JAXWS2.2.7
apache cxf : 2.7.18
wsc : force-wsc-27.0.0.jar , gson-2.8.0.jar , rhino-1.7.7.1.jar

 

   Axis1.4

  • 古いけど、使いやすいです。(特にSObject型が扱い易いです。queryの結果や、insert/upsert/updateでSObjectを使う際に扱い易さが実感できます)
  • 残念なことにMetadataAPIをJavaクラスに変換するとエラーが出るようになってしまいました。(APIVer32までは問題なく変換できていました)。
  • 「Accept-Encoding: gzip 」を使うためには、commons http3.Xが必要です。Commons http3.Xをclasspathに含めて、client-config.wsddファイルで、transportをCommonsHTTPSenderに変更します。

   Axis2

  • Axis2には、JavaClientを作る方法が複数あって、adbとxmlbeansを記載しました。(他にもjaxbriというのがありましたが、SOAP APIのコードジェネレータでエラーが発生してしまい解決できませんでした。)
  • adbはwsdlのextensionが対応してなくて両方×でした。
  • Axis2のxmlbeansは、コードジェネレータでjavaコードとxsbという拡張子のバイナリファイルが出力されます。そのxsbファイルも実行時に必要となります。
  • Axis2のxmlbeansでコードを生成すると、細かく制御ができるのですが、コード量が他に比べて圧倒的に多くなります。

   wsimport

  • Jdkに標準ではいっているコードジェネレータコマンド
  • Webサービスコールの初回で、wsdlファイルを探しに行ったり、標準のままではエンドポイントを変更できなかったりします。
  • 上記のようにwsdlを探しにいくので、salesforceの場合は、jarにwsdlを含めておく必要があります。
  • 標準のままではエンドポイントを変更できないので、 (https://mvnrepository.com/artifact/com.sun.xml.ws/jaxws-rt)から追加のライブラリを入手が必要です。

   apache cxf

  • jaxbやxmlbeansがデータバインディングに使えます。
  • 細かくオプションが用意されていて使いやすい(んだと思います)。
  • wsimportと同様にwsdlを探しにいくので、salesforceの場合は、jarにwsdlを含めておく必要があります。
  • apache cxf2.7とapache cxf3.1を比較すると、cxf3.1の方がデータバインディングがかなり遅くなるので、cxf2.7がおすすめです。

   wsc (Force.com Web Services Connector)

  • salesforceのWebServive用のコードジェネレータ、現時点ではスタンダードなライブラリで、Salesforceのヘルプサイトのサンプルコードもこのライブラリで書かれています。
  • 色々な所で工夫されていてコード量が少なくてすみます。
    例えば、login後に、エンドポイントの変更や、セッションIDのセットを自動で行ってくれたりします。
  • データの登録(insert,upsert,update)の際に、partner.wsdlであってもクライアント側で項目の型とセットする型をチェックしてくれます。項目の型とセットする型は次のチェックです。
    日付型:java.util.Calendar
    日付/時間型:java.util.Calendar
    チェックボックス型:java.lang.Boolean
    int型:java.lang.Integer
    base64型:byte[]
  • xsd:intがJavaのプリミティブintにバインドされますが、nullだったかどうかが通常では判断できません。(private変数に{フィールド名}__is_setがあり、値がセットされたかは判断できるので、リフレクションを使うことで判断はできますが、無理矢理なやり方だと思います)

次回からそれぞれについて、細かくコードを紹介したいと思います。

 

補足

各ライブラリでのSalesforceへレコードを作成・更新する際のSobjectへの項目値の指定の仕方は次の記事に書きました。

crmprogrammer38.hatenablog.com

 

Salesforce: apex:outputText で 日付/時間型はGMTで表示される

outputTextでは、日付/時間型の項目がGMTで表示される仕様ではまりました。

時間まで出力していれば、ずれてるのは比較的気づくのですが日付までしか表示していなかったため発見に時間がかかりました。。

 

[日付がずれる書き方]

<APEX:OUTPUTTEXT value="{0, date, yyyy/MM/dd}">
    <APEX:PARAM value="{!items.datetimefield }"></APEX:PARAM>
</APEX:OUTPUTTEXT>

 [日付がずれない書き方]

<APEX:OUTPUTTEXT value="{0, date, yyyy/MM/dd}">
    <APEX:PARAM value="{!items.datetimefield +9/24}"></APEX:PARAM>
</APEX:OUTPUTTEXT>

 

上記のように9/24を足すことで9時間の補正となります。

ユーザ別の各タイムゾーンに対応したい場合は、apex class内でDatetime.formatでフォーマットした文字列を表示をしたほうがいいです。

 

というのは、yyyy/MM/dd などのフォーマット自体が各地域で異なりますので。

Salesforce: JavaScript FileReaderとsforce.connectionを使って添付ファイルを登録する。

写真を保存したり、書類のPDFを保存したりなどSalesforceにファイルを保存することは多いと思います。

 

写真はプレビューで確認してから登録したり、複数ファイルをまとめて登録する時など、ファイルを登録する際に便利なのがJavaScriptのFileReaderだと思いますが、FileReaderを使って添付ファイルを登録する際のメモです。

 

<script src="/soap/ajax/39.0/connection.js"></script>


<script>

var attachments = [];

//onchangeなどでファイル名とファイルデータを取得後呼ばれる
function fileload(filename,filedata){

var attachment = new sforce.SObject("Attachment");
attachment.parentId = {!parentId}; //どこかのオブジェクトID
attachment.name = filename;

reader.onload = function(e) {

var bytes = new Uint8Array(reader.result);
var binaryText = '';
for (var index = 0; index < reader.result.byteLength; index++) {
binaryText += String.fromCharCode(bytes[index]);
}
attachment.body = window.btoa(binaryText);
attachments.push(attachment);
};

reader.onloadend = function () {
//ここで登録ボタンを押せるような制御を行う
}
reader.readAsArrayBuffer(filedata);
}

//ファイル登録ボタンなどから呼ばれる
function createAttachment(){
sforce.connection.sessionId = "{!$Api.Session_ID}";
var result = sforce.connection.create(attachments);

//登録後の処理
}
</script>

Salesforceの添付ファイル(Attachment)のbodyには、base64エンコードした文字列をセットする必要があるのがポイントです。

Salesforce: キューをデータローダで作成する。

キューをデータローダで作成したい

キューもかなりの数になると画面から設定で作成するのは大変ですし、ミスも増えてくると思います。そこでデータローダでキューを作成する手順の紹介です。

 

[キューに関連するデータモデル]

f:id:crmprogrammer38:20170609142039p:plain

オブジェクトとキューの登録画面のセクションとの対応は次の通りです。

  • グループ:キュー名とアドレス
  • キューのSオブジェクト:サポートされるオブジェクト
  • グループメンバー:キューメンバー

 

[データローダでの登録手順]

 

1) グループに登録する。登録したキューに振られたIDを取得する。

2)1)で取得したキューのIDとSオブジェクト種別(LeadやCase、カスタムオブジェクトなど)でキューのSオブジェクトを登録する。

3)1)で取得したキューのIDとユーザID/公開グループやロールIDで、グループメンバーを登録する。

 

[補足]

キューを作ると、勝手にそのキューに対応するビューが作成されてしまいます。