プログラマ38の日記

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

Java: いまさらになってStream APIをさわってみた

今さらですが、java8で追加されたStream APIをさわってみました。

自分でコードを書く際には使いませんが、別の人のコードを読むときや、Stream APIを使って書かれたコードを修正する時には知ってたほうがいいかなーと思っていて、ざっくりと理解しようとした時のメモです。

※なので、全てを把握してはいません。

 

  • リスト、マップを便利に使える仕組み、forを使って色々処理していることをメソッドとしてコードが書ける
  • メソッドには、各要素に対して操作するもの、要素全体に対して操作するものがある

 

forで定型的なプログラムを書く手間を省くための仕組みだと理解しました。

よくあるリストを使った処理を想定してかき分けてみたいと思います。

 

処理1:

リストの要素をSQLの in の検索条件として使う

    

//このリストを検索条件にしたい

List<String> searchlist = Arrays.asList("レキシ","B'z","サカナクション");





//streamを使った場合

String searchstr = searchlist.stream()

         .map(s-> "'" + s.replaceAll("'", "''") + "'")

         .collect(Collectors.joining(", ", "searchfield in (", ")"));







//今まで通りのfor文を使った場合

StringJoiner tempjoiner = new StringJoiner(", ", "searchfield in (", ")");

for(String s : searchlist) {

    tempjoiner.add("'" + s.replaceAll("'", "''") + "'");

}
String searchstr2 = tempjoiner.toString();

どちらで書いても「searchfield in ('レキシ', 'B''z', 'サカナクション')」の、SQLのWhere句の文字列となります。

上記のポイントとしてシングルクオートはエスケープしています。値の中にシングルクオートを持つ名詞を探したらB'zを見つけたので、ミュージシャンでリストを作成してみました。

streamを使っても使わなくてもあまり変わらないように思います。StringJoinerを使ってるせいかもしれませんが。

 

処理2:

リストの要素をフィルタして別のリストを作る

// 事前準備 start ********************************** 

//こんなクラスを用意

public class UserInfo {

    String username;

    boolean isActive;

    

    public UserInfo(String username, boolean isActive) {

        this.username = username;

        this.isActive = isActive;

    }

}



//こんなリストを用意

UserInfo u1 = new UserInfo("A", true);

UserInfo u2 = new UserInfo("B", false);

UserInfo u3 = new UserInfo("C", true);



List<UserInfo> users = Arrays.asList(u1,u2,u3);

// 事前準備 end  ********************************** 





//streamを使った書き方

List<UserInfo> activeUsers = users.stream()

        .filter(u-> u.isActive == true)

        .collect(Collectors.toList());

List<UserInfo> inactiveUsers = users.stream()

        .filter(u-> u.isActive == false)

        .collect(Collectors.toList());





//for文を使った書き方

List<UserInfo> activeUsersF = new ArrayList<>();

List<UserInfo> inactiveUsersF = new ArrayList<>();



for(UserInfo u : users) {

    if( u.isActive == true ) activeUsersF.add(u);

    if( u.isActive == false ) inactiveUsersF.add(u);                

}    

プログラムでこういうフィルタがしたいということが明確で、streamだとわかりやすさがあります。

 

最後に

上記のメモでは、Streamのmap, collect, filterメソッドだけを使っていますが、Streamにはたくさんのメソッドが用意されていて、例えば、ソート用のsorted、それぞれの要素で処理を行うforEachなどがあります。

さらにcollectメソッドで使うCollectorsクラスには、返却するリストやマップ、文字列をさらに細かく指定できるメソッドが用意されています。StreamとCollectorsを抑えればStream APIで困ることはないかなと考えています。

 

あと、なんでもそうですが、できるからといって技を駆使してプログラムを書くとメンテナンスできないものになります。(perlとかは最たる例だと思います。)

streamは仕様変更には弱そうだなと直感的に感じていて、普通の人が普通にプログラム修正するには癖が強いと思いました。

実際に書く際には、ここまでは利用してここから先は利用しない、もしくはこういった処理だけ利用するなどの線引きが必要だなーと思いました。

 

雑記: 個人環境でhttpプロキシを使いたい時はSquidを使おう

Excel VBAでプログラムを書いておきたいなと思い、今までにいくつかExcel VBAのプログラムを書いてきました。

せっかくなのでSalesforceAPIを操作して楽ができるツールがいいなと思い書いてきたのですが、通信制御のところで不具合があることがわかりました。もう今は全て修正したのですが、不具合は次のものです。

 

