【Salesforce】Apexで連動選択リストを取得する

【Salesforce】Apexで連動選択リストを取得する

Salesforceの選択リストには、他の選択リストと連動した「連動選択リスト」という仕組みがあります。

その連動選択リストをApexコード内で取得したのですが、少し複雑だったのでメモです。

とりあえず、選択リストの項目を取得するとすればスキーマですよね。

連動する選択リスト側のスキーマを取得すれば終わりだと思いましたが、そういうわけではないようです。

https://developer.salesforce.com/docs/atlas.ja-jp.206.0.api.meta/api/sforce_api_calls_describesobjects_describesobjectresult.htm

選択リストのスキーマの中にvalidForという項目があるようなのですが、これが連動関係を示す項目のようです。

しかし、この値をApex内では取得することができないようです。

少し調べてみると、ゼイの人も取れないゼイって言ってました。

http://blog.livedoor.jp/volvic_beer/archives/52382942.html

もう少し調べてみると、スキーマをJSONに変換してvalidForの値を取得している人がいました。

海外の方が作ったもののようですね。

https://developer.salesforce.com/forums/?id=906F0000000BIVqIAO

ソースコードを読んでみると、独自のクラスを作って処理を行っているようです。

これらからヒントを得て、自分でも作ってみることにしました。

/**
 * 連動選択リストのMapを作成する
 * Map<親の値, Map<子の値, 子のラベル>>
 */
public class DependentPickList{

    /**
     * 連動選択リストのMapを作成する
     * @param parentEntryList 親選択リストのスキーマ
     * @param childEntryList 子選択リストのスキーマ
     * @return 作成したMap 作成したMap
     */
    public static Map<String, Map<String, String>> createSelectListMap(List<Schema.PicklistEntry> parentEntryList, List<Schema.PicklistEntry> childEntryList){

        List<Object> jsonStringList;
        Map<String, Map<String, String>> validForMap;
        Map<String, Integer> hexMap;
        Integer parentIndex;

        JSONGenerator gen;
        String jsonString;

        // hex=>intのMapを作成する
        hexMap = DependentPickList.createHexMap();

        // スキーマの状態では取得できない項目があるため、JSONに変換する
        gen = JSON.createGenerator(true);
        gen.writeObject(childEntryList);
        jsonString = gen.getAsString();

        // JSON文字列のリストに変換する
        jsonStringList = (List<Object>)JSON.deserializeUntyped(jsonString);
        // 親選択リストのindexを初期化する
        parentIndex = 0;
        // Mapを作成する
        validForMap = new Map<String, Map<String, String>>();
        // 空白時の値を設定する
        validForMap.put('', new Map<String, String>());
        for(Schema.PicklistEntry parentEntry : parentEntryList){

            String parentValue;
            Integer testBit;

            // テスト用BITを作成する
            testBit = 8 >> (Math.mod(parentIndex, 4));

            // 親項目リストの値を取得する
            parentValue = parentEntry.getValue();
            // Mapに追加する
            validForMap.put(parentValue, new Map<String, String>());

            // 連動項目でループする
            for(Object obj : jsonStringList){

                Map<String, Object> schemaMap;
                String validFor;
                Blob b;
                String hexB;
                Integer bitIndex;
                String targetBit;
                Integer bitArray;

                // スキーマのMapを作成する
                schemaMap = (Map<String, Object>)obj;
                // 連動項目のビットセットを取得する
                validFor = String.valueOf(schemaMap.get('validFor'));
                // 存在しない場合は処理を行わない
                if(String.isEmpty(validFor)){

                    continue;
                }

                // 紐付けByte列を取得する
                b = EncodingUtil.base64Decode(validFor);
                // hexを文字列にする
                hexB = EncodingUtil.convertToHex(b);

                // hexBから親選択リストに対応するBITを含む文字のindexを取得する
                bitIndex = parentIndex / 4;
                // 文字を取得する
                targetBit = hexB.substring(bitIndex, bitIndex + 1);

                // 対象の文字が0である場合は処理を行わない
                if('0'.equals(targetBit)){

                    continue;
                }

                // 文字を数値にする
                bitArray = hexMap.get(targetBit);

                // 対応するBITが一致する場合、TRUE
                if((bitArray & testBit) != 0){

                    String childValue = String.valueOf(schemaMap.get('value'));
                    String childLabel = String.valueOf(schemaMap.get('label'));
                    validForMap.get(parentValue).put(childValue, childLabel);
                }
            }
            
            // 次のindexへ
            parentIndex++;
        }

        return validForMap;
    }

    /**
     * 0~fの値を数値で返すMapを作成する
     */
    private static  Map<String, Integer> createHexMap(){

        Map<String, Integer> hexMap;

        hexMap = new Map<String, Integer>();
        hexMap.put('0', 0);
        hexMap.put('1', 1);
        hexMap.put('2', 2);
        hexMap.put('3', 3);
        hexMap.put('4', 4);
        hexMap.put('5', 5);
        hexMap.put('6', 6);
        hexMap.put('7', 7);
        hexMap.put('8', 8);
        hexMap.put('9', 9);
        hexMap.put('A', 10);
        hexMap.put('B', 11);
        hexMap.put('C', 12);
        hexMap.put('D', 13);
        hexMap.put('E', 14);
        hexMap.put('F', 15);
        hexMap.put('a', 10);
        hexMap.put('b', 11);
        hexMap.put('c', 12);
        hexMap.put('d', 13);
        hexMap.put('e', 14);
        hexMap.put('f', 15);

        return hexMap;
    }
}

