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?

2021年11月26日 星期五

得到瀏覽器 scrollbar 寬度

Get the Scrollbar Width of an Element

const scrollbarWidth = document.body.offsetWidth - document.body.clientWidth;

參考:

https://www.javascripttutorial.net/dom/css/get-the-scrollbar-width-of-an-element/


Note:

2021/11/26

不知道為什麼,現在發現

document.body.offsetWidth 會等於  document.body.clientWidth,
不過研究了一下 Bootstrap Modal 的程式源碼,

發現可以用

window.innerWidth - document.documentElement.clientWidth

來得到正確的 scrollbarWidth 

原因不明,或許跟 web 標準改變有關??

2021年7月1日 星期四

[MS sql server] 使用 Sql 語法在不同 database 中間倒資料的方法

 在登入一個 sql server下,

可以用以下語法存取另一個 sql  server,

例如以下語法可以登入名為 xxxServer, port 為 1300 的 sql server 

exec sp_addlinkedserver 'myXxxServer', '', 'SQLOLEDB', 'xxxServer,1300'  -- create linked server to xxxServer
exec sp_addlinkedsrvlogin 'myXxxServer', 'false',null, '{帳號}', '{密碼}' -- login xxxServer

然後就可以用 myXxxServer 這個名字 (可自取) 存取 xxxServer,

例如讀取其中的一個名為 xxxTable 的 table,其 table 在名為 xxxDatabase 的 database 中:

SELECT *
FROM myXxxServer.xxxDatabase.dbo.xxxTable 

還可以做到跨 sql server 的 JOIN 等操作,非常方便,例如:

SELECT *
FROM myXxxServer.xxxDatabase.dbo.xxxTable A INNER JOIN
someTable B ON A.a = B.b

或是倒資料,例如:

INSERT INTO A(a1, a2)
SELECT b1, b2
FROM myXxxServer.xxxDatabase.dbo.B

最後再用以下語法登出 xxxServer

exec sp_dropserver 'myXxxServer', 'droplogins' -- free myXxxServer linked server


**補充:

如果要倒的資料欄位裡有主鍵 (Primary Key),

需要把禁止修改主鍵的功能關掉,Update 資料完後再開回來。

再來此時不能用星號 "*" 來代表所有欄位,要把欄位名稱一個個寫出來才能成功寫入。

例如:

SET IDENTITY_INSERT A ON;

INSERT INTO A(a1, a2)
SELECT b1, b2
FROM myXxxServer.xxxDatabase.dbo.B


SET IDENTITY_INSERT A OFF;


如果想要快速的得到欄位名稱的字串,可以用以下語法來得到
(以下為得到名為 A 這個 table 的所有欄位名稱,會用逗號分隔欄位名,最後用成一串字來輸出):

SELECT SUBSTRING(
    (
	SELECT ', ' + QUOTENAME(COLUMN_NAME)
        FROM INFORMATION_SCHEMA.COLUMNS
        WHERE TABLE_NAME = 'A'
        ORDER BY ORDINAL_POSITION
        FOR XML path('')
    )
    , 3, 200000
)

執行結果就像是: [a1], [a2], [a3]

2021年3月30日 星期二

Docker 練習 - 安裝 Tomcat - 設定 virtualBox 的 Port Forwarding

此例使用 Windows 10 + virtualBox + Ubuntu 64bits

# 安裝想要的版本的 tomcat
docker pull tomcat:9.0-jdk11-openjdk

# 啟動 container
docker run -d -p 8080:8080 --name myTomcat tomcat:9.0-jdk11-openjdk

# 因為某些原因 (目前還不清楚),tomcat 的預設歡迎頁面被放到 /usr/local/tomcat/webapps.dist 下而不是 /usr/local/tomcat/webapps 下,
# 所以想要看觀迎頁面可以自己手動把頁面放到 /usr/local/tomcat/webapps 下

# 進入 container 終端 (terminal)
docker exec -it myTomcat bash

