Поддержка нестандартного XMPP-протокола с помощью Smack
В этой статье приведены примеры как можно отправлять/получать кастомизированные пакеты XMPP-протокола в Android-приложении.
Для работы с XMPP протоколом была выбрана библиотека Smack 4.1.8.
Первая задача — отправка на сервер Message-пакетов с дополнительными атрибутами в родительском элементе и нестандартными дочерними элементами.
Сразу оговорюсь, с точки зрения XMPP-протокола изменять родительский элемент некорректно. Но в этом проекте нам пришлось так сделать, т.к. сервер к моменту старта разработки Android-приложения был уже реализован и не было возможности его изменить.
Xml для отправки Message-пакета:
Атрибута «company» и элемента «read» нет в XMPP-протоколе.
Стандартная реализация классов IQ, Message, Stanza не предоставляют возможность что-либо добавлять в родительский элемент xml. А для классов IQ, Message даже в случае наследования нет возможности изменять родительский элемент.
Решением является наследование от класса «Stanza» и переопределение метода toXML:
// Класс "ReadMessageStanza” служить для передачи уведомлений, что другой участник
// переписки прочитал сообщение
public class ReadMessageStanza extends Stanza {
@Override
public CharSequence toXML() {
XmlStringBuilder buf = new XmlStringBuilder();
// Добавляем открывающую скобку "<” и название элемента родительского элемента
// rootElement может быть "iq”, "message”, "stanza”.
buf.halfOpenElement(rootElement);
// Добавляем атрибуты "to", "from", "id", "lang" через стандартную функцию.
// Для задания значения "to" необходимо вызвать метод "setTo” класса "Stanza”
// "id", "lang" задаются автоматически по умолчанию в классе "Stanza”
// Значение для "from" будет браться автоматически текущего пользователя, если
// у объекта XMPPTCPConnection вызвать
// "setFromMode(XMPPConnection.FromMode.USER);"
addCommonAttributes(buf);
for (String key : attributes.keySet()) {
// Добавляем свои атрибуты в родительский элемент
buf.attribute(key, attributes.get(key));
}
// Закрываем скобку родительского элемента "/>”
buf.rightAngleBracket();
// Добавляем свои дочерние элементы. Данного метода нет в классе "Stanza”
buf.append(getChildElementXML());
// Стандартная функция для добавления Extensions. По сути это добавление
// стандартных дочерних элементов в xml
buf.append(getExtensionsXML());
// Добавляем закрывающий элемент "”, "”, "”
buf.closeElement(rootElement);
return buf;
}
}
Отправить такой пакет можно как обычный Stanza-пакет без обработки результата:
xmppTCPConnection.sendStanza(new ReadMessageStanza());
В обработчике исходящих пакетов объекта
"xmppTCPConnection”
тип класса будет "ReadMessageStanza”
: xmppTCPConnection.addPacketSendingListener(new StanzaListener() {
@Override
public void processPacket(Stanza packet) throws SmackException.NotConnectedException {
Map map =((ReadMessageStanza )packet).getAttributes();
// Работа с объектом класса "ReadMessageStanza”...
}
}, new StanzaFilter() {
@Override
public boolean accept(Stanza stanza) {
// Фильтруем нужные пакеты
return stanza instanceof ReadMessageStanza;
}
});
Реализация "ReadMessageStanza”
приведена выше в демонстративных целях. Правильнее вынести код в базовый класс "CustomStanza”
или использовать паттерн "Builder”
для построения пакетов.Если сообщение не дошло до сервера или сервер вернул ошибку, то отправленное сообщение возвращается с информацией об ошибке. Парсер в Smack не сможет обработать такой формат данных и выдаст ошибку. Эту проблему можно решить только внося изменения в исходники библиотеки Smack.
Вторая задача — парсинг входящих Message-пакетов из приведенного выше xml.
Для решения этой задачи необходимо создать и зарегистрировать провайдер (парсер).
Для класса "ReadMessageStanza"
провайдер будет следующий:
public class ReadMessageProvider extends ExtensionElementProvider {
// Дочерний элемент пакета
public static final String ELEMENT_NAME = ”read”;
// namespace дочернего элемента пакета
public static final String NAMESPACE = ”urn:xmpp:receipts”;
// Класс для дочернего элемента реализует стандартный интерфейс
// "ExtensionElement” библиотеки Smack.
// Переназначив метод toXML, объект данного класса можно добавлять в качестве
// "Extensions” для отправляемых ReadMessageStanza-пакетов
public static class Element implements ExtensionElement {
private final String id;
Element(String id) { this.id = id; }
public String getId() { return id; }
// В данном примере объект этого класса не используется в качестве "Extension”
// у отправляемых пакетов, потому можно вернуть null в методе toXML
@Override public CharSequence toXML() { return null; }
@Override public String getNamespace() { return NAMESPACE; }
@Override public String getElementName() { return ELEMENT_NAME; }
}
// Парсинг дочерних элементов пакета
@Override
public ReadMessageProvider .Element parse(XmlPullParser parser, int initialDepth) throws XmlPullParserException, IOException, SmackException {
// Получаем идентификатор прочитанного сообщения
return new ReadMessageProvider .Element(parser.getAttributeValue("", "id"));
}
}
Регистрируем свой провайдер:
static {
ProviderManager.addExtensionProvider(ReadMessageProvider.ELEMENT_NAME, ReadMessageProvider.NAMESPACE, new ReadMessageProvider());
}
Создаем обработчик входящих пакетов:
private StanzaListener inComingChatListener = new StanzaListener() {
@Override
public void processPacket(Stanza packet) throws SmackException.NotConnectedException{
Message message = (Message) packet;
// Проверяем, что сообщение содержит нужный дочерний элемент
if(message.hasExtension(ReadMessageProvider.ELEMENT_NAME, ReadMessageProvider.NAMESPACE)) {
ReadMessageProvider.Element element = message.getExtension(ReadMessageProvider.ELEMENT_NAME, ReadMessageProvider.NAMESPACE);
int id = element.getId();
// Обрабатываем сообщение ...
}
};
}
Регистрируем обработчик входящих сообщений с использованием стандартного фильтра
MessageTypeFilter.NORMAL_OR_CHAT
: xmppTCPConnection.addSyncStanzaListener(inComingChatListener, MessageTypeFilter.NORMAL_OR_CHAT);
Третья задача — отправка и получение кастомизированных IQ-пакетов.
Xml для отправки IQ-пакета:
'userJIdTo/Resource'
Здесь атрибуты «xmlns» и «type» принимаю значения, которых нет в XMPP-протоколе. Такой пакет можно формировать по аналогии с
"ReadMessageStanza”
.Xml входящего IQ-пакета:
Message
1482729259000000
Для парсинга дочерних элементов нужно создать и зарегистрировать провайдер:
// Провайдер для парсинга IQ-пакета с историей переписки
public class MyMessagesProvider extends IQProvider {
// Дочерний элемент пакета. В качестве значения берем enum из библиотеки Smack
public static final String ELEMENT_NAME = IQ.Type.result.name();
// namespace элемента пакета
public static final String NAMESPACE = ”xep:mymessages”;
// Класс для дочерних элементов
public static class Result extends IQ
{
// Хранит полученные сообщения
private List mItems = new ArrayList<>();
private Result() { super("items"); }
@Override
protected IQChildElementXmlStringBuilder getIQChildElementBuilder(IQChildElementXmlStringBuilder xml) { return null; }
public List getValue() { return mItems; }
}
@Override
public MyMessagesProvider.Result parse(XmlPullParser parser, int initialDepth) throws XmlPullParserException, IOException, SmackException {
MyMessagesProvider.Result result = new MyMessagesProvider.Result();
result.mItems = new ArrayList<>();
// Парсинг элементов "message” из parser
// ...
return result;
}
}
Регистрируем провайдер:
static { ProviderManager.addIQProvider(MyMessagesProvider.ELEMENT_NAME, MyMessagesProvider.NAMESPACE, new MyMessagesProvider());
}
Отправляем IQ-пакет с обработкой результата:
xmppTCPConnection.sendStanzaWithResponseCallback(
// Исходящий IQ-пакет
new CustomStanza(),
// Фильтр для входящих IQ-пакетов. Если не настроить правильно фильтр, то можно
// получать пакеты от любых других запросов или вообще не получить ничего.
new StanzaFilter() {
@Override
public boolean accept(Stanza stanza) {
return stanza instanceof MyMessagesProvider.Result;
}
},
// Обрабатываем входящий IQ-пакет, который удовлетворяет фильтру
new StanzaListener() {
@Override
public void processPacket(Stanza packet) throws SmackException.NotConnectedException {
List value = ((MyMessagesProvider.Result) packet).getValue();
// Обрабатываем входящие сообщения
}
},
// Обрабатываем ошибки
new ExceptionCallback() {
@Override
public void processException(Exception exception) { }
}
);
Итого: отправили на сервер кастомизированные IQ и Message пакеты, получили и распарсили кастомизированные IQ и Message пакеты не меняя исходников библиотеки Smack.
Весь приведенный выше код носит демонстрационный характер. В проекте мы используем retrolambda, RxJava и дополнительные классы, чтобы код был универсальным и красивым.