(495) 925-0049, ITShop интернет-магазин 229-0436, Учебный Центр 925-0049
  Главная страница Карта сайта Контакты
Поиск
Вход
Регистрация
Рассылки сайта
 
 
 
 
 

Голосовая поддержка в XML: Часть 3. Разрабатываем голосовое приложение для работы с блогами (исходники)

Мартин Браун

Введение

Все больше и больше людей в последнее время увлекаются ведением онлайн-дневников (блогов), чтобы публично выражать свое мнение и попросту заявлять о себе. Почему бы не использовать VoiceXML для нормального голосового общения со своим блогом, в том числе и Twitter? Читая нашу статью, вы научитесь этому и многим другим вещам, таким как:

  • Динамически создавать фрагменты VoiceXML из данных, полученных с удаленных ресурсов.
  • Передавать данные внутрь VXML-файлов.
  • Обмениваться данными с блогом в формате VXML.
  • Автоматически обновлять свой статус в Twitter.

Об этой серии

Аудио и, в частности, голосовые сервисы становятся все более популярны в Интернет. В качестве примеров могут служить всевозможные музыкальные ресурсы, а также Web-трансляции, доступные в онлайн. Статьи нашей серии рассказывают о способах совмещения голосовых технологий и XML при разработке таких приложений, как:

  • В первой части: RSS-reader с голосовой поддержкой.
  • Во второй части: календаря с голосовой поддержкой.
  • В третьей части: голосовой программы для работы с блогами и Twitter.
  • В четвертой части: программы для голосового поиска в Yahoo.

Ведение блога

Нет ничего хуже для заядлого блоггера, чем не иметь возможности сделать очередную запись в своем блоге. К счастью, использование VoiceXML помогает существенно облегчить эту проблему. В частности, можно разработать VoiceXML приложение с голосовыми функциями, позволяющими добавлять записи в блог, просто позвонив по телефону.

К сожалению, существующие на текущий момент VoiceXML-платформы не позволяют надиктовывать произвольный текст для отправки в блог. Тем не менее, с помощью VoiceXML можно делать записи в блоге, выбирая из списка стандартных сообщений.

Чтобы реализовать эту идею, нам потребуется Java™-сервлет, который будет генерировать VXML-фрагмент, а также обрабатывать входные данные, формируя из них записи, которые затем будут отправляться в блог. На первом этапе нас не будут интересовать вопросы выбора блога и аутентификации пользователя по причинам, которые станут ясны позднее, когда мы начнем рассматривать интерфейс для удаленной работы с блогами.

В целом процесс голосовой работы с блогом будет выглядеть следующим образом:

  1. Сервлет получает список категорий данного блога
  2. Сервлет генерирует VXML-файл для голосового вывода списка категорий, а также фраз, доступных для выбора пользователем
  3. Информация, введенная пользователем (выбранные фразы и категория сообщения), передается в сервлет средствами VXML
  4. Сервлет формирует новую запись и отправляет ее в блог
  5. VXML выдает голосовое сообщение "отправка завершена", и весь процесс завершается

Графически процесс работы изображен на рисунке 1.

Рисунок 1. Принцип голосовой работы с блогами
Voice blogging structure

Прежде чем мы перейдем к VXML, необходимо обратить внимание на интерфейс для работы с удаленным блог-сервисом.

Интерфейс для работы с блогами

Существует несколько способов для отправки сообщений в удаленные блоги, но большинство сервисов поддерживают интерфейсы MetaWeblog/MovableType или Blogger API на основе стандартного протокола XML-RPC (XML Remote Procedure Call). Интерфейсы очень похожи, но MetaWeblog/MovableType был разработан с учетом недостатков Blogger API, а поэтому более удобен на практике.

Перед тем как заняться отправкой новой записи в блог, необходимо выполнить определенные подготовительные действия, которые позволят в будущем выполнять вызовы через XML-RPC. Для работы с большинством блог-сервисов необходимы следующие данные:

  1. Адрес (XML-RPC URL) блога. Некоторые сервисы, например, WordPress, предоставляют специальный компонент, служащий для обращения к блогу. В этом случае необходим URL данного скрипта, например, http://mcslp.com/xmlrpc.php.
  2. Username - имя учетной записи пользователя блога.
  3. Password - пароль учетной записи пользователя блога.

