2022年10月15日 星期六

避免在用 Maven 建置 war 檔時, minify-maven-plugin 最小化過的檔案在 package phase 被 maven-war-plugin 用 source code 蓋掉

當我們在使用 Maven 的 maven-war-plugin 建置 war 專案時常會發現,
如果有使用一些 javascript/css 最小化 (minify) 的 plugin,
例如 minify-maven-plugin,
minify-maven-plugin 會先執行把 js/css 檔做最小化,
然後 maven-war-plugin 會在 package phase 時從專案把未被最小化的 source code 放到
輸出資料夾中 (包括 war 檔),此時未被最小化的 source code 
會把己被 minify-maven-plugin 最小化的檔案給覆蓋掉,
造成最後輸出資料夾、war 檔裡的檔案沒有成功被最小化。

以下分享一個解決的方法,
可以使用 minify-maven-plugin 的 <webappTargetDir> 設定
把最小化過的檔案放在另一個資料夾下,
例如:   ${basedir}/target/minify (等同  ${project.build.directory}/minify) 資料夾中 ("minify" 資料夾名可自行決定),

<webappTargetDir>${project.build.directory}/minify</webappTargetDir>

然後再使用 maven-war-plugin 的 <webResource> 設定將 ${basedir}/target/minify 資料夾中的
最小化檔案放回至專案輸出資料夾中 (也包括 war 檔裡)

<webResource>
    <directory>${project.build.directory}/minify</directory>
</webResource>

如以下範例:

<plugin>
	<groupId>com.samaxes.maven</groupId>
	<artifactId>minify-maven-plugin</artifactId>
	<version>1.7.6</version>
	<executions>
	  <execution>
		<id>default-minify</id>
		<configuration>
		  <skipMerge>true</skipMerge>
		  <nosuffix>true</nosuffix>
		  <webappSourceDir>${basedir}/WebContent</webappSourceDir>
		  <webappTargetDir>${project.build.directory}/minify</webappTargetDir>
		  <cssSourceDir>./</cssSourceDir>
		  <cssSourceIncludes>
			<cssSourceInclude>**/*.css</cssSourceInclude>
		  </cssSourceIncludes>
		  <cssSourceExcludes>
			<cssSourceExclude>**/*.min.css</cssSourceExclude>
		  </cssSourceExcludes>			  
		  <jsSourceDir>./</jsSourceDir>
		  <jsSourceIncludes>
			<jsSourceInclude>**/*.js</jsSourceInclude>
		  </jsSourceIncludes>
		  <jsSourceExcludes>
			<jsSourceExclude>**/*.min.js</jsSourceExclude>
			<jsSourceExclude>**/node_modules/**/*.js</jsSourceExclude>
			<jsSourceExclude>**/webpack.config.js</jsSourceExclude>				
		  </jsSourceExcludes>
		  <jsEngine>CLOSURE</jsEngine>
		</configuration>
		<phase>prepare-package</phase>
		<goals>
		  <goal>minify</goal>
		</goals>
	  </execution>
	</executions>
</plugin>
<plugin>
	<artifactId>maven-compiler-plugin</artifactId>
	<version>3.8.0</version>
	<configuration>
		<rules>
			<requireJavaVersion>
				<version>11</version>
			</requireJavaVersion>
		</rules>
	  <source>11</source>
	  <target>11</target>        
	  <release>11</release>
	</configuration>
</plugin>
<plugin>
	<artifactId>maven-war-plugin</artifactId>
	<version>3.2.1</version>
	<configuration>
	  <warSourceDirectory>WebContent</warSourceDirectory>
	  <webResources>
		<webResource>
			<directory>${project.build.directory}/minify</directory>
		</webResource>			
	  </webResources>
	  <archive>
		<addMavenDescriptor>false</addMavenDescriptor>
	  </archive>		  
	</configuration>
</plugin>

參考:

  1. How to get maven to build a war with minified files using yuicompressor-maven-plugin
  2. yuicompressor maven plugin and maven-war-plugin
  3. Maven内置属性、POM属性 - Ruthless - 博客园
  4. Adding and Filtering External Web Resources
  5. Minification of JS and CSS Assets with Maven