# 把觀迎頁面放到 /usr/local/tomcat/webapps 下
cp -r ./webapps.dist/* ./webapps/

# 離開終端
exit

# 在宿主機 (host) 測試是否能得到歡迎頁面的訊息 (應該會是非 404 的頁面)
curl http://localhost:8080


# 如果在 Windows 中想要看到 virtualBox 中的 Linux 的 container 中的歡迎頁面,
# 須要設定 Port Forwarding。
# 我們先要查出"主體 IP" (virtualBox 在 Windows 上使用的網路介面卡上的 IP) 和 "客體 IP" (virtualBox 內部的 Linux 所使用的 內部 IP),然後將它們對應起來。

# 先在 Windows 中使用 ipconfig 指令查出 IP,即"主體 IP",如下圖紅框處


# 再到 virtualBox 中的 Linux 使用 ip address 指今 (hostname -I 也可以)查出"客體 IP",如下圖紅框處

得到"主體 IP"和"客體 IP"後,到 virtualBox 中,
開啟 "設定" --> "網路" --> "附加到" 選 "NAT",按下"連接埠轉送",開始進行設定。
填上剛剛查得的"主體 IP"和"客體 IP"即各自想要對應的 port,"協定" 選擇自己想要的協定,
例如只是要在瀏覽器看到畫面的話,選 TCP 即可,"名稱" 可自訂。

這樣 virtualBox 的 Port Forwarding 就設定好了,可以開始來測試觀迎畫面。
在 Windows 中打開瀏覽器,網址輸入
http://{"主體 IP"}:{port}
例如此例為:
http://192.168.56.1:8080
就可以看到 Tomcat 的歡迎畫面了,如下圖:


參考資料:

Linux - Docker 和 docker-compose 的好用指令記錄

使用 Linux Ubuntu 為例子。

# 安裝 docker 
apt-get install docker.io 
--------------------------------------------------------------

Docker 好用指令 :

# 觀看 Docker 啟動狀態
service docker status

# 啟動 Docker
start docker

# 關閉 Docker
stop docker

# 重啟 Docker
restart docker

----------------------- Image 相關 --------------------
#搜尋 image (映像檔)
docker search {關鍵字}
Ex: docker search ubuntu

# 例出所有 image
docker images

# 下載 XXX image (如要下載最新版,version (在 docker 中又稱 tag) 可省略)
docker pull {image 名}:{version}
Ex: docker pull tomcat:9.0

# 刪除 Image
docker rmi {某個 images}

----------------------- Volume 相關 --------------------
# 建立一個具名 Volume (不給名字會自動幫你建一個亂數名字的 Volume)
docker volume create {自己取的 Volume 名字}

# 列出所有**具名**的 Volume 的資訊,非具名的 Volume 不會在這顯示
(例如在 run container 時直接用 -v {宿主機路徑}:{container 路徑} 指定的 Volume)
docker volume list

# 列出特定 Volume 的詳細資訊
docker volume inspect {Volume 名字}

# 移除 Volume
docker volume rm {Volume 名字}

----------------------- Container 相關 --------------------
# ps [-a] :(process stauts ??) 例出所有運行中的 container (容器),如果要列出全部 (包含非運行的)的 container,可以加上 -a
docker ps
docker ps -a

# rm :(remove) 刪除某 container
docker rm {某 container 的 id 或 name}

# 查看某個 container 的 log
docker logs XXX

# 開啟一個或多個已停止的 container
docker start {container} {container} ......

# 停止關閉某個 container
docker stop XXX

# 重啟某個 container (等同 docker stop + docker start)
docker restart XXX

# cp : 從宿主機複制檔案或資料夾到 container,或從 container 複制檔案或資料夾到 宿主機,container 的路徑前面要加 container 名,並用冒號 (:) 分隔
docker cp {來源路徑} {目標路徑}
Ex: docker cp /home/xxx xxContainer:/user/...

# exec : 在已運行的 container 上執行命令
docker exec {參數} {某個container} {命令}
Ex: docker exec -it mysql bash

#其他可選參數:
# -w : work directory,可設定一開始所在的 container 內部 path
docker exec -w {container 中的路徑} {某個 contenr} {命令}
Ex: docker exec -i -w /some/path/inside/container tomcat pwd
像述指令也可用 bash -c "cd ... && ..." 如以下方法達成:
docker exec -i tomcat bash -c "cd /some/path/inside/container && pwd"

----------------------- 運行 Container 相關 --------------------
# 以 image 建立 container
docker create {image}

# 以 XXX images 運行 container
docker run XXX

# docker run 的各種參數 :

# --name :為要運行的 container 取名
docker run --name {自己取的名字} {某個 container}

# -p : (expose 的縮寫),暴露 container 的 port 並對應到指定的宿主機的 port,
docker run -p {宿主機的port}:{container 的 port} XXX
Ex: docker run -p 8081:8080 tomcat

# -i -t 或 --interactive --tty 或 -it :(stdin和 pueudo-tty 功能的組合) 運行 container 並進入 command line 互動介面
-t:attach時Container的螢幕會接到原來的螢幕上。
-i:attach時鍵盤輸入會被Container接手
docker run -i -t XXX
#或
docker run -it XXX

#離開互動介面
exit

# -d :Detached 的縮寫,以分離模式運行 container,進行背景常駐執行,即不進入 Container 的 命令提示界面,例如像 Daemon 這種常駐程式
docker run -d ubuntu /bin/sh -c "while true; do echo hello world; sleep 1; done"

# -v 或 --volume :分享資料夾,可掛載宿主機 (指運行 Docker 的 Linux) 的某資料夾路徑到 container 的某資料夾路徑,用於持久化檔案,避免檔案因 container 關閉而消失,有幾種寫法

# (掛載特定宿主機資料夾路徑 或 某具名 Volume) 到 container 資料夾路徑
docker -v {宿主機資料夾路徑 或 某具名 Volume}:{container 資料夾路徑}
Ex: docker -v /home/tomcat:/usr/local/tomcat/webapps

--rm : (clean up),可在 container exit 後自動刪除 container 
docker run -it --rm -p 8888:8080 tomcat:9.0
----------------------------------------------
在 Container 中,可用
host.docker.internal
做為 domain  來連至本地端,
此法僅適於 Docker Desktop for Windows,並且通常為開發測試時使用

好用連結:

---------------------- docker-compose 好用指令 ---------------------------------------------------------------
# 依據 docker-compose.yml 的設定 重新 build image 並 start service (如果已經 start 就重新 start) 某個 service
docker-compose up --build -d --no-deps <service 名稱>


參考:

2021年3月29日 星期一

Linux - virtual box share folder 的設定

 在 Windows 中,使用 virtual box 安裝好 Ubuntu 64 server 後,

如果想方便的分享 Windows 的資料夾給 Virtual box 開啟的 VM 使用,

可以使用 VirtualBox 的 "共用資料夾" 功能。

下面示範步驟

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

安裝 VirtualBox Guest Additions :

在 Virutalbox 開啟的 VM 視窗上選擇 "裝置" --> "插入 Guest Additions CD 像"


在 VM 命令列視窗中找指令

建立資料夾 (資料夾可自訂) : mkdir -p /mnt/cdrom

掛載光碟機上去 : mount /dev/cdrom /mnt/cdrom

進入光碟機:cd /mnt/cdrom

安裝 VirtualBox Guest Additions: sh ./VBoxLinuxAdditions.run --nox11

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

接著先把 VM 關機,在 Virtualbox 上對此VM做共用資料夾的設定:"設定" --> "共用資料夾" 


其中,"資料夾路徑" 為 Windows 中要分享給 VM 的路徑,
"資料夾名稱" 為VM 要掛載的名稱 (可自訂),
"掛載點"為VM中對應的分享資料夾路徑 (可自訂)。
接著開啟 VM 並輸入指令:
掛載分享資料夾:sudo mount -t vboxsf {資料夾名稱} {掛載點}
Ex: sudo mount -t vboxsf virtualBoxShareFolder /home/virtualBoxShareFolder

2021年2月7日 星期日

Minecraft Java 版 (1.16.4, jdk 1.8) 自製 Forge Mod - 實作物品、合成表、物品特殊效果

繼上次
後,這次我們要練習自制物品,其中會包括物品的建立、名字的翻譯、相應的合成表和物品特殊效果之類的實作。

這次要建立的是一個叫做 "超級劍 (Super Sword)" 的物品,其有以下特性:
  1. 其物品類別為 "戰鬥 (Combat)" 物品。 
  2. 其有自己的名字 (英文叫 Super Sword,中文叫超級劍)。
  3. 其為"劍 (Sword)"這個物品的客制版本,有自己的攻擊傷害等數值。
  4. 其有自己的2D圖示,並拿此圖示做為 3D 時的樣子 (例如玩家拿在手上的樣子)。
  5. 其被玩家拿在手上時,有自己效果,此例為玩家不會受到攻擊及效果傷害,並且攻擊者會受到最大生命值一半的傷害。
實作版本:
Minecraft Java 版 - 1.16.4
JDK1.8
forge-1.16.4-35.1.4-mdk

先來看一下最後成品的資料結構,在這裡不會重頭無中生有的撰寫程式碼,
我會直接拿這篇文章
的程式碼拿來修改:



首先是建立 SuperSword 的物件,
main.java.com.my.mode.item.SuperSword.java :
package com.my.mode.item;

import net.minecraft.entity.LivingEntity;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.item.IItemTier;
import net.minecraft.item.Item;
import net.minecraft.item.ItemGroup;
import net.minecraft.item.Rarity;
import net.minecraft.item.SwordItem;
import net.minecraft.item.crafting.Ingredient;
import net.minecraft.util.DamageSource;
import net.minecraft.world.World;
import net.minecraftforge.event.entity.living.LivingAttackEvent;
import net.minecraftforge.event.entity.living.LivingHurtEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;

public class SuperSword extends SwordItem{

	public final static String ITEM_ID = "super_sword"; 
	
	public SuperSword() {
		super(new IItemTier() {

			@Override
			public int getMaxUses() {
				// TODO Auto-generated method stub
				return 100;
			}

			@Override
			public float getEfficiency() {
				// TODO Auto-generated method stub
				return 0;
			}

			@Override
			public float getAttackDamage() {
				// TODO Auto-generated method stub
				return 100;
			}

			@Override
			public int getHarvestLevel() {
				// TODO Auto-generated method stub
				return 0;
			}

			@Override
			public int getEnchantability() {
				// TODO Auto-generated method stub
				return 0;
			}

			@Override
			public Ingredient getRepairMaterial() {
				// TODO Auto-generated method stub
				return null;
			}
			
		}, 100, 1, (new Item.Properties()).group(ItemGroup.COMBAT));
	}

	@SubscribeEvent
	public void onLivingAttackEvent(LivingAttackEvent event) {
		LivingEntity livingEntity = event.getEntityLiving();
		World world = livingEntity.getEntityWorld();
		if(!world.isRemote && livingEntity instanceof PlayerEntity) {
			PlayerEntity playerEntity = (PlayerEntity) livingEntity;
			if (playerEntity.getHeldItemMainhand().getItem() instanceof SuperSword
			 || playerEntity.getHeldItemOffhand().getItem() instanceof SuperSword) {
				//把事件取消,效果為攻擊無效,不會愛傷
				event.setCanceled(true);
				
				//增加效果,效果為對攻擊玩家 (player) 的生物造成傷害
				if (event.getSource().getTrueSource() instanceof LivingEntity) {
					LivingEntity attacker = (LivingEntity) event.getSource().getTrueSource();			
					attacker.attackEntityFrom(DamageSource.GENERIC, attacker.getMaxHealth() / 2);
				}
			}
		}
	}
	
	@SubscribeEvent
	public void onLivingAttackEvent(LivingHurtEvent event) {
		LivingEntity livingEntity = event.getEntityLiving();
		World world = livingEntity.getEntityWorld();

		if(!world.isRemote && livingEntity instanceof PlayerEntity) {
			PlayerEntity playerEntity = (PlayerEntity) livingEntity;
			if (playerEntity.getHeldItemMainhand().getItem() instanceof SuperSword
			 || playerEntity.getHeldItemOffhand().getItem() instanceof SuperSword) {
				//把事件取消,效果為傷害無效,不會愛傷
				event.setCanceled(true);
			}
		}
	}
}


在 SuperSword.java 的建構子 (constructor) 中,SuperSword 繼承了 SwordItem,
所以其實本質就是一個 "劍 (Sword)" 物品,
在建構子中,我們用
net.minecraft.item.SwordItem.SwordItem(IItemTier tier, int attackDamageIn, float attackSpeedIn, Properties builderIn)
重新定義了 SuperSword 的攻擊傷害 (attackDamageIn)、攻擊速度 (attackSpeedIn) 和物品類別 (ItemGroup.COMBAT)。

再來我們實作兩個監聽事件的 method,分別為 onLivingAttackEvent()、onLivingAttackEvent() 來監聽 LivingAttackEvent (當生物被攻擊時觸發)、onLivingAttackEvent (當生物被傷害時觸發)。
在 method 中,先取得事件的對象,即被攻擊、被傷害的對象,
LivingEntity livingEntity = event.getEntityLiving();
如果對象為玩家 (PlayerEntity) 的話,再來判斷玩家手上是否拿著 SuperSword 物品
if (playerEntity.getHeldItemMainhand().getItem() instanceof SuperSword
			 || playerEntity.getHeldItemOffhand().getItem() instanceof SuperSword)
如果玩家拿著 SuperSword,則將事件取消,而
LivingAttackEvent 各 LivingHurtEvent 被取消的效果為:事件的對象不會受到攻擊、受傷的傷害。

在 onLivingAttackEvent() 裡我們使用
if (event.getSource().getTrueSource() instanceof LivingEntity) {
     LivingEntity attacker = (LivingEntity) event.getSource().getTrueSource();			
     attacker.attackEntityFrom(DamageSource.GENERIC, attacker.getMaxHealth() / 2);
}
                
再多取得攻擊者物件,判斷攻擊者是否為 LiveingEntity 後,
對其產生了一個普通傷害,傷害為攻擊者最大生命值的一半。

建立設計好 SuperSword.java 後,再來是要把 SuperSword 物品註冊到遊戲中 (先不管名字和外觀),首先先建立一個工具類, Items.java
  
main.java.com.my.mode.item.Items.java :
package com.my.mode.item;

import com.my.mode.MyMod;

import net.minecraft.item.Item;
import net.minecraftforge.fml.RegistryObject;
import net.minecraftforge.registries.DeferredRegister;
import net.minecraftforge.registries.ForgeRegistries;

public class Items {
	private static DeferredRegister<Item> REGISTER = null;
	
	public static DeferredRegister<Item> getRegister(){
		if (REGISTER == null) {
			REGISTER = DeferredRegister.create(ForgeRegistries.ITEMS, MyMod.MOD_ID);
		}		
		//注冊自製物品
		REGISTER.register(SuperSword.ITEM_ID, () -> new SuperSword());
		
        return REGISTER;
    }
}

在 Items.java 裡, 我們會使用 DeferredRegister 來將 SuperSword 註冊進去。
接著我們修改一下 ModEventBusHandler.java

main/java/com/my/mode/ModEventBusHandler.java :
package com.my.mode;

import com.my.mode.item.Items;

import net.minecraftforge.eventbus.api.IEventBus;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent;
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;

public class ModEventBusHandler {
	public static void register() {
		IEventBus modEventBus = FMLJavaModLoadingContext.get().getModEventBus();
		modEventBus.register(ModEventBusHandler.class);
		modEventBus.register(new ModEventBusHandler());
		//註冊自製物品,並將其註冊到 ModEventBus 中
		Items.getRegister().register(modEventBus);
	}
	
	@SubscribeEvent
	public void onCommonSetupEvent(FMLCommonSetupEvent event) {
		ForgeEventBusHandler.register();
	}
}

在這裡,我們使用了
Items.getRegister().register(modEventBus);
來將 SuperSword 主冊到遊戲中。
此時 SuperSword 這個物品已經存在在遊戲中了,但它還沒有效果,即是說 SuperSword 裡的
onLivingAttackEvent() 和 onLivingAttackEvent() 不會有任何的效果,
因為我們只有註冊物品,但還沒有把監聽事 method 給註冊到遊戲中。
所以我們要來修改
com.my.mode.ForgeEventBusHandler.java :
package com.my.mode;

import com.my.mode.item.SuperSword;

import net.minecraft.client.Minecraft;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityType;
import net.minecraft.entity.LivingEntity;
import net.minecraft.entity.item.ItemEntity;
import net.minecraft.entity.monster.MonsterEntity;
import net.minecraft.entity.passive.FoxEntity;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.item.Item;
import net.minecraft.util.DamageSource;
import net.minecraft.world.World;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.event.entity.item.ItemTossEvent;
import net.minecraftforge.event.entity.living.LivingAttackEvent;
import net.minecraftforge.event.entity.living.LivingEvent;
import net.minecraftforge.event.entity.living.LivingHurtEvent;
import net.minecraftforge.eventbus.api.IEventBus;
import net.minecraftforge.eventbus.api.SubscribeEvent;

public class ForgeEventBusHandler {
	public static void register() {
		IEventBus forgeEventBus = MinecraftForge.EVENT_BUS;
		forgeEventBus.register(ForgeEventBusHandler.class);
		forgeEventBus.register(new ForgeEventBusHandler());
		
		//註冊超級劍 (super sword) 的事件效果
		forgeEventBus.register(new SuperSword());
		forgeEventBus.register(SuperSword.class);
	}
	
	// Event 有分成在 ModEventUs 上的,或在 ForgeEventBus 上的,
	// 此例的 ItemTossEvent 為在 ForgeEventBus 上的 Event
	@SubscribeEvent
	public void onItemTossEvent(ItemTossEvent event) {
		//ItemTossEvent: 物品丟棄時觸發的 Event, 
		//在遊戲中可用按鍵 q 來丟棄物品
		ItemEntity item = event.getEntityItem();
		World world = item.getEntityWorld();
		
		if (!world.isRemote) { // 判斷是否為 logical server 端,即處理邏輯的那端
							   // 如果此例的 isRemote 為 true,即為 logical client 端
							   // 可參考
							   // https://hackmd.io/@immortalmice/Hkj9s-CvU/https%3A%2F%2Fhackmd.io%2F%40immortalmice%2FrJKayrf9U
			
			//建立一個狐狸物件
			FoxEntity fox = new FoxEntity(EntityType.FOX, world);
			fox.setPosition(item.getPosX(), item.getPosY(), item.getPosZ());
			//將物件放到世界中
			world.addEntity(fox);
		}
	}
}
在 ForgeEventBusHandler.java 中,只有多加了兩行程式碼,就是
forgeEventBus.register(new SuperSword());
forgeEventBus.register(SuperSword.class);
用來把 SuperSword 裡的事件監聽 method 給註冊到遊戲中 (或說 ForgeEventBus 中)。

Note:

forgeEventBus.register(new SuperSword()); 用來註冊 SuperSword 裡的 non-static method,
forgeEventBus.register(SuperSword.class); 用來註冊 SuperSwrod 裡的 static method。

到這裡,SuperSword 已經正常的被加到遊戲中了,並且只要玩家拿著 SuperSword (左手或是右手),就不會受到任何攻擊及效果傷害,攻擊者如果對玩家發動攻擊就會受到攻擊者一半生命值的傷害。

但是 SuperSword 這時還沒有自己的名字和外觀,此時 SuperSword 在遊戲中的名字會是:
item.mymod.super_sword。

所以我們要來設定 SuperSword 的名字和外觀。
先建立兩個 json 格式的翻譯檔,en_us.json 和 zh_tw.json,其中內容為:
/main/resources/assets/mymod/lang/en_us.json :
{
    "item.mymod.super_sword": "Super Sword"
}
/main/resources/assets/mymod/lang/zh_tw.json :
{
    "item.mymod.super_sword": "超級劍"
}

可以看到其定義了英文 (en_us) 和繁體中文 (zh_tw) 的名字,
要注意的是 json 檔所放的路徑是規定的,其中 "mymod" 可改成自己 mod 的 id。

再來是設定外觀,在這邊我己經先畫了一張 16x16 pxiel 背景透明的圖,super_sword.png,
擺在以下位置,
/main/resources/assets/mymod/textures/item/super_sword.png :
一樣的,路徑是規定的,"mymod" 可換成自己 mod 的 id。

有了圖了以後,建立一個 super_sword.json 檔來設定外觀的資料:
/main/resources/assets/mymod/models/item/super_sword.json:
{
    "parent": "item/generated",
    "textures": {
        "layer0": "mymod:item/super_sword"
    },
    "display": {
	    "thirdperson_righthand": {
	      "rotation": [0, 90, 0],
	      "translation" : [0, 3, 0],
	      "scale" : [1, 1, 1]
	    }
  	}
}

在這邊我們多設定了 thirdpersion_righthand,也就是當物品拿在右手上時,
第三人稱看到時,物品的旋轉(rotation), 位移 (translation) 和縮放 (scale) 狀態,
三個數字分別代表 x, y, z 軸。
其中我調整了 ratation 和 translation,把 SuperSword 調成看起來像是被玩家握在劍柄的狀態,
如果沒調整的話,玩家會握在物件圖片 y 軸 (也就是高度沒軸) 的 2/4 處,對 16 x 16 pixel 的圖就是高度第 5 格到第 8 格處。

Note:

此時如果把 SuperSword 拿在左手,會發現一樣如拿在右手上一樣有吃到調整的設定,
應該是因為當有設定右手時,如果左手沒設定的話會自動以右手設定為準。

此時 SuperSword 的設定大致已完成了,但這時 SuperSword 還沒有自己的合成表配方,
也就是除非使用了遊戲指令,例如:
/give @p mymod:super_sword
不然無法正常得到此物品。

所以我們現在要來建立 SuperSword 的配方。
合成設定除了用程式來實以外,
minecraft forge 推出了方便使用的 json 格式設定,這邊會以 json 格式來做設定。
建立一個 super_sword_from_crafting.json,內容如下:

/main/resources/data/mymod/recipes/super_sword_from_crafting.json :
{
    "type": "minecraft:crafting_shaped",
    "pattern":
    [
        " x ",
        "xxx",
        " x "
    ],
    "key":
    {
        "x":
        {
            "item": "minecraft:dirt"
        }
    },
    "result":
    {
        "item": "mymod:super_sword",
        "count": 1
    }
}

"type" 代表了合成表的類型
minecraft:crafting_shaped 是有序配方,
代表配方為特定物品用特定方式排列,類如 minecraft 中的 "門 (door)" 的合成。
或是可以選 minecraft:crafting_shapeless,為無序配方,
代表此配方不在乎原料如何排列,只有由特定的原料即可合成,
類如 minecraft 中的 "火焰彈 (fire_charge)" 的合成。
可參考

"pattern" 即為合成表,給 type = minecraft:crafting_shaped 使用 (minecraft:crafting_shapeless 則是使用 ingredients),x 為物品的 key 值 (不一定要用x,可以用任何想要的英文字)。
而"key" 代物你在 "pattern" 中使用的 key 值各代表什麼物品。
"Result" 代表產生的物品和數目。

在此例中,我們設定了,只要把5個泥土排成十字就可以合成出 SuperSword。

至此已大功告成了! 我們可以開始來實際看看在遊戲中 SuperSword 的效果了,
以下為最後的成果實機測試影片:



源碼下載:

參考資料:

2021年1月12日 星期二

實作 angularJs 自訂驗證 select option list - 檢查 ng-model 的值有無在 option list 中

 AngularJs 原生的 select option  的 required 驗證,

只能對 ng-model 的值為 undefined , "", null , NaN來做 required error。

參考:

ngRequired

ngModel.NgModelController 的 $isEmpty

但如果 ng-model 不是 undefined , "", null , NaN ,且其值不在 select option 中,

此時希望能檢查出驗證錯誤的話 (validation) ,

就需要自己實作。


例如可能的情境為:

如果 option list 裡都是不為 0 的數字,當 ng-model 是數字 0 時 (可能是程式常態 assign 賦值的),此時 required (或 ng-required) 就無法驗證出 required 的 validation error。


以下為我寫出來的簡單實作,

建立了一個名為 selectOptionCheckValidator 的 directive 來進行驗證:



html : 

<div ng-app="app" ng-controller="controller as ctrl">
  <form name="form">
    <label><input type="radio" name="optionType" ng-value="1" ng-model="ctrl.optionType" /> Set option list 1</label>
    <label><input type="radio" name="optionType" ng-value="2" ng-model="ctrl.optionType" /> Set option list 2</label>

    <select name="mySelector" ng-model="ctrl.selectedVal" ng-options="selectOption.optionValue.id as selectOption.title for selectOption in ctrl.selectOptionList" select-option-check-validator ng-required="true">
    </select>
    
    <button ng-click="ctrl.selectedVal = 0">
      Set selectedVal to 0
    </button>

    <div>
      ctrl.selectedVal : {{ctrl.selectedVal}}
    </div>

    <div>
      form.mySelector.$error.selectedValueInOptionList : {{form.mySelector.$error.selectedValueInOptionList ? "invalid" : "valid"}}
    </div>
    <div>
      form.mySelector.$error.selectedValueInOptionList : {{form.mySelector.$error.required ? "invalid" : "valid"}}
    </div>

  </form>
</div>



Javascript :
angular.module("app", [])
  .controller("controller", ["$scope", function($scope) {
    var self = this;
    self.optionType = 1;
    self.selectedVal = 0;

    self.optionList1 = [{
      optionValue: {
        id: 11
      },
      title: "id 11"
    }, {
      optionValue: {
        id: 12
      },
      title: "id 12"
    }];

    self.optionList2 = [{
      optionValue: {
        id: 21
      },
      title: "id 21"
    }, {
      optionValue: {
        id: 22
      },
      title: "id 22"
    }];

    self.selectOptionList = [];

    $scope.$watch(angular.bind(this, function() {
      return this.optionType;
    }), function(newVal, oldVal) {
      //if value of selectedValue can not be found in self.selectOptionList,
      // selectedValue will be assigned to null.
      if (newVal == 1) {
        self.selectOptionList = self.optionList1;
      } else if (newVal == 2) {
        self.selectOptionList = self.optionList2;
        self.selectedVal = 21;
      }
    });
  }]).directive('selectOptionCheckValidator', ["$parse", function($parse) {
    return {
      require: 'ngModel',
      link: function(scope, element, attrs, ngModel) {

        var regex = /(.+)\sas\s(.+)\sfor\s(.+)\sin\s(.+)/;
        var matchString = regex.exec(attrs.ngOptions);
        var collectionExpression = matchString[4]; // "ctrl.selectOptionList"
        var arryItemVarableName = matchString[3]; // "selectOption"
        var valueExpression = matchString[1]; // "selectOption.optionValue.id"

        scope.$watch(collectionExpression, function(newValue, oldValue) {
          checkIsSelectedValueInOptionList();
        });

        element.on("change", function() {
          scope.$apply(function() {
            checkIsSelectedValueInOptionList();
          });
        });

        function checkIsSelectedValueInOptionList() {
          var isSelectedValueInOptionList = false;
          var selectedValue = $parse(attrs.ngModel)(scope);
          var optionList = $parse(collectionExpression)(scope);

          if (optionList) {
            isSelectedValueInOptionList = optionList.some(function(option) {
            
              // create a custom scope object to render optionValue from valueExpression
              var optionTempObject = new function(){
              	this[arryItemVarableName] = option;
              };
              
              var optionValue = $parse(valueExpression)(optionTempObject);
              
              return optionValue == selectedValue;
            });
            
            ngModel.$setValidity("selectedValueInOptionList", isSelectedValueInOptionList);
          }          
        }
      }
    };
  }]);



在這個例子裡,設置了兩個 option list 作為 <select> 的 option 選項,
當按下例子中的兩個 input type="radio" 時會改變 optionType ,
而 optionType 被改變時,會根據所勾選的值來重新設置不同的 option 選項給 <select>。

在 html <select> 的下方,我把 selectedVal 和 <select> 的 validation error 印出來以利觀察。

可以看到的是,
一開始 selectedVal 被設定成 0,但兩個 option 選項中都並沒有 0 這個值存在,
如果我們希望當 selectedVal 不在 option 選項中時能夠被驗證出來產生 validation error,
就必須自己實作 directive 來達成,即在這個例子中的 selectOptionCheckValidator directive。

AngularJs 原生的 required (或 ng-required) 在 selectedVal = 0 時,
會無法產生 validation error,因為它的實作是在當 selecctedVal = undefined, null, "", NaN 時才會產生 validation required error.
-------------------------------------------------------------------------------------------------------------------
另一個可以注意到的是,
我在這裡對按下第二個 input type="radio" 時,對 selectedVal 賦與 option list 2 中的值,
而按下第一個 input type="radio" 時沒有賦值。
此時可以觀察到當在一個 AngularJs Render 生命週期中,
如果 option list 被改變了,且 selectedValue 在 option list 中找不到的話,
selectedValue 就會被重新設定成 null。

Note

此例中用來解析 ng-option 字串的正規表達式較為簡易,

如果想知道完整的正規表達式,也就是可以解析 AngularJs ng-option 所有可能字串的正規達式,

可以參考官方源碼,其中可以看到正規表達式為:

^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s+([\s\S]+?))?(?:\s+disable\s+when\s+([\s\S]+?))?\s+for\s+(?:([$\w][$\w]*)|(?:\(\s*([$\w][$\w]*)\s*,\s*([$\w][$\w]*)\s*\)))\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?$

圖例



2021年1月11日 星期一

實作能偵測 textarea 內容並自動調整高度的 angularjs directive

實作了能偵測 textarea 內容並自動調整高度的 angularjs directive。

當 <textarea> 裡的文字變多需要高多行的 textarea 時,即需要更高的 textrea 時,

可以自動的調高高度,

而當文字變少時也能自動減少高度。

Note:

因為有使用到一些 jQuery 的 function,例如 innerHeight,所有需要先 include jQuery

直接上程式碼及範例:

html:
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.min.js"></script>

This &lt;textarea&gt; will change its height to contain content.
<div ng-app="app" ng-controller="controller as ctrl">
  <textarea textarea-auto-resizer></textarea>
</div>

javascript:
angular.module("app", [])
  .controller("controller", ["$scope", function($scope) {
  	var self = this;
    self.text = "long words - long words - long words - long words - long words - long words - long words - long words - long words - long words - long words - long words - long words - long words - long words"
  }])
  .directive('textareaAutoResizer', ["$sce", function($sce) {
    return {
      link: function(scope, element, attrs, ngModel) {
        autoResizeTextArea();

        scope.$watch(attrs.ngModel, function(newValue, oldValue) {
        	autoResizeTextArea();
        });
        
        element.on("input", autoResizeTextArea);

        function autoResizeTextArea(event) {
          var dom = angular.element(element);

          dom.css({
            height: ""
          });
          if (dom.prop("scrollHeight") > dom.innerHeight()) {
            //set dom height(without padding part) to scrollHeight - padding part
            dom.height(dom.prop("scrollHeight") - (dom.innerHeight() - dom.height()));
          }
        }
      }
    };
  }]);


參考:

  1. Creating a Directive that Adds Event Listeners

2021年1月1日 星期五

Minecraft Java 版 (1.16.4, jdk 1.8) 自製 Mod - 使用 Forge - 以 Subscribe Event 為例子

 如果只是要在 Minecraft 中玩 Forge 的 Mod (自己做好的或別人做好的),

只需要以下步驟:

  1. Forge 官網下載對應 Minecraft 版本的 Installer。
  2. 執行下載下來的 Installer 並安裝 Client 端。
  3. 把要玩的 Mod (為 jar 檔) 放到 Minecraft 的 mods 資料夾中。
    預設 Minecraft 的根目錄會裝在這:
    C:\Users\XXX\AppData\Roaming\.minecraft
  4. 執行 Minecraft 的 Launcher 啟動 Minecraft,
    點開 "安裝檔" 分頁,可以看到 Forge 的開始遊戲方式,用 Forge 開始遊戲,
    即可開始使用 Mod 玩遊戲了 (在選單介面可以看到點),如下圖所示。



以下開始介紹使用 Forge 製作自己 Mod 的方法:

我的環境為 Win10

使用的 IDE 為 Eclipse

Note:

如果 Eclipse 所使用的 jdk 不是 jdk 1.8,

此時記得要把 Forge 的 Mod 專案的 compile Java 設定成 jdk1.8,

還有在執行 Forge Mod 專案中的 gradle task 時,要指定 jdk1.8,

例如:

gradlew build -Dorg.gradle.java.home={JDK_PATH}

或是在 Eclipse 裡的 gradle plugin run configuration 裡對 gradle task 設定 java_home 的位置


  1. 去 Forge 官網下載對應 Minecraft 版本的 Mdk。

  2. 解壓縮下載下來的檔案後,裡面的東西就是一個完整所需的 Minecraft Forge Mod Java 專案 (以下暫稱 Minecraft Forge Mod 專案),不過還要執行一些 forge 已經幫我們寫好的 gradle task 來設定一些東西。
  3. 以 Windows 環境為例,打開命令列模式視窗執行
    gradlew genEclipseRuns
  4. 執行完 genEclipseRuns 的 gradle task後,再使用 Eclipse 的 Import project - Exsiting Gradle Project 功能把 Minecraft Forge Mod 專案給 Import 進來,就可以開始撰寫程式了。
  5. 要測試自己寫的 Mod 的時候,可以執行 runClient 這個 gradle task,
    此時會有一個 Minecraft 遊戲視窗被打開,進去後應可以看到自己的 Mod 已被載入,開始遊戲後就可以在遊戲中使用自制的 Mod 了。
  6. 寫好 Mod 想要輸出時,可以執行 build 這個 grade task,就會在專案的
    /build/libs 看到輸出的 Mod 的 jar 檔,把此 jar 檔放到遊戲的 mods 資料夾就可以在遊戲中使用了。


我有找到幾個不錯的參考資料,裡面的教學對我幫助很大,

雖然可能 Forge 版本不同有些微差異,但還是能幫助理解使用 Forge 自制 Minecraft Mod 的許多相關知識。

請參考本篇文章下方的 "參考資料"。


接下來我會以 Minecraft Mod 中註冊事件 (Subscribe Event) 為例子,

來實作一個在 Minecraft 中, "丟棄物品時,會在其位置產生一個XXX" 的功能

Note:

在 Minecraft 中,"丟棄" 和 "丟擲" 不同 ,其事件對應到 Forge 的 ItemTossEventProjectileImpactEvent.Throwable

以下是專案的檔案結構:



可以看到裡面已經有一個範例 Mod, ExampleMod.java,

之後會建立自己的 Mod java class,所以可以 ExampleMod.java 刪掉或是把其中的
@Mod 那行刪掉,

被紅框框起來的部份是我自己建立的 Mod Class 檔: com.my.mode.MyMod.java,

接著要為此 Mod 決定一個 Mod 的名稱,也可以說是 Mod Id,只能英文小寫,

此例我們決定叫做 mymod,

決定好 Mod Id 後,要在 

main/resources/META-INF/mods.toml

裡設定 Mod Id,在 mods.toml 中,紀錄著 Mod 的相關資訊,例如 Mod Id, Mod 的名稱、敘述、作者資料等資訊,在 Minecraft 的 Mod 資訊頁面上也會顯示這些東西。

為了簡單,我們不修改其他東西,因為並不影響自制 Mod 的功能,

只要把下列這行修改成自己的 Mod Id 即可:

# The modid of the mod

modId="mymod" #mandatory


以下直接看程式碼:

mods.toml

# This is an example mods.toml file. It contains the data relating to the loading mods.
# There are several mandatory fields (#mandatory), and many more that are optional (#optional).
# The overall format is standard TOML format, v0.5.0.
# Note that there are a couple of TOML lists in this file.
# Find more information on toml format here:  https://github.com/toml-lang/toml
# The name of the mod loader type to load - for regular FML @Mod mods it should be javafml
modLoader="javafml" #mandatory
# A version range to match for said mod loader - for regular FML @Mod it will be the forge version
loaderVersion="[35,)" #mandatory This is typically bumped every Minecraft version by Forge. See our download page for lists of versions.
# The license for you mod. This is mandatory metadata and allows for easier comprehension of your redistributive properties.
# Review your options at https://choosealicense.com/. All rights reserved is the default copyright stance, and is thus the default here.
license="All rights reserved"
# A URL to refer people to when problems occur with this mod
issueTrackerURL="http://my.issue.tracker/" #optional
# A list of mods - how many allowed here is determined by the individual mod loader
[[mods]] #mandatory
# The modid of the mod
modId="mymod" #mandatory
# The version number of the mod - there's a few well known ${} variables useable here or just hardcode it
# ${file.jarVersion} will substitute the value of the Implementation-Version as read from the mod's JAR file metadata
# see the associated build.gradle script for how to populate this completely automatically during a build
version="${file.jarVersion}" #mandatory
 # A display name for the mod
displayName="My Mod" #mandatory
# A URL to query for updates for this mod. See the JSON update specification <here>
updateJSONURL="http://myurl.me/" #optional
# A URL for the "homepage" for this mod, displayed in the mod UI
displayURL="http://example.com/" #optional
# A file name (in the root of the mod JAR) containing a logo for display
logoFile="mymod.png" #optional
# A text field displayed in the mod UI
credits="Thanks for this example mod goes to Java" #optional
# A text field displayed in the mod UI
authors="Love, Cheese and small house plants" #optional
# The description text for the mod (multi line!) (#mandatory)
description='''
This is a long form description of the mod. You can write whatever you want here

Have some lorem ipsum.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed mollis lacinia magna. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Sed sagittis luctus odio eu tempus. Interdum et malesuada fames ac ante ipsum primis in faucibus. Pellentesque volutpat ligula eget lacus auctor sagittis. In hac habitasse platea dictumst. Nunc gravida elit vitae sem vehicula efficitur. Donec mattis ipsum et arcu lobortis, eleifend sagittis sem rutrum. Cras pharetra quam eget posuere fermentum. Sed id tincidunt justo. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
'''
# A dependency - use the . to indicate dependency for a specific modid. Dependencies are optional.
[[dependencies.examplemod]] #optional
    # the modid of the dependency
    modId="forge" #mandatory
    # Does this dependency have to exist - if not, ordering below must be specified
    mandatory=true #mandatory
    # The version range of the dependency
    versionRange="[35,)" #mandatory
    # An ordering relationship for the dependency - BEFORE or AFTER required if the relationship is not mandatory
    ordering="NONE"
    # Side this dependency is applied on - BOTH, CLIENT or SERVER
    side="BOTH"
# Here's another dependency
[[dependencies.examplemod]]
    modId="minecraft"
    mandatory=true
# This version range declares a minimum of the current minecraft version up to but not including the next major version
    versionRange="[1.16.4,1.17)"
    ordering="NONE"
    side="BOTH"

com.my.mod.MyMod.java :

package com.my.mode;

import net.minecraftforge.fml.common.Mod;

@Mod("mymod") // 設定 Mod Id, 請先把 ExampleMod.java 拿掉或把其 @Mod 的那行註解掉
public class MyMod {
	
	public MyMod() {
		// 註冊 EventBus,
		// 先註冊 ModEventBus, 之後 onCommonSetupEvent 會被觸發執行,
		// 接著屬於 ModEventBus 的 Event 都會被觸發執行。
		// 再註冊 ForgeEventBus,
		// 之後屬於 ForgeEventBus 的 Event (例如 ItemTossEvent, ProjectileImpactEvent.Throwable 等) 都會被觸發執行。
		ModEventBusHandler.register();
	}
}

com.my.mod.ModEventBusHandler.java :

package com.my.mode;

import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.eventbus.api.IEventBus;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent;
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;

public class ModEventBusHandler {
	public static void register() {
		IEventBus modEventBus = FMLJavaModLoadingContext.get().getModEventBus();
		modEventBus.register(ModEventBusHandler.class);
		modEventBus.register(new ModEventBusHandler());
	}
	
	@SubscribeEvent
	public void onCommonSetupEvent(FMLCommonSetupEvent event) {
		ForgeEventBusHandler.register();
	}
}

com.my.mod.ForgeEventBusHandler.java :

package com.my.mode;

import net.minecraft.entity.EntityType;
import net.minecraft.entity.item.ItemEntity;
import net.minecraft.entity.passive.FoxEntity;
import net.minecraft.world.World;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.event.entity.item.ItemTossEvent;
import net.minecraftforge.eventbus.api.IEventBus;
import net.minecraftforge.eventbus.api.SubscribeEvent;

public class ForgeEventBusHandler {
	public static void register() {
		IEventBus forgeEventBus = MinecraftForge.EVENT_BUS;
		forgeEventBus.register(ForgeEventBusHandler.class);
		forgeEventBus.register(new ForgeEventBusHandler());
	}
	
	// Event 有分成在 ModEventUs 上的,或在 ForgeEventBus 上的,
	// 此例的 ItemTossEvent 為在 ForgeEventBus 上的 Event
	@SubscribeEvent
	public void onItemTossEvent(ItemTossEvent event) {
		//ItemTossEvent: 物品丟棄時觸發的 Event, 
		//在遊戲中可用按鍵 q 來丟棄物品
		ItemEntity item = event.getEntityItem();
		World world = item.getEntityWorld();
		
		if (!world.isRemote) { // 判斷是否為 logical server 端,即處理邏輯的那端
							   // 如果此例的 isRemote 為 true,即為 logical client 端
							   // 可參考
							   // https://hackmd.io/@immortalmice/Hkj9s-CvU/https%3A%2F%2Fhackmd.io%2F%40immortalmice%2FrJKayrf9U
			
			//建立一個狐狸物件
			FoxEntity fox = new FoxEntity(EntityType.FOX, world);
			fox.setPosition(item.getPosX(), item.getPosY(), item.getPosZ());
			//將物件放到世界中
			world.addEntity(fox);
		}
	}
}

資源分享: