REST API による Db2アクセス

## はじめに

RDBであるIBM Db2 データベースのアクセス方法として、

 * JavaやCなどで書かれたプログラムから接続して、SQLを実行する
 * シェルスクリプトからコマンドを実行して、ExportやLoadなどを実行する

といった使われ方が最も多いと思いますが、Db2 11.5 からは REST API によるアクセスもできるようになっています。
任意のSQLを実行するRESTサービスを登録しておき、呼び出すことができます。

## Db2 における REST API サポート

DBaaSである Db2 on Cloud, Db2 Warehouse on Cloud の場合には
テーブル作成/削除、Load、バックアップ・リストア、ユーザ管理、性能監視、CPUコア数やストレージ数の変更、災害発生時のサーバ切替、、など非常に数多くの REST API がIBMから提供/公開されています。

一方で、オンプレミス版やKubernetes版のDb2に関しては
そういった便利な既製APIの存在はマニュアルに言及されません。
オンプレミス版Db2における "REST APIサポート" とは、以下を指しているようです。

 * RESTサービスの作成と実行
     * ユーザーが事前に登録したSQL(SELECT/INSERT/UPDATE/DELETE) をRESTサービスとして実行
     * SQLの結果セットはjson形式で受け取る
     * サービスは同期実行、バッチジョブとしての非同期実行ともに可能
 * 任意のSQLを実行可能 
     * /v1/services/execsql エンドポイントを使用して SQL を直接実行

## Db2におけるRESTアクセスのおおまかな流れ

今回は、SELECT文を実行して1行の結果セットを受け取る簡単なサービスを作成/実行します。
インターフェースにはcurlコマンドを利用します。

Step1. 事前セットアップ
Step2. Db2 RESTサービス・コンテナ起動
Step3. 認証情報(token)取得
Step4. RESTサービス作成
Step5. RESTサービス呼び出し


## 環境

Db2 REST サービスは、RESTサービス・コンテナを構成することで利用可能になります。
今回は、Db2が稼働するサーバとは別のサーバにRESTサービス・コンテナを立てる構成としています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/468682/db779523-6081-2570-3bff-3191844a87d3.png)

 * Db2 データベース稼働サーバ
     * RHEL 7.9
     * Db2 11.5.6

* Db2 RESTサービス・コンテナ稼働サーバ
     * WSL2/Ubuntu 22.04
     * podman version 3.4.4
     * Db2 Rest Service Container 
     * curl 7.81.0


## Step1. 事前セットアップ

RESTサービスを定義する前に、セットアップが必要となります。
これがかなり手数が多く、面倒です。長くなるので別記事に整理します。

