2025年8月6日 星期三

分享用 Git 管理 OneDrive 的方法 (OneDrive + Git + mklink) - Windows

這裡分享一下我對 OneDrive 用 Git 配合 Windows 的 mklink 指令做版本管理的方法

環境:

  1. 我的電腦系統是 Windows11。
  2. 電腦上有登入 Microsoft 帳號的 OneDrive 同步資料夾。
  3. OneDrive 上有程式碼,且是多人共用。

雖然我個人是不太喜歡把程式放在 OneDrive 中,因為 OneDrive 沒有像 Git 一樣的管控概念,比較像是注重同步檔案功能的工具而已,雖然 OneDrive 可以去看檔案的各個版本及修改時間,但是沒有辦法像 Git 一樣很方便地看到哪一批檔案在哪個時間、被誰修改、修改了哪幾行。

OneDrive 也沒有辦法像 Git 一樣先對檔案進行修改不要同步,等修改確定後再同步,也沒辦法開 Branch 做多 feature 開發管理。

不過因為公司有特別需求 (比如檔案使用者有非RD人員不會用 Git、檔案很少修改、程式內容不多之類的) 所以在這部份採用了 OneDrive,為了我自己能夠較好的管理對 OneDrive 裡各 feature 需求的版本控管,我開始想方法用 Git 來對 OneDrive 進行管理。

我的需求是:

  1. 希望能針對不同的 feature 開發建立 branch 來控管並開發,但各 feature branch 在開發時能不修改到 OneDrive 的檔案,希望等到開發完後才將 branch 的修改 merge 至 OneDrive 中的檔案。
  2. 不希望新增目前沒有在 OneDrive 中的不必要檔案,例如 Git 的 .git, .gitnore 等檔案。
  3. OneDrive 中的檔案變動 (例如可能別人修改了檔案) 能夠即時的反映在 Git repository 中,方便我知道別人修改了哪些檔案,在 feature branch merge 時能夠被檔下來得到提醒告知之類的,也要有能處理 conflict 的能力。

最後這是我想到的,利用了 Windows mklink 指令來把 Git repository directory 跟 OneDrive 資料夾同步,並配合 Git worktree 做 feature branch 版控的方法,特此分享:

假設 OneDrive 資料夾位置在

C:\Users\<userName>\xxx-onedrive-folder

先建立資料夾,例如: D:\MyOneDriveRepository-masterBranch
用 git init 把資料夾設定成 git repository,假設一開始的 branch 叫做 master。

然後執行以下指令 (/J 代表 Directory Junction):

mklink /J C:\Users\<userName>\xxx-onedrive-folder D:\MyOneDriveRepository-masterBranch\repoLinkToOneDrive

這樣就會得到一個被建立起來的資料夾:
D:\MyOneDriveRepository-masterBranch\repoLinkToOneDrive

並且 C:\Users\<userName>\xxx-onedrive-folder 和

D:\MyOneDriveRepository-masterBranch\repoLinkToOneDrive
會連結起來成為同步狀態

把 D:\MyOneDriveRepository-masterBranch\repoLinkToOneDrive 連同裡面的檔案都進行第一次的
git commit 就可以開始進行 Git 版本控管了。

因為 OneDrive 可能會跟別人一起合作共用,所以如果在開發新 feature 前我們不希望對 OneDrive 裡的檔案做修改,
也就是說記住不要切換

D:\MyOneDriveRepository-masterBranch

的 branch,讓它永遠在 master branch。

如果有要開發新 feature,我們可以用 git branch <feature branch> 建立新 branch (不要切換過去),例如新 branch 叫 featureBranch,

利用 git worktree 的方式在另外一個資料夾 checkout feature1 去開發,
例如開發路徑是 D:\MyOneDriveRepository-otherBranch\repoLinkToOneDrive,
可以執行

git worktree add D:\MyOneDriveRepository-otherBranch featureBranch

這樣就會得到一個被建立的資料夾:

D:\MyOneDriveRepository-otherBranch

我們就可以在裡面開發 feature1 branch 的程式了。

最後等 feature 開發完後,可以再回到

D:\MyOneDriveRepository-masterBranch

用 git merge featureBranch --no-ff

來把 featureBranch merge 至 master branch 來改變 OneDrive 的檔案。

這樣的作法有幾點好處:

  1. 如果有別人修改了 OneDrive 中的檔案,因為有 Git 管理的關係,我們也可以很容易的發現,例如 git merge 時會因為改到同一個檔案而被擋下來。
  2. 可以把別人的修改 commit 至 master branch 做記錄,雖然不能容易地知道哪幾行是何時被誰修改的有點可惜 (還要特別去 OneDrive 網頁裡查 log 有點太麻煩了 )。
  3. git 生成出來的 .git 檔案不會被上傳到 OneDrive 上,因為我們是對跟 OneDrive 做 Directory Junction 的資料夾的外層資料夾做 git init,所以 .git 並不在跟 OneDrive 做 Directory Junction 的資料夾之中。