エラー内容

「httpプロキシで、Basic認証をしている環境で、システムエラーが発生する。」

 

具体的には次のコードが駄目です。(少しシンプルにしています)

    Set http = CreateObject("WinHttp.WinHttpRequest.5.1")

    http.setProxy 2, proxyhost & ":" & proxyport, ""

    http.setCredentials proxyusername, proxypassword, 1

    http.Open "POST", sfendpoint, False

上の「setCredentials 」で、httpプロキシのBasic認証のためのユーザ、パスワードを指定しているのですが、この関数は「Open」関数の後に呼び出しをしないとエラーとなります。なので、正しくは、次のようになります。

    Set http = CreateObject("WinHttp.WinHttpRequest.5.1")

    http.setProxy 2, proxyhost & ":" & proxyport, ""

    http.Open "POST", sfendpoint, False

    http.setCredentials proxyusername, proxypassword, 1

いつもhttpプロキシを使わずに通信する環境で作っているためhttpプロキシを経由して通信するテストができていなかったことが原因です。

なので環境を用意してテストできるようにしたいと思いました。

 

 

ローカル環境のhttpプロキシ

ローカルにhttpプロキシを立てるには「Squid」というツールが便利で、情報も揃っています。(ただバージョンの違いで設定も多少違うようです)

インターネットにアクセスするプログラムをローカルで開発する際に、httpプロキシ経由、そしてBasic認証のテストをする時にとても便利です。

使い方は検索すればたくさん出てくると思いますが、Windows10でインストールした時のメモです。

  • Squid.msiをダウンロード(Squid-3.5 64bit)してインストール
  • インストール後、[インストールフォルダ]etcsquidsquid.conf を変更(※1)
  • ユーザ/パスワード用のファイルを用意(※2)
  • インストール後、タスクトレイから「Start Squid Service」 (既にStartしていた場合は、StopしてからStart)
    f:id:crmprogrammer38:20180105194310p:plain
  • ブラウザのプロキシ設定をしてインターネットアクセス時にユーザ/パスワードを求められればOK

※1  とりあえずこれで動いている詳細を把握して設定していないです。

#

# Recommended minimum configuration:

#



auth_param basic program /lib/squid/basic_ncsa_auth /etc/squid/.htpasswd

auth_param basic children 5

auth_param basic realm Squid proxy-caching web server

auth_param basic credentialsttl 2 hours

auth_param basic casesensitive off





# Example rule allowing access from your local networks.

# Adapt to list your (internal) IP networks from where browsing

# should be allowed



acl localnet src 10.0.0.0/8	# RFC1918 possible internal network

acl localnet src 172.16.0.0/12	# RFC1918 possible internal network

acl localnet src 192.168.0.0/16	# RFC1918 possible internal network

acl localnet src fc00::/7       # RFC 4193 local private network range

acl localnet src fe80::/10      # RFC 4291 link-local (directly plugged) machines

acl localnet src 192.168.1.0/255.255.255.0



acl SSL_ports port 443

acl Safe_ports port 80		# http

acl Safe_ports port 21		# ftp

acl Safe_ports port 443		# https

acl Safe_ports port 70		# gopher

acl Safe_ports port 210		# wais

acl Safe_ports port 1025-65535	# unregistered ports

acl Safe_ports port 280		# http-mgmt

acl Safe_ports port 488		# gss-http

acl Safe_ports port 591		# filemaker

acl Safe_ports port 777		# multiling http

acl CONNECT method CONNECT

acl password proxy_auth REQUIRED





visible_hostname PCNAME



#

# Recommended minimum Access Permission configuration:

#



# Only allow cachemgr access from localhost

http_access allow localhost manager

http_access deny manager

http_access allow localnet

http_access allow password



# Deny requests to certain unsafe ports

http_access deny !Safe_ports



# Deny CONNECT to other than secure SSL ports

http_access deny CONNECT !SSL_ports



# We strongly recommend the following be uncommented to protect innocent

# web applications running on the proxy server who think the only

# one who can access services on "localhost" is a local user

#http_access deny to_localhost



#

# INSERT YOUR OWN RULE(S) HERE TO ALLOW ACCESS FROM YOUR CLIENTS

#



# Example rule allowing access from your local networks.

# Adapt localnet in the ACL section to list your (internal) IP networks

# from where browsing should be allowed

http_access allow localnet

http_access allow localhost



# And finally deny all other access to this proxy

http_access deny all