// ***** SAMPLE *****
// Map<String, Map<String, String>> dependentSelectMap;
// List<Schema.PicklistEntry> parentEntryList;
// List<Schema.PicklistEntry> childEntryList;
// 
// // 親選択リストのスキーマを取得する
// parentEntryList = Account.ParentField__c.getDescribe().getPicklistValues();
// // 子選択リストのスキーマを取得する
// childEntryList = Account.ChildField__c.getDescribe().getPicklistValues();
// 
// // Mapを作成する
// dependentSelectMap = DependentPickList.createSelectListMap(parentEntryList, childEntryList);
// ***** SAMPLE *****

ちょっと汚いソースコードになってしまいました。

まず、選択リストのスキーマにある「validFor」の仕組みについて書きます。

一番上のURLにあるように、「validFor」の値はバイトの値なんだそうです。

ビットの列が並んでいて、左から順に親項目の選択リストに連動しているそうです。

たとえば、子項目の選択リストのとある値の「validFor」の一番左のビット列が1の場合、親項目の最初の値の場合に子項目が使用可能になります。

一番上のURLを例にすると

親項目 子項目 validFor(Hex) validFor
コーヒー カフェインなし 80 1000 0000
レギュラー 80 1000 0000
紅茶 カモミール 40 0100 0000
アールグレイ 40 0100 0000
イングリッシュブレックファスト 40 0100 0000

親項目が「コーヒー」の場合に使用可能な「カフェインなし」と「レギュラー」のvalidForの最初のビットは1となります。

また、親項目が「紅茶」の場合に使用可能な「カモミール」「アールグレイ」「イングリッシュブレックファスト」のvalidForの2つ目のビットが1となります。

ちなみに、どちらでも使用可能な項目があった場合の「validFor」は、1番目と2番目のビットの両方が1となります。

Apexで取得できない値である「validFor」ですが、取得できないだけでプロパティとしては持っているそうです。

そのため、スキーマごとJSONの状態に変換して文字列として取得しています。

上のURLの猛者の人もそのように取得しています。

// スキーマの状態では取得できない項目があるため、JSONに変換する
gen = JSON.createGenerator(true);
gen.writeObject(childEntryList);
jsonString = gen.getAsString();

// JSON文字列のリストに変換する
jsonStringList = (List<Object>)JSON.deserializeUntyped(jsonString);

上にも書いたように、連動関係を持っている「validFor」の値はバイトの値となっています。

それを文字列として取得しているため、Base64の値に変換されていますね。

ゼイの人はVisualforce内の処理でBase64をデコードしています。

// 紐付けByte列を取得する
b = EncodingUtil.base64Decode(validFor);

Apexコード内ではbyteの値を触ることが出来ませんが、ビットシフトのシフト演算や論理演算は出来ます。

そのためにはIntegerの型に変換する必要があるため、デコードしたBase64(Blob)をHexの値にしています。

// hexを文字列にする
hexB = EncodingUtil.convertToHex(b);

先ほどの例で書くと、コーヒーの場合に有効な値の「validFor」は「800000」となりました。

また、紅茶の場合に有効な値の「validFor」は「400000」となりました。

どちらも有効な場合は「A00000」となります。

実際に出力すると分かりやすいですね。

ここまでくると、あとは対応するHexと親項目のindexを対応させるだけです。

Hexにはしましたが、実際のバイト列は「1100 0000 0000 0000 0000 0000」となっています。

4bitずつ処理する必要があるため、indexを4で割った際の値にあるHexが対応する値になりますね。

さらに、bitの演算はIntegerで行うため、MapからIntegerの値を取得しています。

// hexBから親選択リストに対応するBITを含む文字のindexを取得する
bitIndex = parentIndex / 4;
// 文字を取得する
targetBit = hexB.substring(bitIndex, bitIndex + 1);
// 文字を数値にする
bitArray = hexMap.get(targetBit);

取得したHexの文字と対応する、判定用のHexの文字を作成します。

// テスト用BITを作成する
testBit = 8 >> (Math.mod(i, 4));

2つのbitで論理演算を行い、1であれば連動して表示される項目となります。

// 対応するBITが一致する場合、TRUE
if((bitArray &amp; testBit) != 0){

String childValue = String.valueOf(schemaMap.get('value'));
String childLabel = String.valueOf(schemaMap.get('label'));
validForMap.get(parentLabel).put(childValue, childLabel);
}

出力されたMapの値を使ってVisualforce側で動作をつけてやれば、連動選択リストを実装できます。

選択リストの項目が大量にあると、処理に時間がかかるので注意が必要です。

少し長くなってしまいましたが、実際に作ってみると楽しかったです。

これを作って遊んでいたら数時間が経過していたことは内緒です。

No comments.

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です