Статьи

Создание простого RSS-клиента в Pivot

Несколько месяцев назад эта статья Эндрю Трайса вдохновила меня на написание демонстрационного приложения, чтобы увидеть, насколько хорошо Pivot будет обрабатывать очень большие наборы табличных данных до миллиона строк (результаты приведены здесь ). Мистер Трис снова вдохновил меня, на этот раз, чтобы увидеть, как легко можно написать приложение Pivot, которое использует RSS-канал и представляет данные в виде списка с помощью специального средства визуализации элементов. Эта статья описывает результаты.

Демо-приложение

На следующем рисунке показан снимок экрана демонстрационного приложения, которое получает данные RSS-канала из Javalobby. Живой пример доступен здесь (для этого требуется Java 6 Update 10 или более поздняя версия; см. Примечания внизу страницы). Приложение содержит один компонент ListView в ScrollPane, который используется для представления данных новостей. Настраиваемое средство отображения элементов списка используется для отображения заголовка, категории и отправителя элемента.

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

<Border styles="{padding:0, color:10}"
xmlns:wtkx="http://incubator.apache.org/pivot/wtkx/1.1"
xmlns:effects="pivot.wtk.effects"
xmlns:rss="pivot.demos.rss"
xmlns="pivot.wtk">
<content>
<CardPane wtkx:id="cardPane" selectedIndex="0">
<Label wtkx:id="statusLabel" text="Loading..."
styles="{horizontalAlignment:'center', verticalAlignment:'center'}"/>
<ScrollPane horizontalScrollBarPolicy="fill">

<view>
<ListView wtkx:id="feedListView"/>
</view>
</ScrollPane>
</CardPane>
</content>

</Border>

Это довольно просто: корневой компонент Border содержит CardPane с двумя вложенными панелями. Первый — это метка состояния, которая используется для предоставления обратной связи пользователю во время загрузки канала. Вторым является ScrollPane, содержащий фактическое представление списка, используемое для представления данных канала.

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

Основной класс приложения

Конструктор приложения показан ниже:

public RSSFeedDemo() {
// Create an XPath instance
xpath = XPathFactory.newInstance().newXPath();

// Set the namespace resolver
xpath.setNamespaceContext(new NamespaceContext() {
public String getNamespaceURI(String prefix) {
String namespaceURI;
if (prefix.equals("dz")) {
namespaceURI = "http://www.developerzone.com/modules/dz/1.0";
} else {
namespaceURI = XMLConstants.NULL_NS_URI;
}

return namespaceURI;
}

public String getPrefix(String uri) {
throw new UnsupportedOperationException();
}

public Iterator getPrefixes(String uri) {
throw new UnsupportedOperationException();
}
});
}

Используя фабричный метод newInstance (), он получает экземпляр javax.xml.xpath.XPath, который позднее используется для получения значений элементов из возвращенных данных. Затем в XPath устанавливается преобразователь пространства имен, что позволяет сопоставить префикс «dz» с URI пространства имен http://www.developerzone.com/modules/dz/1.0.

Метод startup () определяется следующим образом:

public void startup(Display display, Dictionary properties)
throws Exception {
WTKXSerializer wtkxSerializer = new WTKXSerializer();
window = new Window((Component)wtkxSerializer.readObject(getClass().getResource("rss_feed_demo.wtkx")));

feedListView = (ListView)wtkxSerializer.getObjectByName("feedListView");
feedListView.setItemRenderer(new RSSItemRenderer());
feedListView.getComponentMouseButtonListeners().add(new FeedViewMouseButtonHandler());

final CardPane cardPane = (CardPane)wtkxSerializer.getObjectByName("cardPane");
final Label statusLabel = (Label)wtkxSerializer.getObjectByName("statusLabel");

LoadFeedTask loadFeedTask = new LoadFeedTask();
loadFeedTask.execute(new TaskListener() {
public void taskExecuted(Task task) {
feedListView.setListData(new NodeListAdapter(task.getResult()));
cardPane.setSelectedIndex(1);
}

public void executeFailed(Task task) {
statusLabel.setText(task.getFault().toString());
}
});

window.setMaximized(true);
window.open(display);
}

