midPointの「マッピング」におけるスクリプトの利用とPrismデータ構造

# はじめに

midPoint by OpenStandia Advent Calendar 2019 の22日目は、midPointにおける「マッピング」でスクリプトを利用する際、最初に戸惑いやすいポイントについて説明します。
実際にマッピングを利用し始めて、スクリプトにより複雑な値の生成を試みようとしている人に向けて、事前に知っておくと開発の助けになるような予備知識を提供できれば、と思います。

※記事の内容やリンク先は、記事公開時点でのmidPointバージョンを基にしています。

|基本情報| |
|:--|:--|
|バージョン|4.1-SNAPSHOT|
|Git describe|v4.1devel-139-g4aaf38bf26|
|ビルド時刻|Wed, 27 Nov 2019 03:38:18 +0000|

※設定例の「…」は省略を示します。

# マッピングとは？

ID管理システムの中心となるのが同期ですが、その頭脳がマッピングです。

> マッピングは、midPointを介して多くの場所で使用され、ソースプロパティの値をターゲットプロパティにマッピングするメカニズムです。

参考）[Mapping](https://wiki.evolveum.com/display/midPoint/Mapping)

マッピング定義は、3つの基本的な部分で構成されます。

|構成部分|定義内容|
|:--|:--|
|source（ソース）|データを取得する場所を定義、入力変数のマッピング|
|expression（式）|データをどのように 変換／生成／コピー するかを定義|
|target（ターゲット）|結果をどのように処理するか、出力先を定義|

![synchronization-mapping.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/533702/ccfbfff1-78c7-d38c-5049-32e06912b63d.png)

図の引用元：[Mapping](https://wiki.evolveum.com/display/midPoint/Mapping)

# マッピングの適用箇所

前回までの記事でもマッピングは登場してきていましたが、改めて、その適用箇所を把握してみましょう。

以下に、同期の全体像を見渡すことができる図があります。初見ではちょっと面食らうかもしれませんが、今回は「mapping」という箇所にだけ注目してみてください。
[Synchronization Policies](https://wiki.evolveum.com/display/midPoint/Synchronization+Policies)

#### インバウンド・フェーズ
![synchronization-phase-inbound.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/533702/42306b31-d627-42ec-136b-fb63524557ee.png)
図の引用元：[Synchronization Policies](https://wiki.evolveum.com/display/midPoint/Synchronization+Policies)
#### ユーザー・ポリシー・フェーズ
![synchronization-phase-user.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/533702/d12a8655-d964-7d42-e58e-fd2ecd8293cd.png)
図の引用元：[Synchronization Policies](https://wiki.evolveum.com/display/midPoint/Synchronization+Policies)
#### アウトバウンド・フェーズ
![synchronization-phase-outbound.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/533702/a55f3111-39a1-c40e-b4b7-8feb8ca2cc18.png)
図の引用元：[Synchronization Policies](https://wiki.evolveum.com/display/midPoint/Synchronization+Policies)


主として利用するのは、以下の３か所になると思います。

|同期フェーズ|設定箇所|設定|
|:--|:--|:--|
|インバウンド|リソース定義schemaHandling|inbound|
|ユーザー・ポリシー|オブジェクト・テンプレート|mapping|
|アウトバウンド|リソース定義schemaHandling|outbound|

※ [assignment の accountConstruction にも outbound mapping が書ける](https://wiki.evolveum.com/display/midPoint/Assignment+Configuration#AssignmentConfiguration-Attributes) のですが「Do not overuse（使いすぎないこと）」とあり、例外的な場合に使用する設定です。
※ 図にありませんが、[assignment](https://evolveum.com/downloads/midpoint/3.8/3.9/schema-3.9-schemadoc/http---midpoint-evolveum-com-xml-ns-public-common-common-3/type/AssignmentType.html) には、オブジェクト・テンプレートに加えて適用されるマッピングのセットを指定できる focusMappingsもあります。

# マッピングの簡単な例

マッピングの簡単な例として、「リソースＡのアカウント ⇒ ユーザー ⇒ リソースＢのアカウント」の例を以下に示します。

（画面からは触れない設定項目を編集したり、設定ファイルをバージョン管理したりするなど、実際の開発ではXML等で記述された設定を直接編集することが多くなるでしょう。以下、設定をXMLで記載します。）

##### リソースＡのアカウント（firstname、lastname）からユーザー（givenName、familyName）への inbound
```:リソースＡ
<resource …>
    <name>Resource A</name>
    …
    <schemaHandling>
        <objectType>
            <objectClass>ri:AccountObjectClass</objectClass>
            …
            <attribute>
                <ref>ri:firstname</ref>
                <inbound>
                    <strength>strong</strength>
                    <target>
                        <path>givenName</path>
                    </target>
                </inbound>
            </attribute>

            <attribute>
                <ref>ri:lastname</ref>
                <inbound>
                    <strength>strong</strength>
                    <target>
                        <path>familyName</path>
                    </target>
                </inbound>
            </attribute>
            …
```
##### ユーザーの（givenName、familyName）から（fullName）を設定する mapping
```:オブジェクト・テンプレート
<objectTemplate …>
    <name>User Template</name>
    …
    <mapping>
        <strength>strong</strength>
        <source>
            <path>givenName</path>
        </source>
        <source>
            <path>familyName</path>
        </source>
        <expression>
            <script>
                <code>
                    givenName + ' ' + familyName
                </code>
            </script>
        </expression>
        <target>
            <path>fullName</path>
        </target>
    </mapping>
    …
```
##### ユーザー（fullName）からリソースＢのアカウント（fullName）への outbound
```:リソースＢ
<resource …>
    <name>Resource B</name>
    …
    <schemaHandling>
        <objectType>
            <objectClass>ri:AccountObjectClass</objectClass>
            …
            <attribute>
                <ref>ri:fullName</ref>
                <outbound>
                    <strength>strong</strength>
                    <source>
                        <path>fullName</path>
                    </source>
                </outbound>
            </attribute>
            …
```

# inbound / mapping / outbound の違い

アイテム名が異なっていますが、どれも [MappingType](https://evolveum.com/downloads/midpoint/3.8/3.9/schema-3.9-schemadoc/http---midpoint-evolveum-com-xml-ns-public-common-common-3/type/MappingType.html)（またはそのサブタイプ）なので仕組みは同じです。

- 参考）attribute（[ResourceAttributeDefinitionType](https://evolveum.com/downloads/midpoint/4.0.1/midpoint-4.0.1-schemadoc/http---midpoint-evolveum-com-xml-ns-public-common-common-3/type/ResourceAttributeDefinitionType.html)）の inbound と outbound
- 参考）objectTemplate（[ObjectTemplateType](https://evolveum.com/downloads/midpoint/4.0.1/midpoint-4.0.1-schemadoc/http---midpoint-evolveum-com-xml-ns-public-common-common-3/object/ObjectTemplateType.html)）の mapping

入力元（source）と出力先（target）の指定に違いがあります。
inbound / outbound は、それが設定されているリソース・オブジェクトの属性が source / target なので、属性の指定は不要です。

|マッピング|source|target|
|:--|:--|:--|:--|:--|
|inbound |attributeで固定※|   |
|mapping |   |   |
|outbound|   |attributeで固定|

参考）[Source and Target Definitions](https://wiki.evolveum.com/display/midPoint/Mapping#Mapping-SourceandTargetDefinitions)

（※ １つの attribute に複数の inbound を設定したり、inbound に attribute 以外の source を追加することもできます。Type定義を見ると分かりますが、inbound および source の Multiplicity は [0,-1] で、複数設定が可能です。)

# マッピングにおけるスクリプトの利用と、最初に戸惑いやすいポイントについて

マッピングの設定についての詳細は、以下に説明があります。
[Mapping](https://wiki.evolveum.com/display/midPoint/Mapping)

##### strength（強さ）

マッピングがどの程度の強さで適用されるかを指定するのが、strength（強さ）です。
参考）[Mapping Strength](https://wiki.evolveum.com/display/midPoint/Mapping#Mapping-MappingStrength)

ここではstrong（強い）として、常に適用するようにしています。

##### expression（式）

マッピングにおいて、値を生成するのが expression（式）です。
参考）[Expression](https://wiki.evolveum.com/display/midPoint/Expression#Expression-ExpressionEvaluators)

デフォルトは AsIs で、省略した場合は入力値がそのまま出力にコピーされます。先の設定例「リソースＡ」「リソースＢ」の inboundとoutbound における expression は AsIs です。
参考）[AsIs](https://wiki.evolveum.com/display/midPoint/Expression#Expression-AsIs)

expression にスクリプトを記述することで、複雑な変換も可能となります。
先の設定例「オブジェクト・テンプレート」の mapping における expression が Scriptです。
参考）[Script](https://wiki.evolveum.com/display/midPoint/Expression#Expression-Script)

スクリプトの利用において、最初に戸惑いやすいポイントについて、いくつか説明していきます。

## その１）PolyString

mapping の expression のスクリプト（言語のデフォルトはGroovy）を、以下のように変更したとします。

#### ユーザーの（givenName、familyName）から（fullName）を設定する mapping

```:オブジェクト・テンプレート
    <mapping>
        <strength>strong</strength>
        <source>
            <path>givenName</path>
        </source>
        <source>
            <path>familyName</path>
        </source>
        <expression>
            <script>
                <code>
                    if ('Alice' == givenName) {
                        retturn 'アリス ' + familyName
                    }
                    givenName + ' ' + familyName
                </code>
            </script>
        </expression>
        <target>
            <path>fullName</path>
        </target>
    </mapping>
```

givenNameがAliceの場合、if文の中に入ることを期待しているようですが、**この条件がtrueになることはありません**。原因を調べるため、givenName をログに出力してみましょう。
参考）[Logging Library](https://wiki.evolveum.com/display/midPoint/Script+Expression+Functions#ScriptExpressionFunctions-LoggingLibrary)

```:オブジェクト・テンプレート
                <code>
                    log.info('{}, {}', givenName, givenName.class)
                    …
```
```:ログ出力
… Alice, class com.evolveum.midpoint.prism.polystring.PolyString
```

値は Alice のようですが、class が PolyString となっています。
参考）[PolyString.java](https://github.com/Evolveum/midpoint/blob/master/infra/prism-api/src/main/java/com/evolveum/midpoint/prism/polystring/PolyString.java)

```:PolyString.java
public class PolyString implements Matchable<PolyString>, Recomputable, Structured, DebugDumpable, ShortDumpable, Serializable, Comparable<Object> {
```

PolyStringはString派生型ではないため、Stringとの==がtrueとなることはありません。
(Groovyの==の挙動については、後述します。)


### PolyString とは？

こちらに説明があります。
- [PolyString](https://wiki.evolveum.com/display/midPoint/PolyString)
- [PolyString Normalization Configuration](https://wiki.evolveum.com/display/midPoint/PolyString+Normalization+Configuration)

PolyStringは、国際対応のために、文字列を2つの異なる形式で保存しています。
（polyは「複数の～」という接頭辞で、「ポリゴン」（多角形）の「ポリ」と同じですね。）

|形式|説明|例|
|:--|:--|:--|
|元の形式（orig）|ユーザーが入力したテキスト（国際文字、任意の数の空白などが含まれる場合がある）|Coup D'état|
|正規化された形式（norm）|PolyString正規化設定に応じて、元の形式から自動的に導出|coup detat|

origとnormはString型です。PolyStringのgetOrig()およびtoString()はorigを返します。
試しにログ出力してみましょう。

```
                <code>
                    log.info("givenName?.orig       : ${givenName?.orig}")
                    log.info("givenName?.norm       : ${givenName?.norm}")
                    log.info("givenName?.toString() : ${givenName?.toString()}")
                    …
```
```:ログ出力
… givenName?.orig       : Alice
… givenName?.norm       : alice
… givenName?.toString() : Alice
```

### 対処

対処としては、PolyStringから元の形式（orig）をgetOrig()で取り出すか、基本関数ライブラリを使用してStringに変換します。

- 参考）[Basic Library](https://wiki.evolveum.com/display/midPoint/Script+Expression+Functions#ScriptExpressionFunctions-BasicLibrary)

> 基本関数ライブラリは、文字列操作、オブジェクトプロパティの取得などのための非常に基本的な関数を提供します。これらは、シンプルで効率的なスタンドアロン関数です。

基本関数ライブラリのstringify関数は、引数に渡したオブジェクトをStringに変換してくれます。stringifyはStringとPolyStringのどちらにも使えて、Stringの場合はそのまま返却、PolyStringの場合はorigを取り出して返却します。
参考）[BasicExpressionFunctions.java](https://github.com/Evolveum/midpoint/blob/master/model/model-common/src/main/java/com/evolveum/midpoint/model/common/expression/functions/BasicExpressionFunctions.java)

stringifyを使用してコードを書き換えてみましょう。意図した結果になります。

```:オブジェクト・テンプレート
                <code>
                    if ('Alice' == basic.stringify(givenName)) {
                        return 'アリス ' + familyName
                    }
                    givenName + ' ' + familyName
                </code>
```

#### 備考１）文字列属性がすべてPolyStringというわけではありません

たとえば emailAddress は通常のString型です。
参考）[UserType](https://evolveum.com/downloads/midpoint/4.0.1/midpoint-4.0.1-schemadoc/http---midpoint-evolveum-com-xml-ns-public-common-common-3/object/UserType.html)

また、上記の例が「mapping」であることにも注意してください。source は midPointオブジェクトで、入力値は UserTypeのgivenNameでした。これが「inbound」のfirstnameの場合であれば、sourceはリソースからの文字列で、通常のStringです。

#### 備考２）Groovyの==の挙動について

> groovyにおいて a == b は、比較可能であれば a.compareTo(b) == 0 に変換され、そうでなければ a.equals(b) に変換されます

参考）[Groovy Language Documentation 3.2.11. Behaviour of ==](http://docs.groovy-lang.org/latest/html/documentation/#_behaviour_of_code_code)

上記コードの判定式で、左辺値にStringを持ってきていますが、これは説明の都合上で、意図的です。

執筆時点のPolyStringのJava実装は、origが比較対象のStringと等しいとき、

- equals が `false`（不一致）
- compareTo が `0`（一致）

を返します。
PolyStringを左辺値とすると、PolyStringのequals では false ですが == は true となり、混乱してしまうので、説明上の都合でこれを回避しました。

```:（確認用コード）
<code>
import com.evolveum.midpoint.prism.polystring.PolyString

s  = 'a'
ps = new PolyString('a')

log.info('{}', s.equals(ps) )   // false
log.info('{}', ps.equals(s) )   // false

log.info('{}', s == ps )        // false
log.info('{}', ps == s )        // true  ※

log.info('{}', ps.compareTo(s)) // 0(一致) ※equalsでは不一致と判定している比較が、一致と判定されている
```

この挙動はそのうち変更されるかもしれませんが、現時点での予備知識として展開しておきます。

## その２）midPointのデータ表現、Prism

Prism（プリズム）は、midPointのデータ構造です。

- Prismは、midPointシステム全体でユニバーサルデータ表現として使用されます
- すべてのmidPointコンポーネントは、プリズムオブジェクトで機能します
- Prismは、複数の形式で同時にデータを表示できるデータ表現メカニズムです
- ネイティブPrismインターフェイス（API）を使用してアクセスできますが、同じデータは他のさまざまなインターフェイス（JAXB、DOMなど）からもアクセスできます

参考）[Prism Objects](https://wiki.evolveum.com/display/midPoint/Prism+Objects)

![prism.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/533702/f3cc083e-1858-a980-2ccc-0171eeb7ea10.png)
図の引用元：[Prism Objects](https://wiki.evolveum.com/display/midPoint/Prism+Objects#PrismObjects-Introduction)

### midPoint内部へアクセスしてみる

Prismは、midPoint関数ライブラリを使用する際に、意識することになります。

> midPoint関数ライブラリは、midPoint内部へのアクセスを提供します。 IDM固有およびmidPoint固有のロジックを含む複雑な機能を提供します。

参考）[MidPoint Library](https://wiki.evolveum.com/display/midPoint/Script+Expression+Functions#ScriptExpressionFunctions-MidPointLibrary
https://wiki.evolveum.com/display/midPoint/MidPoint+Script+Library)

オブジェクト・テンプレートに、descriptionへのマッピングを追加してみます。
式には以下のようなスクリプトを設定しました。

#### ユーザーを検索し、自分と同じ姓の人を見つけたら、そのことをdescriptionに出力

```:オブジェクト・テンプレート
    <mapping>
        <strength>strong</strength>
        <source>
            <path>familyName</path>
        </source>
        <expression>
            <script>
                <code>
                    import com.evolveum.midpoint.prism.impl.query.builder.QueryBuilder
                    import com.evolveum.midpoint.xml.ns._public.common.common_3.UserType

                    if (familyName) {
                        query = QueryBuilder.queryFor(UserType.class, prismContext)
                            .item(UserType.F_FAMILY_NAME).eq(familyName)
                            .build()

                        user = midpoint.searchObjects(UserType.class, query).find{it.oid != focus?.oid}
                        if (user) {
                            return "${user.name}(${user.givenName})の親戚かも？"
                        }
                    }
                </code>
            </script>
        </expression>
        <target>
            <path>description</path>
        </target>
    </mapping>
```
midPoint関数ライブラリの midpoint.searchObjects でユーザーを検索しています。
注目する箇所は、検索対象として item に指定している UserType.F_FAMILY_NAME で、これは QName（名前空間内の名前）です。値は以下のようなものです。

```:UserType.F_FAMILY_NAME相当
import javax.xml.namespace.QName
new QName("http://midpoint.evolveum.com/xml/ns/public/common/common-3", "familyName")
```

Prismデータ構造は拡張可能であるように設計されていて、これを可能にしている主なメカニズムが、ネームスペースです。Prismはコンテナ構造となっていて、構成要素のアイテムの名前はQName（名前空間内の名前）です。上記コードがPrismデータ構造にアクセスしているのが分かります。

### midPoint関数ライブラリの戻り値について

上記コードに、１つ不思議な点があるのが分かるでしょうか？
Prismデータ構造にはQNameでアクセスする、というようなことを説明しておいて、取得結果に対しては`user.givenName`（Javaだとuser.getGivenName()）のように、setter/getterアクセスしています。
本来はwikiにある例のように、ネイティブPrismインタフェースでQNameを指定してアクセスする必要があるのではないでしょうか？

```:wikiの例
// Invoking native Prism interface to read fullName, returns "Jack Sparrow"
PrismObject<UserType> userPrism = ....
String fullName = userPrism.findProperty(new QName(NS_C, "fullName").getValue().getValue();
```

参考）[Prism Interfaces](https://wiki.evolveum.com/display/midPoint/Prism+Objects#PrismObjects-PrismInterfaces)

これはライブラリが、setter/getterアクセスできるCompile-time class インタフェースに切り替えて戻してくれている(toObjectableListの箇所)ためです。

```:MidpointFunctionsImpl.java
	public <T extends ObjectType> List<T> searchObjects(
			Class<T> type, ObjectQuery query) throws SchemaException,
			ObjectNotFoundException, SecurityViolationException,
			CommunicationException, ConfigurationException, ExpressionEvaluationException {
		return MiscSchemaUtil.toObjectableList(
				modelService.searchObjects(type, query, 
						getDefaultGetOptionCollection(), getCurrentTask(), getCurrentResult()));
	}
```
参考）[MidpointFunctionsImpl.java](https://github.com/Evolveum/midpoint/blob/master/model/model-impl/src/main/java/com/evolveum/midpoint/model/impl/expr/MidpointFunctionsImpl.java)

インタフェースの切り替えについては、以下に記載があります。
参考）[Prism Duality](https://wiki.evolveum.com/display/midPoint/Using+Prism+Objects#UsingPrismObjects-PrismDuality)

例えば、ネイティブPrismインタフェース と Compile-time classインタフェース の間であれば、以下のように変換できます。

　　Native prism interface
asObjectable()　↓　↑　asPrismObject()
　　Compile-time class interface



### PolyString再び

ここで１点、PolyStringについての注意点があります。
上記コードの最後の部分を以下のように修正したとします

#### 検索された同姓ユーザーの givenName が A始まりの場合だけ、値を設定

```:オブジェクト・テンプレート
                        …
                        if (user) {
                            if ( user.givenName.startsWith('A') ) {
                                return "${user.name}(${user.givenName})の親戚かも？"
                            }
                        }
                    }
                </code>
```

しかし、これはエラーになります。

```:ログ出力
… No signature of method: com.evolveum.prism.xml.ns._public.types_3.PolyStringType.startsWith() is applicable for argument types: (String) values: …
```

startsWith(String) が無い？　givenName は PolyString ですので、Stringと違ってstartsWithの実装がないのでしょうか？　しかし確認してみると、PolyStringの実装には存在しています。

```:PolyString.java
    public boolean startsWith(String value) {
        return this.orig.startsWith(value);
    }
```
参考）[PolyString.java](https://github.com/Evolveum/midpoint/blob/master/infra/prism-api/src/main/java/com/evolveum/midpoint/prism/polystring/PolyString.java)

どういうことでしょうか？　エラーをもう一度よく見てみます。エラーになっているのは確かに、PolyString…。いや、PolyString…**Type**？　誰ですか、あなた！

実は、ライブラリが返してくれる Compile-time class インタフェースにおいて、PolyString は PolyStringType となっていて、実装が異なります。
PolyStringType の実装を確認してみると、確かに startsWith がありません。

参考）[PolyStringType.java](https://github.com/Evolveum/midpoint/blob/master/infra/prism-api/src/main/java/com/evolveum/prism/xml/ns/_public/types_3/PolyStringType.java)

PolyStringとPolyStringTypeが存在している経緯が、以下に説明されています。

参考）[PolyString and PolyStringType](https://wiki.evolveum.com/display/midPoint/Using+Prism+Objects#UsingPrismObjects-PolyStringandPolyStringType)

> この混乱の原因は、主にJAXBフレームワークの制限が原因です。 JAXBフレームワークを使用して、XSDスキーマを解析し、コンパイル時クラスを生成しています。
生成されたすべてのコンパイル時クラスの起源はXSD定義にあるため、それらのプロパティタイプもすべてXSD定義を持っている必要があります。これにはPolyStringが含まれます。
JAXBの重要な部分を変更およびカスタマイズしましたが、この混乱を回避する方法は見つかりませんでした。

ただ、このことさえ理解していれば、対応はPolyStringと同様です。
origを取り出して処理するか、basic.stringifyでStringにします。（basic.stringifyはPolyStringTypeも処理してくれます。）

```:オブジェクト・テンプレート
                        …
                        if (user) {
                            if ( basic.stringify(user.givenName).startsWith('A') ) {
                                return "${user.name}(${user.givenName})の親戚かも？"
                            }
                        }
                    }
                </code>
```

# まとめ

midPointにおける「マッピング」、特にスクリプトの利用で最初に戸惑いやすいポイントについて、いくつか説明してみました。
スクリプトを利用していて、意図した動作にならない場合は、midPointのデータ構造と、処理で使用しているインタフェースを意識して、確認してみましょう。