2021年12月10日金曜日

Oracle RDF Graph ServerをAutonomous Databaseで使用する(5) - REST APIを呼び出す

 Oracle RDF Graph ServerはREST APIを提供しています。Autonomous Databaseから、このREST APIを呼び出してみます。

今までのところOracle RDF Graph Serverでは、認証にorg.eclipse.jetty.security.authentication.FormAuthenticatorを使っています。REST APIを保護するには使いにくい(フォームに対してユーザー名/パスワードを送信する必要がある)ため、HTTPのBasic認証を使うように変更します。

jetty.homeのwebapps以下にあるorardf.xmlを変更します。authenticatorとしてorg.eclipse.jetty.security.authentication.BasicAuthenticatorを使うように変更したorardf.xmlは以下になります。

<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
<Configure id="orardf" class="org.eclipse.jetty.webapp.WebAppContext">
<!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -->
<!-- Required minimal context configuration : -->
<!-- + contextPath -->
<!-- + war OR resourceBase -->
<!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -->
<Set name="contextPath">/orardf</Set>
<Set name="war"><Property name="jetty.webapps" default="."/>/orardf.war</Set>
<Get name="securityHandler">
<Set name="loginService">
<New class="org.eclipse.jetty.security.HashLoginService">
<Set name="name">Test Realm</Set>
<Set name="config"><SystemProperty name="jetty.base" default="."/>/etc/realm.properties</Set>
<!-- To enable reload of realm when properties change, uncomment the following lines -->
<!--
<Set name="hotReload">true</Set>
<Call name="start"></Call>
-->
</New>
</Set>
<Set name="authenticator">
<New class="org.eclipse.jetty.security.authentication.BasicAuthenticator">
</New>
</Set>
<!--
<Set name="authenticator">
<New class="org.eclipse.jetty.security.authentication.FormAuthenticator">
<Set name="alwaysSaveUri">true</Set>
</New>
</Set>
-->
<Set name="checkWelcomeFiles">true</Set>
</Get>
</Configure>
view raw orardf.xml hosted with ❤ by GitHub

Oracle RDF Graph Serverが提供するREST APIの仕様は、ダウンロードしたアーカイブoracle-graph-webapps-xx.x.x.ziprdf-doc/orardf_swagger.jsonとして含まれています。これ以外にドキュメントはありません。REST APIのテスト実行もできるように、Oracle RDF Graph ServerよりSwagger UIを使って参照できるようにします。

Oracle RDF Graph Serverを実装したコンピュート・インスタンス上で作業を行います。Swagger UIをインストールするためにgitコマンドを使用します。未インストールであれば、gitをインストールします。

sudo dnf -y install git

[opc@rdfgs ~]$ sudo dnf -y install git

Failed to set locale, defaulting to C.UTF-8

Last metadata expiration check: 0:47:04 ago on Fri Dec 10 03:31:38 2021.

Dependencies resolved.

==========================================================================================================

 Package                     Architecture      Version                     Repository                Size

==========================================================================================================

Installing:

 git                         x86_64            2.27.0-1.el8                ol8_appstream            164 k

Installing dependencies:

 git-core                    x86_64            2.27.0-1.el8                ol8_appstream            5.7 M

 git-core-doc                noarch            2.27.0-1.el8                ol8_appstream            2.5 M

 perl-Error                  noarch            1:0.17025-2.el8             ol8_appstream             46 k

 perl-Git                    noarch            2.27.0-1.el8                ol8_appstream             78 k

 perl-TermReadKey            x86_64            2.37-7.el8                  ol8_appstream             40 k


Transaction Summary

==========================================================================================================

Install  6 Packages


Total download size: 8.5 M

Installed size: 45 M


[中略]


Installed:

  git-2.27.0-1.el8.x86_64             git-core-2.27.0-1.el8.x86_64   git-core-doc-2.27.0-1.el8.noarch    

  perl-Error-1:0.17025-2.el8.noarch   perl-Git-2.27.0-1.el8.noarch   perl-TermReadKey-2.37-7.el8.x86_64  


Complete!

[opc@rdfgs ~]$ 

Swagger UIをGitよりインストールします。ユーザーopcのホーム・ディレクトリで実行します。

git clone https://github.com/swagger-api/swagger-ui.git

[opc@rdfgs ~]$ git clone https://github.com/swagger-api/swagger-ui.git

Cloning into 'swagger-ui'...

remote: Enumerating objects: 34525, done.

remote: Counting objects: 100% (2155/2155), done.

remote: Compressing objects: 100% (842/842), done.

remote: Total 34525 (delta 1399), reused 1985 (delta 1299), pack-reused 32370

Receiving objects: 100% (34525/34525), 377.08 MiB | 5.56 MiB/s, done.