Кроме этого, необходим еще идентификатор блога. Он всегда равен единице для сайтов и сервисов (например, WordPress), на которых размещен только один блог. Но если на сервере располагаются несколько блогов, как, скажем, в случае MovableType, b2evolution или блогов, являющихся частью крупной системы управления контентом, то для обращения к ним нужно использовать уникальный идентификатор.

Очевидно, что передача всех вышеперечисленных данных через голосовой интерфейс VoiceXML сопряжена с определенными проблемами. Однако, можно разработать расширенный сервис, позволяющий пользователям авторизовываться, вводя только числа, а затем загружающий всю необходимую информацию непосредственно из учетной записи.

Для решения проблемы соединения с блогом, мы создадим класс-обертку над XML-RPC-соединением, выполняющую две важные функции:

  • Получение списка категорий
  • Передача введенной пользователем информации в методы основного класса

Таким образом, экземпляр класса-обертки будет в основном отвечать за подготовку ключевых параметров, а также за создание объекта XML-RPC, необходимого для обмена данными с блог-сервисом.

Чтобы упростить разработку, можно использовать Apache-библиотеку XML-RPC. В листинге 1 приведена центральная часть класса BlogPost, включающая конструктор, внутри которого создается объект XML-RPC-клиента.

Листинг 1. Конструктор класса BlogPost

                 
import java.util.*;
import java.io.*;
import java.net.URL;
import org.apache.xmlrpc.client.XmlRpcClient;
import org.apache.xmlrpc.client.XmlRpcClientConfigImpl;

public class BlogPost {
    String username;
    String password;
    String blogid;
    String blogurl;
    XmlRpcClientConfigImpl xmlrpcConfig = new XmlRpcClientConfigImpl();
    XmlRpcClient client = new XmlRpcClient();

    public BlogPost(String user,
                   String pass,
                   String id,
                   String url) {
        username = user;
        password = pass;
        blogid = id;
        blogurl = url;

        try {
            xmlrpcConfig.setServerURL(new URL(blogurl));
            client.setConfig(xmlrpcConfig);
        } catch (Exception ex) {
            ex.printStackTrace();
            System.out.println("ERROR: " + ex.getMessage());
        }
    }

Из листинга 1 видно, как создается экземпляр класса XmlRpcClient, отвечающий за обмен данными с сервисом блогов, а так же, как в него передаются полученные от пользователя данные.

Теперь создадим сам объект типа BlogPost и передадим ему имя пользователя, пароль, URL и идентификатор блога, как показано в листинге 2.

Листинг 2. Создание экземпляра BlogPost

                

BlogPost bp = new BlogPost("user",
                           "pass",
                           "1", 
                           "http://myblog.com/xmlrpc.php");

Кроме непосредственно самого сообщения, пользователь должен еще выбрать категорию из списка доступных категорий для данного блога. Для этого необходимо получить и предоставить этот список пользователю.

Получение списка категорий

Для получения списка категорий, используемых для записей в данном блоге, существует функция getCategories, предоставляемая интерфейсом (API) блога, основанным на XML-RPC. В качестве аргументов функция принимает идентификатор блога, имя пользователя и пароль.

Возвращаемые функцией данные имеют более сложную структуру. Они представляют собой массив хэш-таблиц, каждая из которых содержит идентификатор категории, ее имя, URL блога, в котором используется категория, а так же URL RSS-канала для данной категории. Пример хэш-таблицы, описывающей конкретную категорию, приведен в таблице 1.

Таблица 1. Структура хэш-таблицы, содержащей данные категории

categoryId 3
htmlUrl http://www.thewriting.biz/?cat=3
rssUrl http://www.thewriting.biz?feed=rss2&cat=4
categoryName Making Money
description Making Money

Все, что нам нужно из этих данных - это идентификатор и имя категории (categoryId и categoryName). Идентификатор будет использоваться при отправке сообщения в блог, а имя - для формирования списка доступных категорий. Этот список понадобится внутри VoiceXML для предоставления пользователю возможности выбора категории по имени.

В целом, процесс будет выглядеть следующим образом: сначала посылается запрос к сервису блогов, который, в свою очередь, возвращает список категорий. Затем для каждой категории извлекаются ее идентификатор и имя, и помещаются в хэш-таблицу (экземпляр Java-класса Hashtable). Последняя будет использоваться в VoiceXML-части приложения для формирования списка категорий. Метод GetCategories() класса BlogPost показан в листинге 3.

Листинг 3. Получение и сохранение списка категорий блога в хэш-таблице

                
public Hashtable GetCategories() {
    Hashtable categories = new Hashtable();
    Object[] cats = new Object[] {};

    try {
        cats = (Object [])
            this.client.execute("metaWeblog.getCategories",
                                new Object[] {this.blogid,
                                              this.username, 
                                              this.password});
        
    } catch (Exception ex) {
        ex.printStackTrace();
        System.out.println("ERROR: " + ex.getMessage());
    }

    for(int i = 0; i < cats.length; i++ ) {
        HashMap category = (HashMap)cats[i];
        categories.put(category.get("categoryId"),
                       category.get("categoryName"));
    }

    return categories;
}

С помощью полученной хэш-таблицы сервлет будет создавать список категорий, доступных для выбора пользователем.

Отправка сообщения в блог

Процесс отправки сообщения состоит из ряда шагов, особенно если оно относится к нескольким категориям. Некоторые блог-сервисы обрабатывают всю отправляемую информацию за один шаг, другие же требуют присваивать категории после отправки сообщения. По этой причине лучше выполнять следующую последовательность действий:

  1. Добавить сообщение в блог, но пометить его специальным образом, чтобы оно не было автоматически опубликовано (для этого существует метод newPost в RPC). При этом блог-сервис вернет уникальный номер добавленного сообщения.
  2. Используя полученный номер, присвоить сообщению необходимые категории с помощью RPC-метода setCategories.
  3. Загрузить полную информацию о добавленном сообщении, используя RPC-метод getPost.
  4. Пометить сообщение как готовое для публикации (RPC-метод editPost RPC).

Последовательность XML-RPC-вызовов к удаленному сервису блогов показана в листинге 4. Необходимо отметить, что, как и в случае категорий, формат данных несколько сложнее, чем просто список аргументов. В частности, необходимо создать хэш-таблицу, содержащую информацию о добавляемом сообщении. Еще одна хэш-таблица требуется для списка категорий, которая затем передается в качестве элемента массива в метод, присваивающий сообщению категории. Структура получается аналогичной той, что возвращается методом getCategories, где использовался массив хэш-таблиц.

Листинг 4. Публикация сообщения в блоге

                
public Boolean PostIt(String title,
                      String description,
                      Integer category) {
    
    Hashtable post = new Hashtable();
    if (title != null) post.put("title", title);
    post.put("dateCreated", new Date());
    post.put("description", description);

    Hashtable categories = new Hashtable();
    categories.put("categoryId",category);
    
    String blogpostid = "";
    Boolean result = Boolean.FALSE;

    try {
        blogpostid = (String)
            this.client.execute("metaWeblog.newPost",
                           new Object[] {this.blogid, 
                                         this.username, 
                                         this.password, 
                                         post, 
                                         Boolean.FALSE});

        result = (Boolean) 
            this.client.execute("mt.setPostCategories",
                           new Object[] {blogpostid, 
                                         this.username, 
                                         this.password, 
                                         new Object[] {categories}});

        HashMap postdetail = (HashMap) 
            this.client.execute("metaWeblog.getPost",
                           new Object[] {blogpostid, 
                                         this.username, 
                                         this.password, });

        result = (Boolean) 
            this.client.execute("metaWeblog.editPost",
                           new Object[] {blogpostid, 
                                         this.username, 
                                         this.password,
                                         postdetail,
                                         Boolean.TRUE });

    } catch (Exception ex) {
        ex.printStackTrace();
        System.out.println("ERROR: " + ex.getMessage());
    }
    return(result);
}

Непосредственная публикация сообщения осуществляется вызовом метода PostIt(), принимающим идентификатор сообщения в качестве аргумента:

bp.PostIt("Title","Content",5);
 

Теперь, когда разработан механизм отправки сообщений и есть класс, его реализующий, остается только написать сервлет-обертку на Java, в чьи обязанности будет входить генерирование VXML и обработка входных данных, необходимых для создания сообщения.

Размышления над правилами ввода

Перед тем, как заняться генерированием VXML-документов, стоит обратить внимание на некоторые правила, регулирующие ввод данных от пользователя.

Грамматика для проверки ввода описывается ниже с помощью круглых и квадратных скобок. Как вы помните, пользователь не может диктовать произвольный текст, вместо этого он выбирает из списка стандартных фраз, используемых для создания новых сообщений.

В таблице 2 приведены наиболее часто используемые операторы.

Таблица 2. Основные операторы

Оператор

Пример

Описание

Дизьюнкция [happy sad] Допускается использование одного из слов в списке.
Конкатенация (very happy) Допускается только вся фраза целиком.
Необязательная фраза (?very happy) Фраза, следующая сразу за знаком "?", не является обязательной для ввода. В данном примере и "happy", и "very happy" являются допустимыми значения.
Позитивное замыкание (+very happy) Фраза, следующая за знаком "+", должна быть введена пользователем как минимум один раз. Таким образом, в данном примере фразы "very happy" и "very very happy" являются допустимыми.
Замыкание Клини (*very happy) Фраза, следующая за знаком "*", не является обязательной, но может повторяться произвольное число раз. В данном примере строки "happy", "very happy" и "very very happy" являются корректными.

Разумеется, составлять корректные фразы можно, комбинируя различные варианты с помощью операторов конкатенации и дизьюнкции.

Для нашего приложения мы будем использовать грамматические правила, приведенные в листинге 5.

Листинг 5. Пример грамматических правил, опеределяющих допустимые сообщений блога

                
(eye am [happy {<phrase "I am happy">}
        sad {<phrase "I am sad">}
        traveling {<phrase "I am traveling">}
        (on business){<phrase "I am on business">}
        ] )

Первым делом отметим слово "eye" в том месте, где должно было быть "I". Дело в том, что большинство VoiceXML-систем не могут различить разницу в произношении слова и единичной буквы, а некоторые и вовсе не воспринимают слова, состоящие менее чем из двух символов. В этом случае приходится заменять их на схожие по звучанию многобуквенные слова и использовать предопределенные значения полей VXML для сохранения того, что на самом деле должно было быть введено.

Далее мы просто приведем список фраз для выбора, каждая из которых является корректной, исходя из грамматики в листинге 5:

  • I am happy
  • I am sad
  • I am traveling
  • I am on business

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

Основной фрагмент VXML

Основная часть VXML-документа приведена в листинге 6.

Листинг 6. Фрагмент XVML, определяющий основные голосовые действия

                
<?xml version="1.0" encoding ="UTF-8"?>

<!DOCTYPE vxml PUBLIC "-//W3C//DTD VOICEXML 2.1//EN" 
               "http://www.w3.org/TR/voicexml21/vxml.dtd">

<vxml version="2.1" xmlns="http://www/w3/org/2001/vxml"
    xml:lang="en-US">

<form id="blogging">
 <block>
  <prompt>
     Добро пожаловать в программу для работы с блогом
  </prompt>
 </block>

<field name="phrase">
  <prompt>Скажите Ваше состояние для записи в блог</prompt>

    <grammar type="text/gsl">
        <![CDATA[
(eye am [happy {<phrase "I am happy">}
        sad {<phrase "I am sad">}
        traveling {<phrase "I am traveling">}
        (on business) {<phrase "I am on business">}
        ] )

]]>
</grammar>
 </field>

<field name="location">
<prompt>Скажите Ваше текущее местоположение</prompt>
<option>Эдинбург</option>
<option>Нью-Йорк</option>
<option>Лондон</option>
<option>Париж</option>
<option>Стокгольм</option>
</field>

  <filled>
  <prompt>
    Новая запись в блоге:  <value expr="phrase"/>. 
    Мое местоположение: <value expr="location"/>
  </prompt>

  <submit name="/VXMLBlogPost/blogpost" 
          namelist="phrase category location"/>
  </filled>

</form>

</vxml>

Характерный пример общения с пользователем с помощью данного VXML приведен в листинге 7.

Листинг 7. Пример голосового общения

