2021-06-09

ffmpeg Options

 筆記一下 ffmpeg 時常用參數:

  • -i url: Set the file URL of input. When capture video from X11, the url is comprised of [hostname]:display_number.screen_number. The hostname is localhost by default, and the display_number.screen_number is as known as display_name and could be found by the command xdpyinfo.
  • -f format: Force input or output file format. The format could be x11grab to grab video from the X11 display.
  • -s size: Set the frame size.
  • -r fps: Set the frame-per-second.
  • -filter[:stream_specifier] filtergraph: Define a filtergraph for the stream.
  • -crf value: Set the constant-rate-factor quantizer scale.
  • -preset name: Set the collection name of compression ratio options.
  • -vcodec codec: Set the codec for output video.
  • -g size: Set the size of group-of-pictures.
  • -y: Overwrite output files without asking.

References:

[1] https://ffmpeg.org/ffmpeg.html
[2] https://trac.ffmpeg.org/wiki/Encode/H.264

2021-02-03

SSH Tunnel

當 service provider 與 service consumer 之間被防火牆阻隔,導致無法直接連線時,SSH tunnel 是個常見的處理方式,其中又分為 local forwarding 與 remote forwarding。無論是哪一種 forwarding 模式,都需要有一台同時可與 provider 與 consumer 直接連線的 SSH server 作為中繼,差別在於由哪一方負責執行 SSH client 建立 tunnel;而建立 tunnel 時,需指定一個本機的 port number,與遠端 hostname 與 port number。

Local Forwarding

由 service consumer 執行 SSH client 建立 tunnel。

可能使用情境為:應用系統的資料庫 (service provider) 僅允許應用系統連線存取,且需從自己的電腦 (service consumer) 連線查詢資料,此時本機執行 SSH client 與 database client,分別於本機新增一 listening port 將封包轉送至資料庫,並將 database client 連線指向本機的 listening port。

ssh -L localhost:9001:service.provider:3306 ssh.server

由於 service porvider 的 port number 為 consumer 端建立 tunnel 時輸入,因此 consumer 端可透過輸入其它 port number 甚至是 hostname 達到連線至其它系統的目的。

Remote Forwarding

由 service provider 執行 SSH client 建立 tunnel。

可能使用情境為:內網資訊系統需要請外部廠商連線維護時,希望嚴格限制 consumer 可連線的 hostname 與 port number;此時可於內網執行 SSH client,指定 SSH server 上需新增的 listening port 與內網 service provider 的 hostname 與 port number,service consumer 連線至 SSH server 上的 listening port 時,最終只能被轉傳至 service provider。

ssh -R 9001:service.provider:443 ssh.server

2018-05-02

From Identity Management to Identity Governance

有關認證與授權,各種名詞跟負責的功能常常處於混亂的狀況;比較清楚的多半是偏技術面的通訊協定,其他諸如產品名稱與宣稱功能往往令人迷惑,常見的有:
  • Identity Management (IdM) [1]
  • Identity and Access Management (IAM)
  • Access Management (AM)
  • Identity Governance and Administration (IGA)

首先我們想釐清的重點有兩個:
  • Directory service 與上述各系統之間的關係;
  • 各系統的差異為何?

一般來說,組織或企業在踏入這塊領域之前,通常會預先建置 directory service,就通訊協定標準來說,LDAP 是主流;但由於微軟 Active Directory (AD) 的超高市佔率,AD 儼然成為另一種行業 (de facto) 標準。但即使建置了 directory service,對於企業 IT 而言仍有許多不足的地方:
  • 首先從認證與授權的角度來看,directory service 僅能處理認證;雖然 directory service 除了記錄使用者的帳號 & 密碼外,也提供許多 attributes,甚至是群組關係,但是否讀取這些屬性作為授權依據,傳統上仍由應用系統控制,亦即應用系統可自行決定是否讀取 directory service 上的資訊,並決定授予哪些權限。
  • 其次就帳號是否生效來說,企業內部往往涉及更多人事作業的需求,例如:預約到職或離職、留職停薪、休假等,無法單純地對應成在 directory service 上新增或刪除一個使用者;這裡頭往往需要一些前置作業,例如:資料匯入、流程簽核、發送通知等,才能順利完成。

於是 Identity Management (IdM) 因應而生。IdM 不僅滿足於 directory service 的圖形化操作介面,更包括前面提的資料匯入、流程簽核、發送通知等功能,方便人事在單一系統操作,完成帳號開通或停用,但仍未處理授權議題。

為了解決授權問題,部分廠商提出了 IAM;但受限於應用系統中授權模組設計,IAM 多半只能利用使用者的 attributes,呼叫各應用系統的 API 產生對應的角色,最後由授權模組在實際操作時給予權限。

因此 IAM 的重點在於針對各個應用系統實作相應的 connectors,例如 OneLogin 便具備與 AWS, Atlassian, G Suite, Office 365, Salesforce, Trello 等應用系統串接能力 [2]。這裡頭有關讀取使用者 attributes 到建立應用系統角色的過程,稱為 entitlement provisioning [3];IAM 亦提供圖形介面方便設定規則讓 connectors 定期更新應用系統角色。

前兩者大概處理完認證與授權的問題,最後兩個分別可以視為 IdM 與 IAM 的再進化。

傳統 IdM 因為未擴充通訊協定,只提供 LDAP 認證逐漸無法滿足用戶登入時的便利與安全性需求;在現代的資訊系統中,常見的認證需求主要有:single sign-on (SSO) 與 muti-factor authentication (MFA),甚至是 one-time password (OTP);以 okta 為例,在 SSO 方面就支援了 SAML 2.0 與 OpenID,同時也支援簡訊、Email、手機推播、U2F token、指紋等多種多重認證行為 [4]

如果說 AM 側重於用戶端登入行為,IGA 則側重使用者資訊導入應用系統時的管理。

前面提到 IAM 的重點在於 entitlement provisioning,然而 provisioning 的行為類似 IdM 啟用或停用帳號,也需要資料匯入、流程簽核、發送通知等;亦即並非每一位在 LDAP service 建立帳號的使用者,都具備登入所有系統的權限,各應用系統本身對於部分角色授予,可能也需要相應的簽核流程。

也就是說,IGA 針對各應用系統的 provisioning 提供了差異化管理,同一個使用者可能在某些系統可以直接登入獲得角色,部分系統需要簽核後才有角色,還有一些系統無法登入。

see also:
[1] Wikipedia: Identity Management
[2] OneLogin: App Integration
[3] OneLogin: Provisioning Entitlements
[4] Okta: Adaptive Multi-Factor Authentication

2018-04-10

Salesforce: Record Type and View Model

先前 [1] 用物件導向的觀念分析了一下 Salesforce 中 object, record type, and master-detail object relationships 的特性;就 Salesforce 系統設計而言,是否能表達物件導向並不重要,讓使用者易於設定、儘快進入使用,才是這類 domain-specific service 的重點。

如果用三層式架構中 domain model, data model, and view model 的角度來看,Salesforce 在 data model 原則上採用類似 active record pattern 的作法,設定畫面中的 object 直接對應到 Apex programming 中的 object type 與 SOQL 中的 table,以 1-to-1 的方式呈現。

這時候 record type 很明顯就是為了 view model 而存在,只是多數 Web application development 中,有很多好用的 model mapping libraries 可供使用,view model 與 domain model 之間的關係可以很彈性地為 many-to-many。回到 Saleforce,如果要讓使用者在不需任何程式碼,也能保有 view model 的便利性:
同一個 domain model 可搭配不同的畫面,顯示不同的屬性組合。
這時候就可以透過 record type 的設定,讓 view model 與 domain model 之間達到 many-to-one 之間的關係。

