2021年12月29日 星期三

使用 Gradle 建立 Fat Jar 的範例

使用 Gradle 將專案做成 Fat Jar 型式的 build.gradle 範列:

Note:

Fat Jar 為把所有 Dependency 都包在一起的一種 Jar 包。
有幾種不同的實現,其中 Unshaded 的方式為把依賴的 jar 都解開來,
並一起包進最後的 Jar 包中。

Note:

以下 build.gradle 範例使用了 Gradle Shadow plugin 來打包,
但非必須,只使用 Gradle 自帶的 jar task 也可以打包 fat jar,
只是此範例因依賴了 log4j2,因為 log4j2 在打包 fat jar 時會有
多個 Log4j2Plugins.dat 檔被不正常合併的問題
(每個 log4j plugin 的 Log4j2Plugins.dat 被合成一個檔,但內容互相蓋掉而沒有將內容正確合併),
所以使用了 Gradle Shadow plugin 的 Log4j2PluginsCacheFileTransformer 來解決。

此範例使用了 Gradle 7.3.3 版,建立 Fat Jar 的指令為:
./gradlew clean shadowJar

bundle.gradle :
plugins {
    // Apply the java-library plugin to add support for Java Library
    id 'java-library'
    id 'application'
    id 'com.github.johnrengelman.shadow' version '7.1.2'
}

java {
    sourceCompatibility = JavaVersion.VERSION_11
    targetCompatibility = JavaVersion.VERSION_11
}

mainClassName = "main.Main"

repositories {
    mavenCentral()
}

configurations {
    externalLibs
}

dependencies {
    // This dependency is exported to consumers, that is to say found on their compile classpath.
    api 'org.apache.commons:commons-math3:3.6.1'

    // This dependency is used internally, and not exposed to consumers on their own compile classpath.
    implementation 'com.google.guava:guava:28.0-jre'

    // Use JUnit test framework
    testImplementation 'junit:junit:4.12'
    
    // https://mvnrepository.com/artifact/javax.mail/javax.mail-api
	implementation group: 'javax.mail', name: 'javax.mail-api', version: '1.6.2'
	
	// https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core
	implementation group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.17.1'
	
	// https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-api
	implementation group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.17.1'
	
	// https://mvnrepository.com/artifact/org.slf4j/slf4j-api
	implementation group: 'org.slf4j', name: 'slf4j-api', version: '1.7.25'
	
	// https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-slf4j-impl
	implementation group: 'org.apache.logging.log4j', name: 'log4j-slf4j-impl', version: '2.17.1'
   
   //external libs, for example: xxx.dll
   externalLibs files('xxxExternalLib1, xxxExternalLib2')
}

shadowJar{
  transform(com.github.jengelman.gradle.plugins.shadow.transformers.Log4j2PluginsCacheFileTransformer)
  archiveFileName = "${baseName}.${extension}"
}

jar {
    manifest {
        attributes(
        	'Main-Class': 'main.Main',
        	"Multi-Release": true
    	)
    }
    from configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }    	
    from configurations.externalLibs.collect { it }
}
--------------------------------------------------------------
上列 Fat Jar 的 bundle.gradle 內容中,Gradle Shadow plugin 會讀取 jar task 裡的配置。
在 jar task 中,需要加入以下兩條設定來將依賴放到最終的 Jar 檔裡,
否則會只有專案本身的程式被編譯而已:
from configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }    	
from configurations.externalLibs.collect { it }
編譯後的 Jar 檔會被放在專案的
/build/lib 資料夾中,名稱可以用如以下的設定自行修改:
archiveFileName = "${baseName}.${extension}"

Note:

還有其他各種參數值可以使用,例如:${baseName}, ${appendix}, ${version}, ${classifier}, ${extension} 等
--------------------------------------------------------------

執行 Gradle 指令除了用自己在電腦上安裝的 Gradle 以外 (可能會跟專案用的版本不同),
也可使用專案中自帶的 Gradle Wrapper 來執行 Gradle 指令,
好處是可以使用跟專案開發時一樣本的 Gradle,
並且就算自己電腦上沒有安裝 Gradle 也可以執行,例如:

./gradlew clean build

如果想更改專案用的 Gradle 版本,可執行以下指令,例如要更改成 7.3.3 版:
./gradlew wrapper --gradle-version 7.3.3

可以查看專案目錄中的 
/gradle/wrapper/gradle-wrapper.properties
,其中 distributionUrl 屬性值會有此專案用的 Gradle 資訊,
當電腦中沒有相應版本的 Gradle 時,它會自行下載相應版本

參考資料:

2021年12月25日 星期六

使用 VBScript 上傳檔案( multipart/form-data http post)

這裡紀錄下使用 VBscript 上傳檔案 (httpPost multipart/form-data) 的方法,
在這邊是照著 multipart/form-data 協議來手動刻出所需的封包格式,
詳細可以參考:

這裡我們用 WSF (Window Script File) 檔配合 VBscript 程式來實作,
將以下程式碼存成 UTF-8 編碼格式的 .wsf 檔,
並在命令列模式(command line) 下執行 wscript 或 cscript 用 WSH (Window Script Host) 去跑程式即可,例如:
csript xxx.wsf
要注意的是,因為我們將檔存成了 UTF-8 編碼格式,
所以在 .wsf 檔中必需在 xml 聲明 (XML declaration) 中標示 encoding="UTF-8",
例如:
<?xml version="1.0" encoding="UTF-8"?>
以下為程式碼範例,
建立了一個 uploadFile(filePath, uploadTo) 函式來傳送檔案到 uploadTo 指定的 url,
其中 "http://localhost:8080/uploadFile.do" 是接收 httpPost request 的 server,這裡不做討論,可以參考這篇文,"使用 Java 上傳檔案(發送 enctype=multipart/form-data 的 HttpPost")。
在 uploadFile() 函式中,也可以看到傳了一個中文參數的範例 (uploadData.AddForm)。