                Сервис (С): Добро пожаловать в программу для работы с блогом
С: Скажите Ваше состояние для записи в блог.
Пользователь (П): I am happy.
С: Скажите Ваше текущее местонахождение.
П: Лондон.
С: Новая запись в блоге: I am happy. Мое местоположение: Лондон.

Это сообщение должно формироваться динамически. Затем к нему будет добавлен список категорий из тех, что были созданы для данного блога.

Вывод списка категорий

Для голосового вывода списка категорий используется тег prompt, который вставляется в сгенерированный в сервлете VXML. Для получения голосового ввода от пользователя можно использовать теги <option>, с помощью которых можно так же присвоить выбранной категории уникальный номер, который затем понадобится на этапе отправки сообщения в блог. В листинге 8 приведен фрагмент кода, формирующего список категорий в формате VXML.

Листинг 8. Вывод списка категорий

                
Hashtable cats = this.bp.GetCategories();

Enumeration keys = cats.keys();
while ( keys.hasMoreElements() ) {
    String key = (String)keys.nextElement();
    String cat = (String)cats.get( key );
    out.println("<option value=\"" + key + "\">" + cat + "</option>");
}

Полученный фрагмент VXML вместе с обрамляющим тегом field показан в листинге 9.

Листинг 9. Сгенерированный фрагмент VXML

                
<field name="category">       
  <prompt>Введите категория для данного сообщения</prompt>       
<option value="1">Комментарий</option>
<option value="2">Путешествие в отпуске</option>
</field>

Теперь все готово к обработке голосового ввода от пользователя.

Обработка ввода и отправка сообщения

С помощью тега "submit" данные из полей VXML-документа передаются в удаленное приложение в виде HTTP-строки с параметрами. Этот тег находится внутри блока "filled" как показано в листинге 10.

Листинг 10. Тег submit

                
<submit name="/VXMLBlogPost/blogpost" 
        namelist="phrase category location"/>

Все, что нужно для формирования и отправки сообщения - это разобрать строку с параметрами и передать все данные сообщения в метод PostIt() объекта класса BlogPost. Пример показан в листинге 11.

Листинг 11. Получение данных и отправка сообщения в блог

                
public void doPost(HttpServletRequest req, HttpServletResponse res)
    throws ServletException, IOException {

    PrintWriter out = null;
    out = res.getWriter();
    res.setContentType("text/xml");

    this.bp.PostIt( req.getParameter("title"),
                    req.getParameter("title") +
                    ". I am currently located in " +
                    req.getParameter("location"),
                    Integer.parseInt(req.getParameter("category")));

    printHeader(out);
    out.println("<form><block>" + 
         "<prompt>Blog posted</prompt><disconnect/>" +
         "</block></form>");
    printFooter(out);
}

Теперь вы можете добавлять записи в свой блог, находясь в любой точке земного шара. Все что нужно - это продиктовать по телефону сообщение, свое местонахождение и выбрать желаемую категорию.

Теперь посмотрим, как можно добавить голосовые функции к сервису мини-блогов Twitter.

Twitter

Twitter - это сервис Web 2.0, который привлекает к себе все более пристальное внимание. Он позволяет поддерживать связь с друзьями не только через Web, но и с помощью мобильного телефона и программ для общения (instant messengers). При этом ваши друзья также не теряют вас из виду.

Идея заключается в том, что вы посылаете короткие текстовые сообщения (до 140 символов), говоря, где вы находитесь и чем занимаетесь в текущий момент времени. Изначально Twitter задумывался именно как коротких статусных сообщений, но со временем превратился в вариант мини-блога. Его можно использовать для кратких замечаний, которые затем можно разослать друзьям.

 
Ограничения Twitter
Хотя, в отличие от обычного блога, Twitter не предоставляет списка категорий для сообщений, вы, тем не менее, можете пополнять список местонахождений.
 

Есть ли у этого сервиса что-то общее с голосовыми блогами? Ответ: всё! Но помните, что поскольку в данный момент нельзя делать произвольные голосовые записи в блогах, вам придется ограничиться выбором из списка коротких сообщений. Этого вполне достаточно для путешествующего пользователя Twitter. Вы можете предопределить список наиболее часто используемых вами сообщений или даже предоставить Web-интерфейс, через который другие пользователи могут добавлять свои сообщения.

За исключением того, что Twitter не поддерживает XML-RPC, весь процесс остался неизменным по сравнению с обычными блог-сервисами. Вместо XML-RPC Twitter-сообщения (или "tweets") добавляются через HTTP-запросы типа POST (см. листинг 12).

Листинг 12. Добавления сообщения в Twitter через HTTP POST

                
import java.net.*;
import java.io.*;

public class BlogPost {

    public BlogPost(String user,
       String pass, String status) {
    try{
           Authenticator.setDefault(new PostAuthenticator(user, pass));

             URL url = new URL ("http://twitter.com/statuses/update.xml");
             URLConnection conn = url.openConnection();
             conn.setDoInput (true);
             conn.setDoOutput (true);
              conn.setUseCaches (false);

             conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
             DataOutputStream printout = new DataOutputStream (conn.getOutputStream ());
             String requestBody = "status=" + URLEncoder.encode (status);
             printout.writeBytes (requestBody);
             printout.flush ();
             printout.close ();

             DataInputStream input = new DataInputStream (conn.getInputStream ());
             String str;
             while (null != ((str = input.readLine()))){
                    System.out.println (str);
             }
             input.close ();
      } catch (Exception e){
           e.printStackTrace();
      }
     }
 
