Поддержка нестандартного 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 и дополнительные классы, чтобы код был универсальным и красивым.
    SimbirSoft 23,41
    Компания
    Поделиться публикацией
    Похожие публикации
    Комментарии 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-клиент. Поэтому пришлось реализовывать такое решение.

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

        Самое читаемое