# Squid normally listens to port 3128

http_port 3128



# Uncomment the line below to enable disk caching - path format is /cygdrive/, i.e.

#cache_dir aufs /cygdrive/d/squid/cache 3000 16 256





# Leave coredumps in the first cache dir

coredump_dir /var/cache/squid



# Add any of your own refresh_pattern entries above these.

refresh_pattern ^ftp:		1440	20%	10080

refresh_pattern ^gopher:	1440	0%	1440

refresh_pattern -i (/cgi-bin/|?) 0	0%	0

refresh_pattern .		0	20%	4320



dns_nameservers 8.8.8.8 208.67.222.222



max_filedescriptors 3200



 ※2 ユーザ/パスワードをroot/rootとした時のBasic認証のファイル(.htpasswd)の中身

root:/0ZQd5ttqDR8w

 

最後に

ローカルにhttpプロキシの環境が作成できてとても有難いなと思いました。

本番の通信環境とは異なる環境で開発することの方が多いので本番の通信環境に近い形でテストができるのはとても助かります。

雑記: Windowsのバッチ開発で便利なコマンド

Windowsサーバでバッチ処理を開発したり、バッチ処理の稼動を確認する時に便利だなと思ったコマンドのメモです。

 

よく使うコマンド(引数込み) コマンドで実現できること 利用したい時 備考
CERTUTIL -decode  [intputfile] [outputfile] base64形式のテキストファイル[intputfile]からバイナリファイル[outputfile]へ変更する。 Salesforceのデータローダで添付ファイルをCSV出力した後、CSV内の添付ファイル内容(base64形式)をファイルに変換する。

Windodws標準コマンド

 

もちろんCSV出力時にはヘッダやダブルクオテーションなど不要な文字が含まれているので純粋にbase64部分だけのテキストにする必要はあります。

xcopy /T /E /I [inputdirectory] [outputdirectory] ディレクトリの構成だけをコピーする。 中に含まれているファイルのサイズが大きい、またはファイルの数が多い場合に、ディレクトリの構造だけコピーする。 Windodws標準コマンド
[標準出力に表示するコマンド] | clip コマンドの標準出力をクリップボードにコピーする。 例えば「 dir /b | clip」などでファイルの一覧をクリップボードへコピーする。 Windodws標準コマンド
tail -n50 -f [inputfile] ファイルの末尾を表示する。 ログファイルの確認をする。 gnuコマンド
cygwinなどでコマンドのインストールが必要

利用する際には環境変数「LANG」に「ja_JP.SJIS」を指定するとShift_JISのファイルが文字化けしません。
[標準出力に表示するコマンド] | tee -a [outputfile] 標準出力をそのまま表示しつつ、ファイルにも出力する。 コマンドの実行結果を標準出力で確認しつつ、ログファイルにも保存する。 gnuコマンド
cygwinなどでコマンドのインストールが必要

 

unix系でバッチ処理の開発を行う際には当たり前のように使うコマンドがWindowsだと使えなくて不便なのですが、cygwinなどでgnuコマンドを使えばもどかしい思いをしなくてすみます。(もちろん完全に問題なく動作するわけではないので、確認しながらになりますが)

cygwin使うならbase64コマンドもあるからCERTUTILもいらないんですけど、標準で使えるのでいいかなと思います。

 

最後に

PowerShell使えばいいんだと思いますが、今さら感が強くて覚える気にならないんです。PowerShell駆使したいなら、最初からunixでいいのでは?と思ってしまいますし。

Java: webserviceクライアント別のWSDL complexTypeのany要素の使い方

SalesforceのPartner WSDLSOAP APIを使う時、sObjectのフィールド値をセットする時の書き方が各ライブラリによって大分違っています。

 

書き方の違いは、WSDLでの、complexType で anyの要素が各ライブラリで使い方が違うところに起因します。

 

各ライブラリでの書き方は次の通りとなります。ライブラリは、axis1.4、axis2、 wsimport、apache cxf、wscを対象とします。

 

axis1.4

import org.apache.axis.message.MessageElement;
import com.sforce.soap.partner.sobject.SObject;
import javax.xml.namespace.QName;
-------------------------------------------------

SObject sobject = new SObject();
sobject.setType("Sample__c");

MessageElement f1 = new MessageElement();
f1.setQName(new QName("Field1__c"));
f1.setValue("SampleValue1");

MessageElement f2 = new MessageElement();
f2.setQName(new QName("Field2__c"));
f2.setValue("SampleValue2");

