プログラマ38の日記

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

【Salesforce/Excel】直接AnonymousApexを実行するExcelマクロを作成しました

Salesforceで用意されている関数や開発した関数の動きを確認したり、AnonymousApexを利用する時があります。

 

AnonymousApexは、開発者コンソールやForce.com IDESalesforce Workbenchで実行できますが、直接ExcelからAnonymousApexを実行するマクロを作成しました。

 

ダウンロードはこちらからです。

 

Excelなので、実行したApexコードや実行結果がファイルで保存できます。

 

使い方は次の通りです。

 

Salesforceの接続情報を指定
②通信設定
③Apexコードを入力
④ログカテゴリ・ログレベルを選択し、実行ボタンをクリック

 

 

f:id:crmprogrammer38:20171121162553p:plain

実行後、結果が出力されます。

 

ApexAPIのexecuteAnonymousメソッドを利用していて、出力結果を開発者コンソールの結果に近づけるように整形しているのがポイントです。

 

このExcelマクロを作成していて、ExcelのActive Xコントロールのテキストボックスでは、Redo/Undoが使えないことがわかりました。

コードを書くのに、Redo/Undoが使えないのは問題だとは思いましたが、コードは使いなれたテキストエディタで書けばいいかと諦めました。

 

最後に

例えば、指定したビューやレポートの結果レコードを取得したり、動かしてみないとわからないクラスを使う時はAnonymousApexを実行するのが便利です。

ただ、ちょっと書くだけなのにForce.com IDEなどを立ち上げるのはちょっとなーという時にちょうどいいかなと思います。

処理としては常にログインしてから、Apexの実行なので、セッションがタイムアウトすることはありません。

 

Salesforce APIを操作するExcelマクロを色々用意してみましたが、今回のAnonymous Apex実行で一通りのAPIを触った形となります。(Tooling APIは使っていませんが、私自体が使い道がわかっておりません。。)

Excelマクロが実行できて、TLS1.1以上の通信ができる必要があり、利用できる環境は非常に限定的なものとなっています。そもそもAnonymous Apex自体、利用する頻度は低いので、ほとんど利用する方はいらっしゃらないと思いますが。。。

 

使用してもらえるなら大変うれしく思います。

 

 

【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であれば取得可能です。

【Salesforce】ログインユーザが利用可能なレコードタイプの一覧を取得する

レコードタイプの選択を、Visualforceで行いたい時があります。

標準のレコードタイプ選択画面であれば、ログインしているユーザのプロファイルに応じたリストが表示されますが、 そのリストをApexで作成する場合のメモです。

 

[サンプルコード]    ※Sample__cは適宜置き換えてください。

List<RecordTypeInfo> rtlist =  Sample__c.SObjectType.getDescribe().getRecordTypeInfos();

List<SelectOption> rtoptions = new List<SelectOption>();
for( RecordTypeInfo rt : rtlist ){

  //System.debug(' getName() : ' + rt.getName()  + 
  //               '/ getRecordTypeId(): ' + rt.getRecordTypeId()+ 
  //               '/ isActive(): ' + rt.isActive() + 
  //               '/ isAvailable(): ' + rt.isAvailable() + 
  //               '/ isMaster: ' + rt.isMaster());

  if( rt.isMaster() == false && rt.isAvailable() == true ){
    rtoptions.add( new  SelectOption(rt.getRecordTypeId() , rt.getName() ) );
  }
}

//System.debug(rtoptions);

上のコードでプロファイルに応じたリストが取得できます。

ポイントは次です。

  • RecordTypeInfo.getName() は、翻訳が指定されていれば翻訳された後の値となります。
  • RecordTypeInfo.isAvailable()で、プロファイルで利用可能かが判断できます。RecordTypeInfo.isActive()がfalseの場合は、RecordTypeInfo.isAvailable()は常にfalseとなります。
  • RecordTypeInfo.isMaster()がtrueの場合、"マスタ"レコードタイプが取得できますが除外しています。

最後に

RecordTypeオブジェクトでは、RecordTypeInfo.isActive()までしか取得できず、ログインユーザが利用できるレコードタイプを一覧化するには、getRecordTypeInfos()を使う必要があります。

Schema.getGlobalDescribe()を使えば、文字列で指定したオブジェクトの利用可能なレコードタイプ一覧を取得する関数を作成することが可能になります。

【Java】XML中の制御コードについて

XMLでは、制御コードは改行やタブなどの決められたもの以外は許可されていません。(こちらの説明が詳しいです)

なので、XMLで値をセットする際には許可されていない文字コードは除去する必要があります。(除去する方法はこの記事を参考にさせて頂きました)

 

そして、XMLを扱ってデータのやり取りを行うWebServiceでもこのXMLの許可されていない制御コードの対応が必要です。

 

以前JavaWebServiceクライアントについて記載したことがありますが、それぞれについて許可されていない制御コードは次の通りの動きです。

