понедельник, 17 мая 2010 г.

Знакомимся: RSS/Atom парсер/генератор для Java - проект ROME


Введение


Форматы RSS/Atom активно используются для синдикации данных. Многие пользователи сети Интернет уже не представляют свою жизнь без использования RSS/Atom агрегаторов для чтения блогов, новостных лент, получения бизнес-информации и т.д. Даже архивы списков почтовых рассылок доступны с помощью RSS. Atom, в свою очередь, так же используется как один из форматов взаимодействия с RESTfull сервисами.

В связи с широкой распространенностью RSS и Atom лент необходимо программное обеспечение для их генерации и разбора (парсинга), причем желательно в некоторую объектную модель API работы с которой проще чем DOM. Для решения данной задачи в мире Java существует библиотека com.sun.syndication, которая так же называется проект ROME. Последняя версия библиотеки - 1.0 - выпущена в марте 2009-го года.



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

Помимо непосредственно парсера и генератора потоков в форматах RSS/Atom, ROME включает в себя следующие подпроекты:


  • ROME Fetcher - кэширующий загрузчик фидов, который поддерживает поиск потоков через условный HTTP GET. Обеспечена поддержка ETags, сжатия в формате GZip, работа с RFC3229 Delta encoding.

  • Rome Modules - обеспечивает поддержку таких расширений для фидов, как GeoRSS, iTunes, Microsoft SSE/SLE, Google GData и других. Так же можно писать свои модули.

  • ROME Propono - обеспечивает поддержку протоколов публикации, особенно Atom Publishing Protocol и устаревший MetaWeblog API. Propono включает в себя библиотеку для построения Atom-клиентов, Atom сервлет-фреймворк и блог-клиент, который поддерживает и Atom-протокол, и MetaWeblog API.

  • ROME Aqueduct - определяет слой DAO, используемый для хранения SyndFeed'ов. Реализация DAO под названием Aqueduct-Prevayler позволяет обеспечить простое хранение сотен и тысяч объектов класса SyndFeed и предоставляет механизм запросов, основанный на регулярных выражениях.

  • ROME.Mano - pipeline-фреймворк для RSS и Atom фидов.

  • OPML for ROME - парсер и утилиты для работы с Outline Processor Markup Language.



Библиотека ROME доступна для использования в Maven при разрешении зависимостей. Для этого необходимо добавить репозиторий с последней версией в файл pom.xml:

<repository>

  <id>maven2-repository.dev.java.net</id>

  <name>Java.net Repository for Maven</name>

  <url>http://download.java.net/maven/2/</url>

  <layout>default</layout>

</repository>

 


В данном репозитории находятся jar-архивы со следующими версиями проектов:

  • ROME 1.0
  • ROME Fetcher 1.0
  • ROME Modules 0.3.2


Чтобы добавить ROME в проект, необходимо в pom.xml прописать следующее:

<dependency>

  <groupId>rome</groupId>

  <artifactId>rome</artifactId>

  <version>1.0</version>

</dependency>

 


Так же стоит сказать, что ROME включен в состав проекта Eclipse Orbit в виде бандла com.sun.syndication, который зависит от бандла org.jdom. Наличие библиотеки в проекте Orbit обозначает, что ее можно использовать для разработки проектов, расположенных на еclipse.org, и таким проектам будет проще пройти IP-процесс.

Теперь можно перейти непосредственно к демонстрации того, как же, собственно, использовать библиотеку. Рассмотрим следующие аспекты:

  • Разбор RSS/Atom потока

  • Генерация RSS и Atom лент

  • Использование ROME совместно с Servlet API

  • Добавление дополнительных данных в поток с помощью модулей


Разбор RSS/Atom потока


Задача разбора RSS/Atom потока включает в себя два момента:
- Создание некого объектного представления для XML данных, из которых состоит поток. Это представление строится классом com.sun.syndication.io.XmlReader.
- Непосредственно разбор представления и формирование объекта класса SyndFeed, инкапсулирующего данные о потоке и его элементах (т.е. записях, из которых состоит лента). Разбор осуществляется в методе build класса SyndFeedInput. Данный класс сам определяет формат потока и вызывает нужные обработчики.