　別記事 → [REST API による Db2アクセスのための事前準備](https://qiita.com/mi-kana/items/5ae6d3a719e51bc6162c)

## Step2. Db2 REST サービス・コンテナ起動

事前セットアップの中でRESTサービス・コンテナを作成/起動していますが、
時間が経過した場合などコンテナが停止状態の場合は、起動しておきます。

```
kanako@Ubuntu-Rest:~$ podman start db2rest
db2rest
```

#### 補足：Db2 REST サービスドキュメント
Db2 RESTサービス・コンテナを立てると、RESTのドキュメントを参照できるようになり、
どんな操作が可能であるか、確認できます。
(webで見られる [Db2製品マニュアル](https://www.ibm.com/docs/ja/db2/11.5) とはまた別にドキュメントが提供されています)

__REST ドキュメントURL：__
https://\<db2restservice_container_host>:50050//docs
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/468682/f1b4cfeb-b51a-001c-ed0b-c738fe9adca9.png)

## Step3. 認証情報の取得
Db2 RESTサービスの作成/呼び出しにあたり、認証情報トークンを付与する必要があります。
そこであらかじめ、トークンを取得します。
トークンは、DB接続情報をHTTPリクエストボディ(-dオプション)に記述して、URL「/v1/auth」にPOSTメソッドでアクセスすることで取得します。

__REST docs 引用：__
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/468682/f826e056-3c94-8595-7fd5-4641b0397817.png)

DBサーバーへの接続情報を渡すため、dbParamsに下記の値を指定します。
 * DBサーバーのホスト名
 * DBサーバーのListenポート
 * SSL(TLS)接続有無
 * データベース名
 * DB接続ユーザー名
 * パスワード

また、今回はnon-SSL環境であるため、--insecure オプションを指定してSSL関連の警告を無視するモードでcurlコマンドを実行しています。

__コマンド例：__
```
curl -v --insecure -X POST \
  https://localhost:50050/v1/auth \
  -H "content-type: application/json" \
  -d '{ 
      "dbParms": {
        "dbHost": "DBサーバホスト名", 
        "dbName": "データベース名", 
        "dbPort": DBサーバListenポート,
        "isSSLConnection": false,
        "username": "DB接続ユーザ",
        "password": "DB接続ユーザパスワード"
      },
      "expiryTime": "24h" }'
```

__実行例：__
```
kanako@Ubuntu-Rest:~$ curl --insecure -X POST \
>   https://localhost:50050/v1/auth \
>   -H "content-type: application/json" \
>   -d '{
>       "dbParms": {
>         "dbHost": "testserver",
>         "dbName": "TESTDB2",
>         "dbPort": 50000,
>         "isSSLConnection": false,
>         "username": "db2inst1",
>         "password": "パスワード文字列"
>       },
>       "expiryTime": "24h" }'
{"token":"eyJh..(以下略)"}
```

"token"というキー名の右に、長い文字列が取得されているのがtoken文字列です。

実際のトークンは512文字程度の文字列です。
以降、トークン文字列は「トークン文字列」と表記します。



## Step4. RESTサービス作成

今回作成するRESTサービスの概要は以下の通りです。

 * サービス名は「sql_max_t2」 (→/v1/services/sql_max_t2/<version> というURLで呼び出す)
 * サービス呼び出しを受けて「"SELECT max(C1) FROM T2"」というSQLを実行し、結果セットを戻す

#### 前提(権限)：
サービスの作成、更新、削除にあたっては、以下の権限が必要です。
 * メタデータ表に対する SELECT、INSERT、UPDATE、および DELETE 特権。
 * サービス作成呼び出しに関連するスキーマに対して CREATE SCHEMA または DROP SCHEMA ステートメントを実行する権限。

#### Step4-1. サービス作成
RESTサービス作成について、RESTサービス用ドキュメントに詳細なガイドがあります。

サービスを新規作成するには、「POST /v1/services」にサービス作成用の引数を与えて実行します。
各言語ごとのサンプルコマンドが、上記RESTサービス用ドキュメントの右の黒画面に提供されます。これをコピー＆ペーストして、コード作成を開始することができます。

__REST docs 引用：__
https://\<db2restservice_container_host>:50050/docs#operation/createService
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/468682/e95d8e2d-b1d1-6524-9225-beac78a37443.png)


サンプルコマンドのうち、下記オプションについては環境に合わせて編集します。

 * authorization: 「Step3. 認証情報の取得」で取得したトークン文字列
 * serviceName: サービス名
     * 新規作成するサービスのURL文字列をここで命名します
     * 大文字/小文字が区別されます
 * serviceDescription: サービスについての説明
 * version: バージョン
 * sqlStatement: このサービスが呼び出されたときに実行するSQLステートメント
 

__実行コマンド：__
```
curl --insecure -X POST \
  https://localhost:50050/v1/services \
  -H "authorization: トークン文字列" \
  -H "content-type: application/json" \
  -d '{ 
      "serviceName": "sql_max_t2", 
      "serviceDescription": "get the maximum value of c1 column", 
      "version": "1.0", 
      "sqlStatement": "SELECT max(C1) FROM T2" }'
```

__実行結果：__
```
kanako@Ubuntu-Rest:~$ curl --insecure -X POST \
>   https://localhost:50050/v1/services \
>   -H "authorization: eyJhb..(以下略)" \
>   -H content-type: application/json" \
>   -d '{
>       "serviceName": "sql_max_t2",
>       "serviceDescription": "get the maximum value of c1 column",
>       "version": "1.0",
>       "sqlStatement": "SELECT max(C1) FROM T2" }'
kanako@Ubuntu-Rest:~$
```

#### Step4-2. RESTサービスの存在確認

