您正在查看 Apigee Edge 說明文件。
參閱 Apigee X 說明文件。 資訊
Edge Microgateway 3.3.x 版
觀眾
本主題的適用對象為希望藉由編寫自訂外掛程式擴充 Edge Microgateway 功能的開發人員。如要編寫新的外掛程式,您必須要有 JavaScript 和 Node.js 相關經驗。
什麼是自訂 Edge Microgateway 外掛程式?
外掛程式是一種 Node.js 模組,可為 Edge Microgateway 新增功能。外掛程式模組會遵循一致的模式,並儲存在 Edge Microgateway 已知的位置,讓系統可自動探索及執行外掛程式。安裝 Edge Microgateway 時,系統會提供幾種預先定義的外掛程式。這些外掛程式包括驗證、流量尖峰、配額和數據分析等適用的外掛程式。如要瞭解這些現有的外掛程式,請參閱「使用外掛程式」一文。
您可以編寫自訂外掛程式,為微閘道新增功能。根據預設,Edge Microgateway 基本上是安全的直通 Proxy,可將未經變更的要求和回應傳遞至目標服務。使用自訂外掛程式,您就能以程式輔助方式與透過微閘道傳遞的要求和回應互動。
自訂外掛程式程式碼的位置
下方為 Edge Microgateway 安裝項目提供的自訂外掛程式資料夾:
[prefix]/lib/node_modules/edgemicro/node_modules/microgateway-plugins
其中 [prefix]
是 npm
前置字串目錄,如「安裝 Edge Microgateway」一節「位於何者的安裝位置」所述。
您可以變更這個預設外掛程式目錄。請參閱「哪裡可以找到外掛程式」一節。
查看預先定義的外掛程式
在嘗試自行開發外掛程式前,建議您先檢查所有預先定義的外掛程式都不符合需求。這些外掛程式位於:
[prefix]/lib/node_modules/edgemicro/node_modules/microgateway-plugins
其中 [prefix]
是 npm
前置字串目錄。另請參閱「安裝 Edge Microgateway」中的「在哪些情況下已安裝 Edge Microgateway」。
詳情請參閱 Edge Microgateway 提供的預先定義外掛程式。
編寫簡易的外掛程式
在本節中,我們將逐步說明建立簡易外掛程式所需的步驟。這個外掛程式能使用「Hello, World!」字串覆寫回應資料 (不論資料為何),並將其輸出至終端機。
- 如果 Edge Microgateway 正在執行,請立即停止:
edgemicro stop
-
cd
到自訂外掛程式目錄:cd [prefix]/lib/node_modules/edgemicro/plugins
其中
[prefix]
是npm
前置字串目錄,如「安裝 Edge Microgateway」一節「位於何者的安裝位置」所述。 - 建立名為 response-override 和
cd
的外掛程式專案:
mkdir response-override && cd response-override
- 建立新的 Node.js 專案:
npm init
Hit Return 多次接受預設值。 - 使用文字編輯器建立名為
index.js
的新檔案。 - 將下列程式碼複製到
index.js
中並儲存檔案。
'use strict'; var debug = require('debug') module.exports.init = function(config, logger, stats) { return { ondata_response: function(req, res, data, next) { debug('***** plugin ondata_response'); next(null, null); }, onend_response: function(req, res, data, next) { debug('***** plugin onend_response'); next(null, "Hello, World!\n\n"); } }; }
- 現在您已建立外掛程式,請將外掛程式新增至 Edge Microgateway 設定。開啟
$HOME/.edgemicro/[org]-[env]-config.yaml
檔案,其中org
和env
是您的 Edge 機構和環境名稱。 - 將
response-override
外掛程式新增至plugins:sequence
元素,如下所示。
... plugins: dir: ../plugins sequence: - oauth - response-override ...
- 重新啟動 Edge Microgateway。
- 透過 Edge Microgateway 呼叫 API。(這個 API 呼叫假設您已設定與 API 金鑰安全性教學課程相同的設定,如「設定及設定 Edge Microgateway」中所述):
curl -H 'x-api-key: uAM4gBSb6YoMvTHfx5lXJizYIpr5Jd' http://localhost:8000/hello/echo Hello, World!
外掛程式剖析
以下 Edge Microgateway 範例外掛程式說明開發自己的外掛程式時應遵循的模式。本節討論的範例外掛程式原始碼位於 plugins/header-uppercase/index.js.
- 外掛程式是根資料夾中具有
package.json
和index.js
的標準 NPM 模組。 - 外掛程式必須匯出 init() 函式。
- init() 函式會採用三個引數:config、logger 和 stats。這些引數的說明,請見外掛程式的 init() 函式引數。
- init() 會傳回具有已命名函式處理常式的物件,在要求的生命週期期間發生特定事件時,即會呼叫該函式。
事件處理常式函式
外掛程式必須實作部分或全部的事件處理常式函式。您可以自行決定實作這些函式。任何指定函式都是選用項目,一般外掛程式至少會實作這些功能的子集。
要求流程事件處理常式
在 Edge Microgateway 中的要求事件上呼叫這些函式。
onrequest
ondata_request
onend_request
onclose_request
onerror_request
onrequest
函式
會在用戶端要求開始時呼叫。Edge Microgateway 收到要求的第一個位元組時,這個函式會觸發。這個函式可讓您存取要求標頭、網址、查詢參數和 HTTP 方法。如果您接下來使用傳送的第一個引數 (例如 Error 的執行個體) 呼叫,則系統會停止要求處理作業,並且不會啟動目標要求。
示例:
onrequest: function(req, res, next) { debug('plugin onrequest'); req.headers['x-foo-request-start'] = Date.now(); next(); }
ondata_request
函式
從用戶端接收到資料區塊時,會呼叫此方法。將要求資料傳送至外掛程式序列中的下一個外掛程式。系統會將序列中最後一個外掛程式傳回的值傳送至目標。常見的應用實例如下:先轉換要求資料,再傳送至目標。
示例:
ondata_request: function(req, res, data, next) { debug('plugin ondata_request ' + data.length); var transformed = data.toString().toUpperCase(); next(null, transformed); }
onend_request
函式
從用戶端收到所有請求資料時,會呼叫此方法。
示例:
onend_request: function(req, res, data, next) { debug('plugin onend_request'); next(null, data); }
onclose_request
函式
表示用戶端連線已關閉。您可以在用戶端連線不穩定的情況下使用此函式。與用戶端的通訊端連線關閉時,系統會呼叫此方法。
示例:
onclose_request: function(req, res, next) { debug('plugin onclose_request'); next(); }
onerror_request
函式
如果接收用戶端要求時發生錯誤,就會呼叫此方法。
示例:
onerror_request: function(req, res, err, next) { debug('plugin onerror_request ' + err); next(); }
回應流程事件處理常式
系統會在 Edge Microgateway 的回應事件中呼叫這些函式。
onresponse
ondata_response
onend_response
onclose_response
onerror_response
onresponse
函式
在目標回應開始時呼叫。Edge Microgateway 收到回應的第一個位元組時,這個函式會觸發。這個函式可讓您存取回應標頭和狀態碼。
示例:
onresponse: function(req, res, next) { debug('plugin onresponse'); res.setHeader('x-foo-response-time', Date.now() - req.headers['x-foo-request-start']) next(); }
ondata_response
函式
從目標收到資料區塊時呼叫。
示例:
ondata_response: function(req, res, data, next) { debug('plugin ondata_response ' + data.length); var transformed = data.toString().toUpperCase(); next(null, transformed); }
onend_response
函式
從目標收到所有回應資料時,會呼叫此方法。
示例:
onend_response: function(req, res, data, next) { debug('plugin onend_response'); next(null, data); }
onclose_response
函式
表示目標連線已關閉。當目標連線不穩定時,您可以使用此函式。當與目標的通訊端連線關閉時,系統就會呼叫此方法。
示例:
onclose_response: function(req, res, next) { debug('plugin onclose_response'); next(); }
onerror_response
函式
在接收目標回應時發生錯誤時呼叫。
示例:
onerror_response: function(req, res, err, next) { debug('plugin onerror_response ' + err); next(); }
外掛程式事件處理常式函式的須知事項
系統會針對 Edge Microgateway 處理指定 API 要求時發生的特定事件呼叫外掛程式事件處理常式函式。
- 處理完成後,每個 init() 函式處理常式 (ondata_request、ondata_response 等) 都必須呼叫 next() 回呼。如果您未呼叫 next(),系統將停止處理作業,並停止要求。
- next() 的第一個引數可能是錯誤,將導致要求處理終止。
- ondata_ 和 onend_ 處理常式必須使用第二個引數呼叫 next(),其中含有要傳遞至目標或用戶端的資料。如果外掛程式正在進行緩衝處理,且資料不足以目前無法轉換,這個引數可以是空值。
- 請注意,這個外掛程式的單一執行個體會用來服務所有要求和回應。如果外掛程式想在呼叫之間保留每個要求的狀態,可以將狀態儲存在新增至提供的 request 物件 (req) 的屬性中,其生命週期為 API 呼叫持續時間。
- 請仔細檢查所有錯誤,並呼叫具有錯誤結果的 next()。如果沒有呼叫 next(),將導致 API 呼叫停止運作。
- 請注意,不要造成記憶體流失,因為這會影響 Edge Microgateway 的整體效能,並在記憶體不足時造成當機。
- 請注意,不要在主執行緒中執行會耗用大量運算資源的工作,遵循 Node.js 模型,因為這可能會對 Edge Microgateway 的效能造成負面影響。
關於外掛程式 init() 函式
本節說明傳送至 init() 函式的引數:config、logger 和 stats。
config
藉由將 Edge Microgateway 設定檔與從 Apigee Edge 下載的資料合併取得的設定資料,會放在名為 config
的物件中。
如要將名為 param 且值為 foo 的設定參數新增至名為 response-override 的外掛程式,並將這個參數放入 default.yaml
檔案:
response-override: param: foo
然後,您可以存取外掛程式程式碼中的 參數,如下所示:
// Called when response data is received ondata_response: function(req, res, data, next) { debug('***** plugin ondata_response'); debug('***** plugin ondata_response: config.param: ' + config.param); next(null, data); },
在此情況下,外掛程式偵錯輸出內容中會顯示 foo:
Sun, 13 Dec 2015 21:25:08 GMT plugin:response-override ***** plugin ondata_response: config.param: foo
您可以在子項物件「config.emgConfigs
」中存取合併的微閘道設定,以及下載 Apigee Edge 資料。例如,您可以在 init
函式中存取這項設定資料,如下所示:
module.exports.init = function(config, logger, stats) { let emgconfigs = config.emgConfigs;
以下是 emgConfigs
包含的資料範例:
{ edgemicro: { port: 8000, max_connections: 1000, config_change_poll_interval: 600, logging: { level: 'error', dir: '/var/tmp', stats_log_interval: 60, rotate_interval: 24, stack_trace: false }, plugins: { sequence: [Array] }, global: { org: 'Your Org', env: 'test' } }, headers: { 'x-forwarded-for': true, 'x-forwarded-host': true, 'x-request-id': true, 'x-response-time': true, via: true }, proxies: [ { max_connections: 1000, name: 'edgemicro_delayed', revision: '1', proxy_name: 'default', base_path: '/edgemicro_delayed', target_name: 'default', url: 'https://httpbin.org/delay/10', timeout: 0 } ], product_to_proxy: { EdgeMicroTestProduct: [ 'edgemicro-auth','edgemicro_delayed',] }, product_to_scopes: {prod4: [ 'Admin', 'Guest', 'Student' ] }, product_to_api_resource: { EdgeMicroTestProduct: [ '/*' ] }, _hash: 0, keys: { key: 'Your key', secret: 'Your key ' }, uid: 'Internally generated uuid', targets: [] }
logger
系統記錄器。目前採用的記錄器會匯出這些函式,其中物件可以是字串、HTTP 要求、HTTP 回應或錯誤執行個體。
info(object, message)
warn(object, message)
error(object, message)
trace(object, message)
debug(object, message)
stats
這個物件會保留與透過微閘道執行個體傳輸的要求和回應相關的要求、回應、錯誤及其他匯總統計資料數量。
- treqErrors - 發生錯誤的目標要求數量。
- treqErrors - 發生錯誤的目標回應數量。
- statusCodes - 包含回應代碼數量的物件:
{ 1: number of target responses with 1xx response codes 2: number of target responses with 2xx response codes 3: number of target responses with 3xx response codes 4: number of target responses with 4xx response codes 5: number of target responses with 5xx response codes }
- requests - 要求總數。
- responses - 回應的總數。
- connections:有效目標連線的數量。
關於 next() 函式
所有外掛程式方法都必須呼叫 next()
,才能繼續處理系列中的下一個方法 (或外掛程式程序會停止運作)。在要求生命週期中,呼叫的第一個方法為 onrequest()。下一個要呼叫的方法為 ondata_request()
方法;但「只有」在要求包含資料時才會呼叫 ondata_request
,例如 POST 要求等情況時一樣。下一個呼叫的方法將是 onend_request()
,系統會在要求處理完成時呼叫此方法。只有在發生錯誤時,才會呼叫 onerror_*
函式,而且您可以在需要時透過自訂程式碼處理錯誤。
假設資料在要求中傳送,並呼叫 ondata_request()
。請注意,函式會使用兩個參數呼叫 next()
:
next(null, data);
按照慣例,第一個參數是用來傳送錯誤資訊,您之後可以在鏈結中的後續函式中處理。將其設為 null
(好奇的引數) 即代表沒有任何錯誤,要求處理程序應會正常進行。如果這個引數是誇大的 (例如 Error 物件),則要求處理作業停止,並將要求傳送至目標。
第二個參數會將要求資料傳送至鏈結中的下一個函式。如未進行額外處理,要求資料會維持不變傳送至 API 的目標。不過,您可以在此方法中修改要求資料,並將修改過的要求傳送至目標。舉例來說,如果要求資料是 XML,且目標預期 JSON 檔案,您可以將程式碼新增至 ondata_request()
方法,以便:(a) 將要求標頭的 Content-Type 變更為 application/json
,並使用任何您偏好的方式,將要求資料轉換為 JSON (例如,您可以使用從 NPM 取得的 Node.js xml2json 轉換工具)。
可能的顯示結果如下:
ondata_request: function(req, res, data, next) { debug('****** plugin ondata_request'); var translated_data = parser.toJson(data); next(null, translated_data); },
在這種情況下,要求資料 (假設為 XML) 會轉換為 JSON,而轉換後的資料會透過 next()
傳遞至要求鏈中的下一個函式,然後再傳遞至後端目標。
請注意,您可以新增其他偵錯陳述式,以列印轉換後的資料進行偵錯。例如:
ondata_request: function(req, res, data, next) { debug('****** plugin ondata_request'); var translated_data = parser.toJson(data); debug('****** plugin ondata_response: translated_json: ' + translated_json); next(null, translated_data); },
關於外掛程式處理常式執行順序
如要編寫 Edge Microgateway 適用的外掛程式,就必須瞭解外掛程式事件處理常式的執行順序。
值得注意的是,當您在 Edge Microgateway 設定檔中指定外掛程式序列時,要求處理常式會以「遞增」順序執行,回應處理常式則會以「遞減」的順序執行。
以下範例可協助您瞭解這個執行序列。
1. 建立三個簡單的外掛程式
請考慮使用下列外掛程式。系統只會在呼叫事件處理常式時輸出控制台輸出內容:
plugins/plugin-1/index.js
module.exports.init = function(config, logger, stats) { return { onrequest: function(req, res, next) { console.log('plugin-1: onrequest'); next(); }, onend_request: function(req, res, data, next) { console.log('plugin-1: onend_request'); next(null, data); }, ondata_response: function(req, res, data, next) { console.log('plugin-1: ondata_response ' + data.length); next(null, data); }, onend_response: function(req, res, data, next) { console.log('plugin-1: onend_response'); next(null, data); } }; }
現在,不妨再建立兩個使用相同程式碼的外掛程式 plugin-2
和 plugin-3
,不過,請將 console.log()
陳述式分別變更為 plugin-2
和 plugin-3
。
2. 查看外掛程式程式碼
已匯出的 <microgateway-root-dir>/plugins/plugin-1/index.js
外掛程式函式是事件處理常式,可在要求和回應處理期間於特定時間執行。舉例來說,onrequest
會執行接收到要求標頭的第一個位元組。不過,onend_response
會在收到回應資料最後一個位元組後執行。
請查看 ondata_response,也就是每次收到回應資料區塊時,就會呼叫這個處理常式。值得注意的是,不一定會一次收到所有回應資料。而是可能會以任意長度的區塊分段接收資料。
3. 將外掛程式新增至外掛程式序列
在這個例子中,我們會將外掛程式新增至 Edge Microgateway 設定檔 (~./edgemicro/config.yaml
) 中的外掛程式序列,如下所示。順序十分重要。它會定義外掛程式處理常式的執行順序。
plugins: dir: ../plugins sequence: - plugin-1 - plugin-2 - plugin-3
4. 檢查偵錯輸出內容
現在,我們來看看呼叫這些外掛程式時會產生的輸出內容。請特別注意以下幾點:
- 外掛程式會處理 Edge Microgateway 設定檔 (
~./edgemicro/config.yaml
) 的呼叫順序,指定事件處理常式的呼叫順序。 - 要求處理常式會依「遞增」順序呼叫 (排列在外掛程式序列中的顯示順序:1、2、3)。
- 回應處理常式會依「遞減」順序呼叫:3、2、1。
- 系統會針對每個到達的資料區塊呼叫
ondata_response
處理常式。在此範例中 (輸出如下所示),接收到兩個區塊。
以下是使用這三個外掛程式,並透過 Edge Microgateway 傳送要求時產生的偵錯輸出內容範例。只要注意處理常式的呼叫順序即可:
plugin-1: onrequest plugin-2: onrequest plugin-3: onrequest plugin-1: onend_request plugin-2: onend_request plugin-3: onend_request plugin-3: ondata_response 931 plugin-2: ondata_response 931 plugin-1: ondata_response 931 plugin-3: ondata_response 1808 plugin-3: onend_response plugin-2: ondata_response 1808 plugin-2: onend_response plugin-1: ondata_response 1808 plugin-1: onend_response
摘要
嘗試實作自訂外掛程式功能 (例如累積及轉換要求或回應資料) 時,瞭解外掛程式處理常式的呼叫順序非常重要。
請注意,要求處理常式會按照 Edge Microgateway 設定檔中指定的順序執行,而回應處理常式會以相反順序執行。
關於在外掛程式中使用全域變數
每次向 Edge Microgateway 發出的要求都會傳送至外掛程式的相同執行個體,因此來自其他用戶端的第二個要求狀態會覆寫第一個。儲存外掛程式狀態的唯一安全的地方,是將狀態儲存在要求或回應物件的屬性中 (其生命週期限制為要求的該屬性)。
在外掛程式中重新編寫目標網址
已新增:v2.3.3 版
您可以在外掛程式程式碼中修改以下變數,以動態方式覆寫外掛程式中的預設目標網址:req.targetHostname 和 req.targetPath。
已新增:v2.4.x
您也可以覆寫目標端點通訊埠,並選擇 HTTP 或 HTTPS。請在外掛程式程式碼中修改以下變數:req.targetPort 和 req.targetSecure。如要選擇 HTTPS,請將 req.targetSecure 設為 true;若是 HTTP,請設為 false。如果將 req.targetSecure 設為 true,請參閱這個討論執行緒以瞭解更多資訊。
移除版本:3.3.3 版
名為 eurekaclient 的範例外掛程式已從 v.3.3.3 中的 Edge Microgateway 中移除。請參閱「版本資訊」。
移除這項功能不會影響 Edge Migateway 的核心功能,也不會重新編寫目標網址。您可以在外掛程式層級設定動態端點查詢及覆寫目標變數,例如 req.targetHostname
、req.targetPath
、req.targetPort
和 req.targetSecure
。請參閱「在外掛程式中重新編寫目標網址」。
外掛程式範例
您的 Edge Microgateway 安裝作業會提供這些外掛程式。您可以在以下位置找到 Edge Microgateway 安裝:
[prefix]/lib/node_modules/edgemicro/plugins
其中 [prefix]
是 npm
前置字串目錄,如「安裝 Edge Microgateway」一節「位於何者的安裝位置」所述。
累積要求
這個外掛程式會將用戶端的資料區塊累積到與要求物件連結的陣列屬性。收到所有要求資料時,陣列會串連為緩衝區,然後傳遞至序列中的下一個外掛程式。這個外掛程式必須是序列中的第一個外掛程式,以便後續外掛程式接收累積的要求資料。
module.exports.init = function(config, logger, stats) { function accumulate(req, data) { if (!req._chunks) req._chunks = []; req._chunks.push(data); } return { ondata_request: function(req, res, data, next) { if (data && data.length > 0) accumulate(req, data); next(null, null); }, onend_request: function(req, res, data, next) { if (data && data.length > 0) accumulate(req, data); var content = null; if (req._chunks && req._chunks.length) { content = Buffer.concat(req._chunks); } delete req._chunks; next(null, content); } }; }
累積回應
這個外掛程式會將目標中的資料區塊累積到附加至回應物件的陣列屬性。收到所有回應資料時,陣列會串連為緩衝區,然後傳遞至序列中的下一個外掛程式。這個外掛程式會在回應上運作,而處理順序會反向處理,因此您應該將外掛程式放置為序列中的最後一個外掛程式。
module.exports.init = function(config, logger, stats) { function accumulate(res, data) { if (!res._chunks) res._chunks = []; res._chunks.push(data); } return { ondata_response: function(req, res, data, next) { if (data && data.length > 0) accumulate(res, data); next(null, null); }, onend_response: function(req, res, data, next) { if (data && data.length > 0) accumulate(res, data); var content = Buffer.concat(res._chunks); delete res._chunks; next(null, content); } }; }
header-uppercase 外掛程式
Edge Microgateway 發布內含名為 <microgateway-root-dir>/plugins/header-uppercase
的範例外掛程式。範例包含說明每個函式處理常式的註解。這個範例會執行一些簡單的目標回應資料轉換作業,並在用戶端要求和目標回應中加入自訂標頭。
以下是 <microgateway-root-dir>/plugins/header-uppercase/index.js
的原始碼:
'use strict'; var debug = require('debug')('plugin:header-uppercase'); // required module.exports.init = function(config, logger, stats) { var counter = 0; return { // indicates start of client request // request headers, url, query params, method should be available at this time // request processing stops (and a target request is not initiated) if // next is called with a truthy first argument (an instance of Error, for example) onrequest: function(req, res, next) { debug('plugin onrequest'); req.headers['x-foo-request-id'] = counter++; req.headers['x-foo-request-start'] = Date.now(); next(); }, // indicates start of target response // response headers and status code should be available at this time onresponse: function(req, res, next) { debug('plugin onresponse'); res.setHeader('x-foo-response-id', req.headers['x-foo-request-id']); res.setHeader('x-foo-response-time', Date.now() - req.headers['x-foo-request-start']); next(); }, // chunk of request body data received from client // should return (potentially) transformed data for next plugin in chain // the returned value from the last plugin in the chain is written to the target ondata_request: function(req, res, data, next) { debug('plugin ondata_request ' + data.length); var transformed = data.toString().toUpperCase(); next(null, transformed); }, // chunk of response body data received from target // should return (potentially) transformed data for next plugin in chain // the returned value from the last plugin in the chain is written to the client ondata_response: function(req, res, data, next) { debug('plugin ondata_response ' + data.length); var transformed = data.toString().toUpperCase(); next(null, transformed); }, // indicates end of client request onend_request: function(req, res, data, next) { debug('plugin onend_request'); next(null, data); }, // indicates end of target response onend_response: function(req, res, data, next) { debug('plugin onend_response'); next(null, data); }, // error receiving client request onerror_request: function(req, res, err, next) { debug('plugin onerror_request ' + err); next(); }, // error receiving target response onerror_response: function(req, res, err, next) { debug('plugin onerror_response ' + err); next(); }, // indicates client connection closed onclose_request: function(req, res, next) { debug('plugin onclose_request'); next(); }, // indicates target connection closed onclose_response: function(req, res, next) { debug('plugin onclose_response'); next(); } }; }
大寫
這是一個一般轉換外掛程式,您可以修改此外掛程式來執行您想要的任何轉換。這個範例只會將回應和要求資料轉換為大寫。
*/ module.exports.init = function(config, logger, stats) { // perform content transformation here // the result of the transformation must be another Buffer function transform(data) { return new Buffer(data.toString().toUpperCase()); } return { ondata_response: function(req, res, data, next) { // transform each chunk as it is received next(null, data ? transform(data) : null); }, onend_response: function(req, res, data, next) { // transform accumulated data, if any next(null, data ? transform(data) : null); }, ondata_request: function(req, res, data, next) { // transform each chunk as it is received next(null, data ? transform(data) : null); }, onend_request: function(req, res, data, next) { // transform accumulated data, if any next(null, data ? transform(data) : null); } }; }