提交 0a8d42b1 编写于 作者: Z zyyang

TD-1174: jdbc without host ip

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