sobject.set_any(new MessageElement[]{f1, f2});

axis2 (xmlbeans)

import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.Element;
import com.sforce.soap.partner.sobject.SObject;
----------------------------------------------------

SObject sobject = SObject.Factory.newInstance();
sobject.setType("Sample__c");

Document sobjdoc = sobject.getDomNode().getOwnerDocument();

Element f1 = sobjdoc.createElement("Field1__c");
Node v1 = sobjdoc.createTextNode("SampleValue1");
f1.appendChild(v1);

Element f2 = sobjdoc.createElement("Field2__c");
Node v2 = sobjdoc.createTextNode("SampleValue2");
f2.appendChild(v2);

sobject.getDomNode().appendChild(f1);
sobject.getDomNode().appendChild(f2);

wsimport・apache cxf

import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import com.sforce.soap.partner.sobject.SObject;
--------------------------------------------------------------

SObject sobject = new SObject();
sobject.setType("Sample__c");

Document doc = null; 
try{
  doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
} catch(Exception ignored){}

Element f1 = doc.createElement("Field1__c");
f1.appendChild(doc.createTextNode("SampleValue1"));

Element f2 = doc.createElement("Field1__c");
f2.appendChild(doc.createTextNode("SampleValue1"));

sobject.getAny().add(f1);
sobject.getAny().add(f2);

wsc

import com.sforce.soap.partner.sobject.SObject;
------------------------------------------------------

SObject sobject = new SObject();
sobject.setType("Sample__c");

sobject.addField("Field1__c" , "SampleValue1");
sobject.addField("Field2__c" , "SampleValue2");

 

最後に

各ライブラリによってだいぶ書き方が違うなーと思います。

 

wscはSalesforceに最適化されているので、Salesforceの項目の型に合わせてセットするJavaの型が異なります。

Salesforce:date・datetime  ⇔ Java:Calendar

Salesforce:int ⇔ Java:Integer

Salesforce:boolean ⇔ Java:Boolean

Salesforce:base64Java:byte[]

それ以外は文字列となります、文字列中にxmlで使用不可の文字(制御コードなど)はライブラリ側が自動で取り除いてくれます。

 

wsc以外は、全ての項目は文字列となりますが、Salesforceの型に合わない文字列を指定すると、apiコールそのものがエラーとなります。

Salesforceの各型へセットする文字は、次の通りとなります。

Salesforce : date ⇒ yyyy-MM-dd

Salesforce : datetime ⇒ yyyy-MM-dd'T'HH:mm:ss'.000'Z

Salesforce : 数値の項目 ⇒ 数値に変換可能な数字

Salesforce : booelan ⇒ true or false

Salesforce : base64base64変換後の文字列

それ以外は文字列となりますが、文字列中にxmlで使用不可の文字(制御コードなど)は自動で取り除いてくれないので、自身で取り除く必要があります。

 

Java: JDK9 jshellを使ってみたメモ

jdk9でjshellが追加されました。

簡単なバッチプログラムをjavaで書くことが多いのですが、コンパイルしたclassファイルよりも、テキストのjavaコードをそのまま実行するほうが好都合な時があります。

 

例えば、後で変更が見えてるような場合、柔軟に変更に対応できる設定ファイルの仕様を考えるより、プログラムファイル自体をテキストで用意する方が簡単です。

 

テキストのjavaコードを実行する仕組みとして、BeanShellを使っていましたが、使い勝手が良ければ今後はjshellをメインに使っていこうかと思っています。

 

以下、jshellをバッチ処理用途として使ってみてわかったことのメモになります。

 

コマンド形式

jshell -s --execution local  <スクリプトファイル>

スクリプトファイルを実行する場合、最小限のコマンドは上記となります。
jshell -h とうつことで、コマンドの引数が一覧でわかります。

 

最小限として -s と --execution local は必須としました。

-sを指定することでフィードバックを最小限としています。

そして、--execution local を指定することで、スクリプトの戻り値を取得できるようにしています。--execution local を指定しない場合、スクリプト内でSystem.exit(1); としてもjshellを完了できません。(これ以降 --execution localの前提での記載となります)

さらにclasspathを追加する場合は、--class-path を使います。

 

スクリプトへ引数を渡す

jshellコマンドには、スクリプトファイルへ引数を渡す仕組みがありません。

システム変数か、環境変数で渡すことになります。

rem 1.環境変数の場合
set envvar1=test1

jshell -s --execution local