作成されたすべてのRESTサービスをリストし、Step4-1で作成した「sql_max_t2」が存在することを確認します。
```
kanako@IBM-PF2E8K5F:~$ curl --insecure -X GET \
>   https://localhost:50050/v1/services \
>   -H "authorization: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG1pbiI6dHJ1ZSwiY2xpZW50X2lkIjoiZWJmZWI3NmItYzZmZC00N2MwLWI3YmYtN2Y0YjEzZTUwN2Y2IiwiZXhwIjoxNjY0NTg3ODgyLCJpc3MiOiJkYjJpbnN0MSJ9.rlea5MUbOLdAL4jf9EU0k5R1jTQV5Q42cKy-tlOsfyXUNrZYaNYPk3IyUeQ1_jEsZx8-P6GLjZd9rsaehpauABKhxo53FYka81daGcYjGR3-PA9K3PFOuasz5wyVDOpy6uPEeBVj03wxyDFSxgCU7gLiQ6N4ysHq1f_4rPc_967tkCVbW0Ht8DehUUCtAVDmVOT2yEDC4Pia8XHv_gDrVaIdU4fJORP24bmWsbdlgEkrapKyuWpufLqkASqkdRtG4kaGIlf06fa8WTIpg5lIOGNASXsjlrOwoaW5iBuOdtJ7PN_3Rr5QKayWgsyLj-1EleZP_ljcDwXHkUg2Rp6XfA" \
>   -H "content-type: application/json"
{"Db2Services":[{"lastModified":"2022-09-07T03:29:20.157461Z","serviceCreator":"db2inst1","serviceDescription":"get the maximum value of c1 column","serviceName":"sql_max_t2","serviceUpdater":"db2inst1","version":"1.0"},{"lastModified":"2022-09-27T01:47:29.457954Z","serviceCreator":"db2inst1","serviceDescription":"Lists the t1 table","serviceName":"LISTT1","serviceUpdater":"db2inst1","version":"1.0"}]}
kanako@IBM-PF2E8K5F:~$
```

#### Step4-3. (オプション)RESTサービスの実体であるプロシージャの内容確認

サービス作成が完了すると、「Step1.事前セットアップ」の中で作成された
RESTSERVICE表に、作成したRESTサービスのレコードが1行挿入されます。

__確認用SQLと実行結果：__
```
[db2inst1@4c4fd18c3faf ~]$ db2 "select * from db2rest.restservice"
NAME             VERSION  DESCRIPTION                                                      PROCSCHEMA       PROCNAME         ISQUERY SERVICE_CREATOR  SERVICE_UPDATER  LAST_MODIFIED
---------------- -------- ---------------------------------------------------------------- ---------------- ---------------- ------- ---------------- ---------------- --------------------------
sql_max_t2       1.0      get the maximum value of c1 column                               DB2INST1         REST_SQL_MAX_T2_1_0              Y       db2inst1         db2inst1         2022-09-07-03.29.20.157461

  1 record(s) selected.

[db2inst1@4c4fd18c3faf ~]$
```

このRESTSERVICE表から、作成されたRESTサービス名、バージョン、説明、作成者、最終変更日時などの情報が確認できます。
しかし、実行されるSQLステートメントについてはこの表には格納されません。
RESTサービスの実体はプロシージャであり、このプロシージャの定義は、カタログビューSYSCAT.PROCEDURESにて確認することができます。
RESTサービス作成時に指定したSQLステートメントも、このカタログビューにて確認可能です。

__確認用SQL：__
RESTSERVICE表に格納されるプロシージャ名を条件指定し、1つのプロシージャに絞って検索します。
```
$ db2 "select substr(procschema, 1,16) as procschema, substr(procname,1,64) as procname, create_time, language, fenced, substr(class,1,16) as class, program_type, result_sets, substr(text,1,1000) as text from syscat.procedures where procname='REST_SQL_MAX_T2_1_0'"
```

__確認結果：__
| カタログ列名 | 値 |
|:-:|:-|
| PROCSCHEMA  | DB2INST1  |
| PROCNAME  | REST_SQL_MAX_T2_1_0  |
| CREATE_TIME   | 2022-09-07-03.29.20.214406  |
| LANGUAGE | SQL   |
| FENCED  | (空白)  |
| CLASS  | -  |
| PROGRAM_TYPE  |M   |
| RESULT_SETS  | 1  |
| TEXT  |  CREATE PROCEDURE "DB2INST1"."REST_SQL_MAX_T2_1_0" ()<br> LANGUAGE SQL<br>SPECIFIC "DB2INST1"."REST_SQL_MAX_T2_1_0"<br>DYNAMIC RESULT SETS 1<br>BEGIN<br>DECLARE C1 CURSOR WITH RETURN FOR<br>-- BEGIN SQL STATEMENT<br>SELECT max(C1) FROM T2;<br>-- END SQL STATEMENT<br>OPEN C1;<br>END<br> |



このように、「REST_SQL_MAX_T2_1_0」という名前のSQLプロシージャが、RESTサービス作成時刻近辺で作成されたことがわかります。