2022年9月19日 星期一

用 Java 進行 SSH 連線 - 使用 JSch

 在這篇文中要展示如何用 Java 以 JSch 這個 library 來進行 SSH 連線,

首先是用 Maven 引入 library :

<!-- https://mvnrepository.com/artifact/com.jcraft/jsch -->
<dependency>
	<groupId>com.jcraft</groupId>
	<artifactId>jsch</artifactId>
	<version>0.1.55</version>
</dependency>
接著先直接上程式碼:
package test;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.Session;
import com.jcraft.jsch.SftpATTRS;
import com.jcraft.jsch.SftpException;

public class SftpTest {

	public static void main(String[] args) throws Exception {
		
		String privateKeyPath = "D:\\...\xx.pem"; //your private key file path
		String userName = "xxName"; //your username to access target server
		String host = "xxx.xxx.xxx.xxx"; //hostname of target server
		int port = 22; //target server port
		
		Session session = null;
		
		try (InputStream inputStreamOfPrivateKey = new FileInputStream(new File(privateKeyPath))){			
	        JSch jsch = new JSch();	        
	         
	        jsch.addIdentity(privateKeyPath, inputStreamOfPrivateKey.readAllBytes(), null, null);
	        session = jsch.getSession(userName, host, port);
            //session.setPassword("xxxPassword");
	        session.setConfig("StrictHostKeyChecking", "no");
	        session.setTimeout(60000);
	        session.connect();
			
	        ChannelSftp channelSftp = (ChannelSftp) session.openChannel("sftp");
	        channelSftp.connect();
	        
	        //check if file exists
	        System.out.println(isFileExist(channelSftp, "/xxx/xxx.png"));
	        
	        //check file attributes
	        SftpATTRS fileAttrs = channelSftp.lstat("/xxx/xxx.png");
	        System.out.println(fileAttrs.getSize());
	        
	        //create directory
	        mkdir(channelSftp, "/xxx/xxx");
	        
	        //upload file
	        upload(channelSftp, "D:\\xxx\\xxx.jpg", "/xxx/xxx");

	        channelSftp.exit();
		}catch(Exception e) {
			e.printStackTrace();
		} finally {
			if (session != null && session.isConnected()) {
				session.disconnect();
			}			
		}
		System.out.println("Done");
	}
	
	public static void upload(ChannelSftp channelSftp, String srcFilePath, String targetDirectoryPath) {
    	targetDirectoryPath = targetDirectoryPath.replace("\\", "/");
    	
    	File srcFile = new File(srcFilePath);
    	try (FileInputStream fileInputStream = new FileInputStream(srcFile)){
    		if (isFileExist(channelSftp, targetDirectoryPath)) {
        		channelSftp.cd(targetDirectoryPath);
        	}else {
        		channelSftp.cd("/"); //go to root path
        		
        		String[] targetDirectoryNameList = targetDirectoryPath.split("/");
        		for(String targetDirectoryName : targetDirectoryNameList) {
        			if ("".equals(targetDirectoryName)) {
        				continue;
        			}
        			if (!isFileExist(channelSftp, targetDirectoryName)) {
        				channelSftp.mkdir(targetDirectoryName);
        			}
        			channelSftp.cd(targetDirectoryName);
        		}
        	}
    		channelSftp.put(fileInputStream, srcFile.getName());
    	} catch (IOException | SftpException e) {
			e.printStackTrace();
		}
    }
	
	static void mkdir(ChannelSftp channelSftp, String directoryPath) {
		directoryPath = directoryPath.replace("\\", "/");

    	try {
    		channelSftp.cd("/"); //go to root path
    		
    		String[] directoryNameList = directoryPath.split("/");
    		for(String directoryName : directoryNameList) {
    			if ("".equals(directoryName)) {
    				continue;
    			}
    			if (!isFileExist(channelSftp, directoryName)) {
    				channelSftp.mkdir(directoryName);
    			}
    			channelSftp.cd(directoryName);
    		}
    	} catch (SftpException e) {
			e.printStackTrace();
		}
	}
	
