Salesforce。VisualforceでCSV出力してみた話。

googleで、「Visualforce、CSV出力」で検索すると情報がわんさか出てくるのですが、ちょっとはまってしまったので自身のためにメモ。


要件としては、全銀協データフォーマット形式のCSVを出せと。。。
フォーマットはどの銀行もほぼ同じっぽいので、検索して見つけたものをリンクします。
http://www.boy.co.jp/hojin/eb/bsd/pdf/manual-001.pdf

こんな感じでヘッダレコード、データレコード、トレーラーレコード、エンドレコードと、カラムの長さが違うCSV形式。
ってことで、レポートでCSV出力してそれをデータ部。ヘッダ・トレーラ・エンドExcelマクロとか、javaのバッチとかで植え付ける。
なんてことも考えたのですが、VisualforceでCSVを出せるってことで、技術検証として試してみたのです。

でやってみたら、なかなかうまく出てきませんでした。
<Script>タグが出てきたり、文字コードが違ったり。

最終的に以下のようにしまして、なんとかうまく出ました。

<apex:page controller="CreateCSV" cache="true"
contentType="text/csv;charset=Shift-JIS;#test.csv" readOnly="true">
<apex:repeat value="{!csvRows}" var="row">
<apex:outputText value="{!row.columns}" />
</apex:repeat>
</apex:page>

ContentTypeが第1のポイント。
"text/csv"にします。
charsetはそのときのお好み。
#test.csvっていうのはファイル名。
ファイル名は変数を渡すなど工夫すればシステム日付にすることもできるっぽい。

CSVはrepeatタグを使ってひたすら出力します。


次にコントローラクラス。
/** CSVの出力を行うためのコントローラクラスです。*/
public with sharing class CreateCSV {

    /** CSVデータ(ファイル出力用)*/
    public List<CSVRow> csvRows{get; private set;}

    /**
     * コンストラクタです。各プロパティの初期化を行います。
     */
    public CreateCSV(){
        csvRows = new List<CSVRow>();
    }

    /**
     * 検索ボタンの処理を行います。
     */
    public PageReference searchButton(){
        //検索結果の初期化。
        csvRows = new List<CSVRow>();

        //データ部を検索し、リストに格納。
        List<Account> acList = [select id, name from Account];

        //データ部の検索結果が0件の場合、CSVの出力を行わない。
        if(acList.isEmpty()){
            return null;
        }


        for(Accout ac : acList){
            //行データを格納するリスト
            List<String> l = new List<String>();

            //データ部を順番に格納していく。
            l.add(escapeCsv(ac.id)); 
            l.add(escapeCsv(ac.name));

            //ファイル出力用のリストへ格納
            CSVRow r = new CSVRow();
            r.columns = editCsvRow(l);
            csvRows.add(r);
        }

        return Page.CSVPage;
    }

    /**
     * テキストが格納されているリストクラスからCSV形式の行の形にしたテキストに変換します。
     * @param l CSVにしたいテキストのリスト
     * @return CSV形式にカンマ区切りにしたテキスト
     */
    private String editCsvRow(List<String> l){
        String str = '';
        for(integer i = 0; i<l.size(); i++){
            if(i != l.size() - 1){
                str += l.get(i) + ',';
            }else{
                str += l.get(i) + '\n';
            }
        }
        return str;
    }


    /**
     * テキストをCSVエスケープします。
     * nullの場合は空文字に変換します。
     * @param str テキスト
     * @return CSVエスケープ処理したテキスト
     */
    private String escapeCsv(String str){
        if (str != null){
            return str.escapeCsv();
        }

        return '';
    }

    /**
     * 数値を文字列に変換しCSVエスケープします。
     * nullの場合は空文字に変換します。
     * @param d 数値
     * @return CSVエスケープ処理したテキスト
     */
    private String escapeCsv(Decimal d){
        if (d != null){
            return String.valueOf(d).escapeCsv();
        }

        return '';
    }

    /** CSV出力用のクラスです */
    public class CSVRow{
        /** 行データ */
        public String columns{get;set;}
        /** コンストラクタです。初期化処理を行います。*/
        public CSVRow(){
            columns = '';
        }
    }
}

StringクラスのescapeCsvメソッドというのを使えば、ダブルクオーテーション、カンマ、改行の考慮をしてくれるみたい。
でもなぜかstaticではなかったので、nullだった場合は空文字に変換なんてことをしてます。

内部にclassを作ることで、repeatタグを使いやすいようにしています。
この辺りは、検索一覧を出す手法と同じですね。


--------------------------------------------------------------------------------------

Salesforceのメモ&添付のファイルを一括ダウンロードしたーい。

という話。

Salesforceの
設定⇒データの管理⇒データのエクスポート
とすれば、標準でできるのですが、
1.ダウンロードできるようになるまで時間がかかる。
2.ウィークリー(またはマンスリー)で1回のみ。
3.地味にzipをクリックする作業
なので、使い勝手はあまりよろしくないし、特に2.の設定を間違えたらバックアップは1週間先でリリース中断。
なんてことも考えられます。

