CommUtils 是一個提供常用字串處理、格式化、遮罩等功能的工具類別。此類別設計為靜態工具類別,提供多種實用的字串操作方法,特別適合處理固定格式資料的格式化與驗證。
// 檢查字串是否為數字
boolean isNum = CommUtils.isNumeric("123.45"); // true
boolean notNum = CommUtils.isNumeric("abc"); // false
// 右側補零
String rightZero = CommUtils.rightPadZeros(123, 6); // "123000"
// 左側補零
String leftZero = CommUtils.leadZeroString(123, 6); // "000123"
// 右側補空白
String rightSpace = CommUtils.rightPadSpaces("ABC", 6); // "ABC "
// 左側補空白
String leftSpace = CommUtils.leadSpacesString("ABC", 6); // " ABC"
// 將字串每 3 個字元切分
List<String> parts = CommUtils.splitEqually("ABCDEFGHI", 3);
// 結果:["ABC", "DEF", "GHI"]
// 使用指定分隔符號切分
List<String> parts = CommUtils.splitDelimiter("A,B,C", ",");
// 結果:["A", "B", "C"]
// 預設使用分號分隔
List<String> parts = CommUtils.splitDelimiter("A;B;C", null);
// 使用 {} 作為參數替換符號
String result = CommUtils.stringFormate("Hello {} and {}", "Alice", "Bob");
// 結果:"Hello Alice and Bob"
// 跳脫字元使用
String escaped = CommUtils.stringFormate("Show \\{} and {}", "value");
// 結果:"Show {} and value"
// 指定範圍遮罩
String masked = CommUtils.maskString("ABCDEF", 1, 4); // "A***EF"
// 全部遮罩
String allMasked = CommUtils.maskAllString("ABCDEF"); // "******"
// 姓名遮罩
String name = CommUtils.maskName("張小明"); // "張*明"
// 手機號碼遮罩
String phone = CommUtils.maskCellPhone("0912345678"); // "0912***5678"
// Email 遮罩
String email = CommUtils.maskEmail("test@example.com"); // "t****@example.com"
// 驗證 Email 格式
boolean isValid = CommUtils.isValidEmailAddress("test@example.com"); // true
boolean notValid = CommUtils.isValidEmailAddress("invalid.email"); // false
字串處理
切分與合併
格式化
{} 作為參數替換符號\\ 跳脫特殊字元遮罩處理
這些測試案例展示了以下功能的正確性:
@Test
void testPrivateConstructor() {
Exception exception = assertThrows(IllegalStateException.class, () -> {
Constructor<?> constructor = CommUtils.class.getDeclaredConstructor();
constructor.setAccessible(true);
try {
constructor.newInstance();
} catch (InvocationTargetException e) {
Throwable cause = e.getCause();
if (cause instanceof RuntimeException) {
throw (RuntimeException) cause;
} else {
throw new RuntimeException(cause);
}
}
});
assertEquals("This is a utility class and cannot be instantiated", exception.getMessage());
}
@Test
@DisplayName("isNumeric: valid and invalid cases")
void testIsNumeric() {
assertTrue(CommUtils.isNumeric("123"));
assertTrue(CommUtils.isNumeric("-123"));
assertTrue(CommUtils.isNumeric("123.45"));
assertFalse(CommUtils.isNumeric("abc"));
assertFalse(CommUtils.isNumeric(""));
assertFalse(CommUtils.isNumeric(null));
assertFalse(CommUtils.isNumeric("12a3"));
assertFalse(CommUtils.isNumeric(" "));
}
@Test
@DisplayName("rightPadZeros: pad right with zeros")
void testRightPadZeros() {
assertEquals("1234000", CommUtils.rightPadZeros(1234, 7));
assertEquals("123", CommUtils.rightPadZeros(123, 3));
assertEquals("120", CommUtils.rightPadZeros(12, 3));
assertEquals("100", CommUtils.rightPadZeros(1, 3));
}
@Test
@DisplayName("leadZeroString: pad left with zeros")
void testLeadZeroString() {
assertEquals("00001234", CommUtils.leadZeroString(1234, 8));
assertEquals("00123", CommUtils.leadZeroString(123, 5));
assertEquals("00001", CommUtils.leadZeroString(1, 5));
assertEquals("123", CommUtils.leadZeroString(123, 3));
}
@Test
@DisplayName("splitEqually: split string into fixed size parts")
void testSplitEqually() {
List<String> result = CommUtils.splitEqually("abcdefghij", 3);
assertEquals(Arrays.asList("abc", "def", "ghi", "j"), result);
result = CommUtils.splitEqually(" 123456 ", 2);
assertEquals(Arrays.asList("12", "34", "56"), result);
}
@Test
@DisplayName("splitDelimiter: split string by delimiter")
void testSplitDelimiter() {
List<String> result = CommUtils.splitDelimiter("a,b,c", ",");
assertEquals(Arrays.asList("a", "b", "c"), result);
result = CommUtils.splitDelimiter("a|b|c", "|");
assertEquals(Arrays.asList("a", "b", "c"), result);
result = CommUtils.splitDelimiter(null, ",");
assertEquals(new ArrayList<>(), result);
}
@Test
@DisplayName("stringFormat: format string with {}")
void testStringFormat() {
assertEquals("this is a for b",
CommUtils.stringFormate("this is {} for {}", "a", "b"));
assertEquals("this is {} for a",
CommUtils.stringFormate("this is \\{} for {}", "a"));
assertEquals("this is \\a for b",
CommUtils.stringFormate("this is \\\\{} for {}", "a", "b"));
assertEquals("no placeholders",
CommUtils.stringFormate("no placeholders"));
}
@Test
@DisplayName("maskString: mask substring with *")
void testMaskString() {
assertEquals("ab***fg", CommUtils.maskString("abcdefg", 2, 5));
assertEquals("", CommUtils.maskString(null, 1, 2));
assertEquals("", CommUtils.maskString("", 1, 2));
assertEquals("abc", CommUtils.maskString("abc", 2, 2));
assertEquals("a**", CommUtils.maskString("abc", 1, 3));
}
@Test
@DisplayName("maskName: mask middle of name")
void testMaskName() {
assertEquals("王*明", CommUtils.maskName("王小明"));
assertEquals("A*B", CommUtils.maskName("ACB"));
assertEquals("", CommUtils.maskName(""));
assertEquals("", CommUtils.maskName(null));
}
工具類別保護測試
數字字串驗證
字串補零功能
字串切分功能
格式化功能
遮罩功能
package tw.lewishome.webapp.base.utility.common;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
import org.apache.commons.lang3.StringUtils;
import jakarta.mail.internet.AddressException;
import jakarta.mail.internet.InternetAddress;
/**
* CommUtils 提供常用字串、集合、物件轉換與遮罩等工具方法。
*
* 主要功能包含:
* <ul>
* <li>字串是否為數字檢查</li>
* <li>字串左右補零、補空白</li>
* <li>字串固定長度切分、依分隔字串切分、集合合併</li>
* <li>自訂格式化字串方法({} 參數替換)</li>
* <li>字串遮罩(姓名、手機、Email)</li>
* </ul>
* 注意事項:
* <ul>
* <li>部分方法需依賴外部工具類(如 StringUtils, TypeConvert)</li>
* <li>物件序列化/反序列化請確保物件實現 Serializable 介面</li>
* <li>遮罩方法僅針對格式正確字串有效</li>
* </ul>
*
*/
public class CommUtils {
/** Private constructor to prevent instantiation */
private CommUtils() {
throw new IllegalStateException("This is a utility class and cannot be instantiated");
}
// ==================== 1. 字串處理相關方法 ====================
/**
* <pre>
* 檢核 字串 是否為數字 (文字轉數字前檢核)
*
* 建議使用 StringUtils.isNumeric 取代
* </pre>
*
* @param strNum 字串
* @return boolean true:數字字串
*/
public static boolean isNumeric(String strNum) {
Pattern pattern = Pattern.compile("-?\\d+(\\.\\d+)?");
if (StringUtils.isBlank(strNum)) {
return false;
}
return pattern.matcher(strNum.trim()).matches();
}
/**
* 為輸出格式字串 右補零 ( 1234, 7) == 1234000
*
* @param num 輸入數字 (int)
* @param len 數字字串長度 (int)
* @return String 格式輸出字串
*/
public static String rightPadZeros(int num, int len) {
String strNum = Integer.toString(num);
String result;
if (strNum.length() < len) {
StringBuilder sb = new StringBuilder(strNum);
for (int i = 0; i < len - strNum.length(); i++) {
sb.append(" ");
}
result = sb.toString();
} else {
result = strNum;
}
return result.replace(' ', '0');
}
/**
* 為輸出格式字串 左補零 ( 1234, 8) == 00001234
*
* @param num 輸入數字 (int)
* @param len 數字字串長度 (int)
* @return String 格式輸出字串
*/
public static String leadZeroString(int num, int len) {
StringBuilder stringBuilder = new StringBuilder(Integer.toString(num));
while (stringBuilder.length() < len) {
stringBuilder.insert(0, "0"); // Insert "0" at the beginning (index 0)
}
return stringBuilder.toString();
}
/**
* 為輸出格式字串 右補空白 ( "1234" , 8 == "1234****" *號代表空白)
*
* @param inString 輸入文數字 (String)
* @param len 數字字串長度 (int)
* @return String 格式輸出字串
*/
public static String rightPadSpaces(String inString, int len) {
if (StringUtils.isBlank(inString)) {
return "";
}
String trimmedString = inString.trim();
StringBuilder stringBuilder = new StringBuilder(trimmedString);
for (int i = trimmedString.length(); i < len; i++) {
stringBuilder.append(' ');
}
return stringBuilder.toString();
}
/**
* 為輸出格式字串 左補空白 ( "1234" , 8 == "**** 1234") *號代表空白
*
* @param inString 輸入文數字 (String)
* @param len 數字字串長度 (int)
* @return String 格式輸出字串
*/
public static String leadSpacesString(String inString, int len) {
if (StringUtils.isBlank(inString)) {
return "";
}
String trimmedString = inString.trim();
int paddingLength = len - trimmedString.length();
StringBuilder paddingBuilder = new StringBuilder();
for (int i = 0; i < paddingLength; i++) {
paddingBuilder.append(" ");
}
return paddingBuilder.toString() + trimmedString;
}
// ========= 2. 長字串切分與合併相關方法 ============
/**
* 長字串 固定長度切分
*
* @param inText 長字串
* @param size 固定長度
* @return ArrayList 切分後的資料
*/ public static List<String> splitEqually(String inText, int size) {
String text = inText.trim();
// Give the list the right capacity to start with. You could use an array
// instead if you wanted.
List<String> rtnListString = new ArrayList<>((text.length() + size - 1) / size);
for (int start = 0; start < text.length(); start += size) {
rtnListString.add(text.substring(start, Math.min(text.length(), start + size)).trim());
}
return rtnListString;
}
/**
* 長字串切分 (by 分隔字串)
*
* @param splitString 長字串
* @param delimiter 分隔字串
* @return ArrayList 切分後的資料
*/
public static List<String> splitDelimiter(String splitString, String delimiter) {
List<String> rtnListString = new ArrayList<>();
if (StringUtils.isBlank(delimiter)) {
delimiter = ";";
}
// Give the list the right capacity to start with. You could use an array
// instead if you wanted.
if (StringUtils.isBlank(splitString)) {
return rtnListString;
}
if (StringUtils.isBlank(delimiter)) {
rtnListString.add(splitString);
return rtnListString;
}
Pattern pattern = Pattern.compile(Pattern.quote(delimiter));
String[] splitText = pattern.split(splitString);
for (int i = 0; i < splitText.length; i++) {
rtnListString.add(splitText[i].trim());
}
return rtnListString;
}
// ========= 3. 格式化與轉換常用工具方法 ===========
/** Constant <code>EMPTY_JSON='\{\}'</code> */
public static final String EMPTY_JSON = "{}";
/** Constant <code>C_BACKSLASH='\\'</code> */
public static final char C_BACKSLASH = '\\';
/** Constant <code>C_DELIM_START='{'</code> */
public static final char C_DELIM_START = '{';
/** Constant <code>C_DELIM_END='}'</code> */
public static final char C_DELIM_END = '}';
/**
* 格式化字串 format("this is {} for {}", "a", "b") <br>
* 此方法只是將符號 {} 按照順序替換為參數<br>
* 如果想輸出 {} 使用 \\{ 或 \\} 即可, 如果想輸出 \ 使用 \\\\ 即可<br>
* 例:<br>
* 通常使用:format("this is {} for {}", "a", "b") -> this is a for b<br>
* 輸出{}: format("this is \\{} for {}", "a", "b") -> this is \{} for a<br>
* 輸出\: format("this is \\\\{} for {}", "a", "b") -> this is \a for b<br>
*
* @param stringPattern 樣式字串
* @param argArray 參數
* @return String 結果
*/
public static String stringFormate(String stringPattern, Object... argArray) {
if (StringUtils.isBlank(stringPattern) || argArray == null || argArray.length == 0) {
return stringPattern;
}
int strPatternLength = stringPattern.length();
StringBuilder stringBuilder = new StringBuilder(strPatternLength + 50);
int handledPosition = 0;
int delimIndex;// 替換符號 {} index
for (int argIndex = 0; argIndex < argArray.length; argIndex++) {
delimIndex = stringPattern.indexOf(EMPTY_JSON, handledPosition);
if (delimIndex == -1) {
if (handledPosition == 0) {
return stringPattern;
} else { // 樣板字串模板其他部分不再包含替換符號{},返回結果
stringBuilder.append(stringPattern, handledPosition, strPatternLength);
return stringBuilder.toString();
}
} else {
if (delimIndex > 0 && stringPattern.charAt(delimIndex - 1) == C_BACKSLASH) {
if (delimIndex > 1 && stringPattern.charAt(delimIndex - 2) == C_BACKSLASH) {
// 替代符號 {} 處理位置
stringBuilder.append(stringPattern, handledPosition, delimIndex - 1);
stringBuilder.append(TypeConvert.toUtf8String(argArray[argIndex]));
handledPosition = delimIndex + 2;
} else {
// 有替代符號
argIndex--;
stringBuilder.append(stringPattern, handledPosition, delimIndex - 1);
stringBuilder.append(C_DELIM_START);
handledPosition = delimIndex + 1;
}
} else {
// 正常替代符號字串
stringBuilder.append(stringPattern, handledPosition, delimIndex);
stringBuilder.append(TypeConvert.toUtf8String(argArray[argIndex]));
handledPosition = delimIndex + 2;
}
}
}
// 加入最後一個替代符號字串
stringBuilder.append(stringPattern, handledPosition, stringPattern.length());
return stringBuilder.toString();
}
// ============ 4. 字串遮罩與檢查相關方法 ==========
/**
* 字串遮罩"*"
*
* @param str CharSequence
* @param startInclude 字串遮罩開始位置(包含)
* @param endExclude 字串遮罩結束位置(不包含)
* @return 遮罩後資料
*/
public static String maskString(String str, int startInclude, int endExclude) {
if (StringUtils.isEmpty(str)) {
return "";
}
int strLength = str.length();
if (startInclude > strLength) {
return "";
}
if (endExclude > strLength) {
endExclude = strLength;
}
if (startInclude > endExclude) {
// 如果字串遮罩開始位置大於字串遮罩結束位置,不替換
return "";
}
char[] chars = new char[strLength];
for (int i = 0; i < strLength; i++) {
if (i >= startInclude && i < endExclude) {
chars[i] = '*';
} else {
chars[i] = str.charAt(i);
}
}
return new String(chars);
}
/**
* 字串遮罩全部分為"*"
*
* @param str CharSequence
* @return 遮罩後資料
*/
public static String maskAllString(String str) {
if (StringUtils.isEmpty(str)) {
return "";
}
return maskString(str, 0, str.length());
}
/**
* 姓名中間*遮罩
*
* @param name 姓名
* @return 遮罩後資料
*/
public static String maskName(String name) {
if (StringUtils.isBlank(name)) {
return "";
}
return name.replaceAll("(\\S)\\S(\\S*)", "$1*$2");
}
/**
* 行動電話遮罩
*
* @param cellPhone 姓名
* @return 遮罩後資料
*/
public static String maskCellPhone(String cellPhone) {
if (StringUtils.isBlank(cellPhone)) {
return "";
}
return cellPhone.replaceAll("(\\d{3})\\d{3}(\\d{4})", "$1***$2");
}
/**
* Email遮罩
*
* @param email 電子郵件信箱
* @return 遮罩後資料
*/
public static String maskEmail(String email) {
if (StringUtils.isBlank(email)) {
return "";
}
return email.replaceAll("(^.)[^@]*(@.*$)", "$1****$2");
}
// ========== 5. 各類格式檢查相關方法 ==========
/**
* Email 檢核
*
* @param email 電子郵件信箱
* @return boolean 是否合法
*/
public static boolean isValidEmailAddress(String email) {
if (StringUtils.isBlank(email)) {
return false;
}
boolean result = true;
try {
InternetAddress emailAddr = new InternetAddress(email);
emailAddr.validate();
} catch (AddressException ex) {
result = false;
}
return result;
}
}
package tw.lewishome.webapp.base.utility.common;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.DisplayName;
import java.util.Arrays;
import java.util.List;
import java.util.ArrayList;
import static org.junit.jupiter.api.Assertions.*;
class CommUtilsTest {
@Test
void testPrivateConstructor() {
Exception exception = assertThrows(IllegalStateException.class, () -> {
java.lang.reflect.Constructor<?> constructor = CommUtils.class.getDeclaredConstructor();
constructor.setAccessible(true);
try {
constructor.newInstance();
} catch (java.lang.reflect.InvocationTargetException e) {
// rethrow the underlying cause (expected IllegalStateException from the private constructor)
Throwable cause = e.getCause();
if (cause instanceof RuntimeException) {
throw (RuntimeException) cause;
} else {
throw new RuntimeException(cause);
}
}
});
assertEquals("This is a utility class and cannot be instantiated", exception.getMessage());
}
@Test
@DisplayName("isNumeric: valid and invalid cases")
void testIsNumeric() {
assertTrue(CommUtils.isNumeric("123"));
assertTrue(CommUtils.isNumeric("-123"));
assertTrue(CommUtils.isNumeric("123.45"));
assertFalse(CommUtils.isNumeric("abc"));
assertFalse(CommUtils.isNumeric(""));
assertFalse(CommUtils.isNumeric(null));
assertFalse(CommUtils.isNumeric("12a3"));
assertFalse(CommUtils.isNumeric(" "));
}
@Test
@DisplayName("rightPadZeros: pad right with zeros")
void testRightPadZeros() {
assertEquals("1234000", CommUtils.rightPadZeros(1234, 7));
assertEquals("123", CommUtils.rightPadZeros(123, 3));
assertEquals("120", CommUtils.rightPadZeros(12, 3));
assertEquals("100", CommUtils.rightPadZeros(1, 3));
}
@Test
@DisplayName("leadZeroString: pad left with zeros")
void testLeadZeroString() {
assertEquals("00001234", CommUtils.leadZeroString(1234, 8));
assertEquals("00123", CommUtils.leadZeroString(123, 5));
assertEquals("00001", CommUtils.leadZeroString(1, 5));
assertEquals("123", CommUtils.leadZeroString(123, 3));
}
@Test
@DisplayName("splitEqually: split string into fixed size parts")
void testSplitEqually() {
List<String> result = CommUtils.splitEqually("abcdefghij", 3);
assertEquals(Arrays.asList("abc", "def", "ghi", "j"), result);
result = CommUtils.splitEqually(" 123456 ", 2);
assertEquals(Arrays.asList("12", "34", "56"), result);
}
@Test
@DisplayName("splitDelimiter: split string by delimiter")
void testSplitDelimiter() {
List<String> result = CommUtils.splitDelimiter("a,b,c", ",");
assertEquals(Arrays.asList("a", "b", "c"), result);
result = CommUtils.splitDelimiter("a|b|c", "|");
assertEquals(Arrays.asList("a", "b", "c"), result);
result = CommUtils.splitDelimiter(null, ",");
assertEquals(new ArrayList<>(), result);
result = CommUtils.splitDelimiter("abc", null);
assertEquals(Arrays.asList("abc"), result);
}
@Test
@DisplayName("stringFormat: format string with {}")
void testStringFormat() {
assertEquals("this is a for b", CommUtils.stringFormate("this is {} for {}", "a", "b"));
assertEquals("this is {} for a", CommUtils.stringFormate("this is \\{} for {}", "a"));
assertEquals("this is \\a for b", CommUtils.stringFormate("this is \\\\{} for {}", "a", "b"));
assertEquals("no placeholders", CommUtils.stringFormate("no placeholders"));
assertEquals(null, CommUtils.stringFormate(null, "a"));
assertEquals("{}", CommUtils.stringFormate("{}", (Object[]) null));
}
@Test
@DisplayName("maskString: mask substring with *")
void testMaskString() {
assertEquals("ab***fg", CommUtils.maskString("abcdefg", 2, 5));
assertEquals("", CommUtils.maskString(null, 1, 2));
assertEquals("", CommUtils.maskString("", 1, 2));
assertEquals("", CommUtils.maskString("abc", 5, 2));
assertEquals("abc", CommUtils.maskString("abc", 2, 2));
assertEquals("a**", CommUtils.maskString("abc", 1, 3));
}
@Test
@DisplayName("maskString: mask substring with *")
void testMaskAllString() {
assertEquals("*******", CommUtils.maskAllString("abcdefg"));
assertEquals("", CommUtils.maskAllString(null));
assertEquals("", CommUtils.maskAllString(""));
assertEquals("***", CommUtils.maskAllString("abc"));
}
@Test
@DisplayName("maskName: mask middle of name")
void testMaskName() {
assertEquals("王*明", CommUtils.maskName("王小明"));
assertEquals("A*B", CommUtils.maskName("ACB"));
assertEquals("", CommUtils.maskName(""));
assertEquals("", CommUtils.maskName(null));
}
@Test
@DisplayName("maskCellPhone: mask cell phone number")
void testMaskCellPhone() {
assertEquals("091***5345", CommUtils.maskCellPhone("0912345345"));
assertEquals("", CommUtils.maskCellPhone(""));
assertEquals("", CommUtils.maskCellPhone(null));
}
@Test
@DisplayName("maskEmail: mask email address")
void testMaskEmail() {
assertEquals("t****@gmail.com", CommUtils.maskEmail("test@gmail.com"));
assertEquals("", CommUtils.maskEmail(""));
assertEquals("", CommUtils.maskEmail(null));
}
@Test
@DisplayName("rightPadSpaces: pad right with spaces")
void testRightPadSpaces() {
assertEquals("1234 ", CommUtils.rightPadSpaces("1234", 8));
assertEquals("1234", CommUtils.rightPadSpaces("1234", 4));
assertEquals("123", CommUtils.rightPadSpaces("123 ", 3));
assertEquals("", CommUtils.rightPadSpaces(null, 3));
assertEquals("", CommUtils.rightPadSpaces("", 3));
}
@Test
@DisplayName("leadSpacesString: pad left with spaces")
void testLeadSpacesString() {
assertEquals(" 1234", CommUtils.leadSpacesString("1234", 8));
assertEquals("1234", CommUtils.leadSpacesString("1234", 4));
assertEquals("123", CommUtils.leadSpacesString(" 123 ", 3));
assertEquals("", CommUtils.leadSpacesString(null, 3));
assertEquals("", CommUtils.leadSpacesString("", 3));
}
@Test
@DisplayName("isValidEmailAddress: validate email addresses")
void testIsValidEmailAddress() {
assertTrue(CommUtils.isValidEmailAddress("test@example.com"));
assertTrue(CommUtils.isValidEmailAddress("user.name+tag@example.co.uk"));
assertTrue(CommUtils.isValidEmailAddress("user.name@subdomain.example.com"));
assertFalse(CommUtils.isValidEmailAddress("invalid.email"));
assertFalse(CommUtils.isValidEmailAddress("@invalid.com"));
assertFalse(CommUtils.isValidEmailAddress("invalid@"));
assertFalse(CommUtils.isValidEmailAddress("invalid@.com"));
assertFalse(CommUtils.isValidEmailAddress(null));
}
}