see also:
[1] Salesforce: Objects Generalization and Specialization

2018-02-11

LDAP Basics

LDAP [1] 在現在 IT 系統應用中,幾乎快變成「計概」等級的基礎服務,但是聽過的人多,知道用途的人可能也不少,但是能明確知道應用場域跟侷限的人就不多了;甚至很多人只是看著操作說明,一步步設定起來,對於基礎名詞處於一知半解。

LDAP 顧名思義,它是脫胎自 Directory Access Protocol (DAP;定義於 X.500 [2],a.k.a ISO/IEC 9594-1),最早可追溯到 1990 的版本 [3],幾乎所有基本名詞都在 X.500 就定義好了,這邊整理我認為最重要的三個基本名詞。

ObjectClass
在 LDAP 中,每一筆看到的資料 (entry),舉凡人員或組織,都是一堆屬性 (attribute) 的組合,而個別 entry 必須定義那些屬性?可以擁有那些屬性?則由該 entry 所擁有的 objectclass 決定。(objectclass 本身也是 entry 的屬性之一,特別的是這個屬性是預設必要的,用這個設定值來控制是否帶入其他屬性。)

如同 object-oriented programming 一樣,這邊的 class 裡頭會定義 instance (也就是前述的 entry) 擁有的 fields (前述的 attribute);classes 之間也有繼承關係,可以讓 sub-classes 直接繼承 base class 的所有 attributes 而不用重複宣告。相對於 OOP 中的 extends,這邊繼承時使用的關鍵字是 SUP,如果單純作為 LDAP 使用者不看 LDIF 對 objectclass 定義的話,可能不會接觸這個詞。

不一樣的是:ObjectClass 中並不定義行為 (methods),也沒有 constructor;因此當定義 entry 時,採直接描述 entry 的各個屬性,包括 objectclass 與 attribute。底下是一個 displayname 為 /QNAP 的 organization entry:
dn: O=QNAP
objectclass: organization
objectclass: top
displayname: /QNAP
其中 objectclass 欄位即這個 entry 擁有的「型別」,一個 entry 可以同時擁有多個 objectclass 屬性;但 ObjectClass 底下細分成 ABSTRACT, AUXILIARYSTRUCTURAL 三種,一個 entry 同時間只能有一個 objectclass 屬性為 STRUCTURAL,類似 OOP 中做為 initiate 的 concrete class。

Inheritance of ObjectClass

至於 AUXILIARY 則跟 OOP 中的 interface 類似,用於讓 entry 帶上需要的屬性,而 ABSTRACT 則類似 root interface 或是 Java 中 Object 這個 class。ABSTRACT ObjectClass 一般不會自行定義,僅沿用系統中預帶的 top ABSTRACT ObjectClass。

AttributeType
相對於 ObjectClass 定義 compound type 的結構,AttributeType 可視為用於定義 scalar type。注意:LDAP 的所有 ObjectClasses 都只能是單層結構,不允許擁有型別為其他 compound type 的 fields。

此外,AttributeType 的定義中會指明套用的 equality rule,類似於 OOP class 中常實作的 equals() method。如同 ObjectClass,AttributeType 也可以繼承其他的 AttributeType,用於直接延用 base attribute 的 equality rule。

MatchingRule
即前述 equality rule 的實作,常見的有:
  • caseExactIA5Match
  • caseIgnoreMatch, caseIgnoreIA5Match, caseIgnoreListMatch
  • distinguishedNameMatch
  • facsimileTelephoneNumber, integerMatch, telephoneNumberMatch 等。

了解這三個幾本名詞後,下一步就能對 LDAP 中如何依賴 distinguish name (DN) 定位一個 entry,以及 entries 如何形成 directory information tree (DIT) 樹狀結構進行了解了。

see also:
[1] Wikipedia: LDAP
[2] Wikipedia: X.500
[3] ISO/IEC 9594-1:1990
[4] Understanding the LDAP Protocol, Data Hierarchy, and Entry Components
[5] LdapWiki.com

2018-02-07

C# URL Encoding (Con't)

前一篇 [1] 提到當使用 URL 傳參數遇到一些特殊字元時,.NET framework 提供了幾種編碼方式都不能完整處理,需要另行寫一段轉換程式,將參數另行處理,甚至需要將 IIS 的 allowDoubleEscaping 打開方能順利執行。

為了避免掙扎於 IIS 安全機制與參數編碼難以兩全的問題,同事幫忙找到在編寫轉換程式時可以使用 HttpServerUtility.UrlTokenEncode() 處理參數;雖然還是得自己寫一段程式,但可以不用更改 IIS 設定。本質上這個編碼接近 Base64 [2],但是因為原始 Base64 仍會生成某些特殊字元 (+, /, =),因此這裡等於是 Base64 的變種。

關於這三個 class 與 .NET framework 版本的關係,稍微整理了一下:
Class Name.NET Framework.NET Core
System.Web.HttpServerUtility1.1N/A
System.Web.HttpUtility1.12.0
System.Uri1.11.0

附帶一提:微軟推出新版的 API Browser,將原先在 MSDN 上的 API 文件改放到 docs.microsoft.com;新版的站台提供較多有關 .NET Core 或 PCL 版本的資訊,但也捨棄了 .NET framework 4.5 以前的資訊,因此查不到各別 class 起源於哪個版本的 .NET framework。

see also:
[1] C# URL Encoding
[2] Wikipedia: Base64
[3] HttpServerUtility: MSDN, docs.microsoft.com
[4] HttpUtility: MSDN, docs.microsoft.com
[5] Uri: MSDN, docs.microsoft.com

2018-01-16

C# URL Encoding

被這東西咬第二次了,想想還是釐清楚記下來。

這個議題事實上已經被講很多了,寫得最好得應該是 [1];候選的函式有:HttpUtility.UrlEncode()、Uri.EscapeUriString()、Uri.EscapeDataString(),主要差異在於:
  1. 適用的輸入是 URL 整體,還是個別參數? 上面三個只有 Uri.EscapeUriString() 不會對 ':' 跟 '/' 字元編碼,可以用在 URL 整體,另外兩者都不行。
  2. 會處理的字元跟未編碼字元各有哪些?HttpUtility.UrlEncode() 跟 Uri.EscapeDataString() 除了會對 ':', '/' 編碼外,還會處理 ' ' (space), '?', '+', ' (single quote),對於 '.' (dot) 都不處理。
Uri.EscapeUriString() 雖然可以直接丟整個 URL 進去,但主要問題在於:太多特殊字元都不處理,包括 ' (single quote), '+', '?' 等。至於在 HttpUtility.UrlEncode() 跟 Uri.EscapeDataString() 間選擇時,差異不大,只有大小寫跟空白字元處理方式,兩者都是合法的,可以自由挑選;但也都因為不能處理完整的 URL,所以需要自己另寫一段程式 [1]
  • 分離 query parameter 與原本的 URL;
  • 將 query parameter 的 key 與 value 都使用 Uri.EscapeDataString() 編碼。
注意:範例中的程式碼不處理 path parameter,如需套用至 path parameter 應該也是可行,但當使用 encoded string 發出 request 時,IIS 預設的 request filtering 會擋掉該 request 並拋出 HTTP substatus 404.11 (URL Double Escaped),需要將 allowDoubleEscaping 設定為 true 才能通過。

