2020年11月18日 星期三

AES 加密 - JAVA 程式碼範例

紀錄一下 Java 使用 AES (Advanced Encryption Standard)  加解密的範例程式碼:

import java.io.UnsupportedEncodingException;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.codec.binary.Base64;

public class AESTest {

	public static void main(String[] args) {
		try {
			String seedForRandom = "TestSeed";
			String plaintext = "TestPlaintext";
			
			KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
			// 設定特定的隨機數種子,
			// 如隨機數種子不變,就會得到相同的隨機數,
			// 這裡相當於每次都使用同樣的金鑰
			SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
			secureRandom.setSeed(seedForRandom.getBytes("UTF-8"));
			//產生 AES 金鑰
			keyGenerator.init(256, secureRandom);
			SecretKey secretKey = keyGenerator.generateKey();
			SecretKey AES_secretKey = new SecretKeySpec(secretKey.getEncoded(), "AES");
			// 取得 AES 加解密器
			Cipher cipher = Cipher.getInstance("AES");
			
			//進行加密
			cipher.init(Cipher.ENCRYPT_MODE, AES_secretKey);
			byte[] encryptedText_byteArray = cipher.doFinal(plaintext.getBytes("UTF-8"));
			String encryptedText = new String(encryptedText_byteArray, "UTF-8");
			System.out.println(encryptedText); // W?\?? ???	U]?
			
			//對加密後的 byte[] 進行解密 (再取得一個新的 AES 加解密器來做)
			cipher = Cipher.getInstance("AES");
			cipher.init(Cipher.DECRYPT_MODE, AES_secretKey);
			byte[] decryptedText_byteArray = cipher.doFinal(encryptedText_byteArray);
			String decryptedText = new String(decryptedText_byteArray, "UTF-8");
			System.out.println(decryptedText); // TestPlaintext
			
			//注意!
			//加密後的 byte[] (decryptedText_byteArray) 不能與 String 互轉,
			//會流失一些東西,即上述的 encryptedText 會有轉不回 decryptedText_byteArray 的情況,
			//如果直接拿 encryptedText 來解密會出錯,例如以下例子會產生如下錯誤: 
			//javax.crypto.IllegalBlockSizeException: Input length must be multiple of 16 when decrypting with padded cipher
			//cipher = Cipher.getInstance("AES");
			//cipher.init(Cipher.DECRYPT_MODE, AES_secretKey);
			//byte[] decryptedText_byteArray2 = cipher.doFinal(encryptedText.getBytes("UTF-8"));
			// ==> Execption : javax.crypto.IllegalBlockSizeException: Input length must be multiple of 16 when decrypting with padded cipher
			
			//如上所述,替代方案可使用 base64 編碼,
			//因為加密後的 byte[] (即 decryptedText_byteArray) 可用 base64 編碼來在 byte[] 和 String 之間互轉,
			//如下所示:
			String base64Encoded_encryptedTextcryptedText = Base64.encodeBase64String(encryptedText_byteArray);
			System.out.println(base64Encoded_encryptedTextcryptedText); // V79c5vQIIP/R2wcWCVVdqA==
			
			cipher = Cipher.getInstance("AES");
			cipher.init(Cipher.DECRYPT_MODE, AES_secretKey);
			byte[] decryptedText_byteArray2 = cipher.doFinal(Base64.decodeBase64(base64Encoded_encryptedTextcryptedText));
			String decryptedText2 = new String(decryptedText_byteArray2, "UTF-8");
			System.out.println(decryptedText2); // TestPlaintext
		} catch (NoSuchAlgorithmException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (NoSuchPaddingException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (InvalidKeyException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IllegalBlockSizeException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (BadPaddingException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (UnsupportedEncodingException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

	}

}


參考:

  1. 進階加密標準(Advanced Encryption Standard,AES)
  2. JAVA AES加密與解密
  3. Input length must be multiple of 16 when decrypting with padded cipher

2020年10月27日 星期二

Maven - 使用 profile 功能為不同環境設置不同內容的檔案 - 以 web.xml, proxool.xml 為例

 Maven 可以使用 profile 的功能來設定多個環境,

並對不同環境使用不同的設定,非常方便,

舉個例來說:

通常程式專案可能會有幾個開發階段,

例如開發、測試、正式上線等,

而在不同的階段,我們可能會想使用不同的設定,

例如 Java EE Web 專案的 web.xml 可能在不同的階段會想使用不同的內容設定、

或者是 DB connection pool, Proxool 的 proxool.xml 可能在開發階段想連本地端或測試機的資料庫 (Database),但在正式上線時想連正式機的資料庫等。


這篇文主要是來紀錄使用 Maven 的 profile 功能,來為一個 Jave EE Web 專案使用

不同的 web.xml 和 proxool.xml。

我們希望在使用 maven 產生 war 檔

(命令列指令:

mvn clean install -P{active 的 profile},例如:

mvn clean install -Pdev)

時,能夠依不同的 profile 使用不同的設定。


在這篇文的例子中,先說明幾個比較特別的地方:

  1. 此篇文的 Java EE Web 專案,是由沒有使用 Maven 的舊 Java EE Web 專案轉成的有使用 Maven 的新專案,故檔案結構跟一開始就使用 Maven + webapp archetype 不一樣。
  2. 此篇文使用 Maven 的 maven-war-plugin 來處理 war 檔的發佈,其中 web.xml 可以用 <webXml> 參數來設置。
    而 proxool.xml 用 <webResource> 參數來設置。


如果舊專案是用 Eclipse 產生出來的,可以對專案按右鍵,選 

Configure --> Convert to Maven Project

來將其轉成 Maven 專案,如下圖:



檔案結構如圖所示,其中我們在專案的根目錄下 (也是 pom.xml 所在的位置),建立了一個 config 資料夾,並在其中建立了三個不同環境的資料夾 (dev, demo, production),在各個環境的資料夾中都放置了一份 web.xml 和 proxool.xml,如下圖所示:






首先在 pom.xml 中設定 profile,例如在這我們分三個階段,開發 (dev)、測試 (demo)、正式上線 (production):

<profiles>
  	<profile>			
		<id>dev</id> 
		<properties>
			<profiles.active>dev</profiles.active>	
			<jdk>11</jdk>			
		</properties>
		<activation>
			<activeByDefault>true</activeByDefault>
		</activation>
	</profile>
	<profile>			
		<id>demo</id>
		<properties>
			<profiles.active>demo</profiles.active>			
			<jdk>11</jdk>			
		</properties>		
	</profile>
	<profile>			
		<id>production</id>
		<properties>
			<profiles.active>production</profiles.active>			
			<jdk>11</jdk>			
		</properties>		
	</profile>
</profiles>

接著來設定 web.xml 和 proxool.xml,我們使用了 maven-war-plugin (詳細參數) 來處理,
首先設定 warSourceDirectory 到 WebContent 資料夾 (平常預設是 ${basedir}/src/main/webapp),
然後用 webXml 設定 web.xml 所在的位置 (相對 pom.xml 所在的路徑),
最後用 webResource 設定 proxool.xml :
<plugin>
	<artifactId>maven-war-plugin</artifactId>
	<version>3.2.1</version>
	<configuration>
		<warSourceDirectory>WebContent</warSourceDirectory>
		<webXml>config/${profiles.active}/web.xml</webXml>
		<webResources>
			<webResource>
 				<directory>config/${profiles.active}</directory>
			  	<includes>
			  		<include>proxool.xml</include>
			  	</includes>
			  	<targetPath>WEB-INF</targetPath>
			</webResource>
		</webResources>
	</configuration>
</plugin>
在這邊,${profiles.active} 代表要發佈時所要使用的 profile,即 dev, demo 或 production,
profile 可以在
mvn clean install -P{要的 profile} 
指令中指定,例如如要使用 demo,指令就為:
mvn clean install -Pdemo,
如果沒有在命令中指定 profile (即 -P 參數),就會使用 profile 裡有設定 activeByDefault = true 的 profile (例如此篇文就是設定 dev)
<activation>
	<activeByDefault>true</activeByDefault>
</activation>

在 webResource 設定裡, targetPath 的位置是相對於 warSourceDirectory 的位置,
在這篇文中,就是 WebContent 資料夾

因為在這篇文中,我們沒有把 web.xml 和 proxool.xml 放在專案的 resource directory 之中 (config 資料夾不在 resource directory 裡),
也沒有放在 warSourceDirectory 的設定資料夾中,
也就是 web.xml 和 proxool.xml 不會被發佈到 war 檔裡面,
所以不需要特別對其做 resource excludes 的動作,
例如 resource 的 exclude 
或 <warSourceExcludes> (值為相對於 warSourceDirectory 設定的位置)


參考資料:

Maven - 引用本地端的 jar

這篇文主要來紀錄我使用 Maven 引用本地端 jar 檔的經驗。

在使用 Maven 時,如果想要加入到 library 的 jar 檔在 Maven 的 Repository 找不到、

例如:

  1. 因為某些授權的原因,jar 檔的作者不願意將之放到 Maven Repository 上面,
    或就是沒有人把其 jar 檔放到 Maven Repository 上面。
  2. 可能 jar 檔是來自於合作的第三方,例如跟公司合作的夥伴提供的專屬 jar 檔。
  3. 或是本來原本官方的 jar 檔,有被自己以手動的方式做二次修改的 jar 檔 ,可能是為了相容問題或修正某些官方 bug,例如:Proxool + MS SQL Server DB Driver的相容问题 (參考:
    JAVA+ Proxool+ SQLserver 2008 “signer information does not match signer information of other classe...
    Proxool+SQLserver2008(sqljdbc4.jar) integration problem
  4. 自己寫的,例如公司內部用的 jar 檔,或是想要把舊專案轉成 Maven 時,因年代已久無法考證 jar 檔來原,所以乾脆直接繼續拿來用的 jar 檔。

這時我們就必須要引用本地端 (local) 的 jar 檔,

Maven 大概有幾種作法:

  1. 在 pom.xml 使用 <scope>system</scope> 和 <systemPath> 來引用本地 jar 檔檔案路徑
  2. 將本地 jar 檔放到 local repository (使用命令列指令)
  3. 將本地 jar 檔放到 local repository (在 pom.xml 中加入設定)


首先先講我在實務上希望的需求是:

  1. 能夠在 pom.xml 中容易地看出哪些是自訂的 local jar dependency。
  2. 能夠看得出及容易找出 local jar 檔放在哪裡。
  3. 能夠在不下命令列指令的情況下成功地匯入 jar 檔到 local repository。
  4. 能夠跟 IDE 配合,例如: Eclipse Maven Plugin + Tomcat 

再來在以下分述這三種作法及我實務上的例子,及我最後的選擇偏好:


1. 在 pom.xml 使用 <scope>system</scope> 和 <systemPath> 來引用本地 jar 檔檔案路徑

第一種方法 (稱為 System Dependencies) 在使用上非常簡單,直接在 pom.xml 上指定 local jar 檔的檔案路徑,

例如:

<dependency>
        <groupId>myGroupId</groupId>
        <artifactId>myArtifactId</artifactId>
        <version>1.0.0</version>
	<systemPath>D:\myJar.jar</systemPath>
	<scope>system</scope>
</dependency>

其中 <systemPath> 中也可使用用一些 Maven 自己的參數,例如表示專案路徑的 ${basedir}

此方法有幾個缺點:

  1. Maven 官網說 <scope>system</scope> 的用法 (System Dependencies)已經 deprecated 了 (來源)
  2. 在要使用 maven 指令部屬專案時,例如要產生 war 檔時, mvn clean install 產生出的 war 檔裡並不會把用 System Dependencies 設定的 local jar 檔包進生產的專案中。
  3. 如果使用 Eclipse Maven plugin + Tomcat 來開發,用 System Dependencies 設定的 local jar 檔不會被部屬至 server 上 (例如:  C:\Users\xxxUser\eclipse-workspace\.metadata\.plugins\org.eclipse.wst.server.core\tmp0\wtpwebapps\xxProject\WEB-INF\lib)


2. 將本地 jar 檔放到 local repository (使用命令列指令)

此方法是使用 Maven 的指令直接將本地端的 jar 檔放進 local repository 中,命令如下 (大括弧 {} 中的值可自由填寫):

mvn install:install-file -Dfile={本地端 jar 檔的檔案位置} -DgroupId={自訂的 group id} -DartifactId={自訂的 artifact id} -Dversion={自訂的 version}

例如:

mvn install:install-file -Dfile=D:\myJar-1.0.0.jar -DgroupId=myGroupId -DartifactId=myJar

-Dversion=1.0.0

注意 jar 檔的檔名要改成符合 maven 格式,就是 artifactId-version 這種格式。

執行完命令後,可以到自己本地上的 local respository (通常位置在C:/Users/xxxUser/.m2/repository) 去看是否有成功匯入 jar 檔。

最後在 pom.xml 中加入引用 jar 檔的設定如下:

<dependency>
          <groupId>myGroupId</groupId>
          <artifactId>myArtifactId</artifactId>
          <version>1.0.0</version>
</dependency>


不過個人認為這種方法有一個缺點,

就是如果今天我們換了一台電腦或換了一個開發者要開發時,

必須要對 local jar 檔再一次執行上述的命令例指令以匯入 jar 檔 local repository,

並且單看 pom.xml 很難辨認出哪個 dependency 是使用了自訂的 local jar 檔。

對比我上述的需求,此方法:

  1. 無法在 pom.xml 中容易地看出哪些是自訂的 local jar dependency (除非把命令列指令存起來,再對照去找 local jar 檔位置)。
  2. 單看 pom.xml 看不出 local jar 檔放在哪裡 (除非例如把要下的命令列指令放到 project 中的某一個 file 上)。
  3. 需下命令列指令來匯入 jar 檔到 local repository。
  4. 能夠跟 IDE 配合,例如: Eclipse Maven Plugin + Tomcat 


3. 將本地 jar 檔放到 local repository (在 pom.xml 中加入設定)
補充:不確定是不是 Maven 3 版本以上才需要,發現除了本地 jar 檔,還需要再放一個本地 pom 檔 (除了附檔名,檔名跟本地 jar 檔一樣)

此方法是我目前最喜歡的方法,此方法跟第二種方法一樣,

都是把本地 jar 檔放到 local repository 中,不過差在不是執行命令列指令,

而是把要的設定寫在 pom.xml 中。

首先我們要在 pom.xml 中,設定自訂的 repository,例如:

<repository>
	<id>localLibRepository</id>
	<name>localLibRepository</name>
	<url>file:${basedir}/localLibRepository</url>
</repository>

其中 ${basedir} 為 Maven 的參數,代表專案的根目錄位置,

localLibRepository 為我自己建立的資料夾,用來存放 local jar。


設定好後,接著要來把 local jar 檔放到相應的位置去,

首先像上述第二種方法一樣,把 local jar 檔命名成 maven 的格式,例如: 

myArtifactId-1.0.0.jar

然後仿照 Maven Repository 的檔案路徑格式,即 

{repositoryPath}/{groupId}/{artifactId}/{version}/{jarFileName}

的規則建立資料夾 (如同 java 的 package 一樣,如 com.xxx.yyyy,應為 com, xxx, yyy 三個資料夾),並將 local jar 檔放到正確的位置上,例如:

D:\myProject\localLibRepository\myGroupId\myArtifactId\1.0.0\myArtifactId-1.0.0.jar

接著在同一個位置增加 pom 檔,例如:

D:\myProject\localLibRepository\myGroupId\myArtifactId\1.0.0\myArtifactId-1.0.0.pom

內容範例如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <modelVersion>4.0.0</modelVersion>
  <groupId>myGroupId</groupId>
  <artifactId>myArtifactId</artifactId>
  <version>1.0.0</version>
</project>

最後如第二種方法一樣,在 pom.xml中引入 local jar 檔的 dependency,例如:

<dependency>
          <groupId>myGroupId</groupId>
          <artifactId>myArtifactId</artifactId>
          <version>1.0.0</version>
</dependency>

之所以我最後選擇這個方法的原因是,它完全符合我的需求:

  1. 比較能夠在 pom.xml 中容易地看出哪些是自訂的 local jar dependency
    (對照 pom.xml 中自訂 repository 的設定,打開 repository 的資料夾即可得知)。
  2. 能夠看得出及容易找出 local jar 檔放在哪裡 (看 pom.xml 中即可得知自訂 repository 的位置)。
  3. 能夠在不下命令列指令的情況下成功地匯入 jar 檔到 local repository。
  4. 能夠跟 IDE 配合,例如: Eclipse Maven Plugin + Tomcat 


參考資料:

  1. 3 ways to add local jar to maven project
  2. Adding dependencies to local .jar files in pom.xml
  3. How to include local jar files in Maven project [duplicate]
  4. Maven私服上传pom和jar实操

2020年10月21日 星期三

React - Redux + Hook 練習 - ToDo List (待辦事項) 簡易實作

Hook 是 React v16.8 的新功能,讓我們在撰寫 React Component 時,

可以讓我們用 function 的寫法來取代 Class 的寫法,

來避免在 Class 中常會碰到的問題,例如:

1. Class 中的 this 意義常讓人無法容易地理解。

2. Component 上的事件 (event) 綁定,常須要多寫 bind() 的步驟,並且涵義也容易讓人不容易了解,例如:

class TextInput extends React.Component{

	constructor(props){

		super(props);

		this.onHandleTextChanged = this.onHandleTextChanged.bind(this);

	}

	onHandleTextChanged(e){

		this.props.handleTextChanged(e.target.value);

	}



	render(){

		return (

			<div>

					<input type="text" value={this.props.text} onChange={this.onHandleTextChanged} />

			</div>

		);

	}

}

而因為 function 不像 Class 可以儲存保存自己的成員變數,

為了可以實現像 Class 那樣保存、修改依賴於 Component 的成員變數,

React 使用了 useState() 這個 function,其可以以一個初始值做為輸入,

並返回一個包含 變數、修改變數的 function 的 array,

Ex:

import { useState } from "react"

const [text, setText] = useState("initial value");


React Redux 也推出了使用 React Hook 的版本,

其提供了 useSelector() 和 useDispatch() ,

useSelector() 可以讓我們方便得取得 Reducer Storde 裡 state 的值,

而不用像以前一樣使用

connect(mapStateToProps, actionCreators)

this.props.XXX

等的方法來取得 state 值,

Ex:

import {useSelector} from 'react-redux'

const toDoList = useSelector(state => state.toDoListReducer.toDoList);


useDispatch() 可以讓我們方便得送入要傳給 Reducer 的 Action 來觸發 Component render 元件繪製的行為,

而不用像以前一樣使用

connect(mapStateToProps, actionCreators)

this.props.xxxAction()

等的方法來傳遞 Action 給 Reducer,

Ex:

import {useDispatch} from 'react-redux'

import * as actionCreators from './action'

const dispatch = useDispatch();

function handleDeleteToDo(){

dispatch(actionCreators.deleteToDo(toDo.id));

}


這篇文要用 React Hook 來改寫上一篇,

React - Redux 練習 - ToDo List (待辦事項) 簡易實作

做為練習,並比較沒用 Hook 及有用 Hook 兩者程式碼的差異。


跟之前的例子比,有改動的檔案只有:

Main.jsx

TextDisplayer.jsx

TextInpupt.jsx

ToDoList.jsx

ToDoRow.jsx

如下圖所示


接著來看程式碼的部份:

Main.jsx :

import React, { useState } from "react"
import {TextDisplayer} from "./TextDisplayer.jsx"
import TextInput from "./TextInput.jsx"
import ToDoList from "./ToDoList.jsx"

function Main(){
	const [text, setText] = useState("");	

	return (
		<React.Fragment>
			<TextInput text={text} handleTextChanged={setText}/>
			<TextInput text={text} handleTextChanged={setText}/>
			<TextDisplayer text={text}/>
			<ToDoList/>
		</React.Fragment>
	);
}

export default Main

說明:

可以看到原本的 Main 不用 Class ,而改用了 function 去改寫,

因為只是 function ,所以連 constructor() 也不用了。

原本在用 Class Component 時,Component 所管理的 text 這個成員屬性,

是用 this.state = {text : ""} 的方式指定初始值的。

改用 Function Component 時,我們使用了 useState(初始值) 的方式來獲得設定了初始值的 text 變數,並且還得到了一個可以改變 text 的 function,即 setText() ,

const [text, setText] = useState("");

中的 text 及 setText() ,就跟以前一樣,

可以使用在 Main Component 中做UI的顯示,

也可以傳遞給 Child Component ,讓下層元件可以顯示及修改 text。

例如在 Main Component 中,我們把 text 及  setText() 傳遞給 TextInput、TextDisplayer 及 ToDoList。


TextDisplayer.jsx :

import React from "react"

function TextDisplayer({text}){
    return (<div>Text you typed : {text}</div>);
}

export {TextDisplayer}

說明:

TextDisplayer Component  也寫成了 Hook 的 Function Component 的形式,

此時接收上層元件的值改由從 Function Component 的輸入值傳進來,

輸入會是一個 Object, key 及 value 由上層元件傳進來的時候指定,

例如 在 Main.jsx 中用 text={text} 的方式傳 text 進 TextDisplayer 元件,

TextDisplayer 的 function component 輸入就會是:

{text : text的值} 


TextInpupt.jsx :

import React from "react"

import {useDispatch} from 'react-redux'
import * as actionCreators from './action'

function TextInput({text, handleTextChanged}){	
	const dispatch = useDispatch();

	function onHandleTextChanged(e){
		handleTextChanged(e.target.value);
	}

	function onHandleAddToDo(e){
		dispatch(actionCreators.addToDo(new Date().getTime(), text));
		handleTextChanged("");
		e.preventDefault();
	}

	return (
		<div>
			<form onSubmit={onHandleAddToDo}>
				<input type="text" value={text} onChange={onHandleTextChanged} />
				<input type="submit" value="addToDo"/>
			</form>
		</div>
	);
}

export default TextInput

說明:

TextInput Component 也被改寫成了 Hook 的 Function Component 形式,

此時不像 Class Component 時使用 this.props 的方式來取得上層元件傳進來的值,

而是直接從 Function Component 的 Input 輸入取得,

輸入會是一個 Object,其中會包括由上層元件傳進來的值,例如此例就是 Main.jsx 傳進來的

text 及 handleTextChanged 。

可以注意到的是, onChane 上面綁定的 onHandleTextChanged(),

不用再像以前 Class Component 那樣須要使用 bind() ,例如以前的寫法:

this.onHandleAddToDo = this.onHandleAddToDo.bind(this);

另一個可以注意到的是,我們沒有使用到如

export default connect(mapStateToProps, actionCreators)(TextInput)

的語法去得到 Reducer Store 裡的變數及函式,

那我們要怎麼把 Action 傳遞給 Reducer 呢?

答案是使用 Redux Hook 提供的 useDispatch() ,

其他傳回一個函式 dispatch(要送給 Reducer 的 Action) ,

我們可以直接把要傳遞的 Actoin 傳給 Reducer。


ToDoList.jsx :

import React from "react"
import ToDoRow from "./ToDoRow.jsx"

import {useSelector} from 'react-redux'

function ToDoList(){
	const toDoList = useSelector(state => state.toDoListReducer.toDoList);

	return (
		<ul>
			{	
				toDoList.map(function(toDo){
					return (
						<ToDoRow toDo={toDo} key={toDo.id}/>
					);
				})
			}
		</ul>
	);
}

export default ToDoList

說明:

ToDoList 元件跟 TextInput 元件一樣,沒有使用 connect() 來從 Reducer Store 取得資料,

那要怎麼取得 Reducer Store 中取得資料呢? 例如此例是要取得 toDoList。

答案是使用 Redux Hook 提供的 useSelector(Reducer 的 state) ,useSelector()會有 Redcuer 的 state 作為 callback 輸入,這樣我們就能取得 Reducer State 裡的資料,

在這裡因為我們之前有在 reducer.js 裡使用了 combineReducers() 去 combine toDoListReducer,所以取得 toDoList 的方式為 state.toDoListReducer.toDoList ,

如果沒有用 combineReducers() 的話,就會改成用

state.toDoList 直接取得值。


ToDoRow.jsx :

import React from "react"

import {useDispatch} from 'react-redux'
import * as actionCreators from './action'

function ToDoRow({toDo}){
	const dispatch = useDispatch();

	function handleDeleteToDo(){
		dispatch(actionCreators.deleteToDo(toDo.id));
	}

	return (
		<li>
			{toDo.toDoText}
			<button onClick={handleDeleteToDo}>Delete</button>
		</li>
	);
}

export default ToDoRow

說明:

ToDoRow 元件從本身 Function Component 的輸入中取得由 ToDoList 元件來的 toDo,

並且跟 TextInput 元件一樣使用了 dispatch() 來傳遞要送給 Reducer 的 Action,此例為 actionCreators.deleteToDo(toDo.id)

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

最後我們可以大概總結一下在這篇改用 React Hook 的文中,跟

React - Redux 練習 - ToDo List (待辦事項) 簡易實作

這篇沒使用 React Hook 文不一樣的地方:

  1. Component 元件全部由 Class 寫法改成 Function 寫法,
    故少了 Class 特性的語法,例如: constructor()、 this 的用法、 bind() 的用法。
  2. Component 自己管理的 state (例如 this.state = {text}),因為無法使用 Class 的 this 寫法,
    所以改成用 Hook 來管理,使用方法為 [變數名, 可修改變數的 function] = useState(初始值)。
  3. 上層把變數傳給下層的方法跟以前一樣,只是下層 Hook Component 因為不是 Class,
    所以不用 this.props ,改用從 Function Component 本身的輸入去取得,例如: 
    function ChildComponent({inputValue1, inputValue2, ...})
  4. 在取得 Reducer 管理的 state 方面,不使用 connect() 的方法來將 reducer state 及 action 綁到 this.props 上 ,而是使用 Redux Hook 所提供的 useSelector() 來取得,例如:
    var 要取得的 state value 變數 = useSelector(function(reducer的state){return 要的 value 變數});
  5. 在傳遞 Action 給 Reducer 方面,不使用 connect() 的方法來將 reducer state 及 action 綁到 this.props 上,而是使用 Redux Hook 所提供的 useDispatch() ,
    useDispatch() 可以回傳一個 dispatch(),
    dispatch() 可以輸入一個 action,其會將 action 傳遞給 Reducer,例如:
    var dispatch = useDispatch();
    dispatch(要傳遞的 action)。


最後要注意的地方是,

useState() 、useSelector()、useDispatch() 等

都要在 Function Component 的最上層呼叫,

不要在迴圈、條件式或是巢狀的 function 內呼叫 Hook (參考)。

要理解這個,我們可以從 Hook 實作的原理去理解。


首先是,為什麼元件寫成 function ,卻可以如 Class 搬擁有自己可管理的 state (例如 text),

因為 function 裡的所有變數應該都只是區域變數而己 (local variable),

應該無法長期保存其值 (不管有沒有被修改)。


React Hook 的實作方式可以簡單理解為 (**不代表真實實作的細節,只是類似,可以從這想法大致理解 Hook 的概念),

使用了兩個(或多個,端看 React Hook 的詳細實作)陣列 (或鍵結串列等類似有序串列的資料結構) 來儲存了所有使用

useState() 設定的 值 (暫且稱作 state value) 及 可修改值的function (暫且稱作 setState function),例如:

[value1, value2, value3, ...]  (暫且稱作 state array)

[setValu1, setValu2, setValu3, ...]  (暫且稱作 setState array)

在 React 要重新繪製 (Render) 所有元件時,就會依序執行所有function component 的 function,

如果是第一次繪製,呼叫 useState() 時,各 state value 會用初始值儲存在 state array 中,

並且在相應的 index 中存放可修改 value 的函式在 setState array 中 (所以函式會知道它要去修改哪個 state value)。

如果是之後的繪製,呼叫 useState() 時,各 state value 會以同樣順序的方式從 state array 中取出,

因為 state array 只是個陣列,所以每次取出時必須要照順序,因為它並不知道要取出的 state value 是給哪個 Component 用的,

這樣就能理解,為什麼在 Component 中,只能一次的,在最上層呼叫 useState(),

因為這樣才能確保取出 state value 時,每次都是照一樣的順序



源碼分享下載:
ReactReduxTest.7z

本Blog的其他相關文章:



參考資料:

  1. 使用 State Hook
  2. 打造你自己的 Hook
  3. React Redux | 小孩子才做選擇! Hooks 和 Redux 我全都要!
  4. 原始碼解析 React Hook 構建過程:沒有設計就是最好的設計
  5. React Hooks 不是黑魔法,只是陣列
  6. [译] 深入 React Hook 系统的原理
  7. Hooks
  8. How to pass a React Hook to a child component

2020年7月13日 星期一

使用 JACOB Java-COM Bridge 操作 Microsoft Outlook

Java 無法直接執行作業系統的部份函式庫,
例如無法直接執行並操作如 Microsoft Outlook, Word, Excel 等跟作業系統綁的較緊的軟體,
如果真得要使用 Java 來操作,
則必須要使用例如 JNI 或 Java-COM Bridge 等技術 (例如 JACOB, Jawin 等)來實現。

Java Outlook Connector 是一個提供 Java 控制本地端 Outlook 的 Java Lib,
將底層的 JNO、 COM、MAPI 等複雜的實現進行了封裝,
不過只有試用版 (每次執行都會跳出建議購買的視窗) 及付費版。

這裡要介紹的是另一個方案, JACOB,
為一個免費的 Java-COM Bridge ,
這邊要演示的是使用 JACOB 這個 Java-COM Bridge 來操作 Outlook 的範例。
要注意的是,這裡只是直接操作電腦上的 Outlook 軟體,
並非使用 JavaMail 來連接 mail server,
所以並不需要進行登入連接等動作,
可以想成類似直接使用 VBscript 來控制 Outlook。

首先先去 JACOB 官網下載所需的 JAR 檔和 DLL 檔,
裡面有提供最新版放在 Sourceforge.net 上的檔案下載頁面連結
下載完解壓縮後可以看到所需的 jacob.jar、jacob-1.19-x64.dll  (或著是 32 位元,依執行環境自行選擇),也有附上 Javadoc 的說明文檔。

引入 jacob.jar、jacob-1.19-x64.dll  (例如設定環境變數 path、或放到System32 資料夾下、或放到程式同目錄等,
可參考
source of com.jacob.com.LibraryLoader
System.loadLibrary() 的使用方法彙總
) 後,
就可以開始撰寫程式。

範例程式碼如下,
其中讀取了 Outlook 的 INBOX 收件夾,
找出特定 Subject 的信,並將信上的附件檔存到本地資料夾。

package outlookReaderTest;

import com.jacob.activeX.ActiveXComponent;
import com.jacob.com.Dispatch;

public class JacobTest {

 public static void main(String[] args) {
  // 指定 INBOX 收件夾,參考 https://docs.microsoft.com/zh-tw/office/vba/api/outlook.oldefaultfolders
  int olFolderInbox = 6; 
  ActiveXComponent outlookApplication = new ActiveXComponent("Outlook.Application");
  Dispatch outlookObject = outlookApplication.getObject();
  Dispatch myNamespace = Dispatch.call(outlookObject, "GetNamespace","MAPI").toDispatch();
  Dispatch myFolder = Dispatch.call(myNamespace,"GetDefaultFolder", Integer.valueOf(olFolderInbox)).toDispatch();
  Dispatch mails = Dispatch.get(myFolder, "Items").toDispatch();
  
  /*
   * 上述程式碼等同於 VBscript 的:
   * Dim outlookObject, myNamespace, myFolder, mails
   * Set outlookObject = CreateObject("Outlook.Application")
   * Set myNamespace = outlookObject.GetNamespace("MAPI")
   * Set myFolder = myNamespace.GetDefaultFolder(6)
   * Set mails = myFolder.Items
   * 下面的程式碼跟 VBscript 的對照可以此類推
   */

  int count = Dispatch.call(mails, "Count").getInt();
  System.out.println("Total Mail Count: " + count);
  for (int i = 1; i <= count; i++)
  {
   Dispatch mail = Dispatch.call(mails, "Item", Integer.valueOf(i)).toDispatch();
   String subject = Dispatch.call(mail, "Subject").getString();
 
   if ("This is a test mail".equals(subject)) {
    System.out.println(i);
    System.out.println(subject);
    
    String saveFolderPath = "D:\\tempTestFoler";
    
    //Save attachments
    Dispatch attachs = Dispatch.call(mail, "Attachments").toDispatch();
    int attachCount = Dispatch.call(attachs, "Count").getInt();
    System.out.println("attachCount: " + attachCount);
    for (int j = 1; j <= attachCount; j++) {
     Dispatch attach = Dispatch.call(attachs, "Item", Integer.valueOf(j)).toDispatch();
     String attachName = Dispatch.call(attach, "FileName").getString();     
     Dispatch.call(attach, "SaveAsFile", saveFolderPath + "\\" + attachName);
     System.out.println(attachName + " -- saved");
     //Dispatch.call(mail, "Delete");
    }
   }
  }
 }

}





參考:

  1. Open source java library to read outlook emails, calendar etc
  2. 【JACOB】Java-COM Bridge 介紹
  3. COM - Microsoft物件導向開發架構的基礎
  4. Java Outlook Connector 

2020年5月3日 星期日

將 java 的 Excpetion.printStackTrace() 輸出內容寫到 String 中

將 java 的 Excpetion.printStackTrace() 輸出內容寫到 String 中

import java.io.PrintWriter;
import java.io.StringWriter;

public class Test {

 public static void main(String[] args) {
  try{
   //some code
  }catch(Exception e){
   StringWriter stringWriter = new StringWriter();
   e.printStackTrace(new PrintWriter(stringWriter));
   String exceptionString = stringWriter.toString();
   System.out.println(exceptionString);
  }
 }

}


參考:
  1. [程式][Java] Exception StackTrace to String


2020年3月16日 星期一

ffmpeg 常用命令

ffmpeg 基本參數:
-y : 如果輸出檔案已存在,不詢問直接覆蓋
-n : 如果輸出檔案已存在,不詢問直接忽略不覆蓋

注意:
如果輸出檔案跟輸入檔案相同,
在為 video 檔案時,-y 會失效,
會印出
FFmpeg cannot edit existing files in-place.
錯誤

ffmpeg 常用命令:
=============================================================
將圖或影片 Resize 成特定尺寸:
Ex : 將圖或影片 resize 成 320x240
D:\\ffmpeg\\bin\\ffmpeg -i "input.jpg" -vf scale=320:240 "output_320x240.png"
** 注意:
因為 h264 encoder 規定輸出影片長寬須為偶數,
如指定奇數會有 "ffmpeg height not divisible by 2" 的錯誤訊息,可將指令改成:
D:\\ffmpeg\\bin\\ffmpeg -i "input.jpg" -vf scale=floor(321/2)*2:floor(241/2)*2 "output_321x241.png"

=============================================================
將圖或影片在保持長寬比 ration 的情況下 Resize 到能塞到特別尺寸的矩形中,
如原圖檢來就可塞得下,就不 resize
Ex: 將圖或影片保持長寬比並 resize 至塞得下 300x500 的矩形中
D:\ffmpeg\bin\ffmpeg -i "input.jpg" -vf "scale='min(iw, 300)':'min(ih, 500)':force_original_aspect_ratio=decrease" "output_boundedBy_300x500.png"
** 注意:
因為 h264 encoder 規定輸出影片長寬須為偶數,
如指定奇數會有 "ffmpeg height not divisible by 2" 的錯誤訊息,可將指令改成:
D:\ffmpeg\bin\ffmpeg -i "input.jpg" -vf "scale='floor(iw*min(1,min(301/iw,501/ih))/2)*2':'floor(ih*min(1,min(301/iw,501/ih))/2)*2'" "output_boundedBy_301x501.png"

參考:

  1. FFMPEG (libx264) “height not divisible by 2”
  2. ffmpeg转码遇见错误height not divisible by 2的解决方案
  3. [FFmpeg] error about width or height not divisible by 2
=============================================================
將影片壓縮
D:\\ffmpeg\\bin\\ffmpeg -i "inputVideo.mp4" -r 10 -b:a 32k "outputVideo.mp4"
=============================================================
得到影片中,特定秒數幀(frame)的圖片
Ex: 取得 22:11:00 的 frame
D:\\ffmpeg\\bin\\ffmpeg -i "inputVideo.mp4" -y -f  image2  -ss 22:11:00 -vframes 1 "outputFrame.jpg"
=============================================================
將影片中的 "影片資訊 (moov box 或稱 movie box)" 放到檔案二進位較前的位置,讓例如瀏覽器能夠先讀到馬上開始邊播放邊下載影片 (通常瀏覽器需得到如影片長度等資訊後才會開始播放影片),
而不用等到讀到 "影片資訊 (moov box)" 後才開始播放
參考 : MP4 moov box解析
D:\\ffmpeg\\bin\\ffmpeg -i "inputVideo.mp4" -movflags faststart -acodec copy -vcodec copy "outputVideo.mp4"
=============================================================
取得資源 (影片檔或圖片檔,也可為外部網址 url),的資訊,並以 json 格式輸出,
例如長、寬、影片長度等
D:\\ffmpeg\\bin\\ffprobe -show_streams -print_format json "imageOrVideoUrl"
=============================================================
對圖片或影片進行截剪
Ex: 將圖片在 x=135, y=75 的位置(以左上為原點,向右向下為正),以width=270, height = 150 進行截剪
D:\\ffmpeg\\bin\\ffmpeg -i "input.jpg" -vf crop=270:150:135:75 "out.jpg"
=============================================================
將GIF動圖的某個幀(frame)取出,
或者是將所有幀全數取出 (輸出檔名用 %d ,之後會得到多個圖,檔名中的 %d 會被幀數代替 )
Ex:
D:\\ffmpeg\\bin\\ffmpeg -i "input.gif" -vsync 0 "output.png"
D:\\ffmpeg\\bin\\ffmpeg -i "input.gif" -vsync 0 "output_%d.png"
=============================================================
旋轉或翻轉圖片和影片
D:\\ffmpeg\\bin\\ffmpeg -i -vf "transpose=1" output.mp4
Note:
transpose = 0 : 順時鐘團 90 度後,水平左右翻轉
transpose = 1 : 順時鐘團 90 度
transpose = 2 : 逆時鐘團 90 度
transpose = 3 : 順時鐘團 90 度,水平左右翻轉

可以連續旋轉翻轉,用transpose 指令用雙引號(")包住並用逗號(,)隔開,例如
D:\\ffmpeg\\bin\\ffmpeg -i -vf "transpose=1,transpose=1" output.mp4
參考:
How To Rotate Videos Using FFMpeg From Commandline