SalesforceのAPEXでメタデータを取得する方法

## 初めに
この記事はSalesforce開発者向けの記事です。ある程度Salesforceを知っている前提で記載します。
今回はSalesforceのApexでメタデータを取得する方法を紹介します。

## メタデータを取得する理由
### Salesforceのメタデータとは
SalesforceのメタデータとはオブジェクトやLightning Pageの定義のことです。
オブジェクトやLightning PageはGUI上で定義を行いますが、Salesforceはこの定義をXMLの形で保持しています。
これをメタデータと言います。

### 何故Apexでメタデータを取得するか？
オブジェクトやLightning Pageのメタデータ定義をApexで取得出来ると、例えば設計書を自動で生成することが可能です。例えば画面に表示している項目名を一覧で取得してドキュメントにするといったことが可能です。
注）オブジェクト項目の一覧であればメタデータを取得しなくても、Schemaクラスで取得出来ますが、ここではより沢山の情報が取得出来るメタデータの取得方法を解説します。

## メタデータの取得方法
### メタデータAPIの利用
Apexからメタデータを取得するには[メタデータAPI](https://developer.salesforce.com/docs/atlas.ja-jp.api_meta.meta/api_meta/meta_intro.htm)を利用します。
プログラムは以下の様になります。

```
public class GetMetaDataTest {
    public static void getMetadata() {
        String metadataApiEndpoint = URL.getOrgDomainUrl().toExternalForm() + '/services/Soap/m/51.0';

        // SOAPリクエストの作成
        // これはQAPageというLightning Pageのメタ情報を取得するもの
        String soapRequest = '<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:met="http://soap.sforce.com/2006/04/metadata">';
        soapRequest = soapRequest + '<soapenv:Header>' +
                                      '<met:SessionHeader>' +
                                        '<met:sessionId>' + UserInfo.getSessionId() + '</met:sessionId>' +
                                      '</met:SessionHeader>' +
                                    '</soapenv:Header>';
        soapRequest = soapRequest + '<soapenv:Body>' +
                                      '<met:readMetadata>' +
                                        '<met:type>FlexiPage</met:type>' +
                                        '<met:fullNames>QAPage</met:fullNames>' +
                                      '</met:readMetadata>' +
                                   '</soapenv:Body>' +
                            '</soapenv:Envelope>';

        // HTTPリクエストの作成
        HttpRequest httpRequest = new HttpRequest();
        httpRequest.setEndpoint(metadataApiEndpoint);
        httpRequest.setMethod('POST');
        httpRequest.setHeader('Content-Type', 'text/xml');
        httpRequest.setHeader('SOAPAction', '""');
        httpRequest.setBody(soapRequest);

        // HTTPリクエストの送信
        Http http = new Http();
        HttpResponse httpResponse = http.send(httpRequest);

        // レスポンスの処理
        if (httpResponse.getStatusCode() == 200) {
            // getBodyの中にLightning Pageのメタ情報が入っている
            System.debug('Success: ' + httpResponse.getBody());

            // メタデータXMLの各要素の情報を取得
            Dom.Document doc = new Dom.Document();
            doc.load(httpResponse.getBody());
            Dom.XmlNode rootElement = doc.getRootElement();
			processNode(rootElement);
        } else {
            // エラー処理
            System.debug('Error retrieving Lightning Page information: ' + httpResponse.getStatus());
        }
    }

    // 要素を再帰的に処理するメソッド
    public static void processNode(Dom.XmlNode node) {
        if (node.getNodeType() == Dom.XmlNodeType.ELEMENT) {
            System.debug('Element Name: ' + node.getName());
            System.debug('Text: ' + node.getText());
            // 子要素があれば再帰的に処理
            if (node.getChildElements().size() > 0) {
                for (Dom.XmlNode childNode : node.getChildElements()) {
                    processNode(childNode);
                }
            }
        } else {
            System.debug('Text: ' + node.getText());
        }
    }
}
```

### 取得出来るメタデータ
取得可能なメタデータはSalesforceの以下ページに記載されています。
https://developer.salesforce.com/docs/atlas.ja-jp.api_meta.meta/api_meta/meta_types_list.htm

Lightning Pageのメタデータだとこちらになります。
https://developer.salesforce.com/docs/atlas.ja-jp.api_meta.meta/api_meta/meta_flexipage.htm

上記ページにメタデータ(XML)のサンプルも掲載されていますので、それを見るとメタデータのイメージが湧くと思います。

【メタデータのサンプル】
```
<?xml version="1.0" encoding="UTF-8"?>
<FlexiPage xmlns="http://soap.sforce.com/2006/04/metadata">
    <flexiPageRegions>
        <itemInstances>
            <componentInstance>
                <componentInstanceProperties>
                    <name>collapsed</name>
                    <value>false</value>
                </componentInstanceProperties>
                <componentInstanceProperties>
                    <name>hideChatterActions</name>
                    <value>false</value>
                </componentInstanceProperties>
                <componentInstanceProperties>
                    <name>numVisibleActions</name>
                    <value>3</value>
                </componentInstanceProperties>
                <componentName>force:highlightsPanel</componentName>
            </componentInstance>
        </itemInstances>
        <name>header</name>
        <type>Region</type>
    </flexiPageRegions>
    <flexiPageRegions>
        <itemInstances>
            <componentInstance>
                <componentInstanceProperties>
                    <name>hideUpdateButton</name>
                    <value>false</value>
                </componentInstanceProperties>
                <componentInstanceProperties>
                    <name>variant</name>
                    <value>linear</value>
                </componentInstanceProperties>
                <componentName>runtime_sales_pathassistant:pathAssistant</componentName>
            </componentInstance>
        </itemInstances>
        <name>subheader</name>
        <type>Region</type>
    </flexiPageRegions>
    <flexiPageRegions>
        <itemInstances>
            <componentInstance>
                <componentInstanceProperties>
                    <name>entityNames</name>
                    <valueList>
                        <valueListItems>
                            <value>Opportunity</value>
                        </valueListItems>
                    </valueList>
                </componentInstanceProperties>
                <componentInstanceProperties>
                    <name>maxRecords</name>
                    <value>3</value>
                </componentInstanceProperties>
                <componentName>flexipage:recentItems</componentName>
            </componentInstance>
        </itemInstances>
        <name>Facet-afbed70e-277a-41f5-9919-34651ff97773</name>
        <type>Facet</type>
    </flexiPageRegions>
    <flexiPageRegions>
        <itemInstances>
            <componentInstance>
                <componentInstanceProperties>
                    <name>relatedListComponentOverride</name>
                    <value>NONE</value>
                </componentInstanceProperties>
                <componentName>force:relatedListContainer</componentName>
            </componentInstance>
        </itemInstances>
        <name>facet-77f21b6f-ad73-4d79-838a-79e0df27cc63</name>
        <type>Facet</type>
    </flexiPageRegions>
    <flexiPageRegions>
        <itemInstances>
            <componentInstance>
                <componentName>force:detailPanel</componentName>
            </componentInstance>
        </itemInstances>
        <name>facet-c22fcfa7-d6f2-46ab-ac03-6c92e7398da1</name>
        <type>Facet</type>
    </flexiPageRegions>
    <flexiPageRegions>
        <itemInstances>
            <componentInstance>
                <componentName>runtime_sales_activities:activityPanel</componentName>
            </componentInstance>
        </itemInstances>
        <name>Facet-u9v2x6h8u4k</name>
        <type>Facet</type>
    </flexiPageRegions>
    <flexiPageRegions>
        <itemInstances>
            <componentInstance>
                <componentInstanceProperties>
                    <name>body</name>
                    <value>Facet-afbed70e-277a-41f5-9919-34651ff97773</value>
                </componentInstanceProperties>
                <componentInstanceProperties>
                    <name>title</name>
                    <value>Recent Items</value>
                </componentInstanceProperties>
                <componentName>flexipage:tab</componentName>
            </componentInstance>
        </itemInstances>
        <itemInstances>
            <componentInstance>
                <componentInstanceProperties>
                    <name>active</name>
                    <value>true</value>
                </componentInstanceProperties>
                <componentInstanceProperties>
                    <name>body</name>
                    <value>facet-77f21b6f-ad73-4d79-838a-79e0df27cc63</value>
                </componentInstanceProperties>
                <componentInstanceProperties>
                    <name>title</name>
                    <value>Standard.Tab.relatedLists</value>
                </componentInstanceProperties>
                <componentName>flexipage:tab</componentName>
            </componentInstance>
        </itemInstances>
        <itemInstances>
            <componentInstance>
                <componentInstanceProperties>
                    <name>body</name>
                    <value>facet-c22fcfa7-d6f2-46ab-ac03-6c92e7398da1</value>
                </componentInstanceProperties>
                <componentInstanceProperties>
                    <name>title</name>
                    <value>Standard.Tab.detail</value>
                </componentInstanceProperties>
                <componentName>flexipage:tab</componentName>
            </componentInstance>
        </itemInstances>
        <itemInstances>
            <componentInstance>
                <componentInstanceProperties>
                    <name>body</name>
                    <value>Facet-u9v2x6h8u4k</value>
                </componentInstanceProperties>
                <componentInstanceProperties>
                    <name>title</name>
                    <value>Standard.Tab.activity</value>
                </componentInstanceProperties>
                <componentName>flexipage:tab</componentName>
            </componentInstance>
        </itemInstances>
        <name>facet-27334405-c871-463f-bc20-b3713bbb4884</name>
        <type>Facet</type>
    </flexiPageRegions>
    <flexiPageRegions>
        <itemInstances>
            <componentInstance>
                <componentInstanceProperties>
                    <name>tabs</name>
                    <value>facet-27334405-c871-463f-bc20-b3713bbb4884</value>
                </componentInstanceProperties>
                <componentName>flexipage:tabset</componentName>
            </componentInstance>
        </itemInstances>
        <name>main</name>
        <type>Region</type>
    </flexiPageRegions>
    <flexiPageRegions>
        <itemInstances>
            <componentInstance>
                <componentInstanceProperties>
                    <name>decorate</name>
                    <value>true</value>
                </componentInstanceProperties>
                <componentInstanceProperties>
                    <name>richTextValue</name>
                    <value>&lt;p style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;background-color: rgb(255, 255, 255); font-size: 18px; color: rgb(11, 11, 11);&quot;&gt;A million dollar opportunity closed! Oh yeah!&lt;/span&gt;&lt;/p&gt;</value>
                </componentInstanceProperties>
                <componentName>flexipage:richText</componentName>
                <visibilityRule>
                    <booleanFilter>1 AND 2</booleanFilter>
                    <criteria>
                        <leftValue>{!Record.Amount}</leftValue>
                        <operator>GE</operator>
                        <rightValue>1000000</rightValue>
                    </criteria>
                    <criteria>
                        <leftValue>{!Record.StageName}</leftValue>
                        <operator>EQUAL</operator>
                        <rightValue>Closed Won</rightValue>
                    </criteria>
                </visibilityRule>
            </componentInstance>
        </itemInstances>
        <itemInstances>
            <componentInstance>
                <componentInstanceProperties>
                    <name>decorate</name>
                    <value>true</value>
                </componentInstanceProperties>
                <componentInstanceProperties>
                    <name>richTextValue</name>
                    <value>&lt;p style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;background-color: rgb(255, 255, 255); font-size: 16px; color: rgb(244, 0, 0);&quot;&gt;This component is for mobile users only.&lt;/span&gt;&lt;/p&gt;</value>
                </componentInstanceProperties>
                <componentName>flexipage:richText</componentName>
                <visibilityRule>
                    <criteria>
                        <leftValue>{!$Client.formFactor}</leftValue>
                        <operator>EQUAL</operator>
                        <rightValue>Small</rightValue>
                    </criteria>
                </visibilityRule>
            </componentInstance>
        </itemInstances>
        <itemInstances>
            <componentInstance>
                <componentName>forceChatter:recordFeedContainer</componentName>
            </componentInstance>
        </itemInstances>
        <name>sidebar</name>
        <type>Region</type>
    </flexiPageRegions>
    <masterLabel>New Opportunity Page</masterLabel>
    <sobjectType>Opportunity</sobjectType>
    <template>
        <name>flexipage:recordHomeWithSubheaderTemplateDesktop</name>
    </template>
    <type>RecordPage</type>
</FlexiPage>
```