AppExchangeでダウンロードするようなものないかな?
って探してみたのですが探し方が足りないのか見当たらなかったので、javaとwindowsコマンドでダウンロードする方式を作ってみることにしました。


まずは、javaでSalesforceにアクセスできるようにするため、jarを作成する作業から。
1.
Salesforceの
設定⇒開発⇒API⇒パートナー WSDL の生成
を右クリックし、partner.wsdlを保存。

2.
wsc.jarをダウンロードします。
gitとか
こことか
こことか
などでダウンロードして、partner.wsdlと同一フォルダにいれておきます。

3.
wsdlからjarを作成します。
コマンドプロンプトを立ち上げて、cd コマンドで、先ほどダウンロードしたフォルダ階層に移動。
そして、
java -classpath wsc-XX.jar com.sforce.ws.tools.wsdlc partner.wsdl partner.jar
とすると、partner.jarが作成されます。


ここまで、準備したら次にjavaコーディング。(私の開発環境の都合上java6で書いた)
きれいなコーディングとはいえませんが、下記のようなコーディングをしました。

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Properties;

import com.sforce.soap.partner.Connector;
import com.sforce.soap.partner.LoginResult;
import com.sforce.soap.partner.PartnerConnection;
import com.sforce.soap.partner.QueryResult;
import com.sforce.soap.partner.sobject.SObject;
import com.sforce.ws.ConnectionException;
import com.sforce.ws.ConnectorConfig;

/**
 * メモ&添付のファイルをBASE64として出力する。
 */
public class SFDCFileDownload {

    public static void main(String[] args) {

        SFDCFileDownload sfdcFileDownload = new SFDCFileDownload();
        try {
            sfdcFileDownload.execute();
        } catch (Exception e) {
            // TODO 自動生成された catch ブロック
            e.printStackTrace();
        }
    }

    /**
     * @throws Exception
     */
    private void execute() throws Exception {
        Properties prop = null;
        PartnerConnection connection = null;

        try {
            // プロパティファイルの読み込んでプロパティ情報を取得
            prop = readProperties();

            // Salesforce環境へのログインしコネクション情報を取得
            connection = loginSFDC(prop);

            // メモ&添付ファイルの読み込みとファイル出力
            readAttachment(prop, connection);

        } catch (Exception e) {
            throw e;
        } finally {
            // Salesforceのログアウト
            if (connection != null) {
                connection.logout();
            }
        }

    }

    /**
     * プロパティファイルの読み込み
     * @throws IOException
     */
    private Properties readProperties() throws IOException {

        Properties prop = new Properties();

        // プロパティファイルの読み込み
        BufferedReader br = new BufferedReader(new FileReader(
                "fileDownload.properties"));

        prop.load(br);

        return prop;

    }

    /**
     * SFDCへのログインまで
     * @param prop
     * @return SFのコネクション情報
     * @throws ConnectionException
     */
    private PartnerConnection loginSFDC(Properties prop)
            throws ConnectionException {

        // ログインの設定。
        ConnectorConfig config = new ConnectorConfig();
        config.setUsername(prop.getProperty("userName"));
        config.setPassword(prop.getProperty("userPassword"));

        // 本番orSandbox。ここも後にパラメータ化
        config.setAuthEndpoint(prop.getProperty("url"));

        // 接続
        PartnerConnection connection = Connector.newConnection(config);

        LoginResult lr = connection.login(prop.getProperty("userName"),
                prop.getProperty("userPassword"));

        System.out.println("--------------------------------");
        System.out.println(lr.getUserInfo().getOrganizationName());
        System.out.println(lr.getUserInfo().getUserFullName());
        System.out.println("login success...");
        System.out.println("--------------------------------");

        return connection;
    }

