【RAP】Actionを追加する

##はじめに
この記事ではRAP(ABAP RESTful Application Programming Model)のActionを実装します。[Action](https://help.sap.com/viewer/923180ddb98240829d935862025004d6/Cloud/en-US/83bad707a5a241a2ae93953d81d17a6b.html)とは、標準で実装されるCRUD以外で、ビジネスオブジェクトの値を変更するために定義するものです。

シリーズの先頭はこちら☞ [【RAP】ADTのRAP Generatorを使ってみる](https://qiita.com/tami/items/a9f3d2d4afb28d53919b#%E7%B6%9A%E7%B7%A8)

##シナリオ
[こちら](https://qiita.com/tami/items/a9f3d2d4afb28d53919b)の記事で生成したビジネスオブジェクトに、以下の機能を追加します。

1. Actionを使ってステータスを変更
1. Actionの有効/無効の制御

###1. Actionを使ってステータスを変更
ビジネスオブジェクトにStatusという項目を追加し、UIからステータスを変更するためのActionを実装します。

####1.1. Status項目を追加
まず、テーブルにStatusの項目を追加します。

```abap
define table zperson_m {
  key client            : abap.clnt not null;
  key person_uuid       : sysuuid_x16 not null;
  first_name            : abap.char(100);
  last_name             : abap.char(100);
  email                 : ze_email120;
  birthday              : abap.dats;
  status                : abap.char(1); //追加
  ...

}
```

画面に項目が表示されるよう、Interface ViewおよびConsumption Viewにも追加します。

```abap:ZI_PERSON_M
{
  key person_uuid as PersonUUID,
  @EndUserText.label: 'First Name'
  first_name as FirstName,
  @EndUserText.label: 'Last Name'  
  last_name as LastName,
  @EndUserText.label: 'Email' 
  email as Email,
  @EndUserText.label: 'Birthday'
  birthday as Birthday,
  @EndUserText.label: 'Status' //追加
  status as Status,
  ...
}
```

```abap:
{
  key PersonUUID,
  FirstName,
  LastName,
  Email,
  Birthday,
  Status, //追加
  LocalLastChangedAt  
}
```

また、Behavior Definitionのマッピングにも追加します。これを忘れていたことで、アクションのボタンを押して、ロジックが通っても項目に反映されませんでした！

```abap
  mapping for ZPERSON_M
  {
    PersonUUID = PERSON_UUID;
    FirstName = FIRST_NAME;
    LastName = LAST_NAME;
    Email = EMAIL;
    Birthday = BIRTHDAY;
    Status = STATUS; //追加
    CreatedBy = CREATED_BY;
    CreatedAt = CREATED_AT;
    LastChangedBy = LAST_CHANGED_BY;
    LocalLastChangedAt = LOCAL_LAST_CHANGED_AT;
    LastChangedAt = LAST_CHANGED_AT;
  }
```

####1.2. ドラフトテーブルを再生成
テーブルに項目を追加したので、ドラフトテーブルにも追加する必要があります。マニュアルで追加もできますが、Behavior DefinitionからQuick Fixでドラフトテーブルを再生成できます。

ドラフトテーブルにカーソルを合わせ、Ctrl + 1（Quick Fix）を実行します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/a8e81d52-8ed0-b135-f70e-8cb6cb72ddb8.png)
"Recreate draft table ..."というオプションが出てくるのでそれを選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/a29a879c-ffe6-fa42-40d5-2a515c77786e.png)
ドラフトテーブルが開きます。項目が追加されていることを確認し、有効化します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/0614f607-5230-6a5c-2018-335a431e47cd.png)
※有効化したとき、以下のようなエラーが出ました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/b075e8d2-eb3c-a2a1-b556-d852d4db9d79.png)
原因は、再生成したときに"client"の項目が"mandt"に変わったためでした。"client"に直したところ、有効化できました。

####1.3. Behavior DefinitionでActionを定義
Behavior Definitionに以下のコードを追加します。

```abap
  determination set_email on save {create; }
  validation mandatory_check on save {create; update; field FirstName, LastName; }
  
  action set_complete result [1] $self; //追加
```

**Actionの構文**
Actionの構文は以下のようになります。以下は通常のAction場合で、このほかに新規インスタンスを作るためのFactory Actionというものもありますが（コピーなどで使う）、ここでは割愛します。