Encoding vs Double Escaping?

這邊需要釐清兩件事:
  • 何謂 double escaping?
  • 將 URL 中的 query parameters 編碼是否屬於 double escaping?
由於在 RFC 2396 [2] 中,將特殊字元轉換為 '%' 加上 16 進位數字的動作稱為 escaped encoding,因此 HttpUtility 使用了 encode 這個動詞,而 Uri 與 IIS request filtering 使用 escape,實質上是可以互換通用的。

其次,一般只呼叫一次 Uri.EscapeDataString() 的結果字串屬於 single escaping,並不符合 double escaping 的條件,但由於 ' ' (space) → '+' (plus sign) → %25 這個轉換路徑,IIS request filter 誤以為 %25 是源自 ' ' (space),歷經 double escaping 得到,因此拋出 404.11 錯誤。

這邊有一篇文章 [3] 寫得非常仔細,將 request URL 經 Windows Http.sys、IIS Request Filtering、IIS Core、Handlers 等四層處理過程都詳細解說,值得一讀。

see also:
[1] Huan-Lin 學習筆記: URL 編解碼問題
[2] RFC 2396
[3] Use of special characters like '%' ‘.’ and ‘:’ in an IIS URL

2018-01-04

Kayako Cron Tasks Management

傳統 web application 實作 cron jobs 後,驅動源頭大多來自 web container 或作業系統 crontab 設定,由 application 外部負責定期啟動內部的 cron task manager,看看是否有哪個 cron task 該被執行。

