mirror of
https://gitee.com/willfree/min-dev-java.git
synced 2026-06-18 06:00:25 +08:00
fix:持久化模块的小bug
add:持久化模块的新单元测试
This commit is contained in:
@@ -5,6 +5,7 @@ import minsecurity.identity.Identity;
|
||||
import minsecurity.identity.persist.sqlite.Sqlite;
|
||||
import minsecurity.identity.persist.sqlite.db.Db;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class Persist {
|
||||
@@ -16,6 +17,8 @@ public class Persist {
|
||||
* @throws Exception
|
||||
*/
|
||||
public static void persistIdentity(Identity identity) throws Exception{
|
||||
if (identity == null)
|
||||
throw new Exception("Identity is null");
|
||||
switch (persistPlugin){
|
||||
case Common.SQLITE:
|
||||
Db.persistIdentity(identity);
|
||||
@@ -28,6 +31,8 @@ public class Persist {
|
||||
* @throws Exception
|
||||
*/
|
||||
public static void deleteIdentityByNameFromStorage(String name) throws Exception{
|
||||
if (name == null || name.length() == 0)
|
||||
throw new Exception("name is empty");
|
||||
switch (persistPlugin){
|
||||
case Common.SQLITE:
|
||||
Db.deleteIdentityByName(name);
|
||||
@@ -42,6 +47,9 @@ public class Persist {
|
||||
* @throws Exception
|
||||
*/
|
||||
public static Identity getIdentityByNameFromStorage(String name, String passwd) throws Exception{
|
||||
if (name == null || name.length() == 0)
|
||||
return null;
|
||||
|
||||
switch (persistPlugin){
|
||||
case Common.SQLITE:
|
||||
return Db.getIdentityByNameFromStorage(name);
|
||||
@@ -60,7 +68,7 @@ public class Persist {
|
||||
case Common.SQLITE:
|
||||
return Db.getAllIdentityFromStorage();
|
||||
}
|
||||
return null;
|
||||
return new ArrayList<Identity>();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -69,6 +77,9 @@ public class Persist {
|
||||
* @throws Exception
|
||||
*/
|
||||
public static void setDefaultIdentityByNameInStorage(String name) throws Exception{
|
||||
if (name == null || name.length() == 0)
|
||||
throw new Exception("name is empty");
|
||||
|
||||
switch (persistPlugin){
|
||||
case Common.SQLITE:
|
||||
Db.SetDefaultIdentityByNameInStorage(name);
|
||||
@@ -78,7 +89,7 @@ public class Persist {
|
||||
/**
|
||||
* 获得当前系统默认使用的Identity对象
|
||||
* @param passwd
|
||||
* @return
|
||||
* @return Identity
|
||||
* @throws Exception
|
||||
*/
|
||||
public static Identity getDefaultIdentityFromStorage(String passwd) throws Exception{
|
||||
|
||||
@@ -36,12 +36,16 @@ public class Sqlite {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 将密码转换为16进制表示
|
||||
* @return 16进制字符串 String
|
||||
*/
|
||||
private String passwd2HexKey(){
|
||||
return String.format("x'%s'", passwd);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Sqlite object
|
||||
* 获取Sqlite 实例
|
||||
* @return Sqlite object
|
||||
*/
|
||||
public static Sqlite getInstance() {
|
||||
@@ -90,10 +94,19 @@ public class Sqlite {
|
||||
}
|
||||
}
|
||||
//自定义文件地址,注意要用/结尾,比如:/min/identity/
|
||||
|
||||
/**
|
||||
* 打开用户指定路径的数据库文件
|
||||
* @param filePath String
|
||||
* @throws Exception
|
||||
*/
|
||||
public void open(String filePath) throws Exception {
|
||||
Connection c = null;
|
||||
Statement stmt = null;
|
||||
try{
|
||||
if (filePath == null || filePath.length() == 0){
|
||||
throw new Exception("invalid db path!");
|
||||
}
|
||||
db_path = filePath;
|
||||
boolean db_exists = SqliteUtil.pathExists(db_path);
|
||||
if (!db_exists){
|
||||
@@ -114,6 +127,11 @@ public class Sqlite {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从打开的数据库获取连接
|
||||
* @return Connection
|
||||
* @throws Exception
|
||||
*/
|
||||
public Connection getConn() throws Exception {
|
||||
Connection c = null;
|
||||
try{
|
||||
|
||||
@@ -24,9 +24,11 @@ public final class SqliteUtil {
|
||||
/**
|
||||
* 判断文件是否存在
|
||||
* @param path 特定文件路径
|
||||
* @return 判断结果
|
||||
* @return 判断结果 boolean
|
||||
*/
|
||||
public static boolean pathExists(String path){
|
||||
if (path == null)
|
||||
return false;
|
||||
File file = new File(path);
|
||||
return file.exists();
|
||||
}
|
||||
|
||||
@@ -214,7 +214,7 @@ public final class Db {
|
||||
|
||||
public static void deleteIdentityByName(String name) throws Exception{
|
||||
Connection c = Sqlite.getInstance().getConn();
|
||||
PreparedStatement pstmt = c.prepareStatement("elete from identityinfo where name=?");
|
||||
PreparedStatement pstmt = c.prepareStatement("delete from identityinfo where name=?");
|
||||
pstmt.setString(1, name);
|
||||
pstmt.executeUpdate();
|
||||
pstmt.close();
|
||||
|
||||
@@ -5,10 +5,13 @@ import minsecurity.Common;
|
||||
import minsecurity.certificate.cert.CertUtils;
|
||||
import minsecurity.certificate.cert.Certificate;
|
||||
import minsecurity.crypto.sm2.SM2Base;
|
||||
import minsecurity.crypto.sm2.SM2KeyPair;
|
||||
import minsecurity.crypto.sm2.SM2PrivateKey;
|
||||
import minsecurity.crypto.sm2.SM2PublicKey;
|
||||
import minsecurity.identity.persist.IdentitySerializer;
|
||||
import minsecurity.identity.persist.MapDB;
|
||||
import minsecurity.identity.persist.Persist;
|
||||
import minsecurity.identity.persist.sqlite.Sqlite;
|
||||
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
|
||||
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
|
||||
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
|
||||
@@ -17,6 +20,7 @@ import org.mapdb.BTreeMap;
|
||||
import org.mapdb.DB;
|
||||
import org.mapdb.DBMaker;
|
||||
import org.mapdb.Serializer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.*;
|
||||
@@ -32,136 +36,185 @@ import java.util.concurrent.ConcurrentMap;
|
||||
public class TestPersist {
|
||||
private static final org.slf4j.Logger logger = LoggerFactory.getLogger(TestIdentity.class);
|
||||
|
||||
@Test
|
||||
public void testMapDB() throws Exception {
|
||||
/**
|
||||
* 随机生成身份数据
|
||||
* @return Identity
|
||||
*/
|
||||
private Identity createRandomIdentity() throws Exception{
|
||||
// 测试PersistIdentity
|
||||
SM2KeyPair pair = SM2KeyPair.generateKeyPair();
|
||||
Identity identity = new Identity();
|
||||
identity.setName("wzq"+Math.random());
|
||||
KeyParam keyParam = new KeyParam();
|
||||
keyParam.PublicKeyAlgorithm = 0;
|
||||
keyParam.SignatureAlgorithm = 0;
|
||||
identity.setKeyParam(keyParam);
|
||||
identity.setPrikey(pair.getSm2PrivateKey());
|
||||
identity.setPubkey(pair.getSm2PublicKey());
|
||||
identity.setPasswd("2DD29CA851E7B56E4697B0E1F08507293D761A05CE4D1B628663F411A8086D99");
|
||||
identity.lock("0123456789abcdef", Common.SM4ECB);
|
||||
Certificate cert = new Certificate();
|
||||
cert.setVersion(1);
|
||||
cert.setSerialNumber(1);
|
||||
cert.setPublicKey(pair.getSm2PublicKey());
|
||||
cert.setSignatureAlgorithm(Common.SM3withSM2); // TODO 名字有误? SM2withSM3?
|
||||
cert.setPublicKeyAlgorithm(Common.SM2);
|
||||
cert.setIssueTo("root");
|
||||
cert.setIssuer("root");
|
||||
long timestamp = System.currentTimeMillis() / 1000;
|
||||
cert.setTimestamp(timestamp); // 10bit timestamp
|
||||
cert.setNotAfter(timestamp); // 10bit timestamp
|
||||
cert.setNotBefore(timestamp + 1000); // 10bit timestamp
|
||||
cert.setKeyUsage(Common.CertSign);
|
||||
cert.setCA(true);
|
||||
CertUtils.signCert(cert, pair.getSm2PrivateKey());
|
||||
|
||||
AsymmetricCipherKeyPair keyPair = SM2Base.generateKeyPairParameter();
|
||||
ECPrivateKeyParameters priKey = (ECPrivateKeyParameters) keyPair.getPrivate();
|
||||
ECPublicKeyParameters pubKey = (ECPublicKeyParameters) keyPair.getPublic();
|
||||
byte[] d = priKey.getD().toByteArray();
|
||||
// d = Arrays.copyOf(d,32);
|
||||
byte[] x = pubKey.getQ().getAffineXCoord().getEncoded();
|
||||
byte[] y = pubKey.getQ().getAffineYCoord().getEncoded();
|
||||
// BigInteger bigInteger = priKey.getD();
|
||||
SM2PrivateKey sm2PrivateKey = new SM2PrivateKey(d);
|
||||
SM2PublicKey sm2PublicKey = new SM2PublicKey(x,y);
|
||||
KeyParam keyParam = new KeyParam(Common.SM2, Common.SM3withSM2);
|
||||
Identity identity = new Identity("root",keyParam,sm2PrivateKey,sm2PublicKey, "123456", null, false);
|
||||
Certificate certificate = new Certificate(1, 1, sm2PublicKey, null,
|
||||
Common.SM3withSM2, Common.SM2, "root", "root",
|
||||
System.currentTimeMillis() - 1000, System.currentTimeMillis() + 5000,
|
||||
Common.CertSign, true, System.currentTimeMillis());
|
||||
CertUtils.signCert(certificate, sm2PrivateKey);
|
||||
identity.setCert(certificate);
|
||||
MapDB mapDB = MapDB.getInstance("./target/test.db");
|
||||
int random = new Random().nextInt();
|
||||
mapDB.addIdentity("/test" + random, identity, false);
|
||||
mapDB.commit();
|
||||
mapDB.closeDB();
|
||||
mapDB = MapDB.getInstance("./target/test.db");
|
||||
ArrayList<Identity> arrayList = mapDB.getAllIdentity();
|
||||
logger.debug("db size: {}",arrayList.size());
|
||||
Identity id = mapDB.getIdentityByName("/test" + random);
|
||||
logger.debug(identity.toString());
|
||||
logger.debug(id.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMapDB2() throws Exception {
|
||||
MapDB mapDB = MapDB.getInstance("./target/test.db");
|
||||
for(int i = 0; i < 100; i++){
|
||||
AsymmetricCipherKeyPair keyPair = SM2Base.generateKeyPairParameter();
|
||||
ECPrivateKeyParameters priKey = (ECPrivateKeyParameters) keyPair.getPrivate();
|
||||
ECPublicKeyParameters pubKey = (ECPublicKeyParameters) keyPair.getPublic();
|
||||
byte[] d = priKey.getD().toByteArray();
|
||||
// d = Arrays.copyOf(d,32);
|
||||
byte[] x = pubKey.getQ().getAffineXCoord().getEncoded();
|
||||
byte[] y = pubKey.getQ().getAffineYCoord().getEncoded();
|
||||
// BigInteger bigInteger = priKey.getD();
|
||||
SM2PrivateKey sm2PrivateKey = new SM2PrivateKey(d);
|
||||
SM2PublicKey sm2PublicKey = new SM2PublicKey(x,y);
|
||||
KeyParam keyParam = new KeyParam(Common.SM2, Common.SM3withSM2);
|
||||
Identity identity = new Identity("root",keyParam,sm2PrivateKey,sm2PublicKey, "123456", null, false);
|
||||
Certificate certificate = new Certificate(1, 1, sm2PublicKey, null,
|
||||
Common.SM3withSM2, Common.SM2, "root", "root",
|
||||
System.currentTimeMillis() - 1000, System.currentTimeMillis() + 5000,
|
||||
Common.CertSign, true, System.currentTimeMillis());
|
||||
CertUtils.signCert(certificate, sm2PrivateKey);
|
||||
identity.setCert(certificate);
|
||||
int random = new Random().nextInt();
|
||||
mapDB.addIdentity("/test" + random, identity, false);
|
||||
}
|
||||
mapDB.commit();
|
||||
ArrayList<Identity> arrayList = mapDB.getAllIdentity();
|
||||
logger.debug("size = {}", arrayList.size());
|
||||
for(Identity id : arrayList){
|
||||
logger.debug(id.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHashmap(){
|
||||
HashMap<String, Integer> primeNumbers = new HashMap<>();
|
||||
|
||||
// 往HasMap中添加映射
|
||||
primeNumbers.put("Two", 2);
|
||||
primeNumbers.put("Three", 3);
|
||||
primeNumbers.put("Five", 5);
|
||||
System.out.println("HashMap: " + primeNumbers);
|
||||
|
||||
// 得到value
|
||||
Integer value = primeNumbers.get("Three");
|
||||
System.out.println("key Three 对应的 value: " + value);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDefault() throws Exception {
|
||||
MapDB mapDB = MapDB.getInstance("./target/test.db");
|
||||
for(int i = 0; i < 20; i++){
|
||||
int random = new Random().nextInt();
|
||||
Identity identity = generateIdentity("/test/" + random);
|
||||
mapDB.addIdentity(identity.getName(), identity, false);
|
||||
}
|
||||
// mapDB.commit();
|
||||
ArrayList<Identity> arrayList = mapDB.getAllIdentity();
|
||||
logger.debug("size = {}", arrayList.size());
|
||||
for(Identity id : arrayList){
|
||||
logger.debug(id.getName() + " " + id.isDefault());
|
||||
}
|
||||
logger.debug("----setDefault----");
|
||||
Identity identity = generateIdentity("/abc");
|
||||
identity.setDefault(true);
|
||||
mapDB.addIdentity("/abc", identity, false);
|
||||
mapDB.setDefaultIdentity("/abc",false);
|
||||
mapDB.commit();
|
||||
|
||||
arrayList = mapDB.getAllIdentity();
|
||||
for(Identity id : arrayList){
|
||||
logger.debug(id.getName() + " " + id.isDefault());
|
||||
}
|
||||
logger.debug("get default");
|
||||
logger.debug(mapDB.getDefaultIdentity().toString());
|
||||
}
|
||||
|
||||
|
||||
private Identity generateIdentity(String name) throws Exception {
|
||||
AsymmetricCipherKeyPair keyPair = SM2Base.generateKeyPairParameter();
|
||||
ECPrivateKeyParameters priKey = (ECPrivateKeyParameters) keyPair.getPrivate();
|
||||
ECPublicKeyParameters pubKey = (ECPublicKeyParameters) keyPair.getPublic();
|
||||
byte[] d = priKey.getD().toByteArray();
|
||||
byte[] x = pubKey.getQ().getAffineXCoord().getEncoded();
|
||||
byte[] y = pubKey.getQ().getAffineYCoord().getEncoded();
|
||||
SM2PrivateKey sm2PrivateKey = new SM2PrivateKey(d);
|
||||
SM2PublicKey sm2PublicKey = new SM2PublicKey(x,y);
|
||||
KeyParam keyParam = new KeyParam(Common.SM2, Common.SM3withSM2);
|
||||
Identity identity = new Identity(name,keyParam,sm2PrivateKey,sm2PublicKey, "123456", null, false);
|
||||
Certificate certificate = new Certificate(1, 1, sm2PublicKey, null,
|
||||
Common.SM3withSM2, Common.SM2, "root", "root",
|
||||
System.currentTimeMillis() - 1000, System.currentTimeMillis() + 5000,
|
||||
Common.CertSign, true, System.currentTimeMillis());
|
||||
CertUtils.signCert(certificate, sm2PrivateKey);
|
||||
identity.setCert(certificate);
|
||||
identity.setCert(cert);
|
||||
return identity;
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试PersistIdentity正常使用
|
||||
*/
|
||||
@Test
|
||||
public void testPersistIdentity(){
|
||||
try{
|
||||
// 打开数据库
|
||||
Sqlite.getInstance().openDefault();
|
||||
// 重复五次随机生成并插入数据库过程
|
||||
for (int i = 0; i < 5; i++) {
|
||||
Identity id = createRandomIdentity();
|
||||
Persist.persistIdentity(id);
|
||||
}
|
||||
|
||||
// 获取数据库中所有数据
|
||||
List<Identity> identities = Persist.getAllIdentityFromStorage("");
|
||||
for (Identity i:
|
||||
identities) {
|
||||
logger.debug("当前用户:"+i.getName());
|
||||
// 测试 getIdentityByNameFromStorage
|
||||
Identity id = Persist.getIdentityByNameFromStorage(i.getName(), "");
|
||||
logger.debug("查询用户:"+id.getName());
|
||||
}
|
||||
}catch (Exception ex){
|
||||
logger.debug(String.format("测试失败:%s", ex.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试PersistIdentity异常使用
|
||||
*/
|
||||
@Test
|
||||
public void testPersistIdentity2(){
|
||||
try{
|
||||
// 打开数据库
|
||||
Sqlite.getInstance().openDefault();
|
||||
// 重复五次null插入数据库过程
|
||||
for (int i = 0; i < 5; i++) {
|
||||
Persist.persistIdentity(null); // 抛异常
|
||||
}
|
||||
|
||||
// 获取数据库中所有数据
|
||||
List<Identity> identities = Persist.getAllIdentityFromStorage("");
|
||||
for (Identity i:
|
||||
identities) {
|
||||
logger.debug(i.getName());
|
||||
Identity id = Persist.getIdentityByNameFromStorage(i.getName(), "");
|
||||
logger.debug("查询用户:"+id.getName());
|
||||
}
|
||||
}catch (Exception ex){
|
||||
logger.debug(String.format("测试失败:%s", ex.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试SetDefaultIdentity正常使用
|
||||
*/
|
||||
@Test
|
||||
public void testSetDefaultIdentity(){
|
||||
try{
|
||||
// 打开数据库
|
||||
Sqlite.getInstance().openDefault();
|
||||
|
||||
// 获取数据库中所有数据,并轮流设置默认身份
|
||||
List<Identity> identities = Persist.getAllIdentityFromStorage("");
|
||||
for (Identity i:
|
||||
identities) {
|
||||
logger.debug(String.format("为 %s 设置默认身份", i.getName()));
|
||||
Persist.setDefaultIdentityByNameInStorage(i.getName());
|
||||
Identity did = Persist.getDefaultIdentityFromStorage("");
|
||||
logger.debug(String.format("当前默认身份 %s", did.getName()));
|
||||
}
|
||||
}catch (Exception ex){
|
||||
logger.debug(String.format("测试失败:%s", ex.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试SetDefaultIdentity异常使用
|
||||
*/
|
||||
@Test
|
||||
public void testSetDefaultIdentity2(){
|
||||
try{
|
||||
// 打开数据库
|
||||
Sqlite.getInstance().openDefault();
|
||||
|
||||
// 故意设置错误默认身份
|
||||
List<Identity> identities = Persist.getAllIdentityFromStorage("");
|
||||
for (Identity i:
|
||||
identities) {
|
||||
logger.debug(String.format("为 %s 设置默认身份", i.getName()));
|
||||
Persist.setDefaultIdentityByNameInStorage(null);
|
||||
Persist.setDefaultIdentityByNameInStorage("");
|
||||
Identity did = Persist.getDefaultIdentityFromStorage("");
|
||||
logger.debug(String.format("当前默认身份 %s", did.getName()));
|
||||
}
|
||||
}catch (Exception ex){
|
||||
logger.debug(String.format("测试失败:%s", ex.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试DeleteIdentityByName正常使用
|
||||
*/
|
||||
@Test
|
||||
public void testDeleteIdentityByName(){
|
||||
try{
|
||||
// 打开数据库
|
||||
Sqlite.getInstance().openDefault();
|
||||
|
||||
// 根据用户name先删除再查询
|
||||
List<Identity> identities = Persist.getAllIdentityFromStorage("");
|
||||
for (Identity i:
|
||||
identities) {
|
||||
logger.debug(String.format("删除名称为 %s 的用户", i.getName()));
|
||||
Persist.deleteIdentityByNameFromStorage(i.getName());
|
||||
Identity did = Persist.getIdentityByNameFromStorage(i.getName(), "");
|
||||
logger.debug(String.format("查询 %s 结果: %s", i.getName(), did));
|
||||
}
|
||||
}catch (Exception ex){
|
||||
logger.debug(String.format("测试失败:%s", ex.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试DeleteIdentityByName异常使用
|
||||
*/
|
||||
@Test
|
||||
public void testDeleteIdentityByName2(){
|
||||
try{
|
||||
// 打开数据库
|
||||
Sqlite.getInstance().openDefault();
|
||||
|
||||
// 故意删除错误身份
|
||||
List<Identity> identities = Persist.getAllIdentityFromStorage("");
|
||||
for (Identity i:
|
||||
identities) {
|
||||
logger.debug(String.format("删除名称为 %s 的用户", null));
|
||||
Persist.deleteIdentityByNameFromStorage(null);
|
||||
}
|
||||
}catch (Exception ex){
|
||||
logger.debug(String.format("测试失败:%s", ex.getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,9 @@ public class SqliteTest {
|
||||
assertFalse(SqliteUtil.pathExists("/aaa/1.txt"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 正常情况连接数据库测试
|
||||
*/
|
||||
@Test
|
||||
public void TestSqliteOpen(){
|
||||
try{
|
||||
@@ -35,12 +38,32 @@ public class SqliteTest {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 输入异常数据测试连接数据库
|
||||
*/
|
||||
@Test
|
||||
public void TestSqliteOpen2(){
|
||||
try{
|
||||
// Sqlite.getInstance().open(""); 报异常
|
||||
Sqlite.getInstance().open(null); // 报异常
|
||||
boolean res1 = SqliteUtil.pathExists("/home/zhengqi/test/identity.db");
|
||||
if (!res1){
|
||||
throw new Exception("no target db file!!");
|
||||
}
|
||||
}catch (Exception ex){
|
||||
System.out.println(ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试修改密码后连接数据库
|
||||
*/
|
||||
@Test
|
||||
public void TestSqliteSetPasswd(){
|
||||
try{
|
||||
String newPasswd = "1DD29CA851E7B56E4697B0E1F08507293D761A05CE4D1B628663F411A8086D99";
|
||||
Sqlite.getInstance().setPasswd(newPasswd);
|
||||
Sqlite.getInstance().open("/home/zhengqi/min/identity/");
|
||||
Sqlite.getInstance().open("/home/zhengqi/min/identity/"); // 报异常 数据库密码错误
|
||||
// Sqlite.getInstance().getConn();
|
||||
}catch (Exception ex){
|
||||
System.out.println(ex.getMessage());
|
||||
|
||||
Reference in New Issue
Block a user