	static boolean isFileExist(ChannelSftp channelSftp, String filePath) {
		boolean isFileExist = false;
		
		try {
			channelSftp.lstat(filePath);
			isFileExist = true;
		} catch (SftpException e) {
			if (e.id == ChannelSftp.SSH_FX_NO_SUCH_FILE) {
				isFileExist = false;
			}
		}
		
		return isFileExist;
	}

}

說明:
上面程式碼的情境是想要 SSH 連線至一台 Linux Server,
且已經設定好 ssh public key 並放在 server 的 .ssh 資料夾裡,
而我們本地的機器上也已經有一個對應的 ssh private key,
所以我們只需使用 private key 來進行連線而不用使用密碼。

session.setConfig("StrictHostKeyChecking", "no");
這行程式碼設定了 StrictHostKeyChecking 為 no,
是因為在 SSH 連線時有時會需要終端機互動,
例如在使用命令式模式指令進行 SSH 連線操作時,
server 可能會跳出一些訊息要你輸入 "yes" 等動作才能進行下一步,
設定 StrictHostKeyChecking 為 no 就可以避免這種互動式操作,
可參考:

在程式碼中可以看到,取得了 ChannelSftp 實例後,
就可以使用它來進行對 server 的操作,例如: cd, pwd, ls, mkdir, ... 等,
就像我們平台用命令列模式指令一樣,當使用了 cd 指令後,
會把我們帶到特定的 server 上的檔案路徑位置,
並且可以以目前所在的位置用相對路徑進行 ls, mkdir, ... 等操作,
當然也可使用絕對路徑進行操作。

在上述程式碼中,
我包裝並實作了 isFileExist(), upload(), mkdir() 等 methild 
來取代直接使用 ChannelSftp 自身的 API,
在實作的 method 中限定了檔案路徑參數必須為絕對路徑以避免不必要的
路徑處理麻煩,
isFileExist() 使用了 ChannelSftp.lstat() 和 ChannelSftp.SSH_FX_NO_SUCH_FILE 的補獲來實作。
upload() 和 mkdir() 使用 isFileExist() 來判斷路徑上資料夾的存在與否,
如果不存在的話則幫忙建立相應的資料夾。

2022年9月18日 星期日

在同一個頁面上建立多個 AngularJs 的 module 及 controller - 使用 angular.bootstrap()

ng-app 是很常見的 AngularJs 頁面 module 的設定,
但這樣的方式會有局限性,
因為 AngularJs 只允許一個頁面使用一個 ng-app,
如果有多個 ng-app 則會錯誤或不正常運作。

不過這並不代表 AngularJs 不能在一個頁面上設定多個 module,
AngularJs 可以使用
angular.bootstrap(element, moduleNameArray)
來在一個頁面上設定多個 module,
其中參數 element 是 module 所在的 DOM Object,
而 moduleNameArray 則是個 array<String>,
內部放著要設定到頁面上的 module 名稱。

以下是一個範例,
頁面上有三個要被設定 module 的 DOM Object,
分別是 id="module_1" 、 id="module_2" 和 id="module_3",
然後我們會先設定兩個  AngularJs module,module 名為 "module_A" 和 "module_B",
各 module 裡面各有一個名為 "controller" 的 controller,
我們要把 module_A 設定到 #module_1 和 #module_2 上,
及把 module_A 和 module_B 一起設定到 #module_3 上。

html:
<div id="module_1">
  module_1:
  <div ng-controller="controller as ctrl">
    <input type="text" ng-model="ctrl.text"/>
  </div>
</div>

<div id="module_2">
  module_2:
  <div ng-controller="controller as ctrl">
    <input type="text" ng-model="ctrl.text"/>
  </div>
  
   <div ng-controller="controller as ctrl">
    <input type="text" ng-model="ctrl.text"/>
  </div>
</div>

<div id="module_3">
  module_3:
  <div ng-controller="controller as ctrl">
    <input type="text" ng-model="ctrl.text"/>
  </div>
  
  <div ng-controller="controller as ctrl">
    <input type="text" ng-model="ctrl.text"/>
  </div>
</div>
Javascript:
angular.module("module_A", [])
.controller("controller", [function(){
	var self = this;
  self.text = "A";
}])

angular.module("module_B", [])
.controller("controller", [function(){
	var self = this;
  self.text = "B";
}]);

angular.bootstrap(document.getElementById("module_1"), ["module_A"]);
angular.bootstrap(document.getElementById("module_2"), ["module_A"]);
angular.bootstrap(document.getElementById("module_3"), ["module_A", "module_B"]);
以下是 Jsfiddle 上的成品:
說明:
首先可以先注意到,在 html 中的
id="module_2" 中
故意擺放了名稱一樣的 controller:
ng-controller="controller as ctrl"
這裡是要展示一個特性,就是即使同一個 module 下有相同 controller 名稱的 controller,
也就是它們都使用了在 javascript 中同一個 module 之下的同樣名為 "controller" 的 controller 設定,
它們的 scope 仍然是不同的,方便想像可以把它們看做是同一個 function 所 new 出來的不同  Class 實體 (Class Instance),
所以它們設定的 ng-model 是各自獨立的,並不會互相連動,
可以從 Jsfiddle 上的成品範例實際使用觀察看看。

再來在 html 中的
id="module_1" 和 id="module_2" 雖然都用了在 javascript 中相同的名為 "module_A" 的 module 設定,
不過它們仍然算是互相獨立的不同 module 個體,跟之前同個 module 下的同名 controller 說明很像,
所以它們彼此的同名 controller 的 ng-model 也是不會互相連動的。

最後是在 html 中的
id="module_3",它被設定了 module_A 和 module_B,
所以它可以得到 module_A 之下及 module_B 之下的 controller 設定,
有點像是一個載入了 module_A 和 module_B 的 module_C 一樣,
可以方便想像成如下:
angular.module("module_C", ["module_A", "module_B"]),
然後當然的,跟上面說明的情況一樣,
id="module_3" 上設定的 module 跟 controller 一樣是獨立於 id="module_1" 和 id="module_2",
事實上,id="module_1"、id="module_2" 和 id="module_3" 上的 module 跟 controller 彼此都是互相獨立無關的,
唯一的共同點就只有都有使用到了 module_A 的設定而已。

最後的結果是範例中四個 <input> 的值彼此都不是互相連動的。
這在如果你想要把相同程式邏輯的 module 做成元件,
並放到頁面上的各處,又不想它們彼此共用 scope (即共用 ng-model 去連動改變 scope 中的變數值) 時,
會是一個可利用的不錯的特性。

是 Luis 作者寫的一篇關於 ng-app 限制的文章,
也一樣用到了 angular.bootstrap 去解決 ng-app 限制的問題,
並在 Github 上實作了一個方便來用 DOM 屬性設定 AngularJs module 的 module 工具,
源碼部份也是使用了一樣的觀念,
找出要設定 module 的 DOM,從屬性中讀到要被設定的 module 名稱後進行 module 的設定,
十分地具有參考的價值。

2022年8月19日 星期五

Git - 如何取消歷史 commit 中對某個檔案的修改紀錄

今天遇到一個情況是,
在 Git 的 commit 歷史紀錄中,
希望把其中有一次的 commit 中的某個檔案修改取消掉。
例如假設我現在狀態最新 commit 是 commit_0,
之後修改了檔案 file_1 和檔案 file_2 並 commit 成 commit_1,
接著又修改其他東西 commit 了 commit_2, commit_3 之類的,
但後來我反悔了,想把 commit_1 中對檔案 file_1 的修改取消掉,
也就是希望當作之前在 commit_1 中沒有修改過 file_1。

在這篇文章中特別紀錄一下上述情境的解法:
首先先列出一下 commit 的紀錄如下:

commit_3 ........
commit_2 ........
commit_1 修改了 file_1, file_2
commit_0 ........

首先要執行以下指令進入 git rebase interaction 互動模式

git rebase -i {commit_0 的 commit id}

接著會跳出編輯器供修改,例如可能是以下內容:

pick {commit_0 的 commitId} ......
pick {commit_1 的 commitId} ......
pick {commit_2 的 commitId} ......
pick {commit_3 的 commitId} ......

因為要修改的是 commit_1,所以我們要把 commit_1 的 "pick" 改成 "edit",
改完後儲存關掉,回到指令視窗,
執行以下指令將 file_1 的內容變回在 commit_1 之前,也就是在 commit_0 時的狀態:

git checkout {commit_0 的 commit id} -- {file_1 的路徑}

再來把已回復內容的 file_1 加進暫存區以進行 commit:

git add {file_1 的路徑}

接著再執行如下的 commit amend 指令,重新 commit 以取代原來的 commit_1 ,
這樣因為對於 commit_0 來說 file_1 在這次的 commit 其內容並沒有任何被修改的地方,
所以新的這個 commit_1 將不會有修改 file_1 的紀錄:

git commit --amend

最後我們再執行如下的 rebase continue 指令讓 rebase 的程序繼續進行下去至結束就行了。

git rebase --continue

參考資料:

  1. While editing a commit in `git rebase -i`, have to revert changes in a single file

2022年7月19日 星期二

MS Sql Server 的 SYSDATETIMEOFFSET()、SWITCHOFFSET() 和 TODATETIMEOFFSET()

在 Microsoft Sql Server 中,
datetime 資料型別是沒有時區資訊的,,
比如 2022-01-01 00:00:00 如果沒有時區資訊的話,
它可以是美國時區的 2022-01-01 00:00:00 ,也可以是台灣時區的 2022-01-01 00:00:00,
對 1970-01-01 00:00:00 UTC+0 的毫秒數間隔是不一樣的。

而 datetimeoffset 資料型別就有時區資訊,
例如 2022-01-01 00:00:00 UTC+8 就是台灣時區的 2022-01-01 00:00:00,
對應到 UTC-8 的時區 就是 2001-12-31 08:00:00 UTC-8,
只是表示方式不同,
但對 1970-01-01 00:00:00 UTC+0 的毫秒數間隔通通都是一樣的。

以下介紹我常用的三個好用 Sql Server 函式,
SYSDATETIMEOFFSET()、SWITCHOFFSET() 和 TODATETIMEOFFSET(),
可以用來對日期格式做不同處理:

SYSDATETIMEOFFSET()
傳回擁有時區資訊的系統目前時間 (格式為 datetimeoffset(7),即小數位數有到 7 位的有時區時間)
例如 print SYSDATETIMEOFFSET() 可印出如下結果:
2022-07-19 20:40:24.8558075 -07:00
跟 SYSDATETIME() 的差別是 SYSDATETIME() 是傳回 datetime2 格式的無時區時間,其印出結果如下:
2022-07-19 20:40:24.8558075
可以看到只差在有無包含時區資訊而已

SWITCHOFFSET(datetimeoffset_expression, timezoneoffset_expression):
對特定有時區資訊 (沒給時區的話會被當做是 UTC+0) 的日期(datetimeoffset_expression) 用指定的時區位移(timezoneoffset_expression)
進行換算並返回相應的 dateoffset 型別結果,例如:
print SWITCHOFFSET('2022-01-01 03:00:00 +07:00', '+08:00')
的結果為:
2022-01-01 04:00:00.0000000 +08:00
可以看到 SWITCHOFFSET() 並不會改變日期的值,
也就是其日期和 1970-01-01 00:00:00 之間差距的毫秒數還是一樣,
指的還是同一個日期,只是用不同的時區格式寫出來而已。

TODATETIMEOFFSET(datetime_expression , timezoneoffset_expression):
對特定無時區資訊 (有給時區的話會被忽略) 的日期(datetime_expression) 加上指定的時區位移(timezoneoffset_expression)
資訊,返回日期和時區組合好後的有時區資訊日期,型別為 datetimeoffset,
例如:
print TODATETIMEOFFSET('2022-01-01 03:00:00 +07:00', '+08:00')
的結果為:
2022-01-01 03:00:00.0000000 +08:00
可以看到只有時區部份的資訊被改變了,表示無時區部份的日期資訊並沒有被改變,
所以其日期和 1970-01-01 00:00:00 之間差距的毫秒數也改變了,
變成用新時區去看日期部份得到的新日期。

參考資料:

  1. Transact-SQL (日期和時間資料類型和函式)
  2. 使用內建函式查詢

2022年7月11日 星期一

自訂 <ol> <li> 的項目標式方式 (list-style-type 配合 @counter-style)

