9 января в 15:19

Поддержка нестандартного XMPP-протокола с помощью Smack

В одном из недавних проектов мы реализовывали взаимодействие 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 и дополнительные классы, чтобы код был универсальным и красивым.
Автор: @SSul
SimbirSoft
рейтинг 35,89
Компания прекратила активность на сайте
Похожие публикации

Комментарии (5)

  • 0
    Да уж… а я думал xml в JavaScript-е это боль. На клиенте пользуюсь jxt, а на сервере у меня prosody, там станзы билдить, вообще супер легко. Посмотрел на джаву, хрошо что я не пишу на джаве. И да, использовать неймспейсы готовых стандаротов для разширения возможностей тоже нельзя, это конечно если вы хотите сохранить обратную совместимость.
    • 0

      У вас используется XMPP, или свой протокол?
      Если XMPP, подключение/транспорт пакетов самостоятельно реализуете через bosh/вебсокеты, или библиотеками вроде stanza.io? Может как-то совсем иначе?

      • 0
        У нас ХМРР, но со свиими нестандартными фишками в виде оверрайда стандартного поведения в просоди, плюс множество кастомных модулей. На клиенте пользуемся stanza.io over ws транспорт. И то вся jingle-webrtc часть в станзе.ио у нас переписана, да и некоторые другие части станзы кастомные.
  • +2
    А что у вас за костыли-то? Почему вы не воспользовались, например, готовым хером 313 для поддержки истории (Message Archive Management)? Почему не оформили кастомизацию в виде отдельного тега со своим неймспейсом?
    • 0
      Архитектуру взаимодействия проектировали не мы. К моменту старта разработки Android-клиента уже был реализован web-клиент. Поэтому пришлось реализовывать такое решение.

Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.