     public static void main (String args[]){
           BlogPost bp = new BlogPost("myusername", "mypassword", 
     "Playing with voice blogging to the Twitter API!");
     }
  
}

Код класса Authenticator мы приведем чуть позже. В листинге 12 показано, как установить простое HTTP-соединение с Twitter, а также получать и отправлять через него данные.

Отличительной особенностью POST-запроса в данном примере является то, что параметры, например, status, передаются в теле запроса, а не добавляются к URL.

В зависимости от того, отправляете вы запрос к update.xml или update.json, вы можете получить ответ от сервиса в определенном формате. Его можно проанализировать в случае необходимости, но для работы с блогом он не важен.

Разумеется, потребуется аутентификация: вы же не хотите, чтобы любой желающий получил доступ к вашей учетной записи в Twitter. За это отвечает класс Authenticator, метод которого вызывается в начале листинга.

Сам класс Authenticator является абстрактным, поэтому необходимо создать класс-наследник и реализовать метод getPasswordAuthentication() для сохранения имени пользователя и пароля, как показано в листинге 13.

Листинг 13. Аутентификация пользователя на сервере

                
import java.net.Authenticator;
import java.net.PasswordAuthentication;

public class PostAuthenticator extends Authenticator {

  private String user;
  private String pass;
  
  public PostAuthenticator (String user, String pass){
    this.user = user;
    this.pass = pass;
  }
  
  protected PasswordAuthentication getPasswordAuthentication(){
    char[] passwd = new String(this.pass).toCharArray();
        PasswordAuthentication auth = new PasswordAuthentication(this.user, passwd);
    return auth;
  }
  
}

Возвращаемый этим методом объект можно сохранить в объекте класса Authenticator и использовать в Java-сервлете в момент возникновения необходимости в аутентификации.

Как результат - вы можете отправлять tweets когда захотите.

Заключение

В статье рассказывалось, как создать приложение, использующее VoiceXML для голосового создания и отправки сообщение в блог. К сожалению, полноценный речевой разбор пока не поддерживается достаточно широко, поэтому приходится ограничиваться выбором простых фраз в соответствии с заданными грамматическими правилами. Впрочем, подобный метод отлично работает в случае Twitter. Кроме этого, при отправке сообщений на Twitter можно задавать свое текущее местоположение.

В итоге получилось приложение, выполненное в виде сервлета на Java, которое генерирует VXML-данные для общения с пользователем, а также обрабатывает речевой ввод для последующей отправки сообщения в блог.

Оставайтесь с нами, и в четвертой части серии мы создадим приложение, принимающее на вход VoiceXML и выполняющее как Web, так и локальные запросы через поисковый интерфейс Yahoo Search API.

Файлы для загрузки


 Распечатать »
 Правила публикации »
  Написать редактору 
 Рекомендовать » Дата публикации: 01.08.2008 
 

Магазин программного обеспечения   WWW.ITSHOP.RU
Business Studio 4.2 Enterprise. Конкурентная лицензия + Business Studio Portal 4.2. Пользовательская именная лицензия.
Dr.Web Security Space, продление лицензии на 1 год, 1 ПК
ZBrush 4R6 Win Commercial Single License ESD
Bitdefender Antivirus Plus 2020/1 год/1 ПК
Stimulsoft Reports.Ultimate Single License Includes one year subscription, source code
 
Другие предложения...
 
Курсы обучения   WWW.ITSHOP.RU
 
Другие предложения...
 
Магазин сертификационных экзаменов   WWW.ITSHOP.RU
 
Другие предложения...
 
3D Принтеры | 3D Печать   WWW.ITSHOP.RU
 
Другие предложения...
 
Новости по теме
 
Рассылки Subscribe.ru
Информационные технологии: CASE, RAD, ERP, OLAP
Программирование на Microsoft Access
CASE-технологии
OS Linux для начинающих. Новости + статьи + обзоры + ссылки
СУБД Oracle "с нуля"
Вопросы и ответы по MS SQL Server
Adobe Photoshop: алхимия дизайна
 
Статьи по теме
 
Новинки каталога Download
 
Исходники
 
Документация
 
 



    
rambler's top100 Rambler's Top100