2025年8月5日 星期二

Python 學習紀錄

-------------------------- Python 本身相關 -----------------------------------------------------

#Python 的安裝位置可以自行決定,通常預設在
C:\Users\<userName>\AppData\Local\Programs\Python\Python313

#在環境中安裝套件 (預設是全局安裝,如果虛擬環境被啟動就會安裝在虛擬環境)
pip install <package名>

# sys.path 是一個列表,Python 在查找 module 時會從這列表中去找。
# 當用 python ./xxx/yyy.py 時,./xxx 會被加到 sys.path 中。
# 當用 python -m xxx.yyy , . (執行命令時所在的路徑)會被加到 sys.path 中

# -m 參數,把 module 當 script 來執行,後面可接 module name,會把當下執行路徑加到 sys.path 中
#例如:當下在 D:\\my-python,然後要執行 D:\\my-python\\module\\module1.py,命令為
python -m module.module1 (或是 python -m module.module1.py)
#然後因為此時 D:\\my-python 會被加到 sys.path 的關係,在 module1.py 中,就可以直接以 D:\\my-python 為起始目錄來 import 其他 module 了

#為一個專案建立虛擬環境 (virtual environment),會建立一個<虛擬環境名稱>資料夾,其中包括虛擬環境要用的東西,
並且會把<虛擬環境名稱>的全域路徑加到 sys.path 變數中 (因為是全域路徑,所以我想專案移動檔案位置可能會有問題)
pyton -m venv <虛擬環境名稱>
#例:
python -m venv .venv

#啟動虛擬環境 (有些 IDE 可能可以自動識別 .venv,就不用特別下指令啟動虛擬環境)
source .venv/bin/activate

#輸出當前環境安裝 (全域或是虛擬環境中) 的所有套件及其版本到<requirements 套件 list file path>
pip freeze > <requirements 套件 list file path>
#例:
pip freeze > requirements.txt
#讀取<requirements 套件 list file path> 並在環境中安裝套件
pip install <requirements 套件 list file path>
#例:
pip install -r requirements.txt

#官方的套件依賴配置文件,可取代 requirements.txt
pyproject.toml

#依照 pyproject.toml 安裝套件依賴
#參數: -e: 不要把專案原碼放到 .venv/Lib/site-packages 資料夾中
pip install [-e] .

-------------------------- pipx 工具相關 -----------------------------------------------------
官網

pipx — Install and Run Python Applications in Isolated Environments

pipx 是一個可以安裝及管理 Python 工具的工具,它可將要安裝的 Python 工具安裝到不是全域路徑的位置,方便在不汙染全域環境下較好管理工具。

例如 tool_1 依賴 xxx_package v1 ,但 tool_2 依賴 xxx_package v2,如果都裝在全域下可能就會有衝突問題。
工具安裝位置會像這樣:
不安裝到
C:\Users\<userName>\AppData\Local\Programs\Python\Python313\Lib
改安裝到
C:\Users\<user name>\.local\bin

#安裝 pipx 工具
pip intall [--python <python versoin>] pipx

#用 pipx 安裝並管理其他工具
pipx intall <其他工具>
#例如安裝 uv
pipx intall uv

#例出安裝的所有工具(包括各個的 Python 版本)
pipx list

--------------------------- uv 工具相關 ----------------------------------------------------

官網

uv - An extremely fast Python package and project manager, written in Rust.

uv 是一個可以幫助我們管理專案的套件依賴的工具,隔離各專案避免套件依賴裝在全域造成互相衝突。

#安裝 uv 工具
pip intall uv

#例出管理的 Python
uv python list

#安裝移除特定版本的 Python
uv python install <版本號>
uv python uninstall <版本號>

#設定 uv 預設使用的 Python 版本
uv python pin <版本號>

#初始化專案資料夾
uv init

#建立並使用 venv 虛擬環境,等同 python -m venv .venv 加 source .venv/bin/activate
uv venv

#安裝套件,並修改 pyproject.toml
uv add <套件名>

#移除套件,並修改 pyproject.toml
uv remove <套件名>

#讀取 pyproject.toml、建立虛擬環境 (只有建立沒有啟動)、並安裝依賴
uv sync

#在虛擬環境上下文 (Context) 中執行 Python 檔 (啟動虛擬環境、執行 Python 檔、再退出虛擬環境)
uv run <要執行的 python 檔>

#要使用 -m 參數時,例如要執行 python -m xx.yy.zz.py 時可用
uv run python -m xx.yy.zz