Он загружает пользовательский интерфейс из файла WTKX, устанавливает его как содержимое окна без декораций и получает ссылки на некоторые компоненты, определенные в файле. Он назначает экземпляр RSSItemRenderer в качестве средства визуализации элементов для представления списка и присоединяет экземпляр FeedViewMouseButtonHandler в качестве прослушивателя кнопки мыши в представлении списка (эти классы более подробно обсуждаются ниже). Затем он создает экземпляр LoadFeedTask, выполняет задачу и открывает окно.

LoadFeedTask

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

private class LoadFeedTask extends IOTask<NodeList> {
public NodeList execute() throws TaskExecutionException {
NodeList itemNodeList = null;

DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder documentBuilder;
try {
documentBuilder = documentBuilderFactory.newDocumentBuilder();
} catch(ParserConfigurationException exception) {
throw new TaskExecutionException(exception);
}

Document document;
try {
document = documentBuilder.parse(FEED_URI);
} catch(IOException exception) {
throw new TaskExecutionException(exception);
} catch(SAXException exception) {
throw new TaskExecutionException(exception);
}

try {
itemNodeList = (NodeList)xpath.evaluate("/rss/channel/item",
document, XPathConstants.NODESET);
} catch(XPathExpressionException exception) {
// No-op
}

return itemNodeList;
}
}

Метод execute () загружает XML-документ из URI канала, а затем использует объект XPath для получения списка узлов, содержащего элементы элемента, который он возвращает. После успешного выполнения вызывается метод taskExecuted () прослушивателя задач. Этот метод переносит возвращенный список узлов в экземпляр NodeListAdapter, который используется для адаптации содержимого NodeList для использования ListView (исходный код этого класса доступен здесь ).

Если загрузка не удалась, результирующее исполнение отображается в метке состояния.

RSSItemRenderer

Класс RSSItemRenderer подготавливает данные канала для представления в виде списка. Он реализует интерфейс ListView.ItemRenderer и расширяет вертикальную FlowPane, которую он использует для упорядочивания экземпляров Label, содержащих заголовок, категории и отправителя каждой статьи (обратите внимание, что, хотя эта реализация создает макет панели потока программным способом, она также могла быть определена в WTKX):

private class RSSItemRenderer extends FlowPane implements ListView.ItemRenderer {
private Label titleLabel = new Label();
private Label categoriesHeadingLabel = new Label("subject:");
private Label categoriesLabel = new Label();
private Label submitterHeadingLabel = new Label("submitter:");
private Label submitterLabel = new Label();

public RSSItemRenderer() {
super(Orientation.VERTICAL);

getStyles().put("padding", new Insets(2, 2, 8, 2));

add(titleLabel);

FlowPane categoriesFlowPane = new FlowPane();
add(categoriesFlowPane);

categoriesFlowPane.add(categoriesHeadingLabel);
categoriesFlowPane.add(categoriesLabel);

FlowPane submitterFlowPane = new FlowPane();
add(submitterFlowPane);

submitterFlowPane.add(submitterHeadingLabel);
submitterFlowPane.add(submitterLabel);
}

...

Метод render получает стили шрифта и цвета из компонента и применяет их к своим внутренним меткам. Затем он устанавливает содержимое меток, извлекая содержимое элемента из текущего узла.

