В одном из недавних проектов мы реализовывали взаимодействие Android-приложения с ejabberd-сервером через кастомизированный XMPP-протокол.
В этой статье приведены примеры как можно отправлять/получать кастомизированные пакеты XMPP-протокола в Android-приложении.
Для работы с XMPP протоколом была выбрана библиотека Smack 4.1.8.
Первая задача — отправка на сервер Message-пакетов с дополнительными атрибутами в родительском элементе и нестандартными дочерними элементами.
Сразу оговорюсь, с точки зрения XMPP-протокола изменять родительский элемент некорректно. Но в этом проекте нам пришлось так сделать, т.к. сервер к моменту старта разработки Android-приложения был уже реализован и не было возможности его изменить.
Xml для отправки Message-пакета:
Атрибута ’company’ и элемента “read” нет в XMPP-протоколе.
Стандартная реализация классов IQ, Message, Stanza не предоставляют возможность что-либо добавлять в родительский элемент xml. А для классов IQ, Message даже в случае наследования нет возможности изменять родительский элемент.
Решением является наследование от класса “Stanza” и переопределение метода toXML:
Отправить такой пакет можно как обычный Stanza-пакет без обработки результата:
В обработчике исходящих пакетов объекта
Если сообщение не дошло до сервера или сервер вернул ошибку, то отправленное сообщение возвращается с информацией об ошибке. Парсер в Smack не сможет обработать такой формат данных и выдаст ошибку. Эту проблему можно решить только внося изменения в исходники библиотеки Smack.
Вторая задача — парсинг входящих Message-пакетов из приведенного выше xml.
Для решения этой задачи необходимо создать и зарегистрировать провайдер (парсер).
Для класса
Регистрируем свой провайдер:
Создаем обработчик входящих пакетов:
Регистрируем обработчик входящих сообщений с использованием стандартного фильтра
Третья задача — отправка и получение кастомизированных IQ-пакетов.
Xml для отправки IQ-пакета:
Здесь атрибуты “xmlns” и “type” принимаю значения, которых нет в XMPP-протоколе. Такой пакет можно формировать по аналогии с
Xml входящего IQ-пакета:
Для парсинга дочерних элементов нужно создать и зарегистрировать провайдер:
Регистрируем провайдер:
Отправляем IQ-пакет с обработкой результата:
Итого: отправили на сервер кастомизированные IQ и Message пакеты, получили и распарсили кастомизированные IQ и Message пакеты не меняя исходников библиотеки Smack.
Весь приведенный выше код носит демонстрационный характер. В проекте мы используем retrolambda, RxJava и дополнительные классы, чтобы код был универсальным и красивым.
В этой статье приведены примеры как можно отправлять/получать кастомизированные пакеты 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 и дополнительные классы, чтобы код был универсальным и красивым.
Поделиться с друзьями
Комментарии (5)
ValdikSS
09.01.2017 23:10+2А что у вас за костыли-то? Почему вы не воспользовались, например, готовым хером 313 для поддержки истории (Message Archive Management)? Почему не оформили кастомизацию в виде отдельного тега со своим неймспейсом?
SSul
10.01.2017 08:01Архитектуру взаимодействия проектировали не мы. К моменту старта разработки Android-клиента уже был реализован web-клиент. Поэтому пришлось реализовывать такое решение.
Emily_Rose
Да уж… а я думал xml в JavaScript-е это боль. На клиенте пользуюсь jxt, а на сервере у меня prosody, там станзы билдить, вообще супер легко. Посмотрел на джаву, хрошо что я не пишу на джаве. И да, использовать неймспейсы готовых стандаротов для разширения возможностей тоже нельзя, это конечно если вы хотите сохранить обратную совместимость.
gudvinr
У вас используется XMPP, или свой протокол?
Если XMPP, подключение/транспорт пакетов самостоятельно реализуете через bosh/вебсокеты, или библиотеками вроде stanza.io? Может как-то совсем иначе?
Emily_Rose
У нас ХМРР, но со свиими нестандартными фишками в виде оверрайда стандартного поведения в просоди, плюс множество кастомных модулей. На клиенте пользуемся stanza.io over ws транспорт. И то вся jingle-webrtc часть в станзе.ио у нас переписана, да и некоторые другие части станзы кастомные.