uv build

--------------------------  -----------------------------------------------------



2025年6月23日 星期一

關於 Unicode等價性 (Unicode Normalization) - Java - 如何將被拆分的韓文字母正確合併 - Normalizer - NFD, NFC, NFKD, NFKC

因為工作上碰到在儲存使用者輸入的韓文時,得到的是拆分的子音母音而非完整的字,
例如:패스워드 變成 ᄑ ᅢ ᄉ ᅳ ᄋ ᅯ ᄃ ᅳ ,
所以在這裡紀錄一下關於 Java 是如何處理 Unicode等價性 (Unicode Normalization) 的。

先貼上 Wiki 的 Unicode等價性 (Unicode Normalization)  說明描述:

Unicode等價性(英語:Unicode equivalence)是為和許多現存的標準能夠相容,Unicode(統一碼)包含了許多特殊字元。在這些字元中,有些在功能上會和其它字元或字元序列等價。因此,Unicode將一些碼位序列定義成相等的。Unicode提供了兩種等價概念:標準等價和相容等價。前者是後者的一個子集。例如,字元n後接著組合字元~標準等價和相容等價於Unicode字元ñ。而合字ff則只有相容等價於兩個f字元。

Unicode正規化(英語:Unicode normalization)是文字正規化的一種形式,是指將彼此等價的序列轉成同一列序。此序列在Unicode標準中稱作正規形式。對於每種等價概念,Unicode又定義兩種形式,一種是完全合成的,一種是完全分解的。因此,最後會有四種形式,其縮寫分別為:NFC、NFD、NFKC、NFKD。對於Unicode的文字處理程式而言,正規化是很重要的。因為它影響了比較、搜尋和排序的意義。

簡單地說,就是 Unicode等價性定義了有些字元 (或一組字元) 在意義或視覺上可以等價於另一個字元 (或一組字元)。

而 Unicode 正規化 就是字元之前的轉換方式,又分為

  1. NFC (Normalization Form Canonical Composition) :以標準等價方式來分解,然後以標準等價重組之。若是singleton的話,重組結果有可能和分解前不同。
  2. NFD (Normalization Form Canonical Decomposition):以標準等價方式來分解。
  3. NFKC (Normalization Form Compatibility Composition):以相容等價方式來分解。
  4. NFKD (Normalization Form Compatibility Decomposition):以相容等價方式來分解,然後以標準等價重組之。

相容等價標準較寬鬆,函蓋範圍比標準等價大。

例如:

  1. 韓文的字可由母音子音組合而成:ᄑ + ᅢ =  패
  2. 中文有些部首可以跟某些字等價:對 部首的  "⽅" (\u2F45) 做 NFKC 會得到 一般的字 "方" (\u65B9)
  3. 日文的一些複合字:對 "㍿" 做 NFKC 會得到 "株式会社"
  4. 一些符號:對 ㊋ 做 NFKC 會得到 一般的字 "火"
  5. 上標和下標:對 A² 中的 "上標的2" (\u00B2) 做 NFKC 會得到 一般的數字 "2"

在 Java 中可以使用 
Normalizer.normalize("要處理的文字", Normalizer.Form.NFC (或 NFD, NFKC, NFKD));
來處理,下面直接用程式碼示範。

package test;

import java.io.IOException;
import java.text.Normalizer;

public class UnicodeNormalizationTest {

	public static void main(String[] args) throws IOException {
		String str1 = "패스워드";
		String str2 = "패스워드";
		
		System.out.println(str1 + ", " + str2);
		System.out.println(Normalizer.normalize(str1, Normalizer.Form.NFC).equals(str2)); // true
		System.out.println(Normalizer.normalize(str1, Normalizer.Form.NFKC).equals(str2)); // true
		
		/////////////////////
		str1 = "⽅"; // \u2f45
		str2 = "方"; // \u65b9
		
		System.out.println(str1 + ", " + str2);
		System.out.println(Normalizer.normalize(str1, Normalizer.Form.NFC).equals(str2)); // false
		System.out.println(Normalizer.normalize(str1, Normalizer.Form.NFKC).equals(str2)); // true
		
		/////////////////////
		str1 = "㍿"; // \u337f
		str2 = "株式会社";
		
		System.out.println(str1 + ", " + str2);
		System.out.println(Normalizer.normalize(str1, Normalizer.Form.NFC).equals(str2)); // false
		System.out.println(Normalizer.normalize(str1, Normalizer.Form.NFKC).equals(str2)); // true
		
		/////////////////////
		str1 = "²"; // \u00b2
		str2 = "2";
		
		System.out.println(str1 + ", " + str2);
		System.out.println(Normalizer.normalize(str1, Normalizer.Form.NFC).equals(str2)); // false
		System.out.println(Normalizer.normalize(str1, Normalizer.Form.NFKC).equals(str2)); // true
		/////////////////////
		str1 = "㊋"; // \u328b
		str2 = "火";
		
		System.out.println(str1 + ", " + str2);
		System.out.println(Normalizer.normalize(str1, Normalizer.Form.NFC).equals(str2)); // false
		System.out.println(Normalizer.normalize(str1, Normalizer.Form.NFKC).equals(str2)); // true
	}
}