html 中 <ol> <li> 的項目標式方式,可以使用 css 的 list-style-type 屬性配合  @counter-style 來自訂項目顯示方式,這裡演示一個將
list-style-type : trad-chinese-informal
擴展成自己想要的項目清單顯示方式,例如此例把
一、二、三
改成:
(一)、(二)、(三)

源碼如下:
Html:
<ol class="my-custom-list-style">
  <li>1</li>
  <li>2</li>
  <li>3</li>
</ol>
CSS:
ol.my-custom-list-style {
  list-style-type: my-custom-list-style-type;
}

@counter-style my-custom-list-style-type {
  prefix: "(";
  suffix: ")";
  system: extends trad-chinese-informal;
}
成品如下:

2022年5月10日 星期二

紀錄驗證 Google 第三方登入傳入的RSA 256 JWT token 的程式碼

紀錄驗證 Google 第三方登入傳入的RSA 256 JWT token 的程式碼
, 其官方網站有公開 Public key 的 JWK
JWK網址是:https://www.googleapis.com/oauth2/v3/certs

有兩種驗證的方法:

  1. 使用第三方 JWK 驗證相關的 Library 來做驗證 (不限只能驗證 Google 的 JWK,例如也能驗證 Apple 的 JWK)。
  2. 使用 Google 提供的 LIbrary 來做驗證。

首先是第一種,
1. 使用第三方 JWK 驗證相關的 Library 來做驗證 (不限只能驗證 google 的 JWT)。
範例如下:

Maven 的 pom.xml :

<dependency>
	    <groupId>com.auth0</groupId>
	    <artifactId>java-jwt</artifactId>
	    <version>3.18.2</version>
	</dependency>
	
	<dependency>
	    <groupId>com.auth0</groupId>
	    <artifactId>jwks-rsa</artifactId>
	    <version>0.20.0</version>
	</dependency>

可能會需要 javax.xml.bind 這個 lib,因為 jdk 8 以上沒有 jaxb 模塊,詳見: 真正解决方案:java.lang.NoClassDefFoundError: javax/xml/bind/DatatypeConverter

<!-- https://mvnrepository.com/artifact/javax.xml.bind/jaxb-api -->
	<dependency>
	    <groupId>javax.xml.bind</groupId>
	    <artifactId>jaxb-api</artifactId>
	    <version>2.3.0</version>
	</dependency>

Java:

package test;

import java.io.IOException;
import java.net.URL;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;

import com.auth0.jwk.Jwk;
import com.auth0.jwk.JwkException;
import com.auth0.jwk.JwkProvider;
import com.auth0.jwk.UrlJwkProvider;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.DecodedJWT;

import net.sf.json.JSONObject;

public class JWTTest {
	public static void main(String[] args) throws IOException, GeneralSecurityException, JwkException {
		String token = "someGoogleJwtToken";
		
		verifyToken(token);
		
	}

	public static verifyToken(String token) {
		try {
			DecodedJWT jwt = JWT.decode(token);
			JwkProvider provider = new UrlJwkProvider(new URL("https://www.googleapis.com/oauth2/v3/certs"));
			RSAPublicKey publicKey = (RSAPublicKey) provider.get(jwt.getKeyId()).getPublicKey();
			
			Algorithm algorithm = Algorithm.RSA256(publicKey, null);
			JWTVerifier verifier = JWT.require(algorithm)
					// more validations if needed
					.build();
			jwt = verifier.verify(token);
			
			System.out.println("User Id: " + jwt.getSubject());
			System.out.println("Email: " + jwt.getClaim("email").asString());
		} catch (Exception e) {
			System.out.println("Exception in verifying " + e.toString());
		}
	}
}

再來是第二種,
2. 使用 Google 提供的 LIbrary 來做驗證。
範例如下:

Maven 的 pom.xml :

<dependency>
		<groupId>com.google.api-client</groupId>
		<artifactId>google-api-client</artifactId>
		<version>1.32.1</version>
	</dependency>

Java:

package test;

import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.Collections;

import com.auth0.jwk.JwkException;
import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken;
import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken.Payload;
import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier;
import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport;
import com.google.api.client.json.gson.GsonFactory;