TEXT列にはプロシージャの定義文が格納され、RESTサービス作成時に指定したSQL文字列も確認することができます。
Db2にどんなRESTサービスが定義されているかわからないときは、
RESTSERVICE表を参照し、RESTサービスの実体であるSQLプロシージャの名前を特定し、
SYSCAT.PROCEDURESカタログビューでSQLステートメントを確認すると良さそうです。



## Step5. RESTサービスの呼び出し

RESTサービス呼び出しについても、RESTサービス用マニュアルに詳細なガイドがあります。

__REST docs 引用：__
https://\<db2restservice_container_host>:50050/docs#operation/invokeService
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/468682/4955ec4e-0caa-d568-05f6-e24b496917fd.png)


ここでは、Step4で作成したRESTサービス「sql_max_t2」を、curlで呼び出してみます。
「POST /v1/services」に続けて、「/サービス名/バージョン」を指定します。
　(→ URLは「/v1/services/sql_max_t2/1.0」となります)

__RESTサービス呼び出し(コマンド)：__
```
curl --insecure -X POST \
  https://localhost:50050/v1/services/sql_max_t2/1.0 \
  -H "authorization: トークン文字列" \
  -H "content-type: application/json" \
  -d '{ 
      "sync": true }'
```

登録済のRESTサービスが呼び出され、ジョブステータスコードと結果セットがjson形式で返却されます。

__RESTサービス呼び出し(実行結果)：__
```
kanako@Ubuntu-Rest:~$ curl --insecure -X POST \
>   https://localhost:50050/v1/services/sql_max_t2/1.0 \
>   -H "authorization: eyJh..(以下略)" \
>   -H "content-type: application/json" \
>   -d '{
>       "sync": true }'
{"jobStatus":4,"jobStatusDescription":"Job is complete","resultSet":[{"1":15000}],"rowCount":1}
kanako@Ubuntu-Rest:~$
```

想定通り、結果が取得されました。jobStatus:4 は処理完了を表すコードです。
なお、ジョブステータスコードは下記の種類があります。

 * 0 - ジョブの実行に失敗した。
 * 1 - ジョブはキューに入れられている。
 * 2 - ジョブは実行中。
 * 3 - データが使用可能。 
     * 非同期要求時に戻されるコード(同期実行の場合は登場しない)
     * 例えば、非同期要求が 2500 個のレコードを返すものの、要求が各応答のレコード数を最大 1000 個に制限している場合、最初の 1000 個のレコードを含む最初の応答が受信されると、要求の状況は 3 に変わります。 要求は、最後の 500 個のレコードを含む最終応答を受信するまで状況 3 のままとなり、最終応答を受信した時点で状況 4 に変わります。
 * 4 - ジョブが完了した。
 * 5 - ジョブはキャンセルされ、停止している。