參考資料:

  1. Unicode等價性 - 維基百科,自由的百科全書
  2. Unicode Normalization 文字標準化 | Sean's Note
  3. 从⽅不是方到Unicode正规化NFD, NFC, NFKD, NFKC | 小不的笔记 | 时间之外的往事

2025年5月11日 星期日

Outlook Mail 的製作紀錄 - MSO conditional comments 及 VML (Vector Markup Language)

在製作 Outlook HTML Mail 時,PC 版 Outlook 有著許多的限制,
像是有限的 CSS 支援 (例如不支援 border-radius,要以 inline-css 的方式)、
最好的排版方式是使用 <table> 等,
所以在設計版面上面跟普通的 html page 有著蠻大的不同。

今天想要紀錄的是如何製作一般 Web Mail (如 Gmail、Web 版 Outlook 等) 及 PC 版 Outlook 都可正常顯示的 "在圓角長方形上面的一個有帶連結的圓角長方形按鈕"。

成果圖會像這樣 (還是有些許差異,但大體上設計沒有跑掉):

如果是普通的 Web Mail,HTML 的部份可以這樣設計:

<div style="border-radius: 10px; background-color: #f4f4f4; padding-top: 20px; padding-bottom: 20px; padding-right: 20px; padding-left: 20px;">
	<a href="https://xxx.xxx.xxx" style="background-color: #000000; color: #ffffff; border-radius: 10px; padding-top:10px; padding-bottom:10px; padding-right:20px; padding-left:20px; font-size: 16px; text-decoration: none;">
		Click This Button
	</a>
 </div>

在 Gmail 的樣子如下(Web Outlook 也差不多)

但是 PC Outlook 沒辦法正常處理 border-raidus, padding 等問題,會長得像這樣:

所以需要對 PC Outlook 做特別處理。

這裡我們使用了兩個知識點來處理,分別是用來分辨裝置是否為 PC Outlook 的 MSO conditional comments 和用來繪置圖形的 VML (Vector Markup Language) 。

MSO conditional comments 可以寫在 HTML 裡,可以用來判斷裝置是否為 PC Outlook,
寫法如下,對一般網頁瀏覽器來說,專鬥給 PC Outlook 看的部份因為被 <!-- 和 --> 包圍所以會被視為注解,而給一般網頁瀏覽器看的部份則不會在 PC Outlook 上顯示。
這樣我們就可以區分 PC Outlook 和一般網頁瀏覽器了。

<!--[if mso]>
給 PC Outlook 看的 Content,PC Outlook 會顯示,而一般網頁瀏覽器會視其為注解,因為被 <!-- 和 --> 包圍
<![endif]-->

<!--[if !mso]><!-- -->
給一般網頁瀏覽器看的 Content,PC Outlook 不會顯示
<!--<![endif]-->

再來是 VML (Vector Markup Language) ,雖然 PC Outlook 可以顯示 HTML,但它使用的其實是 Word 的 Render Engine,而其可以使用 VML (Vector Markup Language) 來繪製圖形。
所以我們這邊可以使用 VML 取代 HTML 來繪製我們要的圓角連結按鈕,更詳細的 VML 圖形繪制語法可以參考 使用預先定義的圖形 - Win32 apps | Microsoft Learn