Java ライブラリ XML中の許可されていない制御コード 備考
Axis1.4 エラー リクエスト用のXMLを作成する際に、許可されていない制御コードがあるとエラーとなる。
Axis2 (xmlbeans) エラー リクエスト用のXMLを作成する際に、許可されていない制御コードがあるとエラーとなる。
wsimport エラー リクエスト用のXMLを作成する際に、許可されていない制御コードがあるとエラーとなる。
apache cxf エラー リクエスト用のXMLを作成する際に、許可されていない制御コードがあるとエラーとなる。
wsc(Salesforce専用) 除去する

エラーとならない。
wscの中でXML作成ロジックを書いている。

上記の通りとなり、Salesforce専用のwscは許可されていない制御コードを指定してもエラーとなりません。(wscを利用しているデータローダもエラーとなりません)

 

レガシーから移行したデータや、Web画面から入力されたデータには、垂直タブやシフトイン・シフトアウトなど入っている場合があります。

wsc以外のライブラリではデータの受け渡しで許可されていない制御コードは取り除く処理が必要です。

 

最後に

Salesforceとの連携プログラムを作成するのはwscが便利なのだとあらためて感じました。

 

といっても、wscも癖はあるので用途に応じて使い分けが必要だなと思います。

SalesforceからSalesforceに連携する場合は、wsc以外であれば、取得した値をそのままcreate、upsert、updateに使えばいいのですが、wscだけはint型はintへ、date型はdateへ、datetime型はCalendar、base64型はbinaryへ変換などの処理が必要になります。

 

連携プログラムを作る際の参考になれば幸いです。

【Salesforce】時間型(ベータ)が追加されている件と項目作成マクロの更新の件

Winter18のバージョンアップでカスタム項目の型に、時間型が追加されていますね。

f:id:crmprogrammer38:20171030093621p:plain

まだベータですが、時分を入力する項目を次のように用意することが多いので便利になります。

・時分を入力するテキスト欄を用意

・HH:MMを保証する入力規則を用意

・もしくは時間の選択リスト、分の選択リストを用意

 

Apex Class では、Time Classが時間型に対応するようです。

 

後、以前ご紹介したExcelでカスタム項目を作成するマクロですが、ヘルプテキストと説明項目を追加しました。

crmprogrammer38.hatenablog.com

さすがに、API名、ラベル、型だけしか定義できないのはどうかと思い、改修をしました。 

お試しください。

 

 

【Salesforce/Excel】SOQLとビューのexplain結果を表示するExcelマクロを作成しました

Salesforceで、オブジェクトの件数が増えてくると、SOQLやビューが遅いときがあります。

そんな時は、explainの結果を見てSOQLやビューの条件を見直したり、外部IDを作成、カスタムインデックスの作成を依頼するなどの対応となります。

 

複数のexplain結果をまとめて表示できるExcelマクロを作成しました。

ダウンロードはこちらからです。

 

explainは、開発者コンソールから実行ができますが、1つずつの手動実行になるのと、実行結果をExcelの表として保存できない点が難点だと思います。そんな時にこのExcelマクロが使えると思います。

 

使い方は次の通りです。

Salesforceの接続情報を指定
②通信設定
③explainを取得するSOQL、またはビューIDを指定
④実行プランを取得ボタンをクリック

 

f:id:crmprogrammer38:20171027184223p:plain

 

explainを実行すると、複数のプランが帰ってきます。

その中からSalesforceが一番良いと判断したものを選ぶのだと思っています。

このExcelマクロでは、explainの結果でplanを5個まで表示しています。(勝手に5個までにしています)

正しく把握はできていないのですが、基本的にはplan1でleadingOperationTypeがIndexかOtherとなっていればOKかなと思っています。(件数が少ないならどんなプランでも問題ありませんが件数が多い場合はインデックスを使用した方がいいと思います)

 

ポイントとして、SOQLだけでなく、ビューIDを指定することでビューのexplainも取得できます。遅くなっていそうなビューを特定して、条件の見直しをする時にこのマクロがお役に立てると嬉しく思います。

 

最後に

SalesforceREST APIが用意されていて、基本的にSOAP APIとほぼ同じ機能です。(多少違いますが)

ですが、このexplainは、REST APIだけの機能です。

Excelマクロでは、httpのリクエストを送信するだけなので、REST APIとはとても相性がいいのですが、64bitのExcelJSONを扱うことが難しいので結局XMLで結果をもらっています。

 

後、REST APIのexplainで、SOQLはURLEncodeしてパラメータにセットします。

Excelの数式でurlencodeの関数があるのですが、Excel2010だと使えなくてあきらめました。こういったところはExcelマクロの難しさだと思います。

使うOSのバージョン、32bitか64bitか、Excelのバージョンなど意識しないといけないのでこれはこれで大変だなーと思いました。