    ...

public void render(Object item, ListView listView, boolean selected,
boolean checked, boolean highlighted, boolean disabled) {
// Render styles
Font labelFont = (Font)listView.getStyles().get("font");
Font largeFont = labelFont.deriveFont(Font.BOLD, 14);
titleLabel.getStyles().put("font", largeFont);
categoriesLabel.getStyles().put("font", labelFont);
submitterLabel.getStyles().put("font", labelFont);

Color color = null;
if (listView.isEnabled() && !disabled) {
if (selected) {
if (listView.isFocused()) {
color = (Color)listView.getStyles().get("selectionColor");
} else {
color = (Color)listView.getStyles().get("inactiveSelectionColor");
}
} else {
color = (Color)listView.getStyles().get("color");
}
} else {
color = (Color)listView.getStyles().get("disabledColor");
}

if (color instanceof Color) {
titleLabel.getStyles().put("color", color);
categoriesHeadingLabel.getStyles().put("color", color);
categoriesLabel.getStyles().put("color", color);
submitterHeadingLabel.getStyles().put("color", color);
submitterLabel.getStyles().put("color", color);
}

// Render data
if (item != null) {
Element itemElement = (Element)item;

try {
String title = (String)xpath.evaluate("title", itemElement, XPathConstants.STRING);
titleLabel.setText(title);

String categories = "";
NodeList categoryNodeList = (NodeList)xpath.evaluate("category", itemElement,
XPathConstants.NODESET);
for (int j = 0; j < categoryNodeList.getLength(); j++) {
Element categoryElement = (Element)categoryNodeList.item(j);
String category = categoryElement.getTextContent();
if (j > 0) {
categories += ", ";
}

categories += category;
}

categoriesLabel.setText(categories);

String submitter = (String)xpath.evaluate("dz:submitter/dz:username", itemElement,
XPathConstants.STRING);
submitterLabel.setText(submitter);
} catch(XPathExpressionException exception) {
System.err.println(exception);
}
}
}
}

FeedViewMouseButtonHandler

Класс FeedViewMouseButtonHandler обрабатывает двойной щелчок в представлении списка. При щелчке по представлению списка сохраняется индекс, по которому щелкнули (в случае, если пользователь перемещает мышь до двойного щелчка), а выбранный элемент открывается по двойному щелчку:

private class FeedViewMouseButtonHandler extends ComponentMouseButtonListener.Adapter {
private int index = -1;

@Override
public boolean mouseClick(Component component, Mouse.Button button, int x, int y, int count) {
if (count == 1) {
index = feedListView.getItemAt(y);
} else if (count == 2
&& feedListView.getItemAt(y) == index) {
Element itemElement = (Element)feedListView.getListData().get(index);

try {
String link = (String)xpath.evaluate("link", itemElement, XPathConstants.STRING);
BrowserApplicationContext.open(new URL(link));
} catch(XPathExpressionException exception) {
System.err.print(exception);
} catch(MalformedURLException exception) {
System.err.print(exception);
}
}

return false;
}
};

Резюме

Итак, создать RSS-ридер в Pivot было довольно просто. API-интерфейсы XPath, включенные в платформу Java, очень эффективны для извлечения и анализа данных канала, а Pivot позволяет легко обернуть возвращаемые данные XML в модель списка и настроить их представление в виде списка.

Однако один важный урок заключается в том, что методы фабрики XML в Java не очень хорошо работают с апплетами. Они полагаются на архитектуру JAR-провайдера и будут неоднократно искать файлы дескриптора сервиса в пути к классу апплета, что приводит к множеству ненужных запросов к веб-серверу. Единственный способ предотвратить это — установить для параметра апплета codebase_lookup значение false:

<param name="codebase_lookup" value="false">

Это работает, но это уродливый обходной путь — безусловно, есть допустимые варианты использования для использования как поиска по базе кода, так и анализа XML в апплете; например, апплет, который анализирует XML, который динамически генерируется из сервлета в пути к классам апплета. Надеемся, что Sun (или Oracle) решит эту проблему в будущем обновлении JVM.

Также обратите внимание, что в этой демонстрации используется новая поддержка Java 6 Update 10 для файлов crossdomain.xml. Эта удобная функция позволяет неподписанному апплету устанавливать сетевое соединение с сервером за пределами его источника при условии, что он был загружен с утвержденного URL-адреса. Смотрите эту статью для получения дополнительной информации.

Полный исходный код для демонстрации доступен здесь . Для получения дополнительной информации об Apache Pivot посетите веб- сайт http://incubator.apache.org/pivot .