public class GoogleJWTTest2 {
	public static void main(String[] args) throws IOException, GeneralSecurityException, JwkException {
		String token = "eyJhbGciOiJSUzI1NiIsImtpZCI6ImZjYmQ3ZjQ4MWE4MjVkMTEzZTBkMDNkZDk0ZTYwYjY5ZmYxNjY1YTIiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJuYmYiOjE2NTE4MzA3OTQsImF1ZCI6Ijk2MjE1NTMyNDMyMy1vY3U5MzNkazFiYzY0dGhkM3JrdnVsaXI5MHVya3Nuby5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsInN1YiI6IjEwMjA1NTY1MzA0MDM0ODc0MzExMyIsImVtYWlsIjoiaHVnb2dvNzY0NkBnbWFpbC5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiYXpwIjoiOTYyMTU1MzI0MzIzLW9jdTkzM2RrMWJjNjR0aGQzcmt2dWxpcjkwdXJrc25vLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwibmFtZSI6IumDreeAmumahiIsInBpY3R1cmUiOiJodHRwczovL2xoMy5nb29nbGV1c2VyY29udGVudC5jb20vYS0vQU9oMTRHaExOWHdxT2l2cXAyOHdRY3lNZ3FGZERUU0dyYW1sSDQwTDRTejEzZz1zOTYtYyIsImdpdmVuX25hbWUiOiLngJrpmoYiLCJmYW1pbHlfbmFtZSI6IumDrSIsImlhdCI6MTY1MTgzMTA5NCwiZXhwIjoxNjUxODM0Njk0LCJqdGkiOiJmZDFlY2VmMThiNzAyZDIyNzQwNjRjZjdmMzMwOTExZDBlYzQ4NDI0In0.OFc3-NIiSsChOJWgF_SJZ9yWhSpAhY95PSllh7gSqS8YYiBJD6DIZCvbHnL2SLU69lv2kntoR-hG1aQU07ppgGN5xuqJAagvKJ8KSSkxJxSR5qOLFNMBYPghp0zgFybNEAQDTbj3E5zRlemX7w9irEMqkMliRAMDYE3aUkcOrho9X2vd9wJDrkwKmMaLfXa71MVPwIYpsOrg2Gq82nHLw24eM47VRTp3m1sqXdKz9WHgfW2_2y9GB0qn3E8Fo99wBgegRyAlz6UvbTzDNQOUdrvuSALXcrOZzog5rrfW0MinxdVfRbSNRKL0VGMJWzuGefxNqEV-Fu0CTPIqliXf1A";

		verifyToken(token);

	}

	public static boolean verifyToken(String token) {
		try {
			GoogleIdTokenVerifier verifier = new GoogleIdTokenVerifier.Builder(
					GoogleNetHttpTransport.newTrustedTransport(), new GsonFactory())
							// Specify the CLIENT_ID of the app that accesses the backend:
//							.setAudience(Collections.singletonList(
//									"962155324323-ocu933dk1bc64thd3rkvulir90urksno.apps.googleusercontent.com"))
							// Or, if multiple clients access the backend:
							// .setAudience(Arrays.asList(CLIENT_ID_1, CLIENT_ID_2, CLIENT_ID_3))
							.build();

			// (Receive idTokenString by HTTPS POST)
			GoogleIdToken idToken = verifier.verify(token);
			if (idToken != null) {
				Payload payload = idToken.getPayload();
				// Print user identifier
				String userId = payload.getSubject();
				System.out.println("User ID: " + userId);

				// Get profile information from payload
				String email = payload.getEmail();
				boolean emailVerified = Boolean.valueOf(payload.getEmailVerified());
				String name = (String) payload.get("name");
				String pictureUrl = (String) payload.get("picture");
				String locale = (String) payload.get("locale");
				String familyName = (String) payload.get("family_name");
				String givenName = (String) payload.get("given_name");
				System.out.println(email);
				// Use or store profile information
				// ...

			} else {
				System.out.println("Invalid ID token.");
			}

			return true;
		} catch (Exception e) {
			System.out.println("Exception in verifying " + e.toString());
			return false;
		}
	}
}