mirror of
https://github.com/vanitasvitae/Smack.git
synced 2024-11-23 04:22:05 +01:00
SMACK-412 Added the pingMyServer back in, cleaned up unneeded synchronization and removed minimum ping interval.
git-svn-id: http://svn.igniterealtime.org/svn/repos/smack/branches/smack_3_3_0@13588 b35dd754-fafc-0310-a699-88a17e54d16e
This commit is contained in:
parent
999c86ef4c
commit
a14178990b
8 changed files with 174 additions and 42 deletions
|
@ -162,7 +162,6 @@ class PacketWriter {
|
||||||
while (!done && (writerThread == thisThread)) {
|
while (!done && (writerThread == thisThread)) {
|
||||||
Packet packet = nextPacket();
|
Packet packet = nextPacket();
|
||||||
if (packet != null) {
|
if (packet != null) {
|
||||||
synchronized (writer) {
|
|
||||||
writer.write(packet.toXML());
|
writer.write(packet.toXML());
|
||||||
writer.flush();
|
writer.flush();
|
||||||
if (queue.isEmpty()) {
|
if (queue.isEmpty()) {
|
||||||
|
@ -170,19 +169,16 @@ class PacketWriter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// Flush out the rest of the queue. If the queue is extremely large, it's possible
|
// Flush out the rest of the queue. If the queue is extremely large, it's possible
|
||||||
// we won't have time to entirely flush it before the socket is forced closed
|
// we won't have time to entirely flush it before the socket is forced closed
|
||||||
// by the shutdown process.
|
// by the shutdown process.
|
||||||
try {
|
try {
|
||||||
synchronized (writer) {
|
|
||||||
while (!queue.isEmpty()) {
|
while (!queue.isEmpty()) {
|
||||||
Packet packet = queue.remove();
|
Packet packet = queue.remove();
|
||||||
writer.write(packet.toXML());
|
writer.write(packet.toXML());
|
||||||
}
|
}
|
||||||
writer.flush();
|
writer.flush();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
catch (Exception e) {
|
catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
|
|
24
source/org/jivesoftware/smack/SmackError.java
Normal file
24
source/org/jivesoftware/smack/SmackError.java
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
package org.jivesoftware.smack;
|
||||||
|
|
||||||
|
public enum SmackError {
|
||||||
|
NO_RESPONSE_FROM_SERVER("No response from server.");
|
||||||
|
|
||||||
|
private String message;
|
||||||
|
|
||||||
|
private SmackError(String errMessage) {
|
||||||
|
message = errMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getErrorMessage() {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SmackError getErrorCode(String message) {
|
||||||
|
for (SmackError code : values()) {
|
||||||
|
if (code.message.equals(message)) {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -41,10 +41,12 @@ import java.io.PrintWriter;
|
||||||
* @author Matt Tucker
|
* @author Matt Tucker
|
||||||
*/
|
*/
|
||||||
public class XMPPException extends Exception {
|
public class XMPPException extends Exception {
|
||||||
|
private static final long serialVersionUID = 6881651633890968625L;
|
||||||
|
|
||||||
private StreamError streamError = null;
|
private StreamError streamError = null;
|
||||||
private XMPPError error = null;
|
private XMPPError error = null;
|
||||||
private Throwable wrappedThrowable = null;
|
private Throwable wrappedThrowable = null;
|
||||||
|
private SmackError smackError = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new XMPPException.
|
* Creates a new XMPPException.
|
||||||
|
@ -62,6 +64,16 @@ public class XMPPException extends Exception {
|
||||||
super(message);
|
super(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new XMPPException with a Smack specific error code.
|
||||||
|
*
|
||||||
|
* @param code the root cause of the exception.
|
||||||
|
*/
|
||||||
|
public XMPPException(SmackError code) {
|
||||||
|
super(code.getErrorMessage());
|
||||||
|
smackError = code;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new XMPPException with the Throwable that was the root cause of the
|
* Creates a new XMPPException with the Throwable that was the root cause of the
|
||||||
* exception.
|
* exception.
|
||||||
|
@ -74,7 +86,7 @@ public class XMPPException extends Exception {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cretaes a new XMPPException with the stream error that was the root case of the
|
* Creates a new XMPPException with the stream error that was the root case of the
|
||||||
* exception. When a stream error is received from the server then the underlying
|
* exception. When a stream error is received from the server then the underlying
|
||||||
* TCP connection will be closed by the server.
|
* TCP connection will be closed by the server.
|
||||||
*
|
*
|
||||||
|
@ -144,6 +156,16 @@ public class XMPPException extends Exception {
|
||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the SmackError asscociated with this exception, or <tt>null</tt> if there
|
||||||
|
* isn't one.
|
||||||
|
*
|
||||||
|
* @return the SmackError asscociated with this exception.
|
||||||
|
*/
|
||||||
|
public SmackError getSmackError() {
|
||||||
|
return smackError;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the StreamError asscociated with this exception, or <tt>null</tt> if there
|
* Returns the StreamError asscociated with this exception, or <tt>null</tt> if there
|
||||||
* isn't one. The underlying TCP connection is closed by the server after sending the
|
* isn't one. The underlying TCP connection is closed by the server after sending the
|
||||||
|
|
|
@ -57,8 +57,6 @@ import org.jivesoftware.smackx.ServiceDiscoveryManager;
|
||||||
* @author Florian Schmaus
|
* @author Florian Schmaus
|
||||||
*/
|
*/
|
||||||
public class ServerPingManager {
|
public class ServerPingManager {
|
||||||
public static final long PING_MINIMUM = 10000;
|
|
||||||
|
|
||||||
private static Map<Connection, ServerPingManager> instances = Collections
|
private static Map<Connection, ServerPingManager> instances = Collections
|
||||||
.synchronizedMap(new WeakHashMap<Connection, ServerPingManager>());
|
.synchronizedMap(new WeakHashMap<Connection, ServerPingManager>());
|
||||||
private static long defaultPingInterval = SmackConfiguration.getKeepAliveInterval();
|
private static long defaultPingInterval = SmackConfiguration.getKeepAliveInterval();
|
||||||
|
@ -173,14 +171,17 @@ public class ServerPingManager {
|
||||||
* The new ping time interval in milliseconds.
|
* The new ping time interval in milliseconds.
|
||||||
*/
|
*/
|
||||||
public void setPingInterval(long newPingInterval) {
|
public void setPingInterval(long newPingInterval) {
|
||||||
if (newPingInterval < PING_MINIMUM)
|
|
||||||
newPingInterval = PING_MINIMUM;
|
|
||||||
|
|
||||||
if (pingInterval != newPingInterval) {
|
if (pingInterval != newPingInterval) {
|
||||||
pingInterval = newPingInterval;
|
pingInterval = newPingInterval;
|
||||||
|
|
||||||
|
if (pingInterval < 0) {
|
||||||
|
stopPinging();
|
||||||
|
}
|
||||||
|
else {
|
||||||
schedulePingServerTask();
|
schedulePingServerTask();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stops pinging the server. This cannot stop a ping that has already started, but will prevent another from being triggered.
|
* Stops pinging the server. This cannot stop a ping that has already started, but will prevent another from being triggered.
|
||||||
|
|
|
@ -16,6 +16,7 @@ package org.jivesoftware.smack.util;
|
||||||
import org.jivesoftware.smack.PacketCollector;
|
import org.jivesoftware.smack.PacketCollector;
|
||||||
import org.jivesoftware.smack.SmackConfiguration;
|
import org.jivesoftware.smack.SmackConfiguration;
|
||||||
import org.jivesoftware.smack.Connection;
|
import org.jivesoftware.smack.Connection;
|
||||||
|
import org.jivesoftware.smack.SmackError;
|
||||||
import org.jivesoftware.smack.XMPPException;
|
import org.jivesoftware.smack.XMPPException;
|
||||||
import org.jivesoftware.smack.filter.PacketFilter;
|
import org.jivesoftware.smack.filter.PacketFilter;
|
||||||
import org.jivesoftware.smack.filter.PacketIDFilter;
|
import org.jivesoftware.smack.filter.PacketIDFilter;
|
||||||
|
@ -47,7 +48,7 @@ final public class SyncPacketSend
|
||||||
response.cancel();
|
response.cancel();
|
||||||
|
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
throw new XMPPException("No response from " + packet.getTo());
|
throw new XMPPException(SmackError.NO_RESPONSE_FROM_SERVER);
|
||||||
}
|
}
|
||||||
else if (result.getError() != null) {
|
else if (result.getError() != null) {
|
||||||
throw new XMPPException(result.getError());
|
throw new XMPPException(result.getError());
|
||||||
|
|
|
@ -18,6 +18,7 @@ package org.jivesoftware.smackx.ping;
|
||||||
|
|
||||||
import org.jivesoftware.smack.Connection;
|
import org.jivesoftware.smack.Connection;
|
||||||
import org.jivesoftware.smack.SmackConfiguration;
|
import org.jivesoftware.smack.SmackConfiguration;
|
||||||
|
import org.jivesoftware.smack.SmackError;
|
||||||
import org.jivesoftware.smack.XMPPException;
|
import org.jivesoftware.smack.XMPPException;
|
||||||
import org.jivesoftware.smack.ping.ServerPingManager;
|
import org.jivesoftware.smack.ping.ServerPingManager;
|
||||||
import org.jivesoftware.smack.ping.packet.Ping;
|
import org.jivesoftware.smack.ping.packet.Ping;
|
||||||
|
@ -49,7 +50,9 @@ public class PingManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pings the given jid. This method will return false if an error occurs.
|
* Pings the given jid. This method will return false if an error occurs. The exception
|
||||||
|
* to this, is a server ping, which will always return true if the server is reachable,
|
||||||
|
* event if there is an error on the ping itself (i.e. ping not supported).
|
||||||
* <p>
|
* <p>
|
||||||
* Use {@link #isPingSupported(String)} to determine if XMPP Ping is supported
|
* Use {@link #isPingSupported(String)} to determine if XMPP Ping is supported
|
||||||
* by the entity.
|
* by the entity.
|
||||||
|
@ -65,7 +68,8 @@ public class PingManager {
|
||||||
SyncPacketSend.getReply(connection, ping);
|
SyncPacketSend.getReply(connection, ping);
|
||||||
}
|
}
|
||||||
catch (XMPPException exc) {
|
catch (XMPPException exc) {
|
||||||
return false;
|
|
||||||
|
return (jid.equals(connection.getServiceName()) && (exc.getSmackError() != SmackError.NO_RESPONSE_FROM_SERVER));
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -92,4 +96,17 @@ public class PingManager {
|
||||||
DiscoverInfo result = ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo(jid);
|
DiscoverInfo result = ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo(jid);
|
||||||
return result.containsFeature(Ping.NAMESPACE);
|
return result.containsFeature(Ping.NAMESPACE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pings the server. This method will return true if the server is reachable. It
|
||||||
|
* is the equivalent of calling <code>ping</code> with the XMPP domain.
|
||||||
|
* <p>
|
||||||
|
* Unlike the {@link #ping(String)} case, this method will return true even if
|
||||||
|
* {@link #isPingSupported(String)} is false.
|
||||||
|
*
|
||||||
|
* @return true if a reply was received from the server, false otherwise.
|
||||||
|
*/
|
||||||
|
public boolean pingMyServer() {
|
||||||
|
return ping(connection.getServiceName());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,9 +21,12 @@ import org.jivesoftware.smack.packet.IQ;
|
||||||
import org.jivesoftware.smack.packet.Packet;
|
import org.jivesoftware.smack.packet.Packet;
|
||||||
import org.jivesoftware.smack.ping.packet.Ping;
|
import org.jivesoftware.smack.ping.packet.Ping;
|
||||||
import org.jivesoftware.smack.util.PacketParserUtils;
|
import org.jivesoftware.smack.util.PacketParserUtils;
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
public class ServerPingTest {
|
public class KeepaliveTest {
|
||||||
|
private static final long PING_MINIMUM = 1000;
|
||||||
private static String TO = "juliet@capulet.lit/balcony";
|
private static String TO = "juliet@capulet.lit/balcony";
|
||||||
private static String ID = "s2c1";
|
private static String ID = "s2c1";
|
||||||
|
|
||||||
|
@ -32,6 +35,20 @@ public class ServerPingTest {
|
||||||
outputProperties.put(javax.xml.transform.OutputKeys.OMIT_XML_DECLARATION, "yes");
|
outputProperties.put(javax.xml.transform.OutputKeys.OMIT_XML_DECLARATION, "yes");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int originalTimeout;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void resetProperties()
|
||||||
|
{
|
||||||
|
originalTimeout = SmackConfiguration.getPacketReplyTimeout();
|
||||||
|
SmackConfiguration.setPacketReplyTimeout(1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void restoreProperties()
|
||||||
|
{
|
||||||
|
SmackConfiguration.setPacketReplyTimeout(originalTimeout);
|
||||||
|
}
|
||||||
/*
|
/*
|
||||||
* Stanza copied from spec
|
* Stanza copied from spec
|
||||||
*/
|
*/
|
||||||
|
@ -133,7 +150,7 @@ public class ServerPingTest {
|
||||||
private DummyConnection getConnection() {
|
private DummyConnection getConnection() {
|
||||||
DummyConnection con = new DummyConnection();
|
DummyConnection con = new DummyConnection();
|
||||||
ServerPingManager mgr = ServerPingManager.getInstanceFor(con);
|
ServerPingManager mgr = ServerPingManager.getInstanceFor(con);
|
||||||
mgr.setPingInterval(ServerPingManager.PING_MINIMUM);
|
mgr.setPingInterval(PING_MINIMUM);
|
||||||
|
|
||||||
return con;
|
return con;
|
||||||
}
|
}
|
||||||
|
@ -141,7 +158,7 @@ public class ServerPingTest {
|
||||||
private ThreadedDummyConnection getThreadedConnection() {
|
private ThreadedDummyConnection getThreadedConnection() {
|
||||||
ThreadedDummyConnection con = new ThreadedDummyConnection();
|
ThreadedDummyConnection con = new ThreadedDummyConnection();
|
||||||
ServerPingManager mgr = ServerPingManager.getInstanceFor(con);
|
ServerPingManager mgr = ServerPingManager.getInstanceFor(con);
|
||||||
mgr.setPingInterval(ServerPingManager.PING_MINIMUM);
|
mgr.setPingInterval(PING_MINIMUM);
|
||||||
|
|
||||||
return con;
|
return con;
|
||||||
}
|
}
|
||||||
|
@ -157,6 +174,6 @@ public class ServerPingTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
private long getWaitTime() {
|
private long getWaitTime() {
|
||||||
return ServerPingManager.PING_MINIMUM + SmackConfiguration.getPacketReplyTimeout() + 3000;
|
return PING_MINIMUM + SmackConfiguration.getPacketReplyTimeout() + 3000;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -23,19 +23,28 @@ import org.jivesoftware.smack.packet.Packet;
|
||||||
import org.jivesoftware.smack.ping.packet.Ping;
|
import org.jivesoftware.smack.ping.packet.Ping;
|
||||||
import org.jivesoftware.smack.util.PacketParserUtils;
|
import org.jivesoftware.smack.util.PacketParserUtils;
|
||||||
import org.jivesoftware.smackx.packet.DiscoverInfo;
|
import org.jivesoftware.smackx.packet.DiscoverInfo;
|
||||||
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import static org.junit.Assert.*;
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
public class PingPongTest {
|
public class PingTest {
|
||||||
|
private DummyConnection dummyCon;
|
||||||
|
private ThreadedDummyConnection threadedCon;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setup() {
|
||||||
|
dummyCon = new DummyConnection();
|
||||||
|
threadedCon = new ThreadedDummyConnection();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void checkSendingPing() throws Exception {
|
public void checkSendingPing() throws Exception {
|
||||||
DummyConnection con = new DummyConnection();
|
dummyCon = new DummyConnection();
|
||||||
PingManager pinger = new PingManager(con);
|
PingManager pinger = new PingManager(dummyCon);
|
||||||
pinger.ping("test@myserver.com");
|
pinger.ping("test@myserver.com");
|
||||||
|
|
||||||
Packet sentPacket = con.getSentPacket();
|
Packet sentPacket = dummyCon.getSentPacket();
|
||||||
|
|
||||||
assertTrue(sentPacket instanceof Ping);
|
assertTrue(sentPacket instanceof Ping);
|
||||||
|
|
||||||
|
@ -43,9 +52,9 @@ public class PingPongTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void checkSuccessfulPing() throws Exception {
|
public void checkSuccessfulPing() throws Exception {
|
||||||
ThreadedDummyConnection con = new ThreadedDummyConnection();
|
threadedCon = new ThreadedDummyConnection();
|
||||||
|
|
||||||
PingManager pinger = new PingManager(con);
|
PingManager pinger = new PingManager(threadedCon);
|
||||||
|
|
||||||
boolean pingSuccess = pinger.ping("test@myserver.com");
|
boolean pingSuccess = pinger.ping("test@myserver.com");
|
||||||
|
|
||||||
|
@ -59,8 +68,8 @@ public class PingPongTest {
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
public void checkFailedPingOnTimeout() throws Exception {
|
public void checkFailedPingOnTimeout() throws Exception {
|
||||||
DummyConnection con = new DummyConnection();
|
dummyCon = new DummyConnection();
|
||||||
PingManager pinger = new PingManager(con);
|
PingManager pinger = new PingManager(dummyCon);
|
||||||
|
|
||||||
boolean pingSuccess = pinger.ping("test@myserver.com");
|
boolean pingSuccess = pinger.ping("test@myserver.com");
|
||||||
|
|
||||||
|
@ -69,12 +78,12 @@ public class PingPongTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DummyConnection will not reply so it will timeout.
|
* Server returns an exception for entity.
|
||||||
* @throws Exception
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
public void checkFailedPingError() throws Exception {
|
public void checkFailedPingToEntityError() throws Exception {
|
||||||
ThreadedDummyConnection con = new ThreadedDummyConnection();
|
threadedCon = new ThreadedDummyConnection();
|
||||||
//@formatter:off
|
//@formatter:off
|
||||||
String reply =
|
String reply =
|
||||||
"<iq type='error' id='qrzSp-16' to='test@myserver.com'>" +
|
"<iq type='error' id='qrzSp-16' to='test@myserver.com'>" +
|
||||||
|
@ -84,15 +93,60 @@ public class PingPongTest {
|
||||||
"</error>" +
|
"</error>" +
|
||||||
"</iq>";
|
"</iq>";
|
||||||
//@formatter:on
|
//@formatter:on
|
||||||
|
IQ serviceUnavailable = PacketParserUtils.parseIQ(TestUtils.getIQParser(reply), threadedCon);
|
||||||
|
threadedCon.addIQReply(serviceUnavailable);
|
||||||
|
|
||||||
|
PingManager pinger = new PingManager(threadedCon);
|
||||||
|
|
||||||
|
boolean pingSuccess = pinger.ping("test@myserver.com");
|
||||||
|
|
||||||
|
assertFalse(pingSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void checkPingToServerSuccess() throws Exception {
|
||||||
|
ThreadedDummyConnection con = new ThreadedDummyConnection();
|
||||||
|
PingManager pinger = new PingManager(con);
|
||||||
|
|
||||||
|
boolean pingSuccess = pinger.pingMyServer();
|
||||||
|
|
||||||
|
assertTrue(pingSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Server returns an exception.
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void checkPingToServerError() throws Exception {
|
||||||
|
ThreadedDummyConnection con = new ThreadedDummyConnection();
|
||||||
|
//@formatter:off
|
||||||
|
String reply =
|
||||||
|
"<iq type='error' id='qrzSp-16' to='test@myserver.com' from='" + con.getServiceName() + "'>" +
|
||||||
|
"<ping xmlns='urn:xmpp:ping'/>" +
|
||||||
|
"<error type='cancel'>" +
|
||||||
|
"<service-unavailable xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>" +
|
||||||
|
"</error>" +
|
||||||
|
"</iq>";
|
||||||
|
//@formatter:on
|
||||||
IQ serviceUnavailable = PacketParserUtils.parseIQ(TestUtils.getIQParser(reply), con);
|
IQ serviceUnavailable = PacketParserUtils.parseIQ(TestUtils.getIQParser(reply), con);
|
||||||
con.addIQReply(serviceUnavailable);
|
con.addIQReply(serviceUnavailable);
|
||||||
|
|
||||||
PingManager pinger = new PingManager(con);
|
PingManager pinger = new PingManager(con);
|
||||||
|
|
||||||
boolean pingSuccess = pinger.ping("test@myserver.com");
|
boolean pingSuccess = pinger.pingMyServer();
|
||||||
|
|
||||||
|
assertTrue(pingSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void checkPingToServerTimeout() throws Exception {
|
||||||
|
DummyConnection con = new DummyConnection();
|
||||||
|
PingManager pinger = new PingManager(con);
|
||||||
|
|
||||||
|
boolean pingSuccess = pinger.pingMyServer();
|
||||||
|
|
||||||
assertFalse(pingSuccess);
|
assertFalse(pingSuccess);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
Loading…
Reference in a new issue