    /**
     * メモ&添付を検索、ファイルを出力する。
     * @param prop プロパティ
     * @param connection コネクション
     * @throws ConnectionException
     * @throws IOException
     */
    private void readAttachment(Properties prop, PartnerConnection connection)
            throws ConnectionException, IOException {

        // 出力先フォルダを作成する。
        Date date = new Date();
        DateFormat df = new SimpleDateFormat("yyyyMMddHHmmssSSS");
        File beforeDir = new File("before" + df.format(date));
        if (!beforeDir.exists()) {
            beforeDir.mkdir();
        }

        File afterDir = new File("after" + df.format(date));
        if (!afterDir.exists()) {
            afterDir.mkdir();
        }

        // バッチ用のファイル出力
        PrintWriter pw = new PrintWriter(new OutputStreamWriter(
                new FileOutputStream("base64Decode.bat"), "MS932"));

        // メモ&添付の全件検索。
        StringBuilder soqlQuery = new StringBuilder();
        soqlQuery.append("SELECT ");
        soqlQuery.append("ID, ");
        soqlQuery.append("Name, ");
        soqlQuery.append("Body ");
        soqlQuery.append("FROM ");
        soqlQuery.append("Attachment ");

        QueryResult rs = connection.query(soqlQuery.toString());

        // 1件ずつ抽出添付を抽出。
        boolean done = false;
        while (!done) {
            for (int i = 0; i < rs.getRecords().length; i++) {
                SObject record = rs.getRecords()[i];

                // ファイル名を設定
                String fileName = null;
                if ("1".equals(prop.getProperty("fileName"))) {
                    // Idのみ
                    fileName = record.getId();
                } else {
                    // IDと拡張子
                    // 拡張子のチェック
                    int point = record.getField("Name").toString()
                            .lastIndexOf(".");
                    if (point == -1) {
                        fileName = record.getId();
                    } else {
                        fileName = record.getId()
                                + record.getField("Name").toString()
                                        .substring(point);
                    }
                }

                // ファイル出力
                FileOutputStream writer = new FileOutputStream(beforeDir
                        + File.separator + fileName);
                BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(
                        writer));

                String str = record.getField("Body").toString();
                bw.write(str);

                bw.flush();
                bw.close();
                writer.close();

                // バッチ用のファイル書き込み
                StringBuilder sb = new StringBuilder("certutil -decode ");
                sb.append("\"");
                sb.append(beforeDir);
                sb.append(File.separator);
                sb.append(fileName);
                sb.append("\" \"");
                sb.append(afterDir);
                sb.append(File.separator);
                sb.append(fileName);
                sb.append("\"");
                pw.println(sb.toString());

            }

            if (rs.isDone()) {
                done = true;
            } else {
                rs = connection.queryMore(rs.getQueryLocator());
            }
        }

        pw.close();

    }
}


コーディング内のプロパティファイルは以下のような内容です。
#Salesforceにログインするユーザ名とパスワード
#SalesforceのIP設定によっては、パスワードの後ろにセキュリティトークンもつける必要有。
userName=xxxxxxxxxxxxxxxx@salesforce.com
userPassword=xxxxxx

#SalesforceへのURL:
url=https://login.salesforce.com/services/Soap/u/34.0
#url=https://test.salesforce.com/services/Soap/u/34.0


#ファイル名の出力方法
#1:Id
#2:Id##ファイル名(拡張子あり、windowsで使えない文字は@変換)(うまくいかないので今のところ非推奨)
#3:Id+拡張子
fileName=3


Bodyの内容を見るとBASE64形式の文字列でした。
私のやり方が悪いのか、
ObjectOutputStream.write(Base64.decode(・・・))
のようにしてファイル出力させようとしたのですが、ファイルが壊れてうまくいかず。

そのため、BufferedWriterでテキストとして出力させたあと、全ファイルをコマンドラインの「certUtil -decode」で変換する方式としました。
コマンドラインの内容も一緒に吐き出しているので、javaを起動した後、作成したバッチを動かせばファイルがローカルにばっちり作られました。

めんどくさいので、java(特に意味はなかったけどjarにした)とデコードバッチを起動するバッチも作成。

@echo off
setlocal

rem *********************************************************
rem メモ&添付のファイルをダウンロードします。
rem *********************************************************

java -classpath AttachmentDownload.jar;lib\* SFDCFileDownload

rem *********************************************************
rem ダウンロードしたBASE64のファイルをデコードします
rem *********************************************************

call base64Decode.bat


バックアップしたものをアップロードする方法ですが、そこは未検討。
データローダでできるみたいですが、どうしましょうね。


参考文献
javaのAPI:http://docs.oracle.com/javase/jp/6/api/
java開発者環境の設定:https://developer.salesforce.com/docs/atlas.ja-jp.salesforce_developer_environment_tipsheet.meta/salesforce_developer_environment_tipsheet/salesforce_developer_environment_java_stubs.htm
terraSkyさんのブログ記事:http://www.terrasky.co.jp/blog/2014/140430_001345.php
Pa-kun plus ideaさんのブログ記事:http://web.plus-idea.net/2012/03/salesforce-download/



--------------------------------------------------------------------------------------

このブログについて
  • 全記事一覧(時間順)
  • このブログについて
  • 私のプロフィール
  • 当ブログで扱っている動画について
  • 記事違いなコメントのお返事

  • カテゴリー
    twitter
    カレンダー
    11 | 2015/12 | 01
    - - 1 2 3 4 5
    6 7 8 9 10 11 12
    13 14 15 16 17 18 19
    20 21 22 23 24 25 26
    27 28 29 30 31 - -
    Amazon
    でたらめな当ブログにぴったりな商品を自動で表示するみたいです。



    月別アーカイブ
    プロフィール

    たづみ

    Author:たづみ
    ・1981年生まれの男
    ・もう少し詳細なプロフィールはこちらで

    最新コメント
    アクセスランキング
    [ジャンルランキング]
    日記
    1757位
    アクセスランキングを見る>>

    [サブジャンルランキング]
    会社員・OL
    357位
    アクセスランキングを見る>>