Kayako 在 cron task manager 以下的實作跟一般作法沒有什麼不同,比較特別的是:它把 cron task manager 暴露成一個 Web API (https://site/cron/index.php?/Base/CronManager/Execute) 讓瀏覽器直接執行,而不仰賴 web container 或作業系統設定。

至於執行的時機與頻率,Kayako 把這段邏輯包在 https://site/Core/Default/Compressor/js 中,讓每個頁面都 include 這段 JavaScript,也就是當系統任何一個畫面被瀏覽器訪問時,都會同時執行一次內部的 cron task manager。

Cron task manager 的 Web API 先由 Controller_CronManager 處裡,接著進入真正的 SWIFT_CronManager::RunPendingTasks()。

2017-12-28

Kayako Language Engine and Caching System

先前處理 Kayako email 發送的多國語系問題時,採用新建 LanguageEngine instance,透過 LanguageEngine instance 抓取所需詞彙的方式完成。後來發現,這樣的解法會影響其他信件的發送,例如:ticket auto-close 時的通知信件,會因無法抓到所需詞彙而失敗,追根究柢後發現:無論產生多少新的 LanguageEngine instances,底層實際載入與儲存詞彙都使用相同一塊空間,無法避免動態切換語系時交互影響。

上圖是 LanguageEngine 與 SWIFT caching system 之間的關係。

  1. 首先 LanguageEngine 在載入詞彙時會提供 Laod() 或 LoadQueue() 供外部呼叫,最終都落到 SWIFT 的 caching system,使用 SWIFT_CacheStore->LoadQueue()。
  2. SWIFT_CacheStore 裡頭涉及 2 內部變數:CacheMemory (SWIFT_CacheMemcache) 與 Registry (SWIFT_Registry);因為目前系統並未設定 MEMCACHE_HOST,因此這邊只會使用 Registry 作為實際儲存位置。
  3. SWIFT_Registry 內部的 storage 分為兩層,level 1 的是 in-memory 的 _registryCache,level 2 是 database 的 swregistry table。
這邊要注意:SWIFT_Registry instance 是在 SWIFT 中生成,因此無論每次執行無論處理過多少張 tickets 都會使用相同的 SWIFT_Registry instance,且單一 SWIFT_Registry instance 中,每一個 key 值只會對應到一個 value。

為了避免語系切換時,汙染 SWIFT_Registry instance 中的語言詞彙,因此決定改採直接 SQL query 的方式載入所需語言。

ps. 這邊的 caching system 指的是 data cache,Kayako 在 4.0 剛推出時並沒有針對 Memcached 整合的功能;倒是官方推薦使用針對 code cache 的 XCache [1]。相較於新版 PHP (>5.5) 更被推崇的 code caching system 是 opCache [2]

see also:
[1] Kayako Forum: V4 Very Slow!
[2] StackExchange: PHP 7 opCache v PHP 5.6 XCache

2017-12-10

.NET Standard 2.0

先前 [1] 分別從 framework 與 virtual machine 兩個面向聊過 .NET framework 的角色,其中提及 Common Language Infrastructure (CLI) 的三大組成:Common Intermediate Language (CIL), standard libraries (BCL and FCL), 與 Common Language Runtime (CLR)。

最近因為 .NET Core 2.0 的正式發佈,越來越多人開始回頭看原本 .NET framework 的組成,以及各種實作與標準之間的關係;這兩天剛好看了 InfoQ 上的一篇文章 [2];再繼續之前,首先要抓準一個概念:現行的 .NET 平台實作不只 .NET framework 一家,還有 .NET Core, Mono, Xamarin 等等,而各家實作在跨平台的特性與實現方法上也不盡相同,根本原因在於一開始的 CLI standard library 以大鍋菜的方式設計,雖然有 BCL 與 FCL 之分,但 BCL 並未很好地把 Windows dependent 的功能剝離出去,導致後續跨平台 runtime 實作困難。

以下是順手整理的心得:
  • .NET Standard 重新定義了 BCL 的標準,讓各家 (.NET Framework, Xamarin, and .NET Core) 實作的 BCL 趨於一致。
  • 針對 BCL 之上的 application models,.NET Core 捨棄 Windows Forms 與 Windows Presentation Foundation (WPF),僅保留 Console,並實作 APS.NET Core [3]
簡單來說,.NET Standard 是未來跨平台元件或應用程式開發時的必要 (但非充要) 條件,畢竟一個可以正常運作於 .NET Standard 實作上的元件,仍可能依賴該實作的特定函式庫。

至於 Portable Class Libraries (PCL) 則是針對既有 .NET 平台實作所提的暫時方案;透過 Visual Studio 與 PCL 的幫助,Visual Studio 可自動將程式中 dependency 對應到各家實作的函式,免去開發人員額外偵測與判斷的痛苦。有關 PCL,個人認為 InfoQ 文章中參照的 [4] 寫得比較完整。

附帶一提:當微軟在 2017/8/13 發佈 .NET Standard 文件 [5] 時,裡頭的對應表可能讓人誤以為 .NET framework 4.6.1 就已經實作 .NET Standard 2.0;實際上,在 .NET framework 4.6.1 尚須加裝 .NET Core 2.0 SDK 才能支援,否則會缺少 200 多個 API 實作。因此真正的完整支援要到 .NET framework 4.7.1 才算完成 [7]

see also:
[1] .NET: Framework or Virtual Machine
[2] InfoQ: .NET Core and .NET Standard: What Is the Difference?
[3] .NET Standard - Demystifying .NET Core and .NET Standard
[4] Understanding .NET Core, .NET Standard, .NET Core applications and ASP.NET Core
[5] Microsoft Docs: .NET Standard
[6] Microsoft .NET Blog: Announcing the .NET Framework 4.7.1

Salesforce SOSL, SOQL, and Tokenization

Salesforce 在登入後的畫面頂端提供了一個搜尋框,提供使用者搜尋各式資訊,例如:Account, Contact, Opportunity, Contract 等。

但由於預設的搜尋行為使用其內部的斷詞系統,因此針對一些專有名詞搜尋時,可能就不那麼盡如人意;例如:當資料為東京都時,可以用東京或完整的東京都搜尋到,但是無法用京都搜尋到該筆資料,或許這在很多時候是好用的,尤其當大家公認東京都跟京都是完全不同的 entity,但有時候當使用者只知道公司部分名稱時,就容易造成困擾。以下是 Salesforce 搜尋框的兩個主要限制:
  • 預設行為為 start with:一如前例,系統搜尋行為會找出以京都為起始字串的資料,如果想找到東京都,搜尋時必須輸入「*京都」。
  • 底層以 morphological tokenization 為基礎的索引:換言之,當專有名詞不常見時,可能因斷詞系統無法正確辨識而找不到。

上述兩個問題在斷詞系統剛從 bigram 轉換為 morphological [1] 時備受批評,但在 2017 年 4 月 [2] 跟 6 月 [3] 有修正得更精細些,例如:「横浜市中区」已經可以使用「中区」找到,關鍵不只是 start-with 的問題,更在於斷詞系統能識別「中区」一詞;然而對於廣大可能的中文專由名詞來說,這樣的解法仍然不夠。

其實英文也不見得就完全沒事,因為有時候在專有名詞欄位,使用者可能只輸入縮寫,甚至是自創的縮寫,這時候 morphological 一樣無法正確辨識並提供搜尋時所需的索引。

想要徹底解決,自己動手寫點程式可能免不了,就 Salesforce 提供的 SOSL 與 SOQL 而言,前者仍然以斷詞系統為基礎 [4],因此只能選用 SOQL 的 like-anywhere 完成,但相對效能上難免吃重些。

see also:
[1] Salesforce Release Notes (Winter'15): Users Searching in Chinese, Japanese, Korean, and Thai Find Optimized Results
[2] Salesforce Community Known Issues: New morphology tokenization for Japanese characters after new search framework can cause unexpected morphological tokenization result
[3] Salesforce Community Known Issues: Chinese - Morphology tokenization improvement for mainly proper name noun
[4] Force.com SOQL and SOSL Reference: Introduction to SOQL and SOSL
[5] Salesforce Community Known Issue: Wrong results for Japanese OR search for file content
[6] ROSETTE.com: Why Tokenization Matters

2017-12-08

Unit Test for Kayako

接手 Kayako Fusion 這套系統超過一年了,一直沒有作單元測試,主要原因有以下兩個:
  1. License 驗證:幾乎所有測試需求都跟底層的 SWIFT framework 有關,而要載入這個 framework instance 的前提是 license 被驗證為有效。
  2. 缺乏文件:雖然它們家的 SWIFT_TestCase 也是繼承自 PHPUnit_Framework_TestCase,但不知如何執行。
然而直接在 UI 端作 acceptance testing 就不用煩惱這個問題,只要修改 client 端的 host 設定,將 license 中的 domain name 綁定測試機 IP。這時候即使測試機上設定其它 hostname,只要 client 端瀏覽器使用 license 的 domain name 連道測試機,就可以正常執行。

最近因為修改的東西愈趨複雜,很多問題無法單純在測試機上透過 acceptance testing 讓問題浮現,往往都是 patch 上正式機才爆出,因此需要回頭徹底解決,步驟上大致如下:
  1. 安裝 PHPUnit;因為 PHP 版本還在 5.4 系列;只能安裝 PHPUnit 4.8.36 [1] [2] [3]
  2. 修改 $KAYAKO_HOME/tests/index.php;因為 license 檢驗,會比對 license 中的 domains 與 $_SERVER['HTTP_HOST'],而在 UI 端作 acceptance testing 時,$_SERVER['HTTP_HOST'] 來自 HTTP request header 中的設定 [4] [5],改執行 unit testing 時缺少真正的 request header,因此要在 index.php 中自行設定,並作為執行時的 bootstrap script。
  3. 執行範例如下:/usr/local/bin/phpunit.phar --bootstrap ./index.php ./__swift/library/Date/class.SWIFT_DateTest.php
官方預帶了一些 test cases,分別針對 KQL, KQL2, Cron, Date,可以針對需求仿造。

後續可能要再看一下 mock object 與相關 inject testing database 的方法,這樣就可以把單元測試這塊補上。

see also:
[1] PHPUnit
[2] PHPUnit Download
[3] PHPUnit 4.8 Manual (PDF)
[4] PHP.NET: $_SERVER
[5] StackOverflow: $_SERVER['HTTP_HOST'] not set
[6] Kayako Classic User Guide
[7] Kayako Developer Resources

2017-11-05

Salesforce Secured Callout

在應用系統介接時,使用 Web API 是目前常見的手段;Salesforce 也提供這樣的模式,無論是扮演 service provider (使用內建的 SOAP Web Services,或是自訂 RESTful API),或是 service consumer,都有對應的支援;其中當 Salesforce 扮演 service consumer 時,最常見的是使用 Apex 自行呼叫外部 Web API,這種呼叫模式在 Saleforce 中稱為 callout。

在安全性方面,系統介接時一般會優先討論連線方面的安全性,更嚴謹的才會觸及資料面的安全性;這邊我們只討論前者,也就是透過 TLS 對 HTTP 加密,這樣的外部呼叫稱為 secured callout。

值得注意的是:Salesforce 在實作 secured callout 時,對簽發 target service 憑證的 CA 只認可正向表列中 root CA 簽發的憑證 [1] [2],然而這份列表有些陳舊 [3],許多常見的 root CA 都不在列表中,例如 TWCA Global Root CA,若 target service 不是自有的服務,無法變更憑證時,可能會被迫使用未加密的 HTTP。

另外在 protocol 版本上,Salesforce 支援 TLS 1.1 與 1.2 [4],TLS 1.0 由於可降階至 SSL 3.0 且 SSL 3.0 設計上有安全性缺陷,因此目前已停用 TLS 1.0 [5]

除了 secured callout 外,另一個會使用到憑證的場合是:使用 Salesforce 實作自訂的網站,並掛上自家的網域名稱;這時候對於憑證的限制比前者稍微放寬,允許不是由 root CA 直接簽發的憑證。管理者可以新增所需的 intermediate CA 到 Salesforce [6],但根源的 root CA 仍必須是列表中認可的。

至於新增 custom site 憑證到 Salesforce 時,由於這邊扮演的是 service provider,因此上傳的資訊中必須包括 private key;使用的檔案格式是 Java KeyStore,這方面的基礎知識可以參考 [7]

see also:
[1] Outbound Messaging SSL CA Certificates
[2] Know More about All the SSL Certificates That Are Supported by Salesforce
[2] Salesforce Community Idea: Manage List of Trusted Certificate Authorities 
[3] Summer '15 Release Notes: Test and Use Advanced Networking Protocols
[4] Salesforce Disabling TLS 1.0
[6] SSL Certificate Standards
[8] Making Authenticated Web Service Callouts Using Two-Way SSL

Salesforce Release Cycle and Certification

Salesforce 維持每年三次的更新節奏 [1],分別為:
  • Spring Release: February
  • Summer Release: June
  • Winter Release: October
其中 Winter release 會標示來年的年份,例如 2017/10/16 發表的 Winter release 會標示成 Winter'18。

此外,每次新版發表後,官方會給予所有證照持有人約 8 個月的時間,對更新內容進行測驗;起算時間從測驗提供的起始日起算,例如:Winter'18 的發表日是 2017/10/16,但是更新測驗的起始日是 2017/11/20,往後約 8 個月,到 2018/7/13 是證照持有人需完成測驗的期限。

費用方面,目前基本的 Salesforce Developer Certification 考試費用是 USD $200,之後針對新內容的測驗費是 USD $100/year,如果同時擁有多張證照,每年維持有效的測驗費會有些可觀,但目前上限是 USD $300。

see also:
[1] Salesforce Blog: Seasonal Release
[2] Salesforce Certification: Maintenance Exam
[3] Salesforce: Certification Mainenence Overview
[4] Salesforce: Cost to Maintain a Salesforce Credential

2017-10-30

Salesforce: Objects Generalization and Specialization

Salesforce 在設定畫面上的 object 名詞可能是個讓習於物件導向程式開發者困惑的地方:畫面上的 object 代表 type definition,而非 instance,真正描述 instance 的名詞是 record。

然而在處理 generalization & specialization 議題時,畫面上又給了一個新名詞,record type。系統管理者可以把所有可能用到的欄位都開在同一個 object 裡,透過 record type 的方式,限定各個 record type 所能看到的欄位。值得注意的是:這邊的縮限手段,是採用綁定 layout 的方式達成,如果使用 Apex 程式存取,則不在此限。

從 generalization & specialization 的抽象層次來看,record type 確實提供了一種 specialization 的途徑,與物件導向中的 subclass 類似,但很不一樣的是:record type 處理的是資料面的 specialization,讓特定 record type 只能看到原來眾多欄位的子集合,而 subclass 同時兼顧資料面與行為面的 specialization,各個 subclasses 能夠自己額外定義欄位並實作不同行為。

此外,record type 並不像物件導向的繼承關係將共同的部分獨立抽取至 super class,相對於 record types,object 等於是所有欄位的大雜燴,看不到哪些是共同的欄位,對於 generalization 有潔癖的人來說,似乎只能用類似物件導向 composition 的方式處理 [1],也就是 Salesforce 中的 master-detail object relationships [2]

使用 master-detail relationships 實現 generalization 難免有強加物件導向觀念的味道,但更不幸的是:即使採用這麼複雜的設定,仍然無法達成目的。原因在於:使用 master-detail relationships 實作 generalization 時,生命週期控制在 master 端,相當於是 specialized subclass;但在數量對應上 master 端屬於 one-side 而非 many-side,違背 generalization 的初衷。

see also:
[1] Salesforce Developers Forum: Custom Objects and Inheritance
[2] Salesforce Object Relationships

2017-10-24

Apex Serialize Objects to XML

先前的文章 [1] 中提到,Salesforce 的 REST 同時支援兩種輸出格式,XML 與 JSON;client 可以透過 HttpRequst header 中 Accept 屬性指定想要的 content type;這兩個 serialization 的過程對於 REST service 的實作者而言是完全 transparent 的,意即實作時 method 並不 return string 型別,而是直接 return sObject,由系統的 outbound messaging 機制直接處理掉。

然而對於 Apex 開發者而言,程式語言級別對 XML 與 JSON 在 serialization 的支援度就差很遠。以 JSON 為例,無論物件多複雜,要轉成 JSON string 只要一行 JSON.serialize() 就可以搞定 [2],但是轉成 XML string 時,無論使用 XmlStreamWriter [3] 或 DOM.XmlNode [4] [5] 都需要針對物件中各個 fields 作繁瑣地處理。

為了達到快速生成 XML 字串,我們可以透過 (1) 實作一 REST service;(2) 在 Apex 中呼叫該 service,並將 HttpRequest Accept header 設定為 'application/xml' 的方式,快速從 HttpResponse.getBody() 取得 outbound messaging 生成好的 XML 字串。

概念上是走 loopback invocation 的方式,利用既有的機制,寫最少的程式,完成任務。

see also:
[1] Salesforce RESTful Services: Response Content-Type
[2] JSON Support: Roundtrip Serialization and Deserialization
[3] XML Support: Writing XML Using Streams
[4] XML Support: Reading and Writing XML Using the DOM
[5] XmlNode Class
[6] Salesforce Trailblazer Community Idea: Convert Object to XML (Serialize)

.NET: Framework or Virtual Machine

一般提到 .NET 多半使用 .NET framewrok 這個名詞,內容可能包括:
  • Programming Languages: 包括 C#, C++/CLI, F#, IronLisp, IronPython, VB.NET, Powershell 等 [1]
  • Compiler
  • CLI Standard Library: 裡頭主要有 Framework Class Library (FCL) [2] 與 Base Class Library (BCL) [3]
  • Common Intermediate Language (CIL)
  • Common Language Runtime (CLR)
對於傳統使用 C++ 搭配 MFC 開發視窗程式 (GUI desktop application) 的開發者而言,視線會集中在 C# 與 CLI Standard Library;此時 .NET 更趨近於 framework 的角色,提供眾多的 classes,開發人員根據 framework 規範撰寫 component。

然而就 .NET 整體架構來看,最大的改變在於提供了統一的 intermediate language (IL) 介面,使得前端可以同時支援多種程式語言,呼叫同一套 standard library;且這個 IL 設計是獨立於 CPU architecture 與 operating system platform 之外的 [4]。換言之,開發者可以自由地在任何想要的平台上開發 IL 所需的 runtime,因此角色上更接近 virtual machine。

雖然說大多數的 .NET 應用程式都在 Windows 與其內建的 runtime,但還是有不少開發者作過這方面的努力:
但實際使用上,卻不那麼順利。主因在於 .NET 雖然 CIL 提供了優良的隔離,獨立於 Windows 之外,但最重要的 CLI Standard Library 卻因為服務視窗程式開發者的初衷,納入了太多 Windows 特有的 library,尤其是圖形介面 (GDI) 這段,導致許多在 Windows 上開發的元件,無法順利過渡到其它 runtime 執行。

換言之,雖然 CIL 與 CLR 提供了很大 virtual machine 的特點,但身為 runtime infrastructure 重點的 standard library 並未提供相應的設計,導致整個 .NET 回到近乎 Windows 獨有的情況。

這幾年微軟開始改革 Windows Server 的產品線 [8],將圖形介面剝離出來,打破原本 Windows runtime 包山包海的狀況,同時也開始推更乾淨的 .NET Core;就架構而言這應該有助於第三方 .NET runtime 的普及,但微軟將發展 Mono 的 Xamarin 收入麾下卻讓非微軟勢力瞬間消失。

注意:Mono 在 runtime 實作仍是按照舊有的 CLI (ECMA-335) [9] 而非 CoreCLI,因此 Mono 是否會如 .NET Core 壯大仍有很大變數。

此外,Mono 的貢獻在不僅僅於 runtime,還有跨桌面平台的解決方案,這塊的未來應該屬於 Electron [10];至於依賴 Mono runtime 的跨行動平台方案 - Xamarin,個人認為疑慮就很大,目前 Windows Phone 已確定式微,微軟還願意花多少資源給跨行動平台開發方案,看來非常不樂觀。

這兩年微軟也開始引入 LLVM 技術,實作相關的 compiler (LLILC),想要深入這個主題的可以一起參考看看。

see also:
[1] Wikipedia: CLI Languages
[2] Wikipedia: Framework Class Library (FCL)
[3] Wikipedia: Standard Libraries (CLI) 
[4] Wikipedia: Common Intermediate Language (CIL)
[5] Wikipedia: DotGNU
[6] Wikipedia: Protable.NET
[7] Mono Project
[8] Wikipedia: Microsoft Windows Server Core
[9] ECMA-335: Common Language Infrastructure (CLI)
[10] Electron

2017-10-13

SSL Certificate Standards

要搞懂系統認證之前,首先要對非對稱式加密有基礎瞭解,不外乎就 2 種 key:
  • Public key: 用於加密與驗證簽章
  • Private key: 用於解密與產生簽章
至於這兩把 key 的產生通常會由共同信任的第三方,也就是 CA (Certificate Authority) 發出。[1]

Standards: Encoding Rules & Formats

接下來可能會遇到各種檔案,這裡頭牽涉的標準主要有 2 類:
  1. Encoding rule: 說明內容要如何編碼,主要有 DER 與 PEM [2] 兩種,前者的輸出為 binary,後者為 base64。
  2. Message or file format: 定義檔案應該包括那些資訊,主要有 PFX, P12 (PKCS #12), CSR (PKCS #10), X.509
  • PFX 或 P12 (PKCS #12) [3]: 也就是 CA-signed certificate;這類檔案帶有 private key,一般會被保護起來,很少複製傳遞。
  • CSR (PKCS #10) [4]: Certificate Signing Request; 為 client 向 CA 要求簽發憑證時使用,帶自身站台資訊,最少使用。
  • X.509 [6] 或 P7C (PKCS #7) [7]: 最常見,用於存放 public certificate 或 signature,其中 P7C 預定為 BER encoding,因此不會與其它 encoding rule 併用。
File Name Extensions

上頭這些標準名稱都常見於副檔名,譬如說:
  •  *.pfx 與 *.p12 一定是 CA-signed certificate,未指定 encoding rule,但通常為 binary,可與 PEM 合併使用。
  •  *.p7c 一定是 public certificate,固定為 BER encoding。
  •  *.der 與 *.pem 通常僅為 public certificate,用副檔名描述編碼方式。
除此,*.cer 與 *.crt 也是常見的副檔名,大多也僅為 public certificate,可套用任何 encoding rule。

最後是這些 key 如何被保存到 client 與 server;對系統來說,存放位置分為 TrustStore 或 KeyStore,分別存放扮演 consumer 與 provider 時,使用的 public certificate 與 private keys。
  • 與一般 symmetric authentication 所稱的 certificate 一詞不同,symmetric authentication 中的 certificate 多半為 username/password,但這邊的 certificate 泛指 signature 或包含 private key。
  • 例如:CA-signed certificate 即為兩者組合,而 public certificate 則為 signature。所以當單純看到副檔名 *.crt 或 *.cer,並無法確定內容是否包括 private key,類似的情況也發生在 *.pem 的檔案。

ps. Java 的 KeyStore 副檔名通常為 *.jks,由於 KeyStore 用來存放 CA-signed certificate 的關係,有些系統便使用 Java KeyStore 檔取代 *.pfx 或 *.p12,作為匯入 CA-signed certificate 時使用。


see also:
[1] Public Key Infrastructure
[2] PEM (Privacy-enhanced Electronic Mail): Base64 encoded DER certificate
[3] PKCS #12: Personal Information Exchange Syntax Standard
[4] CSR (Certificate Signing Request)
[5] PKCS (Public Key Cryptography Standards)
[6] X.509
[7] PKCS #7: Cryptographic Message Syntax
[8] DER vs. CRT vs. CER vs. PEM Certificates and How To Convert Them


Salesforce RESTful Services: Publish

當使用 Salesforce 作為 API provider 時,預設採用 OAuth 2.0 作為認證機制,如果想簡化認證過程,則可透過將 API 發佈到公開站台的方式完成,此時需要針對 site profile 進行設定。
  • 首先要進到 site details page;在 Setup / Build / Develop / Sites,點選 Site Label 欄位 (不是 Edit 也不是 Site URL)。
  • Site Details page 上方有個 Public Access Settings button,點進去後可能會因為 Org 不同而出現不同畫面:
  • Personal Develop Org: 畫面為一個很長的表格,要設定的地方有:Enabled Apex Class Access, Field-Level Security 與 Object Permissions。
  • Production Sandbox Org: 分成 Apps 與 System 兩段,需設定的連結落在 Apps 裡頭,分別是 Apex Class Access 與 Object Settings。
設定的重點不外乎:service class 本身以及相關需要存取的物件欄位,因此只要分別在 Apex Class 與 Objects 中將相關權限打開即可。

see also:
[1] Public RESTful Web Services on Force.com Sites
[2] Public Access Settings for Force.com Sites
[3] Public Access Settings in Salesforce

Salesforce RESTful Services: Response Content-Type

Salesforce 實作 RESTful 時,不需額外的程式碼,系統會自動支援 2 種 response Content-Type:JSON 與 XML。

但由於回傳 XML 時,內部的 tag 名稱會代出自訂欄位與物件的名稱,並不存在於 W3C 預設 namespace 中,而 Salesforce 亦不會代為產生對應的 XML schema,因此對於某些 XML parser 來說會造成 parsing error,比較直覺的解法是繞過回傳 XML 改使用 JSON。

ps. 實際測試時,回傳的 XML 字串會設定 namespace 為 http://soap.sforce.com/schemas/class/ 開頭的 URL,但並未實際生成 XML schema。

前面說過 Salesforce 會自動支援 2 種 response Content-Type,切換的依據是根據 request header 中的 Accept 屬性。
  • 一般瀏覽器 (Chrome, Firefox, IE) 預設的 Accept Content-Type 為 text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8,此時 Salesforce 會優先取用 application/xml
  • 當未設定 Accept Content-Type,或指定 Accept Content-Type 為 application/json 或空白時,Salesforce 預設會以 JSON 作為 response Content-Type。
如果想直接在 server 端指定只支援 JSON,則必須將 method 的 return type 改為 void,並自行指定 HttpResponse,填入 JSON 字串。

RestContext.response.addHeader('Content-Type', 'application/json');
RestContext.response.responseBody = Blob.valueOf('{ "result": true}');

ps. 回傳的 charset 預設皆為 UTF-8,不需要額外設定 Accept-Charset。

see also:
[1] Apex REST Methods
[2] How can I return JSON object from REST service?

2017-10-06

Apex Describe Information

Salesforce 針對 SObject 與 field 分別提供相關的 describe information (a.k.a. metadata),有點類似 Java 中的 java.lang.reflect.Type 與 java.lang.reflect.Field;不過 Salesforce 進一部拆分成 token 與 describe result,前者為一個 compile-time generated & runtime immutable reference,沒有細部資料,但可用於比對 (==),而後者才是真正具備細部資料的結構。

  • Token 轉換至 describe result 的行為稱之為 describe,呼叫 getDescribe() method,可視為 dereference & data loading。
  • SObject 與 field 的 token 與 describe result 資料型別與相關取用方式分別為:
下圖為 4 種 describe information 的產生與轉換方式:
  • 除了 SObject 的 token 與 describe result 有機會從 instance 取得,其餘都必須在 Apex code 中指定 SObject 或 Field name 才能獲得。
  • 不只 SObject instance 有提供 getSObjectType() method,其它提供此 method 的還包括:SObject Describe Result, List 與 Map;其中 List 與 Map 呼叫 getSObjectType() 時等同於針對其 element instance 呼叫該 method。

see also:
[1] Apex Developer Guide: Schema Class
[2] Apex Developer Guide: Schame Namespace
[3] Apex Developer Guide: Dynamic Apex

2017-09-28

Salesforce Apex REST

REST 是目前實作 web API 時較為普遍的選擇,Salesforce 原生在 APEX 中也提供了這方面的支援,方便開發者自訂 API 作為系統整合使用。在 APEX 有以下 6 個 REST annotations:
  1. @RestResource
  2. @HttpDelete
  3. @HttpGet
  4. @HttpPatch
  5. @HttpPost
  6. @HttpPut
值得注意的是:APEX REST annotations 中只有 RestResource 才有 urlMapping 參數,且該 annotation 為 class level;換言之:
  • 每一個 service implementation class,針對各種 HTTP requests 類型只能有 1 個 method;不能同時宣告多個 @HttpGet methods 在 1 class 中;
  • Service implementation class 中的 class name 並不會浮現在 URL 中。
see also:
[1] Apex REST Annotations
[2] Creating REST APIs using Apex REST

Kayako Template & Language Engine

Kayako Fusion 是一套 on-premise helpdesk 系統,使用的語言為 PHP,framework 為自有的 SWIFT framework,在修改或整合上會需要一些功夫去瞭解系統架構,才不至於出現難以維護或資料不一致的現象。最近處理了一段有關多國語系的問題,這邊順便將心得記錄一下。

首先,Kayako 中提供了兩個元件來處理多國語系:Template Engine 與 Language Engine,分別負責處理文本的模版 (template) 跟各語言詞彙 (phrase),資料來源都同時支援 database 與 file system, 下圖說明這兩個元件的關係與 data flow。


值得注意的是:
  • Language Engine 透過一個 key-value array 記錄某個 phrase key 對應的字串,其中 phrase key 並不包括語言別資訊,因此一個 engine instance 只能處理一種語言,否則會出現 key conflicts 現象。
  • Template Engine 不涉及語言別的問題,因此系統可共用一個 template engine instance;反之 Language Engine 必須針對需要的產出的語言,生成對應的 instance。
在 Kayako 原本的邏輯中
  • 取用 Language Engine instance 的邏輯被放在 TemplateEngine->Get() 中,除了 Web 操作根據 cookie 在底層 SWIFT 生成不同語言的 instances 外,其餘都使用預設的 (English) Language Engine instance。
  • TemplateEngine->Get() 不帶 phrase keys 參數,Language Engine 負責載入所有的 phrases,交由 Dwoo core 產出最終文本。

因此如果我們要讓非 Web 操作也能產出多語言內容的話,有兩種選擇:
  1. 修改  TemplateEngine->Get(),新增語言別參數,動態產出對應的  Language Engine instance。
  2. 在呼叫  TemplateEngine->Get() 前預先產出所需的 phrases,並於呼叫時傳入。
這邊我採用第 2 種作法,目的在於降低 Language Engine 載入的 phrases 數量。

假設今天我要製作一個具備多國語系的畫面,除了預先將 template 跟 phrase 準備好存入 database 或 file system 外,主要的 rendering 邏輯如下:
  • 根據需要的語言別,初始化 Language Engine instance。
  • 根據 phrase keys 與 language id (or language code) 透過 Language Engine 載入 phrases。
  • 呼叫 Template Engine 中的 Get(),傳入 template key 與 phrases,生成 output content;其中 template engine 會在 SWIFT_Base 中初始化,如果已繼承 SWIFT MVC classes 或 SWIFT_Library,這邊可以直接取用,不需要另行初始化。

2017-09-21

Salesforce: More about SOQL

上一篇整理了 Salesforce 中各式 object relationships 與相關 SOQL patterns;這邊想多聊一些 SOQL 撰寫時要注意的小地方。

Lazy Fetch

SOQL 算是把 lazy fetch 發揮得淋漓盡致;首先,它不像 SQL,SOQL 中並不提供 SELECT * 的語句,所有需要的 fields 除了 ID 都必須完整列舉。其次,即使只 SELECT 一個 field,回傳型別仍為 List<T> where T extends sObject,以下面這個針對 Account 物件搜尋的 SOQL 為例:
SELECT Name FROM Account
回傳的型別是 List<Account> 而不是 List<String>。

Query from One-side
如同操作 relational database,Salesforce 也提供了一些介面方便使用 SOQL 查詢系統中的資料,無論是 Workbench 或 Eclipse 中的 schema view,都會將結果以表格 (tabular) 的方式呈現。這跟 SOQL 以 object-oriented 的操作思維有些衝突,尤其是針對 one-to-many relationships 查詢時。以下是從 one-side 出發使用 outer JOIN 後在 Workbench 中得到的錯誤訊息。
看來 Salesforce 在這塊並不像 report 介面提供表格中再展開子表的能力,直接給了 parent relationships queries are disabled 的錯誤訊息。

2017-09-19

Salesforce Object Relationships

Salesforce 中有關 objects relationships 對新手往往是比較難懂的,主要原因包括:
  1. Object modeling 時採用的 schema builder 看似 ER model,但實際使用 SOQL 操作時,卻是以物件導向的觀點。
  2. SOQL 沒有 JOIN,只能透過 relationship traversal with path expression 完成各式的 implicit JOIN。
  3. Object 中記錄 relationship 的 field naming,有獨特的 convention,有時使用 __c,有時卻要使用 __r。
  4. Relationship 的 cardinality 固定為 one-to-many;使用 SOQL traverse relationship 時,從 one side 或 many side 出發會需要截然不同的 SOQL statement patterns。
另外還有幾點對撰寫 SOQL 關係不大,但也容易造成困擾:
  1. 設定 relationships 可能分為兩類:lookup 與 master-detail (a.k.a. MD)。
  2. 以物件導向的角度,上述分別是 aggregation 與 composition;前者是 whole-part 的關係,後者有一致的生命週期。
  3. 文件中的 parent 與 child,並非專屬於 master-detail relationship,純粹分別用於 relationship 中的 one side 與 many side。
[1] 是目前看過的參考資料中,最為完整的一篇,建議可以從這篇開始入門;相對其他文件,此處的名詞定義叫為明確,範例也相對清楚。這邊特別說明了如果 SOQL 以 one side (parent) 作為起點時,需使用 nested SELECT:
  • Both relationships are defined from the many-to-1 side, namely from a child to a parent. Note that we have utilized a NESTED select to obtain the records of related children from the parent. This kind of expression is very powerful in obtaining related records traversing from the 1 side in a 1-m relationship.
  • It is a useful pattern to obtain related information on a parent and all its children via traversing path expressions with relationship fields.
另外,當 SOQL 以 one side 出發並使用 nested SELECT 時,回傳結果的數量並非 Cartesian product,而是以 nested object 的方式呈現,意即每一筆 parent object 中含有多筆 child objects。

下表整理了 SOQL 針對不同 side 作為起點,如何透過 relationship traversal 達到各式 JOIN 的 patterns:
Outermost FROM Clause
One Side Many Side
Inner JOIN 在 WHERE clause 中使用 nested SELECT
SELECT Name
FROM Position_c
WHERE Id IN (SELECT Position__c FROM Job_Application__c)
在 WHERE clause 中直接取用 relationship
SELECT Name,Position__r.Name,
FROM Job_Application__c
WHERE Position__r.Department__c = ‘Sales’
Outer JOIN 在 SELECT clause 中使用 nested SELECT
SELECT Name, (SELECT Name FROM Job_Applications__r)
FROM Position__c
在 SELECT clause 中直接取用 relationship
SELECT Name, Position__r.Department__c
FROM Job_Application__c
ps. 上述 SOQL 中,只有在取用 relevant object 的欄位時才需要使用 __r notation,若只需該物件 id,則用 __c 即可。

[2] 說明了定義 relationship 時的命名原則:
  • 在 parent object 中定義 relationship 時:名稱須 unique to the parent,並且使用複數形的 child object name 命名。
  • 在 child object 中定義 relationship 時:名稱須 unique to the child,並且使用單數形的 parent object name 命名。
[3] 提供了一個從 child object (Daughter) 定義 lookup relationship 的範例
  • 按前述第 (2) 項原則,relationship filed 的命名應使用單數的 parent object name,此處命名為 Mother_of_Child (應可簡化為 Mother)。
  • Child Relationship Name 則以 child object 為主,命名為 Daughters。
    => Lookup relationship 中才談 Child Relationship Name,master-detail relationship 無此欄位。
  • 從 child object 為起點實作 outer JOIN:SELECT Id, FirstName__c, Mother_of_Child__r.FirstName__c FROM Daughter__c WHERE Mother_of_Child__r.LastName__c LIKE 'C%'
  • 從 parent object 為起點實作 outer JOIN:SELECT LastName__c, (SELECT LastName__c FROM Daughters__r) FROM Mother__c
see also:
[1] A Deeper Look at SOQL and Relationship Queries on Force.com
[2] Understanding Relationship Names
[3] Understanding Relationship Names, Custom Objects, and Custom Fields

2017-07-20

MVC, ASP.NET, and Salesforce VisualPage

表面上寫了好一陣 web application,骨子裡更多是寫 backend, libraries 與一些整合性的 service,UI 這塊一直非常薄弱。一開始想學 Struts2,看了些書,但一直沒落實到開發上;後來因為 Activiti BPM 接觸到 Vaadin 6,大概知道 component-based MVC 的樣貌,之後又碰了些 ASP.NET WebForm 這種以 view 為主的架構,算是有些粗略印象,該是要好好釐清的時候。

傳統的 MVC 源自單機的圖形化程式,model, view, controller 都在同一個 runtime 上,彼此溝通無礙;model 變化時可以順利更新 view,controller 只要負責處理 request 與 response 就好,簡單來說就是源自 UI control 與 model 的 events。其中 model 更新 view 這段連結又稱為 data binding。

這件事到了 web application 世界中有了大挑戰。

在 web application 中,view 屬於 client code,由 rendering engine 負責生成,而 controller 與 model 為 server code,server 端無法隨時連接 client 作畫面更新;因此出現了 MVC Model2。MVC Model2 並沒有解決問題,只是因應限制,釐清與傳統 MVC 的不同,MVC Model2 中,model 不負責將結果更新到 view,改由 controller 負責,至於如何實現一時還沒有答案。

這個時期出現幾種作法,最直覺得就是在 view 上頭掛一段 JavaScript,定期刷新頁面,包括畫面跟資料。微軟為了比較好的使用者體驗,在 1999 率先提出 XMLHttpRequest (XHR) 的概念,也就是說 client 刷新不再以整個 view 為單位,可以只針對需要的資料取得相關的 XML 即可。整個標準化的過程一直到 2002/6 Mozilla Gecko 發佈 1.0 版、2004/2 Safari 1.2 發佈之後才真正落實。

2004-2005 這兩年迎來巨變。

Gmail 跟 Google Maps 相繼於 2004/4 與 2005/2 發表,大家也發現 XMLHttpRequest 可以搭配 client-side JavaScript asynchronous 的行為,讓 JavaScript 負責處理 XMLHttpResponse 就好,不但可以刷新部份頁面,也可以部在一開始就載入完整頁面,當使用者觸發後再進一步抓資料,且不影響既有畫面瀏覽。AJAX 正式登場,並作為 2004/11 Firefox 1.0 發表時的主打特點。

某程度上來說 AJAX 算是解決了部份 data binding 的問題,但主動權仍落在 client 端,並不是真的由 controller 更新 view,屬於單向 (one-way) data binding。即便如此,AJAX 也讓更多人把目光從 data binding 轉移到另一個重點,model 的複用,衍生 component-based MVC frameworks。

對於初入前端的人來說,最棘手的就是各瀏覽器相容性問題,一種解法是使用 cross-browser JavaScript UI libraries,另一種就是交由 MVC frameworks 產出 cross-browser UI components。Component-based MVC 就是後者的產物。

這邊的 component 指的是 UI component;例如一個下拉選單,或 textbox;通常會帶一些資料跟檢核機制。相對於 MVC,component 比較像是 model 在 view 中的呈現,卻又帶一點 controller 對資料檢核的邏輯,但其呈現的不是一整的 page 而是各別 model。使用 component-based MVC 有個很大的好處,前面說的 one-way data binding 都在 framework 中處理好了,不需要額外程式碼,開發人員只要專注如何組裝、複用這些 UI components 即可。

但也因為 components 大大削弱了原本 MVC Model2 中 controller 的角色,處理 request 與更新 view;因此這類 component-based MVC framework 標榜的 architectural pattern 往往不是 MVC Model2,而是 MVP 或 MVVM 等。

ASP.NET 中的 code behind 算不算是 controller?
先回頭看看 controller 的 3 項主要職責:(1) 處理 request;(2) 根據執行結果選擇不同的 view;(3) 更新 view 中 model 的值。從功能性來看,code behind 完全能達到上面三者的要求,那究竟怪在哪裡?

首先 code behind class 裡頭的 member fields 多半宣告在 ASPX/ASPC 中,而 ASPX/ASPC 又只能綁定一個 code behind class,導致兩者 1:1 的強耦合關係,微軟更因此給了個很妙的名稱,page controller;這樣的模糊命名讓原本寫 ASP.NET Web Forms 的開發人員,瞬間也可以宣稱自己在寫 MVC;只是沒有以 ASP.NET MVC 為基礎,真正的 ASP.NET MVC 用的是 front controller pattern。

除了耦合性議題,另一個 page controller 讓人覺得詭異的地方是:在針對執行結果導向對應的 view 時,page controller 往往使用 HttpRequest.Redirect,依賴瀏覽器處理 HTTP redirect 行為,而不是走 server-side internal routing。

所以這個問題若使用微軟模糊的回答,答案是 YES;但面對多數的 MVC framework 是完全對應不起來。

Salesforce VisualPage 的 controller
相對於 ASP.NET ASPX/ASPC 與 code behind 的結構,VisualPage 也有個類似 code behind 的 class,稱為 controller。比 code behind 稍微好一點,VisualPage 所有欄位的宣告被放在 controller,換言之,controller 可脫離 VisualPage 獨立測試。此外在預載欄位資料上,ASP.NET 透過 code behind 塞入資料,而 VisualPage 的 controller 只提供 getter,把塞資料的主動權交給 view 本身,跟 AJAX data binding 的作法一致。

至於連動選單的更新,VisualPage 的作法就完全按照 AJAX data binding,不需要重載整個頁面,由瀏覽器送出 XMLHttpRequest 後刷新 UI component 完成。

See also:
[1] XMLHttpRequest (wikipedia; Mozilla.org)
[2] AJAX Programming (wikipedia)
[3] Ajax: A New Approach to Web Applications
[4] GUI Architectures (Martin Fowler)
[5] MVC是一個巨大誤會
[6] MVVM (wikipedia)
[7] MVP (wikipedia)
[8] Webforms vs MVC and Why MVC is better?
[9] MVC-structured Visualforce Example