在這裡我使用了 <v:roundrect> 來繪製圓角長方形。
第一個 <v:roundrect> 設定如下:

  1. 用 strokecolor 和 fillcolor 設定邊框及底色為灰色 (#f4f4f4) 。
  2. 用 arcsize 設定圓角半徑。
  3. 用 style 設定 width 和 height。
  4. 用 style 設定 position: absoulte 用來等下將下一個 <v:roundrect> 疊在上面顯示。
第二個 <v:roundrect> 設定如下:

    1. 用 strokecolor 和 fillcolor 設定邊框及底色為黑色 (#000000) 。
    2. 用 arcsize 設定圓角半徑。
    3. 用 style 設定 width 和 height。
    4. 用 style 設定 position: absoulte, 配合 top 和 left 設定擺放位置,這樣就可以將這個 <v:roundrect> 放置在第一個 <v:roundrect> 中央。
    5. 用 href 設定連結。
    6. 裡面可以放置一個 <center> 來顯示按鈕文字。
    要注意的是 <v:roundrect> 內部不能再放一個 <v:roundrect> ,因為 <v:roundrect> 不支援嵌套。

    最後的語法結果如下:
    <!--[if mso]>
     <v:roundrect
    	xmlns:v="urn:schemas-microsoft-com:vml"
    	xmlns:w="urn:schemas-microsoft-com:office:word"
    	style="v-text-anchor:middle;position:absolute;height:64px;width:520px;"
    	arcsize="10%"
    	strokecolor="#f4f4f4"
    	fillcolor="#f4f4f4">
     </v:roundrect>
     <v:roundrect
    		xmlns:v="urn:schemas-microsoft-com:vml"
    		xmlns:w="urn:schemas-microsoft-com:office:word"
    		href="https://xxx.xxx.xxx"
    		style="v-text-anchor:middle;position:absolute;top:12;left:180;height:40px;width:160px;"
    		arcsize="10%"
    		strokecolor="#000000"
    		fillcolor="#000000">
    		<w:anchorlock/>
    		<center style="color:#ffffff;font-family:sans-serif;font-size:16px;">
    		  Click This Button
    		</center>
      </v:roundrect>
    <![endif]-->
    
    <!--[if !mso]><!-- -->
     <div style="border-radius: 10px; background-color: #f4f4f4; padding-top: 20px; padding-bottom: 20px; padding-right: 20px; padding-left: 20px;">
    	<a href="https://xxx.xxx.xxx" style="background-color: #000000; color: #ffffff; border-radius: 10px; border-top:10px solid #000000; border-bottom:10px solid #000000; border-right:20px solid #000000; border-left:20px solid #000000; font-size: 16px; text-decoration: none;">
    		Click This Button
    	</a>
     </div>
    <!--<![endif]-->
    

    參考資料:

    1. 向量標記語言 (VML) - Win32 apps | Microsoft Learn
    2. 使用預先定義的圖形 - Win32 apps | Microsoft Learn
    3. 從零開始建立一個 Email HTML 版型 | 電子豹部落格

    2025年4月15日 星期二

    Java Axis 2 client 端呼叫 SOAP API (需要 Basic Authentication 時的 Bug 應對方式)

    最近在工作上接到一個升級 client 端 Apache Axis 版本的任務,
    碰到了一些問題及解決方法,在這邊紀錄一下怕之後忘記可以拿來參考。

    Apache Axis 目前有兩個大版本,v1 含有一些漏洞與弱點 (CVE, Common Vulnerabilities and Exposures),所以建議升級改用 v2 版本的 Apache Axis 2

    因為工作關係只用到 client 端,所以在這篇文章裡只涉及呼叫 SOAP API 的 client  部份,沒有 server 端的部份。

    在升級時我碰到的最大問題是無法正常設定 Basic Authentication header,
    因為我要呼叫的 SOAP Server API 需要 Basic Authentication header 驗證,
    也就是要在 Http Header 上加入如下的 Header 做驗證,把 username 和 password 用冒號 (":") 接起來以後對其做 Base64 encode,再在前端加上 "Basic ",
    將其將做 Value 搭配 "Authorization" 做 Key 設定到 Header 上 :

    Authorization: Basic base64Encode(<username>:<password>)

    根據官網這裡 Basic, Digest and NTLM Authentication 有寫到正常設定 Baisc Authentication 的程式寫法,
    但也提到了目前版本有一個存在的問題導致正常寫法的方式不起作用,
    其中寫到:

    Note: Basic preemptive authentication requires a work around described in https://issues.apache.org/jira/browse/AXIS2-6055 until a proper fix is contributed by the community as we lack committers who use it.

    其中給了一個連結: https://issues.apache.org/jira/browse/AXIS2-6055,
    在連結中有提到問題及 work around 的暫時解法,
    在這篇文中我們會參考在上面提到的解法。

    下面直接上程式,相關說明也都寫在註解中:

    首先是使用的 Maven Dependency (${axis2.version} 我是設定 2.0.0 版)

    <!-- https://mvnrepository.com/artifact/org.apache.axis2/axis2-kernel -->
    	<dependency>
    	    <groupId>org.apache.axis2</groupId>
    	    <artifactId>axis2-kernel</artifactId>
    	    <version>${axis2.version}</version>
    	</dependency>
    	
    	<!-- https://mvnrepository.com/artifact/org.apache.axis2/axis2-adb -->
    	<dependency>
    	    <groupId>org.apache.axis2</groupId>
    	    <artifactId>axis2-adb</artifactId>
    	    <version>${axis2.version}</version>
    	</dependency>
    	
    	<!-- https://mvnrepository.com/artifact/org.apache.axis2/axis2-transport-http -->
    	<dependency>
    	    <groupId>org.apache.axis2</groupId>
    	    <artifactId>axis2-transport-http</artifactId>
    	    <version>${axis2.version}</version>
    	</dependency>
    	
    	<!-- https://mvnrepository.com/artifact/org.apache.axis2/axis2-transport-local -->
    	<dependency>
    	    <groupId>org.apache.axis2</groupId>
    	    <artifactId>axis2-transport-local</artifactId>
    	    <version>${axis2.version}</version>
    	</dependency>
    	
    	<!-- https://mvnrepository.com/artifact/org.apache.axis2/axis2-jaxws -->
    	<dependency>
    	    <groupId>org.apache.axis2</groupId>
    	    <artifactId>axis2-jaxws</artifactId>
    	    <version>${axis2.version}</version>
    	</dependency>

    再來是程式碼:

    package test.soap;
    
    import java.nio.charset.StandardCharsets;
    import java.rmi.RemoteException;
    import java.util.ArrayList;
    import java.util.Base64;
    import java.util.List;
    
    import org.apache.axiom.om.OMAbstractFactory;
    import org.apache.axiom.om.OMElement;
    import org.apache.axiom.om.OMFactory;
    import org.apache.axiom.om.OMNamespace;
    import org.apache.axis2.addressing.EndpointReference;
    import org.apache.axis2.client.Options;
    import org.apache.axis2.client.ServiceClient;
    import org.apache.axis2.context.NamedValue;
    import org.apache.axis2.kernel.http.HTTPConstants;
    
    public class SoapAxis2Test {
    	
    	public static void main(String[] args) throws RemoteException {
    		String username = "username";
    		String password = "password";
    		String namespace = "http://xxx.xxx/";
    		
    		// SOAP API 名稱 (operation)
    		String operationName = "operationName"; 
    		//依需要設定 SOAP Action,有些 Server 端需要有些不需要,
    	    //可自行參考 WSDL 文檔 API 的 soapAction 節點部份
    		String action = namespace + operationName;
    		
    		//對應的 WSDL 文檔位址會是 https://xxx.xxx.xxx?wsdl
    		EndpointReference targetEPR = new EndpointReference("https://xxx.xxx.xxx");
    	    Options options = new Options();
    	    options.setTo(targetEPR);
          
    	    //雖然官網說可以用以下寫法設置 Basic Authorization header,但目前實際上不行,
    	    //問題似乎是出在 preemptive authentication 無法正常設定
    //	    Authenticator auth = new Authenticator();
    //      auth.setUsername(username);
    //      auth.setPassword(password);
    //      auth.setPreemptiveAuthentication(true);
    //      auth.setAuthSchemes(List.of(Authenticator.BASIC));
    //      options.setProperty(HTTPConstants.AUTHENTICATE, auth);
    	    
    	    //根據官網這裡 (https://axis.apache.org/axis2/java/core/docs/http-transport.html#Basic.2C_Digest_and_NTLM_Authentication)
    	    //有寫到目前存在的問題
    	    //Note: Basic preemptive authentication requires a work around described in https://issues.apache.org/jira/browse/AXIS2-6055 until a proper fix is contributed by the community as we lack committers who use it.
    	    //其中給了一個連結: https://issues.apache.org/jira/browse/AXIS2-6055
    	    //在連結中有提到問題及 work around 的暫時解法,其中一個方法如下,
    	    //成功設置後,應可在 http request 上正確地設定 Basic Authorization header
    	    List<NamedValue> authHeaders = new ArrayList<>();
    	    String basicAuth = username + ":" + password;
    	    authHeaders.add(new NamedValue(HTTPConstants.HEADER_AUTHORIZATION, "Basic " + new String(Base64.getEncoder().encode(basicAuth.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8)));
    	    options.setProperty(HTTPConstants.HTTP_HEADERS, authHeaders);
    	    //依需要設定 SOAP Action
    	    //options.setAction(action);
    	    
    	    ServiceClient client = new ServiceClient();
            client.setOptions(options);
            
    	    OMFactory factory = OMAbstractFactory.getOMFactory();
    	    
    	    //依要呼叫的 SOAP API 自行建構 OMElement,供產生 xml 放在 http request body 中
    	    //自行設定 SOAP API 的名稱 (operation)
            OMNamespace omNamespace = factory.createOMNamespace(namespace, "");
            OMElement operationElement = factory.createOMElement(operationName, omNamespace);
    
            //自行設定 SOAP API 所需的參數 (parameter)
            OMElement param1Element = factory.createOMElement("xxParameter1", omNamespace);
            param1Element.setText("xxx");
            operationElement.addChild(param1Element);
            
            OMElement param2Element = factory.createOMElement("xxParameter2", omNamespace);
            param2Element.setText("xxx");
            operationElement.addChild(param2Element);
            
            //Blocking invocation
            OMElement result = client.sendReceive(operationElement);
            //自行解析 SOAP API 回傳的結果
            String resultStr = result.getFirstElement().getText();
    	    
    	    System.out.println(resultStr);
    	}
    }
    

    參考資料:

    1. Migrating from Apache Axis 1.x to Axis2
    2. HTTP transports – Apache Axis2 - Basic, Digest and NTLM Authentication
    3. Basic Auth credentials are missing in request
    4. Axis2 服务器未能识别 HTTP 头 SOAPAction 的值 的解决办法
    5. axis2客户端调用的三种方式-CSDN博客

    2025年3月31日 星期一

    DAO Sql 得到 insert 主鍵的方法,包括一般 JDBC 和 Spring JDBC Template

    一般 JDBC:

    int id = 0;
    String sql = "INSERT INTO ...........";
    
    Connection con = null;
    PreparedStatement pstmt = null;
    ResultSet rs = null;
    
    try{
    	con = DriverManager.getConnection(Constant.DB_MAIN);
    	pstmt = con.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);
    	pstmt.executeUpdate();
    
    	ResultSet generatedKeys = pstmt.getGeneratedKeys();
    	if (generatedKeys.next()){
    		id = generatedKeys.getInt(1);
    	}
    }catch (Exception e) {
    	e.printStackTrace();
    } finally {
    	ConControl.freeConnection(rs, pstmt, con);
    }
    

    Spring JDBC Template:

    public int insertPopularFaq(PopularFaqBean popularFaq) {
    		String sql = "DECLARE @popular_faq_rule_id INT "
    				   + "DECLARE @faq_id INT "
    				   + "SET @popular_faq_rule_id = ? "
    				   + "SET @faq_id = ? "
    				   + "INSERT INTO popular_faq(popular_faq_rule_id, faq_id) VALUES(@popular_faq_rule_id, @faq_id)";
    		
    		KeyHolder keyHolder = new GeneratedKeyHolder();
    		cs_JdbcTemplate.update((Connection con) -> {
    			PreparedStatement pstmt = con.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
    			int i = 1;
    			pstmt.setInt(i++, popularFaq.getPopularFaqRuleId());
    			pstmt.setInt(i++, popularFaq.getFaqId());
    			
    			return pstmt;
    		}, keyHolder);
            
            Number key = keyHolder.getKey();
    		
    		return key.intValue();
    	}
    

    2025年1月7日 星期二

    使用 MS SQL CTE (Common Table Expression) 的遞迴功能

    MS Sql Server 有提供 CTE (Common Table Expression),它可以讓我們用 WITH 語法來建立暫時的 Table。

    例如:

    WITH numRange(num) AS (
    	SELECT 1
    )
    SELECT *
    FROM numRange
    

    查詢結果是:

    1

    而因為 CTE 可以在 AS () 中參考自已本身,
    所以可以達到遞迴的效果,今天就是要來紀錄一下使用的方法。

    在 AS () 中,會由兩種成員組成,分別是放在前面的錨點成員 (anchor) 和放在後面的遞迴成員 (recursive),錨點成員是用來設定初始的 Table 內容,不可參考到 CTE 本身的 Table。

    而遞迴成員會參考到 CTE Table 本身 (準確來說是參考到上一次迭代得到的資料表)。

    錨點成員跟遞迴成員必須要用 UNION ALL 連接。

    在每一次的遞迴迭代中,遞迴成員會參考到上一次得到的資料表,而做完 SELECT 後會產生出一份屬於這次迭代的資料表給下一次迭代的遞迴成員使用,迭代會一直進行下去直到超過次數上限 (可以用 OPTION (MAXRECURSION X) 來設定上限,X是次數,設 0 代表無限) 或是產生不出資料表為止 (就是 SELECT 的結果為空集合)。

    當所有迭代執行結束後,它會把所有產生出的資料表結合起來得到最後的結果資料表。

    下面舉個例子:

    WITH numRange(num) AS (
    	SELECT 1
    
    	UNION ALL
    
    	SELECT num + 1
    	FROM numRange
    	WHERE (num + 1) <= 3
    )
    SELECT *
    FROM numRange
    

    查詢結果是:

    1
    2
    3

    一開始初始第1次迭代的資料表是:

    1

    第2次迭代產生的資料表是

    2

    第3次迭代產生的資料表是

    3

    而第4次因為 SQL 語句 (此時 num 參考上一次、也就是第三次迭代的 num 是 3)

    SELECT num + 1
    	FROM numRange
    	WHERE (num + 1) <= 3
    

    得到的結果集為空,所以迭代結束。

    接著下面來看另一個範例,
    我們有一張表叫做 family,欄位是 name, father, mother,分別代表家族成員的名字、父親名字、母親名字,假設名字都不會重覆,所以名字可以當做 id 主鍵來做 JOIN。

    表的內容如下:

    name mother father
    小孩1 媽媽 爸爸
    小孩2 媽媽 爸爸
    媽媽1 外婆 外公
    爸爸 祖父 祖母
    祖父 NULL NULL
    祖母 NULL NULL
    外公 NULL NULL
    外婆 NULL NULL

    現在我們想找出特定人的所有長輩及其的父母、還有長輩和特定人的輩份距離,就可以像下面這樣查詢,範例是要找出"小孩1"的長輩及其父母和長輩跟"小孩1"的輩份關係,相關的說明也都已寫在範例的注釋中:

    -- 建立資料表
    CREATE TABLE family(
       name NVARCHAR(100),
       mother NVARCHAR(100),
       father NVARCHAR(100)
    );
    
    -- 塞資料
    INSERT INTO family(name, mother, father)
    VALUES(N'小孩1', N'媽媽', N'爸爸'),
          (N'小孩2', N'媽媽', N'爸爸'),
    	  (N'媽媽', N'外婆', N'外公'),
    	  (N'爸爸', N'祖父', N'祖母'),
    	  (N'祖父', NULL, NULL),
    	  (N'祖母', NULL, NULL),
    	  (N'外公', NULL, NULL),
    	  (N'外婆', NULL, NULL);
    
    -- 進行查詢
    WITH elderRelation(elder, elder_mother, elder_father, level) AS (
       -- 這裡是錨點成員,建立一開始的初始資料集
       -- 例如此例是找出 "小孩1" 的父母及其父母和小孩與父母的輩份相差數
    
       -- 只要沒有參考到 elderRelation CTE 本身就是屬於錨點成員
       ---------- 錨點成員 - 開始 ----------
    
       -- 查詢"小孩1"的母親及其父母資料
       SELECT parent.name, parent.mother, parent.father, 1
       FROM family person INNER JOIN family parent ON person.mother = parent.name
       WHERE person.name = N'小孩1'
    
       UNION ALL
    
       -- 查詢"小孩1"的父親及其父母資料
       SELECT parent.name, parent.mother, parent.father, 1
       FROM family person INNER JOIN family parent ON person.father = parent.name
       WHERE person.name = N'小孩1'
    
       -- 上述只是展示錨點成員也可以自己視需要用 UNION 組合多個 Table,
       -- 上述範例也可以用一次查詢完成,例如:
       --SELECT parent.name, parent.mother, parent.father, 1
       --FROM family person INNER JOIN family parent ON person.mother = parent.name OR person.father = parent.name
       --WHERE person.name = N'小孩1'
    
       ---------- 錨點成員 - 結束 ----------
    
       -- 用 UNION ALL 與遞迴成員組合
       UNION ALL
    
       -- 下面因為開始參考到 elderRelation 這個 CTE 本身,所以是遞迴成員
       ---------- 遞迴成員 - 開始 ----------
       -- 找出 elderRelation 母親的父母人員資料 
       SELECT family.name, family.mother,  family.father, level + 1
       FROM elderRelation INNER JOIN family ON elderRelation.elder_mother = family.name
    
       UNION ALL
    
       -- 找出 elderRelation 父親的父母人員資料
       SELECT family.name, family.mother,  family.father, level + 1
       FROM elderRelation INNER JOIN family ON elderRelation.elder_father = family.name
    
       -- 上述只是展示遞迴成員也可以自己視需要用 UNION 組合多個 Table,
       -- 上述範例也可以用一次查詢完成,例如:
       --
       --SELECT family.name, family.mother,  family.father, level + 1
       --FROM elderRelation INNER JOIN family ON elderRelation.elder_mother = family.name OR elderRelation.elder_father = family.name
    
       ---------- 遞迴成員 - 結束 ----------
    )
    SELECT *
    FROM elderRelation
    OPTION (MAXRECURSION 0); -- 可以視需要設置遞迴的最大允許次數,0代表無限大
    

    查詢結果如下:

    elder elder_mother elder_father level
    媽媽 外婆 外公 1
    爸爸 祖父 祖母 1
    祖父 NULL NULL 2
    祖母 NULL NULL 2
    外婆 NULL NULL 2
    外公 NULL NULL 2

    參考資料:

    1. WITH common_table_expression (Transact-SQL)
    2. 利用 MAXRECURSION來突破CTE預設遞迴次數
    3. [SQL] 使用 CTE 遞迴查詢 (PostgreSQL / MSSQL)
    4. [SQL Server] CTE RECURSIVE (遞迴)製作月曆