-> System.out.println( System.getenv("envvar1"));
test1
rem 2.システム変数の場合
jshell -s --execution local  -J-Dpropvar1=var2
-> System.out.println( System.getProperty("propvar1"));
var2

 

スクリプトファイルの文字コード

jshellの引数のスクリプトファイルは、Windowsであれば、Windows-31Jとなりますが、どうしてもutf8で実行したい場合は、システム変数で指定します。

jshell -s --execution local  -J-Dfile.encoding=utf8 <スクリプトファイル>

ただし、utf8のスクリプトファイルがBOM有だとエラーが出ます。

 

vm引数を指定する

メモリサイズ(-Xms -Xmx)などや -server オプションなどは、-J のオプションで指定します。既に上記でシステム変数として使っていますけど。。

jshell -s --execution local  -J-server -J-Xmx3G

--execution local を指定しているので -J のオプションですが、--execution local を指定しない場合、-R のオプションで指定します。

※--execution localを指定しないと、jshellの中でさらに別のvmを起動するようで、そのvmへのオプションは -R として指定するようです。

 

スクリプトの戻り値を取得する

--execution local を指定してjshellを起動し、System.exitで終了します。

jshell -s --execution local
-> System.exit(5);

echo %ERRORLEVEL%
5

 

スクリプト構文

BeanShellと大体同じだなーと思いました。多少違いがあって、スクリプト内の関数はstaticでは定義できなかったりします。BeanShellでは、ジェネリクスや可変引数が使用できませんでしたが、jshellでは使えるのが嬉しいです。

スクリプト内の関数は、呼び出す前に定義しておくことが必要、java.utilなどは最初からimportされているのはBeanShellと同じです。

//サンプルスクリプト
public void func1(){
  System.out.println("func1 called");
}

func1();

System.out.println(Arrays.asList("test1","test2","test3"));
System.exit(4);

 

スクリプト実行の性能

気になるのが、実行性能だと思います。jdk9でjrunscript , jshell BeanShell , コンパイル済みコードで比較してみたいと思います。

比較で使用したのは次のコードです。(フィボナッチ数列です。引数に40を指定しました)

  public int fibonacci(int n) {
       return ( n==1 || n==0 ) ? n:fibonacci(n-2) + fibonacci(n-1);
  }

 

結果は次の表の通りとなり、jshellは、コンパイル済みjavaとほぼ同じとなっています。(バイトコードを作成する仕組みなのかなと思いますが、詳しくことはわかりません)

BeanShellは、比較するととても遅いですが、性能を気にしなくていい時だけ使うので実用上はそこまで問題にはなりません。どちらかといえばnashornの性能の良さに目を見張りました。rhinoよりかなり良くなっていると思います。

実行プログラム 所要時間
(ミリ秒)
実行コード
java
コンパイル済み
661
   public void sample1() {
  long start = System.currentTimeMillis(); 
  int ret =fibonacci(40);
  long end = System.currentTimeMillis();
  System.out.println( end - start );
 }
 
 public int fibonacci(int n) {
     return ( n==1 || n==0 ) ? n:fibonacci(n-2) + fibonacci(n-1);
 }

 public static void main(String[] args) {
  new Sample().sample1();
 }
jshell 672
public void sample1() {
  long start = System.currentTimeMillis(); 
  int ret =fibonacci(40);
  long end = System.currentTimeMillis();
  System.out.println( end - start );
}
 
public int fibonacci(int n) {
     return ( n==1 || n==0 ) ? n:fibonacci(n-2) + fibonacci(n-1);
}

sample1();

jrunscript

[nashorn]

1488
var lib = new JavaImporter(java.lang);
with(lib){
  function sample1() {
    var start = System.currentTimeMillis(); 
    var ret =fibonacci(40);
    var end = System.currentTimeMillis();
    System.out.println( end - start );
  } 
  function fibonacci(n) {
     return ( n==1 || n==0 ) ? n:fibonacci(n-2) + fibonacci(n-1);
  }

  sample1();
}
BeanShell 389118
public void sample1() {
    long start = System.currentTimeMillis();
    int ret =fibonacci(40);
    System.out.println(ret);
    long end = System.currentTimeMillis();
    System.out.println( (end - start) );
}
public int fibonacci(int n) {
   return ( n==1 || n==0 ) ? n:fibonacci(n-2) + fibonacci(n-1);
}

sample1();

※性能は、同じPCで同じJDK9で実施しました。

最後に