Resolving deltas: 100% (21282/21282), done.

[opc@rdfgs ~]$ 


ユーザーopcのホーム・ディレクトリ以下に/home/opc/swagger-uiというディレクトリが作成され、Swagger UIのリポジトリが(丸ごと...)コピーされます。

続けてrfc-docというディレクトリを作成し、Oracle RDF Graph Serverのアーカイブに含まれているrdf-doc以下の2つのファイル(orardf_swagger.jsonは必須)を配置します。

[opc@rdfgs ~]$ mkdir rdf-doc

ファイルの配置作業を行う。

[opc@rdfgs ~]$ ls rdf-doc

orardf_doc_url.txt  orardf_swagger.json

[opc@rdfgs ~]$ 


jetty.homeのwebapps以下に静的ファイルを参照可能にする設定を加えます。

Swagger UIを参照可能にする設定として、swagger-ui.xmlを作成します。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
<Configure class="org.eclipse.jetty.server.handler.ContextHandler">
<Set name="contextPath">/swagger-ui/dist</Set>
<Set name="handler">
<New class="org.eclipse.jetty.server.handler.ResourceHandler">
<Set name="resourceBase">/home/opc/swagger-ui/dist</Set>
<Set name="directoriesListed">true</Set>
</New>
</Set>
</Configure>
view raw swagger-ui.xml hosted with ❤ by GitHub
RDF Graph Serverが提供するREST APIの定義を参照可能にする設定として、rdf-doc.xmlを作成します。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">
<Configure class="org.eclipse.jetty.server.handler.ContextHandler">
<Set name="contextPath">/rdf-doc</Set>
<Set name="handler">
<New class="org.eclipse.jetty.server.handler.ResourceHandler">
<Set name="resourceBase">/home/opc/rdf-doc</Set>
<Set name="directoriesListed">false</Set>
</New>
</Set>
</Configure>
view raw rdf-doc.xml hosted with ❤ by GitHub

これらのファイルを作成したのち、jetty.homeよりwebappsの内容を確認します。以下のファイル構成になります。

[opc@rdfgs jetty-distribution-9.4.44.v20210927]$ ls webapps

README.TXT  orardf.war  orardf.xml  rdf-doc.xml  swagger-ui.xml

[opc@rdfgs jetty-distribution-9.4.44.v20210927]$ 


追加した設定はすぐにJettyに反映されます。Swagger UIにアクセスしてREST APIの定義を確認します。

以下のURLにアクセスします。

https://RDF Graph Serverのホスト名/swagger-ui/dist/

Swagger UIには以下のURLを入力します。

https://RDF Graph Serverのホスト名/rdf-doc/orardf_swagger.json


Swagger UIからGETで定義されているREST APIは発行できますが、POSTについてはCSRF(Cross Site Request Forgery)の対応がなされているため、Swagger UIから実行できません。同様に、APEXからも単純なAPEX_WEB_SERVICE.MAKE_REST_REQUESTでは呼び出せません。

POSTによるREST APIの呼び出しの例として、SPARQLクエリを実行してみます。以下、PL/SQLによる記述です。祖父とその孫をリストします。