```abap
[ internal][static] action
                  [(
                  [features: {instance | global}]
                  [precheck]
                  [ authorization:none]
                  [ authorization:update]
                  [lock:none]
                   )]
                   ActionName [external 'ExternalName']
                  [ InputParameter]
                  [ OutputParameter];
```

参考：[CDS BDL - action](https://help.sap.com/doc/abapdocu_cp_index_htm/CLOUD/en-US/index.htm?file=abenbdl_action.htm)

**オプションの説明**

|項目  |説明  |
|---|---|
|internal  | "internal"とつけるとそのアクションはODataからではなくDeterminationやActionなどのロジックの中からだけアクセスできる  |
|static|デフォルトで、アクションはBOのインスタンスにひもづいている(Bound Action)。"static"とつけるとそのアクションは特定のインスタンスにはひもづかなくなる(Unbound Action)|
|features|アクションの有効/無効をどのようにコントロールするかを指定する。instance:インスタンスごとに有効/無効をコントロール。global: 外部的な要因により有効/無効をコントロール（例：季節や時間帯など）|
|precheck|リクエストがtransactional bufferに届く前にチェックを実行する|
|authorization:none|Behevior definiitonでauthorization masterを指定した場合、アクションは基本的に権限チェックの対象になるが、この設定をすると権限チェックの対象外になる
|authorization:update|当該アクションに対してupdateのときと同じチェックを行う|
|lock:none|インスタンスレベルのアクションは対象のエンティティをロックするが、この設定をするとロックされない|
|external|ODataのメタデータで使用するためのアクションの名前。指定しない場合はもともとのアクション名が使用される|
|InputParameter|アクションに渡すインプットパラメータ。指定可能値は[ヘルプ](https://help.sap.com/doc/abapdocu_cp_index_htm/CLOUD/en-US/index.htm?file=abenbdl_action_input_param.htm)を参照|
|OutputParameter|アクションからの返り値。キーワード`result`の後に指定する。カーディナリティは次の4通り：`[0..1], [1], [0..*], [1..*]` 詳細は[ヘルプ](https://help.sap.com/doc/abapdocu_cp_index_htm/CLOUD/en-US/index.htm?file=abenbdl_action.htm)を参照　|

####1.4. Behavior ImplementationでActionを実装
Quick Fixでset_completeメソッドをBehaviro Implementationに追加し、以下のように実装します。

```abap
      set_complete FOR MODIFY
            IMPORTING keys FOR ACTION Person~set_complete RESULT result.
```

**1. ステータスを更新**
処理対象のインスタンスのStatusを更新します。

```abap
    "update status
    MODIFY ENTITIES OF zi_person_m IN LOCAL MODE
      ENTITY Person
        UPDATE FIELDS ( Status )
        WITH VALUE #( FOR key IN keys ( %tky = key-%tky
                                        Status = 'C' ) ). "Complete
```
ドキュメント：[MODIFY ENTITY, ENTITIES](https://help.sap.com/doc/abapdocu_cp_index_htm/CLOUD/en-US/index.htm?file=abenbdl_action_input_param.htm)


**2. 更新したエンティティを返す**
更新後のエンティティを読み込み、resultに設定します。

```abap
    "read changed data for action result
    READ ENTITIES OF zi_person_m IN LOCAL MODE
      ENTITY person
        ALL FIELDS WITH
        CORRESPONDING #( keys )
        RESULT DATA(persons).

    result = VALUE #( FOR person IN persons ( %tky = person-%tky
                                              %param = person ) ).
```
ドキュメント：[READ ENTITY, ENTITIES](https://help.sap.com/doc/abapdocu_cp_index_htm/CLOUD/en-US/index.htm?file=abenbdl_action.htm)

resultの構造は以下のようになっており、%paramがpersonの構造と対応しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/d1101c68-3d6c-70e9-e087-bcfcba03df36.png)

####1.5. Behavior ProjectionでActionを定義
UIからアクションを使えるようにするには、Behavior Projectionにもアクションを定義する必要があります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/9c1d1ef1-4fc1-738f-b325-a55ef1aa94ea.png)
以下のように、`use action ...`で追加します。


```abap
projection;
strict;
use draft;

define behavior for ZC_PERSON_M alias Person
use etag

{
  use create;
  use update;
  use delete;

  use action Edit;
  use action Activate;
  use action Discard;
  use action Resume;
  use action Prepare;

  use action set_complete;　//追加
}
```

####1.6. Metadata ExtensionでActionを追加
最後に、Metadata ExtensionにUIアノテーションを追加し、アクションがUIに表示されるようにします。FirstNameについている`@UI.lineItem`アノテーションに以下を追加します。実は対象の項目はどれでもよいのですが、今回は一番先頭の項目にしました。

```abap
 { 
    type:#FOR_ACTION,
    dataAction: 'set_complete',
    label: 'Set Complete'    
  }
```
全体では以下のようになります。

```abap
  @UI.lineItem: [ {
    position: 10 , 
    importance: #MEDIUM
  },{ 
    type:#FOR_ACTION,
    dataAction: 'set_complete',
    label: 'Set Complete'    
  } ]
  @UI.identification: [ {
    position: 10 
  } ]
  FirstName;
```

ステータスが一覧から見えるように、Statusの項目を追加しておきます。

```abap
  @UI.lineItem: [ {
    position: 50 , 
    importance: #MEDIUM
  } ]  
  Status;
```

####1.7. 動作確認
テーブルの上に"Set Complete"というボタンが追加されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/a2eeeb1d-ab40-f58e-dd46-27a6690b3881.png)

ボタンを押すとStatusに'C'が設定されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/8effc3a7-4baa-036b-f95f-98899c524d61.png)

###2. Actionの有効/無効の制御
続いて、すでに完了になっているレコードを選択した場合は"Set Complete"を押せないように、アクションの有効/無効を制御します。

####2.1. Behavior Definitionの設定
Actionの定義に`( features: instance )`を追加します。これで、アクションの有効/無効をインスタンスごとに判断できるようになります。

```abap
  action ( features: instance ) set_complete result [1] $self;
```
ドキュメント：[Dynamic Feature Control: Actions](https://help.sap.com/viewer/923180ddb98240829d935862025004d6/Cloud/en-US/9da5c7446b3244ca914ea085ddd236b6.html)

####2.2. Behavior Implementationの実装
Behavior Implementationに以下のメソッドを追加します。Quick Fixがきかなかったので、マニュアルで追加しました。

```abap
CLASS lcl_handler DEFINITION INHERITING FROM cl_abap_behavior_handler.
  PRIVATE SECTION.
    ...
        
    METHODS get_features FOR FEATURES IMPORTING keys REQUEST requested_features FOR Person RESULT result.     

ENDCLASS.

CLASS lcl_handler IMPLEMENTATION.
  
  METHOD get_features.
  ENDMETHOD.
...
```

メソッドget_featuresを以下のように実装します。

```abap
  METHOD get_features.
    READ ENTITIES OF zi_person_m IN LOCAL MODE
    ENTITY person
      FIELDS ( Status )
      WITH CORRESPONDING #( keys )
      RESULT DATA(persons).

    result = VALUE #( FOR person IN persons (
                        %tky = person-%tky
                        %action-set_complete = COND #( WHEN person-Status = 'C'
                                                       THEN if_abap_behv=>fc-o-disabled
                                                       ELSE if_abap_behv=>fc-o-enabled )
                     ) ).

  ENDMETHOD.
```
まずは、いつものように`READ ENTITIES`で処理対象のレコードを取得します。取得したテーブル（実際には1行）をループし、resultに結果を設定します。デバッグで見ると、resultsは以下の項目を持っています。featureコントロールはアクションだけでなく項目制御にも使え、resultsの構造はコントロールする対象によって変わります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/b0b90905-8f5b-a004-458e-74bab87db78c.png)

%action-<アクション名>の項目にdisabled/enabledを設定するときに、[COND](https://help.sap.com/doc/abapdocu_cp_index_htm/CLOUD/en-US/index.htm?file=abapmodify_entity_entities.htm)という命令を使ってコンパクトに書いていますが、`if~else`を使っても書くことができます。以下の1と2は同じ意味です。

```abap
  "1
  isEnabled = COND #( WHEN person-status = 'C'
                           THEN abap_false
                           ELSE abap_true ).

  "2                           
  if person-status = 'C'.
    isEnabled = abap_false.
  else.
    isEnabled = abap_true.
  endif.  
```


####2.3. 動作確認
ステータス'C'が設定された行を選択すると、アクションが有効になりません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/cd8a2836-ec78-4cb2-f578-96052ccba25d.png)
ステータスが未設定の行を選択すると、アクションが有効になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/53707271-65d1-301b-8b32-684b23a35026.png)
複数行選択したときは、有効な行が1行でもあればアクションは有効になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276021/05082850-84c3-e1a0-524b-024b56b97a56.png)