Работа с классом SyndFeed довольно тривиальна, информация о его методах доступна в Javadoc, да и их названия довольно информативны. Скажу только, что есть ряд методов, снабженных суффиксом Ex, например feed.getDescriptionEx(), которые возвращают не строку, а объект класса SyndContent, который инкапсулирует значение, его MIME-тип и режим (mode, характерен для лент в формате Atom 0.3). Так же следует учитывать, что библиотека написана на Java 1.4 и не использует дженерики и другие новшества, появившиеся в Java 1.5.

Пример разбора RSS-потока с помощью ROME:

package name.samolisov.rss.demo;



import java.io.IOException;

import java.net.MalformedURLException;

import java.net.URL;



import com.sun.syndication.feed.synd.SyndContent;

import com.sun.syndication.feed.synd.SyndEntry;

import com.sun.syndication.feed.synd.SyndFeed;

import com.sun.syndication.feed.synd.SyndPerson;

import com.sun.syndication.io.FeedException;

import com.sun.syndication.io.SyndFeedInput;

import com.sun.syndication.io.XmlReader;



public class DemoSyndParser

{

    public SyndFeed parseFeed(String url)

            throws IllegalArgumentException, MalformedURLException, FeedException, IOException

    {

        return new SyndFeedInput().build(new XmlReader(new URL(url)));

    }



    public void printRSSContent(SyndFeed feed)

    {

        System.out.println("About feed:");

        System.out.println("Author: " + feed.getAuthor());

        System.out.println("Authors:");

        if (feed.getAuthors() != null)

        {

            for (Object author : feed.getAuthors())

            {

                System.out.println(((SyndPerson) author).getName());

                System.out.println(((SyndPerson) author).getEmail());

                System.out.println(((SyndPerson) author).getUri());

                System.out.println();

            }

        }

        System.out.println("Title: " + feed.getTitle());

        System.out.println("Title Ex: " + feed.getTitleEx());

        System.out.println("Description: " + feed.getDescription());

        System.out.println("Description Ex: " + feed.getDescriptionEx());

        System.out.println("Date" + feed.getPublishedDate());

        System.out.println("Type: " + feed.getFeedType());

        System.out.println("Encoding: " + feed.getEncoding());

        System.out.println("(C) " + feed.getCopyright());      

        System.out.println();



        for (Object object : feed.getEntries())

        {

            SyndEntry entry = (SyndEntry) object;

            System.out.println(entry.getTitle() + " - " + entry.getAuthor());

            System.out.println(entry.getLink());

            for (Object contobj : entry.getContents())

            {

                SyndContent content = (SyndContent) contobj;

                System.out.println(content.getType());

                System.out.println(content.getValue());

            }



            SyndContent content = entry.getDescription();

            if (content != null)

                System.out.println(content.getValue());



            System.out.println(entry.getPublishedDate());

            System.out.println();

        }

    }



    public static void main(String[] args) throws Exception

    {

        DemoSyndParser parser = new DemoSyndParser();

        parser.printRSSContent(parser.parseFeed("http://feeds2.feedburner.com/samolisov"));

    }

}

 


Генерация RSS и Atom лент


Процесс генерации, естественно, обратен процессу разбора. Прежде всего следует создать объект класса SyndFeedImpl(), а затем заняться его наполнением: указать автора (setAuthor), копирайт (setCopyright), заголовок (setTitle), описание (setDescription), дату публикации (setPublishedDate), при необходимости - ссылку или URI (setLink или setUri, соответственно). Несколько слов о типе ленты, поддерживаются все существующие на данный момент типы RSS- и Atom-лент. Т.к. в Java 1.4 не было перечислимых типов (enum), то данный параметр строковый. Чтобы было проще - создадим enum, в котором перечислим все допустимые значения типов лент:

package name.samolisov.rss.demo;



public enum SyndFeedType

{

    RSS_09("rss_0.9"),

    RSS_091("rss_0.91"),

    RSS_092("rss_0.92"),

    RSS_093("rss_0.93"),

    RSS_O94("rss_0.94"),

    RSS_10("rss_1.0"),