BeanShellだとジェネリクスや可変長引数が使えなかったりして構文エラーにつまづくことがあります。

最新のJava構文が利用できて、性能面も問題ないのでjdk9が普及してきたらjshellは積極的に使っていきたいなと思いました。でも当面はjdk8を使うと思うので、BeanShellを使っていきます。

 

Salesforce: apache httpclient / commons http を使ってREST APIでファイル(ContentVersion)を登録する

Salesforceへバイナリファイルを連携するには、通常はデータローダをはじめとするSOAP APIを使いbase64変換した文字列を送信します。

 

ファイル(ContentVersion)は2GBと大きいサイズでも登録が可能ですが、SOAP APIでは、Base64変換後のサイズで50MBのサイズ制限があります。

Base64変換では1割以上サイズが増えるため物理サイズとしてはもっと小さくなります)

 

ですが、REST APIではその制限はなく50MBより大きなサイズでも登録できます。SOAP APIだとできなくて、REST APIだとできることの1つだと思います。

 

javaプログラムで、REST APIを使ったファイルを登録するサンプルを作りました。

また、サンプルでは、apache httpclient 4.5.3と、commons http 3.1を使ったプログラムを作成してみました。

 

apache httpclient 4.5.3を使ったサンプル

   (さらに、SOAP API用にwsc、Json用にGsonを利用しています。)

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.util.HashMap;

import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.impl.client.HttpClientBuilder;

import com.google.gson.Gson;
import com.sforce.soap.partner.Connector;
import com.sforce.soap.partner.LoginResult;
import com.sforce.soap.partner.PartnerConnection;
import com.sforce.ws.ConnectorConfig;


public class MainHTTPClient4 {

  public static void main(String[] args) throws Exception{
    
    ConnectorConfig cc = new ConnectorConfig();
    cc.setManualLogin(true);
    PartnerConnection pc = Connector.newConnection(cc);
    
    LoginResult lr = pc.login("sampleuser@sample.user", "samplepassword");
    
    String url =lr.getServerUrl();
    String sessionId = lr.getSessionId();

    HttpPost post = new HttpPost(url.substring(0, url.indexOf("/services")) + "/services/data/v41.0/sobjects/ContentVersion");
    post.addHeader("Authorization", ":Bearer " + sessionId);
    
    File file = new File("登録したいファイル");
    
    HashMap<String, String> contentvalue = new HashMap<String, String>();
    contentvalue.put("Title","sampletitle");
    contentvalue.put("SharingOption","A");
    contentvalue.put("SharingPrivacy","N");
    contentvalue.put("PathOnClient",file.getName());
    contentvalue.put("ContentLocation","S");
    String jsonstr =new Gson().toJson(contentvalue);
    
    MultipartEntityBuilder entity = MultipartEntityBuilder.create();
    entity.addTextBody("entity_data", jsonstr,ContentType.APPLICATION_JSON);
    entity.addBinaryBody("VersionData", file);
    
    post.setEntity(entity.build());
    
    HttpClient client = HttpClientBuilder.create().build();
    HttpResponse response = client.execute(post);

    ByteArrayOutputStream byteout = new ByteArrayOutputStream();
    
    response.getEntity().writeTo(byteout);
    
    System.out.println( new String(byteout.toByteArray()) );

  }
}

 

commons http 3.1を使ったサンプル

(さらに、SOAP API用にwsc、Json用にGsonを利用しています。)

import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;

import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.multipart.FilePart;
import org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity;
import org.apache.commons.httpclient.methods.multipart.Part;
import org.apache.commons.httpclient.methods.multipart.StringPart;

import com.google.gson.Gson;
import com.sforce.rest.ContentType;
import com.sforce.rest.RestConnection;
import com.sforce.soap.partner.Connector;
import com.sforce.soap.partner.LoginResult;
import com.sforce.soap.partner.PartnerConnection;
import com.sforce.ws.ConnectorConfig;


public class MainCommonsHttp3 {

