未验证 提交 460ed644 编写于 作者: S Shengliang Guan 提交者: GitHub

Merge pull request #3179 from taosdata/TD-1174_feature

TD-1174: jdbc without host ip
...@@ -14,6 +14,7 @@ ...@@ -14,6 +14,7 @@
*****************************************************************************/ *****************************************************************************/
package com.taosdata.jdbc; package com.taosdata.jdbc;
import java.io.*;
import java.sql.Array; import java.sql.Array;
import java.sql.Blob; import java.sql.Blob;
import java.sql.CallableStatement; import java.sql.CallableStatement;
...@@ -30,336 +31,392 @@ import java.sql.SQLXML; ...@@ -30,336 +31,392 @@ import java.sql.SQLXML;
import java.sql.Savepoint; import java.sql.Savepoint;
import java.sql.Statement; import java.sql.Statement;
import java.sql.Struct; import java.sql.Struct;
import java.util.Enumeration; import java.util.*;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
public class TSDBConnection implements Connection { public class TSDBConnection implements Connection {
private TSDBJNIConnector connector = null; private TSDBJNIConnector connector = null;
protected Properties props = null; protected Properties props = null;
private String catalog = null; private String catalog = null;
private TSDBDatabaseMetaData dbMetaData = null; private TSDBDatabaseMetaData dbMetaData = null;
private Properties clientInfoProps = new Properties(); private Properties clientInfoProps = new Properties();
private int timeoutMilliseconds = 0; private int timeoutMilliseconds = 0;
private String tsCharSet = ""; private String tsCharSet = "";
public TSDBConnection(Properties info, TSDBDatabaseMetaData meta) throws SQLException { public TSDBConnection(Properties info, TSDBDatabaseMetaData meta) throws SQLException {
this.dbMetaData = meta; this.dbMetaData = meta;
connect(info.getProperty(TSDBDriver.PROPERTY_KEY_HOST),
Integer.parseInt(info.getProperty(TSDBDriver.PROPERTY_KEY_PORT, "0")), //load taos.cfg start
info.getProperty(TSDBDriver.PROPERTY_KEY_DBNAME), info.getProperty(TSDBDriver.PROPERTY_KEY_USER), File cfgDir = loadConfigDir(info.getProperty(TSDBDriver.PROPERTY_KEY_CONFIG_DIR));
info.getProperty(TSDBDriver.PROPERTY_KEY_PASSWORD)); File cfgFile = cfgDir.listFiles((dir, name) -> "taos.cfg".equalsIgnoreCase(name))[0];
} List<String> endpoints = loadConfigEndpoints(cfgFile);
if (!endpoints.isEmpty()){
private void connect(String host, int port, String dbName, String user, String password) throws SQLException { info.setProperty(TSDBDriver.PROPERTY_KEY_HOST,endpoints.get(0).split(":")[0]);
this.connector = new TSDBJNIConnector(); info.setProperty(TSDBDriver.PROPERTY_KEY_PORT,endpoints.get(0).split(":")[1]);
this.connector.connect(host, port, dbName, user, password); }
//load taos.cfg end
try {
this.setCatalog(dbName); connect(info.getProperty(TSDBDriver.PROPERTY_KEY_HOST),
} catch (SQLException e) { Integer.parseInt(info.getProperty(TSDBDriver.PROPERTY_KEY_PORT, "0")),
e.printStackTrace(); info.getProperty(TSDBDriver.PROPERTY_KEY_DBNAME), info.getProperty(TSDBDriver.PROPERTY_KEY_USER),
} info.getProperty(TSDBDriver.PROPERTY_KEY_PASSWORD));
}
this.dbMetaData.setConnection(this);
} private List<String> loadConfigEndpoints(File cfgFile){
List<String> endpoints = new ArrayList<>();
public TSDBJNIConnector getConnection() { try(BufferedReader reader = new BufferedReader(new FileReader(cfgFile))) {
return this.connector; String line = null;
} while ((line = reader.readLine())!=null){
if (line.trim().startsWith("firstEp") || line.trim().startsWith("secondEp")){
public Statement createStatement() throws SQLException { endpoints.add(line.substring(line.indexOf('p')+1).trim());
if (!this.connector.isClosed()) { }
return new TSDBStatement(this.connector); if (endpoints.size()>1)
} else { break;
throw new SQLException(TSDBConstants.FixErrMsg(TSDBConstants.JNI_CONNECTION_NULL)); }
} } catch (FileNotFoundException e) {
} e.printStackTrace();
} catch (IOException e) {
public TSDBSubscribe subscribe(String topic, String sql, boolean restart) throws SQLException { e.printStackTrace();
if (this.connector.isClosed()) { }
throw new SQLException(TSDBConstants.FixErrMsg(TSDBConstants.JNI_CONNECTION_NULL)); return endpoints;
} }
long id = this.connector.subscribe(topic, sql, restart, 0); /**
if (id == 0) { * @param cfgDirPath
throw new SQLException(TSDBConstants.WrapErrMsg("failed to create subscription")); * @return return the config dir
} * **/
private File loadConfigDir(String cfgDirPath) {
return new TSDBSubscribe(this.connector, id); if (cfgDirPath == null)
} return loadDefaultConfigDir();
File cfgDir = new File(cfgDirPath);
public PreparedStatement prepareStatement(String sql) throws SQLException { if (!cfgDir.exists())
if (!this.connector.isClosed()) { return loadDefaultConfigDir();
return new TSDBPreparedStatement(this.connector, sql); return cfgDir;
} else { }
throw new SQLException(TSDBConstants.FixErrMsg(TSDBConstants.JNI_CONNECTION_NULL));
} /**
} * @return search the default config dir, if the config dir is not exist will return null
* */
public CallableStatement prepareCall(String sql) throws SQLException { private File loadDefaultConfigDir(){
throw new SQLException(TSDBConstants.UNSUPPORT_METHOD_EXCEPTIONZ_MSG); File cfgDir;
} File cfgDir_linux = new File("/etc/taos");
cfgDir = cfgDir_linux.exists() ? cfgDir_linux : null;
public String nativeSQL(String sql) throws SQLException { File cfgDir_windows = new File("C:\\TDengine\\cfg");
throw new SQLException(TSDBConstants.UNSUPPORT_METHOD_EXCEPTIONZ_MSG); cfgDir = (cfgDir == null && cfgDir_windows.exists()) ? cfgDir_windows : cfgDir;
} return cfgDir;
}
public void setAutoCommit(boolean autoCommit) throws SQLException {
} private void connect(String host, int port, String dbName, String user, String password) throws SQLException {
this.connector = new TSDBJNIConnector();
public boolean getAutoCommit() throws SQLException { this.connector.connect(host, port, dbName, user, password);
return true;
} try {
this.setCatalog(dbName);
public void commit() throws SQLException { } catch (SQLException e) {
} e.printStackTrace();
}
public void rollback() throws SQLException {
throw new SQLException(TSDBConstants.UNSUPPORT_METHOD_EXCEPTIONZ_MSG); this.dbMetaData.setConnection(this);
} }
public void close() throws SQLException { public TSDBJNIConnector getConnection() {
if (this.connector != null && !this.connector.isClosed()) { return this.connector;
this.connector.closeConnection(); }
} else {
throw new SQLException(TSDBConstants.WrapErrMsg("connection is already closed!")); public Statement createStatement() throws SQLException {
} if (!this.connector.isClosed()) {
} return new TSDBStatement(this.connector);
} else {
public boolean isClosed() throws SQLException { throw new SQLException(TSDBConstants.FixErrMsg(TSDBConstants.JNI_CONNECTION_NULL));
return this.connector.isClosed(); }
} }
/** public TSDBSubscribe subscribe(String topic, String sql, boolean restart) throws SQLException {
* A connection's database is able to provide information describing its tables, if (this.connector.isClosed()) {
* its supported SQL grammar, its stored procedures, the capabilities of this throw new SQLException(TSDBConstants.FixErrMsg(TSDBConstants.JNI_CONNECTION_NULL));
* connection, etc. This information is made available through a }
* DatabaseMetaData object.
* long id = this.connector.subscribe(topic, sql, restart, 0);
* @return a DatabaseMetaData object for this connection if (id == 0) {
* @exception SQLException throw new SQLException(TSDBConstants.WrapErrMsg("failed to create subscription"));
* if a database access error occurs }
*/
public DatabaseMetaData getMetaData() throws SQLException { return new TSDBSubscribe(this.connector, id);
return this.dbMetaData; }
}
public PreparedStatement prepareStatement(String sql) throws SQLException {
/** if (!this.connector.isClosed()) {
* This readOnly option is not supported by TDengine. However, the method is intentionally left blank here to return new TSDBPreparedStatement(this.connector, sql);
* support HikariCP connection. } else {
* @param readOnly throw new SQLException(TSDBConstants.FixErrMsg(TSDBConstants.JNI_CONNECTION_NULL));
* @throws SQLException }
*/ }
public void setReadOnly(boolean readOnly) throws SQLException {
} public CallableStatement prepareCall(String sql) throws SQLException {
throw new SQLException(TSDBConstants.UNSUPPORT_METHOD_EXCEPTIONZ_MSG);
public boolean isReadOnly() throws SQLException { }
return true;
} public String nativeSQL(String sql) throws SQLException {
throw new SQLException(TSDBConstants.UNSUPPORT_METHOD_EXCEPTIONZ_MSG);
public void setCatalog(String catalog) throws SQLException { }
this.catalog = catalog;
} public void setAutoCommit(boolean autoCommit) throws SQLException {
}
public String getCatalog() throws SQLException {
return this.catalog; public boolean getAutoCommit() throws SQLException {
} return true;
}
/**
* The transaction isolation level option is not supported by TDengine. public void commit() throws SQLException {
* This method is intentionally left empty to support HikariCP connection. }
* @param level
* @throws SQLException public void rollback() throws SQLException {
*/ throw new SQLException(TSDBConstants.UNSUPPORT_METHOD_EXCEPTIONZ_MSG);
public void setTransactionIsolation(int level) throws SQLException { }
}
public void close() throws SQLException {
/** if (this.connector != null && !this.connector.isClosed()) {
* The transaction isolation level option is not supported by TDengine. this.connector.closeConnection();
* @return } else {
* @throws SQLException throw new SQLException(TSDBConstants.WrapErrMsg("connection is already closed!"));
*/ }
public int getTransactionIsolation() throws SQLException { }
return Connection.TRANSACTION_NONE;
} public boolean isClosed() throws SQLException {
return this.connector.isClosed();
public SQLWarning getWarnings() throws SQLException { }
throw new SQLException(TSDBConstants.UNSUPPORT_METHOD_EXCEPTIONZ_MSG);
} /**
* A connection's database is able to provide information describing its tables,
public void clearWarnings() throws SQLException { * its supported SQL grammar, its stored procedures, the capabilities of this
// left blank to support HikariCP connection * connection, etc. This information is made available through a
//todo: implement getWarnings according to the warning messages returned from TDengine * DatabaseMetaData object.
} *
* @return a DatabaseMetaData object for this connection
public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException { * @throws SQLException if a database access error occurs
throw new SQLException(TSDBConstants.UNSUPPORT_METHOD_EXCEPTIONZ_MSG); */
} public DatabaseMetaData getMetaData() throws SQLException {
return this.dbMetaData;
public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) }
throws SQLException {
// This method is implemented in the current way to support Spark /**
if (resultSetType != ResultSet.TYPE_FORWARD_ONLY) { * This readOnly option is not supported by TDengine. However, the method is intentionally left blank here to
throw new SQLException(TSDBConstants.INVALID_VARIABLES); * support HikariCP connection.
} *
* @param readOnly
if (resultSetConcurrency != ResultSet.CONCUR_READ_ONLY) { * @throws SQLException
throw new SQLException(TSDBConstants.INVALID_VARIABLES); */
} public void setReadOnly(boolean readOnly) throws SQLException {
}
return this.prepareStatement(sql);
} public boolean isReadOnly() throws SQLException {
return true;
public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { }
throw new SQLException(TSDBConstants.UNSUPPORT_METHOD_EXCEPTIONZ_MSG);
} public void setCatalog(String catalog) throws SQLException {
this.catalog = catalog;
public Map<String, Class<?>> getTypeMap() throws SQLException { }
throw new SQLException(TSDBConstants.UNSUPPORT_METHOD_EXCEPTIONZ_MSG);
} public String getCatalog() throws SQLException {
return this.catalog;
public void setTypeMap(Map<String, Class<?>> map) throws SQLException { }
throw new SQLException(TSDBConstants.UNSUPPORT_METHOD_EXCEPTIONZ_MSG);
} /**
* The transaction isolation level option is not supported by TDengine.
public void setHoldability(int holdability) throws SQLException { * This method is intentionally left empty to support HikariCP connection.
// intentionally left empty to support druid connection pool. *
} * @param level
* @throws SQLException
/** */
* the transaction is not supported by TDengine, so the opened ResultSet Objects will remain open public void setTransactionIsolation(int level) throws SQLException {
* @return }
* @throws SQLException
*/ /**
public int getHoldability() throws SQLException { * The transaction isolation level option is not supported by TDengine.
//intentionally left empty to support HikariCP connection. *
return ResultSet.HOLD_CURSORS_OVER_COMMIT; * @return
} * @throws SQLException
*/
public Savepoint setSavepoint() throws SQLException { public int getTransactionIsolation() throws SQLException {
throw new SQLException(TSDBConstants.UNSUPPORT_METHOD_EXCEPTIONZ_MSG); return Connection.TRANSACTION_NONE;
} }
public Savepoint setSavepoint(String name) throws SQLException { public SQLWarning getWarnings() throws SQLException {
throw new SQLException(TSDBConstants.UNSUPPORT_METHOD_EXCEPTIONZ_MSG); throw new SQLException(TSDBConstants.UNSUPPORT_METHOD_EXCEPTIONZ_MSG);
} }
public void rollback(Savepoint savepoint) throws SQLException { public void clearWarnings() throws SQLException {
throw new SQLException(TSDBConstants.UNSUPPORT_METHOD_EXCEPTIONZ_MSG); // left blank to support HikariCP connection
} //todo: implement getWarnings according to the warning messages returned from TDengine
}
public void releaseSavepoint(Savepoint savepoint) throws SQLException {
throw new SQLException(TSDBConstants.UNSUPPORT_METHOD_EXCEPTIONZ_MSG); public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException {
} throw new SQLException(TSDBConstants.UNSUPPORT_METHOD_EXCEPTIONZ_MSG);
}
public Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability)
throws SQLException { public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency)
throw new SQLException(TSDBConstants.UNSUPPORT_METHOD_EXCEPTIONZ_MSG); throws SQLException {
} // This method is implemented in the current way to support Spark
if (resultSetType != ResultSet.TYPE_FORWARD_ONLY) {
public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency, throw new SQLException(TSDBConstants.INVALID_VARIABLES);
int resultSetHoldability) throws SQLException { }
return this.prepareStatement(sql, resultSetType, resultSetConcurrency);
} if (resultSetConcurrency != ResultSet.CONCUR_READ_ONLY) {
throw new SQLException(TSDBConstants.INVALID_VARIABLES);
public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency, }
int resultSetHoldability) throws SQLException {
throw new SQLException(TSDBConstants.UNSUPPORT_METHOD_EXCEPTIONZ_MSG); return this.prepareStatement(sql);
} }
public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException { public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException {
throw new SQLException(TSDBConstants.UNSUPPORT_METHOD_EXCEPTIONZ_MSG); throw new SQLException(TSDBConstants.UNSUPPORT_METHOD_EXCEPTIONZ_MSG);
} }
public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException { public Map<String, Class<?>> getTypeMap() throws SQLException {
throw new SQLException(TSDBConstants.UNSUPPORT_METHOD_EXCEPTIONZ_MSG); throw new SQLException(TSDBConstants.UNSUPPORT_METHOD_EXCEPTIONZ_MSG);
} }
public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException { public void setTypeMap(Map<String, Class<?>> map) throws SQLException {
throw new SQLException(TSDBConstants.UNSUPPORT_METHOD_EXCEPTIONZ_MSG); throw new SQLException(TSDBConstants.UNSUPPORT_METHOD_EXCEPTIONZ_MSG);
} }
public Clob createClob() throws SQLException { public void setHoldability(int holdability) throws SQLException {
throw new SQLException(TSDBConstants.UNSUPPORT_METHOD_EXCEPTIONZ_MSG); // intentionally left empty to support druid connection pool.
} }
public Blob createBlob() throws SQLException { /**
throw new SQLException(TSDBConstants.UNSUPPORT_METHOD_EXCEPTIONZ_MSG); * the transaction is not supported by TDengine, so the opened ResultSet Objects will remain open
} *
* @return
public NClob createNClob() throws SQLException { * @throws SQLException
throw new SQLException(TSDBConstants.UNSUPPORT_METHOD_EXCEPTIONZ_MSG); */
} public int getHoldability() throws SQLException {
//intentionally left empty to support HikariCP connection.
public SQLXML createSQLXML() throws SQLException { return ResultSet.HOLD_CURSORS_OVER_COMMIT;
throw new SQLException(TSDBConstants.UNSUPPORT_METHOD_EXCEPTIONZ_MSG); }
}
public Savepoint setSavepoint() throws SQLException {
public boolean isValid(int timeout) throws SQLException { throw new SQLException(TSDBConstants.UNSUPPORT_METHOD_EXCEPTIONZ_MSG);
return !this.isClosed(); }
}
public Savepoint setSavepoint(String name) throws SQLException {
public void setClientInfo(String name, String value) throws SQLClientInfoException { throw new SQLException(TSDBConstants.UNSUPPORT_METHOD_EXCEPTIONZ_MSG);
clientInfoProps.setProperty(name, value); }
}
public void rollback(Savepoint savepoint) throws SQLException {
public void setClientInfo(Properties properties) throws SQLClientInfoException { throw new SQLException(TSDBConstants.UNSUPPORT_METHOD_EXCEPTIONZ_MSG);
for (Enumeration<Object> enumer = properties.keys(); enumer.hasMoreElements();) { }
String name = (String) enumer.nextElement();
clientInfoProps.put(name, properties.getProperty(name)); public void releaseSavepoint(Savepoint savepoint) throws SQLException {
} throw new SQLException(TSDBConstants.UNSUPPORT_METHOD_EXCEPTIONZ_MSG);
} }
public String getClientInfo(String name) throws SQLException { public Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability)
return clientInfoProps.getProperty(name); throws SQLException {
} throw new SQLException(TSDBConstants.UNSUPPORT_METHOD_EXCEPTIONZ_MSG);
}
public Properties getClientInfo() throws SQLException {
return clientInfoProps; public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency,
} int resultSetHoldability) throws SQLException {
return this.prepareStatement(sql, resultSetType, resultSetConcurrency);
public Array createArrayOf(String typeName, Object[] elements) throws SQLException { }
throw new SQLException(TSDBConstants.UNSUPPORT_METHOD_EXCEPTIONZ_MSG);
} public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency,
int resultSetHoldability) throws SQLException {
public Struct createStruct(String typeName, Object[] attributes) throws SQLException { throw new SQLException(TSDBConstants.UNSUPPORT_METHOD_EXCEPTIONZ_MSG);
throw new SQLException(TSDBConstants.UNSUPPORT_METHOD_EXCEPTIONZ_MSG); }
}
public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException {
public void setSchema(String schema) throws SQLException { throw new SQLException(TSDBConstants.UNSUPPORT_METHOD_EXCEPTIONZ_MSG);
throw new SQLException(TSDBConstants.UNSUPPORT_METHOD_EXCEPTIONZ_MSG); }
}
public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException {
public String getSchema() throws SQLException { throw new SQLException(TSDBConstants.UNSUPPORT_METHOD_EXCEPTIONZ_MSG);
throw new SQLException(TSDBConstants.UNSUPPORT_METHOD_EXCEPTIONZ_MSG); }
}
public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException {
public void abort(Executor executor) throws SQLException { throw new SQLException(TSDBConstants.UNSUPPORT_METHOD_EXCEPTIONZ_MSG);
throw new SQLException(TSDBConstants.UNSUPPORT_METHOD_EXCEPTIONZ_MSG); }
}
public Clob createClob() throws SQLException {
public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException { throw new SQLException(TSDBConstants.UNSUPPORT_METHOD_EXCEPTIONZ_MSG);
this.timeoutMilliseconds = milliseconds; }
}
public Blob createBlob() throws SQLException {
public int getNetworkTimeout() throws SQLException { throw new SQLException(TSDBConstants.UNSUPPORT_METHOD_EXCEPTIONZ_MSG);
return this.timeoutMilliseconds; }
}
public NClob createNClob() throws SQLException {
public <T> T unwrap(Class<T> iface) throws SQLException { throw new SQLException(TSDBConstants.UNSUPPORT_METHOD_EXCEPTIONZ_MSG);
throw new SQLException(TSDBConstants.UNSUPPORT_METHOD_EXCEPTIONZ_MSG); }
}
public SQLXML createSQLXML() throws SQLException {
public boolean isWrapperFor(Class<?> iface) throws SQLException { throw new SQLException(TSDBConstants.UNSUPPORT_METHOD_EXCEPTIONZ_MSG);
throw new SQLException(TSDBConstants.UNSUPPORT_METHOD_EXCEPTIONZ_MSG); }
}
public boolean isValid(int timeout) throws SQLException {
return !this.isClosed();
}
public void setClientInfo(String name, String value) throws SQLClientInfoException {
clientInfoProps.setProperty(name, value);
}
public void setClientInfo(Properties properties) throws SQLClientInfoException {
for (Enumeration<Object> enumer = properties.keys(); enumer.hasMoreElements(); ) {
String name = (String) enumer.nextElement();
clientInfoProps.put(name, properties.getProperty(name));
}
}
public String getClientInfo(String name) throws SQLException {
return clientInfoProps.getProperty(name);
}
public Properties getClientInfo() throws SQLException {
return clientInfoProps;
}
public Array createArrayOf(String typeName, Object[] elements) throws SQLException {
throw new SQLException(TSDBConstants.UNSUPPORT_METHOD_EXCEPTIONZ_MSG);
}
public Struct createStruct(String typeName, Object[] attributes) throws SQLException {
throw new SQLException(TSDBConstants.UNSUPPORT_METHOD_EXCEPTIONZ_MSG);
}
public void setSchema(String schema) throws SQLException {
throw new SQLException(TSDBConstants.UNSUPPORT_METHOD_EXCEPTIONZ_MSG);
}
public String getSchema() throws SQLException {
throw new SQLException(TSDBConstants.UNSUPPORT_METHOD_EXCEPTIONZ_MSG);
}
public void abort(Executor executor) throws SQLException {
throw new SQLException(TSDBConstants.UNSUPPORT_METHOD_EXCEPTIONZ_MSG);
}
public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException {
this.timeoutMilliseconds = milliseconds;
}
public int getNetworkTimeout() throws SQLException {
return this.timeoutMilliseconds;
}
public <T> T unwrap(Class<T> iface) throws SQLException {
throw new SQLException(TSDBConstants.UNSUPPORT_METHOD_EXCEPTIONZ_MSG);
}
public boolean isWrapperFor(Class<?> iface) throws SQLException {
throw new SQLException(TSDBConstants.UNSUPPORT_METHOD_EXCEPTIONZ_MSG);
}
} }
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册