参考：[Db2マニュアル「REST サービスを使用するアプリケーションの作成」](https://www.ibm.com/docs/ja/db2/11.5?topic=endpoints-creating-applications-that-use-rest-services)


## 補足：RESTサービス実行ユーザの権限
今回は簡易検証のため全手順をインスタンスオーナー(db2inst1)で行っていますが、
サービス作成者と利用者とで、使うユーザーを分けて、与える権限も分けることが望ましいです。
RESTサービスを作成しただけでは、作成者以外のユーザーから呼び出すことはできないため、サービスの実体となるプロシージャの実行権限を付与しておく必要があります。

参考：[Db2マニュアル「Db2 で REST エンドポイントを操作するために必要な特権」](https://www.ibm.com/docs/ja/db2/11.5?topic=endpoints-required-privileges)



## 補足：RESTサービス実行時の詳細出力の出し方(curl)
Step4.で出力例を貼っている通り、curlコマンドでRESTサービスを作成すると、標準出力に何も表示されないため、RESTSERVICE表にエントリーが追加されたかどうかを見るまで実行結果がわかりません。

curlコマンドに -v オプションを付与して実行すると、以下のような情報が確認できて非常にわかりやすくなります。
 * 実行結果(成否がHTTPステータスコードでわかります)
 * 通信プロトコルのバージョン
 * TLSハンドシェークの状況や、問題が起きている場合はエラーの発生状況
 * クライアントから送信されたHTTPヘッダの内容
     * 認証トークンなど
 * リクエスト実行完了後の接続の状態(確立された接続がそのまま残されているか)

__実行結果(RESTサービス作成)：__
```
kanako@Ubuntu-Rest:~$ curl -v --insecure -X POST \
>   https://localhost:50050/v1/services \
>   -H "authorization: eyJh..(以下略)" \
>   -H "content-type: application/json" \
>   -d '{
>       "serviceName": "sql_max_t2",
>       "serviceDescription": "get the maximum value of c1 column",
>       "version": "1.0",
>       "sqlStatement": "SELECT max(C1) FROM T2" }'
Note: Unnecessary use of -X or --request, POST is already inferred.
*   Trying 127.0.0.1:50050...
* Connected to localhost (127.0.0.1) port 50050 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* TLSv1.0 (OUT), TLS header, Certificate Status (22):
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS header, Certificate Status (22):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS header, Certificate Status (22):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS header, Certificate Status (22):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS header, Certificate Status (22):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS header, Certificate Status (22):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS header, Finished (20):
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS header, Certificate Status (22):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS header, Finished (20):
* TLSv1.2 (IN), TLS header, Certificate Status (22):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES256-GCM-SHA384
* ALPN, server accepted to use http/1.1
* Server certificate:
*  subject: C=CA; ST=ON; L=TO; O=IBM=OU=Db2Rest; CN=ibm.com
*  start date: Sep  6 09:11:59 2022 GMT
*  expire date: Sep  6 09:11:59 2023 GMT
*  issuer: C=CA; ST=ON; L=TO; O=IBM=OU=Db2Rest; CN=ibm.com
*  SSL certificate verify result: self-signed certificate (18), continuing anyway.
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
> POST /v1/services HTTP/1.1
> Host: localhost:50050
> User-Agent: curl/7.81.0
> Accept: */*
> authorization: eyJh..(以下略)
> content-type: application/json
> Content-Length: 179
>
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* Mark bundle as not supporting multiuse
< HTTP/1.1 201 Created　　　　　　　　　　　　   ←--- ステータスコード201(作成成功)
< Vary: Origin
< Date: Wed, 07 Sep 2022 03:29:20 GMT
< Content-Length: 0
<
* Connection #0 to host localhost left intact   ←--- DBサーバへの接続が残されている
kanako@Ubuntu-Rest:~$

```

同様に、定義済RESTサービスを実行する際にも -v オプションを付与すると
詳細の情報が得られます。

__実行結果(RESTサービス実行)：__
```
kanako@Ubuntu-Rest:~$ curl -v --insecure -X POST \
>   https://localhost:50050/v1/services/sql_max_t2/1.0 \
>   -H "authorization: eyJh..(以下略)" \
>   -H "content-type: application/json" \
{
>   -d '{
>       "sync": true }'
Note: Unnecessary use of -X or --request, POST is already inferred.
*   Trying 127.0.0.1:50050...
* Connected to localhost (127.0.0.1) port 50050 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* TLSv1.0 (OUT), TLS header, Certificate Status (22):
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS header, Certificate Status (22):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS header, Certificate Status (22):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS header, Certificate Status (22):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS header, Certificate Status (22):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS header, Certificate Status (22):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS header, Finished (20):
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS header, Certificate Status (22):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS header, Finished (20):
* TLSv1.2 (IN), TLS header, Certificate Status (22):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES256-GCM-SHA384
* ALPN, server accepted to use http/1.1
* Server certificate:
*  subject: C=CA; ST=ON; L=TO; O=IBM=OU=Db2Rest; CN=ibm.com
*  start date: Sep  6 09:11:59 2022 GMT
*  expire date: Sep  6 09:11:59 2023 GMT
*  issuer: C=CA; ST=ON; L=TO; O=IBM=OU=Db2Rest; CN=ibm.com
*  SSL certificate verify result: self-signed certificate (18), continuing anyway.
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
> POST /v1/services/sql_max_t2/1.0 HTTP/1.1
> Host: localhost:50050
> User-Agent: curl/7.81.0
> Accept: */*
> authorization: eyJh..(以下略)
> content-type: application/json
> Content-Length: 23
>
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Content-Type: application/json
< Vary: Origin
< Date: Wed, 07 Sep 2022 03:34:12 GMT
< Content-Length: 96
<
{"jobStatus":4,"jobStatusDescription":"Job is complete","resultSet":[{"1":15000}],"rowCount":1}
* Connection #0 to host localhost left intact
kanako@Ubuntu-Rest:~$
```


## 参考情報

[Db2マニュアル「Db2 REST サービス」](https://www.ibm.com/docs/ja/db2/11.5?topic=da-db2-rest-service)
[Db2 on Cloud REST API](https://cloud.ibm.com/apidocs/db2-on-cloud/db2-on-cloud-v4)
[Db2 Warehouse on CLoud REST API](https://cloud.ibm.com/apidocs/db2-warehouse-on-cloud/db2-warehouse-on-cloud-v4)