    RSS_20("rss_2.0"),

    ATOM_03("atom_0.3"),

    ATOM_10("atom_1.0");

   

    private String code;

   

    private SyndFeedType(String code)

    {

        this.code = code;

    }

   

    public String getCode()

    {

        return code;

    }

}


После указания параметров ленту следует наполнить, для этого служит метод setEntries, который принимает список записей - объектов типа SyndEntry. В ROME входит реализация SyndEntry по-умолчанию - класс SyndEntryImpl. Для записи так же задаются название, автор, дата публикации, ссылка, категория - объект типа SyndCategory с названием и ссылкой на таксономию категории - и содержание - объект типа SyndContent.

После создания и наполнения объекта типа SyndFeed нужно построить XML-документ в нужном формате. Для этого служит класс SyndFeedOutput, который содержит методы для преобразования фида в строку (outputString), вывода в заданный файл или writer (output), а так же в JDom (outputJDom или W3CDom (outputW3CDom). Наличие стольких методов вывода фида обеспечивает большую гибкость при использовании библиотеки.

Пример генерации RSS-фида с помощью ROME:

package name.samolisov.rss.demo;



import java.util.ArrayList;

import java.util.Arrays;

import java.util.Date;

import java.util.List;



import com.sun.syndication.feed.synd.SyndCategory;

import com.sun.syndication.feed.synd.SyndCategoryImpl;

import com.sun.syndication.feed.synd.SyndContent;

import com.sun.syndication.feed.synd.SyndContentImpl;

import com.sun.syndication.feed.synd.SyndEntry;

import com.sun.syndication.feed.synd.SyndEntryImpl;

import com.sun.syndication.feed.synd.SyndFeed;

import com.sun.syndication.feed.synd.SyndFeedImpl;

import com.sun.syndication.io.FeedException;

import com.sun.syndication.io.SyndFeedOutput;



public class DemoSyndGenerator

{

    public SyndEntry makeEntry(String author, String title, String content, String contentType, String url,

            Date publishedDate, SyndCategory... categories)

    {

        SyndEntry entry = new SyndEntryImpl();

        entry.setAuthor(author);

        entry.setTitle(title);

        entry.setLink(url);

        entry.setUri(url);

        entry.setPublishedDate(publishedDate);

        entry.setCategories(Arrays.asList(categories));



        SyndContent description = new SyndContentImpl();

        description.setType(contentType);

        description.setValue(content);



        entry.setDescription(description);



        return entry;

    }



    public SyndFeed makeFeed(String type, String author, String title, String description, String url, List<SyndEntry> entries)

    {

        SyndFeed feed = new SyndFeedImpl();

        feed.setFeedType(type);

        feed.setAuthor(author);

        feed.setCopyright(author);

        feed.setTitle(title);

        feed.setDescription(description);

        feed.setLink(url);

        feed.setUri(url);

        feed.setEntries(entries);



        return feed;

    }



    public SyndCategory makeCategory(String name, String taxonomyUrl)

    {

        SyndCategory category = new SyndCategoryImpl();

        category.setName(name);

        category.setTaxonomyUri(taxonomyUrl);



        return category;

    }



    public static void main(String[] args) throws FeedException

    {

        DemoSyndGenerator generator = new DemoSyndGenerator();



        SyndCategory category = generator.makeCategory("Programming", "http://en.wikipedia.org/wiki/Computer_programming");



        List<SyndEntry> entries = new ArrayList<SyndEntry>();

        entries.add(generator.makeEntry("Pavel Samolisov", "About hamsters", "A hamsters is the cool thing.",

                "text/html", "http://samolisov.name/page1", new Date(), category));

        entries.add(generator.makeEntry("Pavel Samolisov", "About fish", "A fish is the cool thing.",

                "text/html", "http://samolisov.name/page2", new Date(), category));



        SyndFeed feed = generator.makeFeed(SyndFeedType.RSS_20.getCode(), "Pavel Samolisov", "Pavel Samolisov on the web",

                "About my self", "http://samolisov.name/rss", entries);



        SyndFeedOutput output = new SyndFeedOutput();

        System.out.println(output.outputString(feed));

    }

}

 


Использование ROME совместно с Servlet API


Наличие нескольких методов для вывода фида позволяет легко использовать ROME в рамках сервлет-контейнера: в качестве приемника в методе output можно указать Writer, полученный из HttpServletResponse.

Для использования ROME в сервлет-контейнере, необходимо в каталог WEB-INF/lib поместить библиотеки: com.sun.syndication и org.jdom.

Простой пример сервлета, генерирующего и отдающего фид по запросу пользователя:

package name.samolisov.rss.demo.web;



import java.io.IOException;

import java.util.ArrayList;

import java.util.Date;

import java.util.List;



import javax.servlet.ServletException;

import javax.servlet.http.HttpServlet;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;



import com.sun.syndication.feed.synd.SyndCategory;

import com.sun.syndication.feed.synd.SyndEntry;

import com.sun.syndication.feed.synd.SyndFeed;

import com.sun.syndication.io.SyndFeedOutput;



/**

 * Servlet implementation class RssServlet

 */


public class RssServlet extends HttpServlet

{

    private static final long serialVersionUID = 1L;



    /**

     * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)

     */


    protected void doGet(HttpServletRequest request, HttpServletResponse response)

            throws ServletException, IOException

    {

        DemoSyndGenerator generator = new DemoSyndGenerator();



        SyndCategory category = generator.makeCategory("Programming", "http://en.wikipedia.org/wiki/Computer_programming");



        List<SyndEntry> entries = new ArrayList<SyndEntry>();

        entries.add(generator.makeEntry("Pavel Samolisov", "About hamsters", "A hamsters is the cool thing.",

                "text/html", "http://samolisov.name/page1", new Date(), category));

        entries.add(generator.makeEntry("Pavel Samolisov", "About fish", "A fish is the cool thing.",

                "text/html", "http://samolisov.name/page2", new Date(), category));



        SyndFeed feed = generator.makeFeed(SyndFeedType.RSS_20.getCode(), "Pavel Samolisov", "Pavel Samolisov on the web",

                "About my self", "http://samolisov.name/rss", entries);



        try

        {

            response.setStatus(HttpServletResponse.SC_OK);

            response.setContentType("application/rss+xml");



            SyndFeedOutput output = new SyndFeedOutput();

            output.output(feed, response.getWriter());

        }

        catch (Exception e)

        {

            throw new IOException(e);

        }

    }

}


В качестве Content-Type для RSS-потоков необходимо указывать "application/rss+xml". Это - правильное значение, которое позволит приложениям вроде Яндекс.Лента определить, что им передается RSS-поток и на него можно подписаться. Если в Firefox настроен автоматический редирект RSS-потоков в Яндекс.Ленту, то при переходе по адресу http://localhost:8080/rssdemo/rss, произойдет перенаправление на страницу подписки:



Источник ленты выглядит следующим образом:



Добавление дополнительных данных в поток с помощью механизма модулей


Еще одной интересной особенностью библиотеки является возможность добавлять в потоки (и, соответственно, потом получать при разборе потока) дополнительные данные. Причем, эти данные будут отделены от основного содержимого потока с помощью пространств имен XML. Давайте рассмотрим такое поведение на примере, который не имеет никакого практического смысла, но наглядно демонстрирует добавление дополнительной информации. Создадим модуль, который будет добавлять к фиду и записям (в рамках ROME можно назначать на фид и записи разные модули) комментарий.

Прежде всего необходимо создать интерфейс модуля, который должен наследоваться от интерфейса Module. Интерфейс будет содержать два метода: setComment() и getComment() соответственно:

package name.samolisov.rss.demo;



import com.sun.syndication.feed.module.Module;



public interface CommentModule extends Module

{

    public static final String URI = "http://samolisov.blogspot.com/rome/module/sample";



    public String getComment();



    public void setComment(String comment);

}

 


Теперь необходимо написать его реализацию. Методы setComment/getComment тривиальны - меняют и возвращают значение поля _comment. Модуль должен наследоваться от класса ModuleImpl и в конструкторе вызвать конструктор суперкласса с параметрами: свой класс, свой URI. Данный URI определяет пространство имен, в котором будут располагаться дополнительные, создаваемые модулем данные. Класс ModuleImpl реализует интерфейс CopyFrom, в котором определено два метода: copyFrom(Object obj) - определяет логику клонирования модуля и getInterface - возвращает интерфейс модуля, который можно клонировать с помощью copyFrom.

Реализация класса ModuleImpl:

package name.samolisov.rss.demo;



import com.sun.syndication.feed.module.ModuleImpl;



public class CommentModuleImpl extends ModuleImpl implements CommentModule

{

    private static final long serialVersionUID = 3402049042121907944L;



    private String _comment;



    public CommentModuleImpl()

    {

        super(CommentModule.class, CommentModule.URI);

    }



    @Override

    public void copyFrom(Object obj)

    {

         CommentModule sm = (CommentModule) obj;

         setComment(sm.getComment());

    }



    @Override

    public Class getInterface()

    {

        return CommentModule.class;

    }



    @Override

    public String getComment()

    {

        return _comment;

    }



    @Override

    public void setComment(String comment)

    {

        _comment = comment;

    }

}

 


Теперь для модуля нужно определить генератор - класс, который будет вставлять данные из модуля в RSS/Atom-поток и парсер - класс, который будет извлекать данные, характерные для модуля, из RSS/Atom-потока.

Генератор должен реализовывать интерфейс ModuleGenerator, который содержит три метода:

  • generate(Module module, Element element), непосредственно добавляет данные, определенные в модуле module к элементу element. В качестве элемента может выступать как сам фид, так и каждая его запись.
  • getNamespaceUri(), возвращает URI пространства имен, в котором определены данные, генерируемые модулем.
  • getNamespaces(), возвращает множество пространств имен, которые добавляет модуль к фиду. Данные пространства имен будут добавлены к определению корневого тега пакета.


Код генератора, добавляющего комментарий к фиду и элементам:

package name.samolisov.rss.demo;



import java.util.Collections;

import java.util.HashSet;

import java.util.Set;



import org.jdom.Element;

import org.jdom.Namespace;



import com.sun.syndication.feed.module.Module;

import com.sun.syndication.io.ModuleGenerator;



public class CommentModuleGenerator implements ModuleGenerator

{

    private static final Namespace COMMENT_NS  = Namespace.getNamespace("sample", CommentModule.URI);



    private static final Set<Namespace> NAMESPACES;



    static

    {

        Set<Namespace> nss = new HashSet<Namespace>();

        nss.add(COMMENT_NS);

        NAMESPACES = Collections.unmodifiableSet(nss);

    }



    @Override

    public void generate(Module module, Element element)

    {

        // this is not necessary, it is done to avoid the namespace

        // definition in every item.

        Element root = element;

        while (root.getParent()!=null && root.getParent() instanceof Element)

        {

            root = (Element) element.getParent();

        }

        root.addNamespaceDeclaration(COMMENT_NS);



        CommentModule tm = (CommentModule) module;

        if (tm.getComment() != null)

            element.addContent(makeCommentElement("comment", tm.getComment()));

    }



    protected Element makeCommentElement(String name, String value)

    {

        Element element = new Element(name, COMMENT_NS);

        element.addContent(value);

        return element;

    }



    @Override

    public String getNamespaceUri()

    {

        return CommentModule.URI;

    }



    @Override

    public Set<Namespace> getNamespaces()

    {

        return NAMESPACES;

    }

}

 


Парсер должен реализовывать интерфейс ModuleParser. В данном интерфейсе определены два метода:


  • getNamespaceUri(), возвращает URI пространства имен, в котором определены данные, генерируемые модулем.
  • Module parse(Element root), разбирает содержимое XML-структуры, передаваемой как элемент root (в качестве элемента root может выступать как корневой тег фида, так и тег, определяющий каждую запись) и возвращает созданный из данных, содержащихся в этой структуре, модуль. Если данные, характерные для модуля, не найдены, то необходимо вернуть null.


Код парсера, разбирающего теги, содержащие комментарий к фиду или записям:

package name.samolisov.rss.demo;



import org.jdom.Element;

import org.jdom.Namespace;



import com.sun.syndication.feed.module.Module;

import com.sun.syndication.io.ModuleParser;



public class CommentModuleParser implements ModuleParser

{

    private static final Namespace COMMENT_NS  = Namespace.getNamespace("sample", CommentModule.URI);



    @Override

    public String getNamespaceUri()

    {

        return CommentModule.URI;

    }



    @Override

    public Module parse(Element root)

    {

        boolean foundSomeThing = false;



        CommentModule module = new CommentModuleImpl();



        Element e = root.getChild("comment", COMMENT_NS);

        if (e != null)

        {

            foundSomeThing = true;

            module.setComment(e.getText());

        }



        return foundSomeThing ? module : null;

    }

}

 


После того, как код парсера и генератора написан, необходимо как-то дать знать ROME, о том, что его можно использовать при обработке лент заданного формата. Для этого в корне class-path должен присутствовать файл rome.properties. В данном файле задаются парсеры и генераторы отдельно для всего фида и его элементов и для потоков разного формата. Например, файл, подключающий генератор CommentModuleGenerator и парсер CommentModuleParser к фиду и элементом ленты в формате RSS 2.0 будет выглядеть так:

# Parsers for RSS 2.0 feed modules

rss_2.0.feed.ModuleParser.classes=name.samolisov.rss.demo.CommentModuleParser



# Parsers for RSS 2.0 item modules

rss_2.0.item.ModuleParser.classes=name.samolisov.rss.demo.CommentModuleParser



# Generators for RSS 2.0 feed modules

rss_2.0.feed.ModuleGenerator.classes=name.samolisov.rss.demo.CommentModuleGenerator



# Generators for RSS 2.0 item modules

rss_2.0.item.ModuleGenerator.classes=name.samolisov.rss.demo.CommentModuleGenerator


Не стоит забывать, что помимо подключения к ROME, при генерации ленты нужно задавать данные для конкретного модуля. Для этого создается экземпляр модуля (например, класса CommentModuleImpl), заполняется данными и добавляется в список модулей SyndFeed или SyndEntry:

CommentModule titleModule = new CommentModuleImpl();

titleModule.setComment("My small comment for this feed");

feed.getModules().add(titleModule);


При разборе ленты получить модуль можно по его URI с помощью метода getModule(String URI), а затем извлечь из него данные:

System.out.println("Comment: " + ((CommentModule) feed.getModule(CommentModule.URI)).getComment());


Заключение


В данной статье мы познакомились с библиотекой ROME, предназначенной для разбора и генерации RSS/Atom потоков. Рассмотрели как использовать библиотеку для разбора и генерации ленты, особенности работы с ROME при создании сервлетов, а так же разобрались с тем, как расширять гененрируемые ленты с помощью механизма модулей. Этой информации достаточно для большинства вариантов использования библиотеки. Интересующиеся могут прочитать про дополнительные механизмы расширения ROME по адресу.

Немножко личных впечатлений. На базе ROME мне удалось довольно быстро написать бандл к Eclipse Communication Framework, который предоставляет десериализатор для работы с REST-серверами, возвращающими ответ в форматах RSS/Atom. Впечатления только положительные. Когда бандл включат в состав ECF (наверное это будет уже после выхода Eclipse Helios и ECF 3.3 вместе с ним), то о нем будет отдельная статья.

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

Понравилось сообщение - подпишитесь на блог или читайте меня в twitter

4 комментария:

Andrey комментирует...

Весьма полезно, спасибо

Данил Ходырев комментирует...

Благодарю за качественную статью. Понадобилось для генерации RSS для yandex news (реализация yandex:full-text).

armdev комментирует...

как можно читать entres custom tags? Например в entries у меня есть ZipCode, city, как читать их?
thanks

Pavel Samolisov комментирует...

Здравствуйте.
Об этом написано в статье, начиная со слов "Парсер должен реализовывать интерфейс ModuleParser. В данном интерфейсе определены два метода:". Вам нужно написать класс, реализующий интерфейс ModuleParser.

Отправить комментарий

Любой Ваш комментарий важен для меня, однако, помните, что действует предмодерация. Давайте уважать друг друга!