declare
/* 各種検索条件は定数として定義する。 */
C_ORIGIN constant varchar2(40) := 'https://ホスト名';
C_USERNAME constant varchar2(16) := 'RDF Graph Serverのユーザー名';
C_PASSWORD constant varchar2(16) := 'RDF Graph Serverのユーザーのパスワード';
C_DATASOURCE constant varchar2(16) := 'apexdev';
C_DATASETDEF constant varchar2(160) := '{"metadata":[{"networkOwner":"APEXDEV","networkName":"NET1","models":["VFAMILY"]}]}';
C_SPARQL constant varchar2(4000) :=
q'~PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX : <http://www.example.org/family/>
SELECT ?x ?y
WHERE {?x :grandParentOf ?y . ?x rdf:type :Male}
~';
-- REST APIの呼び出しに使用する。
l_blob blob;
l_url varchar2(400);
l_csrf_token varchar2(80);
-- SPARQLの検索結果となるJSONのパースに使用する。
l_json json_object_t;
l_results json_object_t;
l_bindings json_array_t;
l_row json_object_t;
-- 以下はSPARQLで変わる。今回は ?x ?y なので X, Y が列として返される。
l_x json_object_t;
l_y json_object_t;
begin
/*
* 簡単なREST APIを呼び出し、クッキーとして設定されるORACLE_RDFSERVER_CSRFの値を
* l_csrf_tokenとして取り出す。
*/
l_url := C_ORIGIN || '/orardf/api/v1/utils/user';
l_blob := apex_web_service.make_rest_request_b(
p_url => l_url
, p_http_method => 'GET'
, p_username => C_USERNAME
, p_password => C_PASSWORD
);
for i in 1..apex_web_service.g_response_cookies.count
loop
if apex_web_service.g_response_cookies(i).name = 'ORACLE_RDFSERVER_CSRF' then
l_csrf_token := apex_web_service.g_response_cookies(i).value;
end if;
end loop;
-- dbms_output.put_line('ORACLE_RDFSERVER_CSRF=' || l_csrf_token);
/*
* SPARQLによる検索を実行する。
*/
l_url := C_ORIGIN || '/orardf/api/v1/datasets/query?datasource=' || C_DATASOURCE || '&datasetDef=' || C_DATASETDEF;
-- HTTPリクエスト・ヘッダーを定義する。
apex_web_service.g_request_headers.delete();
apex_web_service.g_request_headers(1).name := 'Content-Type';
apex_web_service.g_request_headers(1).value := 'application/json';
-- CSRF(Cross Site Request Forgery)の対応を行う。
apex_web_service.g_request_headers(2).name := 'X-CSRF-Token';
apex_web_service.g_request_headers(2).value := l_csrf_token;
apex_web_service.g_request_headers(3).name := 'Origin';
apex_web_service.g_request_headers(3).value := C_ORIGIN;
apex_web_service.g_request_headers(4).name := 'Cookie';
apex_web_service.g_request_headers(4).value := 'ORACLE_RDFSERVER_CSRF=' || l_csrf_token;
-- REST APIの呼び出し。
l_blob := apex_web_service.make_rest_request_b(
p_url => l_url
, p_http_method => 'POST'
, p_username => C_USERNAME
, p_password => C_PASSWORD
, p_body => C_SPARQL
);
-- JSON文書から、検索結果となるそれぞれの行を取り出し印刷する。
l_json := json_object_t.parse(l_blob);
l_results := l_json.get_object('results');
l_bindings := l_results.get_array('bindings');
for i in 0..(l_bindings.get_size - 1)
loop
l_row := treat(l_bindings.get(i) as json_object_t);
l_x := l_row.get_object('X');
l_y := l_row.get_object('Y');
-- XとYの値を印刷する。
dbms_output.put_line('X=' || l_x.get_string('value') || ',Y=' || l_y.get_string('value'));
end loop;
end;

最初に簡単なREST APIを呼び出し、ORACLE_RDFSERSVER_CSRFとして返されるクッキーの値を取得します。その値を、POST送信時にHTTPヘッダーX-CSRF-Tokenとして送信します。送信する形式はapplication/jsonとなっているのですが、SPARQLの問い合わせを単純な文字列として送信します。

SQLワークショップのSQLコマンドより実行すると、実行結果として以下が返されます。

X=http://www.example.org/family/John,Y=http://www.example.org/family/Cathy X=http://www.example.org/family/John,Y=http://www.example.org/family/Jack X=http://www.example.org/family/John,Y=http://www.example.org/family/Tom X=http://www.example.org/family/John,Y=http://www.example.org/family/Cindy


実行するSPARQLに依存して取り出される列が決まるので、ポリモーフィックテーブル関数として実装すると、どのようなSPARQLであってもひとつの関数で対応できそうです。とはいえ、APEXからの利用を考えるとSPARQLの実行を、毎回RDF Graph Serverに依頼するよりは、Query cacheにある変換済みのSQLを取得して、直接データベースでSQLを実行する方がコーディングが楽でパフォーマンスも良いと思われます。

Query cacheの参照はGETリクエストなので、比較的簡単です。

