mirror of
https://github.com/vanitasvitae/Smack.git
synced 2024-11-23 20:42:06 +01:00
Allow sending streams in JET
This commit is contained in:
parent
7e7be0f47b
commit
524660c870
7 changed files with 119 additions and 50 deletions
|
@ -17,13 +17,21 @@
|
||||||
package org.jivesoftware.smackx.jet;
|
package org.jivesoftware.smackx.jet;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.security.InvalidAlgorithmParameterException;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.NoSuchProviderException;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.WeakHashMap;
|
import java.util.WeakHashMap;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
import javax.crypto.NoSuchPaddingException;
|
||||||
|
|
||||||
import org.jivesoftware.smack.Manager;
|
import org.jivesoftware.smack.Manager;
|
||||||
import org.jivesoftware.smack.SmackException;
|
import org.jivesoftware.smack.SmackException;
|
||||||
import org.jivesoftware.smack.XMPPConnection;
|
import org.jivesoftware.smack.XMPPConnection;
|
||||||
|
import org.jivesoftware.smack.XMPPException;
|
||||||
import org.jivesoftware.smack.provider.ExtensionElementProvider;
|
import org.jivesoftware.smack.provider.ExtensionElementProvider;
|
||||||
import org.jivesoftware.smackx.ciphers.Aes256GcmNoPadding;
|
import org.jivesoftware.smackx.ciphers.Aes256GcmNoPadding;
|
||||||
import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
|
import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
|
||||||
|
@ -37,6 +45,7 @@ import org.jivesoftware.smackx.jingle.component.JingleSession;
|
||||||
import org.jivesoftware.smackx.jingle.element.JingleContentElement;
|
import org.jivesoftware.smackx.jingle.element.JingleContentElement;
|
||||||
import org.jivesoftware.smackx.jingle.util.Role;
|
import org.jivesoftware.smackx.jingle.util.Role;
|
||||||
import org.jivesoftware.smackx.jingle_filetransfer.JingleFileTransferManager;
|
import org.jivesoftware.smackx.jingle_filetransfer.JingleFileTransferManager;
|
||||||
|
import org.jivesoftware.smackx.jingle_filetransfer.component.JingleFileTransferFile;
|
||||||
import org.jivesoftware.smackx.jingle_filetransfer.component.JingleOutgoingFileOffer;
|
import org.jivesoftware.smackx.jingle_filetransfer.component.JingleOutgoingFileOffer;
|
||||||
import org.jivesoftware.smackx.jingle_filetransfer.controller.OutgoingFileOfferController;
|
import org.jivesoftware.smackx.jingle_filetransfer.controller.OutgoingFileOfferController;
|
||||||
|
|
||||||
|
@ -87,10 +96,7 @@ public final class JetManager extends Manager implements JingleDescriptionManage
|
||||||
throw new IllegalArgumentException("File MUST NOT be null and MUST exist.");
|
throw new IllegalArgumentException("File MUST NOT be null and MUST exist.");
|
||||||
}
|
}
|
||||||
|
|
||||||
ServiceDiscoveryManager disco = ServiceDiscoveryManager.getInstanceFor(connection());
|
throwIfRecipientLacksSupport(recipient);
|
||||||
if (!disco.supportsFeature(recipient, getNamespace()) || !disco.supportsFeature(recipient, envelopeManager.getJingleEnvelopeNamespace())) {
|
|
||||||
throw new SmackException.FeatureNotSupportedException(getNamespace(), recipient);
|
|
||||||
}
|
|
||||||
|
|
||||||
JingleSession session = jingleManager.createSession(Role.initiator, recipient);
|
JingleSession session = jingleManager.createSession(Role.initiator, recipient);
|
||||||
|
|
||||||
|
@ -113,6 +119,30 @@ public final class JetManager extends Manager implements JingleDescriptionManage
|
||||||
return offer;
|
return offer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public OutgoingFileOfferController sendEncryptedStream(InputStream inputStream, JingleFileTransferFile.StreamFile file, FullJid recipient, JingleEnvelopeManager envelopeManager)
|
||||||
|
throws XMPPException.XMPPErrorException, SmackException.FeatureNotSupportedException, SmackException.NotConnectedException,
|
||||||
|
InterruptedException, SmackException.NoResponseException, NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException,
|
||||||
|
JingleEnvelopeManager.JingleEncryptionException, NoSuchProviderException, InvalidAlgorithmParameterException {
|
||||||
|
|
||||||
|
throwIfRecipientLacksSupport(recipient);
|
||||||
|
JingleSession session = jingleManager.createSession(Role.initiator, recipient);
|
||||||
|
|
||||||
|
JingleContent content = new JingleContent(JingleContentElement.Creator.initiator, JingleContentElement.Senders.initiator);
|
||||||
|
session.addContent(content);
|
||||||
|
|
||||||
|
JingleOutgoingFileOffer offer = new JingleOutgoingFileOffer(file, inputStream);
|
||||||
|
content.setDescription(offer);
|
||||||
|
|
||||||
|
JingleTransportManager transportManager = jingleManager.getBestAvailableTransportManager(recipient);
|
||||||
|
content.setTransport(transportManager.createTransportForInitiator(content));
|
||||||
|
|
||||||
|
JetSecurity security = new JetSecurity(envelopeManager, recipient, content.getName(), Aes256GcmNoPadding.NAMESPACE);
|
||||||
|
content.setSecurity(security);
|
||||||
|
session.sendInitiate(connection());
|
||||||
|
|
||||||
|
return offer;
|
||||||
|
}
|
||||||
|
|
||||||
public void registerEnvelopeManager(JingleEnvelopeManager method) {
|
public void registerEnvelopeManager(JingleEnvelopeManager method) {
|
||||||
envelopeManagers.put(method.getJingleEnvelopeNamespace(), method);
|
envelopeManagers.put(method.getJingleEnvelopeNamespace(), method);
|
||||||
}
|
}
|
||||||
|
@ -151,4 +181,10 @@ public final class JetManager extends Manager implements JingleDescriptionManage
|
||||||
public void notifyContentAdd(JingleSession session, JingleContent content) {
|
public void notifyContentAdd(JingleSession session, JingleContent content) {
|
||||||
JingleFileTransferManager.getInstanceFor(connection()).notifyContentAdd(session, content);
|
JingleFileTransferManager.getInstanceFor(connection()).notifyContentAdd(session, content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void throwIfRecipientLacksSupport(FullJid recipient) throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException, SmackException.FeatureNotSupportedException {
|
||||||
|
if (!ServiceDiscoveryManager.getInstanceFor(connection()).supportsFeature(recipient, getNamespace())) {
|
||||||
|
throw new SmackException.FeatureNotSupportedException(getNamespace(), recipient);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -130,7 +130,7 @@ public final class JingleFileTransferManager extends Manager implements JingleDe
|
||||||
return offer;
|
return offer;
|
||||||
}
|
}
|
||||||
|
|
||||||
public OutgoingFileOfferController sendStream(final InputStream stream, JingleFileTransferFile.LocalFile file, FullJid recipient) throws SmackException.FeatureNotSupportedException, XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException {
|
public OutgoingFileOfferController sendStream(final InputStream stream, JingleFileTransferFile.StreamFile file, FullJid recipient) throws SmackException.FeatureNotSupportedException, XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException {
|
||||||
if (!ServiceDiscoveryManager.getInstanceFor(connection()).supportsFeature(recipient, getNamespace())) {
|
if (!ServiceDiscoveryManager.getInstanceFor(connection()).supportsFeature(recipient, getNamespace())) {
|
||||||
throw new SmackException.FeatureNotSupportedException(getNamespace(), recipient);
|
throw new SmackException.FeatureNotSupportedException(getNamespace(), recipient);
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,12 +19,9 @@ package org.jivesoftware.smackx.jingle_filetransfer.component;
|
||||||
/**
|
/**
|
||||||
* Created by vanitas on 22.07.17.
|
* Created by vanitas on 22.07.17.
|
||||||
*/
|
*/
|
||||||
public abstract class AbstractJingleFileOffer<D extends JingleFileTransferFile> extends JingleFileTransfer {
|
public abstract class AbstractJingleFileOffer extends JingleFileTransfer {
|
||||||
|
|
||||||
AbstractJingleFileOffer(D fileTransferFile) {
|
AbstractJingleFileOffer(JingleFileTransferFile fileTransferFile) {
|
||||||
super(fileTransferFile);
|
super(fileTransferFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public abstract D getFile();
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -175,4 +175,51 @@ public abstract class JingleFileTransferFile {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class StreamFile extends JingleFileTransferFile {
|
||||||
|
|
||||||
|
private String name, description, mediaType;
|
||||||
|
private long size;
|
||||||
|
private Date date;
|
||||||
|
private HashElement hashElement;
|
||||||
|
|
||||||
|
public StreamFile(String name, long size, String description, String mediaType, Date date, HashElement hashElement) {
|
||||||
|
this.name = name;
|
||||||
|
this.size = size;
|
||||||
|
this.description = description;
|
||||||
|
this.mediaType = mediaType;
|
||||||
|
this.date = date;
|
||||||
|
this.hashElement = hashElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Date getDate() {
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getSize() {
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDescription() {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getMediaType() {
|
||||||
|
return mediaType;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HashElement getHashElement() {
|
||||||
|
return hashElement;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,7 +38,7 @@ import org.jivesoftware.smackx.jingle_filetransfer.element.JingleFileTransferChi
|
||||||
* Behind the scenes logic of an incoming Jingle file offer.
|
* Behind the scenes logic of an incoming Jingle file offer.
|
||||||
* Created by vanitas on 26.07.17.
|
* Created by vanitas on 26.07.17.
|
||||||
*/
|
*/
|
||||||
public class JingleIncomingFileOffer extends AbstractJingleFileOffer<JingleFileTransferFile.RemoteFile> implements IncomingFileOfferController {
|
public class JingleIncomingFileOffer extends AbstractJingleFileOffer implements IncomingFileOfferController {
|
||||||
|
|
||||||
private static final Logger LOGGER = Logger.getLogger(JingleIncomingFileOffer.class.getName());
|
private static final Logger LOGGER = Logger.getLogger(JingleIncomingFileOffer.class.getName());
|
||||||
private OutputStream target;
|
private OutputStream target;
|
||||||
|
|
|
@ -33,17 +33,18 @@ import org.jivesoftware.smackx.jingle_filetransfer.controller.OutgoingFileOfferC
|
||||||
/**
|
/**
|
||||||
* Created by vanitas on 26.07.17.
|
* Created by vanitas on 26.07.17.
|
||||||
*/
|
*/
|
||||||
public class JingleOutgoingFileOffer extends AbstractJingleFileOffer<JingleFileTransferFile.LocalFile> implements OutgoingFileOfferController {
|
public class JingleOutgoingFileOffer extends AbstractJingleFileOffer implements OutgoingFileOfferController {
|
||||||
private static final Logger LOGGER = Logger.getLogger(JingleOutgoingFileOffer.class.getName());
|
private static final Logger LOGGER = Logger.getLogger(JingleOutgoingFileOffer.class.getName());
|
||||||
|
|
||||||
private final InputStream source;
|
private final InputStream source;
|
||||||
|
|
||||||
public JingleOutgoingFileOffer(File file) throws FileNotFoundException {
|
public JingleOutgoingFileOffer(File file) throws FileNotFoundException {
|
||||||
this(new JingleFileTransferFile.LocalFile(file), new FileInputStream(file));
|
super(new JingleFileTransferFile.LocalFile(file));
|
||||||
|
this.source = new FileInputStream(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
public JingleOutgoingFileOffer(JingleFileTransferFile.LocalFile localFile, InputStream inputStream) {
|
public JingleOutgoingFileOffer(JingleFileTransferFile.StreamFile streamFile, InputStream inputStream) {
|
||||||
super(localFile);
|
super(streamFile);
|
||||||
this.source = inputStream;
|
this.source = inputStream;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,12 +80,11 @@ public class JingleOutgoingFileOffer extends AbstractJingleFileOffer<JingleFileT
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
LOGGER.log(Level.SEVERE, "Exception while sending file: " + e, e);
|
LOGGER.log(Level.SEVERE, "Exception while sending file: " + e, e);
|
||||||
} finally {
|
} finally {
|
||||||
if (source != null) {
|
|
||||||
try {
|
try {
|
||||||
source.close();
|
source.close();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
LOGGER.log(Level.SEVERE, "Could not close FileInputStream: " + e, e);
|
LOGGER.log(Level.SEVERE, "Could not close FileInputStream: " + e, e);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,21 +16,24 @@
|
||||||
*/
|
*/
|
||||||
package org.jivesoftware.smackx.jet;
|
package org.jivesoftware.smackx.jet;
|
||||||
|
|
||||||
import static org.jivesoftware.smackx.jingle_filetransfer.JingleFileTransferIntegrationTest.prepareNewTestFile;
|
|
||||||
import static org.jivesoftware.smackx.omemo.OmemoIntegrationTestHelper.cleanServerSideTraces;
|
import static org.jivesoftware.smackx.omemo.OmemoIntegrationTestHelper.cleanServerSideTraces;
|
||||||
import static org.jivesoftware.smackx.omemo.OmemoIntegrationTestHelper.setUpOmemoManager;
|
import static org.jivesoftware.smackx.omemo.OmemoIntegrationTestHelper.setUpOmemoManager;
|
||||||
import static org.jivesoftware.smackx.omemo.OmemoIntegrationTestHelper.subscribe;
|
import static org.jivesoftware.smackx.omemo.OmemoIntegrationTestHelper.subscribe;
|
||||||
import static org.jivesoftware.smackx.omemo.OmemoIntegrationTestHelper.unidirectionalTrust;
|
import static org.jivesoftware.smackx.omemo.OmemoIntegrationTestHelper.unidirectionalTrust;
|
||||||
import static org.junit.Assert.assertArrayEquals;
|
import static org.junit.Assert.assertArrayEquals;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.FileInputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
import org.jivesoftware.smack.SmackException;
|
import org.jivesoftware.smack.SmackException;
|
||||||
import org.jivesoftware.smack.XMPPException;
|
import org.jivesoftware.smack.XMPPException;
|
||||||
import org.jivesoftware.smackx.jingle.transport.jingle_ibb.JingleIBBTransportManager;
|
import org.jivesoftware.smackx.jingle.transport.jingle_ibb.JingleIBBTransportManager;
|
||||||
|
import org.jivesoftware.smackx.jingle.transport.jingle_s5b.JingleS5BTransportManager;
|
||||||
import org.jivesoftware.smackx.jingle_filetransfer.JingleFileTransferManager;
|
import org.jivesoftware.smackx.jingle_filetransfer.JingleFileTransferManager;
|
||||||
|
import org.jivesoftware.smackx.jingle_filetransfer.component.JingleFileTransferFile;
|
||||||
import org.jivesoftware.smackx.jingle_filetransfer.controller.IncomingFileOfferController;
|
import org.jivesoftware.smackx.jingle_filetransfer.controller.IncomingFileOfferController;
|
||||||
import org.jivesoftware.smackx.jingle_filetransfer.listener.IncomingFileOfferListener;
|
import org.jivesoftware.smackx.jingle_filetransfer.listener.IncomingFileOfferListener;
|
||||||
import org.jivesoftware.smackx.jingle_filetransfer.listener.ProgressListener;
|
import org.jivesoftware.smackx.jingle_filetransfer.listener.ProgressListener;
|
||||||
|
@ -51,20 +54,9 @@ public class JetIntegrationTest extends AbstractOmemoIntegrationTest {
|
||||||
private OmemoManager oa, ob;
|
private OmemoManager oa, ob;
|
||||||
private JetManager ja, jb;
|
private JetManager ja, jb;
|
||||||
private JingleIBBTransportManager ia, ib;
|
private JingleIBBTransportManager ia, ib;
|
||||||
|
private JingleS5BTransportManager sa, sb;
|
||||||
private OmemoStore<?,?,?,?,?,?,?,?,?> store;
|
private OmemoStore<?,?,?,?,?,?,?,?,?> store;
|
||||||
|
|
||||||
private static final File tempDir;
|
|
||||||
|
|
||||||
static {
|
|
||||||
String userHome = System.getProperty("user.home");
|
|
||||||
if (userHome != null) {
|
|
||||||
File f = new File(userHome);
|
|
||||||
tempDir = new File(f, ".config/smack-integration-test/");
|
|
||||||
} else {
|
|
||||||
tempDir = new File("int_test_jingle");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public JetIntegrationTest(SmackIntegrationTestEnvironment environment)
|
public JetIntegrationTest(SmackIntegrationTestEnvironment environment)
|
||||||
throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException,
|
throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException,
|
||||||
SmackException.NoResponseException, TestNotPossibleException {
|
SmackException.NoResponseException, TestNotPossibleException {
|
||||||
|
@ -80,6 +72,8 @@ public class JetIntegrationTest extends AbstractOmemoIntegrationTest {
|
||||||
jb = JetManager.getInstanceFor(conTwo);
|
jb = JetManager.getInstanceFor(conTwo);
|
||||||
ia = JingleIBBTransportManager.getInstanceFor(conOne);
|
ia = JingleIBBTransportManager.getInstanceFor(conOne);
|
||||||
ib = JingleIBBTransportManager.getInstanceFor(conTwo);
|
ib = JingleIBBTransportManager.getInstanceFor(conTwo);
|
||||||
|
sa = JingleS5BTransportManager.getInstanceFor(conOne);
|
||||||
|
sb = JingleS5BTransportManager.getInstanceFor(conTwo);
|
||||||
JetManager.registerEnvelopeProvider(OmemoConstants.OMEMO_NAMESPACE_V_AXOLOTL, new OmemoVAxolotlProvider());
|
JetManager.registerEnvelopeProvider(OmemoConstants.OMEMO_NAMESPACE_V_AXOLOTL, new OmemoVAxolotlProvider());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,6 +83,8 @@ public class JetIntegrationTest extends AbstractOmemoIntegrationTest {
|
||||||
|
|
||||||
final SimpleResultSyncPoint received = new SimpleResultSyncPoint();
|
final SimpleResultSyncPoint received = new SimpleResultSyncPoint();
|
||||||
|
|
||||||
|
Random weakRandom = new Random();
|
||||||
|
|
||||||
//Setup OMEMO
|
//Setup OMEMO
|
||||||
subscribe(oa, ob, "Bob");
|
subscribe(oa, ob, "Bob");
|
||||||
subscribe(ob, oa, "Alice");
|
subscribe(ob, oa, "Alice");
|
||||||
|
@ -100,8 +96,10 @@ public class JetIntegrationTest extends AbstractOmemoIntegrationTest {
|
||||||
ja.registerEnvelopeManager(oa);
|
ja.registerEnvelopeManager(oa);
|
||||||
jb.registerEnvelopeManager(ob);
|
jb.registerEnvelopeManager(ob);
|
||||||
|
|
||||||
File source = prepareNewTestFile("source");
|
byte[] sourceBytes = new byte[16000];
|
||||||
final File target = new File(tempDir, "target");
|
weakRandom.nextBytes(sourceBytes);
|
||||||
|
InputStream sourceStream = new ByteArrayInputStream(sourceBytes);
|
||||||
|
final ByteArrayOutputStream targetStream = new ByteArrayOutputStream(16000);
|
||||||
|
|
||||||
JingleFileTransferManager.getInstanceFor(conTwo).addIncomingFileOfferListener(new IncomingFileOfferListener() {
|
JingleFileTransferManager.getInstanceFor(conTwo).addIncomingFileOfferListener(new IncomingFileOfferListener() {
|
||||||
@Override
|
@Override
|
||||||
|
@ -123,27 +121,18 @@ public class JetIntegrationTest extends AbstractOmemoIntegrationTest {
|
||||||
received.signal();
|
received.signal();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
offer.accept(conTwo, target);
|
offer.accept(conTwo, targetStream);
|
||||||
} catch (InterruptedException | XMPPException.XMPPErrorException | SmackException.NotConnectedException | SmackException.NoResponseException | IOException e) {
|
} catch (InterruptedException | XMPPException.XMPPErrorException | SmackException.NotConnectedException | SmackException.NoResponseException | IOException e) {
|
||||||
received.signal(e);
|
received.signal(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ja.sendEncryptedFile(source, conTwo.getUser().asFullJidOrThrow(), oa);
|
ja.sendEncryptedStream(sourceStream, new JingleFileTransferFile.StreamFile("test", sourceBytes.length, "desc", null, null, null), conTwo.getUser().asFullJidOrThrow(), oa);
|
||||||
|
|
||||||
received.waitForResult(60 * 1000);
|
received.waitForResult(60 * 1000);
|
||||||
|
|
||||||
FileInputStream sIn = new FileInputStream(source);
|
assertArrayEquals(sourceBytes, targetStream.toByteArray());
|
||||||
FileInputStream tIn = new FileInputStream(target);
|
|
||||||
|
|
||||||
byte[] sB = new byte[(int) source.length()];
|
|
||||||
byte[] tB = new byte[(int) target.length()];
|
|
||||||
|
|
||||||
sIn.read(sB);
|
|
||||||
tIn.read(tB);
|
|
||||||
|
|
||||||
assertArrayEquals(sB, tB);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
Loading…
Reference in a new issue