В одном из недавних проектов мы реализовывали взаимодействие Android-приложения с ejabberd-сервером через кастомизированный XMPP-протокол.
В этой статье приведены примеры как можно отправлять/получать кастомизированные пакеты XMPP-протокола в Android-приложении.
Для работы с XMPP протоколом была выбрана библиотека Smack 4.1.8.
Первая задача — отправка на сервер Message-пакетов с дополнительными атрибутами в родительском элементе и нестандартными дочерними элементами.
Сразу оговорюсь, с точки зрения XMPP-протокола изменять родительский элемент некорректно. Но в этом проекте нам пришлось так сделать, т.к. сервер к моменту старта разработки Android-приложения был уже реализован и не было возможности его изменить.
Xml для отправки Message-пакета:
<message from='userJIdFrom/Resource' to='userJIdTo/Resource'
xml:lang='en' id='70720-25' company=’SimbirSoft’>
<read xmlns='urn:xmpp:receipts' id='ILKMe-22'/>
</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());
// Добавляем закрывающий элемент “</id>”, “</message>”, “</stanza>”
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<String, String> 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<ReadMessageProvider.Element> {
// Дочерний элемент пакета
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-пакета:
<iq xmlns='xep:mymessages' to='server' from='userJIdFrom/Resource' id='J8OPC-50' type='history'>
<query count='50' offset='0'>'userJIdTo/Resource'</query>
</iq>
Здесь атрибуты “xmlns” и “type” принимаю значения, которых нет в XMPP-протоколе. Такой пакет можно формировать по аналогии с “ReadMessageStanza”
.
Xml входящего IQ-пакета:
<iq xmlns='xep:mymessages' type='result' to='userJIdFrom/Resource'
id='Ji3H1-43'>
<result>
<message id='cfd6fce4-2f30-d1e3-349e-11eab92bc3fa'
from='userJIdFrom/Resource' to='userJIdTo/Resource'
type='chat'>
<body>Message</body>
<query xmlns='jabber:iq:time'>
<utc>1482729259000000</utc>
</query>
</message>
</result>
</iq>
Для парсинга дочерних элементов нужно создать и зарегистрировать провайдер:
// Провайдер для парсинга IQ-пакета с историей переписки
public class MyMessagesProvider extends IQProvider<MyMessagesProvider.Result> {
// Дочерний элемент пакета. В качестве значения берем 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<CustomMessage> mItems = new ArrayList<>();
private Result() { super("items"); }
@Override
protected IQChildElementXmlStringBuilder getIQChildElementBuilder(IQChildElementXmlStringBuilder xml) { return null; }
public List<CustomMessage> 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<CustomMessage> value = ((MyMessagesProvider.Result) packet).getValue();
// Обрабатываем входящие сообщения
}
},
// Обрабатываем ошибки
new ExceptionCallback() {
@Override
public void processException(Exception exception) { }
}
);
Итого: отправили на сервер кастомизированные IQ и Message пакеты, получили и распарсили кастомизированные IQ и Message пакеты не меняя исходников библиотеки Smack.
Весь приведенный выше код носит демонстрационный характер. В проекте мы используем retrolambda, RxJava и дополнительные классы, чтобы код был универсальным и красивым.
Автор: SimbirSoft