0. Введение.
Наверняка многие из Вас сталкивались с ситуацией, когда приходя на новую работу, обнаруживаешь там зоопарк из серверов, которые постоянно падают и требуют к себе слишком много внимания. Я не стал исключением из правила, и в один прекрасный день встал вопрос о переносе корпоративного Jabber сервера на новую платформу, в связи со старой версией сервиса (jabberd 1.4) и не очень удобной админкой - данные о пользователях хранились в обычных текстовых файлах. Проблема заключалась в том, что перенести базу нужно было абсолютно прозрачно для конечных пользователей, иначе мог случиться апокалипсис.
1. Изучаем старую базу данных.
Jabberd сервер был настроен на использование xml файлов, в качестве базы данных пользователей. С одной стороны это не есть хорошо, ввиду того, что с большим количеством файлов работать достаточно неудобно. Но с другой стороны не надо забывать, что файлики эти не простые, а xml'евые, что существенно упрощает задачу по их разбору. Итак, перейдем непосредственно к анализу существующей БД.
Первым делом посмотрим кол-во пользователей и заглянем в нутро одного из них:
Код: Выделить всё
mail spool # ls -la *.xml | wc -l
610
mail spool # cat testtest.xml
<xdb><password xmlns='jabber:iq:auth' xdbns='jabber:iq:auth'>test12</password><query xmlns='jabber:iq:register' xdbns='jabber:iq:register'><username>TestTest</username><x xmlns='jabber:x:delay' stamp='20100311T09:40:28'>registered</x></query><zerok xmlns='jabber:iq:auth:0k' xdbns='jabber:iq:auth:0k'><hash>395094b9bda14fccaa2308c147e76b8699f15572</hash><token>4B98BA93</token><sequence>500</sequence></zerok><foo xmlns='jabber:x:offline' xdbns='jabber:x:offline'/><query xmlns='jabber:iq:last' last='1268300442' xdbns='jabber:iq:last'>Disconnected</query></xdb>
mail spool #
scripts/user.pl:
Код: Выделить всё
#!/usr/bin/perl
# Подключаем модуль XML
use XML::Simple;
# Подключаем модуль для вывода XML на экран
use Data::Dumper;
# Считываем имя файла
$file = $ARGV[0];
# Создаем XML объект
$xml = new XML::Simple;
# Обрезаем \n
chomp $file;
# Считываем XML файл
$data = $xml->XMLin($file);
# Парсим и выводим на экран
print Dumper($data);
Для запуска скрипта понадобится модули, указанные в начале. Для их установки можно использовать cpan или встроенный менеджер пакетов системы. Я предпочел второй вариант и с помощью emerge установил необходимые модули. Скрипт принимает на вход имя файла, парсит его и выдает его структурный вид. Ну что же - пробуем:
mail spool # perl scripts/user.pl testtest.xml
$VAR1 = {
'password' => {
'xmlns' => 'jabber:iq:auth',
'xdbns' => 'jabber:iq:auth',
'content' => 'test12'
},
'query' => [
{
'xmlns' => 'jabber:iq:register',
'xdbns' => 'jabber:iq:register',
'x' => {
'xmlns' => 'jabber:x:delay',
'stamp' => '20100311T09:40:28',
'content' => 'registered'
},
'username' => 'TestTest'
},
{
'xmlns' => 'jabber:iq:last',
'xdbns' => 'jabber:iq:last',
'content' => 'Disconnected',
'last' => '1268300442'
}
],
'foo' => {
'xmlns' => 'jabber:x:offline',
'xdbns' => 'jabber:x:offline'
},
'zerok' => {
'hash' => '395094b9bda14fccaa2308c147e76b8699f15572',
'xmlns' => 'jabber:iq:auth:0k',
'xdbns' => 'jabber:iq:auth:0k',
'sequence' => '500',
'token' => '4B98BA93'
}
};
mail spool #
Код: Выделить всё
#!/usr/bin/perl
# Подключаем модуль XML
use XML::Simple;
# Задаем переменную для времени последнего входа
my $lastlogindate;
# Составляем список файлов для обработки
@files = `ls *.xml`;
# Создаем XML объект
$xml = new XML::Simple;
# Перебираем все файлики
foreach $file (@files)
{
# Обрезаем \n
chomp $file;
# Считываем XML файл
$data = $xml->XMLin($file);
# Выводим на экран имя файла, который рассматриваем
print $file.":";
# Разгребаем тэг <query>
foreach $each (@{$data->{'query'}})
{
# Получаем инфу о последнем логине пользователя
if ($each->{'xmlns'} eq 'jabber:iq:last')
{
($year, $month, $day) = (localtime($each->{'last'}))[5,4,3];
$date = sprintf ("%04d-%02d-%02d", $year+1900, $month+1, $day);
$lastlogindate = $date;
}
}
# Выводим инфу на экран
print "Last login: ".$lastlogindate."\n";
}
Код: Выделить всё
mail spool # perl scripts/parse.pl | grep testtest
testtest.xml:Last login: 2010-03-11
mail spool #
Код: Выделить всё
mail spool # perl scripts/parse.pl | grep 2011 | wc -l
234
mail spool # perl scripts/parse.pl | grep -v 2011 | wc -l
376
mail spool #
Идем дальше. А дальше нам надо выгрести всю информацию о пользователях:
1. Логин
2. Пароль
3. Контакты
- группы
- контакты
4. vCard
5. Offline сообщения (посланные пользователю, когда он был отключен).
Так как задача стояла сделать все прозрачно для пользователей (чтобы вообще не заметили переезда), то необходимо скопировать всю информацию об их контактах, включая группы. Начнем с простого - логин и пароль, ну и до кучи - последняя дата входа. Стругаем скриптик:
Код: Выделить всё
#!/usr/bin/perl
# Подключаем модуль XML
use XML::Simple;
# Объявляем необходимые переменные
my $password;
my $username;
my $lastlogindate;
# Составляем список файлов для обработки
@files = `ls *.xml`;
# Создаем XML объект
$xml = new XML::Simple;
# Перебираем файлы
foreach $file (@files)
{
# Обрезаем \n
chomp $file;
# Считываем xml файл
$data = $xml->XMLin($file);
# Выводим имя файла, которое обрабатываем
print "########".$file.":\n";
# Получаем пароль
$password = $data->{'password'}->{'content'};
# Разбираем тэг <query>
foreach $each (@{$data->{'query'}})
{
# Получаем имя пользователя
if ($each->{'xmlns'} eq 'jabber:iq:register')
{
$username = lc($each->{'username'});
}
# Получаем инфу о времени последнего входа в систему
if ($each->{'xmlns'} eq 'jabber:iq:last')
{
($year, $month, $day) = (localtime($each->{'last'}))[5,4,3];
$date = sprintf ("%04d-%02d-%02d", $year+1900, $month+1, $day);
$lastlogindate = $date;
}
}
# Выводим полученные нелегким трудом данные
print "Username: ".$username."\n";
print "Password: ".$password."\n";
print "Last login: ".$lastlogindate."\n";
}
Вариант 1:
{
'xmlns' => 'jabber:iq:roster',
'xdbns' => 'jabber:iq:roster',
'item' => [
{
'group' => 'test'
'jid' => 'user4@domen.ru',
'subscription' => 'none'
},
{
'name' => 'user something'
'jid' => 'user3@domen.ru',
'subscription' => 'both'
},
}
Вариант 2:
{
'xmlns' => 'jabber:iq:roster',
'xdbns' => 'jabber:iq:roster',
'item' => {
'user1' => {
'group' => "group1",
'jid' => 'user1@domen.ru',
'subscription' => 'both'
},
'user2' => {
'jid' => 'user2@domen.ru',
'subscription' => 'both'
},
}
}
В первом случае при обращении к элементу item ($data->{query}->[x]->{item}) мы получаем хэш, а во втором случае - массив... Поэтому надо как-то определять, какой вид хранения контактов используется. Убив полдня на поиски красивого любого решения, я остановил свой взгляд на функции ref, которая выдает тип данных, на который указывает ссылка. Ниже приведен кусок кода, позволяющий выцепить контакты пользователя, вне зависимости от того, как они сформированы в файле xml. Надеюсь, что вы сами догадаетесь, куда скопировать данный участок текста.
Код: Выделить всё
# Если попали в секцию контактов
if ($each->{'xmlns'} eq 'jabber:iq:roster')
{
# проверяем тип поля item, если array, то это вариант 1
if(ref($each->{'item'}) eq "ARRAY")
{
foreach $contact (@{$each->{'item'}})
{
$contactname = $contact->{'name'};
$contactgroup = $contact->{'group'};
$contactjid = $contact->{'jid'};
$contactsubscription = $contact->{'subscription'};
# В данном случае (в отличии от HASH)
# имя контакта может быть пустое.
# если оно пустое, то просто делаем его равным jid
if (length($contactname) == 0)
{
$contactname = $contactjid;
}
if (length($contactgroup) == 0)
{
$contactgroup = "DEFAULT";
}
# Здесь делаем что-то с этими данными
}
}
# если тип Hash, то это вариант 2
if(ref($each->{'item'}) eq "HASH")
{
@names = keys %{$each->{'item'}};
foreach $name (@names)
{
$contactname = $name;
$contactgroup = $each->{'item'}->{$name}->{'group'};
$contactjid = $each->{'item'}->{$name}->{'jid'};
$contactsubscription = $each->{'item'}->{$name}->{'subscription'};
# Данная проверка выявила двух пользователей
# со странно сгенерированными xml файлами... но об этом ниже
if (length($contactname) == 0)
{
$contactname = $contactjid;
system("echo \"$file\" >> /tmp/smth-strange");
}
if (length($contactgroup) == 0)
{
$contactgroup = "DEFAULT";
}
# Делаем что-то с данными
}
}
Вот такой вот скрипт позволяет разобрать контакты пользователей и сделать с ними что-то полезное. Теперь о грустном. Как я указал в комментариях к коду - имя контакта может быть пустое и это нормальное поведение SimpleXML при варианте 1, который указан выше. При втором же варианте имя контакта пустым быть не может, ибо это ключ хэша. Небольшой тест показал, что имеется два файла, у которых как раз при втором варианте было пустое имя контакта, в следствии чего контакты этих пользователей не парсились. Я свалил данный баг (или фичу) на плечи SimpleXML. Единственное, что стоит отметить это то, что пользоватли этих аккаунтов работали на маках. Так что аккуратнее - старайтесь проводить хотя бы поверхностные проверки, для определения того, что скрипт отрабатывает правильно, иначе можно сильно напороться. Из-за своей большой лени я решил перенести этих пользователей потом вручную, чтобы не писать дополнительные условия в скрипте. Ну в принципе главной цели в данном разделе мы достигли - научились выгребать информацию о пользователях и обрабатывать ее. Поэтому предлагаю идти дальше.
2. Изучения новой БД.
Это очень важный момент во всей этой истории, поскольку если ошибиться с вводом данных, то сервер может не совсем коректно обрабатывать пользователей. Поэтому предлагаю проделывать действия описанные в этом пункте очень внимательно, дабы ничего не упустить. Сейчас нам предстоит понять, как хранится база пользователей OpenFire. Основные моменты, которые необходимо уяснить:
1. Где хранится Логин.
2. Где хранится пароль и в каком виде (шифрованный или нешифрованный)
3. Где хранятся контакты пользователя
4. Где хранятся группы для контактов конкретного пользователя.
5. Где хранится vcard пользователя.
На самом деле нужно ставить вопрос не "Где хранится", а "как правильно добавить". Обратите внимание на слово правильно - оно тут не случайно. Предлагаю действовать таким способом. Ставим OpenFire на любую машинку (я на виртуалку загнал) и цепляем к чистой базе. Дальше создаем нового пользователя, логинимся под ним и создаем левые контакты, группы и т.д., а потом смотрим в каком виде это все записалось в базу. За отсутствием других идей я так и поступил. Итак, представим что у нас уже поднят OpenFire сервер. Смотрим таблички в БД до добавления пользователя и сравниваем с тем, что получилось потом. Для оптимизации данного процесся я заюзал pg_dump, который просто дампил мне нужную базу данных до и после добавления пользователя:
До:
Код: Выделить всё
mail ~ # mkdir dump
mail ~ # cd dump/
mail dump # pg_dump -Upostgres -h 127.0.0.1 --exclude-table=ofrrds OpenFire > before.sql
mail dump #
Код: Выделить всё
mail dump # pg_dump -Upostgres -h 127.0.0.1 --exclude-table=ofrrds OpenFire > after1.sql
Код: Выделить всё
mail dump # diff -Naur before.sql after1.sql
--- before.sql 2011-02-16 11:05:43.000000000 +0300
+++ after1.sql 2011-02-16 11:11:02.000000000 +0300
@@ -1348,6 +1348,7 @@
23 2
21 2
20 2
+25 2
\.
@@ -1608,6 +1609,7 @@
COPY ofuser (username, plainpassword, encryptedpassword, name, email, creationdate, modificationdate) FROM stdin;
admin \N b2bc0ad012178eeb11367f243a8b9433801c26570cad1818f75ba46bc2d4c207 Administrator root@domen.ru 001297828650125 0
demo \N ac732afe8c0eceae8871ae34c8cd6fee228ca464bb6e608f Fastpath Demo Account demo@fastpath.com 001297828674916 001297828674916
+elisium \N 340f1e99b0217570e50f83ed3bca4cba0c3b9a5e06214d458cedb5e50798fd3d Vasiliy fr33m2n@gmail.com 001297833054376 001297833054376
\.
mail dump #
Код: Выделить всё
mail dump # pg_dump -Upostgres -h 127.0.0.1 --exclude-table=ofrrds OpenFire > after2.sql
mail dump # diff -Naur after1.sql after2.sql
--- after1.sql 2011-02-16 11:11:02.000000000 +0300
+++ after2.sql 2011-02-16 11:58:18.000000000 +0300
@@ -1341,7 +1341,6 @@
--
COPY ofid (idtype, id) FROM stdin;
-18 1
19 1
26 2
24 2
@@ -1349,6 +1348,7 @@
21 2
20 2
25 2
+18 6
\.
@@ -1457,6 +1457,7 @@
--
COPY ofprivate (username, name, namespace, privatedata) FROM stdin;
+elisium roster roster:delimiter <roster xmlns="roster:delimiter">\\</roster>
\.
@@ -1559,6 +1560,8 @@
--
COPY ofroster (rosterid, username, jid, sub, ask, recv, nick) FROM stdin;
+1 elisium ozerovvasiliy@jabber.domen.ru 3 -1 -1 ozerovvasiliy
+2 elisium test@jabber.org 0 0 -1 test
\.
@@ -1567,6 +1570,8 @@
--
COPY ofrostergroups (rosterid, rank, groupname) FROM stdin;
+1 0 testgroup2\\subgrouptest21
+2 0 testgroup\\subgrouptest
\.
@@ -1634,6 +1639,7 @@
--
COPY ofvcard (username, vcard) FROM stdin;
+elisium <vCard xmlns="vcard-temp" version="2.0" prodid="-//HandGen//NONSGML vGen v1.0//EN">\n<FN>ФИО</FN>\n<NICKNAME>ник</NICKNAME>\n<TEL>\n<HOME/>\n<VOICE/>\n<NUMBER>309</NUMBER>\n</TEL>\n<URL>доменру</URL>\n</vCard>
\.
mail dump #
Код: Выделить всё
name sub ask recv
both 3 -1 -1
none 0 -1 -1
from 2 -1 -1
to 1 -1 -1
Вроде с базой более или менее решили. Переходим на следующий уровень.
3. Подготовка к миграции.
Основной задачей на данном этапе мне виделось написание скрипта, который генерил бы SQL запросы на добавления пользователя, его контактов, групп и vcard'а. Ну что же, давайте тогда накидаем сначала запросы вручную для каждой из операций, а потом сделаем скриптик.
- добавление пользователя:
Код: Выделить всё
INSERT INTO ofuser (username, plainpassword, encryptedpassword, name, email, creationdate, modificationdate) VALUES ('tested', 'password', NULL, 'Nickname', 'email', 001297833054376, 0);
- Добавление контакта:
Код: Выделить всё
INSERT INTO ofroster (rosterid, username, jid, sub, ask, recv, nick) VALUES (4, 'tested', 'user@jabber.org', 3, -1, -1, 'name');
- Добавление разделителя имени группы:
Код: Выделить всё
INSERT INTO ofprivate (username, name, namespace, privatedata) VALUES ('tested', 'roster', 'roster:delimiter', '<roster xmlns="roster:delimiter">\\</roster>');
- Добавление группы для контакта:
Код: Выделить всё
INSERT INTO ofrostergroups (rosterid, rank, groupname) VALUES (3, 0, 'group1');
- Добавление карточки пользователя:
Код: Выделить всё
INSERT INTO ofvcard (username, vcard) VALUES ('tested', '<vCard xmlns="vcard-temp" version="2.0" prodid="-//HandGen//NONSGML vGen v1.0//EN">\n<FN>ФИО</FN>\n<NICKNAME>ник</NICKNAME>\n<TEL>\n<HOME/>\n<VOICE/>\n<NUMBER>309</NUMBER>\n</TEL>\n<URL>доменру</URL>\n</vCard>');
Ну вроде как с запросами определились. Теперь вперед - ваять скрипт.
Код: Выделить всё
#!/usr/bin/perl
# Подключаем модуль XML
use XML::Simple;
# Задаем необходимые переменные
my $password;
my $username;
my $lastlogindate;
# Задаем начальный rosterid
my $rosterid = 4;
# Считываем список файлов, которые будем обрабатывать
@files = `ls *.xml`;
# Создаем объект XML
$xml = new XML::Simple;
# Открываем файл, куда будем записывать SQL запросы
open (OUT, ">/root/users-jabber.sql");
# Перебираем все файлы
foreach $file (@files)
{
# Обрезаем \n
chomp $file;
# Считываем xml файл
$data = $xml->XMLin($file);
# выводим на экран имя файла, которое парсим
print "########".$file."\n";
# Получаем пароль пользователя
$password = $data->{'password'}->{'content'};
# Разбираем тэг <query>
foreach $each (@{$data->{'query'}})
{
# Получаем имя
if ($each->{'xmlns'} eq 'jabber:iq:register')
{
$username = lc($each->{'username'});
}
# Получаем инфу о последнем логине
if ($each->{'xmlns'} eq 'jabber:iq:last')
{
($year, $month, $day) = (localtime($each->{'last'}))[5,4,3];
$date = sprintf ("%04d-%02d-%02d", $year+1900, $month+1, $day);
$lastlogindate = $date;
}
# Получаем конктакты
if ($each->{'xmlns'} eq 'jabber:iq:roster')
{
# Вариант 1 хранения пользовательских контактов
if(ref($each->{'item'}) eq "ARRAY")
{
foreach $contact (@{$each->{'item'}})
{
# Получаем данные контакта
$contactname = $contact->{'name'};
$contactgroup = $contact->{'group'};
$contactjid = $contact->{'jid'};
$contactsubscription = $contact->{'subscription'};
# Если имя не задано (нормальная ситуация), то используем jid
if (length($contactname) == 0)
{
$contactname = $contactjid;
}
# В зависимости от типа подписки генерируем запрос
if ($contactsubscription eq 'both')
{ print OUT "INSERT INTO ofroster (rosterid, username, jid, sub, ask, recv, nick) VALUES ($rosterid, '$username', '$contactjid', 3, -1, -1, '$contactname');\n"; };
if ($contactsubscription eq 'none')
{ print OUT "INSERT INTO ofroster (rosterid, username, jid, sub, ask, recv, nick) VALUES ($rosterid, '$username', '$contactjid', 0, -1, -1, '$contactname');\n"; };
if ($contactsubscription eq 'from')
{ print OUT "INSERT INTO ofroster (rosterid, username, jid, sub, ask, recv, nick) VALUES ($rosterid, '$username', '$contactjid', 2, -1, -1, '$contactname');\n"; };
if ($contactsubscription eq 'to')
{ print OUT "INSERT INTO ofroster (rosterid, username, jid, sub, ask, recv, nick) VALUES ($rosterid, '$username', '$contactjid', 1, -1, -1, '$contactname');\n"; };
# Если группа задана, то генерим запрос на ее добавление
if (length($contactgroup) != 0)
{
print OUT "INSERT INTO ofrostergroups (rosterid, rank, groupname) VALUES ($rosterid, 0, '$contactgroup');\n";
}
# Увеличиваем счетчик id
$rosterid++;
}
}
# Вариант 2 хранения пользовательских контактов
if(ref($each->{'item'}) eq "HASH")
{
# Получаем имена контактов
@names = keys %{$each->{'item'}};
foreach $name (@names)
{
# Получаем данные о контакте
$contactname = $name;
$contactgroup = $each->{'item'}->{$name}->{'group'};
$contactjid = $each->{'item'}->{$name}->{'jid'};
$contactsubscription = $each->{'item'}->{$name}->{'subscription'};
# В зависимости от типа подписки генерируем запрос
if ($contactsubscription eq 'both')
{ print OUT "INSERT INTO ofroster (rosterid, username, jid, sub, ask, recv, nick) VALUES ($rosterid, '$username', '$contactjid', 3, -1, -1, '$contactname');\n"; };
if ($contactsubscription eq 'none')
{ print OUT "INSERT INTO ofroster (rosterid, username, jid, sub, ask, recv, nick) VALUES ($rosterid, '$username', '$contactjid', 0, -1, -1, '$contactname');\n"; };
if ($contactsubscription eq 'from')
{ print OUT "INSERT INTO ofroster (rosterid, username, jid, sub, ask, recv, nick) VALUES ($rosterid, '$username', '$contactjid', 2, -1, -1, '$contactname');\n"; };
if ($contactsubscription eq 'to')
{ print OUT "INSERT INTO ofroster (rosterid, username, jid, sub, ask, recv, nick) VALUES ($rosterid, '$username', '$contactjid', 1, -1, -1, '$contactname');\n"; };
# Если группа задана, то генерим запрос на ее добавление
if (length($contactgroup) != 0)
{
print OUT "INSERT INTO ofrostergroups (rosterid, rank, groupname) VALUES ($rosterid, 0, '$contactgroup');\n";
}
# Увеличиваем счетчик id
$rosterid++;
}
}
}
}
# Генерим запрос на добавление пользователя
print OUT "INSERT INTO ofuser (username, plainpassword, encryptedpassword, name, email, creationdate, modificationdate) VALUES ('$username', '$password', NULL, '$username', NULL, 001297833054376, 0);\n";
# Генерим запрос на добавления разделителя для групп
print OUT "INSERT INTO ofprivate (username, name, namespace, privatedata) VALUES ('$username', 'roster', 'roster:delimiter', '\<roster xmlns=\"roster:delimiter\"\>\\\</roster\>');\n";
}
# Закрываем файл с SQL
close OUT;
Код: Выделить всё
mail ~ # psql -Upostgres -g 127.0.0.1 OpenFire < /root/users-jabber.sql
1. Обработать тех двух пользователей (которые под маками работают) вручную
2. Увеличить значение счетчиков в ofid таблице
Да, чуть не забыл - VCard данные я в этом скриптике не [классический секс].. Что-то у меня так и не получилось заставить SimpleXML выдать xml output (впадлу гуглить было), поэтому я наваял второй скриптик для выдерания vcard из файла пользователя:
Код: Выделить всё
#!/usr/bin/perl
use utf8;
use XML::Simple;
my $vcard = "";
@files = `ls *.xml`;
$xml = new XML::Simple;
open (OUT, ">/root/users-vcard.sql");
foreach $user_file (@files)
{
chomp $user_file;
$vcard = "";
$data = $xml->XMLin($user_file);
foreach $each (@{$data->{'query'}})
{
# Getting username
if ($each->{'xmlns'} eq 'jabber:iq:register')
{
$username = lc($each->{'username'});
}
}
open(IN, "<$user_file");
while(<IN>)
{
chomp $_;
print "\$_: ".$_."\n";
if ($_ =~ m/\<vCard/g)
{
($a, $b) = split('<vCard', $_);
($c, $d) = split('</vCard', $b);
}
}
close(IN);
if (!($c eq ''))
{
$vcard = "<vCard ".$c."</vCard>";
$vcard =~ s/\'/\"/g;
} else
{
$vcard = "<vCard></vCard>";
}
print OUT "INSERT INTO ofvcard (username, vcard) VALUES (lower('$username'), '$vcard');\n";
}
close OUT;
4. Подведение итогов.
Небольшое резюме:
- Было проигнорировано достаточное количество полей, как в старой базе, так и в новой, поэтому считать перенос базы полным, как мне кажется, не совсем разумно.
- Назвать перенос автоматическим нельзя, скорее он прошел в полуавтоматическом режиме. Но это неважно - главное перенесено большое число пользователей автоматически.
- Самое главное - это то, что пользователи не заметят переноса базы вообще. Это огромный плюс.
- Перевод базы занял где-то один день, но это все равно явно быстрее, чем при переносе вручную.
Ну вообщем я надеюсь, что данное мини исследование кому-нибудь сможет помочь.
5. Используемая литература.
[1] http://www.igniterealtime.org/builds/op ... .html#ofID
[2] http://community.igniterealtime.org/docs/DOC-1832
[3] http://community.igniterealtime.org/thread/30368
[4] http://xmpp.org/extensions/xep-0162.html
[5] http://community.igniterealtime.org/docs/DOC-1674