  public static void main(String[] args) throws Exception{
    ConnectorConfig cc = new ConnectorConfig();
    cc.setManualLogin(true);
    PartnerConnection pc = Connector.newConnection(cc);
    
    LoginResult lr = pc.login("sampleuser@sample.user", "samplepassword");
    
    String url =lr.getServerUrl();
    String sessionId = lr.getSessionId();
    
    HttpClient client = new HttpClient();
    
    PostMethod post = new PostMethod(url.substring(0, url.indexOf("/services")) + "/services/data/v41.0/sobjects/ContentVersion");
    post.setRequestHeader("Authorization", ":Bearer " + sessionId);

    File file = new File("登録したいファイル");
    
    HashMap<String, String> contentvalue = new HashMap<String, String>();
    contentvalue.put("Title","test1");
    contentvalue.put("SharingOption","A");
    contentvalue.put("SharingPrivacy","N");
    contentvalue.put("PathOnClient",file.getName());
    contentvalue.put("ContentLocation","S");
    String jsonstr =new Gson().toJson(contentvalue);
    
    StringPart contentrec = new StringPart("entity_data",jsonstr);
    contentrec.setContentType("application/json");
    FilePart fp = new FilePart("VersionData", file);
    fp.setContentType("application/excel");
    fp.setCharSet("binary");

    Part[] parts = new Part[] { contentrec,fp };
    post.setRequestEntity(new MultipartRequestEntity(parts, post.getParams()));

    client.executeMethod(post);
    
    System.out.println( new String(post.getResponseBody()) );
  }
}

 

最後に

実はcommons http3.1だけでサンプルプログラムを作って試していたのですが、commons http3.1って相当古いよなーと思い、新しいライブラリで試したので2つのサンプルコードとなっています。

commons http3.1とapache httpclient 4.5.3では、バージョンアップとかそういう次元ではなく別物なんですね。。そもそも名前が変わっていますし。apache httpclient 4.5.3の方が心なしかすっきりしているような気がします。

 

また、REST APIなら大き目のファイルをアップロードできますと書いてはみたもの1GB とかのファイルでは試していません。(200MB程度のファイルは登録してみました。)

後、画面から登録するより多少遅い気がします。

Salesforce: Rest API で思ったこと

SalesforceRest APIで思ったことのメモです。

 

まずSalesforceRest APIは次の特徴を持ちます。(個人的に思っていることです。。詳細な仕様はSalesforceのヘルプを参照ください)

 

  • httpsの通信ができれば、SalesforceAPIのやり取りが可能
  • APIの送受信の形式はJSON文字列かXMLか選べる
  • REST APIでのみ提供されている機能がある

 

httpsの通信ができれば、SalesforceAPIのやり取りが可能

TSL1.1以上でという制限はありますが、httpsの通信ができれば、SOAP APIのようにwsdlソースコードに展開する仕組みがなくても簡単に利用できます。

なので、VBScriptや、Excelマクロ、PowerShellInvoke-RestMethod
)から簡単に利用できます。

 

REST API以外のSalesforceAPIを利用するには、ログインURLとログイン情報(ユーザ、パスワード)を指定してログインし、ログイン後Salesforceから返却されるインスタンスURLとセッションIDを使用してメソッドをコールします。

 

REST APIでも「https://login.salesforce.com/services/oauth2/authorize?response_type=code&client_id=XXX&redirect_uri=YYY&state=mystate」というREST APIでの認証が用意されています。REST APIでの認証をするためには、コンシューマ鍵を別途設定して取得する必要があります。

 

ですが、認証後で必要な情報はセッションID(RESTではaccess_tokenという名前)とインスタンスURLなので、無理にコンシューマ鍵を用意してREST APIで認証する必要はありません。通常のSOAP APIでログインして、セッションIDとインスタンスURLを取得すればいいだけです。

APIの送受信の形式はJSON文字列かXMLか選べる

REST APIのリクエストとレスポンスの形式はJSON文字列かXMLを選べます。JSONのパーサーが利用できればJSONの方が使いやすいと思います。

デフォルトの形式はJSONですが、リクエストヘッダに「Accept: application/xml」とするか、リクエストのURLに「.xml」の拡張子をつけることでXMLに変更できます。