declare
/* 各種検索条件は定数として定義する。 */
C_ORIGIN constant varchar2(40) := 'https://ホスト名';
C_USERNAME constant varchar2(16) := 'RDF Graph Serverのユーザー名';
C_PASSWORD constant varchar2(16) := 'RDF Graph Serverのユーザーのパスワード';
C_DATASOURCE constant varchar2(16) := 'apexdev';
C_NETWORK_OWNER constant varchar2(16) := 'APEXDEV';
C_NETWORK_NAME constant varchar2(16) := 'NET1';
C_MODEL constant varchar2(16) := 'VFAMILY';
-- REST APIの呼び出しに使用する。
l_blob blob;
l_url varchar2(400);
l_parms apex_application_global.VC_ARR2;
l_values apex_application_global.VC_ARR2;
-- SPARQLの検索結果となるJSONのパースに使用する。
l_json json_object_t;
l_items json_array_t;
l_row json_object_t;
begin
/*
* Query cacheの内容を取得する。
*/
l_url := C_ORIGIN || '/orardf/api/v1/datasources/' || C_DATASOURCE || '/sparql/cache/model';
l_parms.delete;
l_values.delete;
l_parms(1) := 'networkOwner';
l_values(1) := C_NETWORK_OWNER;
l_parms(2) := 'networkName';
l_values(2) := C_NETWORK_NAME;
l_parms(3) := 'model';
l_values(3) := C_MODEL;
/* REST APIの呼び出し */
l_blob := apex_web_service.make_rest_request_b(
p_url => l_url
, p_http_method => 'GET'
, p_username => C_USERNAME
, p_password => C_PASSWORD
, p_parm_name => l_parms
, p_parm_value => l_values
);
-- JSON文書から、検索結果となるそれぞれの行を取り出し印刷する。
l_json := json_object_t.parse(l_blob);
l_items := l_json.get_array('items');
for i in 0..(l_items.get_size - 1)
loop
l_row := treat(l_items.get(i) as json_object_t);
dbms_output.put_line('Cache ID: ' || l_row.get_string('cacheId'));
dbms_output.put_line('-------------- SPARQL ----------------');
dbms_output.put_line(l_row.get_string('sparql'));
dbms_output.put_line('-------------- Transalted SQL Start ----------------');
dbms_output.put_line(l_row.get_string('sql'));
dbms_output.put_line('-------------- Transalted SQL End ----------------');
end loop;
end;
view raw query-cache.sql hosted with ❤ by GitHub
SQLコマンドから実行すると以下のような結果になります。
Cache ID: d16221c6-8f48-4e08-8926-490118b3a0c6
-------------- SPARQL ----------------
PREFIX  rdf: <http: rdf-syntax-ns="" www.w3.org="">
PREFIX rdfs: <http: rdf-schema="" www.w3.org="">
PREFIX     : <http: family="" www.example.org="">

SELECT ?x ?y
WHERE {?x :grandParentOf ?y . ?x rdf:type :Male}
-------------- Transalted SQL Start ----------------
SELECT * FROM (
SELECT /*+ NO_MERGE(R) NO_SWAP_JOIN_INPUTS(R) LEADING(R V0 V1) NO_SWAP_JOIN_INPUTS(V0) NO_SWAP_JOIN_INPUTS(V1) */ V0.VNAME_PREFIX || V0.VNAME_SUFFIX AS X, V0.VALUE_ID AS X$RDFVID, V0.VNAME_PREFIX AS X$_PREFIX, V0.VNAME_SUFFIX AS X$_SUFFIX,  (CASE WHEN V0.VALUE_TYPE IS NULL THEN NULL WHEN V0.VALUE_TYPE IN ('UR','URI') THEN 'URI'
 WHEN V0.VALUE_TYPE IN ('BN', 'BLN') THEN 'BLN'
 ELSE 'LIT'
END)  AS X$RDFVTYP, V0.LONG_VALUE AS X$RDFCLOB, V0.LITERAL_TYPE AS X$RDFLTYP, V0.LANGUAGE_TYPE AS X$RDFLANG,
V1.VNAME_PREFIX || V1.VNAME_SUFFIX AS Y, V1.VALUE_ID AS Y$RDFVID, V1.VNAME_PREFIX AS Y$_PREFIX, V1.VNAME_SUFFIX AS Y$_SUFFIX,  (CASE WHEN V1.VALUE_TYPE IS NULL THEN NULL WHEN V1.VALUE_TYPE IN ('UR','URI') THEN 'URI'
 WHEN V1.VALUE_TYPE IN ('BN', 'BLN') THEN 'BLN'
 ELSE 'LIT'
END)  AS Y$RDFVTYP, V1.LONG_VALUE AS Y$RDFCLOB, V1.LITERAL_TYPE AS Y$RDFLTYP, V1.LANGUAGE_TYPE AS Y$RDFLANG,
1 AS SEM$ROWNUM
FROM (SELECT T0.START_NODE_ID AS X$RDFVID,
T0.CANON_END_NODE_ID AS Y$RDFVID,
T0.START_NODE_ID AS BGP$1
FROM "APEXDEV"."NET1#SEMV_VFAMILY" T0, "APEXDEV"."NET1#SEMV_VFAMILY" T1
WHERE T0.P_VALUE_ID = 8440289324123914894 AND
T1.P_VALUE_ID = 834132227519661324 AND
T1.CANON_END_NODE_ID = 3746347748834679532 AND
T0.START_NODE_ID = T1.START_NODE_ID) R, "APEXDEV".NET1#RDF_VALUE$ V0, "APEXDEV".NET1#RDF_VALUE$ V1
WHERE (1=1)  AND (R.X$RDFVID = V0.VALUE_ID) AND (R.Y$RDFVID = V1.VALUE_ID)
) WHERE (1=1)
-------------- Transalted SQL End ----------------

以上で、Oracle RDF Graph ServerをAutonomous Databaseで使用する方法の紹介は終了です。

Oracle APEXのアプリケーション作成の参考になれば幸いです。