uploadFile.wsf:
<?xml version="1.0" encoding="UTF-8"?>
<package>
<job id="xxx">

<script language="VBScript">
<![CDATA[
 uploadFile "D:\未命名.png", "http://localhost:8080/uploadFile.do"
 
 ''''''''''''''''''''''''''''''''''''''''''''''''''''
 Function uploadFile(filePath, uploadTo)
  
  Dim uploadData
  Set uploadData = New XMLUpload
  
  uploadData.Charset = "utf-8" ' see Public Property Let Charset(ByVal strValue)
  uploadData.openWithUrl uploadTo
  
  uploadData.AddForm "param1", "中文參數"  
  uploadData.AddFile "uploadedFile", filePath
  
  Dim responseStr
  responseStr = uploadData.Upload()
  Set uploadData = Nothing
  
  uploadFile = responseStr
 End Function
 
 Class XMLUpload
  Private xmlHttp
  Private objTemp
  Private adTypeBinary, adTypeText
  Private strCharset, strBoundary

  Private Sub Class_Initialize()
   adTypeBinary = 1
   adTypeText = 2
   Set xmlHttp = CreateObject("Msxml2.XMLHTTP")
   Set objTemp = CreateObject("ADODB.Stream")
   objTemp.Type = adTypeBinary
   objTemp.Open
   strCharset = "utf-8"
   strBoundary = GetBoundary()
  End Sub

  Private Sub Class_Terminate()
   objTemp.Close
   Set objTemp = Nothing
   Set xmlHttp = Nothing
  End Sub  
  
  '設置上傳使用的字符集
  Public Property Let Charset(ByVal strValue)
   strCharset = strValue
  End Property
  
  Public Sub openWithUrl(ByVal urlStr)
   xmlHttp.Open "POST", urlStr, False
  End Sub

  '獲取自訂義的表單數據分界線
  Private Function GetBoundary()
   Dim ret(12)
   Dim table
   Dim i
   table = "abcdefghijklmnopqrstuvwxzy0123456789"
   Randomize
   For i = 0 To UBound(ret)
    ret(i) = Mid(table, Int(Rnd() * Len(table) + 1), 1)
   Next
   GetBoundary = "---------------------------" & Join(ret, Empty)
  End Function  

  '添加文本域的名稱和值
  Public Sub AddForm(ByVal strName, ByVal strValue)
   Dim tmp
   tmp = "\r\n--$1\r\nContent-Disposition: form-data; name=""$2""\r\n\r\n$3"
   tmp = Replace(tmp, "\r\n", vbCrLf)
   tmp = Replace(tmp, "$1", strBoundary)
   tmp = Replace(tmp, "$2", strName)
   tmp = Replace(tmp, "$3", strValue)
   objTemp.Write StringToBytes(tmp, strCharset)
  End Sub
  
  '指定字符集的字符串轉字節數組
  Public Function StringToBytes(ByVal strData, ByVal strCharset)
   Dim objFile
   Set objFile = CreateObject("ADODB.Stream")
   objFile.Type = adTypeText
   objFile.Charset = strCharset
   objFile.Open
   objFile.WriteText strData
   objFile.Position = 0
   objFile.Type = adTypeBinary
   If UCase(strCharset) = "UNICODE" Then
    objFile.Position = 2 'delete UNICODE BOM
   ElseIf UCase(strCharset) = "UTF-8" Then
    objFile.Position = 3 'delete UTF-8 BOM
   End If
   StringToBytes = objFile.Read(-1)
   objFile.Close
   Set objFile = Nothing
  End Function

  '設置文件域的名稱/文件名稱/文件MIME類型/文件路徑或文件字節數組
  Public Sub AddFile(ByVal strName, ByVal strFilePath)
   Dim tmp, strFileName, strFileType, strExt   
   
   With CreateObject("Scripting.FileSystemObject")
    If .FileExists(strFilePath) Then
     strFileName = .GetFileName(strFilePath)
     strExt = .GetExtensionName(strFilePath)
    End IF
   End With
   
   With CreateObject("Scripting.Dictionary")
    .Add "php", "application/x-php"
    .Add "vbs", "application/x-vbs"
    .Add "jpe", "image/jpeg"
    .Add "jpg", "image/jpeg"
    .Add "jpeg", "image/jpeg"
    .Add "gif", "image/gif"
    .Add "png", "image/png"
    .Add "bmp", "image/bmp"
    .Add "ico", "image/x-icon"
    .Add "svg", "image/svg+xml"
    .Add "svgz", "image/svg+xml"
    .Add "tif", "image/tiff"
    .Add "tiff", "image/tiff"
    .Add "pct", "image/x-pict"
    .Add "psd", "image/vnd.adobe.photoshop"
    .Add "aac", "audio/x-aac"
    .Add "aif", "audio/x-aiff"
    .Add "flac", "audio/x-flac"
    .Add "m4a", "audio/x-m4a"
    .Add "m4b", "audio/x-m4b"
    .Add "mid", "audio/midi"
    .Add "midi", "audio/midi"
    .Add "mp3", "audio/mpeg"
    .Add "mpa", "audio/mpeg"
    .Add "mpc", "audio/x-musepack"
    .Add "oga", "audio/ogg"
    .Add "ogg", "audio/ogg"
    .Add "ra", "audio/vnd.rn-realaudio"
    .Add "ram", "audio/vnd.rn-realaudio"
    .Add "snd", "audio/x-snd"
    .Add "wav", "audio/x-wav"
    .Add "wma", "audio/x-ms-wma"
    .Add "avi", "video/x-msvideo"
    .Add "divx", "video/divx"
    .Add "flv", "video/x-flv"
    .Add "m4v", "video/mp4"
    .Add "mkv", "video/x-matroska"
    .Add "mov", "video/quicktime"
    .Add "mp4", "video/mp4"
    .Add "mpeg", "video/mpeg"
    .Add "mpg", "video/mpeg"
    .Add "ogm", "application/ogg"
    .Add "ogv", "video/ogg"
    .Add "rm", "application/vnd.rn-realmedia"
    .Add "rmvb", "application/vnd.rn-realmedia-vbr"
    .Add "smil", "application/x-smil"
    .Add "webm", "video/webm"
    .Add "wmv", "video/x-ms-wmv"
    .Add "xvid", "video/x-msvideo"
    .Add "js", "application/javascript"
    .Add "xml", "text/xml"
    .Add "html", "text/html"
    .Add "css", "text/css"
    .Add "txt", "text/plain"
    .Add "py", "text/x-python"
    .Add "pdf", "application/pdf"
    .Add "xhtml", "application/xhtml+xml"
    .Add "zip", "application/x-zip-compressed, application/zip"
    .Add "rar", "application/x-rar-compressed"
    .Add "cmd", "application/cmd"
    .Add "bat", "application/x-bat, application/x-msdos-program"
    .Add "exe", "application/exe, application/x-ms-dos-executable"
    .Add "msi", "application/x-msi"
    .Add "bin", "application/x-binary"
    .Add "crt", "application/x-x509-ca-cert"
    .Add "crl", "application/x-pkcs7-crl"
    .Add "pfx", "application/x-pkcs12"
    .Add "p12", "application/x-pkcs12"
    .Add "odc", "application/vnd.oasis.opendocument.chart"
    .Add "odf", "application/vnd.oasis.opendocument.formula"
    .Add "odb", "application/vnd.oasis.opendocument.database"
    .Add "odg", "application/vnd.oasis.opendocument.graphics"
    .Add "odi", "application/vnd.oasis.opendocument.image"
    .Add "odp", "application/vnd.oasis.opendocument.presentation"
    .Add "ods", "application/vnd.oasis.opendocument.spreadsheet"
    .Add "odt", "application/vnd.oasis.opendocument.tex"
    .Add "docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
    .Add "dotx", "application/vnd.openxmlformats-officedocument.wordprocessingml.template"
    .Add "potx", "application/vnd.openxmlformats-officedocument.presentationml.template"
    .Add "ppsx", "application/vnd.openxmlformats-officedocument.presentationml.slideshow"
    .Add "pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation"
    .Add "xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
    .Add "xltx", "application/vnd.openxmlformats-officedocument.spreadsheetml.template"
    .Add "ppam", "application/vnd.ms-powerpoint.addin.macroEnabled.12"
    .Add "ppa", "application/vnd.ms-powerpoint"
    .Add "potm", "application/vnd.ms-powerpoint.template.macroEnabled.12"
    .Add "ppsm", "application/vnd.ms-powerpoint.slideshow.macroEnabled.12"
    .Add "xlsm", "application/vnd.ms-excel.sheet.macroEnabled.12"
    .Add "pptm", "application/vnd.ms-powerpoint.presentation.macroEnabled.12"
    .Add "dotm", "application/vnd.ms-word.template.macroEnabled.12"
    .Add "docm", "application/vnd.ms-word.document.macroEnabled.12"
    .Add "doc", "application/msword"
    .Add "dot", "application/msword"
    .Add "pps", "application/mspowerpoint"
    .Add "ppt", "application/mspowerpoint,application/powerpoint,application/vnd.ms-powerpoint,application/x-mspowerpoint"
    .Add "xls", "application/vnd.ms-excel"
    .Add "xlt", "application/vnd.ms-excel"

    strFileType = .Item(LCase(strExt))
   End With
   
   tmp = "\r\n--$1\r\nContent-Disposition: form-data; name=""$2""; filename=""$3""\r\nContent-Type: $4\r\n\r\n"
   tmp = Replace(tmp, "\r\n", vbCrLf)
   tmp = Replace(tmp, "$1", strBoundary)
   tmp = Replace(tmp, "$2", strName)
   tmp = Replace(tmp, "$3", strFileName)
   tmp = Replace(tmp, "$4", strFileType)
   
   objTemp.Write StringToBytes(tmp, strCharset)
   objTemp.Write GetFileBinary(strFilePath)
  End Sub
  
  '獲取文件內容的字節數組
  Private Function GetFileBinary(ByVal strPath)
   Dim objFile
   Set objFile = CreateObject("ADODB.Stream")
   objFile.Charset = strCharset
   objFile.Type = adTypeBinary
   objFile.Open   
   objFile.LoadFromFile strPath
   GetFileBinary = objFile.Read(-1)
   objFile.Close
   Set objFile = Nothing
  End Function
  
  Public Sub AddHeader(ByVal strName, ByVal strValue)
   xmlHttp.setRequestHeader strName, strValue
  End Sub
  
  '上傳到指定的URL,并返回服務器應答
  Public Function Upload()
   Call AddEnd   
   xmlHttp.setRequestHeader "Content-Type", "multipart/form-data; boundary=" & strBoundary
   'xmlHttp.setRequestHeader "Content-Length", objTemp.size   
   xmlHttp.Send objTemp
   Upload = xmlHttp.responseText
  End Function
  
  '設置multipart/form-data結束標記
  Private Sub AddEnd()
   Dim tmp
   tmp = "\r\n--$1--\r\n"
   tmp = Replace(tmp, "\r\n", vbCrLf)
   tmp = Replace(tmp, "$1", strBoundary)
   objTemp.Write StringToBytes(tmp, strCharset)
   objTemp.Position = 2
  End Sub
 End Class
]]>
</script>
</job>
</package>

參考資料:

  1. HTTP協議之multipart/form-data請求分析
  2. VBS模拟POST上传文件
  3. File updload in post form in VBS
  4. Issues running JScript or VBScript files with UTF-8 encoding thru Windows Script Host
  5. WSF - Windows Script File XML Format
  6. XML
  7. CDATA
  8. Call 语句

2021年12月20日 星期一

用VBScript 讀本地Outlook信件檔案的收件者

這邊紀錄下利用 VBscript 來
  1. 讀取本地端的 Outlook msg 檔案資訊,移如信件的收信者資訊。
  2. 讀取本地 Outlook 收件夾(或刪除的郵件、寄件備份等資料夾)裡的信件資訊。
以下先上程式碼:
<package>
<job id=XXX>

<script language="VBScript">

parOutlookMsgFile("D:\testOutlookMail.msg")
parseOutlookInboxFolder(6)

'''''''''''''''''''''''''''''''''''''''''''''''''''''

Sub parOutlookMsgFile(msgFilePath)
Dim objOutlook
'Dim objInBoxFolder
'Dim objNameSpace

Set objOutlook = CreateObject("Outlook.Application")
'Set objNameSpace = objOutlook.GetNamespace("MAPI")
'Set objInBoxFolder = objNameSpace.GetDefaultFolder(6)

Dim mail, recips, recip, email_single, pa
Set mail = objOutlook.CreateItemFromTemplate(msgFilePath)
Set recips = mail.Recipients

For Each recip In recips
    Set pa = recip.PropertyAccessor
    email_single = pa.GetProperty("http://schemas.microsoft.com/mapi/proptag/0x39FE001E")
             
    WScript.Echo "Receiver Name: " & recip.Name & ", Receiver Email: " & email_single & ", type: " & recip.Type
Next
End Sub

'''''''''''''''''''''''''''''''''''''''''''''''''''''
Sub parseOutlookInboxFolder(inboxFolderType)
'inboxFolderType:
' received mail inbox: 6
' deleted mail inbox : 3
' sent mail inbox : 5
Dim objOutlook, objInBoxFolder, objNameSpace, objMailItems

Set objOutlook = CreateObject("Outlook.Application")
Set objNameSpace = objOutlook.GetNamespace("MAPI")
Set objInBoxFolder = objNameSpace.GetDefaultFolder(inboxFolderType)

Set objMailItems = objInBoxFolder.Items
Dim i 
i = 1
Dim totalMailCount
totalMailCount = objMailItems.count
While i <= totalMailCount
		Set objMail = objMailItems.Item(i)
		WScript.Echo objMail.Subject
		i = i + 1
Wend

End Sub

</script>

</job>
</package>

說明:
程式碼中有兩個函式,分別是用來讀取單一 Msg 檔資訊的 parOutlookMsgFile()
和 讀取 Outlook 收件夾(或刪除的郵件、寄件備份等資料夾)裡的信件資訊的 parseOutlookInboxFolder()。

在 parOutlookMsgFile() 中,recip.Type 可能有 1 或 2 兩種值, 
Type = 1 代表一般收件者,
Type = 2 代表 cc 副本的收件者。

parseOutlookInboxFolder() 函式可以接收收件夾 type (OlDefaultFolders 形別) 的值,
其中 6 代表收件夾,
3 代表刪除的郵件,

VBA parse JSON 的方法

VBScript 目前沒有能夠直接解析 JSON 語句的能力,
但是 Javascript 可以,
所以在我們的 VBA 需要解析 JSON 時,
可以移用 WSF 檔可以同時存在 VBScript 和 Javascript 的特性,
讓 Javascript 幫助 VBScript 解析 JSON,
以下給出一個範例:

jsonParseTest.wsf:
<package>
<job id=XXX>

<script language="JScript">
 Array.prototype.get = function(x) { return this[x]; };
 function parseJSON(jsonStr){
  return eval("(" + jsonStr + ")");
 }
</script>

<script language="VBScript">
 Dim jsonStr, jsonObj
 jsonStr = "{a:""aaa"", b:{ name:""bb"", value:""text"" }, c:[""item0"", ""item1"", ""item2""]}"
 Set jsonObj = parseJSON(jsonStr)
        WScript.Echo((jsonObj.c).get(0)) ' "item0"
        WScript.Echo(jsonObj.a) ' "aaa"
        WScript.Echo(jsonObj.b.name) ' "bb"
</script>

</job>
</package>

接著打開 Windows 的命令例樣式 cmd Console,
輸入 (假設 jsonParseTest.wsf 放D槽)
wscript (WScript.Echo 會用彈跳視窗的方式顯示內容) 
或 cscript (WScript.Echo 會將內容顯示在命令列(cmd)視窗中) :
wscript D:\jsonParseTest.wsf
cscript D:\jsonParseTest.wsf
結果如下圖:

2021年12月15日 星期三

使用 Java 上傳檔案(發送 enctype=multipart/form-data 的 HttpPost)

這篇記綠下如何使用 Java 上傳檔案 (發送 enctype=multipart/form-data 的 HttpPost)。

先建立一個簡單的檔案接收伺服器以利驗證上傳功能的正確性。
再直接使用 Java 進行 enctype=multipart/form-data 的 HpptPost 上傳檔案,
順便再傳送一個文字參數測試文字參數的傳遞功能是否也正確。

以下直接上程式碼,使用 jdk11,Tomcat 9.0
首先是範例有用到的 Maven Lib Dependency 如下:
<dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>
    
    <!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->
	<dependency>
	    <groupId>javax.servlet</groupId>
	    <artifactId>javax.servlet-api</artifactId>
	    <version>4.0.1</version>
	    <scope>provided</scope>
	</dependency>
	
	<!-- https://mvnrepository.com/artifact/javax.servlet.jsp/javax.servlet.jsp-api -->
	<dependency>
	    <groupId>javax.servlet.jsp</groupId>
	    <artifactId>javax.servlet.jsp-api</artifactId>
	    <version>2.3.3</version>
	    <scope>provided</scope>
	</dependency>
	
	<!-- https://mvnrepository.com/artifact/javax.servlet.jsp.jstl/jstl-api -->
	<dependency>
	    <groupId>javax.servlet.jsp.jstl</groupId>
	    <artifactId>jstl-api</artifactId>
	    <version>1.2</version>
	</dependency>
 
	<!-- https://mvnrepository.com/artifact/org.apache.httpcomponents/httpclient -->
	<dependency>
	    <groupId>org.apache.httpcomponents</groupId>
	    <artifactId>httpclient</artifactId>
	    <version>4.5.13</version>
	</dependency>
	
	<!-- https://mvnrepository.com/artifact/org.apache.httpcomponents/httpmime -->
	<dependency>
	    <groupId>org.apache.httpcomponents</groupId>
	    <artifactId>httpmime</artifactId>
	    <version>4.5.13</version>
	</dependency>
	
	<!-- https://mvnrepository.com/artifact/commons-httpclient/commons-httpclient -->
	<!-- not maintain anymore, can be replaced by org.apache.httpcomponents -->
	<dependency>
	    <groupId>commons-httpclient</groupId>
	    <artifactId>commons-httpclient</artifactId>
	    <version>3.0.1</version>
	</dependency>
		
	<!-- https://mvnrepository.com/artifact/com.jfinal/cos -->
	<dependency>
	    <groupId>com.jfinal</groupId>
	    <artifactId>cos</artifactId>
	    <version>2020.4</version>
	</dependency>
	
  </dependencies>
接著建立一個簡單的 Servlet 來接收檔案 httpPost:
UploadFileAction.java:
import java.io.File;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.oreilly.servlet.MultipartRequest;
import com.oreilly.servlet.multipart.DefaultFileRenamePolicy;

/**
 * Servlet implementation class UploadFileAction
 */
@WebServlet("/uploadFile.do")
public class UploadFileAction extends HttpServlet {
	private static final long serialVersionUID = 1L;

    /**
     * Default constructor. 
     */
    public UploadFileAction() {
        //
    }

	/**
	 * @see HttpServlet#service(HttpServletRequest request, HttpServletResponse response)
	 */
	protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		String dirSaveFilePath = "D:\\tempUploadedFile";
		if(!new File(dirSaveFilePath).exists()) {
			new File(dirSaveFilePath).mkdirs();
		}		
		MultipartRequest multipartRequest = new MultipartRequest(request, dirSaveFilePath, 100 * 1024 * 1024, "UTF-8", new DefaultFileRenamePolicy());
		System.out.println("ContentType: " + multipartRequest.getContentType("uploadedFile"));
		System.out.println("OriginalFileName: " + multipartRequest.getOriginalFileName("uploadedFile"));
		System.out.println("FileSystemName: " + multipartRequest.getFilesystemName("uploadedFile"));
		System.out.println("param1: " + multipartRequest.getParameter("param1"));
	}

	/**
	 * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
	 */
	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		service(request, response);
	}

	/**
	 * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
	 */
	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		service(request, response);
	}

}



最後是會進行檔案上傳的一個很單純的 Java,
這邊我演示了兩個不同的檔案上傳方式,
uploadFile_1():
使用了 org.apache.httpcomponents (較新,取代 commons-httpclient) 的 CloseableHttpClient 和 PostMethod

uploadFile_2():
使用了 commons-httpclient (已不再維護,由 commons-httpclient 取代) 的 HttpClient 和 PostMethod

org.apache.httpcomponents 和 commons-httpclient 的關係可參考
The Commons HttpClient project is now end of life, and is no longer being developed. It has been replaced by the Apache HttpComponents project in its HttpClient and HttpCore modules, which offer better performance and more flexibility.


UploadFileTestjava:
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URLConnection;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;

import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.multipart.FilePart;
import org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity;
import org.apache.commons.httpclient.methods.multipart.Part;
import org.apache.commons.httpclient.methods.multipart.StringPart;
import org.apache.commons.httpclient.util.EncodingUtil;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.mime.HttpMultipartMode;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.entity.mime.content.FileBody;
import org.apache.http.entity.mime.content.StringBody;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.protocol.HTTP;
import org.apache.http.util.EntityUtils;

public class UploadFileTest {

	public static void main(String[] args) throws FileNotFoundException, IOException {
		uploadFile_1("C:\\Users\\Hugo\\Pictures\\未命名.png", "http://localhost:8080/uploadFile.do");
		uploadFile_2("C:\\Users\\Hugo\\Pictures\\未命名.png", "http://localhost:8080/uploadFile.do");
	}

	public static void uploadFile_1(String filePath, String uploadTo) throws FileNotFoundException, IOException {
		File file = new File(filePath);
		HttpPost httpPost = new HttpPost(uploadTo);

		MultipartEntityBuilder multipartEntityBuilder = MultipartEntityBuilder.create();
		//Use RFC6532 mode to avoid encoding Mojibake (garbled text) of fileName
		//使用 RFC6532 來避免中文等的檔案名稱在傳遞後變成亂碼
		multipartEntityBuilder.setMode(HttpMultipartMode.RFC6532); 
		
		multipartEntityBuilder.addPart("uploadedFile", new FileBody(file, ContentType.create(URLConnection.guessContentTypeFromName(file.getName())) , file.getName()));
//		multipartEntityBuilder.addPart("uploadedFile", new FileBody(file, ContentType.APPLICATION_OCTET_STREAM, file.getName()));		
//		multipartEntityBuilder.addBinaryBody("uploadedFile", file, ContentType.APPLICATION_OCTET_STREAM, file.getName());
				
		multipartEntityBuilder.addPart("param1", new StringBody("中文", ContentType.TEXT_PLAIN.withCharset(StandardCharsets.UTF_8)));
//		multipartEntityBuilder.addTextBody("param1", "中文", ContentType.TEXT_PLAIN.withCharset(StandardCharsets.UTF_8));

		HttpEntity httpEntity = multipartEntityBuilder.build();
		httpPost.setEntity(httpEntity);

		try (CloseableHttpClient httpClient = HttpClients.createDefault();
			) {
				CloseableHttpResponse httpResponse = httpClient.execute(httpPost);
				String responseStr = EntityUtils.toString(httpResponse.getEntity());
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	public static void uploadFile_2(String filePath, String uploadTo) {
		File f = new File(filePath);
		PostMethod filePost = new PostMethod(uploadTo);
		filePost.getParams().setContentCharset("UTF-8");
//		filePost.addRequestHeader("xx", "xxx"); // if you want to add any header

		try {
			Part[] parts = { 
					  new StringPart("param1", "中文", "UTF-8")
					, new FilePart("uploadedFile", f) {
							@Override
							protected void sendDispositionHeader(OutputStream out) throws IOException {
								// override 掉sendDispositionHeader 方法以解決中文 fileName 傳送後會變成亂碼的問題
								// 在原始碼中是用 getAsciiBytes(fileName),我們這裡改用utf-8 去 encode
								// super.sendDispositionHeader(out) is overridden, use UTF-8 instead of Ascii to
								// encode filename
								
								// run code copied form Part.sendDispositionHeader() directly
								out.write(CONTENT_DISPOSITION_BYTES);
								out.write(QUOTE_BYTES);
								out.write(EncodingUtil.getAsciiBytes(getName()));
								out.write(QUOTE_BYTES);
				
								String filename = getSource().getFileName();
								if (filename != null) {
									out.write(EncodingUtil.getAsciiBytes(FILE_NAME));
									out.write(QUOTE_BYTES);
									out.write(EncodingUtil.getBytes(filename, "utf-8"));
									out.write(QUOTE_BYTES);
								}
							}
					} 
			};
			filePost.setRequestEntity(new MultipartRequestEntity(parts, filePost.getParams()));
			HttpClient client = new HttpClient();
			int status = client.executeMethod(filePost);
			String responseStr = filePost.getResponseBodyAsString();
		}catch(IOException e) {
			e.printStackTrace();
		}
	}

}
題外話:
  1. 因為用 Eclipse 的 Maven 外掛產生出的 JAVA EE 專案 (Archetype 選 maven-archetype-webapp) 是用舊版的 servlet 2.3,如果要用新版例如 servlet 3.0, 記得去 web.xml 裡把 <web-app> 裡的設定值改對,servlet 3.0 的設定值可參考這裡, 或是直接把 web.xml 刪掉也可以,因為 web.xml 對 servlet 3.0 並不是必需的。
  2. 因為 Eclipse 可能以為還是使用舊版 servlet 的關係,所以可能無法使用 Eclipse 裡面的相關功能, 例如無法用 Tomcat 啟動專案, 這時需要去修改專案的 Project Facets 設定,將 Dynamic Web Module 設成 3.0 (或以上版本),但有可能會發現無法修改, 這時可以去專案的 .setting 資料夾下找到 org.eclipse.wst.common.project.facet.core.xml, 將裡面的 facet="jst.web" verson 改成 3.0 即可,例如:
    <?xml version="1.0" encoding="UTF-8"?>
    <faceted-project>
      <fixed facet="wst.jsdt.web"/>
      <installed facet="jst.web" version="3.0"/>
      <installed facet="wst.jsdt.web" version="1.0"/>
      <installed facet="java" version="11"/>
    </faceted-project>
參考資料:
  1. HTTPClient PostMethod 中文乱码问题解决方案(2种)
  2. HTML <form> 标签的 enctype 属性
  3. Eclipse創建Maven-Web項目及解決 jre版本和web.xml版本問題
  4. Multipart Upload with HttpClient 4
  5. Getting a File’s Mime Type in Java
  6. Eclipse| 修改dynamic web module 为3.0版本
原始碼分享:

2021年12月10日 星期五

Java執行外部程式, 命令提示字元, Command line

這邊紀錄如何使用 Java 執行久部程式,例如 Windows 的命令提示字元指令 (command line) 或 Linux 的指令。

直接上範例程式碼,
其中 String command 的內容是要執行的指令,範例為 ping www.google.com,
並在最後把指令執行完後的結果輸出。
需要注意的地方是 Windows 和 Linux 的 String[] commands 有一些不同,
Windows 是:
cmd.exe /c "some command"

Linux 是:
/bin/sh -c "some command"

說明:
  1. 需要注意的地方是 Windows 和 Linux 的 String[] commands 有一些不同
  2. 在 jdk 1.5 (或以下版本),

package Main;

import java.io.BufferedReader;
import java.io.InputStreamReader;

public class Main {

	public static void main(String[] args) {		
		String command = "ping www.google.com";
		String[] commands = {"cmd.exe", "/c", command }; // windows command

		String SYSTEM_OS = System.getProperty("os.name");
		if (SYSTEM_OS.toLowerCase().contains("windows")) {
			commands = new String[] {"cmd.exe", "/c", command };
		} else if (SYSTEM_OS.toLowerCase().contains("linux")) { // windows command
			commands = new String[] {"/bin/sh", "-c", command }; // linux command
		}

		try {
			final Process p = Runtime.getRuntime().exec(commands);

			// read error
			BufferedReader errorBufferedReader = new BufferedReader(new InputStreamReader(p.getErrorStream()));
			StringBuffer errorStringBuffer = new StringBuffer();
			String errorLine = "";

	        //In jdk 1.5, errorStream.readLine sometimes stock and never return,
	        //if this happened, you can just close the errorStream directly (don't read the response) to let inputStream can read regular response.
			while ((errorLine = errorBufferedReader.readLine()) != null) {
				errorStringBuffer.append(errorLine + "\n");
			}
			if (errorStringBuffer.length() > 0) {
				errorStringBuffer.deleteCharAt(errorStringBuffer.length() - 1);
			}
			errorBufferedReader.close();

			// read regular output
			BufferedReader reqularOuputBufferedReader = new BufferedReader(new InputStreamReader(p.getInputStream()));
			StringBuffer reqularOuputStringBuffer = new StringBuffer();
			String reqularOuputString = "";

			while ((reqularOuputString = reqularOuputBufferedReader.readLine()) != null) {
				reqularOuputStringBuffer.append(reqularOuputString + "\n");
			}
			if (reqularOuputStringBuffer.length() > 0) {
				reqularOuputStringBuffer.deleteCharAt(reqularOuputStringBuffer.length() - 1);
			}
			reqularOuputBufferedReader.close();

			int result = p.waitFor();
			System.out.println("result: " + result);
			System.out.println("reqular output:\n" + reqularOuputStringBuffer.toString());
			System.out.println("error output:\n" + errorStringBuffer.toString());
         /* 
           result: 0
           reqular output:

           Ping www.google.com [2404:6800:4012:3::2004] (使用 32 位元組的資料):
           回覆自 2404:6800:4012:3::2004: 時間=6ms 
           回覆自 2404:6800:4012:3::2004: 時間=6ms 
           回覆自 2404:6800:4012:3::2004: 時間=8ms 
           回覆自 2404:6800:4012:3::2004: 時間=7ms 

           2404:6800:4012:3::2004 的 Ping 統計資料:
               封包: 已傳送 = 4,已收到 = 4, 已遺失 = 0 (0% 遺失),
           大約的來回時間 (毫秒):
               最小值 = 6ms,最大值 = 8ms,平均 = 6ms
           error output:
         */
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

}

參考資料:

2021年12月9日 星期四

Java 自製 Annotation (標註、註解) 範例

紀錄了 Java 的 Annotation (標註、註解)  自製範例。

範例的需求為:

  1. 自定一個 Annotation,名為 CustomAnnotation,其可添加在一個簽名為 public String xxx(String somtText) 的 method 上,例如:
    @CustomAnnotation(info="someInfo")
    public printSomthing(String info){
       //////////////
    }
  2. 我們模擬一個可能的框架,其中希望找出有被標上 CustomAnnotation 的 method,讀出 CustomAnnotation 被設定的 info 值,帶進 method 中的 String 參數並執行之,以上例來說就等於執行:

    printSomthing("someInfo")

為了方便的掃描有被標記 annotation 的 class, method,這裡使用了 org.reflections.reflections 的 lib

Maven dependency:

<dependencies>
  	<!-- https://mvnrepository.com/artifact/org.reflections/reflections -->
	<dependency>
	    <groupId>org.reflections</groupId>
	    <artifactId>reflections</artifactId>
	    <version>0.10.2</version>
	</dependency>  	
</dependencies>
首先是檔案結構:


接著來看每一個 Class 的內容:

otherpackage.Class1.java
package otherpackage;

import customannotation.CustomAnnotation;

public class Class1 {
	
	@CustomAnnotation(info = "Class1 info")
	public static void printInfo(String info) {
		System.out.println("Class1 print info: " + info);
	}
}

otherpackageSubpackage.Class2.java:
package otherpackage.subpackage;

import customannotation.CustomAnnotation;

public class Class2 {
	
	@CustomAnnotation
	public static void printInfo(String info) {
		System.out.println("Class2 print info: " + info);
	}
}


Class1.java 和 Class2.java 沒有做什麼特別的事,只是用來測試我們自製 Annotation (@CustomAnnotation) 的 Class 而已,可以注意到,@CustomAnnotation 都加注在了 method 的上面,並且 Class1.java 有設定 info = "Class1 info" 的 info 參數值,而 Class2.java 沒有設定任何值。

CustomAnnotation.java:
package customannotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CustomAnnotation {
	String info() default "empty info";
}

CustomAnnotation.java 是我們要建立的自製 Annotation,內容非常簡單,只是做了一些基本設定。

main.Test.java:
package main;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Set;

import org.reflections.Reflections;
import org.reflections.scanners.Scanners;
import org.reflections.util.ConfigurationBuilder;

import customannotation.CustomAnnotation;

public class Test {

	public static void main(String[] args) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException {
		Reflections reflections = new Reflections(new ConfigurationBuilder()
														.forPackage("otherpackage")
														.setScanners(Scanners.MethodsAnnotated)
												 );
		Set<Method> foundMethodSet = reflections.getMethodsAnnotatedWith(CustomAnnotation.class);
		for (Method foundMethod : foundMethodSet) {
			Class<?> classDeclareMethod = foundMethod.getDeclaringClass();
			Object objectInstance = classDeclareMethod.getDeclaredConstructor().newInstance();
			
			CustomAnnotation annotation = foundMethod.getAnnotation(CustomAnnotation.class);
			//foundMethod.invoke(objectInstance, annotation.info()); // for non-static method, nned to new a instance
			foundMethod.invoke(null, annotation.info()); //for static method, don't need to new a instance
			//result :
			//Class1 print info: Class1 info
			//Class2 print info: empty info

		}
	}

}

Test 是主要的 Class,在這裡我們使用了 Reflections.getMethosAnnotatedWith() 找出特定 package 路徑下有被加上  CustomAnnotation 註解的 Method,可以再用 Method.getDeclaringClass() 一併找出 Method 是在哪個 Class 裡,
利 用 Method.getAnnotation() 得到設定在 CustomAnnotation 裡的資料,例如 info 屬性的值,
接著我們利用 Java 的反射 (Reflection) 機制來呼叫 Method ,
如果 Method 是 static 的話可以直接用 invoke 呼叫 ,
如果 Method 不是 static 的話,需要配合建立 Method 所屬的 Class 實體 (Instance)。

最後我們可以看到 Class1 的 method 輸出了 "Class print info: Class1 info",
其中 "Class1 info" 是我們在 Class1 中,設定給 CustomAnnotation 的 info 屬性值。
而 Class2 則輸出了 "Class2 print info: empty info",
其中 "empty info" 是我們為 CustomAnnotatoin 設定的 info 屬性預設值。



參考資料:

2021年12月3日 星期五

Java 讀寫檔案文字的幾種方法

在這裡我紀錄了一些 Java 讀寫檔案文字內容的一些方法:
package test;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class FileReadWriteTest {

	public static void main(String[] args) throws IOException {
		String filePath = "D:\\MyTextFileTest.txt";
		new File(filePath).createNewFile();

		writeFileContent_1(filePath, true, "中文字測試");
		writeFileContent_1(filePath, true, "你好哈囉");
		
		System.out.println(readFileContent_1(filePath));
		System.out.println(readFileContent_2(filePath));
	}
	
	/*************** Write File ***************/
	public static void writeFileContent_1(String filePath, boolean isAppend, String contentToWrite) {		
		try (
				FileOutputStream fileOutputStream = new FileOutputStream(new File(filePath), isAppend);
				OutputStreamWriter outputStreamWriter = new OutputStreamWriter(fileOutputStream, "UTF-8");
				BufferedWriter bufferedWriter = new BufferedWriter(outputStreamWriter);
			){
			
			bufferedWriter.write(contentToWrite);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	public static void writeFileContent_2(String filePath, boolean isAppend, String contentToWrite) {
		try {
			if (isAppend) {			
				Files.writeString(Paths.get(filePath), contentToWrite, StandardCharsets.UTF_8, StandardOpenOption.APPEND);			
			}else {
				Files.writeString(Paths.get(filePath), contentToWrite, StandardCharsets.UTF_8);
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
		
	}
	
	/*************** Read File ***************/
	public static String readFileContent_1(String filePath) {
		StringBuffer fileContent = new StringBuffer();

		try (
				FileInputStream fileInputStream = new FileInputStream(new File(filePath));
				InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream, "UTF-8");
				BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
			) {
			
			String readContent = "";
			while ((readContent = bufferedReader.readLine()) != null) {
				fileContent.append(readContent + "\n");
			}
			if (fileContent.length() > 0) {
				//remove the latest added "\n"
				fileContent.deleteCharAt(fileContent.length() - 1);
			}
		} catch (IOException e) {
			e.printStackTrace();
		}

		return fileContent.toString();
	}
	
	public static String readFileContent_2(String filePath) {
		String fileContent = "";
		
		try {
			fileContent = Files.readString(Paths.get(filePath), StandardCharsets.UTF_8);
		} catch (IOException e) {
			e.printStackTrace();
		}
		
		return fileContent;
	}
}

參考資料:

  1. How to read UTF-8 encoded data from a file – Java
  2. How to append text to an existing file in Java?