(URLでXMLを指定する例:https://apX.salesforce.com/services/data/v41.0/query.xml?q=~)

REST APIでのみ提供されている機能がある

SOQLやビューの実行プランはREST APIでのみ取得可能という記事を書いています。(記事内容は実行プランを取得するExcelマクロを作成したという内容です)

それ以外にも、大きなバイナリデータを取得する時は、SOAP APIよりREST APIの方が適している時があります。

静的リソース(StaticResource)、ドキュメント(Document)、添付ファイル(Attachment)は登録できるサイズが制限されているので、SOAP APIで取得できますが、ファイル(ContentVersion)やイベントログ(EventLogFile)は、サイズが大きくなってくるとSOAP APIで取得できない場合があります。

そんな時は、REST APIでバイナリデータを取得することになります。

 

例:ファイルの取得

https://apX.salesforce.com/services/data/v41.0/sobjects/ContentVersion/対象のSalesforceID/VersionData

例:イベントログの取得

https://apX.salesforce.com/services/data/v41.0/sobjects/EventLogFile/対象のSalesforceID/LogFile

 

上記のREST APIを実行すると、HTTPのレスポンスをストリームで受け取れるので大きなバイナリデータもファイルに出力することができます。

 

最後に

JavaREST APIを扱うサンプルプログラムを書いておきたいと思います。

ちょっと長いのですが、通常のhttpsの通信だけでSalesforceのファイル(ContentVersion)からファイルを取得する処理です。

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.URL;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.w3c.dom.Document;


public class RestSample {

  public static void main(String[] args) throws Exception{
    
    
    String loginendpoint = "https://login.salesforce.com/services/Soap/u/41.0";
    String loginusername = "sampleuser@sample.user";
    String loginpassword = "samplepassword";
    
    
    String loginxml =
        "<?xml version="1.0" encoding="UTF-8"?><env:Envelope                                                    " +
            "   xmlns:env="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsd="http://www.w3.org/2001/XMLSchema"  " +
            "   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">                                                " +
            " <env:Body>                                                                                                " +
            "  <m:login xmlns:m="urn:partner.soap.sforce.com" xmlns:sobj="urn:sobject.partner.soap.sforce.com">     " +
            "   <m:username>#sfusername#</m:username>                                                                   " +
            "   <m:password>#sfpassword#</m:password>                                                                   " +
            "  </m:login>                                                                                               " +
            " </env:Body>                                                                                               " +
            "</env:Envelope>                                                                                            " ;
    
    URL url = new URL(loginendpoint);
    
    HttpURLConnection loginhttp = (HttpURLConnection)url.openConnection();
    
    loginhttp.setRequestProperty("Content-Type", "text/xml; charset=utf-8");
    loginhttp.setRequestProperty("SOAPAction", "login");
    loginhttp.setRequestMethod("POST");
    
    
    String loginenv = loginxml.replace("#sfusername#", loginusername);
    loginenv = loginenv.replace("#sfpassword#", loginpassword);
    
    
    loginhttp.setDoOutput(true);
        OutputStreamWriter out = new OutputStreamWriter(loginhttp.getOutputStream());
        out.write(loginenv);
        out.close();
        loginhttp.connect();  
    
        final int status = loginhttp.getResponseCode();
        if (status != HttpURLConnection.HTTP_OK) {
          loginhttp.disconnect();
          throw new Exception("通信エラーが発生 ResponseCode:" +  status);
        }

          
      InputStream in = loginhttp.getInputStream();
        
        DocumentBuilder doc = DocumentBuilderFactory.newInstance().newDocumentBuilder();
        Document document = doc.parse(in);
        in.close();
        loginhttp.disconnect();
        
        
        String serverURL = null;
        String sessionId = null;
        try{
          sessionId = document.getElementsByTagName("sessionId").item(0).getTextContent();
          serverURL = document.getElementsByTagName("serverUrl").item(0).getTextContent();
        } catch(Exception e){
          throw new Exception("ログインで失敗:" +  e);
        }
        
    String instanceurl = serverURL.substring(0, serverURL.indexOf("/services"));
        
        String resturl = instanceurl + "/services/data/v40.0/sobjects/ContentVersion/対象のSalesforceID/VersionData";
    HttpURLConnection resthttp = (HttpURLConnection)new URL(resturl).openConnection();
  
    resthttp.setRequestProperty("Content-Type", "application/octet-stream");
    resthttp.setRequestProperty("Authorization", ":Bearer " + sessionId);
    resthttp.setRequestMethod("GET");
    
    resthttp.connect();
    
    
    
    InputStream responseStream = resthttp.getInputStream();
    
    BufferedInputStream bStream = new BufferedInputStream(responseStream);
    
    BufferedOutputStream bFileStream = new BufferedOutputStream(new FileOutputStream("保存するファイル名"));
    
    while(true){
      int val = bStream.read();
      
      if(val == -1){
        break;
      }
      
      bFileStream.write(val);
    }
    
    bStream.close();
    bFileStream.close();
  }
}

補足ですが、ContentVersionは、自分の参照できるファイルしかアクセスできません。例えば他の人がアップロードしたファイルはシステム管理者であってもContentVersionからは取得できません。

Chatterファイルは、自分が参加しているChatterGroupであれば取得可能です。